http-app.test.mjs 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import test from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import { createServer } from 'node:http';
  4. import { mkdtemp, rm } from 'node:fs/promises';
  5. import { tmpdir } from 'node:os';
  6. import { join } from 'node:path';
  7. import { JobStore } from '../src/job-store.mjs';
  8. import { createHttpHandler } from '../src/http-app.mjs';
  9. async function fixture() {
  10. const directory = await mkdtemp(join(tmpdir(), 'yuban-server-test-'));
  11. const store = new JobStore(directory, 86_400_000);
  12. await store.init();
  13. const enqueued = [];
  14. const worker = {
  15. enqueue(id) {
  16. enqueued.push(id);
  17. },
  18. async cancel(id) {
  19. return store.update(id, { status: 'cancelled', stage: 'cancelled' });
  20. },
  21. };
  22. const config = {
  23. corsAllowedOrigins: ['http://localhost:4200'],
  24. allowedAudioHosts: ['file.yuban.co'],
  25. jobRetentionMs: 86_400_000,
  26. maxActiveJobsPerUser: 2,
  27. maxRetainedJobsPerUser: 20,
  28. };
  29. const authenticator = {
  30. async authenticate() {
  31. return { ownerKey: 'owner-a', userId: 'user-a' };
  32. },
  33. };
  34. const server = createServer(
  35. createHttpHandler({
  36. config,
  37. store,
  38. worker,
  39. authenticator,
  40. validateAudioUrl: async value => new URL(value),
  41. }),
  42. );
  43. await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
  44. const address = server.address();
  45. return {
  46. store,
  47. enqueued,
  48. baseUrl: `http://127.0.0.1:${address.port}`,
  49. async close() {
  50. await new Promise(resolve => server.close(resolve));
  51. await rm(directory, { recursive: true, force: true });
  52. },
  53. };
  54. }
  55. test('健康检查不要求认证', async t => {
  56. const app = await fixture();
  57. t.after(() => app.close());
  58. const response = await fetch(`${app.baseUrl}/health`);
  59. assert.equal(response.status, 200);
  60. const body = await response.json();
  61. assert.equal(body.service, 'yuban-server');
  62. });
  63. test('创建任务立即返回 202,查询不会暴露音频地址或所有者', async t => {
  64. const app = await fixture();
  65. t.after(() => app.close());
  66. const created = await fetch(`${app.baseUrl}/recording-transcription/jobs`, {
  67. method: 'POST',
  68. headers: {
  69. 'Content-Type': 'application/json',
  70. Origin: 'http://localhost:4200',
  71. 'Idempotency-Key': 'chat-voice-1',
  72. },
  73. body: JSON.stringify({ audioUrl: 'https://file.yuban.co/test.mp3' }),
  74. });
  75. assert.equal(created.status, 202);
  76. const createdBody = await created.json();
  77. assert.equal(createdBody.job.status, 'queued');
  78. assert.equal(app.enqueued.length, 1);
  79. const response = await fetch(
  80. `${app.baseUrl}/recording-transcription/jobs/${createdBody.job.id}`,
  81. );
  82. const text = await response.text();
  83. assert.equal(response.status, 200);
  84. assert.equal(text.includes('file.yuban.co'), false);
  85. assert.equal(text.includes('owner-a'), false);
  86. });
  87. test('同一用户和幂等键复用已有任务', async t => {
  88. const app = await fixture();
  89. t.after(() => app.close());
  90. const create = () =>
  91. fetch(`${app.baseUrl}/recording-transcription/jobs`, {
  92. method: 'POST',
  93. headers: { 'Content-Type': 'application/json', 'Idempotency-Key': 'same-recording' },
  94. body: JSON.stringify({ audioUrl: 'https://file.yuban.co/test.mp3' }),
  95. });
  96. const first = await (await create()).json();
  97. const second = await (await create()).json();
  98. assert.equal(first.job.id, second.job.id);
  99. assert.equal(second.reused, true);
  100. assert.equal(app.enqueued.length, 1);
  101. });