From fbce56dcd1674e6a488f8ef1786032a346c4f6be Mon Sep 17 00:00:00 2001 From: B <6723574+louisgv@users.noreply.github.com> Date: Sun, 10 May 2026 18:28:06 +0000 Subject: [PATCH] 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 --- .../src/__tests__/digitalocean-token.test.ts | 29 +++++----- .../cli/src/__tests__/hetzner-cov.test.ts | 58 ++++++++++--------- 2 files changed, 48 insertions(+), 39 deletions(-) diff --git a/packages/cli/src/__tests__/digitalocean-token.test.ts b/packages/cli/src/__tests__/digitalocean-token.test.ts index e0d32912..d8b746d4 100644 --- a/packages/cli/src/__tests__/digitalocean-token.test.ts +++ b/packages/cli/src/__tests__/digitalocean-token.test.ts @@ -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 () => { diff --git a/packages/cli/src/__tests__/hetzner-cov.test.ts b/packages/cli/src/__tests__/hetzner-cov.test.ts index ed1c050f..87aac3d5 100644 --- a/packages/cli/src/__tests__/hetzner-cov.test.ts +++ b/packages/cli/src/__tests__/hetzner-cov.test.ts @@ -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); });