import { describe, it, expect, vi, beforeEach } from 'vitest'; import { buildAuthTestApp } from '../helpers/build-test-app.js'; import { createMockDb, selectChain, insertChain, updateChain, deleteChain, } from '../test-utils.js'; vi.mock('../../src/utils/password.js', () => ({ hashPassword: vi.fn().mockResolvedValue('hashed-password'), verifyPassword: vi.fn().mockResolvedValue(true), })); vi.mock('../../src/utils/jwt.js', () => ({ signAccessToken: vi.fn().mockResolvedValue('access-token'), signRefreshToken: vi.fn().mockResolvedValue('refresh-token'), verifyToken: vi.fn(), isRefreshPayload: vi.fn(), hashToken: vi.fn((t: string) => `hash-${t}`), })); vi.mock('node:crypto', () => ({ randomBytes: vi.fn(() => ({ toString: () => 'abc123' })), randomUUID: vi.fn(() => 'uuid-session-1'), })); import { isRefreshPayload, verifyToken } from '../../src/utils/jwt.js'; describe('Auth routes integration', () => { let app: Awaited>; let mockDb: ReturnType; beforeEach(async () => { vi.clearAllMocks(); mockDb = createMockDb(); app = await buildAuthTestApp(mockDb as never); }); describe('POST /auth/register', () => { it('returns 201 with userId and verificationCode when registration succeeds', async () => { (mockDb.select as ReturnType) .mockReturnValueOnce(selectChain([])) .mockReturnValueOnce(selectChain([])); (mockDb.insert as ReturnType) .mockReturnValueOnce(insertChain([{ id: 'user-123' }])) .mockReturnValueOnce(insertChain([])); const res = await app.inject({ method: 'POST', url: '/auth/register', payload: { email: 'test@example.com', password: 'password123', nickname: 'tester', }, }); expect(res.statusCode).toBe(201); const body = JSON.parse(res.body); expect(body.userId).toBe('user-123'); expect(body.message).toContain('verify your email'); expect(body.verificationCode).toBeDefined(); }); it('returns 409 when email is already taken', async () => { (mockDb.select as ReturnType).mockReturnValueOnce( selectChain([{ id: 'existing', email: 'test@example.com' }]) ); const res = await app.inject({ method: 'POST', url: '/auth/register', payload: { email: 'test@example.com', password: 'password123', nickname: 'tester', }, }); expect(res.statusCode).toBe(409); const body = JSON.parse(res.body); expect(body.error?.code).toBe('EMAIL_TAKEN'); }); it('returns 422 when validation fails', async () => { const res = await app.inject({ method: 'POST', url: '/auth/register', payload: { email: 'short', password: '123', // too short nickname: 'x', // too short }, }); expect([400, 422]).toContain(res.statusCode); const body = JSON.parse(res.body); expect(body.error?.code ?? body.error).toBeDefined(); }); }); describe('POST /auth/login', () => { it('returns tokens when credentials are valid', async () => { const mockUser = { id: 'user-1', email: 'test@example.com', passwordHash: 'hashed', }; (mockDb.select as ReturnType).mockReturnValueOnce( selectChain([mockUser]) ); (mockDb.insert as ReturnType).mockReturnValueOnce( insertChain([]) ); const res = await app.inject({ method: 'POST', url: '/auth/login', payload: { email: 'test@example.com', password: 'password123' }, }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body); expect(body.accessToken).toBe('access-token'); expect(body.refreshToken).toBe('refresh-token'); }); it('returns 401 when credentials are invalid', async () => { (mockDb.select as ReturnType).mockReturnValueOnce( selectChain([]) ); const res = await app.inject({ method: 'POST', url: '/auth/login', payload: { email: 'unknown@example.com', password: 'wrong' }, }); expect(res.statusCode).toBe(401); }); }); describe('POST /auth/logout', () => { it('returns 204 on success', async () => { (mockDb.delete as ReturnType).mockReturnValueOnce( deleteChain() ); const res = await app.inject({ method: 'POST', url: '/auth/logout', payload: { refreshToken: 'some-token' }, }); expect(res.statusCode).toBe(204); }); }); describe('POST /auth/refresh', () => { it('returns new tokens when refresh token is valid', async () => { vi.mocked(verifyToken).mockResolvedValueOnce({ sub: 'user-1', sid: 'sid-1', type: 'refresh', } as never); vi.mocked(isRefreshPayload).mockReturnValueOnce(true); (mockDb.select as ReturnType) .mockReturnValueOnce(selectChain([{ id: 'sess-1', userId: 'user-1' }])) .mockReturnValueOnce(selectChain([{ id: 'user-1', email: 'test@example.com' }])); (mockDb.delete as ReturnType).mockReturnValueOnce( deleteChain() ); (mockDb.insert as ReturnType).mockReturnValueOnce( insertChain([]) ); const res = await app.inject({ method: 'POST', url: '/auth/refresh', payload: { refreshToken: 'valid-refresh-token' }, }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body); expect(body.accessToken).toBe('access-token'); expect(body.refreshToken).toBe('refresh-token'); }); }); describe('POST /auth/verify-email', () => { it('returns success when code is valid', async () => { const mockCode = { id: 'code-1', userId: 'user-1', code: 'ABC123', expiresAt: new Date(Date.now() + 60000), }; (mockDb.select as ReturnType) .mockReturnValueOnce(selectChain([mockCode])) .mockReturnValueOnce(selectChain([{ id: 'user-1', emailVerifiedAt: null }])); (mockDb.update as ReturnType).mockReturnValueOnce( updateChain([{ id: 'user-1' }]) ); (mockDb.delete as ReturnType).mockReturnValueOnce( deleteChain() ); const res = await app.inject({ method: 'POST', url: '/auth/verify-email', payload: { userId: 'user-1', code: 'ABC123' }, }); expect(res.statusCode).toBe(200); }); }); describe('POST /auth/forgot-password', () => { it('returns 200 with generic message', async () => { (mockDb.select as ReturnType).mockReturnValueOnce( selectChain([{ id: 'user-1', email: 'test@example.com' }]) ); (mockDb.insert as ReturnType).mockReturnValueOnce( insertChain([]) ); const res = await app.inject({ method: 'POST', url: '/auth/forgot-password', payload: { email: 'test@example.com' }, }); expect(res.statusCode).toBe(200); }); }); describe('POST /auth/reset-password', () => { it('returns 200 when token is valid', async () => { const mockRecord = { id: 'rec-1', userId: 'user-1', expiresAt: new Date(Date.now() + 60000), }; (mockDb.select as ReturnType).mockReturnValueOnce( selectChain([mockRecord]) ); (mockDb.update as ReturnType).mockReturnValueOnce( updateChain([{ id: 'user-1' }]) ); (mockDb.delete as ReturnType).mockReturnValueOnce( deleteChain() ); const res = await app.inject({ method: 'POST', url: '/auth/reset-password', payload: { token: 'valid-token', newPassword: 'newPassword123' }, }); expect(res.statusCode).toBe(200); }); }); });