Skip to content

Commit 72f00f8

Browse files
committed
test: add command-level tests for env token auth branches
Cover the env-token-aware branches in status, logout, refresh, and login commands to push patch coverage above 80%. - status: 14 tests (env source display, hidden config path, no expiry/refresh for env tokens, credential verification, defaults) - logout: 5 tests (env token blocks clear, correct env var name, fallback to SENTRY_AUTH_TOKEN) - refresh: 6 tests (env token throws, no-refresh-token, success, still-valid, --json output) - login: 1 new test (env token active → remove env var message)
1 parent a846b2c commit 72f00f8

File tree

4 files changed

+691
-1
lines changed

4 files changed

+691
-1
lines changed

test/commands/auth/login.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ function createContext() {
6363

6464
describe("loginCommand.func --token path", () => {
6565
let isAuthenticatedSpy: ReturnType<typeof spyOn>;
66+
let isEnvTokenActiveSpy: ReturnType<typeof spyOn>;
6667
let setAuthTokenSpy: ReturnType<typeof spyOn>;
6768
let getUserRegionsSpy: ReturnType<typeof spyOn>;
6869
let clearAuthSpy: ReturnType<typeof spyOn>;
@@ -73,17 +74,20 @@ describe("loginCommand.func --token path", () => {
7374

7475
beforeEach(async () => {
7576
isAuthenticatedSpy = spyOn(dbAuth, "isAuthenticated");
77+
isEnvTokenActiveSpy = spyOn(dbAuth, "isEnvTokenActive");
7678
setAuthTokenSpy = spyOn(dbAuth, "setAuthToken");
7779
getUserRegionsSpy = spyOn(apiClient, "getUserRegions");
7880
clearAuthSpy = spyOn(dbAuth, "clearAuth");
7981
getCurrentUserSpy = spyOn(apiClient, "getCurrentUser");
8082
setUserInfoSpy = spyOn(dbUser, "setUserInfo");
8183
runInteractiveLoginSpy = spyOn(interactiveLogin, "runInteractiveLogin");
84+
isEnvTokenActiveSpy.mockReturnValue(false);
8285
func = (await loginCommand.loader()) as unknown as LoginFunc;
8386
});
8487

8588
afterEach(() => {
8689
isAuthenticatedSpy.mockRestore();
90+
isEnvTokenActiveSpy.mockRestore();
8791
setAuthTokenSpy.mockRestore();
8892
getUserRegionsSpy.mockRestore();
8993
clearAuthSpy.mockRestore();
@@ -92,7 +96,7 @@ describe("loginCommand.func --token path", () => {
9296
runInteractiveLoginSpy.mockRestore();
9397
});
9498

95-
test("already authenticated: prints message and returns early", async () => {
99+
test("already authenticated (OAuth): prints re-auth message", async () => {
96100
isAuthenticatedSpy.mockResolvedValue(true);
97101

98102
const { context, getStdout } = createContext();
@@ -103,6 +107,18 @@ describe("loginCommand.func --token path", () => {
103107
expect(getCurrentUserSpy).not.toHaveBeenCalled();
104108
});
105109

110+
test("already authenticated (env token): tells user to remove env var", async () => {
111+
isAuthenticatedSpy.mockResolvedValue(true);
112+
isEnvTokenActiveSpy.mockReturnValue(true);
113+
114+
const { context, getStdout } = createContext();
115+
await func.call(context, { timeout: 900 });
116+
117+
expect(getStdout()).toContain("environment variable");
118+
expect(getStdout()).toContain("Remove the env var");
119+
expect(getStdout()).not.toContain("already authenticated");
120+
});
121+
106122
test("--token: stores token, fetches user, writes success", async () => {
107123
isAuthenticatedSpy.mockResolvedValue(false);
108124
setAuthTokenSpy.mockResolvedValue(undefined);

test/commands/auth/logout.test.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* Logout Command Tests
3+
*
4+
* Tests for the logoutCommand func() in src/commands/auth/logout.ts.
5+
* Covers the env-token-aware branches added for headless auth support.
6+
*/
7+
8+
import {
9+
afterEach,
10+
beforeEach,
11+
describe,
12+
expect,
13+
mock,
14+
spyOn,
15+
test,
16+
} from "bun:test";
17+
import { logoutCommand } from "../../../src/commands/auth/logout.js";
18+
// biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking
19+
import * as dbAuth from "../../../src/lib/db/auth.js";
20+
// biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking
21+
import * as dbIndex from "../../../src/lib/db/index.js";
22+
23+
type LogoutFunc = (
24+
this: unknown,
25+
flags: Record<string, never>
26+
) => Promise<void>;
27+
28+
function createContext() {
29+
const stdoutLines: string[] = [];
30+
const context = {
31+
stdout: {
32+
write: mock((s: string) => {
33+
stdoutLines.push(s);
34+
}),
35+
},
36+
stderr: {
37+
write: mock((_s: string) => {
38+
/* no-op */
39+
}),
40+
},
41+
cwd: "/tmp",
42+
setContext: mock((_k: string, _v: unknown) => {
43+
/* no-op */
44+
}),
45+
};
46+
return { context, getStdout: () => stdoutLines.join("") };
47+
}
48+
49+
describe("logoutCommand.func", () => {
50+
let isAuthenticatedSpy: ReturnType<typeof spyOn>;
51+
let isEnvTokenActiveSpy: ReturnType<typeof spyOn>;
52+
let getAuthConfigSpy: ReturnType<typeof spyOn>;
53+
let clearAuthSpy: ReturnType<typeof spyOn>;
54+
let getDbPathSpy: ReturnType<typeof spyOn>;
55+
let func: LogoutFunc;
56+
57+
beforeEach(async () => {
58+
isAuthenticatedSpy = spyOn(dbAuth, "isAuthenticated");
59+
isEnvTokenActiveSpy = spyOn(dbAuth, "isEnvTokenActive");
60+
getAuthConfigSpy = spyOn(dbAuth, "getAuthConfig");
61+
clearAuthSpy = spyOn(dbAuth, "clearAuth");
62+
getDbPathSpy = spyOn(dbIndex, "getDbPath");
63+
64+
clearAuthSpy.mockResolvedValue(undefined);
65+
getDbPathSpy.mockReturnValue("/fake/db/path");
66+
67+
func = (await logoutCommand.loader()) as unknown as LogoutFunc;
68+
});
69+
70+
afterEach(() => {
71+
isAuthenticatedSpy.mockRestore();
72+
isEnvTokenActiveSpy.mockRestore();
73+
getAuthConfigSpy.mockRestore();
74+
clearAuthSpy.mockRestore();
75+
getDbPathSpy.mockRestore();
76+
});
77+
78+
test("not authenticated: prints message and returns", async () => {
79+
isAuthenticatedSpy.mockResolvedValue(false);
80+
81+
const { context, getStdout } = createContext();
82+
await func.call(context, {});
83+
84+
expect(getStdout()).toContain("Not currently authenticated");
85+
expect(clearAuthSpy).not.toHaveBeenCalled();
86+
});
87+
88+
test("OAuth token: clears auth and shows success", async () => {
89+
isAuthenticatedSpy.mockResolvedValue(true);
90+
isEnvTokenActiveSpy.mockReturnValue(false);
91+
92+
const { context, getStdout } = createContext();
93+
await func.call(context, {});
94+
95+
expect(clearAuthSpy).toHaveBeenCalled();
96+
expect(getStdout()).toContain("Logged out successfully");
97+
expect(getStdout()).toContain("/fake/db/path");
98+
});
99+
100+
test("env token (SENTRY_AUTH_TOKEN): does not clear auth, shows env var message", async () => {
101+
isAuthenticatedSpy.mockResolvedValue(true);
102+
isEnvTokenActiveSpy.mockReturnValue(true);
103+
getAuthConfigSpy.mockReturnValue({
104+
token: "sntrys_env_123",
105+
source: "env:SENTRY_AUTH_TOKEN",
106+
});
107+
108+
const { context, getStdout } = createContext();
109+
await func.call(context, {});
110+
111+
expect(clearAuthSpy).not.toHaveBeenCalled();
112+
expect(getStdout()).toContain("SENTRY_AUTH_TOKEN");
113+
expect(getStdout()).toContain("environment variable");
114+
expect(getStdout()).toContain("Unset");
115+
});
116+
117+
test("env token (SENTRY_TOKEN): shows correct env var name", async () => {
118+
isAuthenticatedSpy.mockResolvedValue(true);
119+
isEnvTokenActiveSpy.mockReturnValue(true);
120+
getAuthConfigSpy.mockReturnValue({
121+
token: "sntrys_token_456",
122+
source: "env:SENTRY_TOKEN",
123+
});
124+
125+
const { context, getStdout } = createContext();
126+
await func.call(context, {});
127+
128+
expect(clearAuthSpy).not.toHaveBeenCalled();
129+
expect(getStdout()).toContain("SENTRY_TOKEN");
130+
expect(getStdout()).not.toContain("SENTRY_AUTH_TOKEN");
131+
});
132+
133+
test("env token: falls back to SENTRY_AUTH_TOKEN if source is unexpected", async () => {
134+
isAuthenticatedSpy.mockResolvedValue(true);
135+
isEnvTokenActiveSpy.mockReturnValue(true);
136+
// Simulate edge case: source doesn't start with "env:" prefix
137+
getAuthConfigSpy.mockReturnValue({
138+
token: "sntrys_token",
139+
source: "oauth",
140+
});
141+
142+
const { context, getStdout } = createContext();
143+
await func.call(context, {});
144+
145+
// Falls back to "SENTRY_AUTH_TOKEN" as default
146+
expect(getStdout()).toContain("SENTRY_AUTH_TOKEN");
147+
});
148+
});

test/commands/auth/refresh.test.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* Refresh Command Tests
3+
*
4+
* Tests for the refreshCommand func() in src/commands/auth/refresh.ts.
5+
* Covers the env-token guard and the main refresh flow.
6+
*/
7+
8+
import {
9+
afterEach,
10+
beforeEach,
11+
describe,
12+
expect,
13+
mock,
14+
spyOn,
15+
test,
16+
} from "bun:test";
17+
import { refreshCommand } from "../../../src/commands/auth/refresh.js";
18+
// biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking
19+
import * as dbAuth from "../../../src/lib/db/auth.js";
20+
import { AuthError } from "../../../src/lib/errors.js";
21+
22+
type RefreshFlags = { readonly json: boolean; readonly force: boolean };
23+
type RefreshFunc = (this: unknown, flags: RefreshFlags) => Promise<void>;
24+
25+
function createContext() {
26+
const stdoutLines: string[] = [];
27+
const context = {
28+
stdout: {
29+
write: mock((s: string) => {
30+
stdoutLines.push(s);
31+
}),
32+
},
33+
stderr: {
34+
write: mock((_s: string) => {
35+
/* no-op */
36+
}),
37+
},
38+
cwd: "/tmp",
39+
setContext: mock((_k: string, _v: unknown) => {
40+
/* no-op */
41+
}),
42+
};
43+
return { context, getStdout: () => stdoutLines.join("") };
44+
}
45+
46+
describe("refreshCommand.func", () => {
47+
let isEnvTokenActiveSpy: ReturnType<typeof spyOn>;
48+
let getAuthConfigSpy: ReturnType<typeof spyOn>;
49+
let refreshTokenSpy: ReturnType<typeof spyOn>;
50+
let func: RefreshFunc;
51+
52+
beforeEach(async () => {
53+
isEnvTokenActiveSpy = spyOn(dbAuth, "isEnvTokenActive");
54+
getAuthConfigSpy = spyOn(dbAuth, "getAuthConfig");
55+
refreshTokenSpy = spyOn(dbAuth, "refreshToken");
56+
func = (await refreshCommand.loader()) as unknown as RefreshFunc;
57+
});
58+
59+
afterEach(() => {
60+
isEnvTokenActiveSpy.mockRestore();
61+
getAuthConfigSpy.mockRestore();
62+
refreshTokenSpy.mockRestore();
63+
});
64+
65+
test("env token active: throws AuthError with descriptive message", async () => {
66+
isEnvTokenActiveSpy.mockReturnValue(true);
67+
68+
const { context } = createContext();
69+
70+
try {
71+
await func.call(context, { json: false, force: false });
72+
expect.unreachable("should have thrown");
73+
} catch (err) {
74+
expect(err).toBeInstanceOf(AuthError);
75+
expect((err as AuthError).message).toContain(
76+
"Cannot refresh an environment variable token"
77+
);
78+
}
79+
80+
expect(refreshTokenSpy).not.toHaveBeenCalled();
81+
});
82+
83+
test("no refresh token: throws AuthError about missing refresh token", async () => {
84+
isEnvTokenActiveSpy.mockReturnValue(false);
85+
getAuthConfigSpy.mockReturnValue({
86+
token: "manual_token",
87+
source: "oauth",
88+
});
89+
90+
const { context } = createContext();
91+
92+
try {
93+
await func.call(context, { json: false, force: false });
94+
expect.unreachable("should have thrown");
95+
} catch (err) {
96+
expect(err).toBeInstanceOf(AuthError);
97+
expect((err as AuthError).message).toContain("No refresh token");
98+
}
99+
});
100+
101+
test("successful refresh: shows success message", async () => {
102+
isEnvTokenActiveSpy.mockReturnValue(false);
103+
getAuthConfigSpy.mockReturnValue({
104+
token: "old_token",
105+
source: "oauth",
106+
refreshToken: "refresh_abc",
107+
});
108+
refreshTokenSpy.mockResolvedValue({
109+
token: "new_token",
110+
refreshed: true,
111+
expiresIn: 3600,
112+
expiresAt: Date.now() + 3_600_000,
113+
});
114+
115+
const { context, getStdout } = createContext();
116+
await func.call(context, { json: false, force: false });
117+
118+
expect(getStdout()).toContain("Token refreshed successfully");
119+
expect(getStdout()).toContain("1 hour");
120+
});
121+
122+
test("token still valid: shows still-valid message", async () => {
123+
isEnvTokenActiveSpy.mockReturnValue(false);
124+
getAuthConfigSpy.mockReturnValue({
125+
token: "current_token",
126+
source: "oauth",
127+
refreshToken: "refresh_abc",
128+
});
129+
refreshTokenSpy.mockResolvedValue({
130+
token: "current_token",
131+
refreshed: false,
132+
expiresIn: 1800,
133+
});
134+
135+
const { context, getStdout } = createContext();
136+
await func.call(context, { json: false, force: false });
137+
138+
expect(getStdout()).toContain("Token still valid");
139+
expect(getStdout()).toContain("--force");
140+
});
141+
142+
test("--json: outputs JSON for successful refresh", async () => {
143+
isEnvTokenActiveSpy.mockReturnValue(false);
144+
getAuthConfigSpy.mockReturnValue({
145+
token: "old_token",
146+
source: "oauth",
147+
refreshToken: "refresh_abc",
148+
});
149+
refreshTokenSpy.mockResolvedValue({
150+
token: "new_token",
151+
refreshed: true,
152+
expiresIn: 3600,
153+
expiresAt: Date.now() + 3_600_000,
154+
});
155+
156+
const { context, getStdout } = createContext();
157+
await func.call(context, { json: true, force: false });
158+
159+
const parsed = JSON.parse(getStdout());
160+
expect(parsed.success).toBe(true);
161+
expect(parsed.refreshed).toBe(true);
162+
expect(parsed.expiresIn).toBe(3600);
163+
});
164+
});

0 commit comments

Comments
 (0)