import { beforeEach, describe, expect, test, vi } from 'vitest'; import { NextRequest } from 'next/server'; const mocks = vi.hoisted(() => ({ after: vi.fn(), authorizeCreation: vi.fn(), authorizeList: vi.fn(), accessError: vi.fn(), createJob: vi.fn(), listJobs: vi.fn(), runJob: vi.fn(), })); vi.mock('next/server', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, after: mocks.after }; }); vi.mock('nanoid', () => ({ nanoid: () => 'job-created' })); vi.mock('@/lib/server/classroom-job-store', () => ({ createClassroomGenerationJob: mocks.createJob, listClassroomGenerationJobs: mocks.listJobs, })); vi.mock('@/lib/server/classroom-job-runner', () => ({ runClassroomGenerationJob: mocks.runJob, })); vi.mock('@/lib/server/classroom-storage', () => ({ buildRequestOrigin: () => 'https://server.example', })); vi.mock('@/lib/server/authz/classroom-job-access', () => ({ authorizeClassroomJobCreation: mocks.authorizeCreation, authorizeClassroomJobList: mocks.authorizeList, classroomJobAccessError: mocks.accessError, })); function storedJob(id: string) { return { id, status: 'queued', step: 'queued', progress: 0, message: 'queued', createdAt: '2026-08-16T00:00:00.000Z', updatedAt: '2026-08-16T00:00:00.000Z', inputSummary: { requirementPreview: id, hasPdf: false, pdfTextLength: 0, pdfImageCount: 0, }, input: { requirement: id }, scenesGenerated: 0, }; } describe('generation job create/list authorization boundary', () => { beforeEach(() => { vi.clearAllMocks(); vi.unstubAllEnvs(); mocks.accessError.mockImplementation( (denial: { status: number; code: string; message: string }) => Response.json( { success: false, errorCode: denial.code, error: denial.message }, { status: denial.status }, ), ); mocks.authorizeCreation.mockResolvedValue({ allowed: true, ownership: { ownerPrincipalId: 'account-1' }, }); mocks.createJob.mockResolvedValue(storedJob('job-created')); mocks.listJobs.mockResolvedValue([storedJob('mine'), storedJob('other')]); mocks.authorizeList.mockImplementation(async (_request, jobs) => ({ allowed: true, jobs: [jobs[0]], })); }); test('binds the server-derived owner when creating a public generation job', async () => { const { POST } = await import('@/app/api/generate-classroom/route'); const request = new NextRequest('https://server.example/api/generate-classroom', { method: 'POST', body: JSON.stringify({ requirement: 'Algebra' }), headers: { 'content-type': 'application/json' }, }); const response = await POST(request); expect(response.status).toBe(202); expect(mocks.createJob).toHaveBeenCalledWith( 'job-created', expect.objectContaining({ requirement: 'Algebra' }), { ownerPrincipalId: 'account-1' }, ); expect(mocks.after).toHaveBeenCalledOnce(); }); test('returns a required-mode denial before creating or scheduling work', async () => { mocks.authorizeCreation.mockResolvedValue({ allowed: false, status: 503, code: 'IDENTITY_UNAVAILABLE', message: 'Identity verification is unavailable.', }); const { POST } = await import('@/app/api/generate-classroom/route'); const request = new NextRequest('https://server.example/api/generate-classroom', { method: 'POST', body: JSON.stringify({ requirement: 'Algebra' }), headers: { 'content-type': 'application/json' }, }); const response = await POST(request); expect(response.status).toBe(503); expect(mocks.createJob).not.toHaveBeenCalled(); expect(mocks.after).not.toHaveBeenCalled(); }); test('returns only the jobs selected by required-mode ownership filtering', async () => { const { GET } = await import('@/app/api/generate-classroom/route'); const response = await GET( new NextRequest('https://server.example/api/generate-classroom?limit=20'), ); const body = await response.json(); expect(response.status).toBe(200); expect(body.items).toHaveLength(1); expect(body.items[0].jobId).toBe('mine'); expect(mocks.authorizeList).toHaveBeenCalledWith(expect.any(NextRequest), expect.any(Array), { mode: 'shadow', }); }); test('loads before owner filtering and applies the user limit afterwards in required mode', async () => { vi.stubEnv('OPENMAIC_IDENTITY_ENFORCEMENT', 'required'); const { GET } = await import('@/app/api/generate-classroom/route'); const response = await GET( new NextRequest('https://server.example/api/generate-classroom?limit=1'), ); const body = await response.json(); expect(response.status).toBe(200); expect(mocks.listJobs).toHaveBeenCalledWith(Number.MAX_SAFE_INTEGER); expect(mocks.authorizeList).toHaveBeenCalledWith(expect.any(NextRequest), expect.any(Array), { mode: 'required', }); expect(body.items).toHaveLength(1); }); });