Files
AgentHub/client/src/components/settings/__tests__/GitIdentitySettings.test.tsx
T
nntrivi2001 a58e95a957 chore: v2.8.7 — Git SSH keys in tabs, safer Artifacts preview and search
- Settings → Git → Your git identity: SSH keys are one tab per host kind
  (GitHub · GitLab, AWS CodeCommit, More keys, Other hosts); opens on the
  AWS tab when only an RSA key exists.
- Artifacts: a file type the sandboxed preview cannot show (Office, zip,
  PDF) offers Download only, never a blank Preview.
- Artifacts search: `%`/`_`/`\` in a query match literally (LIKE ... ESCAPE).
- Skill frontmatter rewrite replaces a key with its continuation lines and
  quotes values YAML could misread.
- Plugin marketplace.json is read with a 1 MB streaming cap.
- Artifact lists look each owner up once.
2026-10-15 17:17:47 +07:00

275 lines
13 KiB
TypeScript

/**
* @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; a named key
* is made beside the default ones, and a second AWS account is saved under
* its own name with the one key it uses.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
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> = {}): 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(<GitIdentitySettings />);
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(<GitIdentitySettings />);
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(<GitIdentitySettings />);
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(<GitIdentitySettings />);
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(<GitIdentitySettings />);
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(<GitIdentitySettings />);
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/<repo>`)).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(<GitIdentitySettings />);
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(<GitIdentitySettings />);
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(<GitIdentitySettings />);
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-prod",
type: "rsa" as const,
name: "aws-prod",
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-prod", key: "id_rsa_aws-prod" }],
})
);
vi.mocked(api.gitIdentity.removeKeyById).mockResolvedValue(
view({ sshKeys: [RSA], sshUsers: [] })
);
render(<GitIdentitySettings />);
// Only an RSA key: the AWS tab opens first.
expect(await screen.findByRole("region", { name: "AWS CodeCommit" })).toBeInTheDocument();
await tab("More keys");
let more = screen.getByRole("region", { name: "More SSH keys" });
await userEvent.click(within(more).getByRole("button", { name: "New key" }));
await userEvent.type(within(more).getByLabelText("Key name"), "AWS-Prod");
await userEvent.click(within(more).getByRole("button", { name: "Generate key" }));
expect(api.gitIdentity.generateKey).toHaveBeenCalledWith(false, "rsa", "aws-prod");
expect(await within(more).findByLabelText("Public key aws-prod")).toHaveValue("ssh-rsa B prod");
await tab("AWS CodeCommit");
const aws = screen.getByRole("region", { name: "AWS CodeCommit" });
await userEvent.type(within(aws).getByLabelText("SSH key ID"), "APKA2");
await userEvent.type(within(aws).getByLabelText("Account name"), "aws-prod");
await userEvent.selectOptions(within(aws).getByLabelText("Key"), "id_rsa_aws-prod");
await userEvent.click(within(aws).getByRole("button", { name: "Save" }));
expect(api.gitIdentity.setSshUser).toHaveBeenCalledWith({
host: H,
user: "APKA2",
alias: "aws-prod",
key: "id_rsa_aws-prod",
});
expect(
await within(aws).findByText("aws-prod · ap-southeast-1 · APKA2 · aws-prod")
).toBeInTheDocument();
expect(within(aws).getByText("git clone ssh://aws-prod/v1/repos/<repo>")).toBeInTheDocument();
await tab("More keys (1)");
more = screen.getByRole("region", { name: "More SSH keys" });
await userEvent.click(within(more).getByRole("button", { name: "Delete aws-prod" }));
await waitFor(() =>
expect(api.gitIdentity.removeKeyById).toHaveBeenCalledWith("id_rsa_aws-prod")
);
});
});