import test from 'node:test'; import assert from 'node:assert/strict'; import { createServer } from 'node:http'; import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { JobStore } from '../src/job-store.mjs'; import { createHttpHandler } from '../src/http-app.mjs'; async function fixture() { const directory = await mkdtemp(join(tmpdir(), 'yuban-server-test-')); const store = new JobStore(directory, 86_400_000); await store.init(); const enqueued = []; const worker = { enqueue(id) { enqueued.push(id); }, async cancel(id) { return store.update(id, { status: 'cancelled', stage: 'cancelled' }); }, }; const config = { corsAllowedOrigins: [ 'https://www.yuban.co', 'https://yuban.co', 'http://localhost:4200', 'http://127.0.0.1:4200', ], allowedAudioHosts: ['file.yuban.co'], jobRetentionMs: 86_400_000, maxActiveJobsPerUser: 2, maxRetainedJobsPerUser: 20, }; const authenticator = { async authenticate() { return { ownerKey: 'owner-a', userId: 'user-a' }; }, }; const server = createServer( createHttpHandler({ config, store, worker, authenticator, validateAudioUrl: async value => new URL(value), }), ); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); return { store, enqueued, baseUrl: `http://127.0.0.1:${address.port}`, async close() { await new Promise(resolve => server.close(resolve)); await rm(directory, { recursive: true, force: true }); }, }; } test('健康检查不要求认证', async t => { const app = await fixture(); t.after(() => app.close()); const response = await fetch(`${app.baseUrl}/health`); assert.equal(response.status, 200); const body = await response.json(); assert.equal(body.service, 'yuban-server'); }); test('仅允许配置的网页来源通过 CORS 预检', async t => { const app = await fixture(); t.after(() => app.close()); for (const origin of [ 'https://www.yuban.co', 'https://yuban.co', 'http://localhost:4200', 'http://127.0.0.1:4200', ]) { const response = await fetch(`${app.baseUrl}/recording-transcription/jobs`, { method: 'OPTIONS', headers: { Origin: origin, 'Access-Control-Request-Method': 'POST', 'Access-Control-Request-Headers': 'Authorization, Content-Type, Idempotency-Key', }, }); assert.equal(response.status, 204, origin); assert.equal(response.headers.get('access-control-allow-origin'), origin); assert.match(response.headers.get('vary') || '', /Origin/i); assert.match(response.headers.get('access-control-allow-methods') || '', /POST/); } const rejected = await fetch(`${app.baseUrl}/recording-transcription/jobs`, { method: 'OPTIONS', headers: { Origin: 'https://untrusted.example.com', 'Access-Control-Request-Method': 'POST', }, }); assert.equal(rejected.status, 403); const body = await rejected.json(); assert.equal(body.error.code, 'ORIGIN_NOT_ALLOWED'); assert.equal(rejected.headers.has('access-control-allow-origin'), false); }); test('创建任务立即返回 202,查询不会暴露音频地址或所有者', async t => { const app = await fixture(); t.after(() => app.close()); const created = await fetch(`${app.baseUrl}/recording-transcription/jobs`, { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: 'http://localhost:4200', 'Idempotency-Key': 'chat-voice-1', }, body: JSON.stringify({ audioUrl: 'https://file.yuban.co/test.mp3' }), }); assert.equal(created.status, 202); const createdBody = await created.json(); assert.equal(createdBody.job.status, 'queued'); assert.equal(app.enqueued.length, 1); const response = await fetch( `${app.baseUrl}/recording-transcription/jobs/${createdBody.job.id}`, ); const text = await response.text(); assert.equal(response.status, 200); assert.equal(text.includes('file.yuban.co'), false); assert.equal(text.includes('owner-a'), false); }); test('同一用户和幂等键复用已有任务', async t => { const app = await fixture(); t.after(() => app.close()); const create = () => fetch(`${app.baseUrl}/recording-transcription/jobs`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': 'same-recording' }, body: JSON.stringify({ audioUrl: 'https://file.yuban.co/test.mp3' }), }); const first = await (await create()).json(); const second = await (await create()).json(); assert.equal(first.job.id, second.job.id); assert.equal(second.reused, true); assert.equal(app.enqueued.length, 1); });