import { describe, expect, test } from 'vitest'; import type { PersistedClassroomData } from '@/lib/server/classroom-storage'; import type { PrincipalResolution } from '@/lib/server/authz/principal'; import { authorizeClassroomCreation, authorizeClassroomRequest, } from '@/lib/server/authz/classroom-access'; const request = new Request('https://server.example/api/classroom'); function principal( principalId: string, kind: 'user' | 'device' = 'user', userId?: string, ): PrincipalResolution { return { state: 'authenticated', principal: { principalId, kind, ...(userId ? { userId } : {}), capabilities: [], authMethod: kind === 'device' ? 'device-proof' : 'session', }, }; } function classroom( ownership: { ownerPrincipalId?: string; guestPrincipalId?: string } = {}, ): PersistedClassroomData { return { id: 'classroom-1', ...ownership, stage: { id: 'classroom-1', name: 'Classroom', createdAt: 1, updatedAt: 1 }, scenes: [], createdAt: '2026-08-16T00:00:00.000Z', }; } describe('classroom account ownership policy', () => { test('binds users permanently and unbound devices as migratable guests', async () => { await expect( authorizeClassroomCreation(request, { mode: 'required', resolvePrincipal: async () => principal('account-1'), }), ).resolves.toEqual({ allowed: true, ownership: { ownerPrincipalId: 'account-1' } }); await expect( authorizeClassroomCreation(request, { mode: 'required', resolvePrincipal: async () => principal('device-1', 'device'), }), ).resolves.toEqual({ allowed: true, ownership: { guestPrincipalId: 'device-1' } }); await expect( authorizeClassroomCreation(request, { mode: 'required', resolvePrincipal: async () => principal('device-1', 'device', 'account-1'), }), ).resolves.toEqual({ allowed: true, ownership: { ownerPrincipalId: 'account-1' } }); }); test('allows account owners and bound devices, but denies a different account', async () => { const owned = classroom({ ownerPrincipalId: 'account-1' }); await expect( authorizeClassroomRequest(request, owned, 'read', { mode: 'required', resolvePrincipal: async () => principal('account-1'), }), ).resolves.toEqual({ allowed: true }); await expect( authorizeClassroomRequest(request, owned, 'write', { mode: 'required', resolvePrincipal: async () => principal('device-1', 'device', 'account-1'), }), ).resolves.toEqual({ allowed: true }); await expect( authorizeClassroomRequest(request, owned, 'read', { mode: 'required', resolvePrincipal: async () => principal('account-2'), }), ).resolves.toMatchObject({ allowed: false, status: 403, code: 'FORBIDDEN' }); }); test('fails closed for legacy or corrupt ownership in required mode', async () => { const deps = { mode: 'required' as const, resolvePrincipal: async () => principal('account-1'), }; await expect( authorizeClassroomRequest(request, classroom(), 'read', deps), ).resolves.toMatchObject({ allowed: false, status: 409, code: 'OWNERSHIP_MIGRATION_REQUIRED' }); await expect( authorizeClassroomRequest( request, classroom({ ownerPrincipalId: 'account-1', guestPrincipalId: 'device-1' }), 'read', deps, ), ).resolves.toMatchObject({ allowed: false, status: 409, code: 'OWNERSHIP_MIGRATION_REQUIRED', }); }); test('keeps current desktop behavior in explicit shadow mode', async () => { await expect( authorizeClassroomRequest(request, classroom({ ownerPrincipalId: 'other' }), 'write', { mode: 'shadow', resolvePrincipal: async () => ({ state: 'unavailable', reason: 'resolver_not_configured' }), }), ).resolves.toEqual({ allowed: true }); }); });