fix(sdk): block real network in test context, strip query from HttpError message

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-30 15:38:45 +01:00
co-authored by Claude Opus 5.5
parent a14c4e7747
commit 06a59e1b5c
3 changed files with 32 additions and 2 deletions
+11 -1
View File
@@ -1,10 +1,20 @@
import type { HttpClient } from "./types.ts";
/** Origin + pathname only: query strings may carry API keys or tokens. */
function safeUrl(url: string): string {
try {
const u = new URL(url);
return u.origin + u.pathname;
} catch {
return url.split(/[?#]/)[0] ?? "";
}
}
export class HttpError extends Error {
readonly status: number;
readonly body: string;
constructor(status: number, body: string, url = "") {
super(`HTTP ${status}${url ? ` for ${url}` : ""}`);
super(`HTTP ${status}${url ? ` for ${safeUrl(url)}` : ""}`);
this.name = "HttpError";
this.status = status;
this.body = body;
+5 -1
View File
@@ -21,6 +21,10 @@ export function createFixtureHttp(routes: Record<string, Route>): { fetch: typeo
return { fetch: fn as typeof fetch, calls };
}
const noFetch = (async () => {
throw new Error("createTestContext: no fetch provided");
}) as unknown as typeof fetch;
export function createTestContext(
partial: Partial<PluginContext> & { fetch?: typeof fetch } = {},
): PluginContext & { saved: { credentials: Record<string, string>[]; cursor: string[] } } {
@@ -29,7 +33,7 @@ export function createTestContext(
let creds: Record<string, string> | null = null;
let cursor: string | null = null;
const ctx: PluginContext = {
http: createHttpClient({ userAgent: "sttracker-test/0", fetchImpl, sleep: async () => {} }),
http: createHttpClient({ userAgent: "sttracker-test/0", fetchImpl: fetchImpl ?? noFetch, sleep: async () => {} }),
log: () => {},
config: {},
credentials: {
+16
View File
@@ -104,6 +104,22 @@ describe("http client", () => {
});
});
describe("safety", () => {
test("HttpError message does not leak the query string", async () => {
const { fetchImpl } = seq(res(404, {}));
const http = createHttpClient({ userAgent: "t/1", fetchImpl, sleep: async () => {} });
const err = (await http.json("https://x.test/a?api_key=SECRET").catch((e) => e)) as HttpError;
expect(err).toBeInstanceOf(HttpError);
expect(err.message).not.toContain("SECRET");
expect(err.message).toContain("https://x.test/a");
});
test("createTestContext without fetch never hits the network", async () => {
const ctx = createTestContext();
await expect(ctx.http.json("https://x.test/a")).rejects.toThrow("createTestContext: no fetch provided");
});
});
describe("testing harness", () => {
test("fixture routes match by full url or url without query; unknown throws", async () => {
const fx = createFixtureHttp({