@zereight/mcp-gitlab 2.1.69 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.ko.md +10 -4
  2. package/README.md +238 -233
  3. package/README.pt-BR.md +4 -4
  4. package/README.zh-CN.md +6 -4
  5. package/build/config.js +12 -4
  6. package/build/deprecated-env.js +47 -0
  7. package/build/index.js +456 -237
  8. package/build/oauth-device-flow.js +80 -0
  9. package/build/oauth.js +135 -18
  10. package/build/schemas.js +1 -1
  11. package/build/server/remote-http-stack.js +39 -0
  12. package/build/tools/discover-activation.js +38 -0
  13. package/build/tools/exposed-tools.js +35 -0
  14. package/build/tools/listed-tools.js +76 -0
  15. package/build/tools/registry.js +226 -57
  16. package/build/tools/tool-profile.js +66 -0
  17. package/build/utils/compact-tool-allowlist.js +14 -0
  18. package/build/utils/logger.js +34 -12
  19. package/build/utils/url.js +15 -0
  20. package/package.json +3 -3
  21. package/build/scripts/check-skill-sync.js +0 -143
  22. package/build/scripts/generate-server-card.js +0 -39
  23. package/build/scripts/generate-tool-coverage.js +0 -58
  24. package/build/scripts/generate-tool-docs.js +0 -430
  25. package/build/scripts/tool-coverage/coverage.js +0 -341
  26. package/build/scripts/tool-coverage/coverage.test.js +0 -142
  27. package/build/test/callback-proxy-tests.js +0 -321
  28. package/build/test/cli/args.test.js +0 -175
  29. package/build/test/cli/boolean-flags.test.js +0 -106
  30. package/build/test/cli/compact-mcp-result.test.js +0 -93
  31. package/build/test/cli/exposure.test.js +0 -104
  32. package/build/test/cli/load-config-snapshot.js +0 -5
  33. package/build/test/cli/output.test.js +0 -74
  34. package/build/test/cli/replay-command.test.js +0 -37
  35. package/build/test/cli/router.test.js +0 -183
  36. package/build/test/client-pool-test.js +0 -109
  37. package/build/test/clients/client.js +0 -34
  38. package/build/test/clients/custom-header-client.js +0 -122
  39. package/build/test/clients/sse-client.js +0 -95
  40. package/build/test/clients/stdio-client.js +0 -113
  41. package/build/test/clients/streamable-http-client.js +0 -95
  42. package/build/test/config-allowed-groups.test.js +0 -97
  43. package/build/test/create-commit-actions.test.js +0 -102
  44. package/build/test/draft-note-schema.test.js +0 -44
  45. package/build/test/dynamic-api-url-allowlist.test.js +0 -121
  46. package/build/test/dynamic-api-url-test.js +0 -304
  47. package/build/test/dynamic-routing-tests.js +0 -495
  48. package/build/test/get-merge-request-discussion-schema.test.js +0 -30
  49. package/build/test/gitlab-artifact-entry-schema.test.js +0 -44
  50. package/build/test/gitlab-diff-schema.test.js +0 -36
  51. package/build/test/gitlab-pipeline-schedule-schema.test.js +0 -32
  52. package/build/test/gitlab-tag-schema.test.js +0 -51
  53. package/build/test/group-milestone-schema.test.js +0 -45
  54. package/build/test/mcp-oauth-tests.js +0 -905
  55. package/build/test/mcp-server-name.test.js +0 -87
  56. package/build/test/merge-merge-request-schema.test.js +0 -75
  57. package/build/test/multi-server-test.js +0 -190
  58. package/build/test/no-proxy-integration-test.js +0 -183
  59. package/build/test/no-proxy-test.js +0 -149
  60. package/build/test/nullable-gitlab-response-fields.test.js +0 -37
  61. package/build/test/nullish-tool-arguments-schema.test.js +0 -118
  62. package/build/test/oauth-device-flow-tests.js +0 -450
  63. package/build/test/oauth-startup.test.js +0 -204
  64. package/build/test/oauth-tests.js +0 -428
  65. package/build/test/path-segment-encoding.test.js +0 -98
  66. package/build/test/remote-auth-simple-test.js +0 -277
  67. package/build/test/remote-auth-tests.js +0 -314
  68. package/build/test/response-masking.test.js +0 -445
  69. package/build/test/schema-tests.js +0 -1679
  70. package/build/test/server/metrics.test.js +0 -47
  71. package/build/test/server/request-helpers.test.js +0 -48
  72. package/build/test/server/version.test.js +0 -13
  73. package/build/test/sse-auth-guard.test.js +0 -96
  74. package/build/test/sse-session-limits.test.js +0 -273
  75. package/build/test/stateless/callback-proxy.test.js +0 -510
  76. package/build/test/stateless/client-id.test.js +0 -177
  77. package/build/test/stateless/codec.test.js +0 -328
  78. package/build/test/stateless/config-ttl.test.js +0 -149
  79. package/build/test/stateless/consumed-proxy-code-cache.test.js +0 -150
  80. package/build/test/stateless/session-id-integration.test.js +0 -675
  81. package/build/test/stateless/session-id.test.js +0 -157
  82. package/build/test/streamable-http-concurrent-session.test.js +0 -171
  83. package/build/test/streamable-http-dns-rebinding.test.js +0 -185
  84. package/build/test/streamable-http-sse-stream.test.js +0 -350
  85. package/build/test/streamable-http-static-token-auth.test.js +0 -157
  86. package/build/test/streamable-http-unauthenticated-discovery.test.js +0 -207
  87. package/build/test/test-all-transport-server.js +0 -255
  88. package/build/test/test-auth-retry.js +0 -195
  89. package/build/test/test-blank-filters.js +0 -156
  90. package/build/test/test-ci-catalog.js +0 -177
  91. package/build/test/test-ci-lint.js +0 -191
  92. package/build/test/test-ci-variables.js +0 -309
  93. package/build/test/test-create-repository.js +0 -120
  94. package/build/test/test-dependency-proxy.js +0 -195
  95. package/build/test/test-deployment-tools.js +0 -463
  96. package/build/test/test-download-attachment.js +0 -190
  97. package/build/test/test-dynamic-project-scope.js +0 -524
  98. package/build/test/test-empty-toolsets.js +0 -535
  99. package/build/test/test-get-file-blame.js +0 -145
  100. package/build/test/test-get-file-contents.js +0 -128
  101. package/build/test/test-geteffectiveprojectid.js +0 -812
  102. package/build/test/test-health-check.js +0 -115
  103. package/build/test/test-issue-description-patch.js +0 -256
  104. package/build/test/test-job-artifacts.js +0 -275
  105. package/build/test/test-json-schema.js +0 -166
  106. package/build/test/test-list-group-members.js +0 -150
  107. package/build/test/test-list-group-merge-requests.js +0 -165
  108. package/build/test/test-list-issues.js +0 -170
  109. package/build/test/test-list-merge-requests.js +0 -201
  110. package/build/test/test-list-milestones.js +0 -145
  111. package/build/test/test-list-project-members.js +0 -132
  112. package/build/test/test-merge-request-approval-state-tools.js +0 -213
  113. package/build/test/test-merge-request-approvals.js +0 -187
  114. package/build/test/test-merge-request-pipelines.js +0 -106
  115. package/build/test/test-mr-diffs-filter.js +0 -132
  116. package/build/test/test-mr-file-diffs.js +0 -251
  117. package/build/test/test-oauth-proxy-rate-limit.js +0 -133
  118. package/build/test/test-orbit.js +0 -155
  119. package/build/test/test-permission-mode.js +0 -424
  120. package/build/test/test-pipeline-schedules.js +0 -482
  121. package/build/test/test-protected-branches.js +0 -174
  122. package/build/test/test-remote-downloads.js +0 -510
  123. package/build/test/test-search-code.js +0 -275
  124. package/build/test/test-snippets.js +0 -735
  125. package/build/test/test-tags.js +0 -220
  126. package/build/test/test-todos.js +0 -195
  127. package/build/test/test-token-optimizations.js +0 -691
  128. package/build/test/test-toolset-filtering.js +0 -499
  129. package/build/test/test-update-issue-slim.js +0 -141
  130. package/build/test/test-update-project.js +0 -112
  131. package/build/test/test-upload-markdown.js +0 -206
  132. package/build/test/test-vulnerabilities.js +0 -399
  133. package/build/test/test-webhooks.js +0 -218
  134. package/build/test/tool-description-quality.test.js +0 -50
  135. package/build/test/utils/bulk-publish-options.test.js +0 -32
  136. package/build/test/utils/download-token.test.js +0 -35
  137. package/build/test/utils/forwarded-public-base-url.test.js +0 -46
  138. package/build/test/utils/gitlab-instance-version.test.js +0 -32
  139. package/build/test/utils/gitlab-version-gate.test.js +0 -74
  140. package/build/test/utils/graphql-query.test.js +0 -168
  141. package/build/test/utils/helpers-path.test.js +0 -104
  142. package/build/test/utils/jmespath-tool-result.test.js +0 -91
  143. package/build/test/utils/masking.test.js +0 -384
  144. package/build/test/utils/merge-request-position.test.js +0 -57
  145. package/build/test/utils/mock-gitlab-server.js +0 -614
  146. package/build/test/utils/proxy-client-ip.test.js +0 -28
  147. package/build/test/utils/redact-sensitive.test.js +0 -32
  148. package/build/test/utils/safe-redirect-fetch.test.js +0 -463
  149. package/build/test/utils/server-launcher.js +0 -231
  150. package/build/test/utils/tool-args.test.js +0 -210
  151. package/build/test/utils/url.test.js +0 -128
  152. package/build/test/utils/version-check.test.js +0 -239
  153. package/build/test/utils/wiki-title.test.js +0 -21
  154. package/build/test-note.js +0 -54
  155. package/build/test-resolve-issue-note.js +0 -127
@@ -1,614 +0,0 @@
1
- /**
2
- * Mock GitLab API Server for Testing
3
- * Implements minimal GitLab API endpoints for testing remote authorization
4
- */
5
- import express from "express";
6
- export class MockGitLabServer {
7
- app;
8
- server = null;
9
- config;
10
- requestCount = 0;
11
- customRouter;
12
- customHandlers = new Map();
13
- // Root-level dynamic router (for OAuth paths not under /api/v4)
14
- rootRouter;
15
- rootHandlers = new Map();
16
- // In-memory store for mutable resources (issues, etc.)
17
- issueStore = new Map();
18
- constructor(config) {
19
- this.config = config;
20
- this.app = express();
21
- this.customRouter = express.Router();
22
- this.rootRouter = express.Router();
23
- // Dynamic dispatcher for /api/v4 handlers
24
- this.customRouter.use((req, res, next) => {
25
- // Create a key from method and path (relative to /api/v4)
26
- // req.path is already relative to the mount point
27
- const key = `${req.method.toUpperCase()}:${req.path}`;
28
- console.log(`[CustomRouter] Checking key: '${key}'`);
29
- const handler = this.customHandlers.get(key);
30
- if (handler) {
31
- console.log(`[MockServer] Custom handler hit: ${key}`);
32
- return handler(req, res, next);
33
- }
34
- else {
35
- console.log(`[CustomRouter] No handler found for key: '${key}'. Available keys: ${Array.from(this.customHandlers.keys()).join(", ")}`);
36
- }
37
- next();
38
- });
39
- // Dynamic dispatcher for root-level handlers (OAuth endpoints, well-known, etc.)
40
- this.rootRouter.use((req, res, next) => {
41
- const key = `${req.method.toUpperCase()}:${req.path}`;
42
- const handler = this.rootHandlers.get(key);
43
- if (handler) {
44
- console.log(`[MockServer] Root handler hit: ${key}`);
45
- return handler(req, res, next);
46
- }
47
- next();
48
- });
49
- this.setupMiddleware();
50
- // Root router must be mounted BEFORE setupRoutes() installs the catch-all
51
- this.app.use(this.rootRouter);
52
- this.app.use("/api/v4", this.customRouter); // Mount router on API path
53
- this.setupRoutes();
54
- }
55
- addMockHandler(method, path, handler) {
56
- // Note: path should be relative to /api/v4
57
- const key = `${method.toUpperCase()}:${path}`;
58
- console.log(`[MockServer] Adding custom handler: ${key}`);
59
- this.customHandlers.set(key, handler);
60
- }
61
- /**
62
- * Add a route at the instance root (not under /api/v4).
63
- * Use this for OAuth endpoints (/oauth/*, /.well-known/*) that GitLab
64
- * serves at the instance root rather than under the API prefix.
65
- */
66
- addRootHandler(method, path, handler) {
67
- const key = `${method.toUpperCase()}:${path}`;
68
- console.log(`[MockServer] Adding root handler: ${key}`);
69
- this.rootHandlers.set(key, handler);
70
- }
71
- clearCustomHandlers() {
72
- console.log("[MockServer] Clearing custom handlers");
73
- this.customHandlers.clear();
74
- }
75
- /**
76
- * Setup middleware including auth validation
77
- */
78
- setupMiddleware() {
79
- // Request counter for rate limiting tests - Place this FIRST to log everything
80
- this.app.use((req, res, next) => {
81
- console.log(`[MockServer] ${req.method} ${req.originalUrl}`);
82
- this.requestCount++;
83
- next();
84
- });
85
- this.app.use(express.json());
86
- // Artificial delay middleware (for timeout testing)
87
- if (this.config.responseDelay) {
88
- this.app.use((req, res, next) => {
89
- setTimeout(next, this.config.responseDelay);
90
- });
91
- }
92
- // Rate limiting middleware
93
- if (this.config.rateLimitAfter) {
94
- this.app.use((req, res, next) => {
95
- if (this.requestCount > this.config.rateLimitAfter) {
96
- res.status(429).json({
97
- message: "Rate limit exceeded",
98
- retry_after: 60,
99
- });
100
- return;
101
- }
102
- next();
103
- });
104
- }
105
- // Authentication middleware - applies to all /api/v4/* routes
106
- this.app.use("/api/v4", (req, res, next) => {
107
- const authHeader = req.headers["authorization"];
108
- const privateToken = req.headers["private-token"];
109
- const jobToken = req.headers["job-token"];
110
- let token = null;
111
- if (jobToken) {
112
- token = jobToken.trim();
113
- }
114
- else if (authHeader) {
115
- // Extract token from "Bearer <token>"
116
- const match = authHeader.match(/^Bearer\s+(.+)$/i);
117
- token = match ? match[1].trim() : null;
118
- }
119
- else if (privateToken) {
120
- token = privateToken.trim();
121
- }
122
- if (!token) {
123
- res.status(401).json({
124
- message: "Unauthorized",
125
- error: "Missing authentication token",
126
- });
127
- return;
128
- }
129
- if (!this.config.validTokens.includes(token)) {
130
- res.status(401).json({
131
- message: "Unauthorized",
132
- error: "Invalid authentication token",
133
- });
134
- return;
135
- }
136
- // Store validated token in request
137
- req.gitlabToken = token;
138
- next();
139
- });
140
- }
141
- setupRoutes() {
142
- // GET /api/v4/user - Get current user
143
- this.app.get("/api/v4/user", (req, res) => {
144
- const token = req.gitlabToken || "unknown";
145
- res.json({
146
- id: 1,
147
- username: `user_${token.substring(0, 8)}`,
148
- name: "Test User",
149
- email: "test@example.com",
150
- state: "active",
151
- });
152
- });
153
- // GET /api/v4/version - GitLab instance version
154
- this.app.get("/api/v4/version", (_req, res) => {
155
- res.json({
156
- version: "18.3.1-ee",
157
- revision: "abc1234",
158
- enterprise: true,
159
- });
160
- });
161
- // GET /api/v4/projects/:projectId - Get project
162
- this.app.get("/api/v4/projects/:projectId", (req, res) => {
163
- const projectId = req.params.projectId;
164
- res.json({
165
- id: parseInt(projectId) || 123,
166
- name: "Test Project",
167
- path: "test-project",
168
- path_with_namespace: "test-group/test-project",
169
- description: "A mock test project",
170
- visibility: "private",
171
- created_at: "2024-01-01T00:00:00Z",
172
- web_url: `https://gitlab.mock/project/${projectId}`,
173
- namespace: {
174
- id: 1,
175
- name: "Test Group",
176
- path: "test-group",
177
- kind: "group",
178
- full_path: "test-group",
179
- },
180
- });
181
- });
182
- // GET /api/v4/merge_requests - List all merge requests (global)
183
- this.app.get("/api/v4/merge_requests", (req, res) => {
184
- res.json([
185
- {
186
- id: 1,
187
- iid: 1,
188
- project_id: 123,
189
- title: "Test MR 1",
190
- description: "Description for MR 1",
191
- state: "opened",
192
- created_at: "2024-01-01T00:00:00Z",
193
- updated_at: "2024-01-01T00:00:00Z",
194
- merged_at: null,
195
- closed_at: null,
196
- target_branch: "main",
197
- source_branch: "feature-1",
198
- web_url: "https://gitlab.mock/project/123/merge_requests/1",
199
- merge_commit_sha: null,
200
- author: {
201
- id: 1,
202
- username: "test-user",
203
- name: "Test User",
204
- },
205
- },
206
- {
207
- id: 2,
208
- iid: 2,
209
- project_id: 123,
210
- title: "Test MR 2",
211
- description: "Description for MR 2",
212
- state: "merged",
213
- created_at: "2024-01-02T00:00:00Z",
214
- updated_at: "2024-01-03T00:00:00Z",
215
- merged_at: "2024-01-03T00:00:00Z",
216
- closed_at: null,
217
- target_branch: "main",
218
- source_branch: "feature-2",
219
- web_url: "https://gitlab.mock/project/123/merge_requests/2",
220
- merge_commit_sha: "abcdef1234567890",
221
- author: {
222
- id: 1,
223
- username: "test-user",
224
- name: "Test User",
225
- },
226
- },
227
- ]);
228
- });
229
- // GET /api/v4/projects/:projectId/merge_requests - List merge requests
230
- this.app.get("/api/v4/projects/:projectId/merge_requests", (req, res) => {
231
- res.json([
232
- {
233
- id: 1,
234
- iid: 1,
235
- project_id: 123,
236
- title: "Test MR 1",
237
- description: "Description for MR 1",
238
- state: "opened",
239
- created_at: "2024-01-01T00:00:00Z",
240
- updated_at: "2024-01-01T00:00:00Z",
241
- merged_at: null,
242
- closed_at: null,
243
- target_branch: "main",
244
- source_branch: "feature-1",
245
- web_url: "https://gitlab.mock/project/123/merge_requests/1",
246
- merge_commit_sha: null,
247
- author: {
248
- id: 1,
249
- username: "test-user",
250
- name: "Test User",
251
- },
252
- },
253
- {
254
- id: 2,
255
- iid: 2,
256
- project_id: 123,
257
- title: "Test MR 2",
258
- description: "Description for MR 2",
259
- state: "merged",
260
- created_at: "2024-01-02T00:00:00Z",
261
- updated_at: "2024-01-03T00:00:00Z",
262
- merged_at: "2024-01-03T00:00:00Z",
263
- closed_at: null,
264
- target_branch: "main",
265
- source_branch: "feature-2",
266
- web_url: "https://gitlab.mock/project/123/merge_requests/2",
267
- merge_commit_sha: "abcdef1234567890",
268
- author: {
269
- id: 1,
270
- username: "test-user",
271
- name: "Test User",
272
- },
273
- },
274
- ]);
275
- });
276
- // GET /api/v4/projects/:projectId/merge_requests/:mr_iid - Get single MR
277
- this.app.get("/api/v4/projects/:projectId/merge_requests/:mr_iid", (req, res) => {
278
- const mrIid = parseInt(req.params.mr_iid);
279
- res.json({
280
- id: mrIid,
281
- iid: mrIid,
282
- title: `Test MR ${mrIid}`,
283
- state: "opened",
284
- created_at: "2024-01-01T00:00:00Z",
285
- author: {
286
- id: 1,
287
- username: "test-user",
288
- name: "Test User",
289
- },
290
- source_branch: "feature-branch",
291
- target_branch: "main",
292
- merge_status: "can_be_merged",
293
- });
294
- });
295
- // GET /api/v4/projects/:projectId/issues - List issues
296
- this.app.get("/api/v4/projects/:projectId/issues", (req, res) => {
297
- const projectId = req.params.projectId;
298
- res.json([
299
- {
300
- id: 1,
301
- iid: 1,
302
- project_id: projectId,
303
- title: "Test Issue 1",
304
- description: "Test issue description",
305
- state: "opened",
306
- created_at: "2024-01-01T00:00:00Z",
307
- updated_at: "2024-01-02T00:00:00Z",
308
- closed_at: null,
309
- web_url: `https://gitlab.mock/project/${projectId}/issues/1`,
310
- author: {
311
- id: 1,
312
- username: "test-user",
313
- name: "Test User",
314
- avatar_url: null,
315
- web_url: "https://gitlab.mock/test-user",
316
- },
317
- assignees: [],
318
- labels: [],
319
- milestone: null,
320
- },
321
- ]);
322
- });
323
- // GET /api/v4/projects/:projectId/issues/:issue_iid - Get single issue
324
- this.app.get("/api/v4/projects/:projectId/issues/:issue_iid", (req, res) => {
325
- const issueIid = parseInt(req.params.issue_iid);
326
- const projectId = req.params.projectId;
327
- const storeKey = `${projectId}:${issueIid}`;
328
- const stored = this.issueStore.get(storeKey);
329
- if (stored) {
330
- res.json(stored);
331
- return;
332
- }
333
- res.json({
334
- id: issueIid,
335
- iid: issueIid,
336
- project_id: projectId,
337
- title: `Test Issue ${issueIid}`,
338
- description: `Description for issue ${issueIid}`,
339
- state: "opened",
340
- created_at: "2024-01-01T00:00:00Z",
341
- updated_at: "2024-01-02T00:00:00Z",
342
- closed_at: null,
343
- web_url: `https://gitlab.mock/project/${projectId}/issues/${issueIid}`,
344
- author: {
345
- id: 1,
346
- username: "test-user",
347
- name: "Test User",
348
- avatar_url: null,
349
- web_url: "https://gitlab.mock/test-user",
350
- },
351
- assignees: [],
352
- labels: [],
353
- milestone: issueIid === 100
354
- ? {
355
- id: 42,
356
- iid: 7,
357
- title: "Sprint 42",
358
- description: "A very long milestone description ".repeat(50),
359
- state: "active",
360
- web_url: `https://gitlab.mock/project/${projectId}/milestones/7`,
361
- }
362
- : null,
363
- });
364
- });
365
- // PUT /api/v4/projects/:projectId/issues/:issue_iid - Update issue
366
- this.app.put("/api/v4/projects/:projectId/issues/:issue_iid", (req, res) => {
367
- const issueIid = parseInt(req.params.issue_iid);
368
- const projectId = req.params.projectId;
369
- const storeKey = `${projectId}:${issueIid}`;
370
- // Build response from stored data (if previously updated) or defaults
371
- const stored = this.issueStore.get(storeKey) || {};
372
- const description = req.body?.description ?? stored.description ?? `Description for issue ${issueIid}`;
373
- const title = req.body?.title ?? stored.title ?? `Test Issue ${issueIid}`;
374
- const state = req.body?.state_event === "close" ? "closed" :
375
- req.body?.state_event === "reopen" ? "opened" :
376
- (stored.state ?? "opened");
377
- const updatedIssue = {
378
- id: issueIid,
379
- iid: issueIid,
380
- project_id: projectId,
381
- title,
382
- description,
383
- state,
384
- created_at: stored.created_at ?? "2024-01-01T00:00:00Z",
385
- updated_at: new Date().toISOString(),
386
- closed_at: state === "closed" ? new Date().toISOString() : null,
387
- web_url: `https://gitlab.mock/project/${projectId}/issues/${issueIid}`,
388
- author: {
389
- id: 1,
390
- username: "test-user",
391
- name: "Test User",
392
- avatar_url: null,
393
- web_url: "https://gitlab.mock/test-user",
394
- },
395
- assignees: [],
396
- labels: [],
397
- milestone: null,
398
- };
399
- // Store for subsequent GET requests
400
- this.issueStore.set(storeKey, updatedIssue);
401
- res.json(updatedIssue);
402
- });
403
- // Mock blob search result
404
- const mockBlobResults = [
405
- {
406
- basename: "index",
407
- data: "const searchResult = true;",
408
- path: "src/index.ts",
409
- filename: "index.ts",
410
- id: null,
411
- ref: "main",
412
- startline: 42,
413
- project_id: 1,
414
- }
415
- ];
416
- // GET /api/v4/search - Global search
417
- this.app.get('/api/v4/search', (req, res) => {
418
- if (req.query.scope === 'blobs') {
419
- res.json(mockBlobResults);
420
- }
421
- else {
422
- res.json([]);
423
- }
424
- });
425
- // GET /api/v4/projects/:id/search - Project-level search
426
- this.app.get('/api/v4/projects/:id/search', (req, res) => {
427
- if (req.query.scope === 'blobs') {
428
- // Return req.params.id as string — Express decodes the URL param, so this
429
- // reflects whether the caller properly single-encoded the path segment.
430
- const results = mockBlobResults.map(r => ({ ...r, project_id: req.params.id }));
431
- res.json(results);
432
- }
433
- else {
434
- res.json([]);
435
- }
436
- });
437
- // GET /api/v4/groups/:id/search - Group-level search
438
- this.app.get('/api/v4/groups/:id/search', (req, res) => {
439
- if (req.query.scope === 'blobs') {
440
- // Echo the decoded group ID in the ref field so tests can verify encoding
441
- const results = mockBlobResults.map(r => ({ ...r, ref: req.params.id }));
442
- res.json(results);
443
- }
444
- else {
445
- res.json([]);
446
- }
447
- });
448
- // GET /api/v4/projects - List projects
449
- this.app.get("/api/v4/projects", (req, res) => {
450
- res.json([
451
- {
452
- id: 123,
453
- name: "Test Project",
454
- path: "test-project",
455
- path_with_namespace: "test-group/test-project",
456
- description: "A mock test project",
457
- visibility: "private",
458
- namespace: {
459
- id: 1,
460
- name: "Test Group",
461
- path: "test-group",
462
- kind: "group",
463
- full_path: "test-group",
464
- },
465
- },
466
- ]);
467
- });
468
- // GET /api/v4/projects/:projectId/merge_requests/:mr_iid/changes - Get MR diffs
469
- this.app.get("/api/v4/projects/:projectId/merge_requests/:mr_iid/changes", (req, res) => {
470
- const mrIid = parseInt(req.params.mr_iid);
471
- res.json({
472
- id: mrIid,
473
- iid: mrIid,
474
- project_id: parseInt(req.params.projectId),
475
- title: `Test MR ${mrIid}`,
476
- state: "opened",
477
- created_at: "2024-01-01T00:00:00Z",
478
- changes: [
479
- {
480
- old_path: "src/index.ts",
481
- new_path: "src/index.ts",
482
- a_mode: "100644",
483
- b_mode: "100644",
484
- diff: "@@ -1,1 +1,2 @@\n-line 1\n+line 1 modified\n+new line 2\n",
485
- new_file: false,
486
- renamed_file: false,
487
- deleted_file: false,
488
- },
489
- {
490
- old_path: "vendor/package/file.js",
491
- new_path: "vendor/package/file.js",
492
- a_mode: "100644",
493
- b_mode: "100644",
494
- diff: "@@ -1,1 +1,1 @@\n-vendor content old\n+vendor content new\n",
495
- new_file: false,
496
- renamed_file: false,
497
- deleted_file: false,
498
- },
499
- {
500
- old_path: "README.md",
501
- new_path: "README.md",
502
- a_mode: "100644",
503
- b_mode: "100644",
504
- diff: "@@ -1,1 +1,1 @@\n-old readme\n+new readme\n",
505
- new_file: false,
506
- renamed_file: false,
507
- deleted_file: false,
508
- },
509
- {
510
- old_path: "package-lock.json",
511
- new_path: "package-lock.json",
512
- a_mode: "100644",
513
- b_mode: "100644",
514
- diff: '{\n- "version": "1.0.0"\n+ "version": "1.0.1"\n}\n',
515
- new_file: false,
516
- renamed_file: false,
517
- deleted_file: false,
518
- },
519
- ],
520
- });
521
- });
522
- // GET /api/v4/projects/:projectId/merge_requests/:mr_iid/diffs - Get paginated diffs
523
- this.app.get('/api/v4/projects/:projectId/merge_requests/:mr_iid/diffs', (req, res) => {
524
- const mrIid = parseInt(req.params.mr_iid);
525
- // Paginate results for testing pagination logic
526
- const allDiffs = [
527
- { new_path: 'src/index.ts', old_path: 'src/index.ts' },
528
- { new_path: 'vendor/package/file.js', old_path: 'vendor/package/file.js' },
529
- { new_path: 'README.md', old_path: 'README.md' },
530
- { new_path: 'package-lock.json', old_path: 'package-lock.json' },
531
- { new_path: 'config/settings.yml', old_path: 'config/settings.yml' },
532
- { new_path: 'lib/utils/helper.rb', old_path: 'lib/utils/helper.rb' },
533
- { new_path: 'test/unit_test.py', old_path: 'test/unit_test.py' },
534
- { new_path: 'docs/api.md', old_path: 'docs/api.md' },
535
- { new_path: 'assets/style.css', old_path: 'assets/style.css' },
536
- { new_path: 'scripts/build.sh', old_path: 'scripts/build.sh' },
537
- { new_path: 'src/app.component.ts', old_path: 'src/app.component.ts' },
538
- { new_path: 'views/layout.ejs', old_path: 'views/layout.eis' },
539
- { new_path: 'models/user.go', old_path: 'models/user.go' },
540
- { new_path: 'controllers/controller.rs', old_path: 'controllers/controller.rs' },
541
- { new_path: 'database/schema.sql', old_path: 'database/schema.sql' }
542
- ];
543
- // Parse pagination params
544
- const page = parseInt(req.query.page) || 1;
545
- const perPage = parseInt(req.query.per_page) || 20;
546
- const startIdx = (page - 1) * perPage;
547
- const endIdx = startIdx + perPage;
548
- const paginatedDiffs = allDiffs.slice(startIdx, endIdx);
549
- res.json(paginatedDiffs);
550
- });
551
- // Health check endpoint
552
- this.app.get("/health", (req, res) => {
553
- res.json({ status: "ok", message: "Mock GitLab API is running" });
554
- });
555
- // Catch-all for unimplemented endpoints
556
- this.app.use((req, res) => {
557
- console.log(`Mock GitLab: Unimplemented endpoint: ${req.method} ${req.path}`);
558
- res.status(404).json({
559
- message: "404 Not Found",
560
- error: "Endpoint not implemented in mock server",
561
- });
562
- });
563
- }
564
- async start() {
565
- return new Promise((resolve, reject) => {
566
- this.server = this.app.listen(this.config.port, "127.0.0.1", () => {
567
- const address = this.server?.address();
568
- if (typeof address === "object" && address) {
569
- this.config.port = address.port;
570
- }
571
- console.log(`Mock GitLab API listening on http://127.0.0.1:${this.config.port}`);
572
- resolve();
573
- });
574
- this.server.once("error", reject);
575
- });
576
- }
577
- async stop() {
578
- return new Promise((resolve, reject) => {
579
- if (this.server) {
580
- this.server.close(err => {
581
- if (err)
582
- reject(err);
583
- else {
584
- console.log("Mock GitLab API stopped");
585
- resolve();
586
- }
587
- });
588
- }
589
- else {
590
- resolve();
591
- }
592
- });
593
- }
594
- getUrl() {
595
- return `http://127.0.0.1:${this.config.port}`;
596
- }
597
- getPort() {
598
- return this.config.port;
599
- }
600
- }
601
- /**
602
- * Helper to find available port for mock server (OS-assigned ephemeral port).
603
- * Returns 0 to signal MockGitLabServer.start() to use OS-assigned port,
604
- * avoiding TOCTOU race where another test grabs the port before binding.
605
- */
606
- export async function findMockServerPort() {
607
- return 0;
608
- }
609
- /**
610
- * Reset request counter (useful for rate limit testing)
611
- */
612
- export function resetMockServerState(server) {
613
- server.requestCount = 0;
614
- }
@@ -1,28 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, test } from "node:test";
3
- import { ipKeyGenerator } from "express-rate-limit";
4
- import { normalizeProxyClientIpForRateLimit } from "../../utils/proxy-client-ip.js";
5
- function rateLimitKeyFromForwardedIp(ip) {
6
- return ipKeyGenerator(normalizeProxyClientIpForRateLimit(ip));
7
- }
8
- describe("When normalizeProxyClientIpForRateLimit runs", () => {
9
- describe("with proxy-forwarded client addresses", () => {
10
- test("should strip IPv4 ports before ipKeyGenerator accepts the address", () => {
11
- const key = rateLimitKeyFromForwardedIp("160.79.106.36:38914");
12
- assert.equal(key, "160.79.106.36");
13
- });
14
- test("should strip bracketed IPv6 ports before ipKeyGenerator accepts the address", () => {
15
- const key = rateLimitKeyFromForwardedIp("[2001:db8::1]:5678");
16
- assert.equal(key, "2001:db8::/56");
17
- });
18
- test("should strip brackets from bracketed IPv6 addresses without ports", () => {
19
- const key = rateLimitKeyFromForwardedIp("[2001:db8::1]");
20
- assert.equal(key, "2001:db8::/56");
21
- });
22
- test("should leave plain IPv4 and IPv6 addresses unchanged", () => {
23
- assert.equal(rateLimitKeyFromForwardedIp("1.2.3.4"), "1.2.3.4");
24
- assert.equal(rateLimitKeyFromForwardedIp("2001:db8::1"), "2001:db8::/56");
25
- assert.equal(rateLimitKeyFromForwardedIp("::1"), "::/56");
26
- });
27
- });
28
- });
@@ -1,32 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, test } from "node:test";
3
- import { redactSensitiveGitLabFields } from "../../utils/redact-sensitive.js";
4
- describe("When redactSensitiveGitLabFields runs", () => {
5
- describe("with a project response containing credentials", () => {
6
- test("should remove runners_token and import_url while keeping other fields", () => {
7
- const data = {
8
- id: 42,
9
- name: "demo",
10
- runners_token: "GR1348941secret",
11
- import_url: "https://user:pass@example.com/repo.git",
12
- };
13
- const result = redactSensitiveGitLabFields(data);
14
- assert.equal("runners_token" in result, false);
15
- assert.equal("import_url" in result, false);
16
- assert.equal(result.id, 42);
17
- assert.equal(result.name, "demo");
18
- });
19
- });
20
- describe("with a response that has no sensitive fields", () => {
21
- test("should return the object unchanged", () => {
22
- const data = { id: 1, name: "safe" };
23
- assert.deepEqual(redactSensitiveGitLabFields(data), { id: 1, name: "safe" });
24
- });
25
- });
26
- describe("with a non-object value", () => {
27
- test("should return the value as-is", () => {
28
- assert.equal(redactSensitiveGitLabFields(null), null);
29
- assert.equal(redactSensitiveGitLabFields("raw"), "raw");
30
- });
31
- });
32
- });