/** * @file Tests for Settings → Git → Your git identity: a user with no key * makes one, a key shows only its public half and copies it, replacing asks * first, and a member (S3) adds an HTTPS login of their own while the host's * view leaves HTTPS to the shared Git credentials table, and an SSH user per * host is saved — AWS CodeCommit's key ID inside its own RSA section, any * other host under "Other SSH hosts" — each kind of host in its own tab, so * the sections never mix; "Add another key" makes a named key as one more * card in its own tab, a second AWS account is saved from that card under * the key's name with that key, and a second GitHub key is tied to its host. * @author Nguyễn Ngọc Trí Vĩ */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { GitIdentitySettings } from "../GitIdentitySettings"; import { api, type GitIdentity } from "../../../lib/api"; import { confirmDialog } from "../../../lib/confirm"; import { copyText } from "../../../lib/clipboard"; vi.mock("../../../lib/api", () => ({ api: { gitIdentity: { get: vi.fn(), generateKey: vi.fn(), removeKey: vi.fn(), removeKeyById: vi.fn(), setTerminalSsh: vi.fn(), setSshUser: vi.fn(), removeSshUser: vi.fn(), saveHttps: vi.fn(), saveAuthor: vi.fn(), removeHttps: vi.fn(), }, }, })); vi.mock("../../../lib/toast", () => ({ toast: { success: vi.fn(), error: vi.fn() } })); vi.mock("../../../lib/confirm", () => ({ confirmDialog: vi.fn(() => Promise.resolve(true)) })); vi.mock("../../../lib/clipboard", () => ({ copyText: vi.fn(() => Promise.resolve(true)) })); const KEY = { publicKey: "ssh-ed25519 AAAAC3Nz an@agenthub", fingerprint: "SHA256:abc" }; const view = (over: Partial = {}): GitIdentity => ({ account: "member", writable: true, ssh: null, https: [], author: { name: "An", email: "an@x.vn" }, ...over, }); /** Opens one tab of the SSH keys. */ const tab = async (name: string | RegExp) => userEvent.click(await screen.findByRole("tab", { name })); beforeEach(() => { vi.clearAllMocks(); }); describe("GitIdentitySettings", () => { it("makes a key when there is none, without asking", async () => { vi.mocked(api.gitIdentity.get).mockResolvedValue(view()); vi.mocked(api.gitIdentity.generateKey).mockResolvedValue(view({ ssh: KEY })); render(); const gh = await screen.findByRole("region", { name: /GitHub/ }); await userEvent.click(within(gh).getByRole("button", { name: "Generate key" })); expect(confirmDialog).not.toHaveBeenCalled(); expect(api.gitIdentity.generateKey).toHaveBeenCalledWith(false, "ed25519"); expect(await screen.findByLabelText(/^Public key/)).toHaveValue(KEY.publicKey); }); it("copies the public key and asks before replacing it", async () => { vi.mocked(api.gitIdentity.get).mockResolvedValue(view({ ssh: KEY })); vi.mocked(api.gitIdentity.generateKey).mockResolvedValue(view({ ssh: KEY })); render(); await userEvent.click(await screen.findByRole("button", { name: "Copy public key" })); expect(copyText).toHaveBeenCalledWith(KEY.publicKey); await userEvent.click(screen.getByRole("button", { name: "Replace key" })); expect(confirmDialog).toHaveBeenCalled(); await waitFor(() => expect(api.gitIdentity.generateKey).toHaveBeenCalledWith(true, "ed25519")); }); it("lets a member save an https login of their own", async () => { vi.mocked(api.gitIdentity.get).mockResolvedValue(view()); vi.mocked(api.gitIdentity.saveHttps).mockResolvedValue( view({ https: [{ protocol: "https", host: "github.com", username: "an", hasToken: true }] }) ); render(); await userEvent.click(await screen.findByRole("button", { name: "Add login" })); await userEvent.type(screen.getByLabelText("Server URL"), "https://github.com/o/r"); await userEvent.type(screen.getByLabelText("Username"), "an"); await userEvent.type(screen.getByLabelText("Password or access token"), "tok"); // The second Save: the first belongs to the commit-author form. await userEvent.click(screen.getAllByRole("button", { name: "Save" })[1]!); expect(api.gitIdentity.saveHttps).toHaveBeenCalledWith({ host: "github.com", username: "an", password: "tok", }); expect(await screen.findByText("github.com")).toBeInTheDocument(); }); it("makes the RSA key from the AWS CodeCommit section", async () => { vi.mocked(api.gitIdentity.get).mockResolvedValue(view()); vi.mocked(api.gitIdentity.generateKey).mockResolvedValue( view({ ssh: { ...KEY, type: "rsa", publicKey: "ssh-rsa AAAAB3 an@agenthub" } }) ); render(); await tab("AWS CodeCommit"); const aws = await screen.findByRole("region", { name: "AWS CodeCommit" }); await userEvent.click(within(aws).getByRole("button", { name: "Generate key" })); expect(api.gitIdentity.generateKey).toHaveBeenCalledWith(false, "rsa"); expect(await screen.findByLabelText("Public key RSA")).toBeInTheDocument(); }); it("keeps each key in its own section and deletes each on its own", async () => { const RSA = { type: "rsa" as const, publicKey: "ssh-rsa AAAAB3 an@agenthub", fingerprint: "SHA256:r", }; const ED = { ...KEY, type: "ed25519" as const }; vi.mocked(api.gitIdentity.get).mockResolvedValue( view({ ssh: ED, sshKeys: [ED, RSA], sshTerminal: "on" }) ); vi.mocked(api.gitIdentity.removeKey).mockResolvedValue(view({ ssh: ED, sshKeys: [ED] })); render(); const gh = await screen.findByRole("region", { name: /GitHub/ }); expect(within(gh).getByLabelText("Public key ED25519")).toHaveValue(ED.publicKey); // One tab at a time: the other host's section is not on screen. expect(screen.queryByRole("region", { name: "AWS CodeCommit" })).toBeNull(); expect(within(gh).queryByText(/IAM/)).toBeNull(); await tab("AWS CodeCommit"); const aws = screen.getByRole("region", { name: "AWS CodeCommit" }); expect(screen.queryByRole("region", { name: /GitHub/ })).toBeNull(); expect(within(aws).getByLabelText("Public key RSA")).toHaveValue(RSA.publicKey); expect(within(aws).queryByText(/GitHub/)).toBeNull(); expect(screen.getByRole("switch", { name: /terminal/ })).toBeChecked(); await userEvent.click(within(aws).getByRole("button", { name: "Delete" })); await waitFor(() => expect(api.gitIdentity.removeKey).toHaveBeenCalledWith("rsa")); }); it("saves the CodeCommit SSH key ID for a region and shows the plain clone", async () => { const RSA = { type: "rsa" as const, publicKey: "ssh-rsa AAAAB3 x", fingerprint: "SHA256:r" }; const H = "git-codecommit.eu-west-1.amazonaws.com"; vi.mocked(api.gitIdentity.get).mockResolvedValue(view({ sshKeys: [RSA], sshUsers: [] })); vi.mocked(api.gitIdentity.setSshUser).mockResolvedValue( view({ sshKeys: [RSA], sshUsers: [{ host: H, user: "APKA1" }] }) ); vi.mocked(api.gitIdentity.removeSshUser).mockResolvedValue( view({ sshKeys: [RSA], sshUsers: [] }) ); render(); const aws = await screen.findByRole("region", { name: "AWS CodeCommit" }); await userEvent.type(within(aws).getByLabelText("SSH key ID"), " APKA1 "); await userEvent.selectOptions(within(aws).getByLabelText("Region"), "eu-west-1"); await userEvent.click(within(aws).getByRole("button", { name: "Save" })); expect(api.gitIdentity.setSshUser).toHaveBeenCalledWith({ host: H, user: "APKA1" }); expect(await within(aws).findByText("eu-west-1 · APKA1 · Any key")).toBeInTheDocument(); expect(within(aws).getByText(`git clone ssh://${H}/v1/repos/`)).toBeInTheDocument(); // A CodeCommit host never shows up again under "other hosts". expect(screen.queryByText(`APKA1@${H}`)).toBeNull(); await userEvent.click(within(aws).getByRole("button", { name: `Delete ${H}` })); expect(api.gitIdentity.removeSshUser).toHaveBeenCalledWith(H); }); it("saves a user for any other SSH host under the advanced section", async () => { vi.mocked(api.gitIdentity.get).mockResolvedValue(view({ ssh: KEY, sshUsers: [] })); vi.mocked(api.gitIdentity.setSshUser).mockResolvedValue( view({ ssh: KEY, sshUsers: [{ host: "git.example.com", user: "deploy" }] }) ); render(); await tab("Other hosts"); await userEvent.type(screen.getByLabelText("Host"), "ssh://git.example.com/x"); await userEvent.type(screen.getByLabelText("User name"), "deploy"); const form = screen.getByLabelText("Host").closest("form")!; await userEvent.click(within(form).getByRole("button", { name: "Save" })); expect(api.gitIdentity.setSshUser).toHaveBeenCalledWith({ host: "git.example.com", user: "deploy", }); expect(await screen.findByText("deploy@git.example.com · Any key")).toBeInTheDocument(); }); it("warns while the commit author is unset and saves it", async () => { vi.mocked(api.gitIdentity.get).mockResolvedValue(view({ author: { name: null, email: null } })); vi.mocked(api.gitIdentity.saveAuthor).mockResolvedValue(view()); render(); expect( await screen.findByText(/commits from the dashboard and your agents fail/) ).toBeInTheDocument(); await userEvent.type(screen.getByLabelText("Name"), "An Nguyen"); await userEvent.type(screen.getByLabelText("Email"), "an@x.vn"); await userEvent.click(screen.getAllByRole("button", { name: "Save" })[0]!); expect(api.gitIdentity.saveAuthor).toHaveBeenCalledWith({ name: "An Nguyen", email: "an@x.vn", }); }); it("shows the host's key read-only and no https list", async () => { vi.mocked(api.gitIdentity.get).mockResolvedValue( view({ account: "host", writable: false, ssh: KEY }) ); render(); expect(await screen.findByLabelText(/^Public key/)).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Replace key" })).toBeNull(); expect(screen.queryByRole("button", { name: "Add login" })).toBeNull(); }); it("makes a named key and saves a second AWS account under its name with that key", async () => { const RSA = { id: "id_rsa", type: "rsa" as const, name: null, publicKey: "ssh-rsa A x", fingerprint: "SHA256:r", }; const PROD = { id: "id_rsa_aws-2", type: "rsa" as const, name: "aws-2", publicKey: "ssh-rsa B prod", fingerprint: "SHA256:p", }; const H = "git-codecommit.ap-southeast-1.amazonaws.com"; vi.mocked(api.gitIdentity.get).mockResolvedValue(view({ sshKeys: [RSA], sshUsers: [] })); vi.mocked(api.gitIdentity.generateKey).mockResolvedValue( view({ sshKeys: [RSA, PROD], sshUsers: [] }) ); vi.mocked(api.gitIdentity.setSshUser).mockResolvedValue( view({ sshKeys: [RSA, PROD], sshUsers: [{ host: H, user: "APKA2", alias: "aws-2", key: "id_rsa_aws-2" }], }) ); vi.mocked(api.gitIdentity.removeKeyById).mockResolvedValue( view({ sshKeys: [RSA], sshUsers: [] }) ); render(); // Only an RSA key: the AWS tab opens first; the new key is made inside it. expect(await screen.findByRole("region", { name: "AWS CodeCommit" })).toBeInTheDocument(); expect(screen.queryByRole("tab", { name: /More keys/ })).toBeNull(); // One click makes the key — nothing to type before it. await userEvent.click(screen.getByRole("button", { name: "Add another key" })); expect(api.gitIdentity.generateKey).toHaveBeenCalledWith(false, "rsa", "aws-2"); const prod = await screen.findByRole("region", { name: "aws-2" }); expect(within(prod).getByLabelText("Public key aws-2")).toHaveValue("ssh-rsa B prod"); // Another key can be added below it. expect(screen.getByRole("button", { name: "Add another key" })).toBeInTheDocument(); // Its own step 3: the account name defaults to the key's name. await userEvent.type(within(prod).getByLabelText("SSH key ID"), "APKA2"); await userEvent.click(within(prod).getByRole("button", { name: "Save" })); expect(api.gitIdentity.setSshUser).toHaveBeenCalledWith({ host: H, user: "APKA2", alias: "aws-2", key: "id_rsa_aws-2", }); expect( await within(prod).findByText("aws-2 · ap-southeast-1 · APKA2 · aws-2") ).toBeInTheDocument(); expect(within(prod).getByText("git clone ssh://aws-2/v1/repos/")).toBeInTheDocument(); // The default key's card does not list the named key's login. const def = screen.getByRole("region", { name: "AWS CodeCommit" }); expect(within(def).queryByText(/APKA2/)).toBeNull(); await userEvent.click(within(prod).getByRole("button", { name: "Delete key aws-2" })); await waitFor(() => expect(api.gitIdentity.removeKeyById).toHaveBeenCalledWith("id_rsa_aws-2")); }); it("adds a second GitHub key in its tab and ties it to its host", async () => { const ED = { ...KEY, id: "id_ed25519", type: "ed25519" as const, name: null }; const WORK = { id: "id_ed25519_git-2", type: "ed25519" as const, name: "git-2", publicKey: "ssh-ed25519 W work", fingerprint: "SHA256:w", }; vi.mocked(api.gitIdentity.get).mockResolvedValue(view({ sshKeys: [ED], sshUsers: [] })); vi.mocked(api.gitIdentity.generateKey).mockResolvedValue( view({ sshKeys: [ED, WORK], sshUsers: [] }) ); vi.mocked(api.gitIdentity.setSshUser).mockResolvedValue( view({ sshKeys: [ED, WORK], sshUsers: [{ host: "github.com", user: "git", alias: "git-2", key: "id_ed25519_git-2" }], }) ); render(); await userEvent.click(await screen.findByRole("button", { name: "Add another key" })); expect(api.gitIdentity.generateKey).toHaveBeenCalledWith(false, "ed25519", "git-2"); const work = await screen.findByRole("region", { name: "git-2" }); expect(within(work).getByLabelText("Public key git-2")).toHaveValue("ssh-ed25519 W work"); await userEvent.click(within(work).getByRole("button", { name: "Save" })); expect(api.gitIdentity.setSshUser).toHaveBeenCalledWith({ host: "github.com", user: "git", alias: "git-2", key: "id_ed25519_git-2", }); expect( await within(work).findByText("git clone ssh://git-2//.git") ).toBeInTheDocument(); }); });