// @vitest-environment node import { createHash } from 'node:crypto'; import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import AdmZip from 'adm-zip'; import { createLearningPlayerServer, registerLearningCoursePackage, } from '@electron/services/learning-player-server'; async function writePlayerArtifact(root: string): Promise { const html = '
player
'; await mkdir(join(root, '_next', 'static', 'chunks'), { recursive: true }); await mkdir(join(root, 'avatars'), { recursive: true }); await mkdir(join(root, 'public'), { recursive: true }); await writeFile(join(root, 'index.html'), html); await writeFile(join(root, 'artifact.json'), JSON.stringify({ schemaVersion: 1, entrypoint: 'index.html', htmlSha256: createHash('sha256').update(html).digest('hex'), })); await writeFile(join(root, '_next', 'static', 'chunks', 'app.js'), 'window.player = true'); await writeFile(join(root, 'avatars', 'teacher.png'), 'avatar'); await writeFile(join(root, 'public', 'openmaic-mark.png'), 'mark'); } describe('Learning player static server', () => { let root: string | null = null; let close: (() => Promise) | null = null; afterEach(async () => { if (close) await close(); if (root) await rm(root, { recursive: true, force: true }); close = null; root = null; }); it('serves only the embedded OpenMAIC player and immutable static assets', async () => { root = await mkdtemp(join(tmpdir(), 'makelore-player-server-')); await writePlayerArtifact(root); const server = await createLearningPlayerServer({ artifactRoot: root }); close = server.close; const index = await fetch(server.url); expect(index.status).toBe(200); expect(await index.text()).toContain('player'); expect(index.headers.get('content-security-policy')).toContain("object-src 'none'"); const asset = await fetch(new URL('/_next/static/chunks/app.js', server.url)); expect(asset.headers.get('cache-control')).toContain('immutable'); expect(await asset.text()).toContain('window.player'); const avatar = await fetch(new URL('/avatars/teacher.png', server.url)); expect(avatar.status).toBe(200); expect(avatar.headers.get('content-type')).toBe('image/png'); const publicAsset = await fetch(new URL('/openmaic-mark.png', server.url)); expect(publicAsset.status).toBe(200); expect(publicAsset.headers.get('cache-control')).toContain('immutable'); expect(await publicAsset.text()).toBe('mark'); expect((await fetch(new URL('/package.json', server.url))).status).toBe(404); expect((await fetch(new URL('/%2e%2e/artifact.json', server.url))).status).toBe(404); }); it('serves only audio and media entries from a verified registered course package', async () => { root = await mkdtemp(join(tmpdir(), 'makelore-player-server-')); await writePlayerArtifact(root); const archivePath = join(root, 'course.zip'); const archive = new AdmZip(); archive.addFile('modules/module-1/audio/line.mp3', Buffer.from('audio-bytes')); archive.addFile('modules/module-1/media/diagram.png', Buffer.from('image-bytes')); archive.addFile('modules/module-1/bundle.json', Buffer.from('{}')); archive.writeZip(archivePath); const contentHash = 'a'.repeat(64); registerLearningCoursePackage('course-assets-test', contentHash, archivePath); const server = await createLearningPlayerServer({ artifactRoot: root }); close = server.close; const audio = await fetch(new URL( `/course-assets/course-assets-test/${contentHash}/modules/module-1/audio/line.mp3`, server.url, )); expect(audio.status).toBe(200); expect(audio.headers.get('content-type')).toBe('audio/mpeg'); expect(await audio.text()).toBe('audio-bytes'); expect((await fetch(new URL( `/course-assets/course-assets-test/${contentHash}/modules/module-1/bundle.json`, server.url, ))).status).toBe(404); }); });