@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.
- package/README.md +233 -90
- package/build/gitlab-client-pool.js +114 -6
- package/build/index.js +2244 -98
- package/build/oauth-proxy.js +257 -0
- package/build/oauth.js +11 -6
- package/build/schemas.js +458 -199
- package/build/test/mcp-oauth-tests.js +443 -0
- package/build/test/multi-server-test.js +16 -8
- package/build/test/no-proxy-integration-test.js +183 -0
- package/build/test/no-proxy-test.js +138 -0
- package/build/test/remote-auth-simple-test.js +12 -1
- package/build/test/test-geteffectiveprojectid.js +245 -0
- 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/test-upload-markdown.js +148 -0
- package/build/test/utils/mock-gitlab-server.js +267 -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,11 +101,15 @@ 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[
|
|
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 (
|
|
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:
|
|
93
|
-
error:
|
|
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:
|
|
100
|
-
error:
|
|
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(
|
|
112
|
-
const token = req.gitlabToken ||
|
|
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:
|
|
117
|
-
email:
|
|
118
|
-
state:
|
|
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(
|
|
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:
|
|
127
|
-
path:
|
|
128
|
-
path_with_namespace:
|
|
129
|
-
description:
|
|
130
|
-
visibility:
|
|
131
|
-
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",
|
|
132
162
|
web_url: `https://gitlab.mock/project/${projectId}`,
|
|
133
163
|
namespace: {
|
|
134
164
|
id: 1,
|
|
135
|
-
name:
|
|
136
|
-
path:
|
|
137
|
-
kind:
|
|
138
|
-
full_path:
|
|
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(
|
|
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:
|
|
150
|
-
description:
|
|
151
|
-
state:
|
|
152
|
-
created_at:
|
|
153
|
-
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",
|
|
154
184
|
merged_at: null,
|
|
155
185
|
closed_at: null,
|
|
156
|
-
target_branch:
|
|
157
|
-
source_branch:
|
|
158
|
-
web_url:
|
|
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:
|
|
163
|
-
name:
|
|
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:
|
|
171
|
-
description:
|
|
172
|
-
state:
|
|
173
|
-
created_at:
|
|
174
|
-
updated_at:
|
|
175
|
-
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",
|
|
176
206
|
closed_at: null,
|
|
177
|
-
target_branch:
|
|
178
|
-
source_branch:
|
|
179
|
-
web_url:
|
|
180
|
-
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",
|
|
181
211
|
author: {
|
|
182
212
|
id: 1,
|
|
183
|
-
username:
|
|
184
|
-
name:
|
|
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(
|
|
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:
|
|
197
|
-
description:
|
|
198
|
-
state:
|
|
199
|
-
created_at:
|
|
200
|
-
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",
|
|
201
231
|
merged_at: null,
|
|
202
232
|
closed_at: null,
|
|
203
|
-
target_branch:
|
|
204
|
-
source_branch:
|
|
205
|
-
web_url:
|
|
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:
|
|
210
|
-
name:
|
|
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:
|
|
218
|
-
description:
|
|
219
|
-
state:
|
|
220
|
-
created_at:
|
|
221
|
-
updated_at:
|
|
222
|
-
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",
|
|
223
253
|
closed_at: null,
|
|
224
|
-
target_branch:
|
|
225
|
-
source_branch:
|
|
226
|
-
web_url:
|
|
227
|
-
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",
|
|
228
258
|
author: {
|
|
229
259
|
id: 1,
|
|
230
|
-
username:
|
|
231
|
-
name:
|
|
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(
|
|
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:
|
|
244
|
-
created_at:
|
|
273
|
+
state: "opened",
|
|
274
|
+
created_at: "2024-01-01T00:00:00Z",
|
|
245
275
|
author: {
|
|
246
276
|
id: 1,
|
|
247
|
-
username:
|
|
248
|
-
name:
|
|
277
|
+
username: "test-user",
|
|
278
|
+
name: "Test User",
|
|
249
279
|
},
|
|
250
|
-
source_branch:
|
|
251
|
-
target_branch:
|
|
252
|
-
merge_status:
|
|
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(
|
|
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:
|
|
264
|
-
description:
|
|
265
|
-
state:
|
|
266
|
-
created_at:
|
|
267
|
-
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",
|
|
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:
|
|
273
|
-
name:
|
|
302
|
+
username: "test-user",
|
|
303
|
+
name: "Test User",
|
|
274
304
|
avatar_url: null,
|
|
275
|
-
web_url:
|
|
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(
|
|
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:
|
|
294
|
-
created_at:
|
|
295
|
-
updated_at:
|
|
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:
|
|
301
|
-
name:
|
|
330
|
+
username: "test-user",
|
|
331
|
+
name: "Test User",
|
|
302
332
|
avatar_url: null,
|
|
303
|
-
web_url:
|
|
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(
|
|
386
|
+
this.app.get("/api/v4/projects", (req, res) => {
|
|
312
387
|
res.json([
|
|
313
388
|
{
|
|
314
389
|
id: 123,
|
|
315
|
-
name:
|
|
316
|
-
path:
|
|
317
|
-
path_with_namespace:
|
|
318
|
-
description:
|
|
319
|
-
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",
|
|
320
395
|
namespace: {
|
|
321
396
|
id: 1,
|
|
322
|
-
name:
|
|
323
|
-
path:
|
|
324
|
-
kind:
|
|
325
|
-
full_path:
|
|
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(
|
|
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:
|
|
339
|
-
created_at:
|
|
413
|
+
state: "opened",
|
|
414
|
+
created_at: "2024-01-01T00:00:00Z",
|
|
340
415
|
changes: [
|
|
341
416
|
{
|
|
342
|
-
old_path:
|
|
343
|
-
new_path:
|
|
344
|
-
a_mode:
|
|
345
|
-
b_mode:
|
|
346
|
-
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",
|
|
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:
|
|
353
|
-
new_path:
|
|
354
|
-
a_mode:
|
|
355
|
-
b_mode:
|
|
356
|
-
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",
|
|
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:
|
|
363
|
-
new_path:
|
|
364
|
-
a_mode:
|
|
365
|
-
b_mode:
|
|
366
|
-
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",
|
|
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:
|
|
373
|
-
new_path:
|
|
374
|
-
a_mode:
|
|
375
|
-
b_mode:
|
|
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(
|
|
386
|
-
res.json({ status:
|
|
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:
|
|
393
|
-
error:
|
|
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(
|
|
399
|
-
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", () => {
|
|
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(
|
|
512
|
+
this.server.close(err => {
|
|
409
513
|
if (err)
|
|
410
514
|
reject(err);
|
|
411
515
|
else {
|
|
412
|
-
console.log(
|
|
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(
|
|
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(
|
|
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,
|
|
551
|
+
server.listen(port, "127.0.0.1", () => {
|
|
448
552
|
const addr = server.address();
|
|
449
|
-
const actualPort = typeof addr ===
|
|
553
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
450
554
|
server.close(() => {
|
|
451
555
|
resolve(actualPort);
|
|
452
556
|
});
|