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:
co-authored by
Claude Opus 5.5
parent
a14c4e7747
commit
06a59e1b5c
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user