import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { AuthProvider, UserRole } from '@prisma/client'; import { prisma } from '../src/lib/prisma.js'; import { AUDIT_ACTIONS } from '../src/modules/admin/auditActions.js'; import { sanitizeAuditMetadata } from '../src/modules/admin/audit.service.js'; import { assertNoSecrets, authHeader, createTestApp, isDatabaseAvailable, requestJson, } from './helpers.js'; const app = createTestApp(); const createdUserIds: bigint[] = []; describe('admin role and requireAdmin', () => { 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 } } }); } }); 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; } it('returns 401 for /admin/ping without a session', async (t) => { if (!dbAvailable) { t.skip('MySQL is not running'); return; } const { status, body, raw } = await requestJson(app, '/admin/ping'); assert.equal(status, 401); assert.equal(body.error?.code, 'UNAUTHORIZED'); assertNoSecrets(raw); }); it('returns 403 for a normal user calling /admin/ping', async (t) => { if (!dbAvailable) { t.skip('MySQL is not running'); return; } const email = `user-${Date.now()}@example.com`; const registered = await register(email); assert.equal(registered.status, 201); const token = registered.body.data?.sessionToken as string; assert.ok(token); const { status, body, raw } = await requestJson(app, '/admin/ping', { headers: authHeader(token), }); assert.equal(status, 403); assert.equal(body.error?.code, 'FORBIDDEN'); assertNoSecrets(raw); const user = await prisma.user.findFirst({ where: { identities: { some: { email } } }, }); assert.ok(user); assert.equal(user.role, UserRole.user); const denied = await prisma.auditLog.findFirst({ where: { actorUserId: user.id, action: AUDIT_ACTIONS.ADMIN_ACCESS_DENIED, }, orderBy: { createdAt: 'desc' }, }); assert.ok(denied); const metaRaw = JSON.stringify(denied.metadata ?? {}); assert.equal(/passwordHash/i.test(metaRaw), false); assert.equal(/tokenHash/i.test(metaRaw), false); assert.equal(/sessionToken/i.test(metaRaw), false); }); it('allows an admin user through requireAdmin', async (t) => { if (!dbAvailable) { t.skip('MySQL is not running'); return; } const email = `admin-${Date.now()}@example.com`; 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.findUnique({ where: { externalId } }); assert.ok(user); 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); assert.equal((login.body.data?.user as { role: string }).role, UserRole.admin); const token = login.body.data?.sessionToken as string; const { status, body, raw } = await requestJson(app, '/admin/ping', { headers: authHeader(token), }); assert.equal(status, 200); assert.equal(body.success, true); assert.equal((body.data as { ok?: boolean; role?: string })?.ok, true); assert.equal((body.data as { role?: string })?.role, UserRole.admin); assertNoSecrets(raw); const loginAudit = await prisma.auditLog.findFirst({ where: { actorUserId: user.id, action: AUDIT_ACTIONS.ADMIN_LOGIN, }, orderBy: { createdAt: 'desc' }, }); assert.ok(loginAudit); }); it('promote script logic is idempotent and does not create missing users', async (t) => { if (!dbAvailable) { t.skip('MySQL is not running'); return; } const missing = await prisma.authIdentity.findFirst({ where: { provider: AuthProvider.password, email: `missing-${Date.now()}@example.com`, }, }); assert.equal(missing, null); const email = `promote-${Date.now()}@example.com`; const registered = await register(email); const externalId = (registered.body.data?.user as { id: string }).id; const user = await prisma.user.findUnique({ where: { externalId } }); assert.ok(user); assert.equal(user.role, UserRole.user); const profileBefore = await prisma.userProfile.findUnique({ where: { userId: user.id } }); const identityBefore = await prisma.authIdentity.findFirst({ where: { userId: user.id, provider: AuthProvider.password }, }); assert.ok(identityBefore?.passwordHash); const sessionsBefore = await prisma.authSession.count({ where: { userId: user.id } }); await prisma.user.update({ where: { id: user.id }, data: { role: UserRole.admin }, }); await prisma.user.update({ where: { id: user.id }, data: { role: UserRole.admin }, }); const after = await prisma.user.findUnique({ where: { id: user.id }, include: { identities: true, profile: true }, }); assert.ok(after); assert.equal(after.role, UserRole.admin); assert.equal(after.profile?.name ?? null, profileBefore?.name ?? null); assert.equal( after.identities.find((i) => i.provider === AuthProvider.password)?.passwordHash, identityBefore.passwordHash, ); const sessionsAfter = await prisma.authSession.count({ where: { userId: user.id } }); assert.equal(sessionsAfter, sessionsBefore); }); it('sanitizeAuditMetadata drops secrets and chat-like payloads', () => { const cleaned = sanitizeAuditMetadata({ reason: 'insufficient_role', passwordHash: '$argon2id$v=19$m=19456,t=2,p=1$secret', tokenHash: 'abc123', sessionToken: 'raw-token-value', content: 'full chat message should not be stored', answers: [{ questionId: 'pcq_01', rawValue: 5 }], messages: [{ role: 'user', content: 'hello' }], draftData: { secretField: 'must-not-persist' }, reportText: 'private homework report', completionNote: 'private note', responseData: { nested: true }, rawValue: 5, systemMessage: 'hidden system prompt', prompt: 'provider prompt', email: 'admin@example.com', alreadyAdmin: true, }); const raw = JSON.stringify(cleaned); assert.equal(/passwordHash/i.test(raw), false); assert.equal(/tokenHash/i.test(raw), false); assert.equal(/sessionToken/i.test(raw), false); assert.equal(/full chat message/i.test(raw), false); assert.equal(/pcq_01/i.test(raw), false); assert.equal(/must-not-persist|private homework|private note|hidden system|provider prompt/i.test(raw), false); assert.equal(/draftData|reportText|completionNote|responseData|rawValue|systemMessage/i.test(raw), false); assert.match(raw, /insufficient_role/); assert.match(raw, /admin@example.com/); }); });