@zereight/mcp-gitlab 2.0.34 → 2.0.36

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.
@@ -2,7 +2,7 @@
2
2
  * Mock GitLab API Server for Testing
3
3
  * Implements minimal GitLab API endpoints for testing remote authorization
4
4
  */
5
- import express from 'express';
5
+ import express from "express";
6
6
  export class MockGitLabServer {
7
7
  app;
8
8
  server = null;
@@ -10,11 +10,15 @@ export class MockGitLabServer {
10
10
  requestCount = 0;
11
11
  customRouter;
12
12
  customHandlers = new Map();
13
+ // Root-level dynamic router (for OAuth paths not under /api/v4)
14
+ rootRouter;
15
+ rootHandlers = new Map();
13
16
  constructor(config) {
14
17
  this.config = config;
15
18
  this.app = express();
16
19
  this.customRouter = express.Router();
17
- // Dynamic dispatcher for custom handlers
20
+ this.rootRouter = express.Router();
21
+ // Dynamic dispatcher for /api/v4 handlers
18
22
  this.customRouter.use((req, res, next) => {
19
23
  // Create a key from method and path (relative to /api/v4)
20
24
  // req.path is already relative to the mount point
@@ -26,12 +30,24 @@ export class MockGitLabServer {
26
30
  return handler(req, res, next);
27
31
  }
28
32
  else {
29
- console.log(`[CustomRouter] No handler found for key: '${key}'. Available keys: ${Array.from(this.customHandlers.keys()).join(', ')}`);
33
+ console.log(`[CustomRouter] No handler found for key: '${key}'. Available keys: ${Array.from(this.customHandlers.keys()).join(", ")}`);
34
+ }
35
+ next();
36
+ });
37
+ // Dynamic dispatcher for root-level handlers (OAuth endpoints, well-known, etc.)
38
+ this.rootRouter.use((req, res, next) => {
39
+ const key = `${req.method.toUpperCase()}:${req.path}`;
40
+ const handler = this.rootHandlers.get(key);
41
+ if (handler) {
42
+ console.log(`[MockServer] Root handler hit: ${key}`);
43
+ return handler(req, res, next);
30
44
  }
31
45
  next();
32
46
  });
33
47
  this.setupMiddleware();
34
- this.app.use('/api/v4', this.customRouter); // Mount router on API path
48
+ // Root router must be mounted BEFORE setupRoutes() installs the catch-all
49
+ this.app.use(this.rootRouter);
50
+ this.app.use("/api/v4", this.customRouter); // Mount router on API path
35
51
  this.setupRoutes();
36
52
  }
37
53
  addMockHandler(method, path, handler) {
@@ -40,8 +56,18 @@ export class MockGitLabServer {
40
56
  console.log(`[MockServer] Adding custom handler: ${key}`);
41
57
  this.customHandlers.set(key, handler);
42
58
  }
59
+ /**
60
+ * Add a route at the instance root (not under /api/v4).
61
+ * Use this for OAuth endpoints (/oauth/*, /.well-known/*) that GitLab
62
+ * serves at the instance root rather than under the API prefix.
63
+ */
64
+ addRootHandler(method, path, handler) {
65
+ const key = `${method.toUpperCase()}:${path}`;
66
+ console.log(`[MockServer] Adding root handler: ${key}`);
67
+ this.rootHandlers.set(key, handler);
68
+ }
43
69
  clearCustomHandlers() {
44
- console.log('[MockServer] Clearing custom handlers');
70
+ console.log("[MockServer] Clearing custom handlers");
45
71
  this.customHandlers.clear();
46
72
  }
47
73
  /**
@@ -66,8 +92,8 @@ export class MockGitLabServer {
66
92
  this.app.use((req, res, next) => {
67
93
  if (this.requestCount > this.config.rateLimitAfter) {
68
94
  res.status(429).json({
69
- message: 'Rate limit exceeded',
70
- retry_after: 60
95
+ message: "Rate limit exceeded",
96
+ retry_after: 60,
71
97
  });
72
98
  return;
73
99
  }
@@ -75,10 +101,10 @@ export class MockGitLabServer {
75
101
  });
76
102
  }
77
103
  // Authentication middleware - applies to all /api/v4/* routes
78
- this.app.use('/api/v4', (req, res, next) => {
79
- const authHeader = req.headers['authorization'];
80
- const privateToken = req.headers['private-token'];
81
- const jobToken = req.headers['job-token'];
104
+ this.app.use("/api/v4", (req, res, next) => {
105
+ const authHeader = req.headers["authorization"];
106
+ const privateToken = req.headers["private-token"];
107
+ const jobToken = req.headers["job-token"];
82
108
  let token = null;
83
109
  if (jobToken) {
84
110
  token = jobToken.trim();
@@ -93,15 +119,15 @@ export class MockGitLabServer {
93
119
  }
94
120
  if (!token) {
95
121
  res.status(401).json({
96
- message: 'Unauthorized',
97
- error: 'Missing authentication token'
122
+ message: "Unauthorized",
123
+ error: "Missing authentication token",
98
124
  });
99
125
  return;
100
126
  }
101
127
  if (!this.config.validTokens.includes(token)) {
102
128
  res.status(401).json({
103
- message: 'Unauthorized',
104
- error: 'Invalid authentication token'
129
+ message: "Unauthorized",
130
+ error: "Invalid authentication token",
105
131
  });
106
132
  return;
107
133
  }
@@ -112,180 +138,180 @@ export class MockGitLabServer {
112
138
  }
113
139
  setupRoutes() {
114
140
  // GET /api/v4/user - Get current user
115
- this.app.get('/api/v4/user', (req, res) => {
116
- const token = req.gitlabToken || 'unknown';
141
+ this.app.get("/api/v4/user", (req, res) => {
142
+ const token = req.gitlabToken || "unknown";
117
143
  res.json({
118
144
  id: 1,
119
145
  username: `user_${token.substring(0, 8)}`,
120
- name: 'Test User',
121
- email: 'test@example.com',
122
- state: 'active'
146
+ name: "Test User",
147
+ email: "test@example.com",
148
+ state: "active",
123
149
  });
124
150
  });
125
151
  // GET /api/v4/projects/:projectId - Get project
126
- this.app.get('/api/v4/projects/:projectId', (req, res) => {
152
+ this.app.get("/api/v4/projects/:projectId", (req, res) => {
127
153
  const projectId = req.params.projectId;
128
154
  res.json({
129
155
  id: parseInt(projectId) || 123,
130
- name: 'Test Project',
131
- path: 'test-project',
132
- path_with_namespace: 'test-group/test-project',
133
- description: 'A mock test project',
134
- visibility: 'private',
135
- created_at: '2024-01-01T00:00:00Z',
156
+ name: "Test Project",
157
+ path: "test-project",
158
+ path_with_namespace: "test-group/test-project",
159
+ description: "A mock test project",
160
+ visibility: "private",
161
+ created_at: "2024-01-01T00:00:00Z",
136
162
  web_url: `https://gitlab.mock/project/${projectId}`,
137
163
  namespace: {
138
164
  id: 1,
139
- name: 'Test Group',
140
- path: 'test-group',
141
- kind: 'group',
142
- full_path: 'test-group'
143
- }
165
+ name: "Test Group",
166
+ path: "test-group",
167
+ kind: "group",
168
+ full_path: "test-group",
169
+ },
144
170
  });
145
171
  });
146
172
  // GET /api/v4/merge_requests - List all merge requests (global)
147
- this.app.get('/api/v4/merge_requests', (req, res) => {
173
+ this.app.get("/api/v4/merge_requests", (req, res) => {
148
174
  res.json([
149
175
  {
150
176
  id: 1,
151
177
  iid: 1,
152
178
  project_id: 123,
153
- title: 'Test MR 1',
154
- description: 'Description for MR 1',
155
- state: 'opened',
156
- created_at: '2024-01-01T00:00:00Z',
157
- updated_at: '2024-01-01T00:00:00Z',
179
+ title: "Test MR 1",
180
+ description: "Description for MR 1",
181
+ state: "opened",
182
+ created_at: "2024-01-01T00:00:00Z",
183
+ updated_at: "2024-01-01T00:00:00Z",
158
184
  merged_at: null,
159
185
  closed_at: null,
160
- target_branch: 'main',
161
- source_branch: 'feature-1',
162
- web_url: 'https://gitlab.mock/project/123/merge_requests/1',
186
+ target_branch: "main",
187
+ source_branch: "feature-1",
188
+ web_url: "https://gitlab.mock/project/123/merge_requests/1",
163
189
  merge_commit_sha: null,
164
190
  author: {
165
191
  id: 1,
166
- username: 'test-user',
167
- name: 'Test User'
168
- }
192
+ username: "test-user",
193
+ name: "Test User",
194
+ },
169
195
  },
170
196
  {
171
197
  id: 2,
172
198
  iid: 2,
173
199
  project_id: 123,
174
- title: 'Test MR 2',
175
- description: 'Description for MR 2',
176
- state: 'merged',
177
- created_at: '2024-01-02T00:00:00Z',
178
- updated_at: '2024-01-03T00:00:00Z',
179
- merged_at: '2024-01-03T00:00:00Z',
200
+ title: "Test MR 2",
201
+ description: "Description for MR 2",
202
+ state: "merged",
203
+ created_at: "2024-01-02T00:00:00Z",
204
+ updated_at: "2024-01-03T00:00:00Z",
205
+ merged_at: "2024-01-03T00:00:00Z",
180
206
  closed_at: null,
181
- target_branch: 'main',
182
- source_branch: 'feature-2',
183
- web_url: 'https://gitlab.mock/project/123/merge_requests/2',
184
- merge_commit_sha: 'abcdef1234567890',
207
+ target_branch: "main",
208
+ source_branch: "feature-2",
209
+ web_url: "https://gitlab.mock/project/123/merge_requests/2",
210
+ merge_commit_sha: "abcdef1234567890",
185
211
  author: {
186
212
  id: 1,
187
- username: 'test-user',
188
- name: 'Test User'
189
- }
190
- }
213
+ username: "test-user",
214
+ name: "Test User",
215
+ },
216
+ },
191
217
  ]);
192
218
  });
193
219
  // GET /api/v4/projects/:projectId/merge_requests - List merge requests
194
- this.app.get('/api/v4/projects/:projectId/merge_requests', (req, res) => {
220
+ this.app.get("/api/v4/projects/:projectId/merge_requests", (req, res) => {
195
221
  res.json([
196
222
  {
197
223
  id: 1,
198
224
  iid: 1,
199
225
  project_id: 123,
200
- title: 'Test MR 1',
201
- description: 'Description for MR 1',
202
- state: 'opened',
203
- created_at: '2024-01-01T00:00:00Z',
204
- updated_at: '2024-01-01T00:00:00Z',
226
+ title: "Test MR 1",
227
+ description: "Description for MR 1",
228
+ state: "opened",
229
+ created_at: "2024-01-01T00:00:00Z",
230
+ updated_at: "2024-01-01T00:00:00Z",
205
231
  merged_at: null,
206
232
  closed_at: null,
207
- target_branch: 'main',
208
- source_branch: 'feature-1',
209
- web_url: 'https://gitlab.mock/project/123/merge_requests/1',
233
+ target_branch: "main",
234
+ source_branch: "feature-1",
235
+ web_url: "https://gitlab.mock/project/123/merge_requests/1",
210
236
  merge_commit_sha: null,
211
237
  author: {
212
238
  id: 1,
213
- username: 'test-user',
214
- name: 'Test User'
215
- }
239
+ username: "test-user",
240
+ name: "Test User",
241
+ },
216
242
  },
217
243
  {
218
244
  id: 2,
219
245
  iid: 2,
220
246
  project_id: 123,
221
- title: 'Test MR 2',
222
- description: 'Description for MR 2',
223
- state: 'merged',
224
- created_at: '2024-01-02T00:00:00Z',
225
- updated_at: '2024-01-03T00:00:00Z',
226
- merged_at: '2024-01-03T00:00:00Z',
247
+ title: "Test MR 2",
248
+ description: "Description for MR 2",
249
+ state: "merged",
250
+ created_at: "2024-01-02T00:00:00Z",
251
+ updated_at: "2024-01-03T00:00:00Z",
252
+ merged_at: "2024-01-03T00:00:00Z",
227
253
  closed_at: null,
228
- target_branch: 'main',
229
- source_branch: 'feature-2',
230
- web_url: 'https://gitlab.mock/project/123/merge_requests/2',
231
- merge_commit_sha: 'abcdef1234567890',
254
+ target_branch: "main",
255
+ source_branch: "feature-2",
256
+ web_url: "https://gitlab.mock/project/123/merge_requests/2",
257
+ merge_commit_sha: "abcdef1234567890",
232
258
  author: {
233
259
  id: 1,
234
- username: 'test-user',
235
- name: 'Test User'
236
- }
237
- }
260
+ username: "test-user",
261
+ name: "Test User",
262
+ },
263
+ },
238
264
  ]);
239
265
  });
240
266
  // GET /api/v4/projects/:projectId/merge_requests/:mr_iid - Get single MR
241
- this.app.get('/api/v4/projects/:projectId/merge_requests/:mr_iid', (req, res) => {
267
+ this.app.get("/api/v4/projects/:projectId/merge_requests/:mr_iid", (req, res) => {
242
268
  const mrIid = parseInt(req.params.mr_iid);
243
269
  res.json({
244
270
  id: mrIid,
245
271
  iid: mrIid,
246
272
  title: `Test MR ${mrIid}`,
247
- state: 'opened',
248
- created_at: '2024-01-01T00:00:00Z',
273
+ state: "opened",
274
+ created_at: "2024-01-01T00:00:00Z",
249
275
  author: {
250
276
  id: 1,
251
- username: 'test-user',
252
- name: 'Test User'
277
+ username: "test-user",
278
+ name: "Test User",
253
279
  },
254
- source_branch: 'feature-branch',
255
- target_branch: 'main',
256
- merge_status: 'can_be_merged'
280
+ source_branch: "feature-branch",
281
+ target_branch: "main",
282
+ merge_status: "can_be_merged",
257
283
  });
258
284
  });
259
285
  // GET /api/v4/projects/:projectId/issues - List issues
260
- this.app.get('/api/v4/projects/:projectId/issues', (req, res) => {
286
+ this.app.get("/api/v4/projects/:projectId/issues", (req, res) => {
261
287
  const projectId = req.params.projectId;
262
288
  res.json([
263
289
  {
264
290
  id: 1,
265
291
  iid: 1,
266
292
  project_id: projectId,
267
- title: 'Test Issue 1',
268
- description: 'Test issue description',
269
- state: 'opened',
270
- created_at: '2024-01-01T00:00:00Z',
271
- updated_at: '2024-01-02T00:00:00Z',
293
+ title: "Test Issue 1",
294
+ description: "Test issue description",
295
+ state: "opened",
296
+ created_at: "2024-01-01T00:00:00Z",
297
+ updated_at: "2024-01-02T00:00:00Z",
272
298
  closed_at: null,
273
299
  web_url: `https://gitlab.mock/project/${projectId}/issues/1`,
274
300
  author: {
275
301
  id: 1,
276
- username: 'test-user',
277
- name: 'Test User',
302
+ username: "test-user",
303
+ name: "Test User",
278
304
  avatar_url: null,
279
- web_url: 'https://gitlab.mock/test-user'
305
+ web_url: "https://gitlab.mock/test-user",
280
306
  },
281
307
  assignees: [],
282
308
  labels: [],
283
- milestone: null
284
- }
309
+ milestone: null,
310
+ },
285
311
  ]);
286
312
  });
287
313
  // GET /api/v4/projects/:projectId/issues/:issue_iid - Get single issue
288
- this.app.get('/api/v4/projects/:projectId/issues/:issue_iid', (req, res) => {
314
+ this.app.get("/api/v4/projects/:projectId/issues/:issue_iid", (req, res) => {
289
315
  const issueIid = parseInt(req.params.issue_iid);
290
316
  const projectId = req.params.projectId;
291
317
  res.json({
@@ -294,113 +320,187 @@ export class MockGitLabServer {
294
320
  project_id: projectId,
295
321
  title: `Test Issue ${issueIid}`,
296
322
  description: `Description for issue ${issueIid}`,
297
- state: 'opened',
298
- created_at: '2024-01-01T00:00:00Z',
299
- updated_at: '2024-01-02T00:00:00Z',
323
+ state: "opened",
324
+ created_at: "2024-01-01T00:00:00Z",
325
+ updated_at: "2024-01-02T00:00:00Z",
300
326
  closed_at: null,
301
327
  web_url: `https://gitlab.mock/project/${projectId}/issues/${issueIid}`,
302
328
  author: {
303
329
  id: 1,
304
- username: 'test-user',
305
- name: 'Test User',
330
+ username: "test-user",
331
+ name: "Test User",
306
332
  avatar_url: null,
307
- web_url: 'https://gitlab.mock/test-user'
333
+ web_url: "https://gitlab.mock/test-user",
308
334
  },
309
335
  assignees: [],
310
336
  labels: [],
311
- milestone: null
337
+ milestone: null,
312
338
  });
313
339
  });
340
+ // Mock blob search result
341
+ const mockBlobResults = [
342
+ {
343
+ basename: "index",
344
+ data: "const searchResult = true;",
345
+ path: "src/index.ts",
346
+ filename: "index.ts",
347
+ id: null,
348
+ ref: "main",
349
+ startline: 42,
350
+ project_id: 1,
351
+ }
352
+ ];
353
+ // GET /api/v4/search - Global search
354
+ this.app.get('/api/v4/search', (req, res) => {
355
+ if (req.query.scope === 'blobs') {
356
+ res.json(mockBlobResults);
357
+ }
358
+ else {
359
+ res.json([]);
360
+ }
361
+ });
362
+ // GET /api/v4/projects/:id/search - Project-level search
363
+ this.app.get('/api/v4/projects/:id/search', (req, res) => {
364
+ if (req.query.scope === 'blobs') {
365
+ // Return req.params.id as string — Express decodes the URL param, so this
366
+ // reflects whether the caller properly single-encoded the path segment.
367
+ const results = mockBlobResults.map(r => ({ ...r, project_id: req.params.id }));
368
+ res.json(results);
369
+ }
370
+ else {
371
+ res.json([]);
372
+ }
373
+ });
374
+ // GET /api/v4/groups/:id/search - Group-level search
375
+ this.app.get('/api/v4/groups/:id/search', (req, res) => {
376
+ if (req.query.scope === 'blobs') {
377
+ // Echo the decoded group ID in the ref field so tests can verify encoding
378
+ const results = mockBlobResults.map(r => ({ ...r, ref: req.params.id }));
379
+ res.json(results);
380
+ }
381
+ else {
382
+ res.json([]);
383
+ }
384
+ });
314
385
  // GET /api/v4/projects - List projects
315
- this.app.get('/api/v4/projects', (req, res) => {
386
+ this.app.get("/api/v4/projects", (req, res) => {
316
387
  res.json([
317
388
  {
318
389
  id: 123,
319
- name: 'Test Project',
320
- path: 'test-project',
321
- path_with_namespace: 'test-group/test-project',
322
- description: 'A mock test project',
323
- visibility: 'private',
390
+ name: "Test Project",
391
+ path: "test-project",
392
+ path_with_namespace: "test-group/test-project",
393
+ description: "A mock test project",
394
+ visibility: "private",
324
395
  namespace: {
325
396
  id: 1,
326
- name: 'Test Group',
327
- path: 'test-group',
328
- kind: 'group',
329
- full_path: 'test-group'
330
- }
331
- }
397
+ name: "Test Group",
398
+ path: "test-group",
399
+ kind: "group",
400
+ full_path: "test-group",
401
+ },
402
+ },
332
403
  ]);
333
404
  });
334
405
  // GET /api/v4/projects/:projectId/merge_requests/:mr_iid/changes - Get MR diffs
335
- this.app.get('/api/v4/projects/:projectId/merge_requests/:mr_iid/changes', (req, res) => {
406
+ this.app.get("/api/v4/projects/:projectId/merge_requests/:mr_iid/changes", (req, res) => {
336
407
  const mrIid = parseInt(req.params.mr_iid);
337
408
  res.json({
338
409
  id: mrIid,
339
410
  iid: mrIid,
340
411
  project_id: parseInt(req.params.projectId),
341
412
  title: `Test MR ${mrIid}`,
342
- state: 'opened',
343
- created_at: '2024-01-01T00:00:00Z',
413
+ state: "opened",
414
+ created_at: "2024-01-01T00:00:00Z",
344
415
  changes: [
345
416
  {
346
- old_path: 'src/index.ts',
347
- new_path: 'src/index.ts',
348
- a_mode: '100644',
349
- b_mode: '100644',
350
- diff: '@@ -1,1 +1,2 @@\n-line 1\n+line 1 modified\n+new line 2\n',
417
+ old_path: "src/index.ts",
418
+ new_path: "src/index.ts",
419
+ a_mode: "100644",
420
+ b_mode: "100644",
421
+ diff: "@@ -1,1 +1,2 @@\n-line 1\n+line 1 modified\n+new line 2\n",
351
422
  new_file: false,
352
423
  renamed_file: false,
353
- deleted_file: false
424
+ deleted_file: false,
354
425
  },
355
426
  {
356
- old_path: 'vendor/package/file.js',
357
- new_path: 'vendor/package/file.js',
358
- a_mode: '100644',
359
- b_mode: '100644',
360
- diff: '@@ -1,1 +1,1 @@\n-vendor content old\n+vendor content new\n',
427
+ old_path: "vendor/package/file.js",
428
+ new_path: "vendor/package/file.js",
429
+ a_mode: "100644",
430
+ b_mode: "100644",
431
+ diff: "@@ -1,1 +1,1 @@\n-vendor content old\n+vendor content new\n",
361
432
  new_file: false,
362
433
  renamed_file: false,
363
- deleted_file: false
434
+ deleted_file: false,
364
435
  },
365
436
  {
366
- old_path: 'README.md',
367
- new_path: 'README.md',
368
- a_mode: '100644',
369
- b_mode: '100644',
370
- diff: '@@ -1,1 +1,1 @@\n-old readme\n+new readme\n',
437
+ old_path: "README.md",
438
+ new_path: "README.md",
439
+ a_mode: "100644",
440
+ b_mode: "100644",
441
+ diff: "@@ -1,1 +1,1 @@\n-old readme\n+new readme\n",
371
442
  new_file: false,
372
443
  renamed_file: false,
373
- deleted_file: false
444
+ deleted_file: false,
374
445
  },
375
446
  {
376
- old_path: 'package-lock.json',
377
- new_path: 'package-lock.json',
378
- a_mode: '100644',
379
- b_mode: '100644',
447
+ old_path: "package-lock.json",
448
+ new_path: "package-lock.json",
449
+ a_mode: "100644",
450
+ b_mode: "100644",
380
451
  diff: '{\n- "version": "1.0.0"\n+ "version": "1.0.1"\n}\n',
381
452
  new_file: false,
382
453
  renamed_file: false,
383
- deleted_file: false
384
- }
385
- ]
454
+ deleted_file: false,
455
+ },
456
+ ],
386
457
  });
387
458
  });
459
+ // GET /api/v4/projects/:projectId/merge_requests/:mr_iid/diffs - Get paginated diffs
460
+ this.app.get('/api/v4/projects/:projectId/merge_requests/:mr_iid/diffs', (req, res) => {
461
+ const mrIid = parseInt(req.params.mr_iid);
462
+ // Paginate results for testing pagination logic
463
+ const allDiffs = [
464
+ { new_path: 'src/index.ts', old_path: 'src/index.ts' },
465
+ { new_path: 'vendor/package/file.js', old_path: 'vendor/package/file.js' },
466
+ { new_path: 'README.md', old_path: 'README.md' },
467
+ { new_path: 'package-lock.json', old_path: 'package-lock.json' },
468
+ { new_path: 'config/settings.yml', old_path: 'config/settings.yml' },
469
+ { new_path: 'lib/utils/helper.rb', old_path: 'lib/utils/helper.rb' },
470
+ { new_path: 'test/unit_test.py', old_path: 'test/unit_test.py' },
471
+ { new_path: 'docs/api.md', old_path: 'docs/api.md' },
472
+ { new_path: 'assets/style.css', old_path: 'assets/style.css' },
473
+ { new_path: 'scripts/build.sh', old_path: 'scripts/build.sh' },
474
+ { new_path: 'src/app.component.ts', old_path: 'src/app.component.ts' },
475
+ { new_path: 'views/layout.ejs', old_path: 'views/layout.eis' },
476
+ { new_path: 'models/user.go', old_path: 'models/user.go' },
477
+ { new_path: 'controllers/controller.rs', old_path: 'controllers/controller.rs' },
478
+ { new_path: 'database/schema.sql', old_path: 'database/schema.sql' }
479
+ ];
480
+ // Parse pagination params
481
+ const page = parseInt(req.query.page) || 1;
482
+ const perPage = parseInt(req.query.per_page) || 20;
483
+ const startIdx = (page - 1) * perPage;
484
+ const endIdx = startIdx + perPage;
485
+ const paginatedDiffs = allDiffs.slice(startIdx, endIdx);
486
+ res.json(paginatedDiffs);
487
+ });
388
488
  // Health check endpoint
389
- this.app.get('/health', (req, res) => {
390
- res.json({ status: 'ok', message: 'Mock GitLab API is running' });
489
+ this.app.get("/health", (req, res) => {
490
+ res.json({ status: "ok", message: "Mock GitLab API is running" });
391
491
  });
392
492
  // Catch-all for unimplemented endpoints
393
493
  this.app.use((req, res) => {
394
494
  console.log(`Mock GitLab: Unimplemented endpoint: ${req.method} ${req.path}`);
395
495
  res.status(404).json({
396
- message: '404 Not Found',
397
- error: 'Endpoint not implemented in mock server'
496
+ message: "404 Not Found",
497
+ error: "Endpoint not implemented in mock server",
398
498
  });
399
499
  });
400
500
  }
401
501
  async start() {
402
- return new Promise((resolve) => {
403
- this.server = this.app.listen(this.config.port, '127.0.0.1', () => {
502
+ return new Promise(resolve => {
503
+ this.server = this.app.listen(this.config.port, "127.0.0.1", () => {
404
504
  console.log(`Mock GitLab API listening on http://127.0.0.1:${this.config.port}`);
405
505
  resolve();
406
506
  });
@@ -409,11 +509,11 @@ export class MockGitLabServer {
409
509
  async stop() {
410
510
  return new Promise((resolve, reject) => {
411
511
  if (this.server) {
412
- this.server.close((err) => {
512
+ this.server.close(err => {
413
513
  if (err)
414
514
  reject(err);
415
515
  else {
416
- console.log('Mock GitLab API stopped');
516
+ console.log("Mock GitLab API stopped");
417
517
  resolve();
418
518
  }
419
519
  });
@@ -431,7 +531,7 @@ export class MockGitLabServer {
431
531
  * Helper to find available port for mock server
432
532
  */
433
533
  export async function findMockServerPort(basePort = 9000, maxAttempts = 10) {
434
- const net = await import('net');
534
+ const net = await import("net");
435
535
  const tryPort = async (port, attemptsLeft) => {
436
536
  if (attemptsLeft === 0) {
437
537
  throw new Error(`Could not find available port after ${maxAttempts} attempts starting from ${basePort}`);
@@ -439,7 +539,7 @@ export async function findMockServerPort(basePort = 9000, maxAttempts = 10) {
439
539
  return new Promise((resolve, reject) => {
440
540
  const server = net.createServer();
441
541
  server.unref();
442
- server.on('error', async () => {
542
+ server.on("error", async () => {
443
543
  try {
444
544
  const nextPort = await tryPort(port + 1, attemptsLeft - 1);
445
545
  resolve(nextPort);
@@ -448,9 +548,9 @@ export async function findMockServerPort(basePort = 9000, maxAttempts = 10) {
448
548
  reject(err);
449
549
  }
450
550
  });
451
- server.listen(port, '127.0.0.1', () => {
551
+ server.listen(port, "127.0.0.1", () => {
452
552
  const addr = server.address();
453
- const actualPort = typeof addr === 'object' && addr ? addr.port : port;
553
+ const actualPort = typeof addr === "object" && addr ? addr.port : port;
454
554
  server.close(() => {
455
555
  resolve(actualPort);
456
556
  });