1import {
2 EnvironmentId,
3 ProjectId,
4 type RepositoryIdentity,
5 SourceControlProviderKind,
6} from "@t3tools/contracts";
7import { describe, expect, it } from "vite-plus/test";
8
9import {
10 findProjectForChangeRequest,
11 findProjectOnChangeRequestHost,
12 projectChangeRequestUrl,
13 resolveChangeRequestLinkTarget,
14 resolvePullRequestPreviewTarget,
15} from "./changeRequestLinks.ts";
16
17describe("findProjectForChangeRequest", () => {
18 const project = (identity: Record<string, unknown>) =>
19 ({ id: "p1", repositoryIdentity: identity }) as never;
20
21 it("matches a nested GitLab group by the whole path below the host", () => {
22 const projects = [
23 project({
24 canonicalKey: "gitlab.com/t3tools/platform/t3code",
25 provider: SourceControlProviderKind.make("gitlab"),
26 displayName: "t3tools/platform/t3code",
27 owner: "t3tools",
28 name: "t3code",
29 }),
30 ];
31 expect(
32 findProjectForChangeRequest(projects, {
33 host: "gitlab.com",
34 repository: "t3tools/platform/t3code",
35 number: 42,
36 }),
37 ).toBe(projects[0]);
38 });
39
40 it("claims nothing for a lookalike host, which is what keeps a link a link", () => {
41 const projects = [
42 project({
43 canonicalKey: "github.com/pingdotgg/t3code",
44 provider: SourceControlProviderKind.make("github"),
45 owner: "pingdotgg",
46 name: "t3code",
47 }),
48 ];
49 expect(
50 findProjectForChangeRequest(projects, {
51 host: "github.com-evil.test",
52 repository: "pingdotgg/t3code",
53 number: 1,
54 }),
55 ).toBeUndefined();
56 });
57});
58
59describe("findProjectOnChangeRequestHost", () => {
60 const project = (id: string, identity: Record<string, unknown>) =>
61 ({ id, repositoryIdentity: identity }) as never;
62 const frontend = project("frontend", {
63 canonicalKey: "github.com/acme/frontend",
64 provider: "github",
65 owner: "acme",
66 name: "frontend",
67 });
68 const backend = project("backend", {
69 canonicalKey: "github.com/acme/backend",
70 provider: "github",
71 owner: "acme",
72 name: "backend",
73 });
74
75 it("prefers the project checked out from the link's own repository", () => {
76 expect(
77 findProjectOnChangeRequestHost([frontend, backend], {
78 host: "github.com",
79 repository: "acme/backend",
80 number: 7,
81 }),
82 ).toBe(backend);
83 });
84
85 it("lends any project on the host to a repository nobody has checked out", () => {
86 expect(
87 findProjectOnChangeRequestHost([frontend], {
88 host: "github.com",
89 repository: "acme/backend",
90 number: 7,
91 }),
92 ).toBe(frontend);
93 });
94});
95
96describe("resolveChangeRequestLinkTarget", () => {
97 const local = EnvironmentId.make("local");
98 const remote = EnvironmentId.make("remote");
99 const project = (id: string, environmentId: EnvironmentId, canonicalKey: string) => ({
100 id: ProjectId.make(id),
101 environmentId,
102 repositoryIdentity: {
103 canonicalKey,
104 provider: SourceControlProviderKind.make(
105 canonicalKey.startsWith("git.cafe") ? "gitcafe" : "github",
106 ),
107 displayName: canonicalKey.split("/").slice(1).join("/"),
108 locator: { source: "git-remote", remoteName: "origin", remoteUrl: `https://${canonicalKey}` },
109 } satisfies RepositoryIdentity,
110 });
111 const mirrored = project("mirrored", local, "git.cafe/acme/app");
112 const sibling = project("sibling", local, "github.com/acme/tools");
113 const elsewhere = project("elsewhere", remote, "github.com/acme/app");
114 const projects = [mirrored, sibling, elsewhere];
115 const loaded = () => true;
116 const url = "https://github.com/acme/app/pull/7";
117
118 it("opens a mirror's link in its own environment under a project on the link's host", () => {
119 expect(
120 resolveChangeRequestLinkTarget({
121 url,
122 projects,
123 configLoaded: loaded,
124 environmentId: local,
125 projectId: mirrored.id,
126 lendHostProject: true,
127 }),
128 ).toEqual({ project: sibling, host: "github.com", repository: "acme/app", number: 7 });
129 });
130
131 it("only borrows a host project for views that can show any repository on the host", () => {
132 expect(
133 resolveChangeRequestLinkTarget({
134 url,
135 projects,
136 configLoaded: loaded,
137 environmentId: local,
138 lendHostProject: false,
139 }),
140 ).toBeNull();
141 });
142
143 it("finds the checked-out repository in any environment that reads change requests", () => {
144 expect(
145 resolveChangeRequestLinkTarget({
146 url,
147 projects,
148 configLoaded: loaded,
149 primaryEnvironmentId: local,
150 lendHostProject: false,
151 })?.project,
152 ).toBe(elsewhere);
153 expect(
154 resolveChangeRequestLinkTarget({
155 url,
156 projects,
157 configLoaded: (environmentId) => environmentId !== remote,
158 lendHostProject: false,
159 }),
160 ).toBeNull();
161 });
162});
163
164describe("resolvePullRequestPreviewTarget", () => {
165 const environmentId = EnvironmentId.make("env-1");
166 const project = {
167 id: ProjectId.make("project-1"),
168 environmentId,
169 repositoryIdentity: {
170 canonicalKey: "github.com/acme/web",
171 provider: SourceControlProviderKind.make("github"),
172 displayName: "acme/web",
173 locator: { source: "git-remote", remoteName: "origin", remoteUrl: "git@github.com:acme/web" },
174 } satisfies RepositoryIdentity,
175 };
176
177 it("previews a pull request only under the project that holds its repository", () => {
178 const target = (url: string) =>
179 resolvePullRequestPreviewTarget({
180 environmentId,
181 projects: [project],
182 pullRequestsEnabled: true,
183 url,
184 });
185 expect(target("https://github.com/acme/web/pull/7")).toEqual({
186 environmentId,
187 input: { projectId: project.id, host: "github.com", repository: "acme/web", number: 7 },
188 });
189 expect(target("https://github.com/acme/other/pull/7")).toBeNull();
190 });
191});
192
193describe("projectChangeRequestUrl", () => {
194 const identity = (provider: string): RepositoryIdentity => ({
195 canonicalKey: "github.com/acme/default",
196 provider: SourceControlProviderKind.make(provider),
197 locator: {
198 source: "git-remote",
199 remoteName: "origin",
200 remoteUrl: "git@github.com:acme/default",
201 },
202 });
203
204 it("builds the requested repository's URL on the project's host", () => {
205 expect(projectChangeRequestUrl(identity("github"), "acme/other", 42)).toBe(
206 "https://github.com/acme/other/pull/42",
207 );
208 });
209
210 it("builds the project's own Azure repository from its full path, not its short selector", () => {
211 const azure: RepositoryIdentity = {
212 canonicalKey: "dev.azure.com/org/project/_git/web",
213 provider: SourceControlProviderKind.make("azure-devops"),
214 owner: "org",
215 name: "web",
216 locator: {
217 source: "git-remote",
218 remoteName: "origin",
219 remoteUrl: "https://dev.azure.com/org/project/_git/web",
220 },
221 };
222 expect(projectChangeRequestUrl(azure, "web", 42)).toBe(
223 "https://dev.azure.com/org/project/_git/web/pullrequest/42",
224 );
225 });
226
227 it("builds nothing without a project host that knows its URL shape", () => {
228 expect(projectChangeRequestUrl(null, "acme/repository", 1)).toBeNull();
229 expect(projectChangeRequestUrl(identity("unknown"), "acme/repository", 1)).toBeNull();
230 const { provider: _provider, ...withoutProvider } = identity("github");
231 expect(projectChangeRequestUrl(withoutProvider, "acme/repository", 1)).toBeNull();
232 });
233});