103 lines
4.1 KiB
TypeScript
103 lines
4.1 KiB
TypeScript
import { createServer } from "node:http";
|
|
import type { AddressInfo } from "node:net";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
createEdgeTtsMiddleware,
|
|
type EdgeTtsMiddlewareOptions,
|
|
type EdgeTtsSynthesizer
|
|
} from "./edge-tts-service";
|
|
|
|
const servers: ReturnType<typeof createServer>[] = [];
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
servers.splice(0).map((server) => new Promise<void>((resolve) => server.close(() => resolve())))
|
|
);
|
|
});
|
|
|
|
async function startServer(options: EdgeTtsMiddlewareOptions) {
|
|
const middleware = createEdgeTtsMiddleware(options);
|
|
const server = createServer((request, response) => {
|
|
void middleware(request, response, () => {
|
|
response.statusCode = 404;
|
|
response.end();
|
|
});
|
|
});
|
|
servers.push(server);
|
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
const { port } = server.address() as AddressInfo;
|
|
return `http://127.0.0.1:${port}`;
|
|
}
|
|
|
|
function postSpeech(baseUrl: string, body: unknown, headers = true) {
|
|
return fetch(`${baseUrl}/api/tts/edge`, {
|
|
method: "POST",
|
|
...(headers ? { headers: { "Content-Type": "application/json" } } : {}),
|
|
body: typeof body === "string" ? body : JSON.stringify(body)
|
|
});
|
|
}
|
|
|
|
describe("Edge TTS adapter", () => {
|
|
it("returns synthesized MP3 audio with the server-owned voice", async () => {
|
|
const synthesize = vi.fn<EdgeTtsSynthesizer>(async () => Uint8Array.from([73, 68, 51]));
|
|
const baseUrl = await startServer({ synthesize, defaultVoice: "zh-CN-XiaoxiaoNeural" });
|
|
|
|
const response = await postSpeech(baseUrl, { text: " 朗读调度结果 " });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.headers.get("content-type")).toBe("audio/mpeg");
|
|
expect(new Uint8Array(await response.arrayBuffer())).toEqual(Uint8Array.from([73, 68, 51]));
|
|
expect(synthesize).toHaveBeenCalledWith("朗读调度结果", "zh-CN-XiaoxiaoNeural");
|
|
});
|
|
|
|
it("rejects invalid, unsafe and oversized requests before synthesis", async () => {
|
|
const synthesize = vi.fn<EdgeTtsSynthesizer>(async () => Uint8Array.from([1]));
|
|
const baseUrl = await startServer({ synthesize });
|
|
|
|
expect((await postSpeech(baseUrl, "not-json")).status).toBe(400);
|
|
expect((await postSpeech(baseUrl, { text: "测试" }, false)).status).toBe(415);
|
|
expect((await postSpeech(baseUrl, { text: "测试", voice: "unsafe" })).status).toBe(400);
|
|
expect((await postSpeech(baseUrl, { text: "排".repeat(521) })).status).toBe(413);
|
|
expect(synthesize).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("limits requests by client and releases the window", async () => {
|
|
let currentTime = 1_000;
|
|
const synthesize = vi.fn<EdgeTtsSynthesizer>(async () => Uint8Array.from([1]));
|
|
const baseUrl = await startServer({
|
|
synthesize,
|
|
maxRequestsPerWindow: 1,
|
|
rateWindowMs: 1_000,
|
|
now: () => currentTime
|
|
});
|
|
|
|
expect((await postSpeech(baseUrl, { text: "第一次" })).status).toBe(200);
|
|
expect((await postSpeech(baseUrl, { text: "第二次" })).status).toBe(429);
|
|
currentTime += 1_000;
|
|
expect((await postSpeech(baseUrl, { text: "新窗口" })).status).toBe(200);
|
|
});
|
|
|
|
it("caps concurrent synthesis work", async () => {
|
|
let releaseFirst: ((audio: Uint8Array) => void) | undefined;
|
|
const firstAudio = new Promise<Uint8Array>((resolve) => {
|
|
releaseFirst = resolve;
|
|
});
|
|
const synthesize = vi.fn<EdgeTtsSynthesizer>(() => firstAudio);
|
|
const baseUrl = await startServer({ synthesize, maxConcurrent: 1 });
|
|
|
|
const firstRequest = postSpeech(baseUrl, { text: "第一段" });
|
|
await vi.waitFor(() => expect(synthesize).toHaveBeenCalledTimes(1));
|
|
expect((await postSpeech(baseUrl, { text: "第二段" })).status).toBe(429);
|
|
releaseFirst?.(Uint8Array.from([1]));
|
|
expect((await firstRequest).status).toBe(200);
|
|
});
|
|
|
|
it("returns a gateway timeout without releasing the occupied slot early", async () => {
|
|
const synthesize = vi.fn<EdgeTtsSynthesizer>(() => new Promise(() => undefined));
|
|
const baseUrl = await startServer({ synthesize, timeoutMs: 5 });
|
|
|
|
expect((await postSpeech(baseUrl, { text: "超时测试" })).status).toBe(504);
|
|
});
|
|
});
|