import { afterEach, describe, expect, it } from 'vitest'; import { createServer } from '../src/app.js'; import { db } from '../src/db/knex.js'; import { clearOutbox, outbox } from '../src/services/mail.js'; import { truncateTables } from './truncate.js'; const email = 'doctor@example.com'; /** * @param {import('@hapi/hapi').ServerInjectResponse} response * @param {string} name */ function cookieNamed(response, name) { const raw = response.headers['set-cookie']; const headers = Array.isArray(raw) ? raw : raw ? [raw] : []; const match = headers.find((header) => header.startsWith(`${name}=`)); return match ? match.split(';')[0] : null; } describe('user OTP auth', () => { /** @type {import('@hapi/hapi').Server | undefined} */ let server; afterEach(async () => { clearOutbox(); await server?.stop(); await truncateTables(); }); async function start() { server = await createServer(); return server; } it('requests an OTP, verifies it, returns /api/me, and logs out', async () => { const api = await start(); const requested = await api.inject({ method: 'POST', url: '/api/auth/otp/request', payload: { email }, }); expect(requested.statusCode).toBe(200); expect(requested.result).toMatchObject({ ok: true }); const user = await db('users').where({ email }).first(); expect(user).toBeTruthy(); const application = await db('applications').where({ user_id: user.id }).first(); expect(application).toMatchObject({ email, status: 'DRAFT' }); expect(outbox).toHaveLength(1); expect(outbox[0]?.template).toBe('login_otp'); expect(outbox[0]?.otp).toMatch(/^\d{6}$/); const verified = await api.inject({ method: 'POST', url: '/api/auth/otp/verify', payload: { email, otp: outbox[0].otp }, }); expect(verified.statusCode).toBe(200); expect(verified.result).toMatchObject({ user: { email }, me: { nextStep: 'consent' }, }); const session = cookieNamed(verified, 'user_token'); expect(session).toBeTruthy(); const me = await api.inject({ method: 'GET', url: '/api/me', headers: { cookie: session }, }); expect(me.statusCode).toBe(200); expect(me.result).toMatchObject({ user: { email }, steps: { consent: { done: false }, agreement: { done: false }, survey: { done: false }, }, nextStep: 'consent', }); const logout = await api.inject({ method: 'POST', url: '/api/auth/logout', headers: { cookie: session }, }); expect(logout.statusCode).toBe(200); expect(cookieNamed(logout, 'user_token')).toMatch(/user_token=/); expect(String(logout.headers['set-cookie'])).toMatch(/Max-Age=0|Expires=/i); const missing = await api.inject({ method: 'GET', url: '/api/me' }); expect(missing.statusCode).toBe(401); }); it('rejects an invalid OTP', async () => { const api = await start(); await api.inject({ method: 'POST', url: '/api/auth/otp/request', payload: { email }, }); const verified = await api.inject({ method: 'POST', url: '/api/auth/otp/verify', payload: { email, otp: '000000' }, }); expect(verified.statusCode).toBe(401); }); });