import { describe, expect, it } from "vite-plus/test"; import { changeRequestRepositoryUrl, findProjectForChangeRequest, findProjectOnChangeRequestHost, gitHubPullRequestBrowserUrl, matchesLinkedPullRequestUrl, parseChangeRequestUrl, pullRequestCandidateUrlFromReferenceAutolink, shouldOpenPullRequestExternally, } from "./openPullRequestLink"; import { ProjectId, type RepositoryIdentity } from "@t3tools/contracts"; function repositoryIdentity( provider: string, canonicalKey: string, remoteUrl: string, ): RepositoryIdentity { return { canonicalKey, provider, locator: { source: "git-remote", remoteName: "origin", remoteUrl }, }; } describe("gitHubPullRequestBrowserUrl", () => { it("uses the requested GitHub repository instead of the project's default repository", () => { const identity = repositoryIdentity( "github", "github.com/acme/default", "https://github.com/acme/default.git", ); expect(gitHubPullRequestBrowserUrl(identity, "acme/other", 42)).toBe( "https://github.com/acme/other/pull/42", ); }); it("preserves a custom GitHub HTTP origin without its credentials", () => { const identity = repositoryIdentity( "github", "github.acme.test/team/default", "http://token@github.acme.test:8443/team/default.git", ); expect(gitHubPullRequestBrowserUrl(identity, "platform/api", 7)).toBe( "http://github.acme.test:8443/platform/api/pull/7", ); }); it.each([ { name: "SSH", remoteUrl: "git@github.acme.test:team/default.git", }, { name: "git protocol", remoteUrl: "git://github.acme.test/team/default.git", }, ])("uses the normalized host for a $name remote", ({ remoteUrl }) => { const identity = repositoryIdentity("github", "github.acme.test/team/default", remoteUrl); expect(gitHubPullRequestBrowserUrl(identity, "platform/api", 9)).toBe( "https://github.acme.test/platform/api/pull/9", ); }); it("returns null for missing or invalid GitHub data", () => { expect(gitHubPullRequestBrowserUrl(null, "acme/repository", 1)).toBeNull(); expect( gitHubPullRequestBrowserUrl( repositoryIdentity("github", "github.com/acme/repository", "https://github.com/a/b"), "acme", 1, ), ).toBeNull(); expect( gitHubPullRequestBrowserUrl( repositoryIdentity("github", "github.com/acme/repository", "https://github.com/a/b"), "../repository", 1, ), ).toBeNull(); expect( gitHubPullRequestBrowserUrl( repositoryIdentity("github", "github.com/acme/repository", "https://github.com/a/b"), "acme/repository", 0, ), ).toBeNull(); expect( gitHubPullRequestBrowserUrl( repositoryIdentity("github", "bad host/acme/repository", "not a remote"), "acme/repository", 1, ), ).toBeNull(); }); it.each(["gitlab", "bitbucket", "azure-devops", "unknown"])( "does not build a fallback for %s", (provider) => { expect( gitHubPullRequestBrowserUrl( repositoryIdentity(provider, "github.com/acme/repository", "https://github.com/a/b"), "acme/repository", 1, ), ).toBeNull(); }, ); }); describe("changeRequestRepositoryUrl", () => { it("preserves repository path casing", () => { expect( changeRequestRepositoryUrl( "https://gitlab.example.test/Team/Platform/Repo/-/merge_requests/42/diffs#note_1", ), ).toBe("https://gitlab.example.test/Team/Platform/Repo"); }); it("keeps pull-like segments inside nested GitLab repository paths", () => { expect( changeRequestRepositoryUrl( "https://gitlab.example.test/group/pull/123/repo/-/merge_requests/42", ), ).toBe("https://gitlab.example.test/group/pull/123/repo"); }); }); describe("pullRequestCandidateUrlFromReferenceAutolink", () => { it("turns GitHub's shared issue route into a pull request candidate", () => { expect( pullRequestCandidateUrlFromReferenceAutolink( "https://github.com/pingdotgg/t3code/issues/8600#issuecomment-1", ), ).toBe("https://github.com/pingdotgg/t3code/pull/8600#issuecomment-1"); }); it("does not reinterpret other issue hosts or malformed references", () => { expect( pullRequestCandidateUrlFromReferenceAutolink( "https://gitlab.com/pingdotgg/t3code/-/issues/8600", ), ).toBeNull(); expect( pullRequestCandidateUrlFromReferenceAutolink( "https://github.com/pingdotgg/t3code/issues/not-a-number", ), ).toBeNull(); }); }); describe("matchesLinkedPullRequestUrl", () => { const linkedPullRequest = { projectId: ProjectId.make("project-1"), repository: "pingdotgg/t3code", number: 42, url: "https://github.com/pingdotgg/t3code/pull/42", }; it("matches the same pull request without looking up its project", () => { expect( matchesLinkedPullRequestUrl( linkedPullRequest, "https://github.com/PingDotGG/T3Code/pull/42/files", ), ).toBe(true); }); it("rejects a different pull request or host", () => { expect( matchesLinkedPullRequestUrl(linkedPullRequest, "https://github.com/pingdotgg/t3code/pull/43"), ).toBe(false); expect( matchesLinkedPullRequestUrl( linkedPullRequest, "https://github.example.com/pingdotgg/t3code/pull/42", ), ).toBe(false); }); }); describe("shouldOpenPullRequestExternally", () => { it("uses the browser for command-click and control-click", () => { expect(shouldOpenPullRequestExternally({ metaKey: true, ctrlKey: false })).toBe(true); expect(shouldOpenPullRequestExternally({ metaKey: false, ctrlKey: true })).toBe(true); }); it("keeps an unmodified click in the pull request view", () => { expect(shouldOpenPullRequestExternally({ metaKey: false, ctrlKey: false })).toBe(false); }); }); describe("parseChangeRequestUrl", () => { it("reads a GitHub pull request", () => { expect(parseChangeRequestUrl("https://github.com/T3Tools/T3Code/pull/123")).toEqual({ host: "github.com", repository: "t3tools/t3code", number: 123, }); }); it("reads a pull request on a GitHub Enterprise host", () => { expect(parseChangeRequestUrl("https://github.acme.test/platform/api/pull/7")).toEqual({ host: "github.acme.test", repository: "platform/api", number: 7, }); }); it("reads a GitLab merge request, nested groups and all", () => { expect( parseChangeRequestUrl("https://gitlab.com/t3tools/platform/t3code/-/merge_requests/42"), ).toEqual({ host: "gitlab.com", repository: "t3tools/platform/t3code", number: 42, }); }); it("reads a merge request on a self-hosted GitLab named nothing like GitLab", () => { expect(parseChangeRequestUrl("https://code.acme.test/team/project/-/merge_requests/9")).toEqual( { host: "code.acme.test", repository: "team/project", number: 9, }, ); }); it("reads a Bitbucket pull request", () => { expect(parseChangeRequestUrl("https://bitbucket.org/workspace/repo/pull-requests/5")).toEqual({ host: "bitbucket.org", repository: "workspace/repo", number: 5, }); }); it("reads both Azure DevOps URL forms, keeping `_git` in the repository path", () => { expect( parseChangeRequestUrl("https://dev.azure.com/acme/platform/_git/t3code/pullrequest/17"), ).toEqual({ host: "dev.azure.com", repository: "acme/platform/_git/t3code", number: 17, }); expect( parseChangeRequestUrl("https://acme.visualstudio.com/platform/_git/t3code/pullrequest/17"), ).toEqual({ host: "acme.visualstudio.com", repository: "platform/_git/t3code", number: 17, }); }); it("survives trailing segments, a trailing slash and a query string", () => { expect(parseChangeRequestUrl("https://github.com/t3tools/t3code/pull/123/files?w=1")).toEqual({ host: "github.com", repository: "t3tools/t3code", number: 123, }); expect( parseChangeRequestUrl("https://gitlab.com/team/project/-/merge_requests/42/diffs#note_1"), ).toEqual({ host: "gitlab.com", repository: "team/project", number: 42 }); expect( parseChangeRequestUrl("https://bitbucket.org/team/repo/pull-requests/5/commits"), ).toEqual({ host: "bitbucket.org", repository: "team/repo", number: 5 }); expect(parseChangeRequestUrl("https://github.com/t3tools/t3code/pull/123/")).toEqual({ host: "github.com", repository: "t3tools/t3code", number: 123, }); }); it("claims nothing it cannot be sure of, so the link goes to the browser", () => { for (const link of [ "https://github.com/t3tools/t3code/issues/123", "https://github.com/t3tools/t3code/commit/0a1b2c3", "https://github.com/t3tools/t3code", "https://github.com/t3tools/t3code/pull/abc", "https://gitlab.com/t3tools/t3code/-/snippets/12", "https://gitlab.com/t3tools/t3code/-/issues/12", // A path shape that means nothing off its own host. "https://blog.example.test/2026/updates/pull/3", // A lookalike is deliberately not fought here: `github.com.evil.test` reads as a GitHub // Enterprise install and there is no way to tell it from one. It is `findProjectForChange // Request` that refuses it, because no project in the workspace is checked out from it. "javascript:alert(1)//github.com/t3tools/t3code/pull/1", "not a url", ]) { expect(parseChangeRequestUrl(link), link).toBeNull(); } }); }); describe("findProjectOnChangeRequestHost", () => { const project = (id: string, identity: Record) => ({ id, repositoryIdentity: identity }) as never; const frontend = project("frontend", { canonicalKey: "github.com/acme/frontend", provider: "github", owner: "acme", name: "frontend", }); const backend = project("backend", { canonicalKey: "github.com/acme/backend", provider: "github", owner: "acme", name: "backend", }); it.each([ "ssh.dev.azure.com/v3/org-a/project/web", "vs-ssh.visualstudio.com/v3/org-a/project/web", "org-a.visualstudio.com/defaultcollection/project/_git/web", "dev.azure.com/org-a/project/_git/web", ])("matches Azure browser references against %s", (canonicalKey) => { const checkout = project("azure", { canonicalKey, provider: "azure-devops", displayName: canonicalKey.split("/").slice(1).join("/"), }); const reference = { host: "dev.azure.com", repository: "org-a/project/_git/web", number: 42 }; expect(findProjectForChangeRequest([checkout], reference)).toBe(checkout); expect(findProjectOnChangeRequestHost([checkout], reference)).toBe(checkout); expect( findProjectOnChangeRequestHost([checkout], { ...reference, repository: "org-b/project/_git/web", }), ).toBeUndefined(); expect( findProjectOnChangeRequestHost([checkout], { ...reference, repository: "org-a/other-project/_git/web", }), ).toBeUndefined(); }); it("prefers the project checked out from the link's own repository", () => { expect( findProjectOnChangeRequestHost([frontend, backend], { host: "github.com", repository: "acme/backend", number: 7, }), ).toBe(backend); }); it("lends any project on the host to a repository nobody has checked out", () => { expect( findProjectOnChangeRequestHost([frontend], { host: "github.com", repository: "acme/backend", number: 7, }), ).toBe(frontend); }); it("finds nothing on a host nothing is checked out from", () => { expect( findProjectOnChangeRequestHost([frontend], { host: "gitlab.com", repository: "acme/backend", number: 7, }), ).toBeUndefined(); }); }); describe("findProjectForChangeRequest", () => { const project = (identity: Record) => ({ id: "p1", repositoryIdentity: identity }) as never; it("matches a nested GitLab group by the whole path below the host", () => { // The server identifies a repository by `displayName`, which keeps every group segment; the // two-segment owner/name form would look for `t3tools/t3code` and find nothing. const projects = [ project({ canonicalKey: "gitlab.com/t3tools/platform/t3code", provider: "gitlab", displayName: "t3tools/platform/t3code", owner: "t3tools", name: "t3code", }), ]; expect( findProjectForChangeRequest(projects, { host: "gitlab.com", repository: "t3tools/platform/t3code", number: 42, }), ).toBe(projects[0]); }); it("keeps two hosts apart, so an Enterprise link does not open the public one", () => { const projects = [ project({ canonicalKey: "github.com/pingdotgg/t3code", provider: "github", owner: "pingdotgg", name: "t3code", }), ]; expect( findProjectForChangeRequest(projects, { host: "github.acme.test", repository: "pingdotgg/t3code", number: 1, }), ).toBeUndefined(); }); it("claims nothing for a lookalike host, which is what keeps a link a link", () => { const projects = [ project({ canonicalKey: "github.com/pingdotgg/t3code", provider: "github", owner: "pingdotgg", name: "t3code", }), ]; expect( findProjectForChangeRequest(projects, { host: "github.com-evil.test", repository: "pingdotgg/t3code", number: 1, }), ).toBeUndefined(); }); });