@goodfellow-cms/gitlab 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/testing.js CHANGED
@@ -24,14 +24,319 @@ function fakeGitLab(options) {
24
24
  username: "editor",
25
25
  name: "Test Editor"
26
26
  } }));
27
+ let nextUserId = 1;
28
+ /** Every GitLab account, by username, with its id. */
29
+ const accounts = /* @__PURE__ */ new Map();
30
+ for (const name of [...[...tokens.values(), ...options.oauth ? [options.oauth.user] : []].map((user) => user.username), ...options.accounts ?? []]) if (!accounts.has(name)) accounts.set(name, nextUserId++);
31
+ const members = /* @__PURE__ */ new Map();
32
+ for (const user of [...tokens.values(), ...options.oauth ? [options.oauth.user] : []]) {
33
+ const id = accounts.get(user.username) ?? 0;
34
+ members.set(id, {
35
+ id,
36
+ username: user.username,
37
+ name: user.name,
38
+ accessLevel: user.accessLevel ?? 40
39
+ });
40
+ }
41
+ const invitations = /* @__PURE__ */ new Map();
42
+ /** Protected branches and who may push to them: GitLab protects the default branch for Maintainers. */
43
+ const protectedBranches = /* @__PURE__ */ new Map([[repo.defaultBranch, {
44
+ push: 40,
45
+ merge: 40,
46
+ forcePush: false
47
+ }]]);
48
+ /** The project's members and invitations, and its protected branches. */
49
+ async function handleMembers(request, url, rest, accessLevel) {
50
+ const forbidden = () => json({ message: "403 Forbidden" }, 403);
51
+ const list = (items) => {
52
+ const perPage = Number(url.searchParams.get("per_page") ?? 20);
53
+ const page = Number(url.searchParams.get("page") ?? 1);
54
+ return json(items.slice((page - 1) * perPage, page * perPage));
55
+ };
56
+ const shown = (member) => ({
57
+ id: member.id,
58
+ username: member.username,
59
+ name: member.name ?? member.username,
60
+ avatar_url: `https://avatars.example/${member.username}`,
61
+ access_level: member.accessLevel
62
+ });
63
+ if (rest === "/members/all" && request.method === "GET") return list([...members.values()].map(shown));
64
+ if (rest === "/members" && request.method === "GET") return list([...members.values()].filter((member) => !member.inherited).map(shown));
65
+ if (rest === "/members" && request.method === "POST") {
66
+ if (accessLevel < 40) return forbidden();
67
+ const body = await request.json();
68
+ const username = [...accounts].find(([, id]) => id === body.user_id)?.[0];
69
+ if (!username) return json({ message: "404 User Not Found" }, 404);
70
+ if (members.has(body.user_id)) return json({ message: "Member already exists" }, 409);
71
+ members.set(body.user_id, {
72
+ id: body.user_id,
73
+ username,
74
+ accessLevel: body.access_level
75
+ });
76
+ return json(shown(members.get(body.user_id)), 201);
77
+ }
78
+ const member = rest.match(/^\/members\/(\d+)$/);
79
+ if (member) {
80
+ if (accessLevel < 40) return forbidden();
81
+ const found = members.get(Number(member[1]));
82
+ if (!found || found.inherited) return json({ message: "404 Member Not Found" }, 404);
83
+ if (request.method === "DELETE") {
84
+ members.delete(found.id);
85
+ return new Response(null, { status: 204 });
86
+ }
87
+ if (request.method === "PUT") {
88
+ found.accessLevel = (await request.json()).access_level;
89
+ return json(shown(found));
90
+ }
91
+ }
92
+ if (rest === "/invitations") {
93
+ if (accessLevel < 40) return forbidden();
94
+ if (request.method === "GET") return list([...invitations].map(([email, level]) => ({
95
+ invite_email: email,
96
+ access_level: level
97
+ })));
98
+ if (request.method === "POST") {
99
+ const body = await request.json();
100
+ if (invitations.has(body.email)) return json({
101
+ status: "error",
102
+ message: { [body.email]: "Invite email has already been taken" }
103
+ }, 201);
104
+ invitations.set(body.email, body.access_level);
105
+ return json({ status: "success" }, 201);
106
+ }
107
+ }
108
+ const invitation = rest.match(/^\/invitations\/([^/]+)$/);
109
+ if (invitation) {
110
+ if (accessLevel < 40) return forbidden();
111
+ const email = decodeURIComponent(invitation[1] ?? "");
112
+ if (!invitations.has(email)) return json({ message: "404 Not Found" }, 404);
113
+ if (request.method === "DELETE") {
114
+ invitations.delete(email);
115
+ return new Response(null, { status: 204 });
116
+ }
117
+ if (request.method === "PUT") {
118
+ invitations.set(email, (await request.json()).access_level);
119
+ return json({ invite_email: email });
120
+ }
121
+ }
122
+ const protection = rest.match(/^\/protected_branches\/([^/]+)$/);
123
+ if (protection) {
124
+ const name = decodeURIComponent(protection[1] ?? "");
125
+ const found = protectedBranches.get(name);
126
+ if (!found) return json({ message: "404 Not found" }, 404);
127
+ if (request.method === "GET") return json({
128
+ name,
129
+ push_access_levels: [{ access_level: found.push }],
130
+ merge_access_levels: [{ access_level: found.merge }],
131
+ allow_force_push: found.forcePush
132
+ });
133
+ if (request.method === "DELETE") {
134
+ if (accessLevel < 40) return forbidden();
135
+ protectedBranches.delete(name);
136
+ return new Response(null, { status: 204 });
137
+ }
138
+ }
139
+ if (rest === "/protected_branches" && request.method === "POST") {
140
+ if (accessLevel < 40) return forbidden();
141
+ const body = await request.json();
142
+ if (protectedBranches.has(body.name)) return json({ message: "Protected branch already exists" }, 409);
143
+ protectedBranches.set(body.name, {
144
+ push: body.push_access_level ?? 40,
145
+ merge: body.merge_access_level ?? 40,
146
+ forcePush: body.allow_force_push ?? false
147
+ });
148
+ return json({ name: body.name }, 201);
149
+ }
150
+ }
27
151
  const codes = /* @__PURE__ */ new Map();
28
152
  const refreshTokens = /* @__PURE__ */ new Map();
29
153
  const pipelines = /* @__PURE__ */ new Map();
154
+ /** The project's settings for updates: whether job tokens may push, and its pipeline schedules. */
155
+ const updateSettings = {
156
+ jobTokenPush: false,
157
+ schedules: []
158
+ };
159
+ /** Pipelines the update ran in: scheduled, or started through the API, with the pages job's log. */
160
+ const updateRuns = [];
161
+ /** Simulates a scheduled (or API) pipeline whose pages job logged `trace`. */
162
+ function addUpdateRun(run) {
163
+ const added = {
164
+ id: 1e3 + updateRuns.length,
165
+ source: "schedule",
166
+ status: "success",
167
+ trace: "",
168
+ ...run
169
+ };
170
+ updateRuns.push(added);
171
+ return added;
172
+ }
173
+ /** Failed jobs, by commit, which tests add with `failBuild()`. */
174
+ const failures = /* @__PURE__ */ new Map();
175
+ /**
176
+ * Simulates the pipeline for a commit failing: its `pages` job (or another,
177
+ * such as `pages:deploy`) fails for a GitLab failure reason, with a log.
178
+ */
179
+ function failBuild(sha, failure = {}) {
180
+ if (!pipelines.has(sha)) pipelines.set(sha, { checks: 0 });
181
+ failures.set(sha, {
182
+ job: failure.job ?? "pages",
183
+ failureReason: failure.failureReason ?? "script_failure",
184
+ trace: failure.trace ?? ""
185
+ });
186
+ }
187
+ const pipelineId = (sha) => [...pipelines.keys()].indexOf(sha) + 1;
30
188
  const requests = [];
31
189
  let counter = 0;
32
- function commit(changes, message) {
33
- const sha = repo.commit(changes, message);
34
- if (options.deployAfterChecks !== void 0) pipelines.set(sha, { checks: 0 });
190
+ let graphqlReads = 0;
191
+ const created = /* @__PURE__ */ new Map();
192
+ const pages = options.pages ? {
193
+ uniqueDomain: options.pages.uniqueDomain ?? true,
194
+ forceHttps: true,
195
+ primaryDomain: null,
196
+ domains: /* @__PURE__ */ new Map(),
197
+ dnsReady: /* @__PURE__ */ new Set()
198
+ } : void 0;
199
+ function pagesUrl() {
200
+ const [namespace = "", ...rest] = options.project.split("/");
201
+ return pages?.uniqueDomain ? `https://${rest.join("-")}-1a2b3c.gitlab.io` : `https://${namespace}.gitlab.io/${rest.join("/")}`;
202
+ }
203
+ /** GitLab Pages' settings and custom domains, for the site's own project. */
204
+ async function handlePages(request, rest, accessLevel) {
205
+ if (!rest.startsWith("/pages")) return void 0;
206
+ if (!pages) return json({ message: "404 Not Found" }, 404);
207
+ if (accessLevel < 40) return json({ message: "403 Forbidden" }, 403);
208
+ const method = request.method;
209
+ const domainJson = (name, domain) => ({
210
+ domain: name,
211
+ url: `https://${name}`,
212
+ verified: domain.verified,
213
+ verification_code: domain.verificationCode,
214
+ auto_ssl_enabled: true,
215
+ certificate: domain.certificate ? {
216
+ subject: `/CN=${name}`,
217
+ expired: false
218
+ } : null
219
+ });
220
+ if (rest === "/pages" && method === "GET") return json({
221
+ url: pagesUrl(),
222
+ is_unique_domain_enabled: pages.uniqueDomain,
223
+ force_https: pages.forceHttps,
224
+ primary_domain: pages.primaryDomain
225
+ });
226
+ if (rest === "/pages" && method === "PATCH") {
227
+ const body = await request.json();
228
+ if (body.pages_primary_domain !== void 0 && !pages.domains.has(body.pages_primary_domain)) return json({ message: "Primary domain must be one of the project's domains" }, 400);
229
+ if (body.pages_unique_domain_enabled !== void 0) pages.uniqueDomain = body.pages_unique_domain_enabled;
230
+ if (body.pages_https_only !== void 0) pages.forceHttps = body.pages_https_only;
231
+ if (body.pages_primary_domain !== void 0) pages.primaryDomain = body.pages_primary_domain;
232
+ return json({
233
+ url: pagesUrl(),
234
+ is_unique_domain_enabled: pages.uniqueDomain,
235
+ force_https: pages.forceHttps
236
+ });
237
+ }
238
+ if (rest === "/pages/domains" && method === "GET") return json([...pages.domains].map(([name, domain]) => domainJson(name, domain)));
239
+ if (rest === "/pages/domains" && method === "POST") {
240
+ const body = await request.json();
241
+ if (pages.domains.has(body.domain)) return json({ message: { domain: ["has already been taken"] } }, 400);
242
+ if (!/^[a-z0-9.-]+\.[a-z]+$/.test(body.domain)) return json({ message: { domain: ["is invalid"] } }, 400);
243
+ counter += 1;
244
+ const domain = {
245
+ verified: false,
246
+ verificationCode: `code-${counter}`,
247
+ certificate: false
248
+ };
249
+ pages.domains.set(body.domain, domain);
250
+ return json(domainJson(body.domain, domain), 201);
251
+ }
252
+ const one = rest.match(/^\/pages\/domains\/([^/]+)(\/verify)?$/);
253
+ const name = decodeURIComponent(one?.[1] ?? "");
254
+ const domain = pages.domains.get(name);
255
+ if (!one || !domain) return json({ message: "404 Not Found" }, 404);
256
+ if (one[2] && method === "PUT") {
257
+ if (!pages.dnsReady.has(name)) return json({ message: "Failed to verify domain ownership" }, 400);
258
+ domain.verified = true;
259
+ return json(domainJson(name, domain));
260
+ }
261
+ if (method === "GET") return json(domainJson(name, domain));
262
+ if (method === "DELETE") {
263
+ pages.domains.delete(name);
264
+ if (pages.primaryDomain === name) pages.primaryDomain = null;
265
+ return new Response(null, { status: 204 });
266
+ }
267
+ return json({ message: "404 Not Found" }, 404);
268
+ }
269
+ const groups = (options.groups ?? []).map((group, index) => ({
270
+ id: 100 + index,
271
+ path: group
272
+ }));
273
+ /** Creating projects, and committing to the projects created, as the setup page does. */
274
+ async function handleCreated(request, path, user) {
275
+ if (path === "/namespaces") return json([{
276
+ id: 1,
277
+ kind: "user",
278
+ full_path: user.username,
279
+ name: user.name ?? user.username
280
+ }, ...groups.map((group) => ({
281
+ id: group.id,
282
+ kind: "group",
283
+ full_path: group.path,
284
+ name: group.path
285
+ }))]);
286
+ if (path === "/projects" && request.method === "POST") {
287
+ const body = await request.json();
288
+ const namespace = body.namespace_id === void 0 || body.namespace_id === 1 ? user.username : groups.find((group) => group.id === body.namespace_id)?.path;
289
+ if (!namespace) return json({ message: "404 Namespace Not Found" }, 404);
290
+ if (!/^[\w.-]+$/.test(body.path)) return json({ message: { path: ["is invalid"] } }, 400);
291
+ const fullPath = `${namespace}/${body.path}`;
292
+ if (created.has(fullPath) || fullPath === options.project) return json({ message: { name: ["has already been taken"] } }, 400);
293
+ counter += 1;
294
+ const project = {
295
+ id: 1e3 + counter,
296
+ path: fullPath,
297
+ visibility: body.visibility,
298
+ description: body.description,
299
+ pagesAccessLevel: body.pages_access_level,
300
+ repo: new FakeRepo({})
301
+ };
302
+ created.set(fullPath, project);
303
+ return json({
304
+ id: project.id,
305
+ path_with_namespace: fullPath,
306
+ web_url: `${webUrl}/${fullPath}`
307
+ }, 201);
308
+ }
309
+ const match = path.match(/^\/projects\/([^/]+)(\/.*)?$/);
310
+ const key = decodeURIComponent(match?.[1] ?? "");
311
+ const project = created.get(key) ?? [...created.values()].find((candidate) => String(candidate.id) === key);
312
+ if (!match || !project) return void 0;
313
+ const rest = match[2] ?? "";
314
+ if (rest === "") return json({
315
+ id: project.id,
316
+ path_with_namespace: project.path,
317
+ default_branch: "main"
318
+ });
319
+ if (rest === "/repository/commits" && request.method === "POST") {
320
+ const body = await request.json();
321
+ return json({
322
+ id: project.repo.commit(body.actions.map((action) => action.encoding === "base64" ? {
323
+ path: action.file_path,
324
+ bytes: decodeBase64(action.content ?? "")
325
+ } : {
326
+ path: action.file_path,
327
+ content: action.content ?? ""
328
+ }), body.commit_message, { branch: body.branch }),
329
+ message: body.commit_message
330
+ }, 201);
331
+ }
332
+ return json({ message: "404 Not Found" }, 404);
333
+ }
334
+ function commit(changes, message, author, branch = repo.defaultBranch) {
335
+ const sha = repo.commit(changes, message, {
336
+ author,
337
+ branch
338
+ });
339
+ if (options.deployAfterChecks !== void 0 && branch === repo.defaultBranch) pipelines.set(sha, { checks: 0 });
35
340
  return sha;
36
341
  }
37
342
  function issueTokens(user) {
@@ -89,6 +394,22 @@ function fakeGitLab(options) {
89
394
  }
90
395
  return json({ error: "unsupported_grant_type" }, 400);
91
396
  }
397
+ if (url.pathname === "/api/graphql" && request.method === "POST") {
398
+ if (!tokens.has(request.headers.get("authorization")?.replace(/^Bearer /, "") ?? "")) return json({ message: "401 Unauthorized" }, 401);
399
+ const { variables } = await request.json();
400
+ if (variables.project !== options.project) return json({ data: { project: null } });
401
+ graphqlReads += 1;
402
+ const files = repo.files(variables.ref);
403
+ return json({ data: { project: { repository: { blobs: { nodes: variables.paths.flatMap((path) => {
404
+ const content = files.get(path);
405
+ if (content === void 0) return [];
406
+ return [{
407
+ path,
408
+ rawTextBlob: typeof content === "string" ? content : null,
409
+ size: String(fakeFileBytes(content).length)
410
+ }];
411
+ }) } } } } });
412
+ }
92
413
  if (!url.pathname.startsWith("/api/v4/")) return json({ message: "404 Not Found" }, 404);
93
414
  const user = tokens.get(request.headers.get("authorization")?.replace(/^Bearer /, "") ?? "");
94
415
  if (!user) return json({ message: "401 Unauthorized" }, 401);
@@ -98,22 +419,89 @@ function fakeGitLab(options) {
98
419
  name: user.name,
99
420
  avatar_url: `https://avatars.example/${user.username}`
100
421
  });
422
+ const setup = await handleCreated(request, path, user);
423
+ if (setup) return setup;
424
+ if (path === "/projects" && request.method === "GET") {
425
+ const level = members.get(accounts.get(user.username) ?? 0)?.accessLevel ?? 0;
426
+ const min = Number(url.searchParams.get("min_access_level") ?? "0");
427
+ return json(level > 0 && level >= min ? [{
428
+ path_with_namespace: options.project,
429
+ default_branch: repo.defaultBranch
430
+ }] : []);
431
+ }
432
+ if (path === "/users") {
433
+ const name = url.searchParams.get("username") ?? "";
434
+ const id = [...accounts].find(([username]) => username.toLowerCase() === name.toLowerCase())?.[1];
435
+ return json(id === void 0 ? [] : [{
436
+ id,
437
+ username: name
438
+ }]);
439
+ }
101
440
  const projectMatch = path.match(/^\/projects\/([^/]+)(\/.*)?$/);
102
441
  if (!projectMatch || decodeURIComponent(projectMatch[1] ?? "") !== options.project) return json({ message: "404 Project Not Found" }, 404);
103
442
  const rest = projectMatch[2] ?? "";
104
- const accessLevel = user.accessLevel ?? 40;
105
- if (rest === "") return json({
443
+ const accessLevel = members.get(accounts.get(user.username) ?? 0)?.accessLevel ?? 0;
444
+ const pagesResponse = await handlePages(request, rest, accessLevel);
445
+ if (pagesResponse) return pagesResponse;
446
+ const membersResponse = await handleMembers(request, url, rest, accessLevel);
447
+ if (membersResponse) return membersResponse;
448
+ if (rest === "" && request.method !== "PUT") return json({
106
449
  default_branch: repo.defaultBranch,
107
450
  permissions: {
108
451
  project_access: { access_level: accessLevel },
109
452
  group_access: null
110
- }
453
+ },
454
+ ci_push_repository_for_job_token_allowed: updateSettings.jobTokenPush
111
455
  });
456
+ if (rest === "/repository/branches" && request.method === "POST") {
457
+ if (accessLevel < 30) return json({ message: "403 Forbidden" }, 403);
458
+ const name = url.searchParams.get("branch") ?? "";
459
+ const ref = url.searchParams.get("ref") ?? "";
460
+ if (repo.branches.has(name)) return json({ message: "Branch already exists" }, 400);
461
+ const sha = repo.branches.get(ref) ?? (repo.commitAt(ref) ? ref : void 0);
462
+ if (!sha) return json({ message: "Invalid reference name" }, 400);
463
+ repo.branches.set(name, sha);
464
+ return json({
465
+ name,
466
+ commit: { id: sha }
467
+ }, 201);
468
+ }
112
469
  const branch = rest.match(/^\/repository\/branches\/(.+)$/);
470
+ if (branch && request.method === "DELETE") {
471
+ if (accessLevel < 30) return json({ message: "403 Forbidden" }, 403);
472
+ const name = decodeURIComponent(branch[1] ?? "");
473
+ if (!repo.branches.has(name)) return json({ message: "404 Branch Not Found" }, 404);
474
+ repo.branches.delete(name);
475
+ return new Response(null, { status: 204 });
476
+ }
113
477
  if (branch) {
114
478
  const sha = repo.branches.get(decodeURIComponent(branch[1] ?? ""));
115
479
  return sha ? json({ commit: { id: sha } }) : json({ message: "404 Branch Not Found" }, 404);
116
480
  }
481
+ if (rest === "/repository/merge_base") {
482
+ const refs = url.searchParams.getAll("refs[]").map((ref) => repo.branches.get(ref) ?? ref);
483
+ if (refs.length !== 2 || refs.some((ref) => !repo.commitAt(ref))) return json({ message: "Could not find ref" }, 400);
484
+ const base = repo.mergeBase(refs[0] ?? "", refs[1] ?? "");
485
+ return base ? json({ id: base }) : json({ message: "404 Not Found" }, 404);
486
+ }
487
+ if (rest === "/merge_requests" && request.method === "POST") {
488
+ if (accessLevel < 30) return json({ message: "403 Forbidden" }, 403);
489
+ const body = await request.json();
490
+ if (!repo.branches.has(body.source_branch) || !repo.branches.has(body.target_branch)) return json({ message: ["Source branch does not exist"] }, 422);
491
+ if (repo.reviews.some((review) => review.branch === body.source_branch && review.state === "open")) return json({ message: ["Another open merge request already exists for this source branch"] }, 409);
492
+ const review = repo.openReview(body.source_branch, body.title, body.description ?? "", body.target_branch);
493
+ return json({
494
+ iid: review.number,
495
+ web_url: `${webUrl}/${options.project}/-/merge_requests/${review.number}`
496
+ }, 201);
497
+ }
498
+ if (rest === "/merge_requests" && request.method === "GET") {
499
+ const source = url.searchParams.get("source_branch");
500
+ return json(repo.reviews.filter((review) => review.state === "open" && (!source || review.branch === source)).map((review) => ({
501
+ iid: review.number,
502
+ web_url: `${webUrl}/${options.project}/-/merge_requests/${review.number}`
503
+ })));
504
+ }
117
505
  if (rest === "/repository/tree") {
118
506
  const ref = url.searchParams.get("ref") ?? repo.defaultBranch;
119
507
  const dir = url.searchParams.get("path") ?? "";
@@ -133,6 +521,13 @@ function fakeGitLab(options) {
133
521
  const content = repo.blobs.get(blob[1] ?? "");
134
522
  return content === void 0 ? json({ message: "404 Blob Not Found" }, 404) : new Response(fakeFileBytes(content));
135
523
  }
524
+ const raw = rest.match(/^\/repository\/files\/([^/]+)\/raw$/);
525
+ if (raw) {
526
+ const ref = url.searchParams.get("ref") ?? repo.defaultBranch;
527
+ if (!repo.commitAt(repo.branches.get(ref) ?? ref)) return json({ message: "404 Commit Not Found" }, 404);
528
+ const content = repo.files(ref).get(decodeURIComponent(raw[1] ?? ""));
529
+ return content === void 0 ? json({ message: "404 File Not Found" }, 404) : new Response(fakeFileBytes(content));
530
+ }
136
531
  const file = rest.match(/^\/repository\/files\/([^/]+)$/);
137
532
  if (file) {
138
533
  const filePath = decodeURIComponent(file[1] ?? "");
@@ -145,6 +540,8 @@ function fakeGitLab(options) {
145
540
  }
146
541
  if (rest === "/repository/commits" && request.method === "POST") {
147
542
  if (accessLevel < 30) return json({ message: "403 Forbidden" }, 403);
543
+ const pushTo = await request.clone().json();
544
+ if (accessLevel < (protectedBranches.get(pushTo.branch)?.push ?? 0)) return json({ message: "You are not allowed to push into this branch" }, 403);
148
545
  const body = await request.json();
149
546
  const files = repo.files(body.branch);
150
547
  for (const action of body.actions) {
@@ -162,10 +559,25 @@ function fakeGitLab(options) {
162
559
  } : {
163
560
  path: action.file_path,
164
561
  content: action.content ?? ""
165
- }), body.commit_message),
562
+ }), body.commit_message, user.name ?? user.username, body.branch),
166
563
  message: body.commit_message
167
564
  }, 201);
168
565
  }
566
+ if (rest === "/repository/commits" && request.method === "GET") {
567
+ const ref = url.searchParams.get("ref_name") ?? repo.defaultBranch;
568
+ const file = url.searchParams.get("path");
569
+ if (!repo.commitAt(repo.branches.get(ref) ?? ref)) return json({ message: "404 Reference Not Found" }, 404);
570
+ const perPage = Number(url.searchParams.get("per_page") ?? 20);
571
+ const page = Number(url.searchParams.get("page") ?? 1);
572
+ return json((file ? repo.history(file, ref) : []).slice((page - 1) * perPage, page * perPage).map((commit) => ({
573
+ id: commit.sha,
574
+ title: commit.message.split("\n")[0],
575
+ message: commit.message,
576
+ author_name: commit.author ?? "GitLab",
577
+ authored_date: commit.date,
578
+ committed_date: commit.date
579
+ })));
580
+ }
169
581
  if (rest === "/repository/compare") return json({ diffs: repo.changedPaths(url.searchParams.get("from") ?? "", url.searchParams.get("to") ?? "").map((changedPath) => ({
170
582
  old_path: changedPath,
171
583
  new_path: changedPath
@@ -174,6 +586,15 @@ function fakeGitLab(options) {
174
586
  if (statuses) {
175
587
  const pipeline = pipelines.get(statuses[1] ?? "");
176
588
  if (!pipeline) return json([]);
589
+ const failure = failures.get(statuses[1] ?? "");
590
+ if (failure) {
591
+ const id = pipelineId(statuses[1] ?? "");
592
+ return json([{
593
+ name: failure.job,
594
+ status: "failed",
595
+ target_url: `${webUrl}/${options.project}/-/jobs/${id}`
596
+ }]);
597
+ }
177
598
  pipeline.checks += 1;
178
599
  return json([{
179
600
  name: "pages",
@@ -181,14 +602,111 @@ function fakeGitLab(options) {
181
602
  target_url: `${webUrl}/${options.project}/-/jobs/1`
182
603
  }]);
183
604
  }
184
- if (rest === "/pipelines") return json([...pipelines.keys()].map((sha, index) => ({
185
- id: index + 1,
186
- sha
605
+ if (rest === "/pipelines" && url.searchParams.has("ref")) return json([...updateRuns].reverse().map((run) => ({
606
+ id: run.id,
607
+ source: run.source,
608
+ status: run.status,
609
+ created_at: "2026-01-05T04:17:00Z",
610
+ web_url: `${webUrl}/${options.project}/-/pipelines/${run.id}`
187
611
  })));
612
+ if (rest === "/pipeline" && request.method === "POST") {
613
+ if (accessLevel < 30) return json({ message: "403 Forbidden" }, 403);
614
+ const run = addUpdateRun({
615
+ source: "api",
616
+ status: "created",
617
+ variables: (await request.json()).variables
618
+ });
619
+ return json({
620
+ id: run.id,
621
+ status: run.status
622
+ }, 201);
623
+ }
624
+ if (rest === "/pipeline_schedules") {
625
+ if (accessLevel < 30) return json({ message: "403 Forbidden" }, 403);
626
+ if (request.method === "POST") {
627
+ const body = await request.json();
628
+ const schedule = {
629
+ id: updateSettings.schedules.length + 1,
630
+ ref: body.ref,
631
+ cron: body.cron,
632
+ active: body.active ?? true
633
+ };
634
+ updateSettings.schedules.push(schedule);
635
+ return json(schedule, 201);
636
+ }
637
+ return json(updateSettings.schedules);
638
+ }
639
+ if (rest === "" && request.method === "PUT") {
640
+ if (accessLevel < 40) return json({ message: "403 Forbidden" }, 403);
641
+ const body = await request.json();
642
+ if (body.ci_push_repository_for_job_token_allowed !== void 0) updateSettings.jobTokenPush = body.ci_push_repository_for_job_token_allowed;
643
+ return json({ ci_push_repository_for_job_token_allowed: updateSettings.jobTokenPush });
644
+ }
645
+ const updateJobs = rest.match(/^\/pipelines\/(\d{4,})\/jobs$/);
646
+ if (updateJobs) {
647
+ const run = updateRuns.find((candidate) => candidate.id === Number(updateJobs[1]));
648
+ return run ? json([{
649
+ id: run.id,
650
+ name: "pages",
651
+ status: run.status
652
+ }]) : json({ message: "404 Not Found" }, 404);
653
+ }
654
+ const updateTrace = rest.match(/^\/jobs\/(\d{4,})\/trace$/);
655
+ if (updateTrace) {
656
+ const run = updateRuns.find((candidate) => candidate.id === Number(updateTrace[1]));
657
+ return run ? new Response(run.trace) : json({ message: "404 Not Found" }, 404);
658
+ }
659
+ if (rest === "/pipelines") {
660
+ const sha = url.searchParams.get("sha");
661
+ return json([...pipelines.keys()].map((pipelineSha, index) => ({
662
+ id: index + 1,
663
+ sha: pipelineSha
664
+ })).filter((pipeline) => !sha || pipeline.sha === sha));
665
+ }
666
+ const pipelineJobs = rest.match(/^\/pipelines\/(\d+)\/jobs$/);
667
+ if (pipelineJobs) {
668
+ const id = Number(pipelineJobs[1]);
669
+ const sha = [...pipelines.keys()][id - 1];
670
+ if (!sha) return json({ message: "404 Not Found" }, 404);
671
+ const failure = failures.get(sha);
672
+ return json([failure ? {
673
+ id,
674
+ name: failure.job,
675
+ status: "failed",
676
+ failure_reason: failure.failureReason
677
+ } : {
678
+ id,
679
+ name: "pages",
680
+ status: "success"
681
+ }]);
682
+ }
683
+ const trace = rest.match(/^\/jobs\/(\d+)\/trace$/);
684
+ if (trace) {
685
+ const sha = [...pipelines.keys()][Number(trace[1]) - 1];
686
+ const failure = sha ? failures.get(sha) : void 0;
687
+ return failure ? new Response(failure.trace) : json({ message: "404 Not Found" }, 404);
688
+ }
188
689
  return json({ message: "404 Not Found" }, 404);
189
690
  }
190
691
  return {
692
+ /** How many GraphQL requests read files, as `readFiles()` makes. */
693
+ get graphqlReads() {
694
+ return graphqlReads;
695
+ },
696
+ /** The project's members by user id, invitations by email, and protected branches, which tests can change. */
697
+ members,
698
+ invitations,
699
+ protectedBranches,
700
+ failBuild,
701
+ /** The project's update settings, and the pipelines the update ran in, which tests add with `addUpdateRun()`. */
702
+ updateSettings,
703
+ updateRuns,
704
+ addUpdateRun,
191
705
  repo,
706
+ /** Projects created through the API, by full path. */
707
+ created,
708
+ /** The site's GitLab Pages, if it has them, which tests can change. */
709
+ pages,
192
710
  requests,
193
711
  commit,
194
712
  authorize,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodfellow-cms/gitlab",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Edit a Goodfellow site stored on GitLab, straight from the browser.",
5
5
  "keywords": [
6
6
  "goodfellow",
@@ -10,12 +10,12 @@
10
10
  "gitlab",
11
11
  "git-based-cms"
12
12
  ],
13
- "homepage": "https://chartung17.github.io/goodfellow-cms/docs/hosts",
14
- "bugs": "https://github.com/chartung17/goodfellow-cms/issues",
13
+ "homepage": "https://goodfellow-cms.github.io/goodfellow-cms/docs/hosts",
14
+ "bugs": "https://github.com/goodfellow-cms/goodfellow-cms/issues",
15
15
  "license": "MIT",
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "git+https://github.com/chartung17/goodfellow-cms.git",
18
+ "url": "git+https://github.com/goodfellow-cms/goodfellow-cms.git",
19
19
  "directory": "packages/gitlab"
20
20
  },
21
21
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  "dist"
36
36
  ],
37
37
  "dependencies": {
38
- "@goodfellow-cms/core": "0.1.0"
38
+ "@goodfellow-cms/core": "0.3.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@puckeditor/core": "0.23.0"