Files
headquarter/apps/web/src/components/features/project/repositories-settings-tab.test.tsx
T
Developer b2c84e2064 fix: default to full URL mode and short-circuit SSH URL validation in repo dialog
RepositoryCreateDialog fixes:
- Change useAdvancedUrl default from false to true so full URL is the default
- Move isSshUrl helper before the effect that references it
- Short-circuit SSH URLs client-side in debounced validation so they always
  show as valid without depending on backend parseGitUrl behavior
- Keeps submit-time SSH key requirement: error shown if SSH URL without key

Tests:
- Update repositories-settings-tab tests for full-URL default mode
- Add SSH URL acceptance test with key selected (client-side short-circuit)
- Add SSH URL rejection test without key selected

Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
2026-06-09 14:07:02 +00:00

290 lines
8.1 KiB
TypeScript

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<typeof import("react-router-dom")>(
"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(<RepositoriesSettingsTab />);
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(<RepositoriesSettingsTab />);
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(<RepositoriesSettingsTab />);
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(<RepositoriesSettingsTab />);
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(<RepositoriesSettingsTab />);
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();
});
});