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