import { describe, expect, it, vi } from "vitest"; import { server } from "#/mocks/node"; import type { Branch, GitRepository } from "#/types/git"; const API_BASE = "http://localhost:3000"; const getJson = async (path: string) => { const response = await fetch(`${API_BASE}${path}`); return { body: (await response.json()) as T, headers: response.headers, status: response.status, }; }; describe("mock git browsing", () => { it("requires a provider before listing or searching repositories", async () => { const listing = await getJson("/api/user/repositories"); const search = await getJson("/api/user/search/repositories"); expect(listing).toMatchObject({ body: "Git provider token required. (such as GitHub).", status: 401, }); expect(search).toMatchObject({ body: "Git provider token required.", status: 401, }); }); it("lists the newest repositories first with navigable pagination", async () => { const firstPage = await getJson( "/api/user/repositories?selected_provider=github&per_page=20", ); const secondPage = await getJson( "/api/user/repositories?selected_provider=github&page=2&per_page=20", ); const lastPage = await getJson( "/api/user/repositories?selected_provider=github&page=6&per_page=20", ); expect(firstPage.status).toBe(200); expect(firstPage.body).toHaveLength(20); expect( firstPage.body.every( (repository) => repository.git_provider === "github", ), ).toBe(true); expect( firstPage.body.map(({ pushed_at }) => Date.parse(pushed_at ?? "")), ).toEqual( [...firstPage.body] .map(({ pushed_at }) => Date.parse(pushed_at ?? "")) .sort((a, b) => b - a), ); expect(firstPage.body[0]?.link_header).toBe( '; rel="next", ; rel="last", ; rel="first"', ); expect(secondPage.body[0]?.link_header).toBe( '; rel="prev", ; rel="next", ; rel="last", ; rel="first"', ); expect(lastPage.body[0]?.link_header).toBe( '; rel="prev", ; rel="last", ; rel="first"', ); }); it("supports star sorting, alternative providers, and unpaginated responses", async () => { const starred = await getJson( "/api/user/repositories?selected_provider=gitlab&sort=stars&per_page=200", ); const originalOrder = await getJson( "/api/user/repositories?selected_provider=bitbucket&sort=name&per_page=200", ); const unsupported = await getJson( "/api/user/repositories?selected_provider=unknown", ); expect( starred.body.map(({ stargazers_count }) => stargazers_count), ).toEqual( [...starred.body] .map(({ stargazers_count }) => stargazers_count) .sort((a, b) => (b ?? 0) - (a ?? 0)), ); expect(starred.body[0]?.link_header).toBeUndefined(); expect(originalOrder.body.map(({ id }) => id)).toEqual( Array.from({ length: 120 }, (_, index) => `${index + 1}`), ); expect(unsupported.body).toEqual([]); }); it("limits GitHub repositories to the selected installation", async () => { const firstInstallation = await getJson( "/api/user/repositories?selected_provider=github&installation_id=invalid&sort=name&per_page=30", ); const secondInstallation = await getJson( "/api/user/repositories?selected_provider=github&installation_id=1&sort=name&per_page=30", ); const ignoredForGitLab = await getJson( "/api/user/repositories?selected_provider=gitlab&installation_id=1&sort=name&per_page=200", ); expect(firstInstallation.body.map(({ id }) => id)).toEqual( Array.from({ length: 20 }, (_, index) => `${index + 1}`), ); expect(secondInstallation.body.map(({ id }) => id)).toEqual( Array.from({ length: 20 }, (_, index) => `${index + 21}`), ); expect(ignoredForGitLab.body).toHaveLength(120); }); it("returns an empty page without attaching pagination metadata", async () => { const response = await getJson( "/api/user/repositories?selected_provider=github&page=99&per_page=30", ); expect(response.body).toEqual([]); }); it("uses the public default page sizes for repository lists and searches", async () => { const listing = await getJson( "/api/user/repositories?selected_provider=github&sort=name", ); const search = await getJson( "/api/user/search/repositories?selected_provider=github&query=repo&sort=name", ); expect(listing.body).toHaveLength(30); expect(listing.body[0]?.link_header).toContain("per_page=30"); expect(search.body).toHaveLength(5); }); it("searches repositories case-insensitively and honors ordering and limits", async () => { const descending = await getJson( "/api/user/search/repositories?selected_provider=github&query=REPO-&per_page=8", ); const ascending = await getJson( "/api/user/search/repositories?selected_provider=github&query=repo-&per_page=8&sort=stars&order=asc", ); const originalOrder = await getJson( "/api/user/search/repositories?selected_provider=github&query=repo-1&per_page=50&sort=name", ); const unsupported = await getJson( "/api/user/search/repositories?selected_provider=unknown&query=repo", ); expect(descending.body).toHaveLength(8); expect( descending.body.map(({ stargazers_count }) => stargazers_count), ).toEqual( [...descending.body] .map(({ stargazers_count }) => stargazers_count) .sort((a, b) => (b ?? 0) - (a ?? 0)), ); expect( ascending.body.map(({ stargazers_count }) => stargazers_count), ).toEqual( [...ascending.body] .map(({ stargazers_count }) => stargazers_count) .sort((a, b) => (a ?? 0) - (b ?? 0)), ); expect(originalOrder.body.map(({ id }) => id)).toEqual([ "1", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "100", "101", "102", "103", "104", "105", "106", "107", "108", "109", "110", "111", "112", "113", "114", "115", "116", "117", "118", "119", "120", ]); expect(unsupported.body).toEqual([]); }); it("validates repository names and paginates branch listings", async () => { const missing = await getJson("/api/user/repository/branches"); const firstPage = await getJson<{ branches: Branch[]; current_page: number; has_next_page: boolean; per_page: number; total_count: number; }>("/api/user/repository/branches?repository=user/repo"); const lastPage = await getJson<{ branches: Branch[]; current_page: number; has_next_page: boolean; per_page: number; total_count: number; }>("/api/user/repository/branches?repository=user/repo&page=3&per_page=10"); const exactPage = await getJson<{ branches: Branch[]; has_next_page: boolean; }>("/api/user/repository/branches?repository=user/repo&per_page=25"); expect(missing).toMatchObject({ body: "Repository parameter is required", status: 400, }); expect(firstPage.body).toMatchObject({ current_page: 1, has_next_page: false, per_page: 30, total_count: 25, }); expect(firstPage.body.branches).toHaveLength(25); expect(firstPage.body.branches[0]).toMatchObject({ name: "main", protected: true, }); expect(firstPage.body.branches[1]).toMatchObject({ name: "develop", protected: false, }); expect(firstPage.body.branches[2]?.name).toBe("feature/branch-2"); expect(lastPage.body).toMatchObject({ current_page: 3, has_next_page: false, per_page: 10, total_count: 25, }); expect(lastPage.body.branches).toHaveLength(5); expect(exactPage.body.branches).toHaveLength(25); expect(exactPage.body.has_next_page).toBe(false); }); it("reports when another branch page exists", async () => { const response = await getJson<{ branches: Branch[]; has_next_page: boolean; }>("/api/user/repository/branches?repository=user/repo&page=1&per_page=10"); expect(response.body.branches).toHaveLength(10); expect(response.body.has_next_page).toBe(true); }); it("validates and searches branch names case-insensitively", async () => { const missing = await getJson("/api/user/search/branches"); const defaults = await getJson( "/api/user/search/branches?repository=user/repo", ); const featureMatches = await getJson( "/api/user/search/branches?repository=user/repo&query=FEATURE&per_page=2", ); expect(missing).toMatchObject({ body: "Repository parameter is required", status: 400, }); expect(defaults.body).toHaveLength(25); expect(featureMatches.body).toHaveLength(2); expect( featureMatches.body.every(({ name }) => name.startsWith("feature/")), ).toBe(true); }); it("serves representative workspace changes and unified diff content", async () => { const changes = await getJson>( "/api/git/changes", ); const diff = await getJson<{ modified: string; original: string }>( "/api/git/diff", ); expect(changes.body).toEqual([ { path: "src/components/hello.tsx", status: "UPDATED" }, { path: "src/utils/new-helper.ts", status: "ADDED" }, { path: "src/old-module.py", status: "DELETED" }, ]); expect(diff.body).toEqual({ original: 'def greet(name):\n return f"Hello, {name}!"\n', modified: 'def greet(name: str) -> str:\n return f"Hello, {name}! Welcome."\n', }); }); it("keeps star ordering stable when generated provider metadata has no score", async () => { let randomCall = 0; const random = vi.spyOn(Math, "random").mockImplementation(() => { const repositoryField = randomCall % 3; randomCall += 1; return randomCall <= 1_080 && repositoryField === 1 ? Number.NaN : 0.5; }); vi.resetModules(); const { GIT_REPOSITORY_HANDLERS } = await import("#/mocks/git-repository-handlers"); random.mockRestore(); server.resetHandlers(...GIT_REPOSITORY_HANDLERS); const listing = await getJson( "/api/user/repositories?selected_provider=github&sort=stars&per_page=3", ); const search = await getJson( "/api/user/search/repositories?selected_provider=github&sort=stars&per_page=3", ); expect(listing.status).toBe(200); expect( listing.body.map(({ stargazers_count }) => stargazers_count), ).toEqual([null, null, null]); expect(search.status).toBe(200); expect(search.body.map(({ stargazers_count }) => stargazers_count)).toEqual( [null, null, null], ); }); it("serves deterministic generated metadata through every registered route", async () => { const fixedNow = Date.parse("2025-01-15T12:00:00.000Z"); const random = vi.spyOn(Math, "random").mockReturnValue(0.3); const now = vi.spyOn(Date, "now").mockReturnValue(fixedNow); vi.resetModules(); const { GIT_REPOSITORY_HANDLERS } = await import("#/mocks/git-repository-handlers"); random.mockRestore(); now.mockRestore(); server.resetHandlers(...GIT_REPOSITORY_HANDLERS); const github = await getJson( "/api/user/repositories?selected_provider=github&sort=name&per_page=120", ); const gitlab = await getJson( "/api/user/repositories?selected_provider=gitlab&sort=name&per_page=120", ); const bitbucket = await getJson( "/api/user/repositories?selected_provider=bitbucket&sort=name&per_page=120", ); const search = await getJson( "/api/user/search/repositories?selected_provider=github&query=repo-1&sort=name&per_page=1", ); const branches = await getJson<{ branches: Branch[] }>( "/api/user/repository/branches?repository=user/repo", ); const branchSearch = await getJson( "/api/user/search/branches?repository=user/repo&query=develop", ); const changes = await getJson>( "/api/git/changes", ); const diff = await getJson<{ modified: string; original: string }>( "/api/git/diff", ); const pushedAt = new Date( fixedNow - 0.3 * 90 * 24 * 60 * 60 * 1000, ).toISOString(); const lastPushDate = new Date( fixedNow - 0.3 * 7 * 24 * 60 * 60 * 1000, ).toISOString(); expect(github.body).toHaveLength(120); expect(github.body[0]).toEqual({ id: "1", full_name: "user/repo-1", git_provider: "github", is_public: false, stargazers_count: 300, pushed_at: pushedAt, }); expect(github.body[119]?.id).toBe("120"); expect(gitlab.body[0]?.git_provider).toBe("gitlab"); expect(bitbucket.body[0]?.git_provider).toBe("bitbucket"); expect(search.body[0]?.full_name).toBe("user/repo-1"); expect(branches.body.branches).toHaveLength(25); expect(branches.body.branches.slice(0, 3)).toEqual([ { name: "main", commit_sha: "abc123000", protected: true, last_push_date: lastPushDate, }, { name: "develop", commit_sha: "abc123001", protected: false, last_push_date: lastPushDate, }, { name: "feature/branch-2", commit_sha: "abc123002", protected: false, last_push_date: lastPushDate, }, ]); expect(branchSearch.body).toEqual([branches.body.branches[1]]); expect(changes.body).toEqual([ { path: "src/components/hello.tsx", status: "UPDATED" }, { path: "src/utils/new-helper.ts", status: "ADDED" }, { path: "src/old-module.py", status: "DELETED" }, ]); expect(diff.body).toEqual({ original: 'def greet(name):\n return f"Hello, {name}!"\n', modified: 'def greet(name: str) -> str:\n return f"Hello, {name}! Welcome."\n', }); }); });