144 lines
4.9 KiB
TypeScript
144 lines
4.9 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { apiClient, getApiBaseUrl, setApiBaseUrl, subscribeApiBaseUrl } from "./api-client";
|
|
|
|
describe("apiClient runtime base URL", () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
setApiBaseUrl("http://127.0.0.1:3001");
|
|
});
|
|
|
|
it("rewrites requests to the current runtime daemon port", async () => {
|
|
const seenUrls: string[] = [];
|
|
vi.spyOn(globalThis, "fetch").mockImplementation(async (input: RequestInfo | URL) => {
|
|
seenUrls.push(input instanceof Request ? input.url : input.toString());
|
|
return new Response(JSON.stringify({ projects: [] }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
});
|
|
|
|
setApiBaseUrl("http://127.0.0.1:3037/");
|
|
|
|
const { error } = await apiClient.GET("/api/v1/projects");
|
|
|
|
expect(error).toBeUndefined();
|
|
expect(getApiBaseUrl()).toBe("http://127.0.0.1:3037");
|
|
expect(seenUrls).toEqual(["http://127.0.0.1:3037/api/v1/projects"]);
|
|
});
|
|
|
|
it("rebases POSTs without Request-as-init, preserving method, body, and headers", async () => {
|
|
// Regression: `new Request(target, input)` needs the source request's
|
|
// `duplex` getter, which Electron's Chromium lacks — every request with a
|
|
// body threw. The rewrite must copy fields explicitly instead.
|
|
const seen: { url: string; method?: string; body?: string; contentType?: string | null }[] = [];
|
|
vi.spyOn(globalThis, "fetch").mockImplementation(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
const headers = new Headers(init?.headers);
|
|
seen.push({
|
|
url: input instanceof Request ? input.url : input.toString(),
|
|
method: init?.method,
|
|
body: init?.body instanceof ArrayBuffer ? new TextDecoder().decode(init.body) : undefined,
|
|
contentType: headers.get("content-type"),
|
|
});
|
|
return new Response(JSON.stringify({ session: { id: "s1" } }), {
|
|
status: 201,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
});
|
|
|
|
setApiBaseUrl("http://127.0.0.1:3037");
|
|
|
|
const { error } = await apiClient.POST("/api/v1/sessions", {
|
|
body: { projectId: "p1", prompt: "hello" },
|
|
});
|
|
|
|
expect(error).toBeUndefined();
|
|
expect(seen).toHaveLength(1);
|
|
expect(seen[0].url).toBe("http://127.0.0.1:3037/api/v1/sessions");
|
|
expect(seen[0].method).toBe("POST");
|
|
expect(seen[0].contentType).toBe("application/json");
|
|
expect(JSON.parse(seen[0].body ?? "{}")).toEqual({ projectId: "p1", prompt: "hello" });
|
|
});
|
|
|
|
it("skips the rebase when the request already targets the runtime base URL", async () => {
|
|
const seen: (RequestInfo | URL)[] = [];
|
|
vi.spyOn(globalThis, "fetch").mockImplementation(async (input: RequestInfo | URL) => {
|
|
seen.push(input);
|
|
return new Response(JSON.stringify({ projects: [] }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
});
|
|
|
|
// Match the base openapi-fetch built the request against (the dev origin
|
|
// in jsdom), so the rewrite has nothing to do.
|
|
setApiBaseUrl(window.location.origin);
|
|
const { error } = await apiClient.GET("/api/v1/projects");
|
|
|
|
expect(error).toBeUndefined();
|
|
expect(seen).toHaveLength(1);
|
|
// Untouched pass-through: fetch receives the original Request object.
|
|
expect(seen[0]).toBeInstanceOf(Request);
|
|
});
|
|
|
|
it("passes the request through untouched when the base URL is empty", async () => {
|
|
const seen: Request[] = [];
|
|
vi.spyOn(globalThis, "fetch").mockImplementation(async (input: RequestInfo | URL) => {
|
|
seen.push(input as Request);
|
|
return new Response(JSON.stringify({ projects: [] }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
});
|
|
|
|
setApiBaseUrl("");
|
|
|
|
const { error } = await apiClient.GET("/api/v1/projects");
|
|
|
|
expect(error).toBeUndefined();
|
|
expect(getApiBaseUrl()).toBe("");
|
|
// Empty base → no rewrite; openapi-fetch's own request reaches fetch as-is.
|
|
expect(seen).toHaveLength(1);
|
|
expect(seen[0].url).toContain("/api/v1/projects");
|
|
});
|
|
});
|
|
|
|
describe("subscribeApiBaseUrl", () => {
|
|
afterEach(() => {
|
|
setApiBaseUrl("http://127.0.0.1:3001");
|
|
});
|
|
|
|
it("notifies subscribers when the base URL actually changes", () => {
|
|
const listener = vi.fn();
|
|
const unsubscribe = subscribeApiBaseUrl(listener);
|
|
try {
|
|
setApiBaseUrl("http://127.0.0.1:4555");
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
expect(getApiBaseUrl()).toBe("http://127.0.0.1:4555");
|
|
} finally {
|
|
unsubscribe();
|
|
}
|
|
});
|
|
|
|
it("does not notify for a no-op set (same URL, trailing slash included)", () => {
|
|
setApiBaseUrl("http://127.0.0.1:4555");
|
|
const listener = vi.fn();
|
|
const unsubscribe = subscribeApiBaseUrl(listener);
|
|
try {
|
|
setApiBaseUrl("http://127.0.0.1:4555");
|
|
setApiBaseUrl("http://127.0.0.1:4555/");
|
|
expect(listener).not.toHaveBeenCalled();
|
|
} finally {
|
|
unsubscribe();
|
|
}
|
|
});
|
|
|
|
it("stops notifying after unsubscribe", () => {
|
|
const listener = vi.fn();
|
|
subscribeApiBaseUrl(listener)();
|
|
|
|
setApiBaseUrl("http://127.0.0.1:4555");
|
|
|
|
expect(listener).not.toHaveBeenCalled();
|
|
});
|
|
});
|