Spaces:
Runtime error
Runtime error
| /** | |
| * T-07 — embed proxy route handler tests. | |
| * | |
| * Tests GET/POST/PUT/PATCH/DELETE handlers in | |
| * /dashboard/providers/services/[name]/embed/[...path]/route.ts. | |
| * | |
| * Uses registerSupervisor to inject fake supervisors (ESM live bindings | |
| * can't be reassigned, so direct module patching is not possible). | |
| */ | |
| import { describe, it, afterEach } from "node:test"; | |
| import assert from "node:assert/strict"; | |
| import { registerSupervisor, unregisterSupervisor } from "../../../src/lib/services/registry.ts"; | |
| import type { ServiceSupervisor } from "../../../src/lib/services/ServiceSupervisor.ts"; | |
| import { | |
| GET, | |
| POST, | |
| PUT, | |
| PATCH, | |
| DELETE, | |
| HEAD, | |
| OPTIONS, | |
| } from "../../../src/app/(dashboard)/dashboard/providers/services/[name]/embed/[...path]/route.ts"; | |
| const originalFetch = globalThis.fetch; | |
| afterEach(() => { | |
| globalThis.fetch = originalFetch; | |
| unregisterSupervisor("9router"); | |
| }); | |
| // ─── helpers ──────────────────────────────────────────────────────────────── | |
| function makeFakeParams( | |
| name: string, | |
| path: string[] | |
| ): { params: Promise<{ name: string; path: string[] }> } { | |
| return { params: Promise.resolve({ name, path }) }; | |
| } | |
| function registerFake(state: string, port: number): void { | |
| const fake = { | |
| getStatus: () => ({ | |
| tool: "9router", | |
| state, | |
| port, | |
| pid: null, | |
| health: "unknown" as const, | |
| startedAt: null, | |
| lastError: null, | |
| }), | |
| }; | |
| registerSupervisor(fake as unknown as ServiceSupervisor); | |
| } | |
| // ─── tests ─────────────────────────────────────────────────────────────────── | |
| describe("embed proxy route", () => { | |
| it("returns 404 for unknown service", async () => { | |
| // No supervisor registered — getSupervisor returns null. | |
| const req = new Request("http://localhost/dashboard/providers/services/unknown/embed/"); | |
| const resp = await GET(req, makeFakeParams("unknown", [])); | |
| assert.equal(resp.status, 404); | |
| }); | |
| it("returns 503 when service exists but is not running", async () => { | |
| registerFake("stopped", 20130); | |
| const req = new Request("http://localhost/dashboard/providers/services/9router/embed/"); | |
| const resp = await GET(req, makeFakeParams("9router", [])); | |
| assert.equal(resp.status, 503); | |
| }); | |
| it("proxies GET to the upstream service", async () => { | |
| registerFake("running", 20130); | |
| let capturedUrl = ""; | |
| globalThis.fetch = async (input: string | URL | Request) => { | |
| capturedUrl = String(input); | |
| return new Response("<html>9router UI</html>", { | |
| status: 200, | |
| headers: { "content-type": "text/html" }, | |
| }); | |
| }; | |
| const req = new Request( | |
| "http://localhost/dashboard/providers/services/9router/embed/ui/index.html" | |
| ); | |
| const resp = await GET(req, makeFakeParams("9router", ["ui", "index.html"])); | |
| assert.equal(resp.status, 200); | |
| assert.ok(capturedUrl.startsWith("http://127.0.0.1:20130/ui/index.html")); | |
| assert.ok((await resp.text()).includes("9router UI")); | |
| }); | |
| it("forwards query string to upstream", async () => { | |
| registerFake("running", 20130); | |
| let capturedUrl = ""; | |
| globalThis.fetch = async (input: string | URL | Request) => { | |
| capturedUrl = String(input); | |
| return new Response("{}", { status: 200 }); | |
| }; | |
| const req = new Request( | |
| "http://localhost/dashboard/providers/services/9router/embed/api/models?page=2" | |
| ); | |
| await GET(req, makeFakeParams("9router", ["api", "models"])); | |
| assert.ok(capturedUrl.includes("?page=2")); | |
| }); | |
| it("proxies POST and forwards body", async () => { | |
| registerFake("running", 20130); | |
| let capturedMethod = ""; | |
| globalThis.fetch = async (_input: string | URL | Request, init?: RequestInit) => { | |
| capturedMethod = init?.method ?? "UNKNOWN"; | |
| return new Response('{"ok":true}', { status: 200 }); | |
| }; | |
| const req = new Request("http://localhost/dashboard/providers/services/9router/embed/api/v1", { | |
| method: "POST", | |
| body: JSON.stringify({ test: 1 }), | |
| headers: { "content-type": "application/json" }, | |
| }); | |
| const resp = await POST(req, makeFakeParams("9router", ["api", "v1"])); | |
| assert.equal(resp.status, 200); | |
| assert.equal(capturedMethod, "POST"); | |
| }); | |
| it("strips hop-by-hop headers from the upstream response", async () => { | |
| registerFake("running", 20130); | |
| globalThis.fetch = async () => | |
| new Response("body", { | |
| status: 200, | |
| headers: { | |
| "content-type": "text/plain", | |
| "transfer-encoding": "chunked", | |
| connection: "keep-alive", | |
| "x-custom": "kept", | |
| }, | |
| }); | |
| const req = new Request("http://localhost/dashboard/providers/services/9router/embed/"); | |
| const resp = await GET(req, makeFakeParams("9router", [])); | |
| assert.equal(resp.headers.get("x-custom"), "kept"); | |
| assert.equal(resp.headers.get("transfer-encoding"), null); | |
| assert.equal(resp.headers.get("connection"), null); | |
| }); | |
| it("returns 502 on upstream network error", async () => { | |
| registerFake("running", 20130); | |
| globalThis.fetch = async () => { | |
| throw new Error("ECONNREFUSED"); | |
| }; | |
| const req = new Request("http://localhost/dashboard/providers/services/9router/embed/"); | |
| const resp = await GET(req, makeFakeParams("9router", [])); | |
| assert.equal(resp.status, 502); | |
| }); | |
| it("PUT, PATCH, DELETE are handled", async () => { | |
| registerFake("running", 20130); | |
| globalThis.fetch = async (_input: string | URL | Request, init?: RequestInit) => | |
| new Response(null, { status: 204 }); | |
| const params = makeFakeParams("9router", ["resource", "1"]); | |
| const reqUrl = "http://localhost/dashboard/providers/services/9router/embed/resource/1"; | |
| assert.equal((await PUT(new Request(reqUrl, { method: "PUT" }), params)).status, 204); | |
| assert.equal((await PATCH(new Request(reqUrl, { method: "PATCH" }), params)).status, 204); | |
| assert.equal((await DELETE(new Request(reqUrl, { method: "DELETE" }), params)).status, 204); | |
| }); | |
| // ─── G-05: cookie/auth strip + response header strip + HTML rewrite ────────── | |
| it("G-05: strips cookie header before forwarding to upstream", async () => { | |
| registerFake("running", 20130); | |
| let capturedHeaders: Record<string, string> = {}; | |
| globalThis.fetch = async (_input: string | URL | Request, init?: RequestInit) => { | |
| for (const [k, v] of new Headers(init?.headers as HeadersInit).entries()) { | |
| capturedHeaders[k.toLowerCase()] = v; | |
| } | |
| return new Response("ok", { status: 200 }); | |
| }; | |
| const req = new Request("http://localhost/dashboard/providers/services/9router/embed/", { | |
| headers: { cookie: "session=abc123; jwt=secret" }, | |
| }); | |
| await GET(req, makeFakeParams("9router", [])); | |
| assert.equal(capturedHeaders["cookie"], undefined, "cookie must not be forwarded upstream"); | |
| }); | |
| it("G-05: sets Authorization: Bearer on upstream request", async () => { | |
| registerFake("running", 20130); | |
| let capturedAuth: string | undefined; | |
| globalThis.fetch = async (_input: string | URL | Request, init?: RequestInit) => { | |
| capturedAuth = new Headers(init?.headers as HeadersInit).get("authorization") ?? undefined; | |
| return new Response("ok", { status: 200 }); | |
| }; | |
| const req = new Request("http://localhost/dashboard/providers/services/9router/embed/"); | |
| await GET(req, makeFakeParams("9router", [])); | |
| assert.ok(capturedAuth, "authorization header must be present"); | |
| assert.ok(capturedAuth!.startsWith("Bearer "), "authorization must be a Bearer token"); | |
| }); | |
| it("G-05: strips client Authorization before forwarding, injects service key instead", async () => { | |
| registerFake("running", 20130); | |
| let capturedAuth: string | undefined; | |
| globalThis.fetch = async (_input: string | URL | Request, init?: RequestInit) => { | |
| capturedAuth = new Headers(init?.headers as HeadersInit).get("authorization") ?? undefined; | |
| return new Response("ok", { status: 200 }); | |
| }; | |
| const req = new Request("http://localhost/dashboard/providers/services/9router/embed/", { | |
| headers: { authorization: "Bearer client-token-should-not-leak" }, | |
| }); | |
| await GET(req, makeFakeParams("9router", [])); | |
| assert.ok(capturedAuth, "authorization header must be set"); | |
| assert.notEqual( | |
| capturedAuth, | |
| "Bearer client-token-should-not-leak", | |
| "client authorization must not be forwarded as-is" | |
| ); | |
| }); | |
| it("G-05: strips set-cookie from upstream response", async () => { | |
| registerFake("running", 20130); | |
| globalThis.fetch = async () => | |
| new Response("ok", { | |
| status: 200, | |
| headers: { "set-cookie": "session=upstream; Path=/", "content-type": "text/plain" }, | |
| }); | |
| const resp = await GET( | |
| new Request("http://localhost/dashboard/providers/services/9router/embed/"), | |
| makeFakeParams("9router", []) | |
| ); | |
| assert.equal(resp.headers.get("set-cookie"), null, "set-cookie must be stripped from response"); | |
| }); | |
| it("G-05: strips x-frame-options from upstream response", async () => { | |
| registerFake("running", 20130); | |
| globalThis.fetch = async () => | |
| new Response("ok", { | |
| status: 200, | |
| headers: { "x-frame-options": "DENY", "content-type": "text/plain" }, | |
| }); | |
| const resp = await GET( | |
| new Request("http://localhost/dashboard/providers/services/9router/embed/"), | |
| makeFakeParams("9router", []) | |
| ); | |
| assert.equal(resp.headers.get("x-frame-options"), null, "x-frame-options must be stripped"); | |
| }); | |
| it("G-05: strips content-security-policy from upstream response", async () => { | |
| registerFake("running", 20130); | |
| globalThis.fetch = async () => | |
| new Response("ok", { | |
| status: 200, | |
| headers: { | |
| "content-security-policy": "default-src 'none'", | |
| "content-type": "text/plain", | |
| }, | |
| }); | |
| const resp = await GET( | |
| new Request("http://localhost/dashboard/providers/services/9router/embed/"), | |
| makeFakeParams("9router", []) | |
| ); | |
| assert.equal( | |
| resp.headers.get("content-security-policy"), | |
| null, | |
| "content-security-policy must be stripped" | |
| ); | |
| }); | |
| it("G-05: strips cross-origin-* headers from upstream response", async () => { | |
| registerFake("running", 20130); | |
| globalThis.fetch = async () => | |
| new Response("ok", { | |
| status: 200, | |
| headers: { | |
| "cross-origin-embedder-policy": "require-corp", | |
| "cross-origin-opener-policy": "same-origin", | |
| "cross-origin-resource-policy": "same-site", | |
| "content-type": "text/plain", | |
| }, | |
| }); | |
| const resp = await GET( | |
| new Request("http://localhost/dashboard/providers/services/9router/embed/"), | |
| makeFakeParams("9router", []) | |
| ); | |
| assert.equal( | |
| resp.headers.get("cross-origin-embedder-policy"), | |
| null, | |
| "cross-origin-embedder-policy must be stripped" | |
| ); | |
| assert.equal( | |
| resp.headers.get("cross-origin-opener-policy"), | |
| null, | |
| "cross-origin-opener-policy must be stripped" | |
| ); | |
| assert.equal( | |
| resp.headers.get("cross-origin-resource-policy"), | |
| null, | |
| "cross-origin-resource-policy must be stripped" | |
| ); | |
| }); | |
| it("G-05: HTML response is rewritten — contains injected <base href>", async () => { | |
| registerFake("running", 20130); | |
| globalThis.fetch = async () => | |
| new Response('<html><head></head><body><a href="/dashboard">link</a></body></html>', { | |
| status: 200, | |
| headers: { "content-type": "text/html; charset=utf-8" }, | |
| }); | |
| const resp = await GET( | |
| new Request("http://localhost/dashboard/providers/services/9router/embed/"), | |
| makeFakeParams("9router", []) | |
| ); | |
| const body = await resp.text(); | |
| assert.ok( | |
| body.includes('<base href="/dashboard/providers/services/9router/embed/">'), | |
| `Expected <base href> in rewritten HTML. Got: ${body.substring(0, 200)}` | |
| ); | |
| }); | |
| it("G-05: HTML response rewrites path-absolute links to go through proxy", async () => { | |
| registerFake("running", 20130); | |
| globalThis.fetch = async () => | |
| new Response('<html><head></head><body><a href="/ui/page">x</a></body></html>', { | |
| status: 200, | |
| headers: { "content-type": "text/html" }, | |
| }); | |
| const resp = await GET( | |
| new Request("http://localhost/dashboard/providers/services/9router/embed/"), | |
| makeFakeParams("9router", []) | |
| ); | |
| const body = await resp.text(); | |
| assert.ok( | |
| body.includes('href="/dashboard/providers/services/9router/embed/ui/page"'), | |
| `Expected rewritten href. Got: ${body.substring(0, 300)}` | |
| ); | |
| }); | |
| it("G-05: JSON response is NOT rewritten (streaming pass-through)", async () => { | |
| registerFake("running", 20130); | |
| const jsonPayload = '{"models":["gpt-4","claude-3"]}'; | |
| globalThis.fetch = async () => | |
| new Response(jsonPayload, { | |
| status: 200, | |
| headers: { "content-type": "application/json" }, | |
| }); | |
| const resp = await GET( | |
| new Request("http://localhost/dashboard/providers/services/9router/embed/api/models"), | |
| makeFakeParams("9router", ["api", "models"]) | |
| ); | |
| const body = await resp.text(); | |
| assert.equal(body, jsonPayload, "JSON response must pass through unchanged"); | |
| }); | |
| it("G-05: HEAD method is handled", async () => { | |
| registerFake("running", 20130); | |
| globalThis.fetch = async () => | |
| new Response(null, { status: 200, headers: { "content-type": "text/html" } }); | |
| const req = new Request("http://localhost/dashboard/providers/services/9router/embed/", { | |
| method: "HEAD", | |
| }); | |
| const resp = await HEAD(req, makeFakeParams("9router", [])); | |
| assert.equal(resp.status, 200); | |
| }); | |
| it("G-05: OPTIONS method is handled", async () => { | |
| registerFake("running", 20130); | |
| globalThis.fetch = async () => | |
| new Response(null, { | |
| status: 204, | |
| headers: { allow: "GET, HEAD, POST, OPTIONS" }, | |
| }); | |
| const req = new Request("http://localhost/dashboard/providers/services/9router/embed/", { | |
| method: "OPTIONS", | |
| }); | |
| const resp = await OPTIONS(req, makeFakeParams("9router", [])); | |
| assert.equal(resp.status, 204); | |
| }); | |
| }); | |