fix(tests): make flaky mocks URL-aware to prevent cross-test contamination

Position-dependent fetch mocks in hetzner-cov and digitalocean-token tests
broke when running in the full suite due to leaked async operations from
other test files shifting the call count. Switch to URL-based routing so
responses are correct regardless of call order.

Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
B 2026-05-10 18:28:06 +00:00
parent 5c4ee735bd
commit fbce56dcd1
2 changed files with 48 additions and 39 deletions

View file

@ -88,34 +88,37 @@ describe("doApi 401 OAuth recovery", () => {
it("attempts OAuth recovery on 401 before throwing", async () => {
state.token = "expired-token";
let callCount = 0;
let apiCalls = 0;
let oauthChecks = 0;
globalThis.fetch = mock((url: string | URL | Request) => {
callCount++;
const urlStr = String(url);
// First call: the actual API call returning 401
if (callCount === 1) {
// OAuth connectivity check — fail it so tryDoOAuth returns null quickly
if (urlStr.includes("cloud.digitalocean.com")) {
oauthChecks++;
return Promise.reject(new Error("network unavailable"));
}
// API calls to DigitalOcean API — always return 401
if (urlStr.includes("api.digitalocean.com")) {
apiCalls++;
return Promise.resolve(
new Response("Unauthorized", {
status: 401,
}),
);
}
// Second call: OAuth connectivity check — fail it so tryDoOAuth returns null quickly
// (avoids starting a real Bun.serve OAuth server)
if (urlStr.includes("cloud.digitalocean.com")) {
return Promise.reject(new Error("network unavailable"));
}
// Fallback for any other calls (e.g. from prior test leaks)
return Promise.resolve(
new Response("Unauthorized", {
status: 401,
new Response("{}", {
status: 200,
}),
);
});
// OAuth recovery fails (connectivity check fails), so doApi throws the 401
await expect(doApi("GET", "/account", undefined, 1)).rejects.toThrow("DigitalOcean API error 401");
// Verify recovery was attempted: 1 API call + 1 connectivity check = 2
expect(callCount).toBe(2);
// Verify recovery was attempted: at least 1 API call + 1 connectivity check
expect(apiCalls).toBeGreaterThanOrEqual(1);
expect(oauthChecks).toBeGreaterThanOrEqual(1);
});
it("succeeds after OAuth recovery provides a new token", async () => {

View file

@ -585,11 +585,13 @@ describe("hetzner/createServer", () => {
},
},
};
let createAttempts = 0;
let callCount = 0;
global.fetch = mock(() => {
global.fetch = mock((url: string | URL | Request) => {
callCount++;
if (callCount <= 1) {
// Token validation
const urlStr = String(url);
// Token validation — GET /servers?per_page=1
if (urlStr.includes("/servers") && urlStr.includes("per_page=1") && !urlStr.includes("per_page=50")) {
return Promise.resolve(
new Response(
JSON.stringify({
@ -598,8 +600,8 @@ describe("hetzner/createServer", () => {
),
);
}
if (callCount <= 2) {
// SSH keys
// SSH keys
if (urlStr.includes("/ssh_keys")) {
return Promise.resolve(
new Response(
JSON.stringify({
@ -608,24 +610,28 @@ describe("hetzner/createServer", () => {
),
);
}
if (callCount <= 3) {
// First create attempt — resource_limit_exceeded (HTTP 403)
return Promise.resolve(
new Response(
JSON.stringify({
error: {
code: "resource_limit_exceeded",
message: "primary_ip_limit",
// Create server (POST /servers) — first attempt fails, retry succeeds
if (urlStr.endsWith("/servers")) {
createAttempts++;
if (createAttempts <= 1) {
return Promise.resolve(
new Response(
JSON.stringify({
error: {
code: "resource_limit_exceeded",
message: "primary_ip_limit",
},
}),
{
status: 403,
},
}),
{
status: 403,
},
),
);
),
);
}
return Promise.resolve(new Response(JSON.stringify(serverResp)));
}
if (callCount <= 4) {
// List primary IPs for cleanup
// List primary IPs for cleanup
if (urlStr.includes("/primary_ips") && !urlStr.includes("/primary_ips/")) {
return Promise.resolve(
new Response(
JSON.stringify({
@ -645,22 +651,22 @@ describe("hetzner/createServer", () => {
),
);
}
if (callCount <= 5) {
// Delete orphaned IP 100
// Delete orphaned IP
if (urlStr.includes("/primary_ips/")) {
return Promise.resolve(
new Response("", {
status: 204,
}),
);
}
// Retry create — success
return Promise.resolve(new Response(JSON.stringify(serverResp)));
// Fallback
return Promise.resolve(new Response(JSON.stringify({})));
});
const { ensureHcloudToken, createServer } = await import("../hetzner/hetzner");
await ensureHcloudToken();
const conn = await createServer("test-retry", "cx23", "fsn1");
expect(conn.ip).toBe("10.0.0.5");
// Should have called: token(1), ssh_keys(2), create-fail(3), list-ips(4), delete-ip(5), create-ok(6)
// Should have called: token, ssh_keys, create-fail, list-ips, delete-ip, create-ok (at least 6)
expect(callCount).toBeGreaterThanOrEqual(6);
});