@zereight/mcp-gitlab 2.1.29 → 2.1.38
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 +72 -20
- package/README.md +120 -82
- package/README.zh-CN.md +72 -20
- package/build/config.js +13 -0
- package/build/downloads/proxy.js +199 -0
- package/build/index.js +850 -490
- package/build/schemas.js +115 -28
- package/build/scripts/check-skill-sync.js +137 -0
- package/build/scripts/generate-tool-docs.js +8 -3
- package/build/server/metrics.js +84 -0
- package/build/server/transport-mode.js +25 -0
- package/build/test/callback-proxy-tests.js +1 -1
- package/build/test/client-pool-test.js +1 -1
- package/build/test/dynamic-api-url-allowlist.test.js +2 -2
- package/build/test/dynamic-api-url-test.js +4 -4
- package/build/test/dynamic-routing-tests.js +4 -4
- package/build/test/group-milestone-schema.test.js +45 -0
- package/build/test/mcp-oauth-tests.js +29 -4
- package/build/test/mcp-server-name.test.js +87 -0
- package/build/test/multi-server-test.js +3 -3
- package/build/test/no-proxy-integration-test.js +1 -1
- package/build/test/nullable-gitlab-response-fields.test.js +22 -0
- package/build/test/path-segment-encoding.test.js +11 -0
- package/build/test/remote-auth-simple-test.js +226 -199
- package/build/test/server/metrics.test.js +47 -0
- package/build/test/sse-auth-guard.test.js +2 -2
- package/build/test/stateless/session-id-integration.test.js +3 -3
- package/build/test/streamable-http-concurrent-session.test.js +64 -1
- package/build/test/streamable-http-dns-rebinding.test.js +185 -0
- package/build/test/streamable-http-static-token-auth.test.js +25 -22
- package/build/test/streamable-http-unauthenticated-discovery.test.js +28 -15
- package/build/test/test-ci-catalog.js +1 -1
- package/build/test/test-ci-lint.js +1 -1
- package/build/test/test-ci-variables.js +8 -5
- package/build/test/test-dependency-proxy.js +11 -7
- package/build/test/test-deployment-tools.js +16 -2
- package/build/test/test-download-attachment.js +1 -1
- package/build/test/test-get-file-blame.js +1 -1
- package/build/test/test-geteffectiveprojectid.js +221 -8
- package/build/test/test-issue-description-patch.js +1 -1
- package/build/test/test-job-artifacts.js +1 -1
- package/build/test/test-list-issues.js +1 -1
- package/build/test/test-list-merge-requests.js +1 -1
- package/build/test/test-list-project-members.js +1 -1
- package/build/test/test-merge-request-approval-state-tools.js +1 -1
- package/build/test/test-merge-request-pipelines.js +1 -1
- package/build/test/test-mr-diffs-filter.js +1 -1
- package/build/test/test-mr-file-diffs.js +2 -2
- package/build/test/test-oauth-proxy-rate-limit.js +1 -1
- package/build/test/test-permission-mode.js +216 -0
- package/build/test/test-protected-branches.js +23 -4
- package/build/test/test-remote-downloads.js +2 -2
- package/build/test/test-search-code.js +1 -1
- package/build/test/test-tags.js +1 -1
- package/build/test/test-todos.js +1 -1
- package/build/test/test-token-optimizations.js +3 -3
- package/build/test/test-toolset-filtering.js +3 -3
- package/build/test/test-update-issue-slim.js +141 -0
- package/build/test/test-upload-markdown.js +1 -1
- package/build/test/utils/download-token.test.js +35 -0
- package/build/test/utils/forwarded-public-base-url.test.js +9 -1
- package/build/test/utils/graphql-query.test.js +64 -1
- package/build/test/utils/mock-gitlab-server.js +24 -31
- package/build/test/utils/server-launcher.js +1 -2
- package/build/test/utils/version-check.test.js +52 -0
- package/build/tools/registry.js +93 -5
- package/build/utils/download-token.js +71 -0
- package/build/utils/forwarded-public-base-url.js +22 -0
- package/build/utils/graphql-query.js +109 -12
- package/build/utils/version-check.js +40 -0
- package/package.json +5 -4
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import http from "node:http";
|
|
4
|
+
import { afterEach, describe, test } from "node:test";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { findAvailablePort } from "./utils/server-launcher.js";
|
|
7
|
+
import { MockGitLabServer, findMockServerPort } from "./utils/mock-gitlab-server.js";
|
|
8
|
+
const HOST = process.env.HOST || "127.0.0.1";
|
|
9
|
+
const SERVER_PATH = path.resolve(process.cwd(), "build/index.js");
|
|
10
|
+
const TEST_TOKEN = "test-token-123456789012345";
|
|
11
|
+
const running = new Set();
|
|
12
|
+
const mockGitLabServers = [];
|
|
13
|
+
function hostWithPort(host, port) {
|
|
14
|
+
const normalized = host.trim();
|
|
15
|
+
if (normalized.includes(":") && !normalized.startsWith("["))
|
|
16
|
+
return `[${normalized}]:${port}`;
|
|
17
|
+
return `${normalized}:${port}`;
|
|
18
|
+
}
|
|
19
|
+
function startServer(env, port) {
|
|
20
|
+
const child = spawn("node", [SERVER_PATH], {
|
|
21
|
+
env: {
|
|
22
|
+
...process.env,
|
|
23
|
+
GITLAB_API_URL: "https://gitlab.example.com",
|
|
24
|
+
HOST,
|
|
25
|
+
PORT: String(port),
|
|
26
|
+
STREAMABLE_HTTP: "true",
|
|
27
|
+
REMOTE_AUTHORIZATION: "true",
|
|
28
|
+
GITLAB_MCP_OAUTH: "false",
|
|
29
|
+
GITLAB_USE_OAUTH: "false",
|
|
30
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: "",
|
|
31
|
+
GITLAB_JOB_TOKEN: "",
|
|
32
|
+
GITLAB_AUTH_COOKIE_PATH: "",
|
|
33
|
+
MCP_SERVER_URL: "",
|
|
34
|
+
GITLAB_OAUTH_APP_ID: "",
|
|
35
|
+
...env,
|
|
36
|
+
},
|
|
37
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
38
|
+
});
|
|
39
|
+
running.add(child);
|
|
40
|
+
child.once("exit", () => running.delete(child));
|
|
41
|
+
return child;
|
|
42
|
+
}
|
|
43
|
+
async function startAuthenticatedServer(env, port) {
|
|
44
|
+
const mockPort = await findMockServerPort();
|
|
45
|
+
const mockGitLab = new MockGitLabServer({
|
|
46
|
+
port: mockPort,
|
|
47
|
+
validTokens: [TEST_TOKEN],
|
|
48
|
+
});
|
|
49
|
+
await mockGitLab.start();
|
|
50
|
+
mockGitLabServers.push(mockGitLab);
|
|
51
|
+
startServer({
|
|
52
|
+
GITLAB_API_URL: `${mockGitLab.getUrl()}/api/v4`,
|
|
53
|
+
...env,
|
|
54
|
+
}, port);
|
|
55
|
+
}
|
|
56
|
+
async function waitForHealth(port, timeoutMs = 5000) {
|
|
57
|
+
const deadline = Date.now() + timeoutMs;
|
|
58
|
+
let lastError;
|
|
59
|
+
while (Date.now() < deadline) {
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(`http://${hostWithPort(HOST, port)}/health`);
|
|
62
|
+
if (response.ok)
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
lastError = error;
|
|
67
|
+
}
|
|
68
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`server did not become healthy: ${String(lastError)}`);
|
|
71
|
+
}
|
|
72
|
+
function postMcp(port, headers, body = JSON.stringify({
|
|
73
|
+
jsonrpc: "2.0",
|
|
74
|
+
id: 1,
|
|
75
|
+
method: "initialize",
|
|
76
|
+
params: {
|
|
77
|
+
protocolVersion: "2025-03-26",
|
|
78
|
+
capabilities: {},
|
|
79
|
+
clientInfo: { name: "dns-rebinding-test", version: "1.0.0" },
|
|
80
|
+
},
|
|
81
|
+
})) {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const req = http.request({
|
|
84
|
+
host: HOST,
|
|
85
|
+
port,
|
|
86
|
+
path: "/mcp",
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: {
|
|
89
|
+
Host: hostWithPort(HOST, port),
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
Accept: "application/json, text/event-stream",
|
|
92
|
+
"Private-Token": TEST_TOKEN,
|
|
93
|
+
"Content-Length": Buffer.byteLength(body).toString(),
|
|
94
|
+
...headers,
|
|
95
|
+
},
|
|
96
|
+
}, res => {
|
|
97
|
+
let responseBody = "";
|
|
98
|
+
res.setEncoding("utf8");
|
|
99
|
+
res.on("data", chunk => {
|
|
100
|
+
responseBody += chunk;
|
|
101
|
+
});
|
|
102
|
+
res.on("end", () => resolve({ status: res.statusCode ?? 0, body: responseBody }));
|
|
103
|
+
});
|
|
104
|
+
req.on("error", reject);
|
|
105
|
+
req.end(body);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
afterEach(async () => {
|
|
109
|
+
for (const child of running) {
|
|
110
|
+
if (!child.killed)
|
|
111
|
+
child.kill("SIGTERM");
|
|
112
|
+
}
|
|
113
|
+
running.clear();
|
|
114
|
+
while (mockGitLabServers.length > 0) {
|
|
115
|
+
const mockGitLab = mockGitLabServers.pop();
|
|
116
|
+
if (mockGitLab) {
|
|
117
|
+
await mockGitLab.stop();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
describe("Streamable HTTP DNS rebinding protection", () => {
|
|
122
|
+
test("rejects forged Host and Origin headers before handling /mcp", async () => {
|
|
123
|
+
const port = await findAvailablePort(4700);
|
|
124
|
+
await startAuthenticatedServer({}, port);
|
|
125
|
+
await waitForHealth(port);
|
|
126
|
+
const validHost = hostWithPort(HOST, port);
|
|
127
|
+
const badJson = await postMcp(port, {
|
|
128
|
+
Host: "attacker.example.test",
|
|
129
|
+
Origin: `http://${validHost}`,
|
|
130
|
+
}, "{");
|
|
131
|
+
assert.equal(badJson.status, 403);
|
|
132
|
+
assert.match(badJson.body, /Host header is not allowed/);
|
|
133
|
+
assert.match(badJson.body, /MCP_ALLOWED_HOSTS/);
|
|
134
|
+
const badHost = await postMcp(port, {
|
|
135
|
+
Host: "attacker.example.test",
|
|
136
|
+
Origin: `http://${validHost}`,
|
|
137
|
+
});
|
|
138
|
+
assert.equal(badHost.status, 403);
|
|
139
|
+
assert.match(badHost.body, /Host header is not allowed/);
|
|
140
|
+
assert.match(badHost.body, /MCP_ALLOWED_HOSTS/);
|
|
141
|
+
const badOrigin = await postMcp(port, {
|
|
142
|
+
Host: validHost,
|
|
143
|
+
Origin: "https://attacker.example.test",
|
|
144
|
+
});
|
|
145
|
+
assert.equal(badOrigin.status, 403);
|
|
146
|
+
assert.match(badOrigin.body, /Origin header is not allowed/);
|
|
147
|
+
assert.match(badOrigin.body, /MCP_ALLOWED_ORIGINS/);
|
|
148
|
+
const ok = await postMcp(port, { Host: validHost });
|
|
149
|
+
assert.equal(ok.status, 200);
|
|
150
|
+
});
|
|
151
|
+
test("allows loopback Host and Origin on any port (Docker port mapping)", async () => {
|
|
152
|
+
const port = await findAvailablePort(4720);
|
|
153
|
+
await startAuthenticatedServer({}, port);
|
|
154
|
+
await waitForHealth(port);
|
|
155
|
+
// e.g. docker run -p 3333:3002 -> client sends Host: localhost:3333
|
|
156
|
+
const remappedHost = await postMcp(port, { Host: `localhost:${port + 1000}` });
|
|
157
|
+
assert.equal(remappedHost.status, 200);
|
|
158
|
+
const remappedIpv4 = await postMcp(port, { Host: `127.0.0.1:${port + 1000}` });
|
|
159
|
+
assert.equal(remappedIpv4.status, 200);
|
|
160
|
+
const remappedOrigin = await postMcp(port, {
|
|
161
|
+
Host: `localhost:${port + 1000}`,
|
|
162
|
+
Origin: `http://localhost:${port + 1000}`,
|
|
163
|
+
});
|
|
164
|
+
assert.equal(remappedOrigin.status, 200);
|
|
165
|
+
const bareLoopback = await postMcp(port, { Host: "localhost" });
|
|
166
|
+
assert.equal(bareLoopback.status, 200);
|
|
167
|
+
const loopbackHostBadOrigin = await postMcp(port, {
|
|
168
|
+
Host: `localhost:${port + 1000}`,
|
|
169
|
+
Origin: "http://attacker.example.test",
|
|
170
|
+
});
|
|
171
|
+
assert.equal(loopbackHostBadOrigin.status, 403);
|
|
172
|
+
assert.match(loopbackHostBadOrigin.body, /Origin header is not allowed/);
|
|
173
|
+
assert.match(loopbackHostBadOrigin.body, /MCP_ALLOWED_ORIGINS/);
|
|
174
|
+
});
|
|
175
|
+
test("allows the configured MCP_SERVER_URL host and origin", async () => {
|
|
176
|
+
const port = await findAvailablePort(4710);
|
|
177
|
+
await startAuthenticatedServer({ MCP_SERVER_URL: "https://mcp.example.test" }, port);
|
|
178
|
+
await waitForHealth(port);
|
|
179
|
+
const ok = await postMcp(port, {
|
|
180
|
+
Host: "mcp.example.test",
|
|
181
|
+
Origin: "https://mcp.example.test",
|
|
182
|
+
});
|
|
183
|
+
assert.equal(ok.status, 200);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -4,7 +4,7 @@ import { once } from "node:events";
|
|
|
4
4
|
import { afterEach, describe, test } from "node:test";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { findAvailablePort } from "./utils/server-launcher.js";
|
|
7
|
-
const ERROR_MESSAGE = "STREAMABLE_HTTP=true/--streamable-http with
|
|
7
|
+
const ERROR_MESSAGE = "STREAMABLE_HTTP=true/--streamable-http with server-side GitLab credentials requires REMOTE_AUTHORIZATION=true/--remote-auth=true, GITLAB_MCP_OAUTH=true/--mcp-oauth=true, or STREAMABLE_HTTP_AUTH_TOKEN/--streamable-http-auth-token";
|
|
8
8
|
const HOST = process.env.HOST || "127.0.0.1";
|
|
9
9
|
const SERVER_PATH = path.resolve(process.cwd(), "build/index.js");
|
|
10
10
|
const running = new Set();
|
|
@@ -69,11 +69,15 @@ async function waitForHealth(port, timeoutMs = 5000) {
|
|
|
69
69
|
throw new Error(`server did not become healthy: ${String(lastError)}`);
|
|
70
70
|
}
|
|
71
71
|
async function postMcpWithoutAuth(port) {
|
|
72
|
+
return postMcp(port);
|
|
73
|
+
}
|
|
74
|
+
async function postMcp(port, token) {
|
|
72
75
|
return fetch(`http://${HOST}:${port}/mcp`, {
|
|
73
76
|
method: "POST",
|
|
74
77
|
headers: {
|
|
75
78
|
"Content-Type": "application/json",
|
|
76
79
|
Accept: "application/json, text/event-stream",
|
|
80
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
77
81
|
},
|
|
78
82
|
body: JSON.stringify({
|
|
79
83
|
jsonrpc: "2.0",
|
|
@@ -87,22 +91,6 @@ async function postMcpWithoutAuth(port) {
|
|
|
87
91
|
}),
|
|
88
92
|
});
|
|
89
93
|
}
|
|
90
|
-
async function postMcpWithSessionWithoutAuth(port, sessionId) {
|
|
91
|
-
return fetch(`http://${HOST}:${port}/mcp`, {
|
|
92
|
-
method: "POST",
|
|
93
|
-
headers: {
|
|
94
|
-
"Content-Type": "application/json",
|
|
95
|
-
Accept: "application/json, text/event-stream",
|
|
96
|
-
"Mcp-Session-Id": sessionId,
|
|
97
|
-
},
|
|
98
|
-
body: JSON.stringify({
|
|
99
|
-
jsonrpc: "2.0",
|
|
100
|
-
id: 2,
|
|
101
|
-
method: "tools/list",
|
|
102
|
-
params: {},
|
|
103
|
-
}),
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
94
|
afterEach(() => {
|
|
107
95
|
for (const child of running) {
|
|
108
96
|
if (!child.killed)
|
|
@@ -125,6 +113,13 @@ describe("Streamable HTTP static server token auth", () => {
|
|
|
125
113
|
assert.notEqual(code, 0);
|
|
126
114
|
assert.match(output, new RegExp(ERROR_MESSAGE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
127
115
|
});
|
|
116
|
+
test("refuses startup with cookie auth and no MCP-layer auth", async () => {
|
|
117
|
+
const port = await findAvailablePort(4315);
|
|
118
|
+
const child = startServer({ GITLAB_AUTH_COOKIE_PATH: "./cookies.txt" }, port);
|
|
119
|
+
const { code, output } = await waitForExit(child);
|
|
120
|
+
assert.notEqual(code, 0);
|
|
121
|
+
assert.match(output, new RegExp(ERROR_MESSAGE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
122
|
+
});
|
|
128
123
|
test("allows startup with PAT when REMOTE_AUTHORIZATION is enabled", async () => {
|
|
129
124
|
const port = await findAvailablePort(4320);
|
|
130
125
|
startServer({
|
|
@@ -133,11 +128,19 @@ describe("Streamable HTTP static server token auth", () => {
|
|
|
133
128
|
}, port);
|
|
134
129
|
await waitForHealth(port);
|
|
135
130
|
const initResponse = await postMcpWithoutAuth(port);
|
|
136
|
-
assert.equal(initResponse.status,
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
131
|
+
assert.equal(initResponse.status, 401);
|
|
132
|
+
});
|
|
133
|
+
test("requires configured Streamable HTTP auth token", async () => {
|
|
134
|
+
const port = await findAvailablePort(4325);
|
|
135
|
+
startServer({
|
|
136
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: "glpat_test",
|
|
137
|
+
STREAMABLE_HTTP_AUTH_TOKEN: "mcp_static_secret",
|
|
138
|
+
}, port);
|
|
139
|
+
await waitForHealth(port);
|
|
140
|
+
const unauthenticated = await postMcpWithoutAuth(port);
|
|
141
|
+
assert.equal(unauthenticated.status, 401);
|
|
142
|
+
const authenticated = await postMcp(port, "mcp_static_secret");
|
|
143
|
+
assert.equal(authenticated.status, 200);
|
|
141
144
|
});
|
|
142
145
|
test("allows startup with PAT when GITLAB_MCP_OAUTH is enabled", async () => {
|
|
143
146
|
const port = await findAvailablePort(4330);
|
|
@@ -36,24 +36,26 @@ async function rawMcpRequest(url, body, headers = {}) {
|
|
|
36
36
|
text,
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
+
const initializeBody = {
|
|
40
|
+
jsonrpc: "2.0",
|
|
41
|
+
id: 1,
|
|
42
|
+
method: "initialize",
|
|
43
|
+
params: {
|
|
44
|
+
protocolVersion: "2025-03-26",
|
|
45
|
+
capabilities: {},
|
|
46
|
+
clientInfo: { name: "unauth-discovery-test", version: "1.0.0" },
|
|
47
|
+
},
|
|
48
|
+
};
|
|
39
49
|
async function initialize(mcpUrl) {
|
|
40
|
-
const response = await rawMcpRequest(mcpUrl,
|
|
41
|
-
jsonrpc: "2.0",
|
|
42
|
-
id: 1,
|
|
43
|
-
method: "initialize",
|
|
44
|
-
params: {
|
|
45
|
-
protocolVersion: "2025-03-26",
|
|
46
|
-
capabilities: {},
|
|
47
|
-
clientInfo: { name: "unauth-discovery-test", version: "1.0.0" },
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
+
const response = await rawMcpRequest(mcpUrl, initializeBody);
|
|
50
51
|
assert.strictEqual(response.status, 200, response.text);
|
|
51
52
|
assert.ok(response.sessionId, "initialize should return Mcp-Session-Id");
|
|
52
53
|
return response.sessionId;
|
|
53
54
|
}
|
|
54
55
|
let portOffset = 0;
|
|
55
56
|
async function launchRemoteAuthServer(extraEnv = {}) {
|
|
56
|
-
|
|
57
|
+
// Keep above remote-auth-simple-test timeout suite (3500+) to avoid parallel bind races.
|
|
58
|
+
const port = await findAvailablePort(3600 + portOffset++ * 10);
|
|
57
59
|
const server = await launchServer({
|
|
58
60
|
mode: TransportMode.STREAMABLE_HTTP,
|
|
59
61
|
port,
|
|
@@ -73,12 +75,23 @@ describe("Streamable HTTP unauthenticated tool discovery", { timeout: 20_000 },
|
|
|
73
75
|
cleanupServers(servers);
|
|
74
76
|
servers = [];
|
|
75
77
|
});
|
|
76
|
-
test("keeps unauthenticated
|
|
78
|
+
test("keeps unauthenticated initialize blocked by default", async () => {
|
|
77
79
|
const { server, mcpUrl } = await launchRemoteAuthServer();
|
|
78
80
|
servers.push(server);
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
assert.strictEqual(
|
|
81
|
+
const initResponse = await rawMcpRequest(mcpUrl, initializeBody);
|
|
82
|
+
assert.strictEqual(initResponse.status, 401, initResponse.text);
|
|
83
|
+
assert.strictEqual(initResponse.sessionId, null);
|
|
84
|
+
});
|
|
85
|
+
test("rejects invalid auth headers even when unauthenticated discovery is enabled", async () => {
|
|
86
|
+
const { server, mcpUrl } = await launchRemoteAuthServer({
|
|
87
|
+
GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY: "true",
|
|
88
|
+
});
|
|
89
|
+
servers.push(server);
|
|
90
|
+
const initResponse = await rawMcpRequest(mcpUrl, initializeBody, {
|
|
91
|
+
Authorization: "Bearer definitely-not-valid",
|
|
92
|
+
});
|
|
93
|
+
assert.strictEqual(initResponse.status, 401, initResponse.text);
|
|
94
|
+
assert.strictEqual(initResponse.sessionId, null);
|
|
82
95
|
});
|
|
83
96
|
test("allows unauthenticated tools/list when explicitly enabled", async () => {
|
|
84
97
|
const { server, mcpUrl } = await launchRemoteAuthServer({
|
|
@@ -56,7 +56,7 @@ describe("CI/CD Catalog tools", () => {
|
|
|
56
56
|
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
57
57
|
});
|
|
58
58
|
before(async () => {
|
|
59
|
-
const port = await findMockServerPort(
|
|
59
|
+
const port = await findMockServerPort();
|
|
60
60
|
mockGitLab = new MockGitLabServer({ port, validTokens: [MOCK_TOKEN] });
|
|
61
61
|
mockGitLab.addRootHandler("post", "/api/graphql", (req, res) => {
|
|
62
62
|
if (req.body.query.includes("ListCiCatalogResources")) {
|
|
@@ -92,7 +92,7 @@ describe("GitLab CI lint tools", () => {
|
|
|
92
92
|
let mockGitLab;
|
|
93
93
|
let mockGitLabUrl;
|
|
94
94
|
before(async () => {
|
|
95
|
-
const mockPort = await findMockServerPort(
|
|
95
|
+
const mockPort = await findMockServerPort();
|
|
96
96
|
mockGitLab = new MockGitLabServer({
|
|
97
97
|
port: mockPort,
|
|
98
98
|
validTokens: [MOCK_TOKEN],
|
|
@@ -77,12 +77,14 @@ async function callTool(toolName, args, env) {
|
|
|
77
77
|
}
|
|
78
78
|
describe("CI/CD variable tools", () => {
|
|
79
79
|
let mockServer;
|
|
80
|
-
let
|
|
80
|
+
let mockApiUrl;
|
|
81
81
|
let baseEnv;
|
|
82
82
|
let lastReceivedFilterScope;
|
|
83
83
|
before(async () => {
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
mockServer = new MockGitLabServer({
|
|
85
|
+
port: await findMockServerPort(),
|
|
86
|
+
validTokens: [MOCK_TOKEN],
|
|
87
|
+
});
|
|
86
88
|
// --- Project variable endpoints ---
|
|
87
89
|
mockServer.addMockHandler("get", `/projects/${TEST_PROJECT_ID}/variables`, (_req, res) => {
|
|
88
90
|
res.json([MOCK_PROJECT_VARIABLE]);
|
|
@@ -124,9 +126,10 @@ describe("CI/CD variable tools", () => {
|
|
|
124
126
|
res.status(204).send();
|
|
125
127
|
});
|
|
126
128
|
await mockServer.start();
|
|
129
|
+
mockApiUrl = `${mockServer.getUrl()}/api/v4`;
|
|
127
130
|
baseEnv = {
|
|
128
131
|
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
129
|
-
GITLAB_API_URL:
|
|
132
|
+
GITLAB_API_URL: mockApiUrl,
|
|
130
133
|
GITLAB_TOOLSETS: "variables",
|
|
131
134
|
};
|
|
132
135
|
});
|
|
@@ -245,7 +248,7 @@ describe("CI/CD variable tools", () => {
|
|
|
245
248
|
env: {
|
|
246
249
|
...process.env,
|
|
247
250
|
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
248
|
-
GITLAB_API_URL:
|
|
251
|
+
GITLAB_API_URL: mockApiUrl,
|
|
249
252
|
// No GITLAB_TOOLSETS — default toolsets only
|
|
250
253
|
},
|
|
251
254
|
});
|
|
@@ -45,11 +45,16 @@ async function callTool(toolName, args, env) {
|
|
|
45
45
|
}
|
|
46
46
|
describe("dependency proxy tools", () => {
|
|
47
47
|
let mockServer;
|
|
48
|
-
let
|
|
48
|
+
let mockApiUrl;
|
|
49
49
|
let baseEnv;
|
|
50
50
|
before(async () => {
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
mockServer = new MockGitLabServer({
|
|
52
|
+
port: await findMockServerPort(),
|
|
53
|
+
validTokens: [MOCK_TOKEN],
|
|
54
|
+
});
|
|
55
|
+
await mockServer.start();
|
|
56
|
+
const mockHost = new URL(mockServer.getUrl()).host;
|
|
57
|
+
mockApiUrl = `${mockServer.getUrl()}/api/v4`;
|
|
53
58
|
// Mock GraphQL endpoint for get/list operations
|
|
54
59
|
mockServer.addRootHandler("post", "/api/graphql", (req, res) => {
|
|
55
60
|
const { query } = req.body;
|
|
@@ -63,7 +68,7 @@ describe("dependency proxy tools", () => {
|
|
|
63
68
|
dependencyProxySetting: { enabled: true },
|
|
64
69
|
dependencyProxyBlobCount: 3,
|
|
65
70
|
dependencyProxyTotalSize: "15728640",
|
|
66
|
-
dependencyProxyImagePrefix:
|
|
71
|
+
dependencyProxyImagePrefix: `${mockHost}/my-group/dependency_proxy/containers`,
|
|
67
72
|
dependencyProxyImageTtlPolicy: { enabled: true, ttl: 90 },
|
|
68
73
|
},
|
|
69
74
|
},
|
|
@@ -92,10 +97,9 @@ describe("dependency proxy tools", () => {
|
|
|
92
97
|
mockServer.addMockHandler("delete", `/groups/${TEST_GROUP_PATH}/dependency_proxy/cache`, (_req, res) => {
|
|
93
98
|
res.status(202).send();
|
|
94
99
|
});
|
|
95
|
-
await mockServer.start();
|
|
96
100
|
baseEnv = {
|
|
97
101
|
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
98
|
-
GITLAB_API_URL:
|
|
102
|
+
GITLAB_API_URL: mockApiUrl,
|
|
99
103
|
GITLAB_TOOLSETS: "dependency_proxy",
|
|
100
104
|
};
|
|
101
105
|
});
|
|
@@ -135,7 +139,7 @@ describe("dependency proxy tools", () => {
|
|
|
135
139
|
env: {
|
|
136
140
|
...process.env,
|
|
137
141
|
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
138
|
-
GITLAB_API_URL:
|
|
142
|
+
GITLAB_API_URL: mockApiUrl,
|
|
139
143
|
// No GITLAB_TOOLSETS — default toolsets only
|
|
140
144
|
},
|
|
141
145
|
});
|
|
@@ -114,7 +114,7 @@ describe("deployment and environment tools", () => {
|
|
|
114
114
|
let mockGitLab;
|
|
115
115
|
let mockGitLabUrl;
|
|
116
116
|
before(async () => {
|
|
117
|
-
const mockPort = await findMockServerPort(
|
|
117
|
+
const mockPort = await findMockServerPort();
|
|
118
118
|
mockGitLab = new MockGitLabServer({
|
|
119
119
|
port: mockPort,
|
|
120
120
|
validTokens: [MOCK_TOKEN],
|
|
@@ -318,11 +318,25 @@ describe("deployment and environment tools", () => {
|
|
|
318
318
|
assert.strictEqual(result.name, "stage");
|
|
319
319
|
assert.strictEqual(result.state, "available");
|
|
320
320
|
});
|
|
321
|
-
test("get_merge_request
|
|
321
|
+
test("get_merge_request omits summaries by default", async () => {
|
|
322
322
|
const result = await callTool("get_merge_request", { project_id: TEST_PROJECT_ID, merge_request_iid: TEST_MERGE_REQUEST_IID }, {
|
|
323
323
|
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
324
324
|
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
325
325
|
});
|
|
326
|
+
assert.strictEqual(result.iid, TEST_MERGE_REQUEST_IID);
|
|
327
|
+
assert.strictEqual(result.deployment_summary, undefined, "deployment_summary should be omitted by default");
|
|
328
|
+
assert.strictEqual(result.commit_addition_summary, undefined, "commit_addition_summary should be omitted by default");
|
|
329
|
+
assert.strictEqual(result.approval_summary, undefined, "approval_summary should be omitted by default");
|
|
330
|
+
});
|
|
331
|
+
test("get_merge_request returns compact deployment summary sorted by created_at desc", async () => {
|
|
332
|
+
const result = await callTool("get_merge_request", {
|
|
333
|
+
project_id: TEST_PROJECT_ID,
|
|
334
|
+
merge_request_iid: TEST_MERGE_REQUEST_IID,
|
|
335
|
+
include_summaries: true,
|
|
336
|
+
}, {
|
|
337
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
338
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
339
|
+
});
|
|
326
340
|
assert.ok(result.deployment_summary, "deployment_summary should be present");
|
|
327
341
|
assert.strictEqual(result.diverged_commits_count, TEST_DIVERGED_COMMITS_COUNT, "diverged_commits_count should be requested and returned by default");
|
|
328
342
|
assert.strictEqual(result.deployment_summary.lookup_sha, TEST_MERGE_REQUEST_SHA);
|
|
@@ -60,7 +60,7 @@ describe('download_attachment', () => {
|
|
|
60
60
|
let mockGitLab;
|
|
61
61
|
let env;
|
|
62
62
|
before(async () => {
|
|
63
|
-
const port = await findMockServerPort(
|
|
63
|
+
const port = await findMockServerPort();
|
|
64
64
|
mockGitLab = new MockGitLabServer({ port, validTokens: [MOCK_TOKEN] });
|
|
65
65
|
await mockGitLab.start();
|
|
66
66
|
env = {
|
|
@@ -72,7 +72,7 @@ describe("get_file_blame", () => {
|
|
|
72
72
|
let mockGitLabUrl;
|
|
73
73
|
let lastQuery = {};
|
|
74
74
|
before(async () => {
|
|
75
|
-
const mockPort = await findMockServerPort(
|
|
75
|
+
const mockPort = await findMockServerPort();
|
|
76
76
|
mockGitLab = new MockGitLabServer({
|
|
77
77
|
port: mockPort,
|
|
78
78
|
validTokens: [MOCK_TOKEN],
|