Skip to content

Commit 3d628d4

Browse files
committed
test(auth): cover every taskless auth status outcome
1 parent ca3667d commit 3d628d4

3 files changed

Lines changed: 122 additions & 28 deletions

File tree

‎packages/cli/src/auth/status.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { V2Outcome, WhoamiResult } from "../api/v2";
2+
import { isEnvironmentToken, rejectedTokenRemedy } from "./token";
3+
4+
/**
5+
* The lines `taskless auth` prints for a token, given the service's answer
6+
* about it.
7+
*
8+
* A rejected token and an unreachable service read differently on purpose: the
9+
* first names its fix, the second says nothing about the token, which may be
10+
* fine.
11+
*/
12+
export function describeAuthStatus(
13+
outcome: V2Outcome<WhoamiResult, never>
14+
): string[] {
15+
const source = isEnvironmentToken() ? " via TASKLESS_TOKEN" : "";
16+
switch (outcome.status) {
17+
case "ok": {
18+
const orgs = outcome.data.orgs.map((o) => o.name);
19+
const orgSuffix = orgs.length > 0 ? ` (${orgs.join(", ")})` : "";
20+
return [`Logged in as ${outcome.data.user}${orgSuffix}${source}.`];
21+
}
22+
case "unauthorized": {
23+
return [
24+
`Logged in${source}, but the token was rejected.`,
25+
`It is invalid or expired. ${rejectedTokenRemedy()}`,
26+
];
27+
}
28+
case "unavailable": {
29+
return [
30+
`Logged in${source}, but unable to verify identity.`,
31+
`The Taskless service was unreachable (${outcome.reason}).`,
32+
];
33+
}
34+
default: {
35+
return [`Logged in${source}, but unable to verify identity.`];
36+
}
37+
}
38+
}

‎packages/cli/src/commands/auth.ts‎

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,8 @@ import { defineCommand } from "citty";
44

55
import { whoami } from "../api/v2";
66
import { loginInteractive } from "../auth/login-interactive";
7-
import {
8-
getToken,
9-
isEnvironmentToken,
10-
rejectedTokenRemedy,
11-
removeToken,
12-
} from "../auth/token";
7+
import { describeAuthStatus } from "../auth/status";
8+
import { getToken, isEnvironmentToken, removeToken } from "../auth/token";
139
import { getTelemetry } from "../telemetry";
1410
import { type CLIErrorCode, writeJsonError } from "../types/errors";
1511
import { splitRawArguments } from "../util/argv";
@@ -210,28 +206,8 @@ export const authCommand = defineCommand({
210206
return;
211207
}
212208

213-
const source = isEnvironmentToken() ? " via TASKLESS_TOKEN" : "";
214-
const outcome = await whoami(token);
215-
switch (outcome.status) {
216-
case "ok": {
217-
const orgs = outcome.data.orgs.map((o) => o.name);
218-
const orgSuffix = orgs.length > 0 ? ` (${orgs.join(", ")})` : "";
219-
console.log(`Logged in as ${outcome.data.user}${orgSuffix}${source}.`);
220-
return;
221-
}
222-
case "unauthorized": {
223-
console.log(`Logged in${source}, but the token was rejected.`);
224-
console.log(`It is invalid or expired. ${rejectedTokenRemedy()}`);
225-
return;
226-
}
227-
default: {
228-
console.log(`Logged in${source}, but unable to verify identity.`);
229-
if (outcome.status === "unavailable") {
230-
console.log(
231-
`The Taskless service was unreachable (${outcome.reason}).`
232-
);
233-
}
234-
}
209+
for (const line of describeAuthStatus(await whoami(token))) {
210+
console.log(line);
235211
}
236212
},
237213
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
2+
3+
import type { WhoamiResult } from "../src/api/v2";
4+
import { describeAuthStatus } from "../src/auth/status";
5+
6+
const identity: WhoamiResult = {
7+
user: "someone",
8+
orgs: [
9+
{
10+
orgId: 1,
11+
id: "00000000-0000-0000-0000-000000000001",
12+
name: "acme",
13+
source: "github",
14+
url: "https://github.com/acme",
15+
},
16+
],
17+
};
18+
19+
let previousToken: string | undefined;
20+
21+
beforeEach(() => {
22+
previousToken = process.env.TASKLESS_TOKEN;
23+
delete process.env.TASKLESS_TOKEN;
24+
});
25+
26+
afterEach(() => {
27+
if (previousToken === undefined) {
28+
delete process.env.TASKLESS_TOKEN;
29+
} else {
30+
process.env.TASKLESS_TOKEN = previousToken;
31+
}
32+
});
33+
34+
describe("describeAuthStatus", () => {
35+
it("names the user and orgs for an accepted saved token", () => {
36+
expect(describeAuthStatus({ status: "ok", data: identity })).toEqual([
37+
"Logged in as someone (acme).",
38+
]);
39+
});
40+
41+
it("says when the token comes from TASKLESS_TOKEN", () => {
42+
process.env.TASKLESS_TOKEN = "env-token";
43+
44+
expect(describeAuthStatus({ status: "ok", data: identity })).toEqual([
45+
"Logged in as someone (acme) via TASKLESS_TOKEN.",
46+
]);
47+
});
48+
49+
it("reports a rejected saved token with auth login as the fix", () => {
50+
const lines = describeAuthStatus({ status: "unauthorized" });
51+
52+
expect(lines[0]).toBe("Logged in, but the token was rejected.");
53+
expect(lines[1]).toMatch(/auth login` to replace the saved token\.$/);
54+
});
55+
56+
it("reports a rejected TASKLESS_TOKEN with the variable as the fix", () => {
57+
process.env.TASKLESS_TOKEN = "env-token";
58+
59+
const lines = describeAuthStatus({ status: "unauthorized" });
60+
61+
expect(lines[0]).toBe(
62+
"Logged in via TASKLESS_TOKEN, but the token was rejected."
63+
);
64+
expect(lines[1]).toMatch(/replace or unset it/);
65+
});
66+
67+
it("does not call a token invalid when the service is unreachable", () => {
68+
const lines = describeAuthStatus({
69+
status: "unavailable",
70+
reason: "network error: offline",
71+
retryable: true,
72+
});
73+
74+
expect(lines).toEqual([
75+
"Logged in, but unable to verify identity.",
76+
"The Taskless service was unreachable (network error: offline).",
77+
]);
78+
expect(lines.join("\n")).not.toMatch(/rejected|invalid|expired/);
79+
});
80+
});

0 commit comments

Comments
 (0)