Files
diegosouzapw--omniroute/tests/unit/proxyfetch-vercel-relay-2743.test.ts
2026-07-13 13:39:12 +08:00

183 lines
8.7 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
// #2743 (gap c — deferred test debt): the Vercel-relay dispatch path had no direct
// coverage. `tests/unit/proxy-fetch.test.ts` exercises the HTTP/SOCKS/TLS-context
// branches but never the relay short-circuit, and `proxy-registry.test.ts` only
// asserts the DB schema for `type:vercel`. This file covers:
// 1. `buildVercelRelayHeaders` as a pure function (header shape + values).
// 2. The relay short-circuit in `proxyFetch` — that a `vercel`-type proxy context
// rewrites the request to the relay endpoint with the relay headers and never
// touches the TCP/proxy-dispatcher path.
//
// IMPORTANT: the relay branch fires `originalFetch(...)`, which is captured from
// `globalThis.fetch` at module-load time (proxyFetch.ts line ~54/63). To intercept it
// deterministically we install a fetch spy on `globalThis.fetch` BEFORE the first
// import of the proxyFetch module, then dynamically import it so `originalFetch`
// resolves to our spy. The module also reassigns `globalThis.fetch = patchedFetch`
// during init; we capture `proxyFetch` (the named export) which calls `patchedFetch`
// directly, so the spy stays the relay-branch sink and is not shadowed.
// --- Install the relay sink BEFORE importing the module under test. ---
type FetchCall = { input: unknown; init: RequestInit & { headers?: HeadersInit } };
const relayCalls: FetchCall[] = [];
const realGlobalFetch = globalThis.fetch;
// The spy stands in for the native (pre-patch) fetch that the relay branch calls.
// It records the call and returns a canned Response so no network I/O happens.
const relaySink = (async (input: unknown, init: RequestInit = {}) => {
relayCalls.push({ input, init });
return Response.json({ via: "vercel-relay" });
}) as unknown as typeof globalThis.fetch;
globalThis.fetch = relaySink;
// Dynamic import AFTER the stub so `originalFetch` (captured at module init) === relaySink.
const proxyDispatcher = await import("../../open-sse/utils/proxyDispatcher.ts");
const { buildVercelRelayHeaders } = proxyDispatcher;
const proxyFetchMod = await import("../../open-sse/utils/proxyFetch.ts");
const { proxyFetch, runWithProxyContext } = proxyFetchMod;
test.after(() => {
// Restore whatever the module installed (or the real native fetch) so we never
// leave a global spy behind for sibling tests sharing the process.
globalThis.fetch = realGlobalFetch;
});
test.beforeEach(() => {
relayCalls.length = 0;
});
// --------------------------------------------------------------------------
// 1. buildVercelRelayHeaders — pure function
// --------------------------------------------------------------------------
test("buildVercelRelayHeaders splits target into origin + path/query and forwards auth", () => {
const headers = buildVercelRelayHeaders(
"https://api.anthropic.com/v1/messages?beta=true",
"secret-relay-token"
);
assert.deepEqual(headers, {
"x-relay-target": "https://api.anthropic.com",
"x-relay-path": "/v1/messages?beta=true",
"x-relay-auth": "secret-relay-token",
});
});
test("buildVercelRelayHeaders preserves a non-default port in the target origin", () => {
const headers = buildVercelRelayHeaders("https://upstream.example.com:8443/v1/chat", "tok");
// URL.host includes the port (URL.hostname would drop it), so the relay edge
// function reconstructs the exact origin including the explicit port.
assert.equal(headers["x-relay-target"], "https://upstream.example.com:8443");
assert.equal(headers["x-relay-path"], "/v1/chat");
assert.equal(headers["x-relay-auth"], "tok");
});
test("buildVercelRelayHeaders yields a bare '/' path when the target has no path", () => {
const headers = buildVercelRelayHeaders("https://api.openai.com", "tok2");
// new URL("https://host").pathname === "/" and .search === "".
assert.equal(headers["x-relay-target"], "https://api.openai.com");
assert.equal(headers["x-relay-path"], "/");
assert.equal(headers["x-relay-auth"], "tok2");
});
test("buildVercelRelayHeaders carries the relayAuth verbatim (empty string allowed at this layer)", () => {
// The helper itself does not validate relayAuth — it copies it verbatim.
// The missing-relayAuth GUARD lives in proxyFetch's relay branch (tested below),
// which throws before this helper is ever called. This asserts the helper's
// actual contract rather than a behavior it does not own.
const headers = buildVercelRelayHeaders("https://api.example.com/x", "");
assert.equal(headers["x-relay-auth"], "");
assert.equal(headers["x-relay-target"], "https://api.example.com");
assert.equal(headers["x-relay-path"], "/x");
});
test("buildVercelRelayHeaders throws on an unparsable target URL", () => {
// It delegates to `new URL(targetUrl)`, which throws on garbage input.
assert.throws(() => buildVercelRelayHeaders("not-a-url", "tok"), /Invalid URL/);
});
// --------------------------------------------------------------------------
// 2. proxyFetch relay short-circuit (dispatch decision + header rewrite)
// --------------------------------------------------------------------------
const VERCEL_CTX = {
type: "vercel" as const,
host: "omniroute-relay-abc123.vercel.app",
relayAuth: "live-relay-secret",
};
test("proxyFetch routes a vercel-type context through the relay endpoint with relay headers", async () => {
const response = await runWithProxyContext(VERCEL_CTX, () =>
proxyFetch("https://api.anthropic.com/v1/messages?x=1", {
method: "POST",
headers: { "x-existing": "keep-me" },
})
);
// The canned relay response proves the relay sink (originalFetch) was hit and the
// TCP/proxy-dispatcher path was skipped (a real dispatcher would have failed to
// connect to the fake host).
assert.deepEqual(await response.json(), { via: "vercel-relay" });
assert.equal(relayCalls.length, 1, "exactly one relay dispatch");
const call = relayCalls[0];
// The request is rewritten to the relay edge endpoint, NOT the upstream target.
assert.equal(call.input, "https://omniroute-relay-abc123.vercel.app");
const sentHeaders = new Headers(call.init.headers);
// Relay headers carry the real upstream routing.
assert.equal(sentHeaders.get("x-relay-target"), "https://api.anthropic.com");
assert.equal(sentHeaders.get("x-relay-path"), "/v1/messages?x=1");
assert.equal(sentHeaders.get("x-relay-auth"), "live-relay-secret");
// Pre-existing caller headers are preserved (merged, not dropped).
assert.equal(sentHeaders.get("x-existing"), "keep-me");
// The original request method survives the rewrite.
assert.equal(call.init.method, "POST");
// duplex:"half" is set so a streamed request body is allowed by undici/native fetch.
assert.equal((call.init as { duplex?: string }).duplex, "half");
});
test("proxyFetch throws (without dispatching) when a vercel context is missing relayAuth", async () => {
await assert.rejects(
runWithProxyContext({ type: "vercel", host: "relay.vercel.app" }, () =>
proxyFetch("https://api.anthropic.com/v1/messages", { method: "POST" })
),
/Vercel relay configuration error: missing relayAuth/
);
// Fail-closed: no request was sent to the relay endpoint.
assert.equal(relayCalls.length, 0, "no relay dispatch when relayAuth is missing");
});
test("the missing-relayAuth error message does not leak internal [ProxyFetch] diagnostics", async () => {
// Guards the comment at proxyFetch.ts: the throw can bubble into response bodies,
// so it must stay free of internal labels and stack-trace markers.
await runWithProxyContext({ type: "vercel", host: "relay.vercel.app" }, async () => {
try {
await proxyFetch("https://api.anthropic.com/v1/messages", { method: "POST" });
assert.fail("expected the relay branch to throw on missing relayAuth");
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
assert.ok(!message.includes("[ProxyFetch]"), "no internal [ProxyFetch] label");
assert.ok(!message.includes("at /"), "no stack-trace path leaked");
}
});
});
test("a non-vercel proxy context never takes the relay branch", async () => {
// Sanity guard on the dispatch decision: a plain http proxy context must NOT be
// routed through the relay sink. runWithProxyContext fast-fails the unreachable
// bogus proxy (PROXY_UNREACHABLE) before fn() runs — that throw is expected and
// swallowed here; the point is that the relay sink stays untouched either way.
await runWithProxyContext({ type: "http", host: "127.0.0.1", port: "9" }, async () => {
await proxyFetch("https://api.anthropic.com/v1/messages", { method: "POST" });
}).catch(() => undefined);
assert.equal(relayCalls.length, 0, "http proxy context must not hit the vercel relay sink");
});