mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-26 20:45:37 +02:00
* 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>
213 lines
6.2 KiB
TypeScript
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");
|
|
});
|
|
});
|