fix(tests): resolve mock test failures in PR #3355

Use URL-based mock dispatch instead of sequential callCount thresholds
to prevent telemetry flush from injecting spurious fetch calls that
shift the mock response sequence.

Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
B 2026-05-14 18:54:20 +00:00
parent a359f0234f
commit 8e52e0a608
2 changed files with 124 additions and 103 deletions

View file

@ -88,34 +88,34 @@ 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
// (avoids starting a real Bun.serve OAuth server)
if (urlStr.includes("cloud.digitalocean.com")) {
oauthChecks++;
return Promise.reject(new Error("network unavailable"));
}
// DigitalOcean API calls — 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"));
}
return Promise.resolve(
new Response("Unauthorized", {
status: 401,
}),
);
// Ignore unrelated calls (e.g. telemetry flush)
return Promise.resolve(new Response("ok"));
});
// 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 (API call + OAuth connectivity check + retries)
expect(callCount).toBeGreaterThanOrEqual(2);
// Verify recovery was attempted: 1 API call + 1 connectivity check
expect(apiCalls).toBe(1);
expect(oauthChecks).toBe(1);
});
it("succeeds after OAuth recovery provides a new token", async () => {

View file

@ -585,47 +585,46 @@ describe("hetzner/createServer", () => {
},
},
};
let callCount = 0;
global.fetch = mock(() => {
callCount++;
if (callCount <= 1) {
// Token validation
return Promise.resolve(
new Response(
JSON.stringify({
servers: [],
}),
),
);
let hetznerCalls = 0;
let postServerAttempts = 0;
global.fetch = mock((url: string | URL | Request, opts?: RequestInit) => {
const urlStr = String(url);
// Ignore non-Hetzner calls (e.g. telemetry flush)
if (!urlStr.includes("api.hetzner.cloud")) {
return Promise.resolve(new Response("ok"));
}
if (callCount <= 2) {
// SSH keys
return Promise.resolve(
new Response(
JSON.stringify({
ssh_keys: [],
}),
),
);
}
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",
hetznerCalls++;
const method = opts?.method ?? "GET";
// POST /servers — first attempt fails, retry succeeds
if (method === "POST" && urlStr.includes("/servers")) {
postServerAttempts++;
if (postServerAttempts === 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)));
}
// DELETE /primary_ips — cleanup orphaned IP
if (method === "DELETE" && urlStr.includes("/primary_ips/")) {
return Promise.resolve(
new Response("", {
status: 204,
}),
);
}
if (callCount <= 4) {
// List primary IPs for cleanup
// GET /primary_ips — list IPs for cleanup
if (urlStr.includes("/primary_ips")) {
return Promise.resolve(
new Response(
JSON.stringify({
@ -645,40 +644,8 @@ describe("hetzner/createServer", () => {
),
);
}
if (callCount <= 5) {
// Delete orphaned IP 100
return Promise.resolve(
new Response("", {
status: 204,
}),
);
}
// Retry create — success
return Promise.resolve(new Response(JSON.stringify(serverResp)));
});
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)
expect(callCount).toBeGreaterThanOrEqual(6);
});
it("throws with guidance when resource limit hit and no orphaned IPs to clean", async () => {
process.env.HCLOUD_TOKEN = "test-token";
let callCount = 0;
global.fetch = mock(() => {
callCount++;
if (callCount <= 1) {
return Promise.resolve(
new Response(
JSON.stringify({
servers: [],
}),
),
);
}
if (callCount <= 2) {
// GET /ssh_keys
if (urlStr.includes("/ssh_keys")) {
return Promise.resolve(
new Response(
JSON.stringify({
@ -687,8 +654,39 @@ describe("hetzner/createServer", () => {
),
);
}
if (callCount <= 3) {
// Create fails with resource_limit_exceeded
// GET /servers (token validation)
if (urlStr.includes("/servers")) {
return Promise.resolve(
new Response(
JSON.stringify({
servers: [],
}),
),
);
}
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, ssh_keys, create-fail, list-ips, delete-ip, create-ok
expect(hetznerCalls).toBeGreaterThanOrEqual(6);
});
it("throws with guidance when resource limit hit and no orphaned IPs to clean", async () => {
process.env.HCLOUD_TOKEN = "test-token";
let postServerAttempts = 0;
global.fetch = mock((url: string | URL | Request, opts?: RequestInit) => {
const urlStr = String(url);
// Ignore non-Hetzner calls (e.g. telemetry flush)
if (!urlStr.includes("api.hetzner.cloud")) {
return Promise.resolve(new Response("ok"));
}
const method = opts?.method ?? "GET";
// POST /servers — always fail with resource_limit_exceeded
if (method === "POST" && urlStr.includes("/servers")) {
postServerAttempts++;
return Promise.resolve(
new Response(
JSON.stringify({
@ -703,20 +701,43 @@ describe("hetzner/createServer", () => {
),
);
}
// List primary IPs — all attached (none orphaned)
return Promise.resolve(
new Response(
JSON.stringify({
primary_ips: [
{
id: 100,
ip: "1.2.3.4",
assignee_id: 42,
},
],
}),
),
);
// GET /primary_ips — all attached (none orphaned)
if (urlStr.includes("/primary_ips")) {
return Promise.resolve(
new Response(
JSON.stringify({
primary_ips: [
{
id: 100,
ip: "1.2.3.4",
assignee_id: 42,
},
],
}),
),
);
}
// GET /ssh_keys
if (urlStr.includes("/ssh_keys")) {
return Promise.resolve(
new Response(
JSON.stringify({
ssh_keys: [],
}),
),
);
}
// GET /servers (token validation)
if (urlStr.includes("/servers")) {
return Promise.resolve(
new Response(
JSON.stringify({
servers: [],
}),
),
);
}
return Promise.resolve(new Response(JSON.stringify({})));
});
const { ensureHcloudToken, createServer } = await import("../hetzner/hetzner");
await ensureHcloudToken();