Files
multica/apps/web/config/runtime-urls.test.ts
苗大 a90aa92d0c fix(web): resolve API and docs upstreams at runtime (#4840)
* fix(web): resolve upstream URLs at runtime

* fix(web): keep unconfigured upstreams same-origin

* fix(web): restore dev-only localhost fallbacks for API and docs upstreams

`next dev` on a developer machine now falls back to the conventional
http://localhost:8080 backend (honoring BACKEND_PORT) and
http://localhost:4000 docs origin when nothing is configured, so a bare
`pnpm dev:web` keeps proxying out of the box. Builds and the runtime
proxy keep the strict resolvers, so prebuilt images still leave unset
upstreams unproxied.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 11:02:46 +08:00

213 lines
6.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
resolveBrowserApiBaseUrl,
resolveBrowserWsUrl,
resolveDevDocsUrl,
resolveDevRemoteApiUrl,
resolveDocsUrl,
resolveRemoteApiUrl,
runtimeRewriteDestination,
} from "./runtime-urls";
describe("resolveRemoteApiUrl", () => {
it("prefers REMOTE_API_URL when explicitly configured", () => {
expect(
resolveRemoteApiUrl({
REMOTE_API_URL: "http://backend:8080",
NEXT_PUBLIC_API_URL: "http://localhost:19000",
PORT: "18080",
}),
).toBe("http://backend:8080");
});
it("uses NEXT_PUBLIC_API_URL when REMOTE_API_URL is unset", () => {
expect(
resolveRemoteApiUrl({
NEXT_PUBLIC_API_URL: "http://localhost:19000",
PORT: "18080",
}),
).toBe("http://localhost:19000");
});
it("does not infer a backend URL from frontend or backend port env vars", () => {
expect(
resolveRemoteApiUrl({
BACKEND_PORT: "28080",
API_PORT: "38080",
SERVER_PORT: "48080",
PORT: "3000",
}),
).toBeUndefined();
expect(resolveRemoteApiUrl({ PORT: "3000" })).toBeUndefined();
});
it("does not use relative public API URLs for server-side rewrites", () => {
expect(
resolveRemoteApiUrl({
NEXT_PUBLIC_API_URL: "/api",
}),
).toBeUndefined();
});
it("ignores whitespace-only or invalid backend URL values", () => {
expect(
resolveRemoteApiUrl({
REMOTE_API_URL: " ",
NEXT_PUBLIC_API_URL: "ftp://api.example.com",
PORT: "19080",
}),
).toBeUndefined();
});
it("returns undefined when no API origin is configured", () => {
expect(resolveRemoteApiUrl({})).toBeUndefined();
});
});
describe("resolveDocsUrl", () => {
it("uses DOCS_URL when configured", () => {
expect(resolveDocsUrl({ DOCS_URL: " http://docs:4000/ " })).toBe(
"http://docs:4000",
);
});
it("returns undefined when no docs origin is configured", () => {
expect(resolveDocsUrl({})).toBeUndefined();
});
it("ignores relative or invalid docs URL values", () => {
expect(resolveDocsUrl({ DOCS_URL: "/docs" })).toBeUndefined();
expect(resolveDocsUrl({ DOCS_URL: "ftp://docs.example.com" })).toBeUndefined();
});
});
describe("browser runtime URLs", () => {
it("exposes NEXT_PUBLIC_API_URL at server render time", () => {
expect(
resolveBrowserApiBaseUrl({
NEXT_PUBLIC_API_URL: " https://api.example.com/ ",
}),
).toBe("https://api.example.com");
});
it("derives browser websocket URL from the public API URL", () => {
expect(
resolveBrowserWsUrl({
NEXT_PUBLIC_API_URL: "https://api.example.com/base",
}),
).toBe("wss://api.example.com/base/ws");
});
it("prefers an explicit browser websocket URL", () => {
expect(
resolveBrowserWsUrl({
NEXT_PUBLIC_API_URL: "https://api.example.com",
NEXT_PUBLIC_WS_URL: " wss://ws.example.com/socket/ ",
}),
).toBe("wss://ws.example.com/socket");
});
it("falls back to same-origin websocket derivation for relative public API URLs", () => {
expect(
resolveBrowserWsUrl({
NEXT_PUBLIC_API_URL: "/api",
}),
).toBeUndefined();
});
});
describe("runtimeRewriteDestination", () => {
it("keeps same-origin fallback when no runtime upstreams are configured", () => {
expect(runtimeRewriteDestination("/api/config", {})).toBeUndefined();
expect(runtimeRewriteDestination("/auth/send-code", {})).toBeUndefined();
expect(
runtimeRewriteDestination("/uploads/workspaces/a.png", {}),
).toBeUndefined();
expect(runtimeRewriteDestination("/ws", {})).toBeUndefined();
expect(runtimeRewriteDestination("/docs/zh", {})).toBeUndefined();
});
it("keeps same-origin fallback for runtime API paths when only frontend PORT is configured", () => {
expect(
runtimeRewriteDestination("/api/config", {
PORT: "3000",
}),
).toBeUndefined();
});
it("does not rewrite runtime API paths to relative public API URLs", () => {
expect(
runtimeRewriteDestination("/api/config", {
NEXT_PUBLIC_API_URL: "/api",
}),
).toBeUndefined();
});
it("maps backend HTTP paths to the runtime API origin", () => {
expect(
runtimeRewriteDestination("/api/config", {
REMOTE_API_URL: "http://backend:8080",
}),
).toBe("http://backend:8080/api/config");
expect(
runtimeRewriteDestination("/auth/send-code", {
REMOTE_API_URL: "http://backend:8080",
}),
).toBe("http://backend:8080/auth/send-code");
expect(
runtimeRewriteDestination("/uploads/workspaces/a.png", {
REMOTE_API_URL: "http://backend:8080",
}),
).toBe("http://backend:8080/uploads/workspaces/a.png");
});
it("does not rewrite frontend auth callback pages", () => {
expect(runtimeRewriteDestination("/auth/callback", {})).toBeUndefined();
expect(
runtimeRewriteDestination("/auth/hg-sso/callback", {}),
).toBeUndefined();
});
it("maps docs paths to the runtime docs origin", () => {
expect(
runtimeRewriteDestination("/docs/zh/agents", {
DOCS_URL: "http://multica-docs:3000",
}),
).toBe("http://multica-docs:3000/docs/zh/agents");
});
it("maps websocket paths to the runtime API origin", () => {
expect(
runtimeRewriteDestination("/ws", {
REMOTE_API_URL: "http://backend:8080",
}),
).toBe("http://backend:8080/ws");
});
});
describe("dev-only fallbacks", () => {
it("falls back to the conventional local backend port", () => {
expect(resolveDevRemoteApiUrl({})).toBe("http://localhost:8080");
});
it("honors BACKEND_PORT for the dev backend fallback", () => {
expect(resolveDevRemoteApiUrl({ BACKEND_PORT: "19080" })).toBe(
"http://localhost:19080",
);
});
it("prefers configured origins over the dev fallbacks", () => {
expect(
resolveDevRemoteApiUrl({ REMOTE_API_URL: "http://backend:8080" }),
).toBe("http://backend:8080");
expect(resolveDevDocsUrl({ DOCS_URL: "http://docs:4000" })).toBe(
"http://docs:4000",
);
});
it("falls back to the local docs port", () => {
expect(resolveDevDocsUrl({})).toBe("http://localhost:4000");
});
});