@zereight/mcp-gitlab 2.1.70 → 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 (157) hide show
  1. package/README.ko.md +3 -1
  2. package/README.md +6 -2
  3. package/README.pt-BR.md +1 -1
  4. package/README.zh-CN.md +3 -1
  5. package/build/config.js +12 -4
  6. package/build/deprecated-env.js +47 -0
  7. package/build/index.js +369 -184
  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/package.json +3 -3
  20. package/build/scripts/check-skill-sync.js +0 -143
  21. package/build/scripts/generate-server-card.js +0 -39
  22. package/build/scripts/generate-tool-coverage.js +0 -58
  23. package/build/scripts/generate-tool-docs.js +0 -430
  24. package/build/scripts/tool-coverage/coverage.js +0 -341
  25. package/build/scripts/tool-coverage/coverage.test.js +0 -142
  26. package/build/test/callback-proxy-tests.js +0 -321
  27. package/build/test/cli/args.test.js +0 -175
  28. package/build/test/cli/boolean-flags.test.js +0 -106
  29. package/build/test/cli/compact-mcp-result.test.js +0 -93
  30. package/build/test/cli/credentials.test.js +0 -153
  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-api-tool-errors.test.js +0 -112
  50. package/build/test/gitlab-artifact-entry-schema.test.js +0 -44
  51. package/build/test/gitlab-diff-schema.test.js +0 -36
  52. package/build/test/gitlab-pipeline-schedule-schema.test.js +0 -32
  53. package/build/test/gitlab-tag-schema.test.js +0 -51
  54. package/build/test/group-milestone-schema.test.js +0 -45
  55. package/build/test/mcp-oauth-tests.js +0 -905
  56. package/build/test/mcp-server-name.test.js +0 -87
  57. package/build/test/merge-merge-request-schema.test.js +0 -75
  58. package/build/test/multi-server-test.js +0 -190
  59. package/build/test/no-proxy-integration-test.js +0 -183
  60. package/build/test/no-proxy-test.js +0 -149
  61. package/build/test/nullable-gitlab-response-fields.test.js +0 -37
  62. package/build/test/nullish-tool-arguments-schema.test.js +0 -118
  63. package/build/test/oauth-device-flow-tests.js +0 -450
  64. package/build/test/oauth-download-url.test.js +0 -157
  65. package/build/test/oauth-startup.test.js +0 -204
  66. package/build/test/oauth-tests.js +0 -428
  67. package/build/test/path-segment-encoding.test.js +0 -98
  68. package/build/test/remote-auth-simple-test.js +0 -277
  69. package/build/test/remote-auth-tests.js +0 -314
  70. package/build/test/response-masking.test.js +0 -445
  71. package/build/test/schema-tests.js +0 -1679
  72. package/build/test/server/metrics.test.js +0 -47
  73. package/build/test/server/request-helpers.test.js +0 -48
  74. package/build/test/server/version.test.js +0 -13
  75. package/build/test/sse-auth-guard.test.js +0 -96
  76. package/build/test/sse-session-limits.test.js +0 -273
  77. package/build/test/stateless/callback-proxy.test.js +0 -510
  78. package/build/test/stateless/client-id.test.js +0 -177
  79. package/build/test/stateless/codec.test.js +0 -328
  80. package/build/test/stateless/config-ttl.test.js +0 -149
  81. package/build/test/stateless/consumed-proxy-code-cache.test.js +0 -150
  82. package/build/test/stateless/session-id-integration.test.js +0 -675
  83. package/build/test/stateless/session-id.test.js +0 -157
  84. package/build/test/streamable-http-concurrent-session.test.js +0 -171
  85. package/build/test/streamable-http-dns-rebinding.test.js +0 -185
  86. package/build/test/streamable-http-sse-stream.test.js +0 -350
  87. package/build/test/streamable-http-static-token-auth.test.js +0 -157
  88. package/build/test/streamable-http-unauthenticated-discovery.test.js +0 -207
  89. package/build/test/test-all-transport-server.js +0 -255
  90. package/build/test/test-auth-retry.js +0 -195
  91. package/build/test/test-blank-filters.js +0 -156
  92. package/build/test/test-ci-catalog.js +0 -177
  93. package/build/test/test-ci-lint.js +0 -191
  94. package/build/test/test-ci-variables.js +0 -309
  95. package/build/test/test-create-repository.js +0 -120
  96. package/build/test/test-dependency-proxy.js +0 -195
  97. package/build/test/test-deployment-tools.js +0 -463
  98. package/build/test/test-download-attachment.js +0 -190
  99. package/build/test/test-dynamic-project-scope.js +0 -524
  100. package/build/test/test-empty-toolsets.js +0 -535
  101. package/build/test/test-get-file-blame.js +0 -145
  102. package/build/test/test-get-file-contents.js +0 -128
  103. package/build/test/test-geteffectiveprojectid.js +0 -804
  104. package/build/test/test-health-check.js +0 -115
  105. package/build/test/test-issue-description-patch.js +0 -256
  106. package/build/test/test-job-artifacts.js +0 -275
  107. package/build/test/test-json-schema.js +0 -166
  108. package/build/test/test-list-group-members.js +0 -150
  109. package/build/test/test-list-group-merge-requests.js +0 -165
  110. package/build/test/test-list-issues.js +0 -170
  111. package/build/test/test-list-merge-requests.js +0 -201
  112. package/build/test/test-list-milestones.js +0 -145
  113. package/build/test/test-list-project-members.js +0 -132
  114. package/build/test/test-merge-request-approval-state-tools.js +0 -213
  115. package/build/test/test-merge-request-approvals.js +0 -187
  116. package/build/test/test-merge-request-pipelines.js +0 -106
  117. package/build/test/test-mr-diffs-filter.js +0 -132
  118. package/build/test/test-mr-file-diffs.js +0 -251
  119. package/build/test/test-oauth-proxy-rate-limit.js +0 -133
  120. package/build/test/test-orbit.js +0 -155
  121. package/build/test/test-permission-mode.js +0 -424
  122. package/build/test/test-pipeline-schedules.js +0 -484
  123. package/build/test/test-protected-branches.js +0 -174
  124. package/build/test/test-remote-downloads.js +0 -510
  125. package/build/test/test-search-code.js +0 -275
  126. package/build/test/test-snippets.js +0 -735
  127. package/build/test/test-tags.js +0 -220
  128. package/build/test/test-todos.js +0 -195
  129. package/build/test/test-token-optimizations.js +0 -691
  130. package/build/test/test-toolset-filtering.js +0 -499
  131. package/build/test/test-update-issue-slim.js +0 -141
  132. package/build/test/test-update-project.js +0 -112
  133. package/build/test/test-upload-markdown.js +0 -206
  134. package/build/test/test-vulnerabilities.js +0 -399
  135. package/build/test/test-webhooks.js +0 -218
  136. package/build/test/tool-description-quality.test.js +0 -50
  137. package/build/test/utils/bulk-publish-options.test.js +0 -32
  138. package/build/test/utils/download-token.test.js +0 -35
  139. package/build/test/utils/forwarded-public-base-url.test.js +0 -46
  140. package/build/test/utils/gitlab-instance-version.test.js +0 -32
  141. package/build/test/utils/gitlab-version-gate.test.js +0 -74
  142. package/build/test/utils/graphql-query.test.js +0 -168
  143. package/build/test/utils/helpers-path.test.js +0 -104
  144. package/build/test/utils/jmespath-tool-result.test.js +0 -91
  145. package/build/test/utils/masking.test.js +0 -384
  146. package/build/test/utils/merge-request-position.test.js +0 -57
  147. package/build/test/utils/mock-gitlab-server.js +0 -614
  148. package/build/test/utils/proxy-client-ip.test.js +0 -28
  149. package/build/test/utils/redact-sensitive.test.js +0 -32
  150. package/build/test/utils/safe-redirect-fetch.test.js +0 -463
  151. package/build/test/utils/server-launcher.js +0 -231
  152. package/build/test/utils/tool-args.test.js +0 -210
  153. package/build/test/utils/url.test.js +0 -141
  154. package/build/test/utils/version-check.test.js +0 -239
  155. package/build/test/utils/wiki-title.test.js +0 -21
  156. package/build/test-note.js +0 -54
  157. 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
- });