import { cleanup, fireEvent, render, screen, waitFor, } from "@testing-library/react"; 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 = [ { id: "repo-1", name: "Main Repo", path: "/repos/main", project_id: "proj-1", owner_id: "user-1", is_mirror: false, remote_url: null, ssh_key_id: null, last_push: null, created_at: null, }, ]; vi.mock("react-router-dom", async () => { const actual = await vi.importActual( "react-router-dom", ); return { ...actual, useParams: () => ({ projectId: "proj-1" }), }; }); afterEach(() => { cleanup(); vi.restoreAllMocks(); }); describe("RepositoriesSettingsTab", () => { it("opens create dialog and clones using full URL by default", async () => { const listMock = vi .spyOn(gitRepositoriesApi, "listRepositories") .mockResolvedValue(mockRepositories); const createMock = vi .spyOn(gitRepositoriesApi, "createRepository") .mockResolvedValue(mockRepositories[0]); const parseMock = vi .spyOn(gitRepositoriesApi, "parseGitUrl") .mockResolvedValue({ original_url: "https://github.com/user/repo.git", base_url: "https://github.com/user/repo.git", is_valid_clone_url: true, needs_parsing: false, host: "github.com", message: "Valid git repository URL", error_code: null, }); vi.spyOn(sshKeysApi, "listSSHKeys").mockResolvedValue([]); render(); await waitFor(() => { expect(screen.getByText("Main Repo")).toBeInTheDocument(); }); fireEvent.click(screen.getByRole("button", { name: /add repository/i })); fireEvent.change(screen.getByPlaceholderText(/repository-name/i), { target: { value: "New Repo" }, }); fireEvent.change( screen.getByPlaceholderText(/https:\/\/github.com\/user\/repo.git/i), { target: { value: "https://github.com/user/repo.git" }, }, ); await waitFor(() => { expect(parseMock).toHaveBeenCalledWith("https://github.com/user/repo.git"); }); fireEvent.click(screen.getByRole("button", { name: /clone repository/i })); await waitFor(() => { expect(createMock).toHaveBeenCalledWith("proj-1", { name: "New Repo", remote_url: "https://github.com/user/repo.git", }); }); expect(listMock).toHaveBeenCalledTimes(2); }); it("switches to owner/repo mode and requires SSH key for generated SSH URL", 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(); await waitFor(() => { expect(screen.getByText("Main Repo")).toBeInTheDocument(); }); fireEvent.click(screen.getByRole("button", { name: /add repository/i })); fireEvent.change(screen.getByPlaceholderText(/repository-name/i), { target: { value: "New Repo" }, }); fireEvent.click( screen.getByRole("button", { name: /use owner\/repo instead/i }), ); fireEvent.change(screen.getByPlaceholderText(/owner/i), { target: { value: "alice" }, }); 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); }); it("accepts SSH URL in full URL mode when SSH key is selected", async () => { const listMock = vi .spyOn(gitRepositoriesApi, "listRepositories") .mockResolvedValue(mockRepositories); const createMock = vi .spyOn(gitRepositoriesApi, "createRepository") .mockResolvedValue(mockRepositories[0]); // SSH URLs are short-circuited client-side; parseGitUrl should NOT be called const parseMock = vi .spyOn(gitRepositoriesApi, "parseGitUrl") .mockResolvedValue({ original_url: "", base_url: null, is_valid_clone_url: false, needs_parsing: false, host: null, message: "", error_code: null, }); vi.spyOn(sshKeysApi, "listSSHKeys").mockResolvedValue([ { id: "key-1", name: "My Key", public_key: "ssh-ed25519 AAA...", created_at: "2024-01-01", }, ]); render(); await waitFor(() => { expect(screen.getByText("Main Repo")).toBeInTheDocument(); }); fireEvent.click(screen.getByRole("button", { name: /add repository/i })); fireEvent.change(screen.getByPlaceholderText(/repository-name/i), { target: { value: "New Repo" }, }); fireEvent.change( screen.getByPlaceholderText(/https:\/\/github.com\/user\/repo.git/i), { target: { value: "git@github.com:user/repo.git" }, }, ); // Client-side short-circuit shows valid without calling backend await waitFor(() => { expect(screen.getByText(/valid git url/i)).toBeInTheDocument(); }); expect(parseMock).not.toHaveBeenCalled(); 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@github.com:user/repo.git", ssh_key_id: "key-1", }); }); expect(listMock).toHaveBeenCalledTimes(2); }); it("rejects SSH URL in full URL mode without an SSH key", async () => { 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(); await waitFor(() => { expect(screen.getByText("Main Repo")).toBeInTheDocument(); }); fireEvent.click(screen.getByRole("button", { name: /add repository/i })); fireEvent.change(screen.getByPlaceholderText(/repository-name/i), { target: { value: "New Repo" }, }); fireEvent.change( screen.getByPlaceholderText(/https:\/\/github.com\/user\/repo.git/i), { target: { value: "git@github.com:user/repo.git" }, }, ); // Do NOT select an SSH key fireEvent.click(screen.getByRole("button", { name: /clone repository/i })); expect( screen.getByText(/an ssh key is required for ssh urls/i), ).toBeInTheDocument(); expect(createMock).not.toHaveBeenCalled(); }); it("shows validation when cloning without a remote url", async () => { vi.spyOn(gitRepositoriesApi, "listRepositories").mockResolvedValue( mockRepositories, ); render(); await waitFor(() => { expect(screen.getByText("Main Repo")).toBeInTheDocument(); }); fireEvent.click(screen.getByRole("button", { name: /add repository/i })); fireEvent.change(screen.getByPlaceholderText(/repository-name/i), { target: { value: "New Repo" }, }); fireEvent.change( screen.getByPlaceholderText(/https:\/\/github.com\/user\/repo.git/i), { target: { value: "" }, }, ); fireEvent.click(screen.getByRole("button", { name: /clone repository/i })); expect( screen.getByText(/remote url is required for advanced cloning/i), ).toBeInTheDocument(); }); });