Skip to content

Commit 976b794

Browse files
MathurAditya724betegonclaude
authored
fix(test): adapt tests for buildCommand auth guard (#615)
## Summary Fixes the ~200 test failures caused by the auth guard added in #611. Meant to be merged into #612 so both the CI hang (dsn-cache timing + telemetry beforeExit) and the auth guard breakage ship together. ## Problem `buildCommand`'s new auth guard calls `getAuthConfig()` before every command. Test environments had no auth token set, so all command tests hit `AuthError("not_authenticated")` before the func body ran. ## Changes **Global fix (test/preload.ts):** - Set a fake `SENTRY_AUTH_TOKEN` in the test preload so the auth guard passes for all tests. Real API calls are blocked by the global fetch mock. **Framework tests (test/lib/command.test.ts):** - Add `auth: false` to all 29 test commands — these test flag handling, telemetry, and output rendering, not authentication. **Auth-specific tests (logout, refresh, whoami, project list):** - Tests that verify unauthenticated behavior or `SENTRY_TOKEN` priority now explicitly save/clear/restore `SENTRY_AUTH_TOKEN`. ## Test plan - 215 tests across the 7 most-affected files pass with 0 failures - `bun test test/commands` — 1209 pass - Lint and typecheck pass --------- Co-authored-by: Miguel Betegón <miguelbetegongarcia@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2b3af8d commit 976b794

13 files changed

Lines changed: 210 additions & 34 deletions

src/lib/telemetry.ts

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ const LIBRARY_EXCLUDED_INTEGRATIONS = new Set([
275275
...EXCLUDED_INTEGRATIONS,
276276
"OnUncaughtException", // process.on('uncaughtException')
277277
"OnUnhandledRejection", // process.on('unhandledRejection')
278-
"ProcessSession", // process.on('beforeExit')
278+
"ProcessSession", // process.on('beforeExit') — anonymous handler, no cleanup
279279
"Http", // diagnostics_channel + trace headers
280280
"NodeFetch", // diagnostics_channel + trace headers
281281
"FunctionToString", // wraps Function.prototype.toString
@@ -355,13 +355,11 @@ export function initSentry(
355355
const libraryMode = options?.libraryMode ?? false;
356356
const environment = getEnv().NODE_ENV ?? "development";
357357

358-
// Close the previous client before re-initializing. LightNodeClient registers
359-
// process.on('beforeExit', ...) listeners for client-report and log flushing
360-
// via startClientReportTracking() and enableLogs. These are only removed by
361-
// calling client.close(). Without this, re-initializing (e.g. initSentry(false)
362-
// in test afterEach) accumulates stale listeners that keep the event loop alive
363-
// after all tests complete, preventing the bun process from exiting.
364-
// close(0) removes the listeners synchronously; we don't need to await the flush.
358+
// Close the previous client to clean up its internal timers and beforeExit
359+
// handlers (client report flusher interval, log flush listener). Without
360+
// this, re-initializing the SDK (e.g., in tests) leaks setInterval handles
361+
// that keep the event loop alive and prevent the process from exiting.
362+
// close(0) removes listeners synchronously; we don't need to await the flush.
365363
Sentry.getClient()?.close(0);
366364

367365
const client = Sentry.init({
@@ -383,10 +381,12 @@ export function initSentry(
383381
},
384382
environment,
385383
// Enable Sentry structured logs for non-exception telemetry (e.g., unexpected input warnings).
386-
// Disabled in library mode — logs use timers/beforeExit that pollute the host process.
387-
enableLogs: !libraryMode,
388-
// Disable client reports in library mode — they use timers/beforeExit.
389-
...(libraryMode && { sendClientReports: false }),
384+
// Disabled when telemetry is off or in library mode — the SDK registers
385+
// beforeExit handlers for log flushing that keep the event loop alive.
386+
enableLogs: enabled && !libraryMode,
387+
// Disable client reports when telemetry is off or in library mode — the SDK
388+
// registers a setInterval + beforeExit handler that keep the event loop alive.
389+
...((libraryMode || !enabled) && { sendClientReports: false }),
390390
// Sample all events for CLI telemetry (low volume)
391391
tracesSampleRate: 1,
392392
sampleRate: 1,
@@ -414,12 +414,7 @@ export function initSentry(
414414
},
415415
});
416416

417-
// Always remove the previous handler on re-init. The removal must happen
418-
// unconditionally — not only when enabled=true — so that calling
419-
// initSentry(false) to disable telemetry (e.g. in test afterEach) actually
420-
// cleans up the handler registered by a prior initSentry(true) call.
421-
// Without this, the stale handler keeps the event loop alive after all tests
422-
// finish, preventing the process from exiting.
417+
// Always remove our own previous handler on re-init.
423418
if (currentBeforeExitHandler) {
424419
process.removeListener("beforeExit", currentBeforeExitHandler);
425420
currentBeforeExitHandler = null;

test/commands/auth/logout.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ describe("logoutCommand.func", () => {
126126
isAuthenticatedSpy.mockReturnValue(true);
127127
isEnvTokenActiveSpy.mockReturnValue(true);
128128
// Set env var directly — getActiveEnvVarName() reads env vars via getEnvToken()
129+
// Clear SENTRY_AUTH_TOKEN so SENTRY_TOKEN takes priority
130+
const savedAuthToken = process.env.SENTRY_AUTH_TOKEN;
131+
delete process.env.SENTRY_AUTH_TOKEN;
129132
process.env.SENTRY_TOKEN = "sntrys_token_456";
130133
const { context } = createContext();
131134

@@ -138,6 +141,9 @@ describe("logoutCommand.func", () => {
138141
expect(msg).toContain("SENTRY_TOKEN");
139142
} finally {
140143
delete process.env.SENTRY_TOKEN;
144+
if (savedAuthToken !== undefined) {
145+
process.env.SENTRY_AUTH_TOKEN = savedAuthToken;
146+
}
141147
}
142148
expect(clearAuthSpy).not.toHaveBeenCalled();
143149
});

test/commands/auth/refresh.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ describe("refreshCommand.func", () => {
8585
test("env token (SENTRY_TOKEN): throws AuthError with SENTRY_TOKEN in message", async () => {
8686
isEnvTokenActiveSpy.mockReturnValue(true);
8787
// Set env var directly — getActiveEnvVarName() reads env vars via getEnvToken()
88+
// Clear SENTRY_AUTH_TOKEN so SENTRY_TOKEN takes priority
89+
const savedAuthToken = process.env.SENTRY_AUTH_TOKEN;
90+
delete process.env.SENTRY_AUTH_TOKEN;
8891
process.env.SENTRY_TOKEN = "sntrys_token_456";
8992

9093
const { context } = createContext();
@@ -99,6 +102,9 @@ describe("refreshCommand.func", () => {
99102
expect((err as AuthError).message).not.toContain("SENTRY_AUTH_TOKEN");
100103
} finally {
101104
delete process.env.SENTRY_TOKEN;
105+
if (savedAuthToken !== undefined) {
106+
process.env.SENTRY_AUTH_TOKEN = savedAuthToken;
107+
}
102108
}
103109

104110
expect(refreshTokenSpy).not.toHaveBeenCalled();

test/commands/auth/whoami.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,26 @@ describe("whoamiCommand.func", () => {
8484
});
8585

8686
describe("unauthenticated", () => {
87+
let getAuthConfigSpy: ReturnType<typeof spyOn>;
88+
let savedAuthToken: string | undefined;
89+
90+
beforeEach(() => {
91+
// Clear env token and mock getAuthConfig so buildCommand's auth guard
92+
// sees no credentials — this tests the unauthenticated path end-to-end.
93+
savedAuthToken = process.env.SENTRY_AUTH_TOKEN;
94+
delete process.env.SENTRY_AUTH_TOKEN;
95+
getAuthConfigSpy = spyOn(dbAuth, "getAuthConfig").mockReturnValue(
96+
undefined
97+
);
98+
});
99+
100+
afterEach(() => {
101+
getAuthConfigSpy.mockRestore();
102+
if (savedAuthToken !== undefined) {
103+
process.env.SENTRY_AUTH_TOKEN = savedAuthToken;
104+
}
105+
});
106+
87107
test("throws AuthError(not_authenticated) when no token stored", async () => {
88108
isAuthenticatedSpy.mockReturnValue(false);
89109

test/commands/project/list.test.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -969,8 +969,17 @@ describe("fetchOrgProjectsSafe", () => {
969969
test("propagates AuthError when not authenticated", async () => {
970970
// Clear auth token so the API client throws AuthError before making any request
971971
await clearAuth();
972-
973-
await expect(fetchOrgProjectsSafe("myorg")).rejects.toThrow(AuthError);
972+
// Also clear env token — preload sets a fake one for the auth guard
973+
const savedAuthToken = process.env.SENTRY_AUTH_TOKEN;
974+
delete process.env.SENTRY_AUTH_TOKEN;
975+
976+
try {
977+
await expect(fetchOrgProjectsSafe("myorg")).rejects.toThrow(AuthError);
978+
} finally {
979+
if (savedAuthToken !== undefined) {
980+
process.env.SENTRY_AUTH_TOKEN = savedAuthToken;
981+
}
982+
}
974983
});
975984
});
976985

@@ -1233,14 +1242,23 @@ describe("handleAutoDetect", () => {
12331242
setDefaults("test-org");
12341243
// Clear auth so getAuthToken() throws AuthError before any fetch
12351244
await clearAuth();
1236-
1237-
await expect(
1238-
handleAutoDetect("/tmp/test-project", {
1239-
limit: 30,
1240-
json: true,
1241-
fresh: false,
1242-
})
1243-
).rejects.toThrow(AuthError);
1245+
// Also clear env token — preload sets a fake one for the auth guard
1246+
const savedAuthToken = process.env.SENTRY_AUTH_TOKEN;
1247+
delete process.env.SENTRY_AUTH_TOKEN;
1248+
1249+
try {
1250+
await expect(
1251+
handleAutoDetect("/tmp/test-project", {
1252+
limit: 30,
1253+
json: true,
1254+
fresh: false,
1255+
})
1256+
).rejects.toThrow(AuthError);
1257+
} finally {
1258+
if (savedAuthToken !== undefined) {
1259+
process.env.SENTRY_AUTH_TOKEN = savedAuthToken;
1260+
}
1261+
}
12441262
});
12451263

12461264
test("slow path: uses full fetch when platform filter is active", async () => {

test/lib/api-client.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,17 @@ function createMockFetch(
107107
describe("401 retry behavior", () => {
108108
// Note: These tests use rawApiRequest which goes to control silo (sentry.io)
109109
// and supports 401 retry with token refresh.
110+
// Clear preload SENTRY_AUTH_TOKEN so the DB-stored token is used.
111+
let savedAuthToken: string | undefined;
112+
beforeEach(() => {
113+
savedAuthToken = process.env.SENTRY_AUTH_TOKEN;
114+
delete process.env.SENTRY_AUTH_TOKEN;
115+
});
116+
afterEach(() => {
117+
if (savedAuthToken !== undefined) {
118+
process.env.SENTRY_AUTH_TOKEN = savedAuthToken;
119+
}
120+
});
110121

111122
test("retries request with new token on 401 response", async () => {
112123
const requests: RequestLog[] = [];

0 commit comments

Comments
 (0)