Skip to content

Commit 94bed09

Browse files
authored
feat: add auth update-profile command (#11)
adds `auth update-profile` subcommand to update the authenticated user's first name, last name, and email. Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
1 parent 9e11730 commit 94bed09

2 files changed

Lines changed: 141 additions & 1 deletion

File tree

src/commands/auth.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { createServer } from "node:http";
22
import { Command } from "commander";
33
import open from "open";
44
import pc from "picocolors";
5+
import yoctoSpinner from "yocto-spinner";
56
import { OAUTH_CALLBACK_PATH } from "../lib/constants.js";
7+
import { graphqlRequest } from "../lib/graphql-client.js";
68
import {
79
buildAuthorizationUrl,
810
exchangeCodeForTokens,
@@ -18,7 +20,7 @@ import {
1820
loadTokens,
1921
saveTokens,
2022
} from "../lib/token-storage.js";
21-
import { printError, printSuccess } from "../lib/utils.js";
23+
import { printError, printRecord, printSuccess } from "../lib/utils.js";
2224

2325
const SUCCESS_HTML = `<!DOCTYPE html>
2426
<html>
@@ -264,3 +266,66 @@ authCommand
264266
process.exitCode = 1;
265267
}
266268
});
269+
270+
authCommand
271+
.command("update-profile")
272+
.description("Update your profile (first name, last name, email)")
273+
.option("--first-name <name>", "First name")
274+
.option("--last-name <name>", "Last name")
275+
.option("--email <email>", "Email address")
276+
.action(
277+
async (opts: { firstName?: string; lastName?: string; email?: string }) => {
278+
const input: Record<string, string> = {};
279+
if (opts.firstName) {
280+
input.firstName = opts.firstName;
281+
}
282+
if (opts.lastName) {
283+
input.lastName = opts.lastName;
284+
}
285+
if (opts.email) {
286+
input.email = opts.email;
287+
}
288+
289+
if (Object.keys(input).length === 0) {
290+
printError(
291+
"No update options provided. Use --help to see available options."
292+
);
293+
process.exitCode = 1;
294+
return;
295+
}
296+
297+
const spinner = yoctoSpinner({ text: "Updating profile..." }).start();
298+
try {
299+
const result = await graphqlRequest<{
300+
updateUserProfile: {
301+
id: string;
302+
auth: { email: string };
303+
profile: { firstName: string | null; lastName: string | null };
304+
};
305+
}>({
306+
query: `mutation($input: UpdateUserProfileInput!) {
307+
updateUserProfile(input: $input) {
308+
id
309+
auth { email }
310+
profile { firstName lastName }
311+
}
312+
}`,
313+
variables: { input },
314+
});
315+
spinner.stop();
316+
const { updateUserProfile: user } = result;
317+
printSuccess("Profile updated successfully.");
318+
printRecord({
319+
email: user.auth.email,
320+
firstName: user.profile.firstName ?? "",
321+
lastName: user.profile.lastName ?? "",
322+
});
323+
} catch (error) {
324+
spinner.stop();
325+
printError(
326+
error instanceof Error ? error.message : "An unknown error occurred"
327+
);
328+
process.exitCode = 1;
329+
}
330+
}
331+
);

tests/core/auth.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ vi.mock("../../src/lib/oauth.js", () => ({
2222
revokeToken: (...args: unknown[]) => revokeToken(...args),
2323
}));
2424
vi.mock("open", () => ({ default: vi.fn() }));
25+
vi.mock("yocto-spinner", () => {
26+
const spinner: Record<string, unknown> = { text: "" };
27+
spinner.start = vi.fn(() => spinner);
28+
spinner.stop = vi.fn(() => spinner);
29+
return { default: () => spinner };
30+
});
31+
vi.mock("../../src/lib/program.js", () => ({
32+
program: { opts: () => ({}) },
33+
}));
34+
35+
const graphqlRequest = vi.fn();
36+
vi.mock("../../src/lib/graphql-client.js", () => ({
37+
graphqlRequest: (...args: unknown[]) => graphqlRequest(...args),
38+
}));
2539

2640
const { authCommand } = await import("../../src/commands/auth.js");
2741

@@ -104,4 +118,65 @@ describe("auth", () => {
104118
expect(loadTokens).toHaveBeenCalled();
105119
});
106120
});
121+
122+
describe("update-profile", () => {
123+
it("sends first name and last name", async () => {
124+
graphqlRequest.mockResolvedValueOnce({
125+
updateUserProfile: {
126+
id: "usr_1",
127+
auth: { email: "test@example.com" },
128+
profile: { firstName: "Ben", lastName: "Sabic" },
129+
},
130+
});
131+
132+
await runCommand(authCommand, [
133+
"update-profile",
134+
"--first-name",
135+
"Ben",
136+
"--last-name",
137+
"Sabic",
138+
]);
139+
140+
const call = graphqlRequest.mock.calls[0][0];
141+
expect(call.variables.input).toEqual({
142+
firstName: "Ben",
143+
lastName: "Sabic",
144+
});
145+
});
146+
147+
it("sends email only", async () => {
148+
graphqlRequest.mockResolvedValueOnce({
149+
updateUserProfile: {
150+
id: "usr_1",
151+
auth: { email: "new@example.com" },
152+
profile: { firstName: "Ben", lastName: "Sabic" },
153+
},
154+
});
155+
156+
await runCommand(authCommand, [
157+
"update-profile",
158+
"--email",
159+
"new@example.com",
160+
]);
161+
162+
const call = graphqlRequest.mock.calls[0][0];
163+
expect(call.variables.input).toEqual({ email: "new@example.com" });
164+
});
165+
166+
it("rejects with no options", async () => {
167+
const original = process.exitCode;
168+
await runCommand(authCommand, ["update-profile"]);
169+
expect(process.exitCode).toBe(1);
170+
process.exitCode = original;
171+
});
172+
173+
it("handles errors gracefully", async () => {
174+
graphqlRequest.mockRejectedValueOnce(new Error("Unauthorized"));
175+
176+
const original = process.exitCode;
177+
await runCommand(authCommand, ["update-profile", "--first-name", "Test"]);
178+
expect(process.exitCode).toBe(1);
179+
process.exitCode = original;
180+
});
181+
});
107182
});

0 commit comments

Comments
 (0)