e6b50fb4f1
Fix all build/type/lint errors (zod 3.25 compat wrapper, Svelte 5 fixes), write 115 unit tests across 10 test files, expand seed script with demo data, update Docker config with migration on startup.
115 lines
3.1 KiB
TypeScript
115 lines
3.1 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
// Mock prisma before importing authService
|
|
vi.mock('../../prisma.js', () => ({
|
|
prisma: {
|
|
user: {
|
|
update: vi.fn(),
|
|
findUnique: vi.fn()
|
|
}
|
|
}
|
|
}));
|
|
|
|
// Set JWT_SECRET for tests
|
|
process.env.JWT_SECRET = 'test-secret-key-for-unit-tests';
|
|
|
|
import {
|
|
hashPassword,
|
|
verifyPassword,
|
|
signAccessToken,
|
|
verifyAccessToken,
|
|
generateRefreshToken,
|
|
getRefreshTokenExpiry,
|
|
rotateTokens
|
|
} from '../authService.js';
|
|
import { prisma } from '../../prisma.js';
|
|
|
|
describe('authService', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('hashPassword / verifyPassword', () => {
|
|
it('hashes a password and verifies it correctly', async () => {
|
|
const password = 'mySecurePassword123';
|
|
const hash = await hashPassword(password);
|
|
|
|
expect(hash).not.toBe(password);
|
|
expect(hash.length).toBeGreaterThan(0);
|
|
|
|
const isValid = await verifyPassword(password, hash);
|
|
expect(isValid).toBe(true);
|
|
});
|
|
|
|
it('rejects wrong password', async () => {
|
|
const hash = await hashPassword('correct-password');
|
|
const isValid = await verifyPassword('wrong-password', hash);
|
|
expect(isValid).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('signAccessToken / verifyAccessToken', () => {
|
|
it('signs and verifies a token', () => {
|
|
const payload = { userId: 'usr-1', email: 'test@test.com', role: 'user' };
|
|
const token = signAccessToken(payload);
|
|
|
|
expect(typeof token).toBe('string');
|
|
expect(token.split('.')).toHaveLength(3);
|
|
|
|
const decoded = verifyAccessToken(token);
|
|
expect(decoded.userId).toBe('usr-1');
|
|
expect(decoded.email).toBe('test@test.com');
|
|
expect(decoded.role).toBe('user');
|
|
});
|
|
|
|
it('throws for invalid token', () => {
|
|
expect(() => verifyAccessToken('invalid.token.value')).toThrow(
|
|
'Invalid or expired access token'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('generateRefreshToken', () => {
|
|
it('generates a hex string', () => {
|
|
const token = generateRefreshToken();
|
|
expect(typeof token).toBe('string');
|
|
expect(token.length).toBe(96); // 48 bytes * 2 hex chars
|
|
expect(/^[0-9a-f]+$/.test(token)).toBe(true);
|
|
});
|
|
|
|
it('generates unique tokens', () => {
|
|
const token1 = generateRefreshToken();
|
|
const token2 = generateRefreshToken();
|
|
expect(token1).not.toBe(token2);
|
|
});
|
|
});
|
|
|
|
describe('getRefreshTokenExpiry', () => {
|
|
it('returns a future date', () => {
|
|
const expiry = getRefreshTokenExpiry();
|
|
expect(expiry.getTime()).toBeGreaterThan(Date.now());
|
|
});
|
|
|
|
it('defaults to 7 days from now', () => {
|
|
const expiry = getRefreshTokenExpiry();
|
|
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
|
|
const diff = expiry.getTime() - Date.now();
|
|
// Allow 10 seconds tolerance
|
|
expect(diff).toBeGreaterThan(sevenDaysMs - 10000);
|
|
expect(diff).toBeLessThan(sevenDaysMs + 10000);
|
|
});
|
|
});
|
|
|
|
describe('rotateTokens', () => {
|
|
it('generates new token pair and saves refresh token', async () => {
|
|
vi.mocked(prisma.user.update).mockResolvedValue({} as never);
|
|
|
|
const result = await rotateTokens('usr-1', 'test@test.com', 'user');
|
|
|
|
expect(result.accessToken).toBeTruthy();
|
|
expect(result.refreshToken).toBeTruthy();
|
|
expect(prisma.user.update).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
});
|