@zereight/mcp-gitlab 2.1.70 → 2.2.1

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