Files
openmaic/OpenMAIC/tests/server/classroom-access.test.ts
2026-08-16 14:58:47 +08:00

117 lines
3.9 KiB
TypeScript

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 });
});
});