PPPAIMT-Crest/backend/test/userAuth.test.js

217 lines
6.9 KiB
JavaScript

import { randomUUID } from 'node:crypto';
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() {
await db('pppaimt_doctors').insert({ email, hcp_name: 'Dr Test' });
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);
expect(await db('audit_log').where({ action: 'USER_LOGOUT' }).first()).toBeTruthy();
});
it('clears the cookie even without a valid session', async () => {
const api = await start();
const logout = await api.inject({ method: 'POST', url: '/api/auth/logout' });
expect(logout.statusCode).toBe(200);
expect(logout.result).toEqual({ ok: true });
expect(String(logout.headers['set-cookie'])).toMatch(/user_token=.*(Max-Age=0|Expires=)/i);
expect(await db('audit_log').where({ action: 'USER_LOGOUT' }).first()).toBeUndefined();
});
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);
});
it('refuses an email that is not in pppaimt_doctors without creating anything', async () => {
const api = await start();
const stranger = 'not-a-doctor@example.com';
const requested = await api.inject({
method: 'POST',
url: '/api/auth/otp/request',
payload: { email: stranger },
});
expect(requested.statusCode).toBe(403);
expect(requested.result.error.message).toBe('Invalid User!');
expect(await db('users').where({ email: stranger }).first()).toBeUndefined();
expect(await db('applications').where({ email: stranger }).first()).toBeUndefined();
expect(await db('login_otps').where({ email: stranger }).first()).toBeUndefined();
expect(outbox).toHaveLength(0);
const verified = await api.inject({
method: 'POST',
url: '/api/auth/otp/verify',
payload: { email: stranger, otp: '123456' },
});
expect(verified.statusCode).toBe(403);
});
it('fills the user and draft application name from HCP NAME without a leading Dr', async () => {
const api = await start();
const doctorEmail = 'tilve@example.com';
await db('pppaimt_doctors').insert({ email: doctorEmail, hcp_name: 'Dr. Sandeep S Tilve' });
await api.inject({ method: 'POST', url: '/api/auth/otp/request', payload: { email: doctorEmail } });
const user = await db('users').where({ email: doctorEmail }).first();
expect(user.name).toBe('Sandeep S Tilve');
const application = await db('applications').where({ user_id: user.id }).first();
expect(application.full_name).toBe('Sandeep S Tilve');
expect(application.account_holder).toBeNull();
});
it('keeps names the doctor already entered', async () => {
const api = await start();
const [userId] = await db('users').insert({ email, name: 'Typed Name', is_active: true });
await db('applications').insert({
public_id: randomUUID(),
email,
status: 'DRAFT',
user_id: userId,
full_name: 'Typed Full Name',
});
await api.inject({ method: 'POST', url: '/api/auth/otp/request', payload: { email } });
expect((await db('users').where({ id: userId }).first()).name).toBe('Typed Name');
expect((await db('applications').where({ user_id: userId }).first()).full_name).toBe(
'Typed Full Name',
);
});
it('does not strip names that only start with the letters Dr', async () => {
const api = await start();
const doctorEmail = 'drew@example.com';
await db('pppaimt_doctors').insert({ email: doctorEmail, hcp_name: 'Drew Mathews' });
await api.inject({ method: 'POST', url: '/api/auth/otp/request', payload: { email: doctorEmail } });
expect((await db('users').where({ email: doctorEmail }).first()).name).toBe('Drew Mathews');
});
it('matches the doctor email regardless of case', async () => {
const api = await start();
const requested = await api.inject({
method: 'POST',
url: '/api/auth/otp/request',
payload: { email: 'Doctor@Example.COM' },
});
expect(requested.statusCode).toBe(200);
expect(outbox).toHaveLength(1);
});
});