import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { randomUUID } from 'node:crypto'; import { AttemptPhase, AttemptStatus, UserRole } from '@prisma/client'; import { prisma } from '../src/lib/prisma.js'; import { AUDIT_ACTIONS } from '../src/modules/admin/auditActions.js'; import { PCQ_QUESTION_IDS } from '../src/modules/assessments/scoring/index.js'; import { isReverseScored } from '../src/modules/assessments/scoring/reverseScoring.js'; import { QUESTION_DIMENSION_MAP } from '../src/modules/assessments/scoring/dimensions.js'; import { assertNoSecrets, authHeader, createTestApp, isDatabaseAvailable, requestJson, } from './helpers.js'; const app = createTestApp(); const createdUserIds: bigint[] = []; async function trackUser(externalId: string | undefined) { if (!externalId) return; const user = await prisma.user.findUnique({ where: { externalId } }); if (user) createdUserIds.push(user.id); } async function register(email: string, password = 'password12') { const result = await requestJson(app, '/auth/register', { method: 'POST', body: JSON.stringify({ email, password }), }); await trackUser((result.body.data?.user as { id?: string } | undefined)?.id); return result; } async function promoteAndLogin(email: string): Promise<{ token: string; userId: bigint }> { const registered = await register(email); assert.equal(registered.status, 201); const externalId = (registered.body.data?.user as { id: string }).id; const user = await prisma.user.findUniqueOrThrow({ where: { externalId } }); await prisma.user.update({ where: { id: user.id }, data: { role: UserRole.admin } }); const login = await requestJson(app, '/auth/login', { method: 'POST', body: JSON.stringify({ email, password: 'password12' }), }); assert.equal(login.status, 200); return { token: login.body.data?.sessionToken as string, userId: user.id, }; } async function createFullAttempt(userId: bigint, phase: AttemptPhase, likert = 4) { const now = new Date(); const answers = PCQ_QUESTION_IDS.map((questionId) => { const reverse = isReverseScored(questionId); const points = reverse ? 7 - likert : likert; return { questionId, rawValue: likert, points, dimension: QUESTION_DIMENSION_MAP[questionId]!, isReverseScored: reverse, }; }); const efficacy = answers.slice(0, 6).reduce((s, a) => s + a.points, 0); const hope = answers.slice(6, 12).reduce((s, a) => s + a.points, 0); const resilience = answers.slice(12, 18).reduce((s, a) => s + a.points, 0); const optimism = answers.slice(18, 24).reduce((s, a) => s + a.points, 0); const total = efficacy + hope + resilience + optimism; return prisma.assessmentAttempt.create({ data: { userId, phase, status: AttemptStatus.scored, submittedAt: now, scoredAt: now, answers: { create: answers }, scores: { create: [ { metricKey: 'efficacy', rawScore: efficacy }, { metricKey: 'hope', rawScore: hope }, { metricKey: 'resilience', rawScore: resilience }, { metricKey: 'optimism', rawScore: optimism }, { metricKey: 'total', rawScore: total }, ], }, }, }); } describe('admin user assessments', () => { let dbAvailable = false; before(async () => { dbAvailable = await isDatabaseAvailable(); }); after(async () => { if (dbAvailable && createdUserIds.length > 0) { await prisma.auditLog.deleteMany({ where: { OR: [ { actorUserId: { in: createdUserIds } }, { targetUserId: { in: createdUserIds } }, ], }, }); await prisma.user.deleteMany({ where: { id: { in: createdUserIds } } }); } }); it('returns 401 without session', async (t) => { if (!dbAvailable) { t.skip('MySQL is not running'); return; } const { status, raw } = await requestJson(app, `/admin/users/${randomUUID()}/assessments`); assert.equal(status, 401); assertNoSecrets(raw); }); it('returns 403 for a normal user', async (t) => { if (!dbAvailable) { t.skip('MySQL is not running'); return; } const registered = await register(`assess-user-${Date.now()}@example.com`); const token = registered.body.data?.sessionToken as string; const id = (registered.body.data?.user as { id: string }).id; const { status, body, raw } = await requestJson(app, `/admin/users/${id}/assessments`, { headers: authHeader(token), }); assert.equal(status, 403); assert.equal(body.error?.code, 'FORBIDDEN'); assertNoSecrets(raw); }); it('returns 422 for invalid externalId', async (t) => { if (!dbAvailable) { t.skip('MySQL is not running'); return; } const { token } = await promoteAndLogin(`assess-invalid-${Date.now()}@example.com`); const { status, body, raw } = await requestJson(app, '/admin/users/not-a-uuid/assessments', { headers: authHeader(token), }); assert.equal(status, 422); assert.equal(body.error?.code, 'VALIDATION_ERROR'); assertNoSecrets(raw); }); it('returns 404 for missing user', async (t) => { if (!dbAvailable) { t.skip('MySQL is not running'); return; } const { token } = await promoteAndLogin(`assess-missing-${Date.now()}@example.com`); const { status, body, raw } = await requestJson( app, `/admin/users/${randomUUID()}/assessments`, { headers: authHeader(token) }, ); assert.equal(status, 404); assert.equal(body.error?.code, 'NOT_FOUND'); assertNoSecrets(raw); }); it('returns empty phases when user has no attempts', async (t) => { if (!dbAvailable) { t.skip('MySQL is not running'); return; } const stamp = Date.now(); const target = await register(`assess-empty-${stamp}@example.com`); const targetId = (target.body.data?.user as { id: string }).id; const { token } = await promoteAndLogin(`assess-empty-admin-${stamp}@example.com`); const { status, body, raw } = await requestJson(app, `/admin/users/${targetId}/assessments`, { headers: authHeader(token), }); assert.equal(status, 200); assertNoSecrets(raw); const data = body.data as { pretest: { exists: boolean; answers: unknown[] }; posttest: { exists: boolean; answers: unknown[] }; }; assert.equal(data.pretest.exists, false); assert.equal(data.posttest.exists, false); assert.equal(data.pretest.answers.length, 0); assert.equal(data.posttest.answers.length, 0); }); it('returns pretest only, ordered answers, stored scores, reverse flags, and audit', async (t) => { if (!dbAvailable) { t.skip('MySQL is not running'); return; } const stamp = Date.now(); const targetReg = await register(`assess-pre-${stamp}@example.com`); const targetId = (targetReg.body.data?.user as { id: string }).id; const target = await prisma.user.findUniqueOrThrow({ where: { externalId: targetId } }); await createFullAttempt(target.id, AttemptPhase.pre, 4); const { token, userId: adminId } = await promoteAndLogin( `assess-pre-admin-${stamp}@example.com`, ); const { status, body, raw } = await requestJson(app, `/admin/users/${targetId}/assessments`, { headers: authHeader(token), }); assert.equal(status, 200); assertNoSecrets(raw); const data = body.data as { user: { id: string; email: string | null }; pretest: { exists: boolean; scores: Record; answers: Array<{ questionId: string; questionNumber: number; isReverseScored: boolean; points: number; rawValue: number; }>; answerCount: number; }; posttest: { exists: boolean }; }; assert.equal(data.user.id, targetId); assert.equal(data.pretest.exists, true); assert.equal(data.posttest.exists, false); assert.equal(data.pretest.answerCount, 24); assert.equal(data.pretest.answers.length, 24); assert.equal(data.pretest.answers[0]?.questionId, 'pcq_01'); assert.equal(data.pretest.answers[0]?.questionNumber, 1); assert.equal(data.pretest.answers[23]?.questionId, 'pcq_24'); assert.equal(data.pretest.answers[23]?.questionNumber, 24); const reverseIds = data.pretest.answers .filter((a) => a.isReverseScored) .map((a) => a.questionId); assert.deepEqual(reverseIds, ['pcq_13', 'pcq_20', 'pcq_23']); const reverseAnswer = data.pretest.answers.find((a) => a.questionId === 'pcq_13'); assert.equal(reverseAnswer?.rawValue, 4); assert.equal(reverseAnswer?.points, 3); assert.equal(data.pretest.scores.total, 93); assert.equal(data.pretest.scores.efficacy, 24); assert.equal(typeof data.pretest.scores.hope, 'number'); assert.equal('passwordHash' in data, false); assert.equal('messages' in data, false); assert.equal('assignments' in data, false); const audit = await prisma.auditLog.findFirst({ where: { actorUserId: adminId, targetUserId: target.id, action: AUDIT_ACTIONS.ADMIN_VIEW_ASSESSMENT, }, orderBy: { createdAt: 'desc' }, }); assert.ok(audit); const meta = JSON.stringify(audit.metadata ?? {}); assert.match(meta, /"view":"assessment"/); assert.equal(/passwordHash|tokenHash|rawValue|pcq_01/i.test(meta), false); }); it('separates pre and post attempts', async (t) => { if (!dbAvailable) { t.skip('MySQL is not running'); return; } const stamp = Date.now(); const targetReg = await register(`assess-both-${stamp}@example.com`); const targetId = (targetReg.body.data?.user as { id: string }).id; const target = await prisma.user.findUniqueOrThrow({ where: { externalId: targetId } }); await createFullAttempt(target.id, AttemptPhase.pre, 3); await createFullAttempt(target.id, AttemptPhase.post, 5); const { token } = await promoteAndLogin(`assess-both-admin-${stamp}@example.com`); const { status, body, raw } = await requestJson(app, `/admin/users/${targetId}/assessments`, { headers: authHeader(token), }); assert.equal(status, 200); assertNoSecrets(raw); const data = body.data as { pretest: { exists: boolean; phase: string; scores: { total: number | null } }; posttest: { exists: boolean; phase: string; scores: { total: number | null } }; }; assert.equal(data.pretest.exists, true); assert.equal(data.posttest.exists, true); assert.equal(data.pretest.phase, 'pre'); assert.equal(data.posttest.phase, 'post'); assert.notEqual(data.pretest.scores.total, data.posttest.scores.total); }); it('handles attempt without scores without inventing values', async (t) => { if (!dbAvailable) { t.skip('MySQL is not running'); return; } const stamp = Date.now(); const targetReg = await register(`assess-noscore-${stamp}@example.com`); const targetId = (targetReg.body.data?.user as { id: string }).id; const target = await prisma.user.findUniqueOrThrow({ where: { externalId: targetId } }); const now = new Date(); await prisma.assessmentAttempt.create({ data: { userId: target.id, phase: AttemptPhase.pre, status: AttemptStatus.submitted, submittedAt: now, scoredAt: now, answers: { create: [ { questionId: 'pcq_01', rawValue: 2, points: 2, dimension: 'efficacy', isReverseScored: false, }, ], }, }, }); const { token } = await promoteAndLogin(`assess-noscore-admin-${stamp}@example.com`); const { status, body, raw } = await requestJson(app, `/admin/users/${targetId}/assessments`, { headers: authHeader(token), }); assert.equal(status, 200); assertNoSecrets(raw); const pretest = (body.data as { pretest: { scores: unknown; answers: unknown[]; status: string } }) .pretest; assert.equal(pretest.scores, null); assert.equal(pretest.answers.length, 1); assert.equal(pretest.status, 'submitted'); }); });