import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { NextRequest } from 'next/server'; import { POST } from '@/app/api/runtime/v1/bindings/validate/route'; describe('Makelore Learning Runtime binding', () => { let root = ''; const previousStore = process.env.MAKELORE_COURSE_STORE_DIR; const previousToken = process.env.MAKELORE_RUNTIME_TOKEN; beforeEach(async () => { root = await mkdtemp(join(tmpdir(), 'makelore-runtime-')); process.env.MAKELORE_COURSE_STORE_DIR = root; process.env.MAKELORE_RUNTIME_TOKEN = 'runtime-secret'; }); afterEach(async () => { await rm(root, { recursive: true, force: true }); if (previousStore === undefined) delete process.env.MAKELORE_COURSE_STORE_DIR; else process.env.MAKELORE_COURSE_STORE_DIR = previousStore; if (previousToken === undefined) delete process.env.MAKELORE_RUNTIME_TOKEN; else process.env.MAKELORE_RUNTIME_TOKEN = previousToken; }); it('accepts only a retained course with the exact immutable hash', async () => { const hash = 'a'.repeat(64); await mkdir(root, { recursive: true }); await writeFile(join(root, 'course-1.json'), JSON.stringify({ schemaVersion: 1, courseId: 'course-1', contentHash: hash, title: 'Python 基础入门', classroom: { stage: { id: 'stage-1', name: 'Python 基础入门' }, scenes: [] }, })); const request = new NextRequest('http://localhost/api/runtime/v1/bindings/validate', { method: 'POST', headers: { Authorization: 'Bearer runtime-secret', 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: 'session-1', owner_user_id: 'user-1', runtime: 'learning', runtime_version: 'v1', binding: { kind: 'learning-course', key: `course-1:${hash}` }, }), }); const response = await POST(request); expect(response.status).toBe(200); await expect(response.json()).resolves.toMatchObject({ type: 'learning.course.snapshot', payload: { courseId: 'course-1', contentHash: hash, sceneCount: 0 }, }); }); it('rejects calls without the private runtime token', async () => { const response = await POST(new NextRequest('http://localhost/api/runtime/v1/bindings/validate', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' }, })); expect(response.status).toBe(401); }); });