a58e95a957
- 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.
275 lines
13 KiB
TypeScript
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")
|
|
);
|
|
});
|
|
});
|