fix(browser): handle IPv6 loopback auth and dedupe fetch auth tests

This commit is contained in:
Peter Steinberger
2026-02-18 13:15:00 +00:00
parent eb775ff24b
commit 28b8101eef
2 changed files with 30 additions and 25 deletions

View File

@@ -31,6 +31,18 @@ vi.mock("./routes/dispatcher.js", () => ({
import { fetchBrowserJson } from "./client-fetch.js";
function stubJsonFetchOk() {
const fetchMock = vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>(
async () =>
new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);
return fetchMock;
}
describe("fetchBrowserJson loopback auth", () => {
beforeEach(() => {
vi.restoreAllMocks();
@@ -49,14 +61,7 @@ describe("fetchBrowserJson loopback auth", () => {
});
it("adds bearer auth for loopback absolute HTTP URLs", async () => {
const fetchMock = vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>(
async () =>
new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);
const fetchMock = stubJsonFetchOk();
const res = await fetchBrowserJson<{ ok: boolean }>("http://127.0.0.1:18888/");
expect(res.ok).toBe(true);
@@ -67,14 +72,7 @@ describe("fetchBrowserJson loopback auth", () => {
});
it("does not inject auth for non-loopback absolute URLs", async () => {
const fetchMock = vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>(
async () =>
new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);
const fetchMock = stubJsonFetchOk();
await fetchBrowserJson<{ ok: boolean }>("http://example.com/");
@@ -84,14 +82,7 @@ describe("fetchBrowserJson loopback auth", () => {
});
it("keeps caller-supplied auth header", async () => {
const fetchMock = vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>(
async () =>
new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);
const fetchMock = stubJsonFetchOk();
await fetchBrowserJson<{ ok: boolean }>("http://localhost:18888/", {
headers: {
@@ -103,4 +94,14 @@ describe("fetchBrowserJson loopback auth", () => {
const headers = new Headers(init?.headers);
expect(headers.get("authorization")).toBe("Bearer caller-token");
});
it("injects auth for IPv6 loopback absolute URLs", async () => {
const fetchMock = stubJsonFetchOk();
await fetchBrowserJson<{ ok: boolean }>("http://[::1]:18888/");
const init = fetchMock.mock.calls[0]?.[1];
const headers = new Headers(init?.headers);
expect(headers.get("authorization")).toBe("Bearer loopback-token");
});
});

View File

@@ -21,7 +21,11 @@ function isAbsoluteHttp(url: string): boolean {
function isLoopbackHttpUrl(url: string): boolean {
try {
const host = new URL(url).hostname.trim().toLowerCase();
return host === "127.0.0.1" || host === "localhost" || host === "::1";
// URL hostnames may keep IPv6 brackets (for example "[::1]"); normalize before checks.
const normalizedHost = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
return (
normalizedHost === "127.0.0.1" || normalizedHost === "localhost" || normalizedHost === "::1"
);
} catch {
return false;
}