121 lines
3.2 KiB
JavaScript
121 lines
3.2 KiB
JavaScript
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);
|
|
});
|
|
});
|