mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-18 06:20:35 +00:00
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:
parent
5c4ee735bd
commit
fbce56dcd1
2 changed files with 48 additions and 39 deletions
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue