import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { randomUUID } from 'node:crypto'; import { AssignmentType, UserAssignmentStatus, UserRole } from '@prisma/client'; import { prisma } from '../src/lib/prisma.js'; import { assertNoSecrets, authHeader, createTestApp, isDatabaseAvailable, requestJson, } from './helpers.js'; const app = createTestApp(); const createdUserIds: bigint[] = []; const createdDefinitionIds: 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 ensureCourseSession(order: number, title: string) { const existing = await prisma.courseSession.findUnique({ where: { order } }); if (existing) { return prisma.courseSession.update({ where: { order }, data: { title }, }); } return prisma.courseSession.create({ data: { sessionKey: `admin-filter-test-${order}-${Date.now()}`, order, title, description: 'admin filter test', estimatedDurationMinutes: 10, blocks: [], }, }); } async function createDefinition(input: { key: string; sessionOrder: number; title: string; assignmentType: AssignmentType; sessionTitle: string; }) { await ensureCourseSession(input.sessionOrder, input.sessionTitle); const def = await prisma.assignmentDefinition.create({ data: { key: input.key, sessionOrder: input.sessionOrder, title: input.title, description: 'desc', instructions: 'instr', assignmentType: input.assignmentType, expectedDurationDays: 3, displayOrder: input.sessionOrder, }, }); createdDefinitionIds.push(def.id); return def; } describe('admin assignments filter', () => { let dbAvailable = false; before(async () => { dbAvailable = await isDatabaseAvailable(); }); after(async () => { if (!dbAvailable) return; if (createdDefinitionIds.length > 0) { await prisma.userAssignment.deleteMany({ where: { definitionId: { in: createdDefinitionIds } }, }); } if (createdUserIds.length > 0) { await prisma.auditLog.deleteMany({ where: { OR: [ { actorUserId: { in: createdUserIds } }, { targetUserId: { in: createdUserIds } }, ], }, }); await prisma.user.deleteMany({ where: { id: { in: createdUserIds } } }); } if (createdDefinitionIds.length > 0) { await prisma.assignmentDefinition.deleteMany({ where: { id: { in: createdDefinitionIds } }, }); } }); it('rejects invalid filter query values with 422', async (t) => { if (!dbAvailable) { t.skip('MySQL is not running'); return; } const { token } = await promoteAndLogin(`filter-invalid-${Date.now()}@example.com`); const registered = await register(`filter-invalid-target-${Date.now()}@example.com`); const id = (registered.body.data?.user as { id: string }).id; const badStatus = await requestJson( app, `/admin/users/${id}/assignments?status=PENDING`, { headers: authHeader(token) }, ); assert.equal(badStatus.status, 422); const badType = await requestJson( app, `/admin/users/${id}/assignments?assignmentType=WEEKLY`, { headers: authHeader(token) }, ); assert.equal(badType.status, 422); const badSort = await requestJson( app, `/admin/users/${id}/assignments?sortBy=passwordHash`, { headers: authHeader(token) }, ); assert.equal(badSort.status, 422); const badDir = await requestJson( app, `/admin/users/${id}/assignments?sortDir=sideways`, { headers: authHeader(token) }, ); assert.equal(badDir.status, 422); const longSearch = 'x'.repeat(101); const badSearch = await requestJson( app, `/admin/users/${id}/assignments?search=${longSearch}`, { headers: authHeader(token) }, ); assert.equal(badSearch.status, 422); }); it('filters and searches only the target user assignments', async (t) => { if (!dbAvailable) { t.skip('MySQL is not running'); return; } const stamp = Date.now(); const { token } = await promoteAndLogin(`filter-admin-${stamp}@example.com`); const targetReg = await register(`filter-target-${stamp}@example.com`); const otherReg = await register(`filter-other-${stamp}@example.com`); const targetId = (targetReg.body.data?.user as { id: string }).id; const otherId = (otherReg.body.data?.user as { id: string }).id; const target = await prisma.user.findUniqueOrThrow({ where: { externalId: targetId } }); const other = await prisma.user.findUniqueOrThrow({ where: { externalId: otherId } }); const defJoy = await createDefinition({ key: `filter-joy-${stamp}`, sessionOrder: 2, title: 'تمرین شادی ویژه', assignmentType: AssignmentType.DAILY, sessionTitle: `جلسه شکرگزاری فیلتر-${stamp}`, }); const defMeaning = await createDefinition({ key: `filter-meaning-${stamp}`, sessionOrder: 4, title: 'تمرین معنا', assignmentType: AssignmentType.ONE_TIME, sessionTitle: `جلسه معنایی-${stamp}`, }); const defOther = await createDefinition({ key: `filter-other-${stamp}`, sessionOrder: 3, title: 'تمرین شادی ویژه', assignmentType: AssignmentType.DAILY, sessionTitle: `جلسه شکرگزاری فیلتر-${stamp}`, }); const uaCompleted = await prisma.userAssignment.create({ data: { userId: target.id, definitionId: defJoy.id, status: UserAssignmentStatus.COMPLETED, activatedAt: new Date('2026-05-01T10:00:00.000Z'), completedAt: new Date('2026-05-03T10:00:00.000Z'), }, }); const uaActive = await prisma.userAssignment.create({ data: { userId: target.id, definitionId: defMeaning.id, status: UserAssignmentStatus.ACTIVE, activatedAt: new Date('2026-05-02T10:00:00.000Z'), }, }); await prisma.userAssignment.create({ data: { userId: other.id, definitionId: defOther.id, status: UserAssignmentStatus.COMPLETED, }, }); const beforeCount = await prisma.userAssignment.count({ where: { userId: target.id } }); const noFilter = await requestJson(app, `/admin/users/${targetId}/assignments`, { headers: authHeader(token), }); assert.equal(noFilter.status, 200); const noFilterItems = (noFilter.body.data as { items: Array<{ assignmentId: string }> }) .items; assert.equal(noFilterItems.length, 2); const byStatus = await requestJson( app, `/admin/users/${targetId}/assignments?status=COMPLETED`, { headers: authHeader(token) }, ); assert.equal(byStatus.status, 200); const statusItems = ( byStatus.body.data as { items: Array<{ assignmentId: string; status: string }>; summary: { total: number; completed: number }; } ).items; assert.equal(statusItems.length, 1); assert.equal(statusItems[0]!.assignmentId, uaCompleted.id.toString()); assert.equal(statusItems[0]!.status, UserAssignmentStatus.COMPLETED); assert.equal( (byStatus.body.data as { summary: { total: number } }).summary.total, 1, ); const byType = await requestJson( app, `/admin/users/${targetId}/assignments?assignmentType=ONE_TIME`, { headers: authHeader(token) }, ); assert.equal(byType.status, 200); const typeItems = (byType.body.data as { items: Array<{ assignmentId: string }> }).items; assert.equal(typeItems.length, 1); assert.equal(typeItems[0]!.assignmentId, uaActive.id.toString()); const combined = await requestJson( app, `/admin/users/${targetId}/assignments?status=COMPLETED&assignmentType=DAILY`, { headers: authHeader(token) }, ); assert.equal(combined.status, 200); assert.equal((combined.body.data as { items: unknown[] }).items.length, 1); const searchTitle = await requestJson( app, `/admin/users/${targetId}/assignments?search=${encodeURIComponent('شادی')}`, { headers: authHeader(token) }, ); assert.equal(searchTitle.status, 200); assert.equal((searchTitle.body.data as { items: unknown[] }).items.length, 1); const searchKey = await requestJson( app, `/admin/users/${targetId}/assignments?search=${encodeURIComponent(`filter-meaning-${stamp}`)}`, { headers: authHeader(token) }, ); assert.equal(searchKey.status, 200); assert.equal((searchKey.body.data as { items: unknown[] }).items.length, 1); const searchSession = await requestJson( app, `/admin/users/${targetId}/assignments?search=${encodeURIComponent(`شکرگزاری فیلتر-${stamp}`)}`, { headers: authHeader(token) }, ); assert.equal(searchSession.status, 200); assert.equal((searchSession.body.data as { items: unknown[] }).items.length, 1); const searchMiss = await requestJson( app, `/admin/users/${targetId}/assignments?search=zzzz-not-found`, { headers: authHeader(token) }, ); assert.equal(searchMiss.status, 200); assert.equal((searchMiss.body.data as { items: unknown[] }).items.length, 0); const whitespaceSearch = await requestJson( app, `/admin/users/${targetId}/assignments?search=${encodeURIComponent(' ')}`, { headers: authHeader(token) }, ); assert.equal(whitespaceSearch.status, 200); assert.equal((whitespaceSearch.body.data as { items: unknown[] }).items.length, 2); const sortAsc = await requestJson( app, `/admin/users/${targetId}/assignments?sortBy=sessionOrder&sortDir=asc`, { headers: authHeader(token) }, ); const sortDesc = await requestJson( app, `/admin/users/${targetId}/assignments?sortBy=sessionOrder&sortDir=desc`, { headers: authHeader(token) }, ); const ascOrders = ( sortAsc.body.data as { items: Array<{ sessionOrder: number }> } ).items.map((item) => item.sessionOrder); const descOrders = ( sortDesc.body.data as { items: Array<{ sessionOrder: number }> } ).items.map((item) => item.sessionOrder); assert.deepEqual(ascOrders, [2, 4]); assert.deepEqual(descOrders, [4, 2]); const afterCount = await prisma.userAssignment.count({ where: { userId: target.id } }); assert.equal(afterCount, beforeCount); assertNoSecrets(byStatus.raw); assert.equal(!byStatus.raw.includes(otherId), true); }); it('returns 401/403/404 for filter endpoint', async (t) => { if (!dbAvailable) { t.skip('MySQL is not running'); return; } const unauth = await requestJson(app, `/admin/users/${randomUUID()}/assignments?status=ACTIVE`); assert.equal(unauth.status, 401); const registered = await register(`filter-forbidden-${Date.now()}@example.com`); const token = registered.body.data?.sessionToken as string; const id = (registered.body.data?.user as { id: string }).id; const forbidden = await requestJson(app, `/admin/users/${id}/assignments?status=ACTIVE`, { headers: authHeader(token), }); assert.equal(forbidden.status, 403); const { token: adminToken } = await promoteAndLogin( `filter-404-${Date.now()}@example.com`, ); const missing = await requestJson( app, `/admin/users/${randomUUID()}/assignments?status=ACTIVE`, { headers: authHeader(adminToken) }, ); assert.equal(missing.status, 404); }); });