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