@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.
- package/README.md +327 -92
- package/build/gitlab-client-pool.js +6 -0
- package/build/index.js +2224 -52
- package/build/oauth-proxy.js +264 -0
- package/build/schemas.js +457 -201
- package/build/test/mcp-oauth-tests.js +552 -0
- package/build/test/multi-server-test.js +16 -8
- package/build/test/schema-tests.js +77 -3
- package/build/test/test-geteffectiveprojectid.js +211 -202
- package/build/test/test-mr-file-diffs.js +251 -0
- package/build/test/test-search-code.js +272 -0
- package/build/test/test-toolset-filtering.js +22 -17
- package/build/test/utils/mock-gitlab-server.js +263 -163
- package/build/test/utils/server-launcher.js +45 -41
- package/package.json +3 -2
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
79
|
-
const authHeader = req.headers[
|
|
80
|
-
const privateToken = req.headers[
|
|
81
|
-
const jobToken = req.headers[
|
|
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:
|
|
97
|
-
error:
|
|
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:
|
|
104
|
-
error:
|
|
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(
|
|
116
|
-
const token = req.gitlabToken ||
|
|
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:
|
|
121
|
-
email:
|
|
122
|
-
state:
|
|
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(
|
|
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:
|
|
131
|
-
path:
|
|
132
|
-
path_with_namespace:
|
|
133
|
-
description:
|
|
134
|
-
visibility:
|
|
135
|
-
created_at:
|
|
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:
|
|
140
|
-
path:
|
|
141
|
-
kind:
|
|
142
|
-
full_path:
|
|
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(
|
|
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:
|
|
154
|
-
description:
|
|
155
|
-
state:
|
|
156
|
-
created_at:
|
|
157
|
-
updated_at:
|
|
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:
|
|
161
|
-
source_branch:
|
|
162
|
-
web_url:
|
|
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:
|
|
167
|
-
name:
|
|
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:
|
|
175
|
-
description:
|
|
176
|
-
state:
|
|
177
|
-
created_at:
|
|
178
|
-
updated_at:
|
|
179
|
-
merged_at:
|
|
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:
|
|
182
|
-
source_branch:
|
|
183
|
-
web_url:
|
|
184
|
-
merge_commit_sha:
|
|
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:
|
|
188
|
-
name:
|
|
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(
|
|
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:
|
|
201
|
-
description:
|
|
202
|
-
state:
|
|
203
|
-
created_at:
|
|
204
|
-
updated_at:
|
|
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:
|
|
208
|
-
source_branch:
|
|
209
|
-
web_url:
|
|
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:
|
|
214
|
-
name:
|
|
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:
|
|
222
|
-
description:
|
|
223
|
-
state:
|
|
224
|
-
created_at:
|
|
225
|
-
updated_at:
|
|
226
|
-
merged_at:
|
|
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:
|
|
229
|
-
source_branch:
|
|
230
|
-
web_url:
|
|
231
|
-
merge_commit_sha:
|
|
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:
|
|
235
|
-
name:
|
|
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(
|
|
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:
|
|
248
|
-
created_at:
|
|
273
|
+
state: "opened",
|
|
274
|
+
created_at: "2024-01-01T00:00:00Z",
|
|
249
275
|
author: {
|
|
250
276
|
id: 1,
|
|
251
|
-
username:
|
|
252
|
-
name:
|
|
277
|
+
username: "test-user",
|
|
278
|
+
name: "Test User",
|
|
253
279
|
},
|
|
254
|
-
source_branch:
|
|
255
|
-
target_branch:
|
|
256
|
-
merge_status:
|
|
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(
|
|
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:
|
|
268
|
-
description:
|
|
269
|
-
state:
|
|
270
|
-
created_at:
|
|
271
|
-
updated_at:
|
|
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:
|
|
277
|
-
name:
|
|
302
|
+
username: "test-user",
|
|
303
|
+
name: "Test User",
|
|
278
304
|
avatar_url: null,
|
|
279
|
-
web_url:
|
|
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(
|
|
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:
|
|
298
|
-
created_at:
|
|
299
|
-
updated_at:
|
|
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:
|
|
305
|
-
name:
|
|
330
|
+
username: "test-user",
|
|
331
|
+
name: "Test User",
|
|
306
332
|
avatar_url: null,
|
|
307
|
-
web_url:
|
|
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(
|
|
386
|
+
this.app.get("/api/v4/projects", (req, res) => {
|
|
316
387
|
res.json([
|
|
317
388
|
{
|
|
318
389
|
id: 123,
|
|
319
|
-
name:
|
|
320
|
-
path:
|
|
321
|
-
path_with_namespace:
|
|
322
|
-
description:
|
|
323
|
-
visibility:
|
|
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:
|
|
327
|
-
path:
|
|
328
|
-
kind:
|
|
329
|
-
full_path:
|
|
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(
|
|
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:
|
|
343
|
-
created_at:
|
|
413
|
+
state: "opened",
|
|
414
|
+
created_at: "2024-01-01T00:00:00Z",
|
|
344
415
|
changes: [
|
|
345
416
|
{
|
|
346
|
-
old_path:
|
|
347
|
-
new_path:
|
|
348
|
-
a_mode:
|
|
349
|
-
b_mode:
|
|
350
|
-
diff:
|
|
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:
|
|
357
|
-
new_path:
|
|
358
|
-
a_mode:
|
|
359
|
-
b_mode:
|
|
360
|
-
diff:
|
|
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:
|
|
367
|
-
new_path:
|
|
368
|
-
a_mode:
|
|
369
|
-
b_mode:
|
|
370
|
-
diff:
|
|
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:
|
|
377
|
-
new_path:
|
|
378
|
-
a_mode:
|
|
379
|
-
b_mode:
|
|
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(
|
|
390
|
-
res.json({ status:
|
|
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:
|
|
397
|
-
error:
|
|
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(
|
|
403
|
-
this.server = this.app.listen(this.config.port,
|
|
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(
|
|
512
|
+
this.server.close(err => {
|
|
413
513
|
if (err)
|
|
414
514
|
reject(err);
|
|
415
515
|
else {
|
|
416
|
-
console.log(
|
|
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(
|
|
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(
|
|
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,
|
|
551
|
+
server.listen(port, "127.0.0.1", () => {
|
|
452
552
|
const addr = server.address();
|
|
453
|
-
const actualPort = typeof addr ===
|
|
553
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
454
554
|
server.close(() => {
|
|
455
555
|
resolve(actualPort);
|
|
456
556
|
});
|