Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 66 additions & 1 deletion src/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 = `<!DOCTYPE html>
<html>
Expand Down Expand Up @@ -264,3 +266,66 @@ authCommand
process.exitCode = 1;
}
});

authCommand
.command("update-profile")
.description("Update your profile (first name, last name, email)")
.option("--first-name <name>", "First name")
.option("--last-name <name>", "Last name")
.option("--email <email>", "Email address")
.action(
async (opts: { firstName?: string; lastName?: string; email?: string }) => {
const input: Record<string, string> = {};
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;
}
}
);
75 changes: 75 additions & 0 deletions tests/core/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = { 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");

Expand Down Expand Up @@ -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;
});
});
});