// CRITICAL import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { NextRequest } from "next/server"; import { GET } from "./route"; import { getUpstreamTimeoutMs } from "./proxy-timeouts"; import { getApiSettings } from "@/lib/api-settings"; vi.mock("@/lib/api-settings", () => ({ getApiSettings: vi.fn(), })); const getApiSettingsMock = vi.mocked(getApiSettings); const ALLOWLIST_ENV_KEY = "VLLM_STUDIO_PROXY_OVERRIDE_ALLOWLIST"; describe("proxy upstream timeouts", () => { it("gives slow status/log/metrics endpoints enough time", () => { expect(getUpstreamTimeoutMs(["logs"])).toBe(20_000); expect(getUpstreamTimeoutMs(["logs", "session", "stream"])).toBe(20_000); expect(getUpstreamTimeoutMs(["v1", "metrics", "vllm"])).toBe(20_000); expect(getUpstreamTimeoutMs(["status"])).toBe(5_000); }); }); describe("GET /api/proxy/[...path]", () => { beforeEach(() => { vi.restoreAllMocks(); delete process.env[ALLOWLIST_ENV_KEY]; getApiSettingsMock.mockResolvedValue({ backendUrl: "https://api.example.test", apiKey: "test-key", voiceUrl: "", voiceModel: "whisper-large-v3-turbo", }); }); afterEach(() => { delete process.env[ALLOWLIST_ENV_KEY]; }); it("falls back to configured backend when cookie override returns plain-text 404", async () => { const upstreamFetch = vi .fn() .mockResolvedValueOnce( new Response("not found", { status: 404, headers: { "Content-Type": "text/plain; charset=utf-8" }, }), ) .mockResolvedValueOnce( new Response(JSON.stringify({ running: false, process: null, inference_port: 8000 }), { status: 200, headers: { "Content-Type": "application/json" }, }), ); vi.stubGlobal("fetch", upstreamFetch); const request = new NextRequest("http://localhost/api/proxy/status", { method: "GET", headers: { Cookie: "vllmstudio_backend_url=https%3A%2F%2Foverride.example.com%3A8080", }, }); const response = await GET(request, { params: Promise.resolve({ path: ["status"] }) }); expect(response.status).toBe(200); expect(response.headers.get("x-backend-override-invalid")).toBe("1"); expect(response.headers.get("set-cookie")).toContain("vllmstudio_backend_url="); const payload = await response.json(); expect(payload.running).toBe(false); expect(upstreamFetch).toHaveBeenCalledTimes(2); expect(upstreamFetch.mock.calls[0]?.[0]).toBe("https://override.example.com:8080/status"); expect(upstreamFetch.mock.calls[1]?.[0]).toBe("https://api.example.test/status"); }); it("falls back to configured backend when override request throws a network error", async () => { const upstreamFetch = vi .fn() .mockRejectedValueOnce(new Error("connect ECONNREFUSED")) .mockResolvedValueOnce( new Response(JSON.stringify({ status: "ok" }), { status: 200, headers: { "Content-Type": "application/json" }, }), ); vi.stubGlobal("fetch", upstreamFetch); const request = new NextRequest("http://localhost/api/proxy/health", { method: "GET", headers: { Cookie: "vllmstudio_backend_url=https%3A%2F%2Foverride.example.com%3A8080", }, }); const response = await GET(request, { params: Promise.resolve({ path: ["health"] }) }); expect(response.status).toBe(200); expect(response.headers.get("x-backend-override-invalid")).toBe("1"); expect(response.headers.get("set-cookie")).toContain("vllmstudio_backend_url="); const payload = await response.json(); expect(payload.status).toBe("ok"); expect(upstreamFetch).toHaveBeenCalledTimes(2); expect(upstreamFetch.mock.calls[0]?.[0]).toBe("https://override.example.com:8080/health"); expect(upstreamFetch.mock.calls[1]?.[0]).toBe("https://api.example.test/health"); }); it("blocks private network override URLs provided via header when not allowlisted", async () => { const upstreamFetch = vi.fn(); vi.stubGlobal("fetch", upstreamFetch); const request = new NextRequest("http://localhost/api/proxy/status", { method: "GET", headers: { "X-Backend-Url": "http://10.0.0.10:8080", }, }); const response = await GET(request, { params: Promise.resolve({ path: ["status"] }) }); expect(response.status).toBe(403); expect(response.headers.get("x-backend-override-invalid")).toBe("1"); expect(response.headers.get("set-cookie")).toContain("vllmstudio_backend_url="); expect(upstreamFetch).not.toHaveBeenCalled(); }); it("ignores blocked private cookie override and uses configured backend", async () => { const upstreamFetch = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ status: "ok" }), { status: 200, headers: { "Content-Type": "application/json" }, }), ); vi.stubGlobal("fetch", upstreamFetch); const request = new NextRequest("http://localhost/api/proxy/health", { method: "GET", headers: { Cookie: "vllmstudio_backend_url=http%3A%2F%2F10.0.0.10%3A8080", }, }); const response = await GET(request, { params: Promise.resolve({ path: ["health"] }) }); expect(response.status).toBe(200); expect(response.headers.get("x-backend-override-invalid")).toBe("1"); expect(response.headers.get("set-cookie")).toContain("vllmstudio_backend_url="); expect(upstreamFetch).toHaveBeenCalledTimes(1); expect(upstreamFetch.mock.calls[0]?.[0]).toBe("https://api.example.test/health"); }); it("allows private network override URLs when allowlisted", async () => { process.env[ALLOWLIST_ENV_KEY] = "http://10.0.0.10:8080"; const upstreamFetch = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ running: true }), { status: 200, headers: { "Content-Type": "application/json" }, }), ); vi.stubGlobal("fetch", upstreamFetch); const request = new NextRequest("http://localhost/api/proxy/status", { method: "GET", headers: { "X-Backend-Url": "http://10.0.0.10:8080", }, }); const response = await GET(request, { params: Promise.resolve({ path: ["status"] }) }); expect(response.status).toBe(200); expect(upstreamFetch).toHaveBeenCalledTimes(1); expect(upstreamFetch.mock.calls[0]?.[0]).toBe("http://10.0.0.10:8080/status"); }); it("uses override when it succeeds and does not fallback", async () => { const upstreamFetch = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ running: true }), { status: 200, headers: { "Content-Type": "application/json" }, }), ); vi.stubGlobal("fetch", upstreamFetch); const request = new NextRequest("http://localhost/api/proxy/status", { method: "GET", headers: { "X-Backend-Url": "https://override.example.com", }, }); const response = await GET(request, { params: Promise.resolve({ path: ["status"] }) }); expect(response.status).toBe(200); expect(response.headers.get("x-backend-override-invalid")).toBeNull(); const payload = await response.json(); expect(payload.running).toBe(true); expect(upstreamFetch).toHaveBeenCalledTimes(1); expect(upstreamFetch).toHaveBeenCalledWith( "https://override.example.com/status", expect.objectContaining({ method: "GET" }), ); }); });