9dd97a369c
- Plugin update reports its running step (plugin-update-progress.json → plugin_update_progress); a hidden update or clone leaves a progress chip. - Clone here can run in the background. - Git identity: extra SSH keys are added inline per host (no More keys tab). - Users: spend today (viewer's local day, ?tz_offset=). - Prettier on the files touched since v2.8.20.
308 lines
14 KiB
TypeScript
308 lines
14 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; "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ĩ <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-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(<GitIdentitySettings />);
|
|
// 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/<repo>")).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(<GitIdentitySettings />);
|
|
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/<org>/<repo>.git")
|
|
).toBeInTheDocument();
|
|
});
|
|
});
|