@goodfellow-cms/gitlab 0.1.0 → 0.2.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.d.ts CHANGED
@@ -21,11 +21,108 @@ export interface FakeGitLabOptions {
21
21
  };
22
22
  /** Simulates a GitLab Pages pipeline that succeeds after being checked this many times. Off by default. */
23
23
  deployAfterChecks?: number;
24
+ /** Groups the users belong to, which new projects can be created in. */
25
+ groups?: string[];
26
+ /** Publishes the site with GitLab Pages. Off by default. */
27
+ pages?: {
28
+ uniqueDomain?: boolean;
29
+ };
30
+ /** Other GitLab accounts that exist, which can be added to the project. Every token's user exists and is a member. */
31
+ accounts?: string[];
32
+ }
33
+ /** A member of the site's project. */
34
+ export interface FakeMember {
35
+ id: number;
36
+ username: string;
37
+ name?: string;
38
+ accessLevel: number;
39
+ /** A member through a group rather than the project itself. */
40
+ inherited?: boolean;
41
+ }
42
+ /** A custom domain added to the site's GitLab Pages. */
43
+ export interface FakePagesDomain {
44
+ verified: boolean;
45
+ verificationCode: string;
46
+ /** Whether Let's Encrypt has issued the certificate. */
47
+ certificate: boolean;
48
+ }
49
+ /** The site's GitLab Pages settings, which tests can change. */
50
+ export interface FakeGitLabPages {
51
+ uniqueDomain: boolean;
52
+ forceHttps: boolean;
53
+ primaryDomain: string | null;
54
+ domains: Map<string, FakePagesDomain>;
55
+ /** Domains whose TXT record GitLab would find, so verifying them works. */
56
+ dnsReady: Set<string>;
57
+ }
58
+ /** A project created through the fake's API, as the setup page creates them. */
59
+ export interface FakeCreatedProject {
60
+ id: number;
61
+ path: string;
62
+ visibility: string;
63
+ description?: string;
64
+ pagesAccessLevel?: string;
65
+ repo: FakeRepo;
24
66
  }
25
67
  export declare function fakeGitLab(options: FakeGitLabOptions): {
68
+ /** The project's members by user id, invitations by email, and protected branches, which tests can change. */
69
+ members: Map<number, FakeMember>;
70
+ invitations: Map<string, number>;
71
+ protectedBranches: Map<string, {
72
+ push: number;
73
+ merge: number;
74
+ forcePush: boolean;
75
+ }>;
76
+ failBuild: (sha: string, failure?: {
77
+ job?: string;
78
+ failureReason?: string;
79
+ trace?: string;
80
+ }) => void;
81
+ /** The project's update settings, and the pipelines the update ran in, which tests add with `addUpdateRun()`. */
82
+ updateSettings: {
83
+ jobTokenPush: boolean;
84
+ schedules: Array<{
85
+ id: number;
86
+ ref: string;
87
+ cron: string;
88
+ active: boolean;
89
+ }>;
90
+ };
91
+ updateRuns: {
92
+ id: number;
93
+ source: string;
94
+ status: string;
95
+ variables?: Array<{
96
+ key: string;
97
+ value: string;
98
+ }>;
99
+ trace: string;
100
+ }[];
101
+ addUpdateRun: (run: {
102
+ source?: string;
103
+ status?: string;
104
+ trace?: string;
105
+ variables?: Array<{
106
+ key: string;
107
+ value: string;
108
+ }>;
109
+ }) => {
110
+ variables?: Array<{
111
+ key: string;
112
+ value: string;
113
+ }>;
114
+ id: number;
115
+ source: string;
116
+ status: string;
117
+ trace: string;
118
+ };
26
119
  repo: FakeRepo;
120
+ /** Projects created through the API, by full path. */
121
+ created: Map<string, FakeCreatedProject>;
122
+ /** The site's GitLab Pages, if it has them, which tests can change. */
123
+ pages: FakeGitLabPages | undefined;
27
124
  requests: Request[];
28
- commit: (changes: FileChange[], message: string) => string;
125
+ commit: (changes: FileChange[], message: string, author?: string) => string;
29
126
  authorize: (authorizeUrl: string) => string;
30
127
  handle: (request: Request) => Promise<Response>;
31
128
  fetch: typeof fetch;
package/dist/testing.js CHANGED
@@ -24,13 +24,314 @@ 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);
190
+ const created = /* @__PURE__ */ new Map();
191
+ const pages = options.pages ? {
192
+ uniqueDomain: options.pages.uniqueDomain ?? true,
193
+ forceHttps: true,
194
+ primaryDomain: null,
195
+ domains: /* @__PURE__ */ new Map(),
196
+ dnsReady: /* @__PURE__ */ new Set()
197
+ } : void 0;
198
+ function pagesUrl() {
199
+ const [namespace = "", ...rest] = options.project.split("/");
200
+ return pages?.uniqueDomain ? `https://${rest.join("-")}-1a2b3c.gitlab.io` : `https://${namespace}.gitlab.io/${rest.join("/")}`;
201
+ }
202
+ /** GitLab Pages' settings and custom domains, for the site's own project. */
203
+ async function handlePages(request, rest, accessLevel) {
204
+ if (!rest.startsWith("/pages")) return void 0;
205
+ if (!pages) return json({ message: "404 Not Found" }, 404);
206
+ if (accessLevel < 40) return json({ message: "403 Forbidden" }, 403);
207
+ const method = request.method;
208
+ const domainJson = (name, domain) => ({
209
+ domain: name,
210
+ url: `https://${name}`,
211
+ verified: domain.verified,
212
+ verification_code: domain.verificationCode,
213
+ auto_ssl_enabled: true,
214
+ certificate: domain.certificate ? {
215
+ subject: `/CN=${name}`,
216
+ expired: false
217
+ } : null
218
+ });
219
+ if (rest === "/pages" && method === "GET") return json({
220
+ url: pagesUrl(),
221
+ is_unique_domain_enabled: pages.uniqueDomain,
222
+ force_https: pages.forceHttps,
223
+ primary_domain: pages.primaryDomain
224
+ });
225
+ if (rest === "/pages" && method === "PATCH") {
226
+ const body = await request.json();
227
+ 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);
228
+ if (body.pages_unique_domain_enabled !== void 0) pages.uniqueDomain = body.pages_unique_domain_enabled;
229
+ if (body.pages_https_only !== void 0) pages.forceHttps = body.pages_https_only;
230
+ if (body.pages_primary_domain !== void 0) pages.primaryDomain = body.pages_primary_domain;
231
+ return json({
232
+ url: pagesUrl(),
233
+ is_unique_domain_enabled: pages.uniqueDomain,
234
+ force_https: pages.forceHttps
235
+ });
236
+ }
237
+ if (rest === "/pages/domains" && method === "GET") return json([...pages.domains].map(([name, domain]) => domainJson(name, domain)));
238
+ if (rest === "/pages/domains" && method === "POST") {
239
+ const body = await request.json();
240
+ if (pages.domains.has(body.domain)) return json({ message: { domain: ["has already been taken"] } }, 400);
241
+ if (!/^[a-z0-9.-]+\.[a-z]+$/.test(body.domain)) return json({ message: { domain: ["is invalid"] } }, 400);
242
+ counter += 1;
243
+ const domain = {
244
+ verified: false,
245
+ verificationCode: `code-${counter}`,
246
+ certificate: false
247
+ };
248
+ pages.domains.set(body.domain, domain);
249
+ return json(domainJson(body.domain, domain), 201);
250
+ }
251
+ const one = rest.match(/^\/pages\/domains\/([^/]+)(\/verify)?$/);
252
+ const name = decodeURIComponent(one?.[1] ?? "");
253
+ const domain = pages.domains.get(name);
254
+ if (!one || !domain) return json({ message: "404 Not Found" }, 404);
255
+ if (one[2] && method === "PUT") {
256
+ if (!pages.dnsReady.has(name)) return json({ message: "Failed to verify domain ownership" }, 400);
257
+ domain.verified = true;
258
+ return json(domainJson(name, domain));
259
+ }
260
+ if (method === "GET") return json(domainJson(name, domain));
261
+ if (method === "DELETE") {
262
+ pages.domains.delete(name);
263
+ if (pages.primaryDomain === name) pages.primaryDomain = null;
264
+ return new Response(null, { status: 204 });
265
+ }
266
+ return json({ message: "404 Not Found" }, 404);
267
+ }
268
+ const groups = (options.groups ?? []).map((group, index) => ({
269
+ id: 100 + index,
270
+ path: group
271
+ }));
272
+ /** Creating projects, and committing to the projects created, as the setup page does. */
273
+ async function handleCreated(request, path, user) {
274
+ if (path === "/namespaces") return json([{
275
+ id: 1,
276
+ kind: "user",
277
+ full_path: user.username,
278
+ name: user.name ?? user.username
279
+ }, ...groups.map((group) => ({
280
+ id: group.id,
281
+ kind: "group",
282
+ full_path: group.path,
283
+ name: group.path
284
+ }))]);
285
+ if (path === "/projects" && request.method === "POST") {
286
+ const body = await request.json();
287
+ const namespace = body.namespace_id === void 0 || body.namespace_id === 1 ? user.username : groups.find((group) => group.id === body.namespace_id)?.path;
288
+ if (!namespace) return json({ message: "404 Namespace Not Found" }, 404);
289
+ if (!/^[\w.-]+$/.test(body.path)) return json({ message: { path: ["is invalid"] } }, 400);
290
+ const fullPath = `${namespace}/${body.path}`;
291
+ if (created.has(fullPath) || fullPath === options.project) return json({ message: { name: ["has already been taken"] } }, 400);
292
+ counter += 1;
293
+ const project = {
294
+ id: 1e3 + counter,
295
+ path: fullPath,
296
+ visibility: body.visibility,
297
+ description: body.description,
298
+ pagesAccessLevel: body.pages_access_level,
299
+ repo: new FakeRepo({})
300
+ };
301
+ created.set(fullPath, project);
302
+ return json({
303
+ id: project.id,
304
+ path_with_namespace: fullPath,
305
+ web_url: `${webUrl}/${fullPath}`
306
+ }, 201);
307
+ }
308
+ const match = path.match(/^\/projects\/([^/]+)(\/.*)?$/);
309
+ const key = decodeURIComponent(match?.[1] ?? "");
310
+ const project = created.get(key) ?? [...created.values()].find((candidate) => String(candidate.id) === key);
311
+ if (!match || !project) return void 0;
312
+ const rest = match[2] ?? "";
313
+ if (rest === "") return json({
314
+ id: project.id,
315
+ path_with_namespace: project.path,
316
+ default_branch: "main"
317
+ });
318
+ if (rest === "/repository/commits" && request.method === "POST") {
319
+ const body = await request.json();
320
+ return json({
321
+ id: project.repo.commit(body.actions.map((action) => action.encoding === "base64" ? {
322
+ path: action.file_path,
323
+ bytes: decodeBase64(action.content ?? "")
324
+ } : {
325
+ path: action.file_path,
326
+ content: action.content ?? ""
327
+ }), body.commit_message, { branch: body.branch }),
328
+ message: body.commit_message
329
+ }, 201);
330
+ }
331
+ return json({ message: "404 Not Found" }, 404);
332
+ }
333
+ function commit(changes, message, author) {
334
+ const sha = repo.commit(changes, message, { author });
34
335
  if (options.deployAfterChecks !== void 0) pipelines.set(sha, { checks: 0 });
35
336
  return sha;
36
337
  }
@@ -98,16 +399,31 @@ function fakeGitLab(options) {
98
399
  name: user.name,
99
400
  avatar_url: `https://avatars.example/${user.username}`
100
401
  });
402
+ const setup = await handleCreated(request, path, user);
403
+ if (setup) return setup;
404
+ if (path === "/users") {
405
+ const name = url.searchParams.get("username") ?? "";
406
+ const id = [...accounts].find(([username]) => username.toLowerCase() === name.toLowerCase())?.[1];
407
+ return json(id === void 0 ? [] : [{
408
+ id,
409
+ username: name
410
+ }]);
411
+ }
101
412
  const projectMatch = path.match(/^\/projects\/([^/]+)(\/.*)?$/);
102
413
  if (!projectMatch || decodeURIComponent(projectMatch[1] ?? "") !== options.project) return json({ message: "404 Project Not Found" }, 404);
103
414
  const rest = projectMatch[2] ?? "";
104
- const accessLevel = user.accessLevel ?? 40;
105
- if (rest === "") return json({
415
+ const accessLevel = members.get(accounts.get(user.username) ?? 0)?.accessLevel ?? 0;
416
+ const pagesResponse = await handlePages(request, rest, accessLevel);
417
+ if (pagesResponse) return pagesResponse;
418
+ const membersResponse = await handleMembers(request, url, rest, accessLevel);
419
+ if (membersResponse) return membersResponse;
420
+ if (rest === "" && request.method !== "PUT") return json({
106
421
  default_branch: repo.defaultBranch,
107
422
  permissions: {
108
423
  project_access: { access_level: accessLevel },
109
424
  group_access: null
110
- }
425
+ },
426
+ ci_push_repository_for_job_token_allowed: updateSettings.jobTokenPush
111
427
  });
112
428
  const branch = rest.match(/^\/repository\/branches\/(.+)$/);
113
429
  if (branch) {
@@ -133,6 +449,13 @@ function fakeGitLab(options) {
133
449
  const content = repo.blobs.get(blob[1] ?? "");
134
450
  return content === void 0 ? json({ message: "404 Blob Not Found" }, 404) : new Response(fakeFileBytes(content));
135
451
  }
452
+ const raw = rest.match(/^\/repository\/files\/([^/]+)\/raw$/);
453
+ if (raw) {
454
+ const ref = url.searchParams.get("ref") ?? repo.defaultBranch;
455
+ if (!repo.commitAt(repo.branches.get(ref) ?? ref)) return json({ message: "404 Commit Not Found" }, 404);
456
+ const content = repo.files(ref).get(decodeURIComponent(raw[1] ?? ""));
457
+ return content === void 0 ? json({ message: "404 File Not Found" }, 404) : new Response(fakeFileBytes(content));
458
+ }
136
459
  const file = rest.match(/^\/repository\/files\/([^/]+)$/);
137
460
  if (file) {
138
461
  const filePath = decodeURIComponent(file[1] ?? "");
@@ -145,6 +468,8 @@ function fakeGitLab(options) {
145
468
  }
146
469
  if (rest === "/repository/commits" && request.method === "POST") {
147
470
  if (accessLevel < 30) return json({ message: "403 Forbidden" }, 403);
471
+ const pushTo = await request.clone().json();
472
+ if (accessLevel < (protectedBranches.get(pushTo.branch)?.push ?? 0)) return json({ message: "You are not allowed to push into this branch" }, 403);
148
473
  const body = await request.json();
149
474
  const files = repo.files(body.branch);
150
475
  for (const action of body.actions) {
@@ -162,10 +487,25 @@ function fakeGitLab(options) {
162
487
  } : {
163
488
  path: action.file_path,
164
489
  content: action.content ?? ""
165
- }), body.commit_message),
490
+ }), body.commit_message, user.name ?? user.username),
166
491
  message: body.commit_message
167
492
  }, 201);
168
493
  }
494
+ if (rest === "/repository/commits" && request.method === "GET") {
495
+ const ref = url.searchParams.get("ref_name") ?? repo.defaultBranch;
496
+ const file = url.searchParams.get("path");
497
+ if (!repo.commitAt(repo.branches.get(ref) ?? ref)) return json({ message: "404 Reference Not Found" }, 404);
498
+ const perPage = Number(url.searchParams.get("per_page") ?? 20);
499
+ const page = Number(url.searchParams.get("page") ?? 1);
500
+ return json((file ? repo.history(file, ref) : []).slice((page - 1) * perPage, page * perPage).map((commit) => ({
501
+ id: commit.sha,
502
+ title: commit.message.split("\n")[0],
503
+ message: commit.message,
504
+ author_name: commit.author ?? "GitLab",
505
+ authored_date: commit.date,
506
+ committed_date: commit.date
507
+ })));
508
+ }
169
509
  if (rest === "/repository/compare") return json({ diffs: repo.changedPaths(url.searchParams.get("from") ?? "", url.searchParams.get("to") ?? "").map((changedPath) => ({
170
510
  old_path: changedPath,
171
511
  new_path: changedPath
@@ -174,6 +514,15 @@ function fakeGitLab(options) {
174
514
  if (statuses) {
175
515
  const pipeline = pipelines.get(statuses[1] ?? "");
176
516
  if (!pipeline) return json([]);
517
+ const failure = failures.get(statuses[1] ?? "");
518
+ if (failure) {
519
+ const id = pipelineId(statuses[1] ?? "");
520
+ return json([{
521
+ name: failure.job,
522
+ status: "failed",
523
+ target_url: `${webUrl}/${options.project}/-/jobs/${id}`
524
+ }]);
525
+ }
177
526
  pipeline.checks += 1;
178
527
  return json([{
179
528
  name: "pages",
@@ -181,14 +530,107 @@ function fakeGitLab(options) {
181
530
  target_url: `${webUrl}/${options.project}/-/jobs/1`
182
531
  }]);
183
532
  }
184
- if (rest === "/pipelines") return json([...pipelines.keys()].map((sha, index) => ({
185
- id: index + 1,
186
- sha
533
+ if (rest === "/pipelines" && url.searchParams.has("ref")) return json([...updateRuns].reverse().map((run) => ({
534
+ id: run.id,
535
+ source: run.source,
536
+ status: run.status,
537
+ created_at: "2026-01-05T04:17:00Z",
538
+ web_url: `${webUrl}/${options.project}/-/pipelines/${run.id}`
187
539
  })));
540
+ if (rest === "/pipeline" && request.method === "POST") {
541
+ if (accessLevel < 30) return json({ message: "403 Forbidden" }, 403);
542
+ const run = addUpdateRun({
543
+ source: "api",
544
+ status: "created",
545
+ variables: (await request.json()).variables
546
+ });
547
+ return json({
548
+ id: run.id,
549
+ status: run.status
550
+ }, 201);
551
+ }
552
+ if (rest === "/pipeline_schedules") {
553
+ if (accessLevel < 30) return json({ message: "403 Forbidden" }, 403);
554
+ if (request.method === "POST") {
555
+ const body = await request.json();
556
+ const schedule = {
557
+ id: updateSettings.schedules.length + 1,
558
+ ref: body.ref,
559
+ cron: body.cron,
560
+ active: body.active ?? true
561
+ };
562
+ updateSettings.schedules.push(schedule);
563
+ return json(schedule, 201);
564
+ }
565
+ return json(updateSettings.schedules);
566
+ }
567
+ if (rest === "" && request.method === "PUT") {
568
+ if (accessLevel < 40) return json({ message: "403 Forbidden" }, 403);
569
+ const body = await request.json();
570
+ if (body.ci_push_repository_for_job_token_allowed !== void 0) updateSettings.jobTokenPush = body.ci_push_repository_for_job_token_allowed;
571
+ return json({ ci_push_repository_for_job_token_allowed: updateSettings.jobTokenPush });
572
+ }
573
+ const updateJobs = rest.match(/^\/pipelines\/(\d{4,})\/jobs$/);
574
+ if (updateJobs) {
575
+ const run = updateRuns.find((candidate) => candidate.id === Number(updateJobs[1]));
576
+ return run ? json([{
577
+ id: run.id,
578
+ name: "pages",
579
+ status: run.status
580
+ }]) : json({ message: "404 Not Found" }, 404);
581
+ }
582
+ const updateTrace = rest.match(/^\/jobs\/(\d{4,})\/trace$/);
583
+ if (updateTrace) {
584
+ const run = updateRuns.find((candidate) => candidate.id === Number(updateTrace[1]));
585
+ return run ? new Response(run.trace) : json({ message: "404 Not Found" }, 404);
586
+ }
587
+ if (rest === "/pipelines") {
588
+ const sha = url.searchParams.get("sha");
589
+ return json([...pipelines.keys()].map((pipelineSha, index) => ({
590
+ id: index + 1,
591
+ sha: pipelineSha
592
+ })).filter((pipeline) => !sha || pipeline.sha === sha));
593
+ }
594
+ const pipelineJobs = rest.match(/^\/pipelines\/(\d+)\/jobs$/);
595
+ if (pipelineJobs) {
596
+ const id = Number(pipelineJobs[1]);
597
+ const sha = [...pipelines.keys()][id - 1];
598
+ if (!sha) return json({ message: "404 Not Found" }, 404);
599
+ const failure = failures.get(sha);
600
+ return json([failure ? {
601
+ id,
602
+ name: failure.job,
603
+ status: "failed",
604
+ failure_reason: failure.failureReason
605
+ } : {
606
+ id,
607
+ name: "pages",
608
+ status: "success"
609
+ }]);
610
+ }
611
+ const trace = rest.match(/^\/jobs\/(\d+)\/trace$/);
612
+ if (trace) {
613
+ const sha = [...pipelines.keys()][Number(trace[1]) - 1];
614
+ const failure = sha ? failures.get(sha) : void 0;
615
+ return failure ? new Response(failure.trace) : json({ message: "404 Not Found" }, 404);
616
+ }
188
617
  return json({ message: "404 Not Found" }, 404);
189
618
  }
190
619
  return {
620
+ /** The project's members by user id, invitations by email, and protected branches, which tests can change. */
621
+ members,
622
+ invitations,
623
+ protectedBranches,
624
+ failBuild,
625
+ /** The project's update settings, and the pipelines the update ran in, which tests add with `addUpdateRun()`. */
626
+ updateSettings,
627
+ updateRuns,
628
+ addUpdateRun,
191
629
  repo,
630
+ /** Projects created through the API, by full path. */
631
+ created,
632
+ /** The site's GitLab Pages, if it has them, which tests can change. */
633
+ pages,
192
634
  requests,
193
635
  commit,
194
636
  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.2.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.2.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@puckeditor/core": "0.23.0"