import { describe, expect, test, vi } from 'vitest'; import { NextRequest } from 'next/server'; import type { ClassroomGenerationJob } from '@/lib/server/classroom-job-store'; import { observeClassroomJobAuthorization } from '@/lib/server/authz/classroom-job-shadow'; import { observeResourceAuthorization } from '@/lib/server/authz/shadow-observer'; const request = new NextRequest('https://server.example/api/generate-classroom/job-1'); function job(owner: string | undefined, includeOwner = true): ClassroomGenerationJob { return { id: 'job-1', ...(includeOwner ? { ownerPrincipalId: owner } : {}), status: 'failed', step: 'failed', progress: 100, message: 'Failed', createdAt: '2026-08-15T00:00:00.000Z', updatedAt: '2026-08-15T00:00:00.000Z', inputSummary: { requirementPreview: 'topic', hasPdf: false, pdfTextLength: 0, pdfImageCount: 0, }, input: { requirement: 'topic' }, scenesGenerated: 0, }; } describe('observe-only resource authorization', () => { test('reports a decision through parameter-injected dependencies', async () => { const observer = vi.fn(); await observeResourceAuthorization( { request, resourceType: 'classroom-generation-job', resourceId: 'job-1', action: 'read', ownership: { state: 'owned', ownerPrincipalId: 'principal-1' }, }, { resolvePrincipal: async () => ({ state: 'authenticated', principal: { principalId: 'principal-1', kind: 'user', capabilities: [], authMethod: 'session', }, }), observer, }, ); expect(observer).toHaveBeenCalledWith({ resourceType: 'classroom-generation-job', resourceId: 'job-1', action: 'read', principalState: 'authenticated', ownershipState: 'owned', decision: { effect: 'allow', reason: 'owner_match' }, }); }); test('contains observer and resolver failures', async () => { await expect( observeResourceAuthorization( { request, resourceType: 'classroom-generation-job', resourceId: 'job-1', action: 'delete', ownership: { state: 'legacy-unowned' }, }, { resolvePrincipal: async () => ({ state: 'unavailable', reason: 'resolver_not_configured', }), observer: () => { throw new Error('observer failed'); }, }, ), ).resolves.toBeUndefined(); await expect( observeResourceAuthorization( { request, resourceType: 'classroom-generation-job', resourceId: 'job-1', action: 'delete', ownership: { state: 'legacy-unowned' }, }, { resolvePrincipal: async () => { throw new Error('resolver failed'); }, observer: vi.fn(), }, ), ).resolves.toBeUndefined(); }); test('classifies absent legacy owners and malformed owners as indeterminate', async () => { const observations: unknown[] = []; const deps = { resolvePrincipal: async () => ({ state: 'unavailable' as const, reason: 'resolver_not_configured' as const, }), observer: (observation: unknown) => { observations.push(observation); }, }; await observeClassroomJobAuthorization(request, job(undefined, false), 'read', deps); await observeClassroomJobAuthorization(request, job(undefined), 'delete', deps); expect(observations).toEqual([ expect.objectContaining({ ownershipState: 'legacy-unowned', decision: { effect: 'indeterminate', reason: 'owner_missing' }, }), expect.objectContaining({ ownershipState: 'unknown', decision: { effect: 'indeterminate', reason: 'owner_unknown' }, }), ]); }); test('classifies a temporary device binding as guest ownership', async () => { const observer = vi.fn(); const guestJob = { ...job(undefined, false), guestPrincipalId: 'device-1', ownershipBoundAt: '2026-08-16T00:00:00.000Z', }; await observeClassroomJobAuthorization(request, guestJob, 'read', { resolvePrincipal: async () => ({ state: 'authenticated', principal: { principalId: 'device-1', kind: 'device', capabilities: [], authMethod: 'device-proof', }, }), observer, }); expect(observer).toHaveBeenCalledWith( expect.objectContaining({ ownershipState: 'guest-owned', decision: { effect: 'allow', reason: 'guest_match' }, }), ); }); test('fails closed when a persisted job contains both owner and guest subjects', async () => { const observer = vi.fn(); await observeClassroomJobAuthorization( request, { ...job('account-1'), guestPrincipalId: 'device-1', }, 'read', { resolvePrincipal: async () => ({ state: 'authenticated', principal: { principalId: 'account-1', kind: 'user', capabilities: [], authMethod: 'session', }, }), observer, }, ); expect(observer).toHaveBeenCalledWith( expect.objectContaining({ ownershipState: 'unknown', decision: { effect: 'indeterminate', reason: 'owner_unknown' }, }), ); }); });