import { describe, expect, test, vi } from 'vitest'; import type { ClassroomGenerationJob } from '@/lib/server/classroom-job-store'; import { authorizeClassroomJobCreation, authorizeClassroomJobList, authorizeClassroomJobRequest, CLASSROOM_JOB_MANAGE_ANY_CAPABILITY, } from '@/lib/server/authz/classroom-job-access'; import { resolveIdentityEnforcementMode } from '@/lib/server/authz/identity-enforcement'; import type { PrincipalResolution } from '@/lib/server/authz/principal'; const request = new Request('https://server.example/api/generate-classroom/job-1'); function principal( kind: 'user' | 'device' | 'ops' | 'service', principalId: string, options: { userId?: string; capabilities?: string[] } = {}, ): PrincipalResolution { return { state: 'authenticated', principal: { principalId, kind, ...(options.userId ? { userId: options.userId } : {}), capabilities: options.capabilities ?? [], authMethod: kind === 'device' ? 'device-proof' : kind === 'ops' ? 'ops-cookie' : kind === 'service' ? 'service-bearer' : 'session', }, }; } function job( id: string, ownership: { ownerPrincipalId?: string; guestPrincipalId?: string } = {}, ): ClassroomGenerationJob { return { id, ...ownership, status: 'failed', step: 'failed', progress: 100, message: 'Failed', createdAt: '2026-08-16T00:00:00.000Z', updatedAt: '2026-08-16T00:00:00.000Z', inputSummary: { requirementPreview: 'topic', hasPdf: false, pdfTextLength: 0, pdfImageCount: 0, }, input: { requirement: 'topic' }, scenesGenerated: 0, }; } describe('identity enforcement mode', () => { test('requires an explicit required value and otherwise remains shadow-compatible', () => { expect(resolveIdentityEnforcementMode(undefined)).toBe('shadow'); expect(resolveIdentityEnforcementMode('shadow')).toBe('shadow'); expect(resolveIdentityEnforcementMode('unexpected')).toBe('shadow'); expect(resolveIdentityEnforcementMode(' REQUIRED ')).toBe('required'); }); }); describe('classroom job creation ownership', () => { test('binds account and guest subjects without trusting ops/service kinds as owners', async () => { await expect( authorizeClassroomJobCreation(request, { mode: 'required', resolvePrincipal: async () => principal('user', 'account-1'), }), ).resolves.toEqual({ allowed: true, ownership: { ownerPrincipalId: 'account-1' } }); await expect( authorizeClassroomJobCreation(request, { mode: 'required', resolvePrincipal: async () => principal('device', 'device-1'), }), ).resolves.toEqual({ allowed: true, ownership: { guestPrincipalId: 'device-1' } }); await expect( authorizeClassroomJobCreation(request, { mode: 'required', resolvePrincipal: async () => principal('device', 'device-1', { userId: 'account-1' }), }), ).resolves.toEqual({ allowed: true, ownership: { ownerPrincipalId: 'account-1' } }); await expect( authorizeClassroomJobCreation(request, { mode: 'required', resolvePrincipal: async () => principal('ops', 'ops-1'), }), ).resolves.toMatchObject({ allowed: false, status: 403, code: 'FORBIDDEN' }); }); test('fails closed only in required mode when identity is unavailable', async () => { const resolvePrincipal = async () => ({ state: 'unavailable', reason: 'resolver_not_configured' }) as const; await expect( authorizeClassroomJobCreation(request, { mode: 'shadow', resolvePrincipal }), ).resolves.toEqual({ allowed: true, ownership: {} }); await expect( authorizeClassroomJobCreation(request, { mode: 'required', resolvePrincipal }), ).resolves.toMatchObject({ allowed: false, status: 503, code: 'IDENTITY_UNAVAILABLE', }); }); }); describe('classroom job resource enforcement', () => { test('allows account owners and bound device proofs, but denies a different account', async () => { const owned = job('job-1', { ownerPrincipalId: 'account-1' }); await expect( authorizeClassroomJobRequest(request, owned, 'delete', { mode: 'required', resolvePrincipal: async () => principal('user', 'account-1'), }), ).resolves.toEqual({ allowed: true }); await expect( authorizeClassroomJobRequest(request, owned, 'read', { mode: 'required', resolvePrincipal: async () => principal('device', 'device-1', { userId: 'account-1' }), }), ).resolves.toEqual({ allowed: true }); await expect( authorizeClassroomJobRequest(request, owned, 'read', { mode: 'required', resolvePrincipal: async () => principal('user', 'account-2'), }), ).resolves.toMatchObject({ allowed: false, status: 403, code: 'FORBIDDEN' }); }); test('keeps shadow responses unchanged and records the future decision', async () => { const observer = vi.fn(); await expect( authorizeClassroomJobRequest( request, job('job-shadow', { ownerPrincipalId: 'account-1' }), 'read', { mode: 'shadow', resolvePrincipal: async () => principal('user', 'account-2'), observer, }, ), ).resolves.toEqual({ allowed: true }); expect(observer).toHaveBeenCalledWith( expect.objectContaining({ decision: { effect: 'deny', reason: 'owner_mismatch' } }), ); }); test('filters list results by owner and gives only an exact capability the global view', async () => { const jobs = [ job('mine', { ownerPrincipalId: 'account-1' }), job('other', { ownerPrincipalId: 'account-2' }), job('legacy'), ]; await expect( authorizeClassroomJobList(request, jobs, { mode: 'required', resolvePrincipal: async () => principal('user', 'account-1'), }), ).resolves.toMatchObject({ allowed: true, jobs: [{ id: 'mine' }] }); await expect( authorizeClassroomJobList(request, jobs, { mode: 'required', resolvePrincipal: async () => principal('ops', 'ops-1', { capabilities: [CLASSROOM_JOB_MANAGE_ANY_CAPABILITY], }), }), ).resolves.toEqual({ allowed: true, jobs }); }); });