Files
pwa/backend/tests/admin.test.ts
T
ar.badieiandCursor c6243caa35 Add read-only Noya admin panel with assignments and UX polish.
Ship admin auth, user overview, assessments, chats, assignment filters/export, security hardening, and a separate desktop admin shell with clearer assessment answers.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-21 08:00:24 +03:30

241 lines
7.8 KiB
TypeScript

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/);
});
});