@zereight/mcp-gitlab 2.0.33 → 2.0.35

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,11 +101,15 @@ 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'];
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"];
81
108
  let token = null;
82
- if (authHeader) {
109
+ if (jobToken) {
110
+ token = jobToken.trim();
111
+ }
112
+ else if (authHeader) {
83
113
  // Extract token from "Bearer <token>"
84
114
  const match = authHeader.match(/^Bearer\s+(.+)$/i);
85
115
  token = match ? match[1].trim() : null;
@@ -89,15 +119,15 @@ export class MockGitLabServer {
89
119
  }
90
120
  if (!token) {
91
121
  res.status(401).json({
92
- message: 'Unauthorized',
93
- error: 'Missing authentication token'
122
+ message: "Unauthorized",
123
+ error: "Missing authentication token",
94
124
  });
95
125
  return;
96
126
  }
97
127
  if (!this.config.validTokens.includes(token)) {
98
128
  res.status(401).json({
99
- message: 'Unauthorized',
100
- error: 'Invalid authentication token'
129
+ message: "Unauthorized",
130
+ error: "Invalid authentication token",
101
131
  });
102
132
  return;
103
133
  }
@@ -108,180 +138,180 @@ export class MockGitLabServer {
108
138
  }
109
139
  setupRoutes() {
110
140
  // GET /api/v4/user - Get current user
111
- this.app.get('/api/v4/user', (req, res) => {
112
- const token = req.gitlabToken || 'unknown';
141
+ this.app.get("/api/v4/user", (req, res) => {
142
+ const token = req.gitlabToken || "unknown";
113
143
  res.json({
114
144
  id: 1,
115
145
  username: `user_${token.substring(0, 8)}`,
116
- name: 'Test User',
117
- email: 'test@example.com',
118
- state: 'active'
146
+ name: "Test User",
147
+ email: "test@example.com",
148
+ state: "active",
119
149
  });
120
150
  });
121
151
  // GET /api/v4/projects/:projectId - Get project
122
- this.app.get('/api/v4/projects/:projectId', (req, res) => {
152
+ this.app.get("/api/v4/projects/:projectId", (req, res) => {
123
153
  const projectId = req.params.projectId;
124
154
  res.json({
125
155
  id: parseInt(projectId) || 123,
126
- name: 'Test Project',
127
- path: 'test-project',
128
- path_with_namespace: 'test-group/test-project',
129
- description: 'A mock test project',
130
- visibility: 'private',
131
- 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",
132
162
  web_url: `https://gitlab.mock/project/${projectId}`,
133
163
  namespace: {
134
164
  id: 1,
135
- name: 'Test Group',
136
- path: 'test-group',
137
- kind: 'group',
138
- full_path: 'test-group'
139
- }
165
+ name: "Test Group",
166
+ path: "test-group",
167
+ kind: "group",
168
+ full_path: "test-group",
169
+ },
140
170
  });
141
171
  });
142
172
  // GET /api/v4/merge_requests - List all merge requests (global)
143
- this.app.get('/api/v4/merge_requests', (req, res) => {
173
+ this.app.get("/api/v4/merge_requests", (req, res) => {
144
174
  res.json([
145
175
  {
146
176
  id: 1,
147
177
  iid: 1,
148
178
  project_id: 123,
149
- title: 'Test MR 1',
150
- description: 'Description for MR 1',
151
- state: 'opened',
152
- created_at: '2024-01-01T00:00:00Z',
153
- 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",
154
184
  merged_at: null,
155
185
  closed_at: null,
156
- target_branch: 'main',
157
- source_branch: 'feature-1',
158
- 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",
159
189
  merge_commit_sha: null,
160
190
  author: {
161
191
  id: 1,
162
- username: 'test-user',
163
- name: 'Test User'
164
- }
192
+ username: "test-user",
193
+ name: "Test User",
194
+ },
165
195
  },
166
196
  {
167
197
  id: 2,
168
198
  iid: 2,
169
199
  project_id: 123,
170
- title: 'Test MR 2',
171
- description: 'Description for MR 2',
172
- state: 'merged',
173
- created_at: '2024-01-02T00:00:00Z',
174
- updated_at: '2024-01-03T00:00:00Z',
175
- 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",
176
206
  closed_at: null,
177
- target_branch: 'main',
178
- source_branch: 'feature-2',
179
- web_url: 'https://gitlab.mock/project/123/merge_requests/2',
180
- 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",
181
211
  author: {
182
212
  id: 1,
183
- username: 'test-user',
184
- name: 'Test User'
185
- }
186
- }
213
+ username: "test-user",
214
+ name: "Test User",
215
+ },
216
+ },
187
217
  ]);
188
218
  });
189
219
  // GET /api/v4/projects/:projectId/merge_requests - List merge requests
190
- this.app.get('/api/v4/projects/:projectId/merge_requests', (req, res) => {
220
+ this.app.get("/api/v4/projects/:projectId/merge_requests", (req, res) => {
191
221
  res.json([
192
222
  {
193
223
  id: 1,
194
224
  iid: 1,
195
225
  project_id: 123,
196
- title: 'Test MR 1',
197
- description: 'Description for MR 1',
198
- state: 'opened',
199
- created_at: '2024-01-01T00:00:00Z',
200
- 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",
201
231
  merged_at: null,
202
232
  closed_at: null,
203
- target_branch: 'main',
204
- source_branch: 'feature-1',
205
- 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",
206
236
  merge_commit_sha: null,
207
237
  author: {
208
238
  id: 1,
209
- username: 'test-user',
210
- name: 'Test User'
211
- }
239
+ username: "test-user",
240
+ name: "Test User",
241
+ },
212
242
  },
213
243
  {
214
244
  id: 2,
215
245
  iid: 2,
216
246
  project_id: 123,
217
- title: 'Test MR 2',
218
- description: 'Description for MR 2',
219
- state: 'merged',
220
- created_at: '2024-01-02T00:00:00Z',
221
- updated_at: '2024-01-03T00:00:00Z',
222
- 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",
223
253
  closed_at: null,
224
- target_branch: 'main',
225
- source_branch: 'feature-2',
226
- web_url: 'https://gitlab.mock/project/123/merge_requests/2',
227
- 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",
228
258
  author: {
229
259
  id: 1,
230
- username: 'test-user',
231
- name: 'Test User'
232
- }
233
- }
260
+ username: "test-user",
261
+ name: "Test User",
262
+ },
263
+ },
234
264
  ]);
235
265
  });
236
266
  // GET /api/v4/projects/:projectId/merge_requests/:mr_iid - Get single MR
237
- 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) => {
238
268
  const mrIid = parseInt(req.params.mr_iid);
239
269
  res.json({
240
270
  id: mrIid,
241
271
  iid: mrIid,
242
272
  title: `Test MR ${mrIid}`,
243
- state: 'opened',
244
- created_at: '2024-01-01T00:00:00Z',
273
+ state: "opened",
274
+ created_at: "2024-01-01T00:00:00Z",
245
275
  author: {
246
276
  id: 1,
247
- username: 'test-user',
248
- name: 'Test User'
277
+ username: "test-user",
278
+ name: "Test User",
249
279
  },
250
- source_branch: 'feature-branch',
251
- target_branch: 'main',
252
- merge_status: 'can_be_merged'
280
+ source_branch: "feature-branch",
281
+ target_branch: "main",
282
+ merge_status: "can_be_merged",
253
283
  });
254
284
  });
255
285
  // GET /api/v4/projects/:projectId/issues - List issues
256
- this.app.get('/api/v4/projects/:projectId/issues', (req, res) => {
286
+ this.app.get("/api/v4/projects/:projectId/issues", (req, res) => {
257
287
  const projectId = req.params.projectId;
258
288
  res.json([
259
289
  {
260
290
  id: 1,
261
291
  iid: 1,
262
292
  project_id: projectId,
263
- title: 'Test Issue 1',
264
- description: 'Test issue description',
265
- state: 'opened',
266
- created_at: '2024-01-01T00:00:00Z',
267
- 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",
268
298
  closed_at: null,
269
299
  web_url: `https://gitlab.mock/project/${projectId}/issues/1`,
270
300
  author: {
271
301
  id: 1,
272
- username: 'test-user',
273
- name: 'Test User',
302
+ username: "test-user",
303
+ name: "Test User",
274
304
  avatar_url: null,
275
- web_url: 'https://gitlab.mock/test-user'
305
+ web_url: "https://gitlab.mock/test-user",
276
306
  },
277
307
  assignees: [],
278
308
  labels: [],
279
- milestone: null
280
- }
309
+ milestone: null,
310
+ },
281
311
  ]);
282
312
  });
283
313
  // GET /api/v4/projects/:projectId/issues/:issue_iid - Get single issue
284
- 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) => {
285
315
  const issueIid = parseInt(req.params.issue_iid);
286
316
  const projectId = req.params.projectId;
287
317
  res.json({
@@ -290,113 +320,187 @@ export class MockGitLabServer {
290
320
  project_id: projectId,
291
321
  title: `Test Issue ${issueIid}`,
292
322
  description: `Description for issue ${issueIid}`,
293
- state: 'opened',
294
- created_at: '2024-01-01T00:00:00Z',
295
- 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",
296
326
  closed_at: null,
297
327
  web_url: `https://gitlab.mock/project/${projectId}/issues/${issueIid}`,
298
328
  author: {
299
329
  id: 1,
300
- username: 'test-user',
301
- name: 'Test User',
330
+ username: "test-user",
331
+ name: "Test User",
302
332
  avatar_url: null,
303
- web_url: 'https://gitlab.mock/test-user'
333
+ web_url: "https://gitlab.mock/test-user",
304
334
  },
305
335
  assignees: [],
306
336
  labels: [],
307
- milestone: null
337
+ milestone: null,
308
338
  });
309
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
+ });
310
385
  // GET /api/v4/projects - List projects
311
- this.app.get('/api/v4/projects', (req, res) => {
386
+ this.app.get("/api/v4/projects", (req, res) => {
312
387
  res.json([
313
388
  {
314
389
  id: 123,
315
- name: 'Test Project',
316
- path: 'test-project',
317
- path_with_namespace: 'test-group/test-project',
318
- description: 'A mock test project',
319
- 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",
320
395
  namespace: {
321
396
  id: 1,
322
- name: 'Test Group',
323
- path: 'test-group',
324
- kind: 'group',
325
- full_path: 'test-group'
326
- }
327
- }
397
+ name: "Test Group",
398
+ path: "test-group",
399
+ kind: "group",
400
+ full_path: "test-group",
401
+ },
402
+ },
328
403
  ]);
329
404
  });
330
405
  // GET /api/v4/projects/:projectId/merge_requests/:mr_iid/changes - Get MR diffs
331
- 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) => {
332
407
  const mrIid = parseInt(req.params.mr_iid);
333
408
  res.json({
334
409
  id: mrIid,
335
410
  iid: mrIid,
336
411
  project_id: parseInt(req.params.projectId),
337
412
  title: `Test MR ${mrIid}`,
338
- state: 'opened',
339
- created_at: '2024-01-01T00:00:00Z',
413
+ state: "opened",
414
+ created_at: "2024-01-01T00:00:00Z",
340
415
  changes: [
341
416
  {
342
- old_path: 'src/index.ts',
343
- new_path: 'src/index.ts',
344
- a_mode: '100644',
345
- b_mode: '100644',
346
- 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",
347
422
  new_file: false,
348
423
  renamed_file: false,
349
- deleted_file: false
424
+ deleted_file: false,
350
425
  },
351
426
  {
352
- old_path: 'vendor/package/file.js',
353
- new_path: 'vendor/package/file.js',
354
- a_mode: '100644',
355
- b_mode: '100644',
356
- 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",
357
432
  new_file: false,
358
433
  renamed_file: false,
359
- deleted_file: false
434
+ deleted_file: false,
360
435
  },
361
436
  {
362
- old_path: 'README.md',
363
- new_path: 'README.md',
364
- a_mode: '100644',
365
- b_mode: '100644',
366
- 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",
367
442
  new_file: false,
368
443
  renamed_file: false,
369
- deleted_file: false
444
+ deleted_file: false,
370
445
  },
371
446
  {
372
- old_path: 'package-lock.json',
373
- new_path: 'package-lock.json',
374
- a_mode: '100644',
375
- b_mode: '100644',
447
+ old_path: "package-lock.json",
448
+ new_path: "package-lock.json",
449
+ a_mode: "100644",
450
+ b_mode: "100644",
376
451
  diff: '{\n- "version": "1.0.0"\n+ "version": "1.0.1"\n}\n',
377
452
  new_file: false,
378
453
  renamed_file: false,
379
- deleted_file: false
380
- }
381
- ]
454
+ deleted_file: false,
455
+ },
456
+ ],
382
457
  });
383
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
+ });
384
488
  // Health check endpoint
385
- this.app.get('/health', (req, res) => {
386
- 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" });
387
491
  });
388
492
  // Catch-all for unimplemented endpoints
389
493
  this.app.use((req, res) => {
390
494
  console.log(`Mock GitLab: Unimplemented endpoint: ${req.method} ${req.path}`);
391
495
  res.status(404).json({
392
- message: '404 Not Found',
393
- error: 'Endpoint not implemented in mock server'
496
+ message: "404 Not Found",
497
+ error: "Endpoint not implemented in mock server",
394
498
  });
395
499
  });
396
500
  }
397
501
  async start() {
398
- return new Promise((resolve) => {
399
- 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", () => {
400
504
  console.log(`Mock GitLab API listening on http://127.0.0.1:${this.config.port}`);
401
505
  resolve();
402
506
  });
@@ -405,11 +509,11 @@ export class MockGitLabServer {
405
509
  async stop() {
406
510
  return new Promise((resolve, reject) => {
407
511
  if (this.server) {
408
- this.server.close((err) => {
512
+ this.server.close(err => {
409
513
  if (err)
410
514
  reject(err);
411
515
  else {
412
- console.log('Mock GitLab API stopped');
516
+ console.log("Mock GitLab API stopped");
413
517
  resolve();
414
518
  }
415
519
  });
@@ -427,7 +531,7 @@ export class MockGitLabServer {
427
531
  * Helper to find available port for mock server
428
532
  */
429
533
  export async function findMockServerPort(basePort = 9000, maxAttempts = 10) {
430
- const net = await import('net');
534
+ const net = await import("net");
431
535
  const tryPort = async (port, attemptsLeft) => {
432
536
  if (attemptsLeft === 0) {
433
537
  throw new Error(`Could not find available port after ${maxAttempts} attempts starting from ${basePort}`);
@@ -435,7 +539,7 @@ export async function findMockServerPort(basePort = 9000, maxAttempts = 10) {
435
539
  return new Promise((resolve, reject) => {
436
540
  const server = net.createServer();
437
541
  server.unref();
438
- server.on('error', async () => {
542
+ server.on("error", async () => {
439
543
  try {
440
544
  const nextPort = await tryPort(port + 1, attemptsLeft - 1);
441
545
  resolve(nextPort);
@@ -444,9 +548,9 @@ export async function findMockServerPort(basePort = 9000, maxAttempts = 10) {
444
548
  reject(err);
445
549
  }
446
550
  });
447
- server.listen(port, '127.0.0.1', () => {
551
+ server.listen(port, "127.0.0.1", () => {
448
552
  const addr = server.address();
449
- const actualPort = typeof addr === 'object' && addr ? addr.port : port;
553
+ const actualPort = typeof addr === "object" && addr ? addr.port : port;
450
554
  server.close(() => {
451
555
  resolve(actualPort);
452
556
  });