@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.
- package/README.ko.md +3 -1
- package/README.md +6 -2
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +3 -1
- package/build/config.js +12 -4
- package/build/deprecated-env.js +47 -0
- package/build/index.js +369 -184
- package/build/oauth-device-flow.js +80 -0
- package/build/oauth.js +135 -18
- package/build/schemas.js +1 -1
- package/build/server/remote-http-stack.js +39 -0
- package/build/tools/discover-activation.js +38 -0
- package/build/tools/exposed-tools.js +35 -0
- package/build/tools/listed-tools.js +76 -0
- package/build/tools/registry.js +226 -57
- package/build/tools/tool-profile.js +66 -0
- package/build/utils/compact-tool-allowlist.js +14 -0
- package/build/utils/logger.js +34 -12
- package/package.json +3 -3
- package/build/scripts/check-skill-sync.js +0 -143
- package/build/scripts/generate-server-card.js +0 -39
- package/build/scripts/generate-tool-coverage.js +0 -58
- package/build/scripts/generate-tool-docs.js +0 -430
- package/build/scripts/tool-coverage/coverage.js +0 -341
- package/build/scripts/tool-coverage/coverage.test.js +0 -142
- package/build/test/callback-proxy-tests.js +0 -321
- package/build/test/cli/args.test.js +0 -175
- package/build/test/cli/boolean-flags.test.js +0 -106
- package/build/test/cli/compact-mcp-result.test.js +0 -93
- package/build/test/cli/credentials.test.js +0 -153
- package/build/test/cli/exposure.test.js +0 -104
- package/build/test/cli/load-config-snapshot.js +0 -5
- package/build/test/cli/output.test.js +0 -74
- package/build/test/cli/replay-command.test.js +0 -37
- package/build/test/cli/router.test.js +0 -183
- package/build/test/client-pool-test.js +0 -109
- package/build/test/clients/client.js +0 -34
- package/build/test/clients/custom-header-client.js +0 -122
- package/build/test/clients/sse-client.js +0 -95
- package/build/test/clients/stdio-client.js +0 -113
- package/build/test/clients/streamable-http-client.js +0 -95
- package/build/test/config-allowed-groups.test.js +0 -97
- package/build/test/create-commit-actions.test.js +0 -102
- package/build/test/draft-note-schema.test.js +0 -44
- package/build/test/dynamic-api-url-allowlist.test.js +0 -121
- package/build/test/dynamic-api-url-test.js +0 -304
- package/build/test/dynamic-routing-tests.js +0 -495
- package/build/test/get-merge-request-discussion-schema.test.js +0 -30
- package/build/test/gitlab-api-tool-errors.test.js +0 -112
- package/build/test/gitlab-artifact-entry-schema.test.js +0 -44
- package/build/test/gitlab-diff-schema.test.js +0 -36
- package/build/test/gitlab-pipeline-schedule-schema.test.js +0 -32
- package/build/test/gitlab-tag-schema.test.js +0 -51
- package/build/test/group-milestone-schema.test.js +0 -45
- package/build/test/mcp-oauth-tests.js +0 -905
- package/build/test/mcp-server-name.test.js +0 -87
- package/build/test/merge-merge-request-schema.test.js +0 -75
- package/build/test/multi-server-test.js +0 -190
- package/build/test/no-proxy-integration-test.js +0 -183
- package/build/test/no-proxy-test.js +0 -149
- package/build/test/nullable-gitlab-response-fields.test.js +0 -37
- package/build/test/nullish-tool-arguments-schema.test.js +0 -118
- package/build/test/oauth-device-flow-tests.js +0 -450
- package/build/test/oauth-download-url.test.js +0 -157
- package/build/test/oauth-startup.test.js +0 -204
- package/build/test/oauth-tests.js +0 -428
- package/build/test/path-segment-encoding.test.js +0 -98
- package/build/test/remote-auth-simple-test.js +0 -277
- package/build/test/remote-auth-tests.js +0 -314
- package/build/test/response-masking.test.js +0 -445
- package/build/test/schema-tests.js +0 -1679
- package/build/test/server/metrics.test.js +0 -47
- package/build/test/server/request-helpers.test.js +0 -48
- package/build/test/server/version.test.js +0 -13
- package/build/test/sse-auth-guard.test.js +0 -96
- package/build/test/sse-session-limits.test.js +0 -273
- package/build/test/stateless/callback-proxy.test.js +0 -510
- package/build/test/stateless/client-id.test.js +0 -177
- package/build/test/stateless/codec.test.js +0 -328
- package/build/test/stateless/config-ttl.test.js +0 -149
- package/build/test/stateless/consumed-proxy-code-cache.test.js +0 -150
- package/build/test/stateless/session-id-integration.test.js +0 -675
- package/build/test/stateless/session-id.test.js +0 -157
- package/build/test/streamable-http-concurrent-session.test.js +0 -171
- package/build/test/streamable-http-dns-rebinding.test.js +0 -185
- package/build/test/streamable-http-sse-stream.test.js +0 -350
- package/build/test/streamable-http-static-token-auth.test.js +0 -157
- package/build/test/streamable-http-unauthenticated-discovery.test.js +0 -207
- package/build/test/test-all-transport-server.js +0 -255
- package/build/test/test-auth-retry.js +0 -195
- package/build/test/test-blank-filters.js +0 -156
- package/build/test/test-ci-catalog.js +0 -177
- package/build/test/test-ci-lint.js +0 -191
- package/build/test/test-ci-variables.js +0 -309
- package/build/test/test-create-repository.js +0 -120
- package/build/test/test-dependency-proxy.js +0 -195
- package/build/test/test-deployment-tools.js +0 -463
- package/build/test/test-download-attachment.js +0 -190
- package/build/test/test-dynamic-project-scope.js +0 -524
- package/build/test/test-empty-toolsets.js +0 -535
- package/build/test/test-get-file-blame.js +0 -145
- package/build/test/test-get-file-contents.js +0 -128
- package/build/test/test-geteffectiveprojectid.js +0 -804
- package/build/test/test-health-check.js +0 -115
- package/build/test/test-issue-description-patch.js +0 -256
- package/build/test/test-job-artifacts.js +0 -275
- package/build/test/test-json-schema.js +0 -166
- package/build/test/test-list-group-members.js +0 -150
- package/build/test/test-list-group-merge-requests.js +0 -165
- package/build/test/test-list-issues.js +0 -170
- package/build/test/test-list-merge-requests.js +0 -201
- package/build/test/test-list-milestones.js +0 -145
- package/build/test/test-list-project-members.js +0 -132
- package/build/test/test-merge-request-approval-state-tools.js +0 -213
- package/build/test/test-merge-request-approvals.js +0 -187
- package/build/test/test-merge-request-pipelines.js +0 -106
- package/build/test/test-mr-diffs-filter.js +0 -132
- package/build/test/test-mr-file-diffs.js +0 -251
- package/build/test/test-oauth-proxy-rate-limit.js +0 -133
- package/build/test/test-orbit.js +0 -155
- package/build/test/test-permission-mode.js +0 -424
- package/build/test/test-pipeline-schedules.js +0 -484
- package/build/test/test-protected-branches.js +0 -174
- package/build/test/test-remote-downloads.js +0 -510
- package/build/test/test-search-code.js +0 -275
- package/build/test/test-snippets.js +0 -735
- package/build/test/test-tags.js +0 -220
- package/build/test/test-todos.js +0 -195
- package/build/test/test-token-optimizations.js +0 -691
- package/build/test/test-toolset-filtering.js +0 -499
- package/build/test/test-update-issue-slim.js +0 -141
- package/build/test/test-update-project.js +0 -112
- package/build/test/test-upload-markdown.js +0 -206
- package/build/test/test-vulnerabilities.js +0 -399
- package/build/test/test-webhooks.js +0 -218
- package/build/test/tool-description-quality.test.js +0 -50
- package/build/test/utils/bulk-publish-options.test.js +0 -32
- package/build/test/utils/download-token.test.js +0 -35
- package/build/test/utils/forwarded-public-base-url.test.js +0 -46
- package/build/test/utils/gitlab-instance-version.test.js +0 -32
- package/build/test/utils/gitlab-version-gate.test.js +0 -74
- package/build/test/utils/graphql-query.test.js +0 -168
- package/build/test/utils/helpers-path.test.js +0 -104
- package/build/test/utils/jmespath-tool-result.test.js +0 -91
- package/build/test/utils/masking.test.js +0 -384
- package/build/test/utils/merge-request-position.test.js +0 -57
- package/build/test/utils/mock-gitlab-server.js +0 -614
- package/build/test/utils/proxy-client-ip.test.js +0 -28
- package/build/test/utils/redact-sensitive.test.js +0 -32
- package/build/test/utils/safe-redirect-fetch.test.js +0 -463
- package/build/test/utils/server-launcher.js +0 -231
- package/build/test/utils/tool-args.test.js +0 -210
- package/build/test/utils/url.test.js +0 -141
- package/build/test/utils/version-check.test.js +0 -239
- package/build/test/utils/wiki-title.test.js +0 -21
- package/build/test-note.js +0 -54
- 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
|
-
});
|