@zereight/mcp-gitlab 2.0.33 → 2.0.34
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.md +12 -1
- package/build/gitlab-client-pool.js +108 -6
- package/build/index.js +110 -59
- package/build/oauth.js +11 -6
- package/build/schemas.js +3 -0
- package/build/test/no-proxy-integration-test.js +183 -0
- package/build/test/no-proxy-test.js +138 -0
- package/build/test/remote-auth-simple-test.js +12 -1
- package/build/test/test-geteffectiveprojectid.js +236 -0
- package/build/test/test-upload-markdown.js +148 -0
- package/build/test/utils/mock-gitlab-server.js +5 -1
- package/package.json +1 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NO_PROXY Integration Test
|
|
3
|
+
* Tests NO_PROXY functionality with mock servers
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test, after, before } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import { launchServer, findAvailablePort, cleanupServers, TransportMode, HOST } from './utils/server-launcher.js';
|
|
8
|
+
import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server.js';
|
|
9
|
+
import { CustomHeaderClient } from './clients/custom-header-client.js';
|
|
10
|
+
// Test constants
|
|
11
|
+
const MOCK_TOKEN = 'glpat-mock-token-12345';
|
|
12
|
+
// Port ranges
|
|
13
|
+
const MOCK_GITLAB_PORT_BASE = 9600;
|
|
14
|
+
const MCP_SERVER_PORT_BASE = 3600;
|
|
15
|
+
console.log('🌐 NO_PROXY Integration Test Suite');
|
|
16
|
+
console.log('');
|
|
17
|
+
describe('NO_PROXY Integration Tests', () => {
|
|
18
|
+
let mcpUrl;
|
|
19
|
+
let mockGitLab;
|
|
20
|
+
let servers = [];
|
|
21
|
+
let mockGitLabUrl;
|
|
22
|
+
let mockGitLabHost;
|
|
23
|
+
before(async () => {
|
|
24
|
+
// Start mock GitLab server
|
|
25
|
+
const mockPort = await findMockServerPort(MOCK_GITLAB_PORT_BASE);
|
|
26
|
+
mockGitLab = new MockGitLabServer({
|
|
27
|
+
port: mockPort,
|
|
28
|
+
validTokens: [MOCK_TOKEN]
|
|
29
|
+
});
|
|
30
|
+
await mockGitLab.start();
|
|
31
|
+
mockGitLabUrl = mockGitLab.getUrl();
|
|
32
|
+
// Extract the host:port part for NO_PROXY
|
|
33
|
+
const url = new URL(mockGitLabUrl);
|
|
34
|
+
mockGitLabHost = url.host; // This includes port if non-standard
|
|
35
|
+
console.log(`Mock GitLab: ${mockGitLabUrl}`);
|
|
36
|
+
console.log(`Mock GitLab Host: ${mockGitLabHost}`);
|
|
37
|
+
});
|
|
38
|
+
after(async () => {
|
|
39
|
+
cleanupServers(servers);
|
|
40
|
+
if (mockGitLab) {
|
|
41
|
+
await mockGitLab.stop();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
test('should bypass proxy when hostname is in NO_PROXY', async () => {
|
|
45
|
+
// Start MCP server with proxy settings and NO_PROXY
|
|
46
|
+
const mcpPort = await findAvailablePort(MCP_SERVER_PORT_BASE);
|
|
47
|
+
const server = await launchServer({
|
|
48
|
+
mode: TransportMode.STREAMABLE_HTTP,
|
|
49
|
+
port: mcpPort,
|
|
50
|
+
timeout: 5000,
|
|
51
|
+
env: {
|
|
52
|
+
STREAMABLE_HTTP: 'true',
|
|
53
|
+
REMOTE_AUTHORIZATION: 'true',
|
|
54
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
55
|
+
// Set a fake proxy that would fail if used
|
|
56
|
+
HTTP_PROXY: 'http://nonexistent-proxy.example.com:9999',
|
|
57
|
+
HTTPS_PROXY: 'http://nonexistent-proxy.example.com:9999',
|
|
58
|
+
// Bypass proxy for our mock GitLab server
|
|
59
|
+
NO_PROXY: mockGitLabHost,
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
servers.push(server);
|
|
63
|
+
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
64
|
+
console.log(`MCP Server: ${mcpUrl}`);
|
|
65
|
+
console.log(`NO_PROXY: ${mockGitLabHost}`);
|
|
66
|
+
// Create client and make a request
|
|
67
|
+
const client = new CustomHeaderClient({
|
|
68
|
+
'authorization': `Bearer ${MOCK_TOKEN}`,
|
|
69
|
+
});
|
|
70
|
+
await client.connect(mcpUrl);
|
|
71
|
+
// This should succeed because the proxy is bypassed
|
|
72
|
+
const result = await client.callTool('list_projects', { per_page: 1 });
|
|
73
|
+
console.log(' ✓ Request succeeded with NO_PROXY bypass');
|
|
74
|
+
assert.ok(result, 'Request should succeed');
|
|
75
|
+
await client.disconnect();
|
|
76
|
+
});
|
|
77
|
+
test('should use proxy when hostname is NOT in NO_PROXY', async () => {
|
|
78
|
+
// Start MCP server with proxy settings but NO_PROXY doesn't match
|
|
79
|
+
const mcpPort = await findAvailablePort(MCP_SERVER_PORT_BASE + 1);
|
|
80
|
+
const server = await launchServer({
|
|
81
|
+
mode: TransportMode.STREAMABLE_HTTP,
|
|
82
|
+
port: mcpPort,
|
|
83
|
+
timeout: 5000,
|
|
84
|
+
env: {
|
|
85
|
+
STREAMABLE_HTTP: 'true',
|
|
86
|
+
REMOTE_AUTHORIZATION: 'true',
|
|
87
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
88
|
+
// Set a fake proxy that would fail if used
|
|
89
|
+
HTTP_PROXY: 'http://nonexistent-proxy.example.com:9999',
|
|
90
|
+
HTTPS_PROXY: 'http://nonexistent-proxy.example.com:9999',
|
|
91
|
+
// NO_PROXY doesn't match our server
|
|
92
|
+
NO_PROXY: 'different-host.example.com,10.0.0.1',
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
servers.push(server);
|
|
96
|
+
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
97
|
+
console.log(`MCP Server: ${mcpUrl}`);
|
|
98
|
+
console.log(`NO_PROXY: different-host.example.com,10.0.0.1 (should NOT match)`);
|
|
99
|
+
// Create client and make a request
|
|
100
|
+
const client = new CustomHeaderClient({
|
|
101
|
+
'authorization': `Bearer ${MOCK_TOKEN}`,
|
|
102
|
+
});
|
|
103
|
+
await client.connect(mcpUrl);
|
|
104
|
+
// This should fail because it tries to use the nonexistent proxy
|
|
105
|
+
try {
|
|
106
|
+
await client.callTool('list_projects', { per_page: 1 });
|
|
107
|
+
assert.fail('Request should have failed due to proxy connection error');
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
console.log(' ✓ Request failed as expected (proxy error)');
|
|
111
|
+
// Expected to fail with connection/proxy error
|
|
112
|
+
assert.ok(error, 'Should throw an error when proxy fails');
|
|
113
|
+
}
|
|
114
|
+
await client.disconnect();
|
|
115
|
+
});
|
|
116
|
+
test('should bypass proxy with wildcard NO_PROXY', async () => {
|
|
117
|
+
// Start MCP server with wildcard NO_PROXY
|
|
118
|
+
const mcpPort = await findAvailablePort(MCP_SERVER_PORT_BASE + 2);
|
|
119
|
+
const server = await launchServer({
|
|
120
|
+
mode: TransportMode.STREAMABLE_HTTP,
|
|
121
|
+
port: mcpPort,
|
|
122
|
+
timeout: 5000,
|
|
123
|
+
env: {
|
|
124
|
+
STREAMABLE_HTTP: 'true',
|
|
125
|
+
REMOTE_AUTHORIZATION: 'true',
|
|
126
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
127
|
+
// Set a fake proxy that would fail if used
|
|
128
|
+
HTTP_PROXY: 'http://nonexistent-proxy.example.com:9999',
|
|
129
|
+
HTTPS_PROXY: 'http://nonexistent-proxy.example.com:9999',
|
|
130
|
+
// Wildcard bypasses all proxies
|
|
131
|
+
NO_PROXY: '*',
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
servers.push(server);
|
|
135
|
+
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
136
|
+
console.log(`MCP Server: ${mcpUrl}`);
|
|
137
|
+
console.log(`NO_PROXY: * (wildcard - bypasses all)`);
|
|
138
|
+
// Create client and make a request
|
|
139
|
+
const client = new CustomHeaderClient({
|
|
140
|
+
'authorization': `Bearer ${MOCK_TOKEN}`,
|
|
141
|
+
});
|
|
142
|
+
await client.connect(mcpUrl);
|
|
143
|
+
// This should succeed because wildcard bypasses all proxies
|
|
144
|
+
const result = await client.callTool('list_projects', { per_page: 1 });
|
|
145
|
+
console.log(' ✓ Request succeeded with wildcard NO_PROXY');
|
|
146
|
+
assert.ok(result, 'Request should succeed');
|
|
147
|
+
await client.disconnect();
|
|
148
|
+
});
|
|
149
|
+
test('should bypass proxy with exact IP and localhost', async () => {
|
|
150
|
+
// Start MCP server with explicit IP and localhost patterns
|
|
151
|
+
const mcpPort = await findAvailablePort(MCP_SERVER_PORT_BASE + 3);
|
|
152
|
+
const server = await launchServer({
|
|
153
|
+
mode: TransportMode.STREAMABLE_HTTP,
|
|
154
|
+
port: mcpPort,
|
|
155
|
+
timeout: 5000,
|
|
156
|
+
env: {
|
|
157
|
+
STREAMABLE_HTTP: 'true',
|
|
158
|
+
REMOTE_AUTHORIZATION: 'true',
|
|
159
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
160
|
+
// Set a fake proxy that would fail if used
|
|
161
|
+
HTTP_PROXY: 'http://nonexistent-proxy.example.com:9999',
|
|
162
|
+
HTTPS_PROXY: 'http://nonexistent-proxy.example.com:9999',
|
|
163
|
+
// Use explicit localhost and 127.0.0.1 patterns
|
|
164
|
+
NO_PROXY: 'localhost,127.0.0.1',
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
servers.push(server);
|
|
168
|
+
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
169
|
+
console.log(`MCP Server: ${mcpUrl}`);
|
|
170
|
+
console.log(`NO_PROXY: localhost,127.0.0.1 (exact matches)`);
|
|
171
|
+
// Create client and make a request
|
|
172
|
+
const client = new CustomHeaderClient({
|
|
173
|
+
'authorization': `Bearer ${MOCK_TOKEN}`,
|
|
174
|
+
});
|
|
175
|
+
await client.connect(mcpUrl);
|
|
176
|
+
// This should succeed because 127.0.0.1 and localhost are explicitly in NO_PROXY
|
|
177
|
+
const result = await client.callTool('list_projects', { per_page: 1 });
|
|
178
|
+
console.log(' ✓ Request succeeded with exact IP/localhost NO_PROXY match');
|
|
179
|
+
assert.ok(result, 'Request should succeed');
|
|
180
|
+
await client.disconnect();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
console.log('✅ NO_PROXY integration tests completed');
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NO_PROXY Test Suite
|
|
3
|
+
* Tests NO_PROXY pattern matching and proxy bypass functionality
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import { GitLabClientPool } from '../gitlab-client-pool.js';
|
|
8
|
+
console.log('🚫 NO_PROXY Test Suite');
|
|
9
|
+
console.log('');
|
|
10
|
+
describe('NO_PROXY Pattern Matching', () => {
|
|
11
|
+
test('should bypass proxy for exact hostname match', () => {
|
|
12
|
+
const pool = new GitLabClientPool({
|
|
13
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
14
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
15
|
+
noProxy: 'gitlab.internal.com',
|
|
16
|
+
});
|
|
17
|
+
// Create agent for the NO_PROXY matched host
|
|
18
|
+
const agent = pool.getOrCreateAgentForUrl('https://gitlab.internal.com/api/v4');
|
|
19
|
+
// The agent should NOT be a proxy agent
|
|
20
|
+
// It should be a standard HTTPS agent
|
|
21
|
+
assert.ok(agent, 'Agent should be created');
|
|
22
|
+
assert.strictEqual(agent.constructor.name, 'Agent', 'Should be a standard Agent, not a proxy agent');
|
|
23
|
+
});
|
|
24
|
+
test('should use proxy for non-matching hostname', () => {
|
|
25
|
+
const pool = new GitLabClientPool({
|
|
26
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
27
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
28
|
+
noProxy: 'gitlab.internal.com',
|
|
29
|
+
});
|
|
30
|
+
// Create agent for a host that should use proxy
|
|
31
|
+
const agent = pool.getOrCreateAgentForUrl('https://gitlab.external.com/api/v4');
|
|
32
|
+
// The agent should be a proxy agent
|
|
33
|
+
assert.ok(agent, 'Agent should be created');
|
|
34
|
+
assert.notStrictEqual(agent.constructor.name, 'Agent', 'Should be a proxy agent, not standard Agent');
|
|
35
|
+
});
|
|
36
|
+
test('should bypass proxy for domain suffix match', () => {
|
|
37
|
+
const pool = new GitLabClientPool({
|
|
38
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
39
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
40
|
+
noProxy: '.internal.com',
|
|
41
|
+
});
|
|
42
|
+
// Test multiple subdomains
|
|
43
|
+
const agent1 = pool.getOrCreateAgentForUrl('https://gitlab.internal.com/api/v4');
|
|
44
|
+
const agent2 = pool.getOrCreateAgentForUrl('https://api.internal.com/api/v4');
|
|
45
|
+
const agent3 = pool.getOrCreateAgentForUrl('https://dev.gitlab.internal.com/api/v4');
|
|
46
|
+
assert.strictEqual(agent1.constructor.name, 'Agent', 'gitlab.internal.com should bypass proxy');
|
|
47
|
+
assert.strictEqual(agent2.constructor.name, 'Agent', 'api.internal.com should bypass proxy');
|
|
48
|
+
assert.strictEqual(agent3.constructor.name, 'Agent', 'dev.gitlab.internal.com should bypass proxy');
|
|
49
|
+
});
|
|
50
|
+
test('should bypass proxy for localhost', () => {
|
|
51
|
+
const pool = new GitLabClientPool({
|
|
52
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
53
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
54
|
+
noProxy: 'localhost,127.0.0.1',
|
|
55
|
+
});
|
|
56
|
+
const agent1 = pool.getOrCreateAgentForUrl('http://localhost:8080/api/v4');
|
|
57
|
+
const agent2 = pool.getOrCreateAgentForUrl('http://127.0.0.1:8080/api/v4');
|
|
58
|
+
assert.strictEqual(agent1.constructor.name, 'Agent', 'localhost should bypass proxy');
|
|
59
|
+
assert.strictEqual(agent2.constructor.name, 'Agent', '127.0.0.1 should bypass proxy');
|
|
60
|
+
});
|
|
61
|
+
test('should bypass proxy for wildcard pattern', () => {
|
|
62
|
+
const pool = new GitLabClientPool({
|
|
63
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
64
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
65
|
+
noProxy: '*',
|
|
66
|
+
});
|
|
67
|
+
const agent = pool.getOrCreateAgentForUrl('https://gitlab.com/api/v4');
|
|
68
|
+
assert.strictEqual(agent.constructor.name, 'Agent', 'Wildcard should bypass all proxies');
|
|
69
|
+
});
|
|
70
|
+
test('should handle multiple NO_PROXY patterns', () => {
|
|
71
|
+
const pool = new GitLabClientPool({
|
|
72
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
73
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
74
|
+
noProxy: 'localhost,.internal.com,192.168.1.1',
|
|
75
|
+
});
|
|
76
|
+
const agent1 = pool.getOrCreateAgentForUrl('http://localhost/api/v4');
|
|
77
|
+
const agent2 = pool.getOrCreateAgentForUrl('https://gitlab.internal.com/api/v4');
|
|
78
|
+
const agent3 = pool.getOrCreateAgentForUrl('http://192.168.1.1/api/v4');
|
|
79
|
+
const agent4 = pool.getOrCreateAgentForUrl('https://gitlab.com/api/v4');
|
|
80
|
+
assert.strictEqual(agent1.constructor.name, 'Agent', 'localhost should bypass proxy');
|
|
81
|
+
assert.strictEqual(agent2.constructor.name, 'Agent', '.internal.com should bypass proxy');
|
|
82
|
+
assert.strictEqual(agent3.constructor.name, 'Agent', '192.168.1.1 should bypass proxy');
|
|
83
|
+
assert.notStrictEqual(agent4.constructor.name, 'Agent', 'gitlab.com should use proxy');
|
|
84
|
+
});
|
|
85
|
+
test('should handle NO_PROXY with whitespace', () => {
|
|
86
|
+
const pool = new GitLabClientPool({
|
|
87
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
88
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
89
|
+
noProxy: ' localhost , .internal.com , 192.168.1.1 ',
|
|
90
|
+
});
|
|
91
|
+
const agent1 = pool.getOrCreateAgentForUrl('http://localhost/api/v4');
|
|
92
|
+
const agent2 = pool.getOrCreateAgentForUrl('https://gitlab.internal.com/api/v4');
|
|
93
|
+
assert.strictEqual(agent1.constructor.name, 'Agent', 'Should handle whitespace in NO_PROXY');
|
|
94
|
+
assert.strictEqual(agent2.constructor.name, 'Agent', 'Should handle whitespace in NO_PROXY');
|
|
95
|
+
});
|
|
96
|
+
test('should work without NO_PROXY set', () => {
|
|
97
|
+
const pool = new GitLabClientPool({
|
|
98
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
99
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
100
|
+
});
|
|
101
|
+
const agent = pool.getOrCreateAgentForUrl('https://gitlab.com/api/v4');
|
|
102
|
+
// Should use proxy when NO_PROXY is not set
|
|
103
|
+
assert.notStrictEqual(agent.constructor.name, 'Agent', 'Should use proxy when NO_PROXY is not set');
|
|
104
|
+
});
|
|
105
|
+
test('should work with NO_PROXY set but empty', () => {
|
|
106
|
+
const pool = new GitLabClientPool({
|
|
107
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
108
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
109
|
+
noProxy: '',
|
|
110
|
+
});
|
|
111
|
+
const agent = pool.getOrCreateAgentForUrl('https://gitlab.com/api/v4');
|
|
112
|
+
// Should use proxy when NO_PROXY is empty
|
|
113
|
+
assert.notStrictEqual(agent.constructor.name, 'Agent', 'Should use proxy when NO_PROXY is empty');
|
|
114
|
+
});
|
|
115
|
+
test('should handle port-specific patterns', () => {
|
|
116
|
+
const pool = new GitLabClientPool({
|
|
117
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
118
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
119
|
+
noProxy: 'gitlab.internal.com:443',
|
|
120
|
+
});
|
|
121
|
+
const agent1 = pool.getOrCreateAgentForUrl('https://gitlab.internal.com/api/v4'); // HTTPS uses port 443 by default
|
|
122
|
+
const agent2 = pool.getOrCreateAgentForUrl('http://gitlab.internal.com/api/v4'); // HTTP uses port 80 by default
|
|
123
|
+
assert.strictEqual(agent1.constructor.name, 'Agent', 'Should bypass proxy for matching port');
|
|
124
|
+
assert.notStrictEqual(agent2.constructor.name, 'Agent', 'Should use proxy for non-matching port');
|
|
125
|
+
});
|
|
126
|
+
test('should handle case-insensitive matching', () => {
|
|
127
|
+
const pool = new GitLabClientPool({
|
|
128
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
129
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
130
|
+
noProxy: 'GitLab.Internal.COM',
|
|
131
|
+
});
|
|
132
|
+
const agent1 = pool.getOrCreateAgentForUrl('https://gitlab.internal.com/api/v4');
|
|
133
|
+
const agent2 = pool.getOrCreateAgentForUrl('https://GITLAB.INTERNAL.COM/api/v4');
|
|
134
|
+
assert.strictEqual(agent1.constructor.name, 'Agent', 'Should match case-insensitively (lowercase)');
|
|
135
|
+
assert.strictEqual(agent2.constructor.name, 'Agent', 'Should match case-insensitively (uppercase)');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
console.log('✅ NO_PROXY tests completed');
|
|
@@ -9,6 +9,7 @@ import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server
|
|
|
9
9
|
import { CustomHeaderClient } from './clients/custom-header-client.js';
|
|
10
10
|
// Test constants
|
|
11
11
|
const MOCK_TOKEN = 'glpat-mock-token-12345';
|
|
12
|
+
const MOCK_JOB_TOKEN = 'glcbt-mock-job-token-9876';
|
|
12
13
|
const TEST_PROJECT_ID = '123';
|
|
13
14
|
// Port ranges to avoid collisions
|
|
14
15
|
const MOCK_GITLAB_PORT_BASE = 9000;
|
|
@@ -32,7 +33,7 @@ describe('Remote Authorization - Basic Functionality', () => {
|
|
|
32
33
|
const mockPort = await findMockServerPort(MOCK_GITLAB_PORT_BASE);
|
|
33
34
|
mockGitLab = new MockGitLabServer({
|
|
34
35
|
port: mockPort,
|
|
35
|
-
validTokens: [MOCK_TOKEN]
|
|
36
|
+
validTokens: [MOCK_TOKEN, MOCK_JOB_TOKEN],
|
|
36
37
|
});
|
|
37
38
|
await mockGitLab.start();
|
|
38
39
|
const mockGitLabUrl = mockGitLab.getUrl();
|
|
@@ -80,6 +81,16 @@ describe('Remote Authorization - Basic Functionality', () => {
|
|
|
80
81
|
console.log(` ✓ Connected with Private-Token, got ${tools.tools.length} tools`);
|
|
81
82
|
await client.disconnect();
|
|
82
83
|
});
|
|
84
|
+
test('should connect with JOB-TOKEN header', async () => {
|
|
85
|
+
const client = new CustomHeaderClient({
|
|
86
|
+
'job-token': MOCK_JOB_TOKEN,
|
|
87
|
+
});
|
|
88
|
+
await client.connect(mcpUrl);
|
|
89
|
+
const tools = await client.listTools();
|
|
90
|
+
assert.ok(tools.tools.length > 0, 'Should have tools');
|
|
91
|
+
console.log(` ✓ Connected with JOB-TOKEN, got ${tools.tools.length} tools`);
|
|
92
|
+
await client.disconnect();
|
|
93
|
+
});
|
|
83
94
|
test('should successfully call listTools with auth', async () => {
|
|
84
95
|
const client = new CustomHeaderClient({
|
|
85
96
|
'authorization': `Bearer ${MOCK_TOKEN}`
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test suite for getEffectiveProjectId function
|
|
3
|
+
* Tests the behavior of project ID resolution with different environment configurations
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test, before, after } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import { launchServer, findAvailablePort, cleanupServers, TransportMode, HOST } from './utils/server-launcher.js';
|
|
8
|
+
import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server.js';
|
|
9
|
+
import { StreamableHTTPTestClient } from './clients/streamable-http-client.js';
|
|
10
|
+
// Use the same token that will be passed via GITLAB_TOKEN_TEST environment variable
|
|
11
|
+
const MOCK_TOKEN = process.env.GITLAB_TOKEN_TEST || 'glpat-mock-token-12345';
|
|
12
|
+
const DEFAULT_PROJECT_ID = '123';
|
|
13
|
+
const OTHER_PROJECT_ID = '456';
|
|
14
|
+
console.log('🔍 Testing getEffectiveProjectId functionality');
|
|
15
|
+
console.log('');
|
|
16
|
+
describe('getEffectiveProjectId - No GITLAB_ALLOWED_PROJECT_IDS', () => {
|
|
17
|
+
let mcpUrl;
|
|
18
|
+
let mockGitLab;
|
|
19
|
+
let servers = [];
|
|
20
|
+
let client;
|
|
21
|
+
before(async () => {
|
|
22
|
+
// Start mock GitLab server
|
|
23
|
+
const mockPort = await findMockServerPort(9100);
|
|
24
|
+
mockGitLab = new MockGitLabServer({
|
|
25
|
+
port: mockPort,
|
|
26
|
+
validTokens: [MOCK_TOKEN]
|
|
27
|
+
});
|
|
28
|
+
await mockGitLab.start();
|
|
29
|
+
const mockGitLabUrl = mockGitLab.getUrl();
|
|
30
|
+
// Start MCP server WITHOUT GITLAB_ALLOWED_PROJECT_IDS
|
|
31
|
+
const mcpPort = await findAvailablePort(3100);
|
|
32
|
+
const server = await launchServer({
|
|
33
|
+
mode: TransportMode.STREAMABLE_HTTP,
|
|
34
|
+
port: mcpPort,
|
|
35
|
+
timeout: 5000,
|
|
36
|
+
env: {
|
|
37
|
+
STREAMABLE_HTTP: 'true',
|
|
38
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
39
|
+
GITLAB_PROJECT_ID: DEFAULT_PROJECT_ID,
|
|
40
|
+
GITLAB_READ_ONLY_MODE: 'true',
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
servers.push(server);
|
|
44
|
+
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
45
|
+
client = new StreamableHTTPTestClient();
|
|
46
|
+
await client.connect(mcpUrl);
|
|
47
|
+
console.log(`Mock GitLab: ${mockGitLabUrl}`);
|
|
48
|
+
console.log(`MCP Server: ${mcpUrl}`);
|
|
49
|
+
console.log(`Default Project: ${DEFAULT_PROJECT_ID}`);
|
|
50
|
+
});
|
|
51
|
+
after(async () => {
|
|
52
|
+
if (client) {
|
|
53
|
+
await client.disconnect();
|
|
54
|
+
}
|
|
55
|
+
cleanupServers(servers);
|
|
56
|
+
if (mockGitLab) {
|
|
57
|
+
await mockGitLab.stop();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
test('should use GITLAB_PROJECT_ID when no project_id is provided', async () => {
|
|
61
|
+
// Call get_project without specifying project_id
|
|
62
|
+
const result = await client.callTool('get_project', {
|
|
63
|
+
project_id: ''
|
|
64
|
+
});
|
|
65
|
+
assert.ok(result.content, 'Should have content');
|
|
66
|
+
const content = result.content[0];
|
|
67
|
+
assert.ok('text' in content, 'Content should have text');
|
|
68
|
+
const project = JSON.parse(content.text);
|
|
69
|
+
// The mock server should receive a request for the default project
|
|
70
|
+
assert.strictEqual(project.id.toString(), DEFAULT_PROJECT_ID, 'Should use GITLAB_PROJECT_ID as default');
|
|
71
|
+
console.log(` ✓ Used default project ${DEFAULT_PROJECT_ID} when no project_id provided`);
|
|
72
|
+
});
|
|
73
|
+
test('should prioritize passed project_id over GITLAB_PROJECT_ID', async () => {
|
|
74
|
+
// Call get_project with a different project_id
|
|
75
|
+
const result = await client.callTool('get_project', {
|
|
76
|
+
project_id: OTHER_PROJECT_ID
|
|
77
|
+
});
|
|
78
|
+
assert.ok(result.content, 'Should have content');
|
|
79
|
+
const content = result.content[0];
|
|
80
|
+
assert.ok('text' in content, 'Content should have text');
|
|
81
|
+
const project = JSON.parse(content.text);
|
|
82
|
+
// Should use the passed project_id, not GITLAB_PROJECT_ID
|
|
83
|
+
assert.strictEqual(project.id.toString(), OTHER_PROJECT_ID, 'Should use passed project_id');
|
|
84
|
+
console.log(` ✓ Used passed project_id ${OTHER_PROJECT_ID} instead of default ${DEFAULT_PROJECT_ID}`);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe('getEffectiveProjectId - With single GITLAB_ALLOWED_PROJECT_IDS', () => {
|
|
88
|
+
let mcpUrl;
|
|
89
|
+
let mockGitLab;
|
|
90
|
+
let servers = [];
|
|
91
|
+
let client;
|
|
92
|
+
before(async () => {
|
|
93
|
+
// Start mock GitLab server
|
|
94
|
+
const mockPort = await findMockServerPort(9200);
|
|
95
|
+
mockGitLab = new MockGitLabServer({
|
|
96
|
+
port: mockPort,
|
|
97
|
+
validTokens: [MOCK_TOKEN]
|
|
98
|
+
});
|
|
99
|
+
await mockGitLab.start();
|
|
100
|
+
const mockGitLabUrl = mockGitLab.getUrl();
|
|
101
|
+
// Start MCP server WITH single GITLAB_ALLOWED_PROJECT_IDS
|
|
102
|
+
const mcpPort = await findAvailablePort(3200);
|
|
103
|
+
const server = await launchServer({
|
|
104
|
+
mode: TransportMode.STREAMABLE_HTTP,
|
|
105
|
+
port: mcpPort,
|
|
106
|
+
timeout: 5000,
|
|
107
|
+
env: {
|
|
108
|
+
STREAMABLE_HTTP: 'true',
|
|
109
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
110
|
+
GITLAB_PROJECT_ID: DEFAULT_PROJECT_ID,
|
|
111
|
+
GITLAB_ALLOWED_PROJECT_IDS: DEFAULT_PROJECT_ID,
|
|
112
|
+
GITLAB_READ_ONLY_MODE: 'true',
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
servers.push(server);
|
|
116
|
+
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
117
|
+
client = new StreamableHTTPTestClient();
|
|
118
|
+
await client.connect(mcpUrl);
|
|
119
|
+
console.log(`Mock GitLab: ${mockGitLabUrl}`);
|
|
120
|
+
console.log(`MCP Server: ${mcpUrl}`);
|
|
121
|
+
console.log(`Allowed Project: ${DEFAULT_PROJECT_ID}`);
|
|
122
|
+
});
|
|
123
|
+
after(async () => {
|
|
124
|
+
if (client) {
|
|
125
|
+
await client.disconnect();
|
|
126
|
+
}
|
|
127
|
+
cleanupServers(servers);
|
|
128
|
+
if (mockGitLab) {
|
|
129
|
+
await mockGitLab.stop();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
test('should use single allowed project as default', async () => {
|
|
133
|
+
const result = await client.callTool('get_project', {
|
|
134
|
+
project_id: ''
|
|
135
|
+
});
|
|
136
|
+
assert.ok(result.content, 'Should have content');
|
|
137
|
+
const content = result.content[0];
|
|
138
|
+
assert.ok('text' in content, 'Content should have text');
|
|
139
|
+
const project = JSON.parse(content.text);
|
|
140
|
+
assert.strictEqual(project.id.toString(), DEFAULT_PROJECT_ID, 'Should use allowed project as default');
|
|
141
|
+
console.log(` ✓ Used allowed project ${DEFAULT_PROJECT_ID} as default`);
|
|
142
|
+
});
|
|
143
|
+
test('should reject access to non-allowed project', async () => {
|
|
144
|
+
try {
|
|
145
|
+
await client.callTool('get_project', {
|
|
146
|
+
project_id: OTHER_PROJECT_ID
|
|
147
|
+
});
|
|
148
|
+
assert.fail('Should have rejected access to non-allowed project');
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
assert.ok(error instanceof Error);
|
|
152
|
+
assert.ok(error.message.includes('Access denied'), 'Should indicate access denied');
|
|
153
|
+
console.log(' ✓ Correctly rejected access to non-allowed project');
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('getEffectiveProjectId - With multiple GITLAB_ALLOWED_PROJECT_IDS', () => {
|
|
158
|
+
let mcpUrl;
|
|
159
|
+
let mockGitLab;
|
|
160
|
+
let servers = [];
|
|
161
|
+
let client;
|
|
162
|
+
before(async () => {
|
|
163
|
+
// Start mock GitLab server
|
|
164
|
+
const mockPort = await findMockServerPort(9300);
|
|
165
|
+
mockGitLab = new MockGitLabServer({
|
|
166
|
+
port: mockPort,
|
|
167
|
+
validTokens: [MOCK_TOKEN]
|
|
168
|
+
});
|
|
169
|
+
await mockGitLab.start();
|
|
170
|
+
const mockGitLabUrl = mockGitLab.getUrl();
|
|
171
|
+
// Start MCP server WITH multiple GITLAB_ALLOWED_PROJECT_IDS
|
|
172
|
+
const mcpPort = await findAvailablePort(3300);
|
|
173
|
+
const server = await launchServer({
|
|
174
|
+
mode: TransportMode.STREAMABLE_HTTP,
|
|
175
|
+
port: mcpPort,
|
|
176
|
+
timeout: 5000,
|
|
177
|
+
env: {
|
|
178
|
+
STREAMABLE_HTTP: 'true',
|
|
179
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
180
|
+
GITLAB_ALLOWED_PROJECT_IDS: `${DEFAULT_PROJECT_ID},${OTHER_PROJECT_ID}`,
|
|
181
|
+
GITLAB_READ_ONLY_MODE: 'true',
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
servers.push(server);
|
|
185
|
+
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
186
|
+
client = new StreamableHTTPTestClient();
|
|
187
|
+
await client.connect(mcpUrl);
|
|
188
|
+
console.log(`Mock GitLab: ${mockGitLabUrl}`);
|
|
189
|
+
console.log(`MCP Server: ${mcpUrl}`);
|
|
190
|
+
console.log(`Allowed Projects: ${DEFAULT_PROJECT_ID},${OTHER_PROJECT_ID}`);
|
|
191
|
+
});
|
|
192
|
+
after(async () => {
|
|
193
|
+
if (client) {
|
|
194
|
+
await client.disconnect();
|
|
195
|
+
}
|
|
196
|
+
cleanupServers(servers);
|
|
197
|
+
if (mockGitLab) {
|
|
198
|
+
await mockGitLab.stop();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
test('should require explicit project_id when multiple projects allowed', async () => {
|
|
202
|
+
try {
|
|
203
|
+
await client.callTool('get_project', {
|
|
204
|
+
project_id: ''
|
|
205
|
+
});
|
|
206
|
+
assert.fail('Should have required explicit project_id');
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
assert.ok(error instanceof Error);
|
|
210
|
+
assert.ok(error.message.includes('Please specify a project ID'), 'Should require project ID');
|
|
211
|
+
console.log(' ✓ Correctly required explicit project_id');
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
test('should allow access to first allowed project', async () => {
|
|
215
|
+
const result = await client.callTool('get_project', {
|
|
216
|
+
project_id: DEFAULT_PROJECT_ID
|
|
217
|
+
});
|
|
218
|
+
assert.ok(result.content, 'Should have content');
|
|
219
|
+
const content = result.content[0];
|
|
220
|
+
assert.ok('text' in content, 'Content should have text');
|
|
221
|
+
const project = JSON.parse(content.text);
|
|
222
|
+
assert.strictEqual(project.id.toString(), DEFAULT_PROJECT_ID, 'Should allow first project');
|
|
223
|
+
console.log(` ✓ Allowed access to first project ${DEFAULT_PROJECT_ID}`);
|
|
224
|
+
});
|
|
225
|
+
test('should allow access to second allowed project', async () => {
|
|
226
|
+
const result = await client.callTool('get_project', {
|
|
227
|
+
project_id: OTHER_PROJECT_ID
|
|
228
|
+
});
|
|
229
|
+
assert.ok(result.content, 'Should have content');
|
|
230
|
+
const content = result.content[0];
|
|
231
|
+
assert.ok('text' in content, 'Content should have text');
|
|
232
|
+
const project = JSON.parse(content.text);
|
|
233
|
+
assert.strictEqual(project.id.toString(), OTHER_PROJECT_ID, 'Should allow second project');
|
|
234
|
+
console.log(` ✓ Allowed access to second project ${OTHER_PROJECT_ID}`);
|
|
235
|
+
});
|
|
236
|
+
});
|