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

167 lines
5.7 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { promises as fs } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import type { CoursewareRecord, CoursewareRepo } from '@/lib/courseware-repo/types';
import { createFileCoursewareRepo } from '@/lib/courseware-repo/store';
import {
hasPublishedCourseReceiptSource,
isPublishedClassroomSource,
mayAccessRawClassroom,
protectsPublishedClassroomSources,
} from '@/lib/server/published-classroom-access';
const noPublicationReceipt = async () => false;
function record(coursewareId: string, sourceClassroomId?: string): CoursewareRecord {
return {
coursewareId,
...(sourceClassroomId ? { sourceClassroomId } : {}),
version: 1,
title: 'Frozen module',
status: 'published',
publishedAt: '2026-08-15T00:00:00.000Z',
contentHash: 'a'.repeat(64),
byteSize: 1,
entryCount: 1,
sceneCount: 1,
quizSceneCount: 0,
knowledgeVersion: 1,
complete: true,
bundleUrl: '/bundle.zip',
storageKey: 'module/v1.zip',
};
}
function repo(records: CoursewareRecord[]): CoursewareRepo {
return {
nextVersion: vi.fn(),
saveRecord: vi.fn(),
setStatus: vi.fn(),
getRecord: vi.fn(),
getLatestRecord: vi.fn(),
listRecords: vi.fn().mockResolvedValue(records),
listLatest: vi.fn(),
};
}
describe('published classroom source access', () => {
it('protects public-serving deployments but leaves the ops workbench available', () => {
expect(protectsPublishedClassroomSources('learner')).toBe(true);
expect(protectsPublishedClassroomSources('server')).toBe(true);
expect(protectsPublishedClassroomSources('ops')).toBe(false);
expect(protectsPublishedClassroomSources('all')).toBe(false);
});
it('recognizes both new private source metadata and legacy shared ids', async () => {
const records = repo([
record('course_public_module_1', 'classroom-private-source'),
record('legacy-classroom-id'),
]);
await expect(
isPublishedClassroomSource('classroom-private-source', records, noPublicationReceipt),
).resolves.toBe(true);
await expect(
isPublishedClassroomSource('legacy-classroom-id', records, noPublicationReceipt),
).resolves.toBe(true);
await expect(
isPublishedClassroomSource('unpublished-source', records, noPublicationReceipt),
).resolves.toBe(false);
});
it('uses the current ops receipt when the remote registry is not mounted locally', async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'published-source-receipt-'));
try {
await fs.writeFile(
path.join(dir, 'course-remote.json'),
JSON.stringify({
id: 'course-remote',
publication: {
receiptVersion: 1,
manifestVersion: 3,
publishedAt: '2026-08-15T00:00:00.000Z',
modules: [
{
index: 1,
sourceClassroomId: 'remote-source-classroom',
sourceRevisionHash: 'a'.repeat(64),
sourceSemanticHash: 'b'.repeat(64),
coursewareId: 'course_course-remote_module_1',
coursewareVersion: 2,
contentHash: 'c'.repeat(64),
},
],
},
}),
'utf-8',
);
const lookup = (classroomId: string) => hasPublishedCourseReceiptSource(classroomId, dir);
const emptyRegistry = repo([]);
await expect(
isPublishedClassroomSource('remote-source-classroom', emptyRegistry, lookup),
).resolves.toBe(true);
await expect(
isPublishedClassroomSource('ordinary-source-classroom', emptyRegistry, lookup),
).resolves.toBe(false);
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});
it('fails closed when an ops publication receipt is corrupt', async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'published-source-receipt-corrupt-'));
try {
await fs.writeFile(path.join(dir, 'course-corrupt.json'), '{not-json', 'utf-8');
await expect(
hasPublishedCourseReceiptSource('ordinary-source-classroom', dir),
).rejects.toThrow();
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});
it('blocks a published source for learners while preserving ops review', async () => {
const records = repo([record('course_public_module_1', 'classroom-private-source')]);
await expect(
mayAccessRawClassroom('classroom-private-source', {
role: 'learner',
repo: records,
receiptLookup: noPublicationReceipt,
}),
).resolves.toBe(false);
await expect(
mayAccessRawClassroom('classroom-private-source', {
role: 'ops',
repo: records,
receiptLookup: noPublicationReceipt,
}),
).resolves.toBe(true);
await expect(
mayAccessRawClassroom('ordinary-user-classroom', {
role: 'learner',
repo: records,
receiptLookup: noPublicationReceipt,
}),
).resolves.toBe(true);
});
it('uses a fail-closed reverse lookup for the file registry', async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'published-source-access-'));
try {
const records = createFileCoursewareRepo(dir);
await records.saveRecord(record('course_public_module_1', 'classroom-private-source'));
await expect(
isPublishedClassroomSource('classroom-private-source', records, noPublicationReceipt),
).resolves.toBe(true);
await fs.writeFile(path.join(dir, 'corrupt.json'), '{not-json', 'utf-8');
await expect(
isPublishedClassroomSource('ordinary-user-classroom', records, noPublicationReceipt),
).rejects.toThrow();
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});
});