@zereight/mcp-gitlab 2.1.70 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.ko.md +9 -5
  2. package/README.md +15 -11
  3. package/README.pt-BR.md +9 -5
  4. package/README.zh-CN.md +9 -5
  5. package/build/config.js +12 -4
  6. package/build/deprecated-env.js +63 -0
  7. package/build/index.js +430 -200
  8. package/build/oauth-device-flow.js +80 -0
  9. package/build/oauth.js +135 -18
  10. package/build/schemas.js +1 -1
  11. package/build/server/remote-http-stack.js +39 -0
  12. package/build/tools/discover-activation.js +38 -0
  13. package/build/tools/exposed-tools.js +35 -0
  14. package/build/tools/listed-tools.js +76 -0
  15. package/build/tools/registry.js +232 -63
  16. package/build/tools/tool-descriptions.js +3 -3
  17. package/build/tools/tool-profile.js +66 -0
  18. package/build/utils/compact-tool-allowlist.js +14 -0
  19. package/build/utils/logger.js +34 -12
  20. package/package.json +3 -3
  21. package/build/scripts/check-skill-sync.js +0 -143
  22. package/build/scripts/generate-server-card.js +0 -39
  23. package/build/scripts/generate-tool-coverage.js +0 -58
  24. package/build/scripts/generate-tool-docs.js +0 -430
  25. package/build/scripts/tool-coverage/coverage.js +0 -341
  26. package/build/scripts/tool-coverage/coverage.test.js +0 -142
  27. package/build/test/callback-proxy-tests.js +0 -321
  28. package/build/test/cli/args.test.js +0 -175
  29. package/build/test/cli/boolean-flags.test.js +0 -106
  30. package/build/test/cli/compact-mcp-result.test.js +0 -93
  31. package/build/test/cli/credentials.test.js +0 -153
  32. package/build/test/cli/exposure.test.js +0 -104
  33. package/build/test/cli/load-config-snapshot.js +0 -5
  34. package/build/test/cli/output.test.js +0 -74
  35. package/build/test/cli/replay-command.test.js +0 -37
  36. package/build/test/cli/router.test.js +0 -183
  37. package/build/test/client-pool-test.js +0 -109
  38. package/build/test/clients/client.js +0 -34
  39. package/build/test/clients/custom-header-client.js +0 -122
  40. package/build/test/clients/sse-client.js +0 -95
  41. package/build/test/clients/stdio-client.js +0 -113
  42. package/build/test/clients/streamable-http-client.js +0 -95
  43. package/build/test/config-allowed-groups.test.js +0 -97
  44. package/build/test/create-commit-actions.test.js +0 -102
  45. package/build/test/draft-note-schema.test.js +0 -44
  46. package/build/test/dynamic-api-url-allowlist.test.js +0 -121
  47. package/build/test/dynamic-api-url-test.js +0 -304
  48. package/build/test/dynamic-routing-tests.js +0 -495
  49. package/build/test/get-merge-request-discussion-schema.test.js +0 -30
  50. package/build/test/gitlab-api-tool-errors.test.js +0 -112
  51. package/build/test/gitlab-artifact-entry-schema.test.js +0 -44
  52. package/build/test/gitlab-diff-schema.test.js +0 -36
  53. package/build/test/gitlab-pipeline-schedule-schema.test.js +0 -32
  54. package/build/test/gitlab-tag-schema.test.js +0 -51
  55. package/build/test/group-milestone-schema.test.js +0 -45
  56. package/build/test/mcp-oauth-tests.js +0 -905
  57. package/build/test/mcp-server-name.test.js +0 -87
  58. package/build/test/merge-merge-request-schema.test.js +0 -75
  59. package/build/test/multi-server-test.js +0 -190
  60. package/build/test/no-proxy-integration-test.js +0 -183
  61. package/build/test/no-proxy-test.js +0 -149
  62. package/build/test/nullable-gitlab-response-fields.test.js +0 -37
  63. package/build/test/nullish-tool-arguments-schema.test.js +0 -118
  64. package/build/test/oauth-device-flow-tests.js +0 -450
  65. package/build/test/oauth-download-url.test.js +0 -157
  66. package/build/test/oauth-startup.test.js +0 -204
  67. package/build/test/oauth-tests.js +0 -428
  68. package/build/test/path-segment-encoding.test.js +0 -98
  69. package/build/test/remote-auth-simple-test.js +0 -277
  70. package/build/test/remote-auth-tests.js +0 -314
  71. package/build/test/response-masking.test.js +0 -445
  72. package/build/test/schema-tests.js +0 -1679
  73. package/build/test/server/metrics.test.js +0 -47
  74. package/build/test/server/request-helpers.test.js +0 -48
  75. package/build/test/server/version.test.js +0 -13
  76. package/build/test/sse-auth-guard.test.js +0 -96
  77. package/build/test/sse-session-limits.test.js +0 -273
  78. package/build/test/stateless/callback-proxy.test.js +0 -510
  79. package/build/test/stateless/client-id.test.js +0 -177
  80. package/build/test/stateless/codec.test.js +0 -328
  81. package/build/test/stateless/config-ttl.test.js +0 -149
  82. package/build/test/stateless/consumed-proxy-code-cache.test.js +0 -150
  83. package/build/test/stateless/session-id-integration.test.js +0 -675
  84. package/build/test/stateless/session-id.test.js +0 -157
  85. package/build/test/streamable-http-concurrent-session.test.js +0 -171
  86. package/build/test/streamable-http-dns-rebinding.test.js +0 -185
  87. package/build/test/streamable-http-sse-stream.test.js +0 -350
  88. package/build/test/streamable-http-static-token-auth.test.js +0 -157
  89. package/build/test/streamable-http-unauthenticated-discovery.test.js +0 -207
  90. package/build/test/test-all-transport-server.js +0 -255
  91. package/build/test/test-auth-retry.js +0 -195
  92. package/build/test/test-blank-filters.js +0 -156
  93. package/build/test/test-ci-catalog.js +0 -177
  94. package/build/test/test-ci-lint.js +0 -191
  95. package/build/test/test-ci-variables.js +0 -309
  96. package/build/test/test-create-repository.js +0 -120
  97. package/build/test/test-dependency-proxy.js +0 -195
  98. package/build/test/test-deployment-tools.js +0 -463
  99. package/build/test/test-download-attachment.js +0 -190
  100. package/build/test/test-dynamic-project-scope.js +0 -524
  101. package/build/test/test-empty-toolsets.js +0 -535
  102. package/build/test/test-get-file-blame.js +0 -145
  103. package/build/test/test-get-file-contents.js +0 -128
  104. package/build/test/test-geteffectiveprojectid.js +0 -804
  105. package/build/test/test-health-check.js +0 -115
  106. package/build/test/test-issue-description-patch.js +0 -256
  107. package/build/test/test-job-artifacts.js +0 -275
  108. package/build/test/test-json-schema.js +0 -166
  109. package/build/test/test-list-group-members.js +0 -150
  110. package/build/test/test-list-group-merge-requests.js +0 -165
  111. package/build/test/test-list-issues.js +0 -170
  112. package/build/test/test-list-merge-requests.js +0 -201
  113. package/build/test/test-list-milestones.js +0 -145
  114. package/build/test/test-list-project-members.js +0 -132
  115. package/build/test/test-merge-request-approval-state-tools.js +0 -213
  116. package/build/test/test-merge-request-approvals.js +0 -187
  117. package/build/test/test-merge-request-pipelines.js +0 -106
  118. package/build/test/test-mr-diffs-filter.js +0 -132
  119. package/build/test/test-mr-file-diffs.js +0 -251
  120. package/build/test/test-oauth-proxy-rate-limit.js +0 -133
  121. package/build/test/test-orbit.js +0 -155
  122. package/build/test/test-permission-mode.js +0 -424
  123. package/build/test/test-pipeline-schedules.js +0 -484
  124. package/build/test/test-protected-branches.js +0 -174
  125. package/build/test/test-remote-downloads.js +0 -510
  126. package/build/test/test-search-code.js +0 -275
  127. package/build/test/test-snippets.js +0 -735
  128. package/build/test/test-tags.js +0 -220
  129. package/build/test/test-todos.js +0 -195
  130. package/build/test/test-token-optimizations.js +0 -691
  131. package/build/test/test-toolset-filtering.js +0 -499
  132. package/build/test/test-update-issue-slim.js +0 -141
  133. package/build/test/test-update-project.js +0 -112
  134. package/build/test/test-upload-markdown.js +0 -206
  135. package/build/test/test-vulnerabilities.js +0 -399
  136. package/build/test/test-webhooks.js +0 -218
  137. package/build/test/tool-description-quality.test.js +0 -50
  138. package/build/test/utils/bulk-publish-options.test.js +0 -32
  139. package/build/test/utils/download-token.test.js +0 -35
  140. package/build/test/utils/forwarded-public-base-url.test.js +0 -46
  141. package/build/test/utils/gitlab-instance-version.test.js +0 -32
  142. package/build/test/utils/gitlab-version-gate.test.js +0 -74
  143. package/build/test/utils/graphql-query.test.js +0 -168
  144. package/build/test/utils/helpers-path.test.js +0 -104
  145. package/build/test/utils/jmespath-tool-result.test.js +0 -91
  146. package/build/test/utils/masking.test.js +0 -384
  147. package/build/test/utils/merge-request-position.test.js +0 -57
  148. package/build/test/utils/mock-gitlab-server.js +0 -614
  149. package/build/test/utils/proxy-client-ip.test.js +0 -28
  150. package/build/test/utils/redact-sensitive.test.js +0 -32
  151. package/build/test/utils/safe-redirect-fetch.test.js +0 -463
  152. package/build/test/utils/server-launcher.js +0 -231
  153. package/build/test/utils/tool-args.test.js +0 -210
  154. package/build/test/utils/url.test.js +0 -141
  155. package/build/test/utils/version-check.test.js +0 -239
  156. package/build/test/utils/wiki-title.test.js +0 -21
  157. package/build/test-note.js +0 -54
  158. package/build/test-resolve-issue-note.js +0 -127
@@ -1,204 +0,0 @@
1
- /**
2
- * OAuth startup tests.
3
- *
4
- * - isAuthInvalidTokenResponse: pure classifier — refresh failures that mean
5
- * "re-login would fix it" vs transient/config errors that must not open a browser.
6
- * - ensureOAuthToken: lazy first-use delivery — the caller must receive the
7
- * token even when the on-disk token is still valid (regression: returning
8
- * early on hasValidToken() alone leaves requests unauthenticated).
9
- *
10
- * Temp token files + a loopback mock for /oauth/token only; no MCP server.
11
- * The missing-token and invalid_grant paths start the interactive browser
12
- * flow and are deliberately not covered here.
13
- */
14
- import { describe, test, afterEach } from "node:test";
15
- import assert from "node:assert";
16
- import * as fs from "node:fs";
17
- import * as os from "node:os";
18
- import * as path from "node:path";
19
- import * as http from "node:http";
20
- import { GitLabOAuth, ensureOAuthToken, isAuthInvalidTokenResponse } from "../oauth.js";
21
- const validTokenFile = {
22
- access_token: "cached-access-token",
23
- refresh_token: "cached-refresh-token",
24
- expires_in: 7200,
25
- created_at: Date.now(),
26
- token_type: "Bearer",
27
- };
28
- const expiredTokenFile = {
29
- ...validTokenFile,
30
- created_at: Date.now() - 10 * 3600 * 1000,
31
- };
32
- const tmpDirs = [];
33
- const servers = [];
34
- afterEach(async () => {
35
- for (const server of servers.splice(0)) {
36
- await new Promise(resolve => server.close(() => resolve()));
37
- }
38
- for (const dir of tmpDirs.splice(0)) {
39
- fs.rmSync(dir, { recursive: true, force: true });
40
- }
41
- });
42
- function makeTokenFile(data) {
43
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "oauth-startup-"));
44
- tmpDirs.push(dir);
45
- const file = path.join(dir, "token.json");
46
- fs.writeFileSync(file, JSON.stringify(data));
47
- return file;
48
- }
49
- function makeClient(tokenStoragePath, gitlabUrl, extra) {
50
- return new GitLabOAuth({
51
- clientId: "test-client",
52
- redirectUri: "http://127.0.0.1:8888/callback",
53
- gitlabUrl,
54
- scopes: ["api"],
55
- tokenStoragePath,
56
- ...extra,
57
- });
58
- }
59
- async function startTokenServer(handler) {
60
- const bodies = [];
61
- const server = http.createServer((req, res) => {
62
- let body = "";
63
- req.on("data", (chunk) => {
64
- body += chunk.toString();
65
- });
66
- req.on("end", () => {
67
- bodies.push(body);
68
- const { status, json } = handler();
69
- res.writeHead(status, { "Content-Type": "application/json" });
70
- res.end(JSON.stringify(json));
71
- });
72
- });
73
- servers.push(server);
74
- await new Promise(resolve => server.listen(0, "127.0.0.1", () => resolve()));
75
- const addr = server.address();
76
- assert(addr && typeof addr !== "string");
77
- return { url: `http://127.0.0.1:${addr.port}`, bodies };
78
- }
79
- describe("isAuthInvalidTokenResponse", () => {
80
- test("400 invalid_grant is auth-invalid", () => {
81
- assert.strictEqual(isAuthInvalidTokenResponse(400, '{"error":"invalid_grant"}'), true);
82
- });
83
- test("400 invalid_token is auth-invalid", () => {
84
- assert.strictEqual(isAuthInvalidTokenResponse(400, '{"error":"invalid_token"}'), true);
85
- });
86
- test("401 invalid_grant is auth-invalid", () => {
87
- assert.strictEqual(isAuthInvalidTokenResponse(401, '{"error":"invalid_grant"}'), true);
88
- });
89
- test("400 invalid_client is not auth-invalid (config error)", () => {
90
- assert.strictEqual(isAuthInvalidTokenResponse(400, '{"error":"invalid_client"}'), false);
91
- });
92
- test("400 invalid_request is not auth-invalid (config error)", () => {
93
- assert.strictEqual(isAuthInvalidTokenResponse(400, '{"error":"invalid_request"}'), false);
94
- });
95
- test("500 server_error is not auth-invalid (transient)", () => {
96
- assert.strictEqual(isAuthInvalidTokenResponse(500, '{"error":"server_error"}'), false);
97
- });
98
- test("429 is not auth-invalid (rate limited)", () => {
99
- assert.strictEqual(isAuthInvalidTokenResponse(429, '{"error":"invalid_grant"}'), false);
100
- });
101
- test("non-JSON body is not auth-invalid", () => {
102
- assert.strictEqual(isAuthInvalidTokenResponse(400, "gateway timeout"), false);
103
- });
104
- });
105
- describe("ensureOAuthToken", () => {
106
- test("null client is a no-op", async () => {
107
- const delivered = [];
108
- await ensureOAuthToken(null, null, t => {
109
- delivered.push(t);
110
- });
111
- await ensureOAuthToken(undefined, undefined, t => {
112
- delivered.push(t);
113
- });
114
- assert.deepStrictEqual(delivered, []);
115
- });
116
- test("first call with valid cached token delivers it without network", async () => {
117
- // Closed port: any fetch attempt fails, proving the cached path is network-free.
118
- const client = makeClient(makeTokenFile(validTokenFile), "http://127.0.0.1:9");
119
- const delivered = [];
120
- await ensureOAuthToken(client, null, t => {
121
- delivered.push(t);
122
- });
123
- assert.deepStrictEqual(delivered, ["cached-access-token"]);
124
- });
125
- test("already-held valid token is not re-delivered", async () => {
126
- const client = makeClient(makeTokenFile(validTokenFile), "http://127.0.0.1:9");
127
- const delivered = [];
128
- await ensureOAuthToken(client, "cached-access-token", t => {
129
- delivered.push(t);
130
- });
131
- assert.deepStrictEqual(delivered, []);
132
- });
133
- test("expired token refreshes via /oauth/token and persists", async () => {
134
- const { url, bodies } = await startTokenServer(() => ({
135
- status: 200,
136
- json: {
137
- access_token: "fresh-token",
138
- refresh_token: "fresh-refresh",
139
- expires_in: 7200,
140
- token_type: "Bearer",
141
- },
142
- }));
143
- const file = makeTokenFile(expiredTokenFile);
144
- const client = makeClient(file, url);
145
- const delivered = [];
146
- await ensureOAuthToken(client, null, t => {
147
- delivered.push(t);
148
- });
149
- assert.deepStrictEqual(delivered, ["fresh-token"]);
150
- assert.strictEqual(bodies.length, 1);
151
- assert.match(bodies[0], /grant_type=refresh_token/);
152
- assert.match(bodies[0], /refresh_token=cached-refresh-token/);
153
- const saved = JSON.parse(fs.readFileSync(file, "utf8"));
154
- assert.strictEqual(saved.access_token, "fresh-token");
155
- });
156
- test("transient refresh failure propagates without opening a browser", async () => {
157
- const { url } = await startTokenServer(() => ({ status: 500, json: { error: "server_error" } }));
158
- const client = makeClient(makeTokenFile(expiredTokenFile), url);
159
- const delivered = [];
160
- // Rejection (not a browser popup) proves the no-browser path.
161
- await assert.rejects(() => ensureOAuthToken(client, null, t => {
162
- delivered.push(t);
163
- }), /Token refresh failed: 500/);
164
- assert.deepStrictEqual(delivered, []);
165
- });
166
- test("token script resolves once, then fast path holds", async () => {
167
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "oauth-startup-"));
168
- tmpDirs.push(dir);
169
- const client = makeClient(path.join(dir, "token.json"), "http://127.0.0.1:9", {
170
- tokenScript: "echo script-token-1",
171
- });
172
- const delivered = [];
173
- await ensureOAuthToken(client, null, t => {
174
- delivered.push(t);
175
- });
176
- assert.deepStrictEqual(delivered, ["script-token-1"]);
177
- await ensureOAuthToken(client, delivered[0], t => {
178
- delivered.push(t);
179
- });
180
- assert.deepStrictEqual(delivered, ["script-token-1"]);
181
- });
182
- test("concurrent calls coalesce into a single refresh request", async () => {
183
- const { url, bodies } = await startTokenServer(() => ({
184
- status: 200,
185
- json: {
186
- access_token: "coalesced-token",
187
- refresh_token: "new-refresh",
188
- expires_in: 7200,
189
- token_type: "Bearer",
190
- },
191
- }));
192
- const file = makeTokenFile(expiredTokenFile);
193
- const client = makeClient(file, url);
194
- const deliveredA = [];
195
- const deliveredB = [];
196
- await Promise.all([
197
- ensureOAuthToken(client, null, t => deliveredA.push(t)),
198
- ensureOAuthToken(client, null, t => deliveredB.push(t)),
199
- ]);
200
- assert.deepStrictEqual(deliveredA, ["coalesced-token"]);
201
- assert.deepStrictEqual(deliveredB, ["coalesced-token"]);
202
- assert.strictEqual(bodies.length, 1);
203
- });
204
- });
@@ -1,428 +0,0 @@
1
- #!/usr/bin/env tsx
2
- /**
3
- * OAuth Authentication Tests
4
- * Tests for GitLab OAuth2 authentication flow
5
- */
6
- import * as fs from 'fs';
7
- import * as path from 'path';
8
- import * as http from 'http';
9
- import * as net from 'net';
10
- import { GitLabOAuth } from '../oauth.js';
11
- // Test configuration
12
- const TEST_CLIENT_ID = process.env.GITLAB_OAUTH_CLIENT_ID || 'test-client-id';
13
- const TEST_REDIRECT_URI = process.env.GITLAB_OAUTH_REDIRECT_URI || 'http://127.0.0.1:8888/callback';
14
- const TEST_GITLAB_URL = process.env.GITLAB_API_URL?.replace('/api/v4', '') || 'https://gitlab.com';
15
- const TEST_TOKEN_PATH = path.join(process.cwd(), '.test-gitlab-token.json');
16
- const testResults = [];
17
- // Helper function to run a single test
18
- async function runTest(name, testFn, skip = false) {
19
- if (skip) {
20
- console.log(`ā­ļø SKIPPED: ${name}`);
21
- testResults.push({ name, status: 'skipped', duration: 0 });
22
- return;
23
- }
24
- const startTime = Date.now();
25
- try {
26
- console.log(`🧪 Testing: ${name}`);
27
- await testFn();
28
- const duration = Date.now() - startTime;
29
- console.log(`āœ… PASSED: ${name} (${duration}ms)`);
30
- testResults.push({ name, status: 'passed', duration });
31
- }
32
- catch (error) {
33
- const duration = Date.now() - startTime;
34
- const errorMsg = error instanceof Error ? error.message : String(error);
35
- console.log(`āŒ FAILED: ${name} (${duration}ms)`);
36
- console.log(` Error: ${errorMsg}`);
37
- testResults.push({ name, status: 'failed', duration, error: errorMsg });
38
- }
39
- }
40
- // Helper function to assert conditions
41
- function assert(condition, message) {
42
- if (!condition) {
43
- throw new Error(`Assertion failed: ${message}`);
44
- }
45
- }
46
- // Helper function to check if port is available
47
- async function isPortAvailable(port) {
48
- return new Promise((resolve) => {
49
- const server = net.createServer();
50
- server.once('error', (err) => {
51
- if (err.code === 'EADDRINUSE') {
52
- resolve(false);
53
- }
54
- else {
55
- resolve(true);
56
- }
57
- });
58
- server.once('listening', () => {
59
- server.close();
60
- resolve(true);
61
- });
62
- server.listen(port, '127.0.0.1');
63
- });
64
- }
65
- // Clean up test token file
66
- function cleanupTestToken() {
67
- if (fs.existsSync(TEST_TOKEN_PATH)) {
68
- fs.unlinkSync(TEST_TOKEN_PATH);
69
- }
70
- }
71
- // Test 1: GitLabOAuth class instantiation
72
- async function testOAuthInstantiation() {
73
- const oauth = new GitLabOAuth({
74
- clientId: TEST_CLIENT_ID,
75
- redirectUri: TEST_REDIRECT_URI,
76
- gitlabUrl: TEST_GITLAB_URL,
77
- scopes: ['api'],
78
- tokenStoragePath: TEST_TOKEN_PATH,
79
- });
80
- assert(oauth !== null, 'OAuth instance should be created');
81
- assert(typeof oauth.getAccessToken === 'function', 'Should have getAccessToken method');
82
- assert(typeof oauth.clearToken === 'function', 'Should have clearToken method');
83
- assert(typeof oauth.hasValidToken === 'function', 'Should have hasValidToken method');
84
- }
85
- // Test 2: Token storage path configuration
86
- async function testTokenStoragePath() {
87
- const customPath = path.join(process.cwd(), '.custom-test-token.json');
88
- const oauth = new GitLabOAuth({
89
- clientId: TEST_CLIENT_ID,
90
- redirectUri: TEST_REDIRECT_URI,
91
- gitlabUrl: TEST_GITLAB_URL,
92
- scopes: ['api'],
93
- tokenStoragePath: customPath,
94
- });
95
- assert(oauth !== null, 'OAuth instance with custom path should be created');
96
- // Clean up
97
- if (fs.existsSync(customPath)) {
98
- fs.unlinkSync(customPath);
99
- }
100
- }
101
- // Test 3: Scope configuration
102
- async function testScopeConfiguration() {
103
- const oauth = new GitLabOAuth({
104
- clientId: TEST_CLIENT_ID,
105
- redirectUri: TEST_REDIRECT_URI,
106
- gitlabUrl: TEST_GITLAB_URL,
107
- scopes: ['api'],
108
- tokenStoragePath: TEST_TOKEN_PATH,
109
- });
110
- assert(oauth !== null, 'OAuth instance with api scope should be created');
111
- }
112
- // Test 4: Multiple scopes (should still work but is redundant)
113
- async function testMultipleScopesRedundant() {
114
- const oauth = new GitLabOAuth({
115
- clientId: TEST_CLIENT_ID,
116
- redirectUri: TEST_REDIRECT_URI,
117
- gitlabUrl: TEST_GITLAB_URL,
118
- scopes: ['api', 'read_user', 'read_api', 'write_repository'],
119
- tokenStoragePath: TEST_TOKEN_PATH,
120
- });
121
- assert(oauth !== null, 'OAuth instance with multiple scopes should be created');
122
- }
123
- // Test 5: hasValidToken returns false when no token exists
124
- async function testHasValidTokenNoToken() {
125
- cleanupTestToken();
126
- const oauth = new GitLabOAuth({
127
- clientId: TEST_CLIENT_ID,
128
- redirectUri: TEST_REDIRECT_URI,
129
- gitlabUrl: TEST_GITLAB_URL,
130
- scopes: ['api'],
131
- tokenStoragePath: TEST_TOKEN_PATH,
132
- });
133
- const hasToken = oauth.hasValidToken();
134
- assert(hasToken === false, 'Should return false when no token exists');
135
- }
136
- // Test 6: hasValidToken returns true with valid token
137
- async function testHasValidTokenWithToken() {
138
- const tokenData = {
139
- access_token: 'test-token',
140
- token_type: 'Bearer',
141
- created_at: Date.now(),
142
- expires_in: 7200, // 2 hours
143
- };
144
- fs.writeFileSync(TEST_TOKEN_PATH, JSON.stringify(tokenData), { mode: 0o600 });
145
- const oauth = new GitLabOAuth({
146
- clientId: TEST_CLIENT_ID,
147
- redirectUri: TEST_REDIRECT_URI,
148
- gitlabUrl: TEST_GITLAB_URL,
149
- scopes: ['api'],
150
- tokenStoragePath: TEST_TOKEN_PATH,
151
- });
152
- const hasToken = oauth.hasValidToken();
153
- assert(hasToken === true, 'Should return true with valid token');
154
- cleanupTestToken();
155
- }
156
- // Test 7: hasValidToken returns false with expired token
157
- async function testHasValidTokenExpired() {
158
- const tokenData = {
159
- access_token: 'test-token',
160
- token_type: 'Bearer',
161
- created_at: Date.now() - 10000000, // 2.7+ hours ago
162
- expires_in: 7200, // 2 hours
163
- };
164
- fs.writeFileSync(TEST_TOKEN_PATH, JSON.stringify(tokenData), { mode: 0o600 });
165
- const oauth = new GitLabOAuth({
166
- clientId: TEST_CLIENT_ID,
167
- redirectUri: TEST_REDIRECT_URI,
168
- gitlabUrl: TEST_GITLAB_URL,
169
- scopes: ['api'],
170
- tokenStoragePath: TEST_TOKEN_PATH,
171
- });
172
- const hasToken = oauth.hasValidToken();
173
- assert(hasToken === false, 'Should return false with expired token');
174
- cleanupTestToken();
175
- }
176
- // Test 8: clearToken removes token file
177
- async function testClearToken() {
178
- const tokenData = {
179
- access_token: 'test-token',
180
- token_type: 'Bearer',
181
- created_at: Date.now(),
182
- };
183
- fs.writeFileSync(TEST_TOKEN_PATH, JSON.stringify(tokenData), { mode: 0o600 });
184
- const oauth = new GitLabOAuth({
185
- clientId: TEST_CLIENT_ID,
186
- redirectUri: TEST_REDIRECT_URI,
187
- gitlabUrl: TEST_GITLAB_URL,
188
- scopes: ['api'],
189
- tokenStoragePath: TEST_TOKEN_PATH,
190
- });
191
- oauth.clearToken();
192
- assert(!fs.existsSync(TEST_TOKEN_PATH), 'Token file should be deleted');
193
- }
194
- // Test 9: Token file has correct permissions (Unix only)
195
- async function testTokenFilePermissions() {
196
- if (process.platform === 'win32') {
197
- throw new Error('Skipping permission test on Windows');
198
- }
199
- const tokenData = {
200
- access_token: 'test-token',
201
- token_type: 'Bearer',
202
- created_at: Date.now(),
203
- };
204
- fs.writeFileSync(TEST_TOKEN_PATH, JSON.stringify(tokenData), { mode: 0o600 });
205
- const stats = fs.statSync(TEST_TOKEN_PATH);
206
- const mode = stats.mode & 0o777;
207
- assert(mode === 0o600, `Token file should have 0600 permissions, got ${mode.toString(8)}`);
208
- cleanupTestToken();
209
- }
210
- // Test 10: Port availability check
211
- async function testPortAvailability() {
212
- const port = 8888;
213
- const available = await isPortAvailable(port);
214
- // We just check that the function works, not the actual availability
215
- assert(typeof available === 'boolean', 'Port availability check should return boolean');
216
- }
217
- // Test: External OAuth token script
218
- async function testOAuthTokenScript() {
219
- const scriptPath = path.join(process.cwd(), '.test-oauth-token-script.sh');
220
- const writeScript = (output) => {
221
- fs.writeFileSync(scriptPath, `#!/bin/sh\nprintf '%s\\n' '${output}'\n`, { mode: 0o700 });
222
- };
223
- const oauth = () => new GitLabOAuth({
224
- clientId: TEST_CLIENT_ID,
225
- redirectUri: TEST_REDIRECT_URI,
226
- gitlabUrl: TEST_GITLAB_URL,
227
- scopes: ['api'],
228
- tokenStoragePath: TEST_TOKEN_PATH,
229
- tokenScript: scriptPath,
230
- });
231
- try {
232
- writeScript('script-token-123');
233
- const plainToken = await oauth().getAccessToken();
234
- assert(plainToken === 'script-token-123', 'Should return plain token from external script');
235
- assert(oauth().hasValidToken(), 'Token script should count as a valid token source');
236
- writeScript(JSON.stringify({ access_token: 'json-token-123' }));
237
- const jsonToken = await oauth().getAccessToken();
238
- assert(jsonToken === 'json-token-123', 'Should extract access_token from JSON output');
239
- writeScript(JSON.stringify({ expires_in: 3600 }));
240
- try {
241
- await oauth().getAccessToken();
242
- assert(false, 'Should reject JSON output without a token field');
243
- }
244
- catch (error) {
245
- assert(error instanceof Error &&
246
- error.message.includes('OAuth token script JSON must include a string access_token or token field'), 'Should explain missing token field in JSON output');
247
- }
248
- }
249
- finally {
250
- if (fs.existsSync(scriptPath)) {
251
- fs.unlinkSync(scriptPath);
252
- }
253
- }
254
- }
255
- // Test 11: OAuth redirect URI parsing
256
- async function testRedirectUriParsing() {
257
- const redirectUri = 'http://127.0.0.1:8888/callback';
258
- const url = new URL(redirectUri);
259
- assert(url.port === '8888', 'Should correctly parse port from redirect URI');
260
- assert(url.pathname === '/callback', 'Should correctly parse path from redirect URI');
261
- assert(url.hostname === '127.0.0.1', 'Should correctly parse hostname from redirect URI');
262
- }
263
- // Test 12: Token expiration calculation
264
- async function testTokenExpirationCalculation() {
265
- const now = Date.now();
266
- const expiresIn = 7200; // 2 hours in seconds
267
- const buffer = 5 * 60 * 1000; // 5 minutes in milliseconds
268
- const expiryTime = now + (expiresIn * 1000);
269
- const shouldRefreshAt = expiryTime - buffer;
270
- assert(shouldRefreshAt < expiryTime, 'Refresh time should be before expiry');
271
- assert(shouldRefreshAt > now, 'Refresh time should be in the future for new token');
272
- }
273
- // Test 13: Concurrent OAuth server handling (shared server concept)
274
- async function testSharedServerConcept() {
275
- // Test that multiple instances can theoretically share a port
276
- const port = 9999;
277
- // First instance: start server
278
- const server = http.createServer((req, res) => {
279
- res.writeHead(200);
280
- res.end('OK');
281
- });
282
- await new Promise((resolve) => {
283
- server.listen(port, '127.0.0.1', () => resolve());
284
- });
285
- // Check port is now in use
286
- const inUse = !(await isPortAvailable(port));
287
- assert(inUse === true, 'Port should be in use after server starts');
288
- // Clean up
289
- await new Promise((resolve) => {
290
- server.close(() => resolve());
291
- });
292
- // Check port is available again
293
- const available = await isPortAvailable(port);
294
- assert(available === true, 'Port should be available after server closes');
295
- }
296
- // Test 14: Environment variable configuration
297
- async function testEnvironmentVariableConfig() {
298
- const clientId = process.env.GITLAB_OAUTH_CLIENT_ID;
299
- const redirectUri = process.env.GITLAB_OAUTH_REDIRECT_URI || 'http://127.0.0.1:8888/callback';
300
- assert(typeof clientId === 'string' || clientId === undefined, 'Client ID should be string or undefined');
301
- assert(typeof redirectUri === 'string', 'Redirect URI should be string');
302
- const url = new URL(redirectUri);
303
- assert(url.protocol === 'http:', 'Redirect URI should use http protocol for localhost');
304
- }
305
- // Test 15: Token data structure validation
306
- async function testTokenDataStructure() {
307
- const tokenData = {
308
- access_token: 'glpat-test123456789',
309
- refresh_token: 'refresh-test123456789',
310
- token_type: 'Bearer',
311
- expires_in: 7200,
312
- created_at: Date.now(),
313
- };
314
- assert(typeof tokenData.access_token === 'string', 'access_token should be string');
315
- assert(typeof tokenData.token_type === 'string', 'token_type should be string');
316
- assert(typeof tokenData.created_at === 'number', 'created_at should be number');
317
- assert(tokenData.expires_in === undefined || typeof tokenData.expires_in === 'number', 'expires_in should be number or undefined');
318
- }
319
- // Test 16: Invalid token storage path handling
320
- async function testInvalidTokenStoragePath() {
321
- const invalidPath = '/root/nonexistent/directory/.token.json';
322
- const oauth = new GitLabOAuth({
323
- clientId: TEST_CLIENT_ID,
324
- redirectUri: TEST_REDIRECT_URI,
325
- gitlabUrl: TEST_GITLAB_URL,
326
- scopes: ['api'],
327
- tokenStoragePath: invalidPath,
328
- });
329
- // Should create instance even with invalid path (error occurs during save)
330
- assert(oauth !== null, 'Should create instance with invalid path');
331
- }
332
- // Test 17: Self-hosted GitLab URL configuration
333
- async function testSelfHostedGitLabUrl() {
334
- const selfHostedUrl = 'https://gitlab.example.com';
335
- const oauth = new GitLabOAuth({
336
- clientId: TEST_CLIENT_ID,
337
- redirectUri: TEST_REDIRECT_URI,
338
- gitlabUrl: selfHostedUrl,
339
- scopes: ['api'],
340
- tokenStoragePath: TEST_TOKEN_PATH,
341
- });
342
- assert(oauth !== null, 'Should create instance with self-hosted URL');
343
- }
344
- // Test 18: Custom port in redirect URI
345
- async function testCustomPortInRedirectUri() {
346
- const customRedirectUri = 'http://127.0.0.1:9999/callback';
347
- const oauth = new GitLabOAuth({
348
- clientId: TEST_CLIENT_ID,
349
- redirectUri: customRedirectUri,
350
- gitlabUrl: TEST_GITLAB_URL,
351
- scopes: ['api'],
352
- tokenStoragePath: TEST_TOKEN_PATH,
353
- });
354
- assert(oauth !== null, 'Should create instance with custom port');
355
- const url = new URL(customRedirectUri);
356
- assert(url.port === '9999', 'Should correctly parse custom port');
357
- }
358
- // Main test runner
359
- async function runOAuthTests() {
360
- console.log('šŸš€ GitLab OAuth Authentication Tests\n');
361
- console.log('='.repeat(50));
362
- // Core functionality tests
363
- await runTest('OAuth class instantiation', testOAuthInstantiation);
364
- await runTest('Token storage path configuration', testTokenStoragePath);
365
- await runTest('Scope configuration with api only', testScopeConfiguration);
366
- await runTest('Multiple scopes configuration (redundant)', testMultipleScopesRedundant);
367
- // Token management tests
368
- await runTest('hasValidToken returns false without token', testHasValidTokenNoToken);
369
- await runTest('hasValidToken returns true with valid token', testHasValidTokenWithToken);
370
- await runTest('hasValidToken returns false with expired token', testHasValidTokenExpired);
371
- await runTest('clearToken removes token file', testClearToken);
372
- await runTest('Token file has correct permissions', testTokenFilePermissions, process.platform === 'win32');
373
- await runTest('External OAuth token script', testOAuthTokenScript, process.platform === 'win32');
374
- // Network and configuration tests
375
- await runTest('Port availability check', testPortAvailability);
376
- await runTest('OAuth redirect URI parsing', testRedirectUriParsing);
377
- await runTest('Token expiration calculation', testTokenExpirationCalculation);
378
- await runTest('Shared server concept', testSharedServerConcept);
379
- // Configuration tests
380
- await runTest('Environment variable configuration', testEnvironmentVariableConfig);
381
- await runTest('Token data structure validation', testTokenDataStructure);
382
- await runTest('Invalid token storage path handling', testInvalidTokenStoragePath);
383
- await runTest('Self-hosted GitLab URL configuration', testSelfHostedGitLabUrl);
384
- await runTest('Custom port in redirect URI', testCustomPortInRedirectUri);
385
- // Cleanup
386
- cleanupTestToken();
387
- // Print summary
388
- console.log('\n' + '='.repeat(50));
389
- console.log('šŸ“Š Test Results Summary\n');
390
- const passed = testResults.filter(r => r.status === 'passed').length;
391
- const failed = testResults.filter(r => r.status === 'failed').length;
392
- const skipped = testResults.filter(r => r.status === 'skipped').length;
393
- const total = testResults.length;
394
- console.log(`Total tests: ${total}`);
395
- console.log(`āœ… Passed: ${passed}`);
396
- console.log(`āŒ Failed: ${failed}`);
397
- console.log(`ā­ļø Skipped: ${skipped}`);
398
- if (total > 0) {
399
- const successRate = ((passed / (total - skipped)) * 100).toFixed(1);
400
- console.log(`Success rate: ${successRate}%`);
401
- }
402
- // Show failed tests
403
- const failedTests = testResults.filter(r => r.status === 'failed');
404
- if (failedTests.length > 0) {
405
- console.log('\nāŒ Failed Tests:');
406
- failedTests.forEach(test => {
407
- console.log(` - ${test.name}`);
408
- console.log(` ${test.error}`);
409
- });
410
- }
411
- // Save results to file
412
- const reportPath = 'test-results-oauth.json';
413
- fs.writeFileSync(reportPath, JSON.stringify(testResults, null, 2));
414
- console.log(`\nšŸ“„ Detailed results saved to ${reportPath}`);
415
- return failed === 0;
416
- }
417
- // Run tests if this is the main module
418
- if (import.meta.url === `file://${process.argv[1]}`) {
419
- runOAuthTests()
420
- .then(success => {
421
- process.exit(success ? 0 : 1);
422
- })
423
- .catch(error => {
424
- console.error('Error running tests:', error);
425
- process.exit(1);
426
- });
427
- }
428
- export { runOAuthTests, testResults };