From c4abaa91924105ed946c6720dd2953d3675c12f6 Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Sat, 21 Feb 2026 09:07:14 +1100 Subject: [PATCH] feat: add auth update-profile command --- src/commands/auth.ts | 67 +++++++++++++++++++++++++++++++++++- tests/core/auth.test.ts | 75 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/src/commands/auth.ts b/src/commands/auth.ts index 420f896..77eb737 100644 --- a/src/commands/auth.ts +++ b/src/commands/auth.ts @@ -2,7 +2,9 @@ import { createServer } from "node:http"; import { Command } from "commander"; import open from "open"; import pc from "picocolors"; +import yoctoSpinner from "yocto-spinner"; import { OAUTH_CALLBACK_PATH } from "../lib/constants.js"; +import { graphqlRequest } from "../lib/graphql-client.js"; import { buildAuthorizationUrl, exchangeCodeForTokens, @@ -18,7 +20,7 @@ import { loadTokens, saveTokens, } from "../lib/token-storage.js"; -import { printError, printSuccess } from "../lib/utils.js"; +import { printError, printRecord, printSuccess } from "../lib/utils.js"; const SUCCESS_HTML = ` @@ -264,3 +266,66 @@ authCommand process.exitCode = 1; } }); + +authCommand + .command("update-profile") + .description("Update your profile (first name, last name, email)") + .option("--first-name ", "First name") + .option("--last-name ", "Last name") + .option("--email ", "Email address") + .action( + async (opts: { firstName?: string; lastName?: string; email?: string }) => { + const input: Record = {}; + if (opts.firstName) { + input.firstName = opts.firstName; + } + if (opts.lastName) { + input.lastName = opts.lastName; + } + if (opts.email) { + input.email = opts.email; + } + + if (Object.keys(input).length === 0) { + printError( + "No update options provided. Use --help to see available options." + ); + process.exitCode = 1; + return; + } + + const spinner = yoctoSpinner({ text: "Updating profile..." }).start(); + try { + const result = await graphqlRequest<{ + updateUserProfile: { + id: string; + auth: { email: string }; + profile: { firstName: string | null; lastName: string | null }; + }; + }>({ + query: `mutation($input: UpdateUserProfileInput!) { + updateUserProfile(input: $input) { + id + auth { email } + profile { firstName lastName } + } +}`, + variables: { input }, + }); + spinner.stop(); + const { updateUserProfile: user } = result; + printSuccess("Profile updated successfully."); + printRecord({ + email: user.auth.email, + firstName: user.profile.firstName ?? "", + lastName: user.profile.lastName ?? "", + }); + } catch (error) { + spinner.stop(); + printError( + error instanceof Error ? error.message : "An unknown error occurred" + ); + process.exitCode = 1; + } + } + ); diff --git a/tests/core/auth.test.ts b/tests/core/auth.test.ts index db30c52..9778fde 100644 --- a/tests/core/auth.test.ts +++ b/tests/core/auth.test.ts @@ -22,6 +22,20 @@ vi.mock("../../src/lib/oauth.js", () => ({ revokeToken: (...args: unknown[]) => revokeToken(...args), })); vi.mock("open", () => ({ default: vi.fn() })); +vi.mock("yocto-spinner", () => { + const spinner: Record = { text: "" }; + spinner.start = vi.fn(() => spinner); + spinner.stop = vi.fn(() => spinner); + return { default: () => spinner }; +}); +vi.mock("../../src/lib/program.js", () => ({ + program: { opts: () => ({}) }, +})); + +const graphqlRequest = vi.fn(); +vi.mock("../../src/lib/graphql-client.js", () => ({ + graphqlRequest: (...args: unknown[]) => graphqlRequest(...args), +})); const { authCommand } = await import("../../src/commands/auth.js"); @@ -104,4 +118,65 @@ describe("auth", () => { expect(loadTokens).toHaveBeenCalled(); }); }); + + describe("update-profile", () => { + it("sends first name and last name", async () => { + graphqlRequest.mockResolvedValueOnce({ + updateUserProfile: { + id: "usr_1", + auth: { email: "test@example.com" }, + profile: { firstName: "Ben", lastName: "Sabic" }, + }, + }); + + await runCommand(authCommand, [ + "update-profile", + "--first-name", + "Ben", + "--last-name", + "Sabic", + ]); + + const call = graphqlRequest.mock.calls[0][0]; + expect(call.variables.input).toEqual({ + firstName: "Ben", + lastName: "Sabic", + }); + }); + + it("sends email only", async () => { + graphqlRequest.mockResolvedValueOnce({ + updateUserProfile: { + id: "usr_1", + auth: { email: "new@example.com" }, + profile: { firstName: "Ben", lastName: "Sabic" }, + }, + }); + + await runCommand(authCommand, [ + "update-profile", + "--email", + "new@example.com", + ]); + + const call = graphqlRequest.mock.calls[0][0]; + expect(call.variables.input).toEqual({ email: "new@example.com" }); + }); + + it("rejects with no options", async () => { + const original = process.exitCode; + await runCommand(authCommand, ["update-profile"]); + expect(process.exitCode).toBe(1); + process.exitCode = original; + }); + + it("handles errors gracefully", async () => { + graphqlRequest.mockRejectedValueOnce(new Error("Unauthorized")); + + const original = process.exitCode; + await runCommand(authCommand, ["update-profile", "--first-name", "Test"]); + expect(process.exitCode).toBe(1); + process.exitCode = original; + }); + }); });