Skip to content

Commit 07957df

Browse files
authored
fix(test): silence "unexpected fetch call to" warnings in unit tests (#716)
## Summary - Eliminate all 182 `[TEST] Unexpected fetch call to:` warnings from the unit and isolated test suites - Fix incomplete fetch mocking across 11 test files that allowed code paths to reach the global fetch trap in `test/preload.ts` - All 4797 tests continue to pass with zero warnings ## Root Causes Fixed | Category | Files | Fix | |----------|-------|-----| | Missing org region cache | resolve-target-listing, span/view, project/create | Added `setOrgRegion()` in `beforeEach` | | Fire-and-forget `warmOrgCache()` | auth/login, login-reauth | Spied on `listOrganizationsUncached` | | Resolution cascade fall-through | resolve-target, index, help-positional, issue/utils | Added silent fetch mocks or `cwd: "/tmp"` | | Background version check | version-check | Added fetch mock for GitHub API | | Missing per-test fetch mock | resolve-effective-org | Added 401 mock for unauthenticated test | Ref: https://github.com/getsentry/cli/actions/runs/24265638656/job/70864771196
1 parent 8245dec commit 07957df

12 files changed

Lines changed: 187 additions & 86 deletions

AGENTS.md

Lines changed: 42 additions & 79 deletions
Large diffs are not rendered by default.

test/commands/auth/login.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ describe("loginCommand.func --token path", () => {
8787
let setUserInfoSpy: ReturnType<typeof spyOn>;
8888
let runInteractiveLoginSpy: ReturnType<typeof spyOn>;
8989
let hasStoredAuthCredentialsSpy: ReturnType<typeof spyOn>;
90+
let listOrgsUncachedSpy: ReturnType<typeof spyOn>;
9091
let func: LoginFunc;
9192

9293
beforeEach(async () => {
@@ -99,6 +100,11 @@ describe("loginCommand.func --token path", () => {
99100
setUserInfoSpy = spyOn(dbUser, "setUserInfo");
100101
runInteractiveLoginSpy = spyOn(interactiveLogin, "runInteractiveLogin");
101102
hasStoredAuthCredentialsSpy = spyOn(dbAuth, "hasStoredAuthCredentials");
103+
// Prevent warmOrgCache() fire-and-forget from hitting real fetch.
104+
// After successful login, warmOrgCache() calls listOrganizationsUncached()
105+
// which triggers API calls that leak as "unexpected fetch" warnings.
106+
listOrgsUncachedSpy = spyOn(apiClient, "listOrganizationsUncached");
107+
listOrgsUncachedSpy.mockResolvedValue([]);
102108
isEnvTokenActiveSpy.mockReturnValue(false);
103109
hasStoredAuthCredentialsSpy.mockReturnValue(false);
104110
func = (await loginCommand.loader()) as unknown as LoginFunc;
@@ -114,6 +120,7 @@ describe("loginCommand.func --token path", () => {
114120
setUserInfoSpy.mockRestore();
115121
runInteractiveLoginSpy.mockRestore();
116122
hasStoredAuthCredentialsSpy.mockRestore();
123+
listOrgsUncachedSpy.mockRestore();
117124
});
118125

119126
test("already authenticated (non-TTY, no --force): prints re-auth message with --force hint", async () => {

test/commands/issue/utils.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { setAuthToken } from "../../../src/lib/db/auth.js";
1717
import { setCachedProject } from "../../../src/lib/db/project-cache.js";
1818
import { setOrgRegion } from "../../../src/lib/db/regions.js";
1919
import { ApiError, ResolutionError } from "../../../src/lib/errors.js";
20-
import { useTestConfigDir } from "../../helpers.js";
20+
import { mockFetch, useTestConfigDir } from "../../helpers.js";
2121

2222
describe("buildCommandHint", () => {
2323
test("suggests <org>/ID for numeric IDs", () => {
@@ -81,6 +81,12 @@ let originalFetch: typeof globalThis.fetch;
8181

8282
beforeEach(async () => {
8383
originalFetch = globalThis.fetch;
84+
// Default to a silent 404 so tests that don't set a custom fetch mock
85+
// won't produce "unexpected fetch" warnings from the preload trap.
86+
globalThis.fetch = mockFetch(
87+
async () =>
88+
new Response(JSON.stringify({ detail: "Not found" }), { status: 404 })
89+
);
8490
await setAuthToken("test-token");
8591
// Pre-populate region cache for orgs used in tests to avoid region resolution API calls
8692
setOrgRegion("test-org", DEFAULT_SENTRY_URL);

test/commands/project/create.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
import { createCommand } from "../../../src/commands/project/create.js";
1919
// biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking
2020
import * as apiClient from "../../../src/lib/api-client.js";
21+
import { DEFAULT_SENTRY_URL } from "../../../src/lib/constants.js";
22+
import { setOrgRegion } from "../../../src/lib/db/regions.js";
2123
import {
2224
ApiError,
2325
CliError,
@@ -27,6 +29,7 @@ import {
2729
// biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking
2830
import * as resolveTarget from "../../../src/lib/resolve-target.js";
2931
import type { SentryProject, SentryTeam } from "../../../src/types/index.js";
32+
import { useTestConfigDir } from "../../helpers.js";
3033

3134
const sampleTeam: SentryTeam = {
3235
id: "1",
@@ -52,6 +55,10 @@ const sampleProject: SentryProject = {
5255
dateCreated: "2026-02-12T10:00:00Z",
5356
};
5457

58+
// Isolated DB for region cache — prevents "unexpected fetch" warnings
59+
// from resolveOrgRegion when buildOrgNotFoundError calls resolveEffectiveOrg
60+
useTestConfigDir("test-project-create-");
61+
5562
function createMockContext() {
5663
const stdoutWrite = mock(() => true);
5764
return {
@@ -73,6 +80,11 @@ describe("project create", () => {
7380
let resolveOrgSpy: ReturnType<typeof spyOn>;
7481

7582
beforeEach(() => {
83+
// Pre-populate region cache for orgs used in tests to avoid
84+
// "unexpected fetch" warnings from resolveOrgRegion
85+
setOrgRegion("acme-corp", DEFAULT_SENTRY_URL);
86+
setOrgRegion("123", DEFAULT_SENTRY_URL);
87+
7688
listTeamsSpy = spyOn(apiClient, "listTeams");
7789
createProjectSpy = spyOn(apiClient, "createProject");
7890
createTeamSpy = spyOn(apiClient, "createTeam");

test/commands/span/view.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
} from "../../../src/commands/span/view.js";
2121
// biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking
2222
import * as apiClient from "../../../src/lib/api-client.js";
23+
import { DEFAULT_SENTRY_URL } from "../../../src/lib/constants.js";
24+
import { setOrgRegion } from "../../../src/lib/db/regions.js";
2325
import { ContextError, ValidationError } from "../../../src/lib/errors.js";
2426
import { validateSpanId } from "../../../src/lib/hex-id.js";
2527
// biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking
@@ -280,6 +282,7 @@ function makeTraceSpan(spanId: string, children: unknown[] = []): unknown {
280282
describe("viewCommand.func", () => {
281283
let func: ViewFunc;
282284
let getDetailedTraceSpy: ReturnType<typeof spyOn>;
285+
let getSpanDetailsSpy: ReturnType<typeof spyOn>;
283286
let resolveOrgAndProjectSpy: ReturnType<typeof spyOn>;
284287

285288
function createContext() {
@@ -305,15 +308,24 @@ describe("viewCommand.func", () => {
305308
beforeEach(async () => {
306309
func = (await viewCommand.loader()) as unknown as ViewFunc;
307310
getDetailedTraceSpy = spyOn(apiClient, "getDetailedTrace");
311+
getSpanDetailsSpy = spyOn(apiClient, "getSpanDetails").mockResolvedValue({
312+
itemId: "mock-span",
313+
itemType: "span",
314+
attributes: [],
315+
});
308316
resolveOrgAndProjectSpy = spyOn(resolveTarget, "resolveOrgAndProject");
309317
resolveOrgAndProjectSpy.mockResolvedValue({
310318
org: "test-org",
311319
project: "test-project",
312320
});
321+
// Pre-populate org region cache to prevent resolveOrgRegion from fetching
322+
setOrgRegion("test-org", DEFAULT_SENTRY_URL);
323+
setOrgRegion("my-org", DEFAULT_SENTRY_URL);
313324
});
314325

315326
afterEach(() => {
316327
getDetailedTraceSpy.mockRestore();
328+
getSpanDetailsSpy.mockRestore();
317329
resolveOrgAndProjectSpy.mockRestore();
318330
});
319331

test/isolated/login-reauth.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ describe("login re-authentication interactive prompt", () => {
114114
let clearAuthSpy: ReturnType<typeof spyOn>;
115115
let runInteractiveLoginSpy: ReturnType<typeof spyOn>;
116116
let getUserInfoSpy: ReturnType<typeof spyOn>;
117+
let listOrgsUncachedSpy: ReturnType<typeof spyOn>;
117118
let func: LoginFunc;
118119

119120
beforeEach(async () => {
@@ -122,6 +123,11 @@ describe("login re-authentication interactive prompt", () => {
122123
clearAuthSpy = spyOn(dbAuth, "clearAuth");
123124
runInteractiveLoginSpy = spyOn(interactiveLogin, "runInteractiveLogin");
124125
getUserInfoSpy = spyOn(dbUser, "getUserInfo");
126+
// Prevent warmOrgCache() fire-and-forget from hitting real fetch.
127+
// After successful login, warmOrgCache() calls listOrganizationsUncached()
128+
// which triggers API calls that leak as "unexpected fetch" warnings.
129+
listOrgsUncachedSpy = spyOn(apiClient, "listOrganizationsUncached");
130+
listOrgsUncachedSpy.mockResolvedValue([]);
125131

126132
// Defaults
127133
isEnvTokenActiveSpy.mockReturnValue(false);
@@ -139,6 +145,7 @@ describe("login re-authentication interactive prompt", () => {
139145
clearAuthSpy.mockRestore();
140146
runInteractiveLoginSpy.mockRestore();
141147
getUserInfoSpy.mockRestore();
148+
listOrgsUncachedSpy.mockRestore();
142149
});
143150

144151
test("shows prompt with user identity when authenticated on TTY", async () => {

test/lib/help-positional.test.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,33 @@
1212
* and verify help output is shown when resolution fails.
1313
*/
1414

15-
import { describe, expect, test } from "bun:test";
15+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
1616
import { run } from "@stricli/core";
1717
import { app } from "../../src/app.js";
1818
import type { SentryContext } from "../../src/context.js";
19-
import { useTestConfigDir } from "../helpers.js";
19+
import { mockFetch, useTestConfigDir } from "../helpers.js";
2020

2121
useTestConfigDir("help-positional-");
2222

23+
// Silence unmocked fetch calls from the resolution cascade.
24+
// Commands run through run(app, args) with "help" as a positional arg
25+
// trigger real resolution (e.g., findProjectsBySlug("help") → listOrganizations)
26+
// before the help-recovery error handler fires. A silent 404 prevents
27+
// preload warnings while preserving the error → recovery behavior.
28+
let originalFetch: typeof globalThis.fetch;
29+
30+
beforeEach(() => {
31+
originalFetch = globalThis.fetch;
32+
globalThis.fetch = mockFetch(
33+
async () =>
34+
new Response(JSON.stringify({ detail: "Not found" }), { status: 404 })
35+
);
36+
});
37+
38+
afterEach(() => {
39+
globalThis.fetch = originalFetch;
40+
});
41+
2342
/** Captured output from a command run */
2443
type CapturedOutput = {
2544
stdout: string;

test/lib/index.test.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,44 @@
1-
import { describe, expect, test } from "bun:test";
1+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
22
import createSentrySDK, { SentryError } from "../../src/index.js";
3+
import { mockFetch } from "../helpers.js";
34

45
describe("createSentrySDK() library API", () => {
6+
// Silence unmocked fetch calls from resolution cascade.
7+
// SDK tests that call commands like "issue list" or "org list" trigger
8+
// the org/project resolution cascade which hits real API endpoints.
9+
// A silent 404 prevents preload warnings while preserving error behavior.
10+
let originalFetch: typeof globalThis.fetch;
11+
12+
beforeEach(() => {
13+
originalFetch = globalThis.fetch;
14+
// Return empty successes rather than 404s so the resolution cascade
15+
// terminates cleanly without triggering follow-up requests that could
16+
// outlive the test and spill into later test files.
17+
globalThis.fetch = mockFetch(async (input) => {
18+
let url: string;
19+
if (typeof input === "string") {
20+
url = input;
21+
} else if (input instanceof URL) {
22+
url = input.href;
23+
} else {
24+
url = new Request(input).url;
25+
}
26+
if (url.includes("/regions/")) {
27+
return new Response(JSON.stringify({ regions: [] }), { status: 200 });
28+
}
29+
if (url.includes("/organizations/")) {
30+
return new Response(JSON.stringify([]), { status: 200 });
31+
}
32+
// Return empty 200 for all other endpoints (projects, issues, etc.)
33+
// to prevent follow-up requests from outliving the test.
34+
return new Response(JSON.stringify({}), { status: 200 });
35+
});
36+
});
37+
38+
afterEach(() => {
39+
globalThis.fetch = originalFetch;
40+
});
41+
542
test("sdk.run returns version string for --version", async () => {
643
const sdk = createSentrySDK();
744
const result = await sdk.run("--version");
@@ -19,7 +56,9 @@ describe("createSentrySDK() library API", () => {
1956
});
2057

2158
test("sdk.run throws when auth is required but missing", async () => {
22-
const sdk = createSentrySDK();
59+
// Use cwd:/tmp to prevent DSN scanning of the repo root which finds
60+
// real DSNs and triggers async project resolution that can outlive the test.
61+
const sdk = createSentrySDK({ cwd: "/tmp" });
2362
try {
2463
// issue list requires auth — with no token and isolated config, it should fail
2564
await sdk.run("issue", "list");
@@ -43,7 +82,7 @@ describe("createSentrySDK() library API", () => {
4382
});
4483

4584
test("process.env is unchanged after failed call", async () => {
46-
const sdk = createSentrySDK();
85+
const sdk = createSentrySDK({ cwd: "/tmp" });
4786
const envBefore = { ...process.env };
4887
try {
4988
await sdk.run("issue", "list");

test/lib/resolve-effective-org.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,16 @@ describe("resolveEffectiveOrg with API refresh", () => {
241241
const { clearAuth } = await import("../../src/lib/db/auth.js");
242242
await clearAuth();
243243

244+
// Set a silent fetch mock to prevent preload warnings.
245+
// Without auth, resolveEffectiveOrg tries an API refresh that fails,
246+
// then falls back to the original slug.
247+
globalThis.fetch = mockFetch(
248+
async () =>
249+
new Response(JSON.stringify({ detail: "Unauthorized" }), {
250+
status: 401,
251+
})
252+
);
253+
244254
const result = await resolveEffectiveOrg("o1081365");
245255
expect(result).toBe("o1081365");
246256
});

test/lib/resolve-target-listing.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ describe("resolveOrgProjectTarget", () => {
129129
resolveTargetModule,
130130
"resolveOrgAndProject"
131131
);
132+
// Pre-populate org region cache so resolveEffectiveOrg doesn't fetch
133+
setOrgRegion("my-org", DEFAULT_SENTRY_URL);
132134
});
133135

134136
afterEach(() => {

0 commit comments

Comments
 (0)