feat(workspace): check the git identity before cloning extra folders

The workspace's "Or clone repositories" box now reads GET /api/me/git and
checks every URL as it is typed: an SSH link while the account has no SSH
key is never sent and points to Settings → Git (make a key, add its public
key to the host); an HTTPS host with no saved login is flagged with an
optional login row, since a public repo needs none. The identity is re-read
when the window regains focus, and a refused SSH login links to the same
settings. The server's ECREDENTIALS / ESSHAUTH answers still decide.
This commit is contained in:
2026-10-19 11:54:49 +07:00
parent 0c8782ea75
commit e2d8d314f6
5 changed files with 165 additions and 12 deletions
@@ -1,7 +1,9 @@
/**
* @file Tests for CloneSourcesForm: every URL in the box is cloned in turn as
* an extra folder, a host that wants a login opens the login row, and Retry
* re-runs only the failed URLs with that login.
* re-runs only the failed URLs with that login. An SSH link while the
* account has no key is never sent — the user is pointed at Settings → Git —
* and an HTTPS host with no saved login is flagged before Clone.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
@@ -11,16 +13,20 @@ import userEvent from "@testing-library/user-event";
import "../../../i18n";
vi.mock("../../../lib/api", () => ({
api: { lanes: { cloneFolder: vi.fn() } },
api: { lanes: { cloneFolder: vi.fn() }, gitIdentity: { get: vi.fn() } },
}));
import { api, type LaneSourcesView } from "../../../lib/api";
import { CloneSourcesForm } from "../CloneSourcesForm";
import { api, type GitIdentity, type LaneSourcesView } from "../../../lib/api";
import { CloneSourcesForm, cloneTarget } from "../CloneSourcesForm";
const view = { sources: [] } as unknown as LaneSourcesView & { path: string };
describe("CloneSourcesForm", () => {
beforeEach(() => vi.mocked(api.lanes.cloneFolder).mockReset());
beforeEach(() => {
vi.mocked(api.lanes.cloneFolder).mockReset();
// Unknown identity: nothing is checked up front.
vi.mocked(api.gitIdentity.get).mockReset().mockRejectedValue(new Error("old server"));
});
it("clones each URL, asks for a login where needed, and retries only that one", async () => {
const user = userEvent.setup();
@@ -58,4 +64,36 @@ describe("CloneSourcesForm", () => {
});
expect(screen.getByTestId("workspace-clone-input")).toHaveValue("");
});
it("tells an SSH link from an HTTPS one", () => {
expect(cloneTarget("git@github.com:o/r.git")).toEqual({ kind: "ssh", host: "github.com" });
expect(cloneTarget("ssh://git@h.test:2222/o/r")).toEqual({ kind: "ssh", host: "h.test" });
expect(cloneTarget("https://h.test/o/r.git")).toEqual({ kind: "https", host: "h.test" });
expect(cloneTarget("not a url")).toBeNull();
});
it("does not clone an SSH link before the account has a key, and flags an HTTPS host with no login", async () => {
const user = userEvent.setup();
vi.mocked(api.gitIdentity.get).mockResolvedValue({
ssh: null,
sshKeys: [],
https: [],
} as unknown as GitIdentity);
vi.mocked(api.lanes.cloneFolder).mockResolvedValue(view);
render(<CloneSourcesForm laneId={3} parent="/w" onAdded={vi.fn()} />);
await waitFor(() => expect(api.gitIdentity.get).toHaveBeenCalled());
await user.type(
screen.getByTestId("workspace-clone-input"),
"git@github.com:o/r.git\nhttps://h.test/a.git"
);
expect(screen.getByTestId("workspace-clone-need-ssh")).toHaveTextContent("github.com");
expect(screen.getByTestId("workspace-clone-need-login")).toHaveTextContent("h.test");
await user.click(screen.getByTestId("workspace-clone-run"));
await waitFor(() => expect(api.lanes.cloneFolder).toHaveBeenCalledTimes(1));
expect(api.lanes.cloneFolder).toHaveBeenCalledWith(3, { url: "https://h.test/a.git" });
expect(await screen.findByText(/has no SSH key yet/)).toBeInTheDocument();
expect(screen.getByTestId("workspace-clone-input")).toHaveValue("git@github.com:o/r.git");
});
});