diff --git a/apps/web/src/components/features/project/repositories-settings-tab.test.tsx b/apps/web/src/components/features/project/repositories-settings-tab.test.tsx index 4c7702f..65a74cc 100644 --- a/apps/web/src/components/features/project/repositories-settings-tab.test.tsx +++ b/apps/web/src/components/features/project/repositories-settings-tab.test.tsx @@ -3,6 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { RepositoriesSettingsTab } from "./repositories-settings-tab"; import * as gitRepositoriesApi from "../../../api/git-repositories"; +import * as sshKeysApi from "../../../api/ssh-keys"; const mockRepositories = [ { @@ -35,6 +36,9 @@ describe("RepositoriesSettingsTab", () => { it("opens create dialog and clones an existing repository", async () => { const listMock = vi.spyOn(gitRepositoriesApi, "listRepositories").mockResolvedValue(mockRepositories); const createMock = vi.spyOn(gitRepositoriesApi, "createRepository").mockResolvedValue(mockRepositories[0]); + vi.spyOn(sshKeysApi, "listSSHKeys").mockResolvedValue([ + { id: "key-1", name: "My Key", public_key: "ssh-ed25519 AAA...", created_at: "2024-01-01" }, + ]); render(); @@ -52,12 +56,17 @@ describe("RepositoriesSettingsTab", () => { fireEvent.change(screen.getByPlaceholderText(/repo-name/i), { target: { value: "demo" }, }); + const sshSelect = screen.getByRole("combobox", { name: /ssh key/i }); + await waitFor(() => expect(sshSelect.querySelector('option[value="key-1"]')).toBeTruthy()); + (sshSelect as HTMLSelectElement).value = "key-1"; + fireEvent.change(sshSelect); fireEvent.click(screen.getByRole("button", { name: /clone repository/i })); await waitFor(() => { expect(createMock).toHaveBeenCalledWith("proj-1", { name: "New Repo", remote_url: "git@git.commumedia.org:alice/demo.git", + ssh_key_id: "key-1", }); }); expect(listMock).toHaveBeenCalledTimes(2); @@ -66,6 +75,7 @@ describe("RepositoriesSettingsTab", () => { it("uses advanced url fallback when requested", async () => { const listMock = vi.spyOn(gitRepositoriesApi, "listRepositories").mockResolvedValue(mockRepositories); const createMock = vi.spyOn(gitRepositoriesApi, "createRepository").mockResolvedValue(mockRepositories[0]); + vi.spyOn(sshKeysApi, "listSSHKeys").mockResolvedValue([]); render(); diff --git a/apps/web/src/components/features/project/repository-create-dialog.tsx b/apps/web/src/components/features/project/repository-create-dialog.tsx index c867075..cb5b80a 100644 --- a/apps/web/src/components/features/project/repository-create-dialog.tsx +++ b/apps/web/src/components/features/project/repository-create-dialog.tsx @@ -124,6 +124,11 @@ export const RepositoryCreateDialog = ({ onClose(); }; + const isSshUrl = (url: string): boolean => { + const u = url.trim().toLowerCase(); + return u.startsWith("git@") || u.startsWith("ssh://"); + }; + const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); setFormError(null); @@ -140,19 +145,27 @@ export const RepositoryCreateDialog = ({ }; if (createMode === "clone") { + let remoteUrl: string; if (useAdvancedUrl) { if (!advancedUrl.trim()) { setFormError("Remote URL is required for advanced cloning"); return; } - input.remote_url = advancedUrl.trim(); + remoteUrl = advancedUrl.trim(); } else { if (!owner.trim() || !repoName.trim()) { setFormError("Owner and repository name are required"); return; } - input.remote_url = `git@git.commumedia.org:${owner.trim()}/${repoName.trim()}.git`; + remoteUrl = `git@git.commumedia.org:${owner.trim()}/${repoName.trim()}.git`; } + + if (isSshUrl(remoteUrl) && !selectedSshKey) { + setFormError("An SSH key is required for SSH URLs"); + return; + } + + input.remote_url = remoteUrl; if (selectedSshKey) { input.ssh_key_id = selectedSshKey; } @@ -202,24 +215,24 @@ export const RepositoryCreateDialog = ({ blank bare repo here.

-
-