@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.
@@ -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
+ });