/** * Auth API Tests – Register / Login / Logout / Me / Password Change */ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import type { FastifyInstance } from 'fastify'; import { SqliteAdapter } from '../src/database/SqliteAdapter.js'; import { createServer } from '../src/server.js'; describe('Auth API', () => { let app: FastifyInstance; let db: SqliteAdapter; let authToken: string | null = null; beforeAll(async () => { db = new SqliteAdapter(':memory:'); await db.init(); app = await createServer({ db, port: 0 }); await app.ready(); }); afterAll(async () => { await app.close(); db.close(); }); // ─── Register ──────────────────────────────────────── describe('POST /api/auth/register', () => { it('should register a new user with 201', async () => { const response = await app.inject({ method: 'POST', url: '/api/auth/register', payload: { email: 'testuser@example.com', password: 'TestPass123!', name: 'Test User', role: 'planer', }, }); expect(response.statusCode).toBe(201); const body = JSON.parse(response.body); expect(body.user).toBeDefined(); expect(body.user.email).toBe('testuser@example.com'); expect(body.user.name).toBe('Test User'); expect(body.user.password_hash).toBeUndefined(); expect(body.session).toBeDefined(); expect(body.session.token).toBeDefined(); authToken = body.session.token; }); it('should reject duplicate registration with 409', async () => { const response = await app.inject({ method: 'POST', url: '/api/auth/register', payload: { email: 'testuser@example.com', password: 'TestPass123!', name: 'Test User 2', }, }); expect(response.statusCode).toBe(409); const body = JSON.parse(response.body); expect(body.error).toContain('already registered'); }); it('should reject missing fields with 400', async () => { const response = await app.inject({ method: 'POST', url: '/api/auth/register', payload: { email: 'incomplete@example.com' }, }); expect(response.statusCode).toBe(400); }); }); // ─── Login ────────────────────────────────────────── describe('POST /api/auth/login', () => { it('should login with correct credentials', async () => { const response = await app.inject({ method: 'POST', url: '/api/auth/login', payload: { email: 'testuser@example.com', password: 'TestPass123!', }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.user).toBeDefined(); expect(body.session.token).toBeDefined(); authToken = body.session.token; }); it('should reject wrong password with 401', async () => { const response = await app.inject({ method: 'POST', url: '/api/auth/login', payload: { email: 'testuser@example.com', password: 'WrongPassword!', }, }); expect(response.statusCode).toBe(401); }); it('should reject non-existent email with 401', async () => { const response = await app.inject({ method: 'POST', url: '/api/auth/login', payload: { email: 'nobody@example.com', password: 'SomePassword!', }, }); expect(response.statusCode).toBe(401); }); it('should reject missing fields with 400', async () => { const response = await app.inject({ method: 'POST', url: '/api/auth/login', payload: { email: 'testuser@example.com' }, }); expect(response.statusCode).toBe(400); }); }); // ─── Me ───────────────────────────────────────────── describe('GET /api/auth/me', () => { it('should return current user profile with valid token', async () => { const response = await app.inject({ method: 'GET', url: '/api/auth/me', headers: { authorization: `Bearer ${authToken}` }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.email).toBe('testuser@example.com'); expect(body.password_hash).toBeUndefined(); }); it('should reject without token with 401', async () => { const response = await app.inject({ method: 'GET', url: '/api/auth/me', }); expect(response.statusCode).toBe(401); }); it('should reject invalid token with 401', async () => { const response = await app.inject({ method: 'GET', url: '/api/auth/me', headers: { authorization: 'Bearer invalid-token-xxx' }, }); expect(response.statusCode).toBe(401); }); }); // ─── Password Change ─────────────────────────────── describe('PATCH /api/auth/password', () => { it('should change password with correct old password', async () => { const response = await app.inject({ method: 'PATCH', url: '/api/auth/password', headers: { authorization: `Bearer ${authToken}` }, payload: { oldPassword: 'TestPass123!', newPassword: 'NewPassword456!', }, }); expect(response.statusCode).toBe(204); }); it('should allow login with new password', async () => { const response = await app.inject({ method: 'POST', url: '/api/auth/login', payload: { email: 'testuser@example.com', password: 'NewPassword456!', }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.session.token).toBeDefined(); authToken = body.session.token; }); it('should reject old password after change', async () => { const response = await app.inject({ method: 'POST', url: '/api/auth/login', payload: { email: 'testuser@example.com', password: 'TestPass123!', }, }); expect(response.statusCode).toBe(401); }); it('should reject password change with wrong old password', async () => { const response = await app.inject({ method: 'PATCH', url: '/api/auth/password', headers: { authorization: `Bearer ${authToken}` }, payload: { oldPassword: 'WrongOldPassword!', newPassword: 'AnotherNew789!', }, }); expect(response.statusCode).toBe(400); }); it('should reject password change without auth', async () => { const response = await app.inject({ method: 'PATCH', url: '/api/auth/password', payload: { oldPassword: 'NewPassword456!', newPassword: 'AnotherNew789!', }, }); expect(response.statusCode).toBe(401); }); it('should reject missing password fields', async () => { const response = await app.inject({ method: 'PATCH', url: '/api/auth/password', headers: { authorization: `Bearer ${authToken}` }, payload: { oldPassword: 'NewPassword456!' }, }); expect(response.statusCode).toBe(400); }); }); // ─── Logout ───────────────────────────────────────── describe('POST /api/auth/logout', () => { it('should logout and invalidate token', async () => { const response = await app.inject({ method: 'POST', url: '/api/auth/logout', headers: { authorization: `Bearer ${authToken}` }, }); expect(response.statusCode).toBe(204); // Token should now be invalid const meResponse = await app.inject({ method: 'GET', url: '/api/auth/me', headers: { authorization: `Bearer ${authToken}` }, }); expect(meResponse.statusCode).toBe(401); }); it('should return 204 even without token', async () => { const response = await app.inject({ method: 'POST', url: '/api/auth/logout', }); expect(response.statusCode).toBe(204); }); }); });