Files
hunjing/bff/test/proxy.test.ts
T

102 lines
4.0 KiB
TypeScript

/** 照壁代理行为测试:头透传、SSE 流顺序、断线续传头、上游错误与不可达映射。 */
import http from "node:http";
import type { AddressInfo } from "node:net";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { buildServer } from "../src/server.js";
let upstream: http.Server;
let upstreamUrl = "";
let upstreamCalls: Array<{ method: string; url: string; headers: http.IncomingHttpHeaders; body: string }> = [];
beforeAll(async () => {
upstream = http.createServer((req, res) => {
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => {
upstreamCalls.push({ method: req.method ?? "", url: req.url ?? "", headers: req.headers, body });
if (req.url === "/api/v1/echo") {
res.writeHead(200, { "content-type": "application/json", "x-trace-id": String(req.headers["x-trace-id"] ?? "") });
res.end(JSON.stringify({ ok: true, auth: req.headers.authorization ?? null, body }));
} else if (req.url === "/api/v1/stream") {
res.writeHead(200, { "content-type": "text/event-stream; charset=utf-8" });
const frames = ["id: 1\nevent: run.started\n\n", "id: 2\nevent: llm.delta\n\n", "id: 3\nevent: run.finished\n\n"];
frames.forEach((frame, i) => setTimeout(() => res.write(frame), i * 20));
setTimeout(() => res.end(), frames.length * 20 + 10);
} else if (req.url === "/api/v1/boom") {
res.writeHead(404, { "content-type": "application/json" });
res.end(JSON.stringify({ error: { code: "HJ-1002", message: "不存在" } }));
} else {
res.writeHead(200, { "content-type": "application/json" });
res.end("{}");
}
});
});
await new Promise<void>((resolve) => upstream.listen(0, "127.0.0.1", resolve));
upstreamUrl = `http://127.0.0.1:${(upstream.address() as AddressInfo).port}`;
});
afterAll(async () => {
await new Promise<void>((resolve) => upstream.close(() => resolve()));
});
function app() {
return buildServer({
port: 0,
backendUrl: upstreamUrl,
bodyLimit: 1024 * 1024,
logLevel: "silent",
});
}
describe("照壁代理", () => {
it("转发 Authorization 与 Idempotency-Key,透传响应体与 x-trace-id", async () => {
const res = await app().inject({
method: "POST",
url: "/api/v1/echo",
headers: { authorization: "Bearer tok", "idempotency-key": "idem-1", "x-trace-id": "trace-9" },
payload: JSON.stringify({ a: 1 }),
});
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body.ok).toBe(true);
expect(body.auth).toBe("Bearer tok");
expect(body.body).toBe(JSON.stringify({ a: 1 }));
expect(res.headers["x-trace-id"]).toBe("trace-9");
const call = upstreamCalls.at(-1)!;
expect(call.headers["authorization"]).toBe("Bearer tok");
expect(call.headers["idempotency-key"]).toBe("idem-1");
});
it("SSE 流式透传:帧序完整、禁缓冲头、Last-Event-ID 透传", async () => {
const res = await app().inject({
method: "GET",
url: "/api/v1/stream",
headers: { "last-event-id": "2", authorization: "Bearer tok" },
});
expect(res.statusCode).toBe(200);
expect(res.headers["content-type"]).toContain("text/event-stream");
expect(res.headers["x-accel-buffering"]).toBe("no");
expect(res.body.indexOf("run.started")).toBeLessThan(res.body.indexOf("run.finished"));
const call = upstreamCalls.at(-1)!;
expect(call.headers["last-event-id"]).toBe("2");
});
it("上游错误状态与错误体原样透传", async () => {
const res = await app().inject({ method: "GET", url: "/api/v1/boom" });
expect(res.statusCode).toBe(404);
expect(res.json().error.code).toBe("HJ-1002");
});
it("上游不可达映射 HJ-3001", async () => {
const dead = buildServer({
port: 0,
backendUrl: "http://127.0.0.1:1",
bodyLimit: 1024,
logLevel: "silent",
});
const res = await dead.inject({ method: "GET", url: "/api/v1/echo" });
expect(res.statusCode).toBe(502);
expect(res.json().error.code).toBe("HJ-3001");
});
});