@trayio/cdk-cli 5.5.0 → 5.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/OAUTH2_TOKEN.md +290 -0
- package/README.md +38 -1
- package/dist/commands/connector/oauth2-token.d.ts +45 -0
- package/dist/commands/connector/oauth2-token.d.ts.map +1 -0
- package/dist/commands/connector/oauth2-token.js +243 -0
- package/dist/commands/connector/oauth2-token.unit.test.d.ts +2 -0
- package/dist/commands/connector/oauth2-token.unit.test.d.ts.map +1 -0
- package/dist/commands/connector/oauth2-token.unit.test.js +1244 -0
- package/dist/lib/oauth2-token/flows/authorization-code.d.ts +4 -0
- package/dist/lib/oauth2-token/flows/authorization-code.d.ts.map +1 -0
- package/dist/lib/oauth2-token/flows/authorization-code.js +218 -0
- package/dist/lib/oauth2-token/flows/client-credentials.d.ts +4 -0
- package/dist/lib/oauth2-token/flows/client-credentials.d.ts.map +1 -0
- package/dist/lib/oauth2-token/flows/client-credentials.js +55 -0
- package/dist/lib/oauth2-token/flows/device-code.d.ts +4 -0
- package/dist/lib/oauth2-token/flows/device-code.d.ts.map +1 -0
- package/dist/lib/oauth2-token/flows/device-code.js +143 -0
- package/dist/lib/oauth2-token/flows/index.d.ts +8 -0
- package/dist/lib/oauth2-token/flows/index.d.ts.map +1 -0
- package/dist/lib/oauth2-token/flows/index.js +14 -0
- package/dist/lib/oauth2-token/flows/refresh-token.d.ts +4 -0
- package/dist/lib/oauth2-token/flows/refresh-token.d.ts.map +1 -0
- package/dist/lib/oauth2-token/flows/refresh-token.js +60 -0
- package/dist/lib/oauth2-token/token-writer.d.ts +7 -0
- package/dist/lib/oauth2-token/token-writer.d.ts.map +1 -0
- package/dist/lib/oauth2-token/token-writer.js +83 -0
- package/dist/lib/oauth2-token/types.d.ts +34 -0
- package/dist/lib/oauth2-token/types.d.ts.map +1 -0
- package/dist/lib/oauth2-token/types.js +5 -0
- package/dist/lib/oauth2-token/utils/browser.d.ts +2 -0
- package/dist/lib/oauth2-token/utils/browser.d.ts.map +1 -0
- package/dist/lib/oauth2-token/utils/browser.js +22 -0
- package/dist/lib/oauth2-token/utils/crypto.d.ts +6 -0
- package/dist/lib/oauth2-token/utils/crypto.d.ts.map +1 -0
- package/dist/lib/oauth2-token/utils/crypto.js +47 -0
- package/dist/lib/oauth2-token/utils/env.d.ts +7 -0
- package/dist/lib/oauth2-token/utils/env.d.ts.map +1 -0
- package/dist/lib/oauth2-token/utils/env.js +85 -0
- package/dist/lib/oauth2-token/utils/file.d.ts +4 -0
- package/dist/lib/oauth2-token/utils/file.d.ts.map +1 -0
- package/dist/lib/oauth2-token/utils/file.js +40 -0
- package/dist/lib/oauth2-token/utils/index.d.ts +10 -0
- package/dist/lib/oauth2-token/utils/index.d.ts.map +1 -0
- package/dist/lib/oauth2-token/utils/index.js +25 -0
- package/dist/lib/oauth2-token/utils/json.d.ts +9 -0
- package/dist/lib/oauth2-token/utils/json.d.ts.map +1 -0
- package/dist/lib/oauth2-token/utils/json.js +52 -0
- package/dist/lib/oauth2-token/utils/url.d.ts +6 -0
- package/dist/lib/oauth2-token/utils/url.d.ts.map +1 -0
- package/dist/lib/oauth2-token/utils/url.js +22 -0
- package/oclif.manifest.json +150 -1
- package/package.json +11 -9
|
@@ -0,0 +1,1244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
const stdout_stderr_1 = require("stdout-stderr");
|
|
30
|
+
const core_1 = require("@oclif/core");
|
|
31
|
+
const fs = __importStar(require("fs/promises"));
|
|
32
|
+
const http = __importStar(require("http"));
|
|
33
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
34
|
+
// Note: open is mocked below, so we don't need to import it
|
|
35
|
+
const AxiosHttpClient_1 = require("@trayio/axios/http/AxiosHttpClient");
|
|
36
|
+
const BufferExtensions_1 = require("@trayio/commons/buffer/BufferExtensions");
|
|
37
|
+
const E = __importStar(require("fp-ts/Either"));
|
|
38
|
+
const child_process_1 = require("child_process");
|
|
39
|
+
const dotenv = __importStar(require("dotenv"));
|
|
40
|
+
const oauth2_token_1 = __importDefault(require("./oauth2-token"));
|
|
41
|
+
// Mock all external dependencies
|
|
42
|
+
jest.mock('fs/promises');
|
|
43
|
+
jest.mock('inquirer');
|
|
44
|
+
jest.mock('child_process');
|
|
45
|
+
jest.mock('chalk', () => {
|
|
46
|
+
const mockChalk = jest.fn((text) => text);
|
|
47
|
+
mockChalk.bold = jest.fn((text) => text);
|
|
48
|
+
mockChalk.gray = jest.fn((text) => text);
|
|
49
|
+
mockChalk.green = jest.fn((text) => text);
|
|
50
|
+
mockChalk.red = jest.fn((text) => text);
|
|
51
|
+
mockChalk.yellow = jest.fn((text) => text);
|
|
52
|
+
mockChalk.blue = jest.fn((text) => text);
|
|
53
|
+
mockChalk.cyan = jest.fn((text) => text);
|
|
54
|
+
mockChalk.magenta = jest.fn((text) => text);
|
|
55
|
+
mockChalk.white = jest.fn((text) => text);
|
|
56
|
+
mockChalk.black = jest.fn((text) => text);
|
|
57
|
+
mockChalk.dim = jest.fn((text) => text);
|
|
58
|
+
mockChalk.underline = jest.fn((text) => text);
|
|
59
|
+
mockChalk.inverse = jest.fn((text) => text);
|
|
60
|
+
mockChalk.strikethrough = jest.fn((text) => text);
|
|
61
|
+
return mockChalk;
|
|
62
|
+
});
|
|
63
|
+
jest.mock('@trayio/axios/http/AxiosHttpClient');
|
|
64
|
+
jest.mock('@trayio/commons/buffer/BufferExtensions');
|
|
65
|
+
jest.mock('http');
|
|
66
|
+
jest.mock('dotenv');
|
|
67
|
+
jest.mock('crypto', () => ({
|
|
68
|
+
...jest.requireActual('crypto'),
|
|
69
|
+
randomBytes: jest
|
|
70
|
+
.fn()
|
|
71
|
+
.mockReturnValue(Buffer.from('mock-random-bytes-for-testing-purposes')),
|
|
72
|
+
}));
|
|
73
|
+
describe('OAuth2Token', () => {
|
|
74
|
+
const mockFs = fs;
|
|
75
|
+
const mockInquirer = inquirer_1.default;
|
|
76
|
+
const mockExec = jest.mocked(child_process_1.exec);
|
|
77
|
+
const mockAxiosHttpClient = AxiosHttpClient_1.AxiosHttpClient;
|
|
78
|
+
const mockBufferExtensions = BufferExtensions_1.BufferExtensions;
|
|
79
|
+
const mockHttp = http;
|
|
80
|
+
const mockDotenv = dotenv;
|
|
81
|
+
// Helper function to create proper ArrayBuffer from JSON
|
|
82
|
+
const createArrayBufferFromJson = (obj) => {
|
|
83
|
+
const jsonString = JSON.stringify(obj);
|
|
84
|
+
const buffer = Buffer.from(jsonString, 'utf-8');
|
|
85
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
86
|
+
};
|
|
87
|
+
// Sample test context data
|
|
88
|
+
const mockContext = {
|
|
89
|
+
client_id: 'test-client-id',
|
|
90
|
+
client_secret: 'test-client-secret',
|
|
91
|
+
token_url: 'https://oauth.example.com/token',
|
|
92
|
+
authorize_url: 'https://oauth.example.com/authorize',
|
|
93
|
+
scope: 'read write',
|
|
94
|
+
};
|
|
95
|
+
// Sample token response
|
|
96
|
+
const mockTokenResponse = {
|
|
97
|
+
access_token: 'test-access-token',
|
|
98
|
+
refresh_token: 'test-refresh-token',
|
|
99
|
+
expires_in: 3600,
|
|
100
|
+
token_type: 'Bearer',
|
|
101
|
+
};
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
jest.clearAllMocks();
|
|
104
|
+
// Default file system mocks
|
|
105
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify(mockContext));
|
|
106
|
+
mockFs.writeFile.mockResolvedValue();
|
|
107
|
+
mockFs.access.mockResolvedValue(undefined); // .env file exists by default
|
|
108
|
+
// Default dotenv mock
|
|
109
|
+
mockDotenv.config.mockReturnValue({
|
|
110
|
+
parsed: {},
|
|
111
|
+
error: undefined,
|
|
112
|
+
});
|
|
113
|
+
// Mock BufferExtensions - readableToArrayBuffer returns a TaskEither (function that returns Promise)
|
|
114
|
+
mockBufferExtensions.readableToArrayBuffer.mockImplementation(() => () => Promise.resolve(E.right(createArrayBufferFromJson(mockTokenResponse))));
|
|
115
|
+
// Default HTTP client mock - execute returns a TaskEither (function that returns Promise)
|
|
116
|
+
const mockHttpInstance = {
|
|
117
|
+
execute: jest.fn().mockReturnValue(() => Promise.resolve(E.right({
|
|
118
|
+
statusCode: 200,
|
|
119
|
+
headers: {},
|
|
120
|
+
body: {}, // Readable stream mock
|
|
121
|
+
}))),
|
|
122
|
+
};
|
|
123
|
+
mockAxiosHttpClient.mockImplementation(() => mockHttpInstance);
|
|
124
|
+
// Default exec mock - return a mock ChildProcess
|
|
125
|
+
mockExec.mockImplementation(() => ({}));
|
|
126
|
+
});
|
|
127
|
+
afterAll(() => {
|
|
128
|
+
jest.restoreAllMocks();
|
|
129
|
+
});
|
|
130
|
+
describe('Client Credentials Flow', () => {
|
|
131
|
+
it('should successfully request token with client credentials', async () => {
|
|
132
|
+
const startSpy = jest.spyOn(core_1.ux.action, 'start');
|
|
133
|
+
const stopSpy = jest.spyOn(core_1.ux.action, 'stop');
|
|
134
|
+
stdout_stderr_1.stdout.start();
|
|
135
|
+
await oauth2_token_1.default.run([
|
|
136
|
+
'--ctxPath',
|
|
137
|
+
'test.ctx.json',
|
|
138
|
+
'--grantType',
|
|
139
|
+
'client_credentials',
|
|
140
|
+
]);
|
|
141
|
+
stdout_stderr_1.stdout.stop();
|
|
142
|
+
expect(startSpy).toHaveBeenCalledWith('Requesting OAuth2 token (client_credentials)');
|
|
143
|
+
expect(stopSpy).toHaveBeenCalledWith(expect.stringContaining('done'));
|
|
144
|
+
expect(stdout_stderr_1.stdout.output).toContain('Access token written to test context file');
|
|
145
|
+
// Verify file was written with token
|
|
146
|
+
expect(mockFs.writeFile).toHaveBeenCalledWith(expect.stringContaining('test.ctx.json'), expect.stringContaining(mockTokenResponse.access_token), 'utf-8');
|
|
147
|
+
});
|
|
148
|
+
it('should handle missing required fields for client credentials', async () => {
|
|
149
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify({}));
|
|
150
|
+
const command = new oauth2_token_1.default(['--grantType', 'client_credentials'], {});
|
|
151
|
+
await expect(command.run()).rejects.toThrow();
|
|
152
|
+
});
|
|
153
|
+
it('should include scope and audience in request when provided', async () => {
|
|
154
|
+
const mockExecute = jest.fn().mockReturnValue(() => Promise.resolve(E.right({
|
|
155
|
+
statusCode: 200,
|
|
156
|
+
headers: {},
|
|
157
|
+
body: {},
|
|
158
|
+
})));
|
|
159
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
160
|
+
await oauth2_token_1.default.run([
|
|
161
|
+
'--grantType',
|
|
162
|
+
'client_credentials',
|
|
163
|
+
'--scope',
|
|
164
|
+
'custom:scope',
|
|
165
|
+
'--audience',
|
|
166
|
+
'https://api.example.com',
|
|
167
|
+
]);
|
|
168
|
+
const requestBody = mockExecute.mock.calls[0][2].body.toString();
|
|
169
|
+
expect(requestBody).toContain('scope=custom%3Ascope');
|
|
170
|
+
expect(requestBody).toContain('audience=https%3A%2F%2Fapi.example.com');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
describe('Authorization Code Flow', () => {
|
|
174
|
+
beforeEach(() => {
|
|
175
|
+
// Mock HTTP server for callback
|
|
176
|
+
const mockServer = {
|
|
177
|
+
listen: jest.fn().mockImplementation((port, host, callback) => {
|
|
178
|
+
callback?.();
|
|
179
|
+
}),
|
|
180
|
+
close: jest.fn().mockImplementation((callback) => {
|
|
181
|
+
callback?.();
|
|
182
|
+
}),
|
|
183
|
+
};
|
|
184
|
+
mockHttp.createServer.mockImplementation((requestHandler) => {
|
|
185
|
+
// Simulate successful callback - capture the actual state from the authorization URL
|
|
186
|
+
setTimeout(() => {
|
|
187
|
+
// With mocked crypto.randomBytes, the state will be predictable
|
|
188
|
+
// The state is base64url encoded from the mocked random bytes
|
|
189
|
+
const mockReq = {
|
|
190
|
+
url: '/callback?code=test-auth-code&state=bW9jay1yYW5kb20tYnl0ZXMtZm9yLXRlc3RpbmctcHVycG9zZXM',
|
|
191
|
+
};
|
|
192
|
+
const mockRes = {
|
|
193
|
+
writeHead: jest.fn(),
|
|
194
|
+
end: jest.fn(),
|
|
195
|
+
};
|
|
196
|
+
requestHandler?.(mockReq, mockRes);
|
|
197
|
+
}, 100);
|
|
198
|
+
return mockServer;
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
it('should successfully complete authorization code flow', async () => {
|
|
202
|
+
const startSpy = jest.spyOn(core_1.ux.action, 'start');
|
|
203
|
+
const stopSpy = jest.spyOn(core_1.ux.action, 'stop');
|
|
204
|
+
stdout_stderr_1.stdout.start();
|
|
205
|
+
await oauth2_token_1.default.run([
|
|
206
|
+
'--grantType',
|
|
207
|
+
'authorization_code',
|
|
208
|
+
'--openBrowser',
|
|
209
|
+
]);
|
|
210
|
+
stdout_stderr_1.stdout.stop();
|
|
211
|
+
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('oauth.example.com/authorize'));
|
|
212
|
+
expect(startSpy).toHaveBeenCalledWith('Exchanging code for tokens');
|
|
213
|
+
expect(stopSpy).toHaveBeenCalledWith(expect.stringContaining('done'));
|
|
214
|
+
expect(stdout_stderr_1.stdout.output).toContain('Opening browser for authorization');
|
|
215
|
+
expect(stdout_stderr_1.stdout.output).toContain('Access token written to test context file');
|
|
216
|
+
});
|
|
217
|
+
it('should not open browser when openBrowser is disabled', async () => {
|
|
218
|
+
// Test with client_credentials flow which doesn't open browser
|
|
219
|
+
stdout_stderr_1.stdout.start();
|
|
220
|
+
await oauth2_token_1.default.run(['--grantType', 'client_credentials']);
|
|
221
|
+
stdout_stderr_1.stdout.stop();
|
|
222
|
+
expect(mockExec).not.toHaveBeenCalled();
|
|
223
|
+
});
|
|
224
|
+
it('should handle non-localhost redirect URI with manual input', async () => {
|
|
225
|
+
const contextWithRemoteRedirect = {
|
|
226
|
+
...mockContext,
|
|
227
|
+
redirect_uri: 'https://example.com/callback',
|
|
228
|
+
};
|
|
229
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify(contextWithRemoteRedirect));
|
|
230
|
+
mockInquirer.prompt.mockResolvedValue({
|
|
231
|
+
pastedUrl: 'https://example.com/callback?code=test-code&state=bW9jay1yYW5kb20tYnl0ZXMtZm9yLXRlc3RpbmctcHVycG9zZXM',
|
|
232
|
+
});
|
|
233
|
+
stdout_stderr_1.stdout.start();
|
|
234
|
+
await oauth2_token_1.default.run(['--grantType', 'authorization_code']);
|
|
235
|
+
stdout_stderr_1.stdout.stop();
|
|
236
|
+
expect(mockInquirer.prompt).toHaveBeenCalledWith([
|
|
237
|
+
{
|
|
238
|
+
name: 'pastedUrl',
|
|
239
|
+
message: 'Paste the full redirect URL:',
|
|
240
|
+
type: 'input',
|
|
241
|
+
},
|
|
242
|
+
]);
|
|
243
|
+
expect(stdout_stderr_1.stdout.output).toContain('Redirect URI is not localhost');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
describe('Device Code Flow', () => {
|
|
247
|
+
const mockDeviceInit = {
|
|
248
|
+
device_code: 'test-device-code',
|
|
249
|
+
user_code: 'ABC-123',
|
|
250
|
+
verification_uri: 'https://oauth.example.com/device',
|
|
251
|
+
verification_uri_complete: 'https://oauth.example.com/device?user_code=ABC-123',
|
|
252
|
+
expires_in: 900,
|
|
253
|
+
interval: 5,
|
|
254
|
+
};
|
|
255
|
+
beforeEach(() => {
|
|
256
|
+
const mockExecute = jest
|
|
257
|
+
.fn()
|
|
258
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
259
|
+
statusCode: 200,
|
|
260
|
+
headers: {},
|
|
261
|
+
body: {},
|
|
262
|
+
})))
|
|
263
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
264
|
+
statusCode: 200,
|
|
265
|
+
headers: {},
|
|
266
|
+
body: {},
|
|
267
|
+
})));
|
|
268
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
269
|
+
// Reset and mock BufferExtensions for device code responses
|
|
270
|
+
mockBufferExtensions.readableToArrayBuffer.mockReset();
|
|
271
|
+
mockBufferExtensions.readableToArrayBuffer
|
|
272
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson(mockDeviceInit))))
|
|
273
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson(mockTokenResponse))));
|
|
274
|
+
});
|
|
275
|
+
it('should successfully complete device code flow', async () => {
|
|
276
|
+
const contextWithDeviceCode = {
|
|
277
|
+
...mockContext,
|
|
278
|
+
device_code_url: 'https://oauth.example.com/device',
|
|
279
|
+
};
|
|
280
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify(contextWithDeviceCode));
|
|
281
|
+
const startSpy = jest.spyOn(core_1.ux.action, 'start');
|
|
282
|
+
const stopSpy = jest.spyOn(core_1.ux.action, 'stop');
|
|
283
|
+
stdout_stderr_1.stdout.start();
|
|
284
|
+
await oauth2_token_1.default.run(['--grantType', 'device_code']);
|
|
285
|
+
stdout_stderr_1.stdout.stop();
|
|
286
|
+
expect(startSpy).toHaveBeenCalledWith('Starting device authorization');
|
|
287
|
+
expect(startSpy).toHaveBeenCalledWith('Waiting for user authorization');
|
|
288
|
+
expect(stopSpy).toHaveBeenCalledWith(expect.stringContaining('done'));
|
|
289
|
+
expect(stdout_stderr_1.stdout.output).toContain('User code: ABC-123');
|
|
290
|
+
expect(stdout_stderr_1.stdout.output).toContain('Visit: https://oauth.example.com/device');
|
|
291
|
+
});
|
|
292
|
+
it('should open browser for device code flow when openBrowser is true', async () => {
|
|
293
|
+
const contextWithDeviceCode = {
|
|
294
|
+
...mockContext,
|
|
295
|
+
device_code_url: 'https://oauth.example.com/device',
|
|
296
|
+
};
|
|
297
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify(contextWithDeviceCode));
|
|
298
|
+
await oauth2_token_1.default.run(['--grantType', 'device_code', '--openBrowser']);
|
|
299
|
+
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('https://oauth.example.com/device?user_code=ABC-123'));
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
describe('Auto Grant Type Detection', () => {
|
|
303
|
+
it('should auto-detect authorization_code when authorize_url is present', async () => {
|
|
304
|
+
// Mock HTTP server for callback
|
|
305
|
+
const mockServer = {
|
|
306
|
+
listen: jest
|
|
307
|
+
.fn()
|
|
308
|
+
.mockImplementation((port, host, callback) => callback?.()),
|
|
309
|
+
close: jest.fn().mockImplementation((callback) => callback?.()),
|
|
310
|
+
};
|
|
311
|
+
mockHttp.createServer.mockImplementation((requestHandler) => {
|
|
312
|
+
setTimeout(() => {
|
|
313
|
+
const mockReq = {
|
|
314
|
+
url: '/callback?code=test-code&state=bW9jay1yYW5kb20tYnl0ZXMtZm9yLXRlc3RpbmctcHVycG9zZXM',
|
|
315
|
+
};
|
|
316
|
+
const mockRes = { writeHead: jest.fn(), end: jest.fn() };
|
|
317
|
+
requestHandler?.(mockReq, mockRes);
|
|
318
|
+
}, 100);
|
|
319
|
+
return mockServer;
|
|
320
|
+
});
|
|
321
|
+
stdout_stderr_1.stdout.start();
|
|
322
|
+
await oauth2_token_1.default.run(['--grantType', 'auto']);
|
|
323
|
+
stdout_stderr_1.stdout.stop();
|
|
324
|
+
expect(stdout_stderr_1.stdout.output).toContain('Opening browser for authorization');
|
|
325
|
+
});
|
|
326
|
+
it('should auto-detect device_code when device_code_url is present', async () => {
|
|
327
|
+
const contextWithDeviceCode = {
|
|
328
|
+
...mockContext,
|
|
329
|
+
device_code_url: 'https://oauth.example.com/device',
|
|
330
|
+
authorize_url: undefined, // Remove authorize_url to prefer device code
|
|
331
|
+
};
|
|
332
|
+
delete contextWithDeviceCode.authorize_url;
|
|
333
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify(contextWithDeviceCode));
|
|
334
|
+
const mockExecute = jest
|
|
335
|
+
.fn()
|
|
336
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
337
|
+
statusCode: 200,
|
|
338
|
+
headers: {},
|
|
339
|
+
body: {},
|
|
340
|
+
})))
|
|
341
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
342
|
+
statusCode: 200,
|
|
343
|
+
headers: {},
|
|
344
|
+
body: {},
|
|
345
|
+
})));
|
|
346
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
347
|
+
// Mock BufferExtensions for this test
|
|
348
|
+
mockBufferExtensions.readableToArrayBuffer.mockReset();
|
|
349
|
+
mockBufferExtensions.readableToArrayBuffer
|
|
350
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson({
|
|
351
|
+
device_code: 'test-device-code',
|
|
352
|
+
user_code: 'ABC-123',
|
|
353
|
+
verification_uri: 'https://oauth.example.com/device',
|
|
354
|
+
expires_in: 900,
|
|
355
|
+
interval: 5,
|
|
356
|
+
}))))
|
|
357
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson(mockTokenResponse))));
|
|
358
|
+
stdout_stderr_1.stdout.start();
|
|
359
|
+
await oauth2_token_1.default.run(['--grantType', 'auto']);
|
|
360
|
+
stdout_stderr_1.stdout.stop();
|
|
361
|
+
expect(stdout_stderr_1.stdout.output).toContain('User code: ABC-123');
|
|
362
|
+
});
|
|
363
|
+
it('should default to client_credentials when no other flow is detected', async () => {
|
|
364
|
+
const contextWithoutFlowUrls = {
|
|
365
|
+
client_id: 'test-client-id',
|
|
366
|
+
client_secret: 'test-client-secret',
|
|
367
|
+
token_url: 'https://oauth.example.com/token',
|
|
368
|
+
};
|
|
369
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify(contextWithoutFlowUrls));
|
|
370
|
+
const startSpy = jest.spyOn(core_1.ux.action, 'start');
|
|
371
|
+
await oauth2_token_1.default.run(['--grantType', 'auto']);
|
|
372
|
+
expect(startSpy).toHaveBeenCalledWith('Requesting OAuth2 token (client_credentials)');
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
describe('Dry Run Mode', () => {
|
|
376
|
+
it('should output token JSON without writing to file in dry run mode', async () => {
|
|
377
|
+
stdout_stderr_1.stdout.start();
|
|
378
|
+
await oauth2_token_1.default.run(['--grantType', 'client_credentials', '--dryRun']);
|
|
379
|
+
stdout_stderr_1.stdout.stop();
|
|
380
|
+
expect(stdout_stderr_1.stdout.output).toContain(mockTokenResponse.access_token);
|
|
381
|
+
expect(stdout_stderr_1.stdout.output).toContain('test-refresh-token');
|
|
382
|
+
expect(mockFs.writeFile).not.toHaveBeenCalled();
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
describe('Error Handling', () => {
|
|
386
|
+
it('should handle HTTP client errors gracefully', async () => {
|
|
387
|
+
const mockError = new Error('Network error');
|
|
388
|
+
const mockExecute = jest
|
|
389
|
+
.fn()
|
|
390
|
+
.mockReturnValue(() => Promise.resolve(E.left(mockError)));
|
|
391
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
392
|
+
const command = new oauth2_token_1.default(['--grantType', 'client_credentials'], {});
|
|
393
|
+
await expect(command.run()).rejects.toThrow();
|
|
394
|
+
});
|
|
395
|
+
it('should handle missing access_token in response', async () => {
|
|
396
|
+
const invalidTokenResponse = { error: 'invalid_grant' };
|
|
397
|
+
const mockExecute = jest.fn().mockReturnValue(() => Promise.resolve(E.right({
|
|
398
|
+
statusCode: 400,
|
|
399
|
+
headers: {},
|
|
400
|
+
body: {},
|
|
401
|
+
})));
|
|
402
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
403
|
+
mockBufferExtensions.readableToArrayBuffer.mockReset();
|
|
404
|
+
mockBufferExtensions.readableToArrayBuffer.mockReturnValue(() => Promise.resolve(E.right(createArrayBufferFromJson(invalidTokenResponse))));
|
|
405
|
+
const command = new oauth2_token_1.default(['--grantType', 'client_credentials'], {});
|
|
406
|
+
await expect(command.run()).rejects.toThrow('No access_token in token response');
|
|
407
|
+
});
|
|
408
|
+
it('should handle file read errors', async () => {
|
|
409
|
+
mockFs.readFile.mockRejectedValue(new Error('File not found'));
|
|
410
|
+
const command = new oauth2_token_1.default(['--ctxPath', 'nonexistent.json'], {});
|
|
411
|
+
await expect(command.run()).rejects.toThrow('File not found');
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
describe('Flag Overrides', () => {
|
|
415
|
+
it('should use flag overrides instead of context values', async () => {
|
|
416
|
+
const mockExecute = jest.fn().mockReturnValue(() => Promise.resolve(E.right({
|
|
417
|
+
statusCode: 200,
|
|
418
|
+
headers: {},
|
|
419
|
+
body: {},
|
|
420
|
+
})));
|
|
421
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
422
|
+
await oauth2_token_1.default.run([
|
|
423
|
+
'--grantType',
|
|
424
|
+
'client_credentials',
|
|
425
|
+
'--clientId',
|
|
426
|
+
'override-client-id',
|
|
427
|
+
'--clientSecret',
|
|
428
|
+
'override-client-secret',
|
|
429
|
+
'--tokenUrl',
|
|
430
|
+
'https://override.example.com/token',
|
|
431
|
+
]);
|
|
432
|
+
const [method, url, request] = mockExecute.mock.calls[0];
|
|
433
|
+
const requestBody = request.body.toString();
|
|
434
|
+
expect(url).toBe('https://override.example.com/token');
|
|
435
|
+
expect(requestBody).toContain('client_id=override-client-id');
|
|
436
|
+
expect(requestBody).toContain('client_secret=override-client-secret');
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
describe('refresh token flow', () => {
|
|
440
|
+
let mockExecute;
|
|
441
|
+
beforeEach(() => {
|
|
442
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify({
|
|
443
|
+
client_id: 'test-client-id',
|
|
444
|
+
client_secret: 'test-client-secret',
|
|
445
|
+
token_url: 'https://example.com/token',
|
|
446
|
+
refresh_token: 'test-refresh-token',
|
|
447
|
+
}));
|
|
448
|
+
// Set up HTTP client mock for refresh tests
|
|
449
|
+
mockExecute = jest.fn();
|
|
450
|
+
const mockHttpInstance = {
|
|
451
|
+
execute: mockExecute,
|
|
452
|
+
};
|
|
453
|
+
mockAxiosHttpClient.mockImplementation(() => mockHttpInstance);
|
|
454
|
+
});
|
|
455
|
+
it('should refresh token successfully', async () => {
|
|
456
|
+
const refreshTokenResponse = {
|
|
457
|
+
access_token: 'new-access-token',
|
|
458
|
+
refresh_token: 'new-refresh-token',
|
|
459
|
+
expires_in: 3600,
|
|
460
|
+
token_type: 'Bearer',
|
|
461
|
+
};
|
|
462
|
+
mockExecute.mockReturnValue(() => Promise.resolve(E.right({
|
|
463
|
+
statusCode: 200,
|
|
464
|
+
headers: {},
|
|
465
|
+
body: {},
|
|
466
|
+
})));
|
|
467
|
+
mockBufferExtensions.readableToArrayBuffer.mockImplementation(() => () => Promise.resolve(E.right(createArrayBufferFromJson(refreshTokenResponse))));
|
|
468
|
+
await oauth2_token_1.default.run(['--refresh']);
|
|
469
|
+
expect(mockExecute).toHaveBeenCalledTimes(1);
|
|
470
|
+
const [method, url, request] = mockExecute.mock.calls[0];
|
|
471
|
+
const requestBody = request.body.toString();
|
|
472
|
+
expect(method).toBe('POST');
|
|
473
|
+
expect(url).toBe('https://example.com/token');
|
|
474
|
+
expect(requestBody).toContain('grant_type=refresh_token');
|
|
475
|
+
expect(requestBody).toContain('refresh_token=test-refresh-token');
|
|
476
|
+
expect(requestBody).toContain('client_id=test-client-id');
|
|
477
|
+
expect(requestBody).toContain('client_secret=test-client-secret');
|
|
478
|
+
expect(mockFs.writeFile).toHaveBeenCalledWith(expect.stringContaining('test.ctx.json'), expect.stringContaining('new-access-token'), 'utf-8');
|
|
479
|
+
});
|
|
480
|
+
it('should fail when no refresh token is found', async () => {
|
|
481
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify({
|
|
482
|
+
client_id: 'test-client-id',
|
|
483
|
+
client_secret: 'test-client-secret',
|
|
484
|
+
token_url: 'https://example.com/token',
|
|
485
|
+
// No refresh_token
|
|
486
|
+
}));
|
|
487
|
+
await expect(oauth2_token_1.default.run(['--refresh'])).rejects.toThrow('No refresh token found in context file');
|
|
488
|
+
});
|
|
489
|
+
it('should fail when token URL is missing', async () => {
|
|
490
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify({
|
|
491
|
+
client_id: 'test-client-id',
|
|
492
|
+
refresh_token: 'test-refresh-token',
|
|
493
|
+
// No token_url
|
|
494
|
+
}));
|
|
495
|
+
await expect(oauth2_token_1.default.run(['--refresh'])).rejects.toThrow('Unable to locate tokenUrl or clientId');
|
|
496
|
+
});
|
|
497
|
+
it('should work with dry run', async () => {
|
|
498
|
+
const dryRunTokenResponse = {
|
|
499
|
+
access_token: 'new-access-token',
|
|
500
|
+
refresh_token: 'new-refresh-token',
|
|
501
|
+
expires_in: 3600,
|
|
502
|
+
token_type: 'Bearer',
|
|
503
|
+
};
|
|
504
|
+
mockExecute.mockReturnValue(() => Promise.resolve(E.right({
|
|
505
|
+
statusCode: 200,
|
|
506
|
+
headers: {},
|
|
507
|
+
body: {},
|
|
508
|
+
})));
|
|
509
|
+
mockBufferExtensions.readableToArrayBuffer.mockImplementation(() => () => Promise.resolve(E.right(createArrayBufferFromJson(dryRunTokenResponse))));
|
|
510
|
+
stdout_stderr_1.stdout.start();
|
|
511
|
+
await oauth2_token_1.default.run(['--refresh', '--dryRun']);
|
|
512
|
+
stdout_stderr_1.stdout.stop();
|
|
513
|
+
expect(mockExecute).toHaveBeenCalledTimes(1);
|
|
514
|
+
expect(mockFs.writeFile).not.toHaveBeenCalled();
|
|
515
|
+
expect(stdout_stderr_1.stdout.output).toContain('new-access-token');
|
|
516
|
+
});
|
|
517
|
+
it('should include optional parameters when provided', async () => {
|
|
518
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify({
|
|
519
|
+
client_id: 'test-client-id',
|
|
520
|
+
client_secret: 'test-client-secret',
|
|
521
|
+
token_url: 'https://example.com/token',
|
|
522
|
+
refresh_token: 'test-refresh-token',
|
|
523
|
+
scope: 'read write',
|
|
524
|
+
audience: 'https://api.example.com',
|
|
525
|
+
}));
|
|
526
|
+
const scopeTokenResponse = {
|
|
527
|
+
access_token: 'new-access-token',
|
|
528
|
+
expires_in: 3600,
|
|
529
|
+
};
|
|
530
|
+
mockExecute.mockReturnValue(() => Promise.resolve(E.right({
|
|
531
|
+
statusCode: 200,
|
|
532
|
+
headers: {},
|
|
533
|
+
body: {},
|
|
534
|
+
})));
|
|
535
|
+
mockBufferExtensions.readableToArrayBuffer.mockImplementation(() => () => Promise.resolve(E.right(createArrayBufferFromJson(scopeTokenResponse))));
|
|
536
|
+
await oauth2_token_1.default.run(['--refresh']);
|
|
537
|
+
const [method, url, request] = mockExecute.mock.calls[0];
|
|
538
|
+
const requestBody = request.body.toString();
|
|
539
|
+
expect(requestBody).toContain('scope=read+write');
|
|
540
|
+
expect(requestBody).toContain('audience=https%3A%2F%2Fapi.example.com');
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
describe('URL utility functions', () => {
|
|
544
|
+
it('should handle invalid URL in isLocalhostRedirect', async () => {
|
|
545
|
+
// Test the error case where URL parsing fails
|
|
546
|
+
const contextWithInvalidRedirect = {
|
|
547
|
+
...mockContext,
|
|
548
|
+
redirect_uri: 'not-a-valid-url',
|
|
549
|
+
};
|
|
550
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify(contextWithInvalidRedirect));
|
|
551
|
+
// Mock inquirer to handle non-localhost redirect
|
|
552
|
+
mockInquirer.prompt.mockResolvedValue({
|
|
553
|
+
pastedUrl: 'https://example.com/callback?code=test-code&state=bW9jay1yYW5kb20tYnl0ZXMtZm9yLXRlc3RpbmctcHVycG9zZXM',
|
|
554
|
+
});
|
|
555
|
+
stdout_stderr_1.stdout.start();
|
|
556
|
+
await oauth2_token_1.default.run(['--grantType', 'authorization_code']);
|
|
557
|
+
stdout_stderr_1.stdout.stop();
|
|
558
|
+
// Invalid URL should trigger manual paste flow
|
|
559
|
+
expect(mockInquirer.prompt).toHaveBeenCalled();
|
|
560
|
+
expect(stdout_stderr_1.stdout.output).toContain('Redirect URI is not localhost');
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
describe('Browser utility functions', () => {
|
|
564
|
+
it('should handle Windows platform browser opening', async () => {
|
|
565
|
+
const originalPlatform = process.platform;
|
|
566
|
+
Object.defineProperty(process, 'platform', {
|
|
567
|
+
value: 'win32',
|
|
568
|
+
writable: true,
|
|
569
|
+
});
|
|
570
|
+
const contextWithDeviceCode = {
|
|
571
|
+
...mockContext,
|
|
572
|
+
device_code_url: 'https://oauth.example.com/device',
|
|
573
|
+
};
|
|
574
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify(contextWithDeviceCode));
|
|
575
|
+
const mockExecute = jest
|
|
576
|
+
.fn()
|
|
577
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
578
|
+
statusCode: 200,
|
|
579
|
+
headers: {},
|
|
580
|
+
body: {},
|
|
581
|
+
})))
|
|
582
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
583
|
+
statusCode: 200,
|
|
584
|
+
headers: {},
|
|
585
|
+
body: {},
|
|
586
|
+
})));
|
|
587
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
588
|
+
mockBufferExtensions.readableToArrayBuffer.mockReset();
|
|
589
|
+
mockBufferExtensions.readableToArrayBuffer
|
|
590
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson({
|
|
591
|
+
device_code: 'test-device-code',
|
|
592
|
+
user_code: 'ABC-123',
|
|
593
|
+
verification_uri: 'https://oauth.example.com/device',
|
|
594
|
+
verification_uri_complete: 'https://oauth.example.com/device?user_code=ABC-123',
|
|
595
|
+
expires_in: 900,
|
|
596
|
+
interval: 5,
|
|
597
|
+
}))))
|
|
598
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson(mockTokenResponse))));
|
|
599
|
+
await oauth2_token_1.default.run(['--grantType', 'device_code', '--openBrowser']);
|
|
600
|
+
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('start'));
|
|
601
|
+
Object.defineProperty(process, 'platform', {
|
|
602
|
+
value: originalPlatform,
|
|
603
|
+
writable: true,
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
it('should handle Linux platform browser opening', async () => {
|
|
607
|
+
const originalPlatform = process.platform;
|
|
608
|
+
Object.defineProperty(process, 'platform', {
|
|
609
|
+
value: 'linux',
|
|
610
|
+
writable: true,
|
|
611
|
+
});
|
|
612
|
+
const contextWithDeviceCode = {
|
|
613
|
+
...mockContext,
|
|
614
|
+
device_code_url: 'https://oauth.example.com/device',
|
|
615
|
+
};
|
|
616
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify(contextWithDeviceCode));
|
|
617
|
+
const mockExecute = jest
|
|
618
|
+
.fn()
|
|
619
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
620
|
+
statusCode: 200,
|
|
621
|
+
headers: {},
|
|
622
|
+
body: {},
|
|
623
|
+
})))
|
|
624
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
625
|
+
statusCode: 200,
|
|
626
|
+
headers: {},
|
|
627
|
+
body: {},
|
|
628
|
+
})));
|
|
629
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
630
|
+
mockBufferExtensions.readableToArrayBuffer.mockReset();
|
|
631
|
+
mockBufferExtensions.readableToArrayBuffer
|
|
632
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson({
|
|
633
|
+
device_code: 'test-device-code',
|
|
634
|
+
user_code: 'ABC-123',
|
|
635
|
+
verification_uri: 'https://oauth.example.com/device',
|
|
636
|
+
verification_uri_complete: 'https://oauth.example.com/device?user_code=ABC-123',
|
|
637
|
+
expires_in: 900,
|
|
638
|
+
interval: 5,
|
|
639
|
+
}))))
|
|
640
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson(mockTokenResponse))));
|
|
641
|
+
await oauth2_token_1.default.run(['--grantType', 'device_code', '--openBrowser']);
|
|
642
|
+
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('xdg-open'));
|
|
643
|
+
Object.defineProperty(process, 'platform', {
|
|
644
|
+
value: originalPlatform,
|
|
645
|
+
writable: true,
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
describe('Environment variable error handling', () => {
|
|
650
|
+
it('should handle .env file read errors gracefully', async () => {
|
|
651
|
+
// Mock dotenv to throw an error
|
|
652
|
+
mockDotenv.config.mockReturnValue({
|
|
653
|
+
parsed: {},
|
|
654
|
+
error: new Error('Permission denied'),
|
|
655
|
+
});
|
|
656
|
+
// Should still work with context file
|
|
657
|
+
await oauth2_token_1.default.run(['--grantType', 'client_credentials']);
|
|
658
|
+
expect(mockAxiosHttpClient).toHaveBeenCalled();
|
|
659
|
+
});
|
|
660
|
+
it('should return empty config when .env parse fails', async () => {
|
|
661
|
+
mockDotenv.config.mockReturnValue({
|
|
662
|
+
parsed: undefined,
|
|
663
|
+
error: new Error('Parse error'),
|
|
664
|
+
});
|
|
665
|
+
await oauth2_token_1.default.run(['--grantType', 'client_credentials']);
|
|
666
|
+
expect(mockAxiosHttpClient).toHaveBeenCalled();
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
describe('Device Code Flow Error Handling', () => {
|
|
670
|
+
beforeEach(() => {
|
|
671
|
+
const contextWithDeviceCode = {
|
|
672
|
+
...mockContext,
|
|
673
|
+
device_code_url: 'https://oauth.example.com/device',
|
|
674
|
+
};
|
|
675
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify(contextWithDeviceCode));
|
|
676
|
+
});
|
|
677
|
+
it('should handle missing required parameters in device code flow', async () => {
|
|
678
|
+
const invalidContext = {
|
|
679
|
+
client_id: 'test-client-id',
|
|
680
|
+
token_url: 'https://oauth.example.com/token',
|
|
681
|
+
// Missing device_code_url
|
|
682
|
+
};
|
|
683
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify(invalidContext));
|
|
684
|
+
await expect(oauth2_token_1.default.run(['--grantType', 'device_code'])).rejects.toThrow('Missing required parameters: tokenUrl, clientId, and deviceCodeUrl are required for device_code flow');
|
|
685
|
+
});
|
|
686
|
+
it('should handle HTTP client error during device initialization', async () => {
|
|
687
|
+
const mockError = new Error('Network error');
|
|
688
|
+
const mockExecute = jest
|
|
689
|
+
.fn()
|
|
690
|
+
.mockReturnValue(() => Promise.resolve(E.left(mockError)));
|
|
691
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
692
|
+
await expect(oauth2_token_1.default.run(['--grantType', 'device_code'])).rejects.toThrow('Network error');
|
|
693
|
+
});
|
|
694
|
+
it('should handle buffer read error during device initialization', async () => {
|
|
695
|
+
const mockExecute = jest.fn().mockReturnValue(() => Promise.resolve(E.right({
|
|
696
|
+
statusCode: 200,
|
|
697
|
+
headers: {},
|
|
698
|
+
body: {},
|
|
699
|
+
})));
|
|
700
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
701
|
+
const bufferError = new Error('Buffer read failed');
|
|
702
|
+
mockBufferExtensions.readableToArrayBuffer.mockReset();
|
|
703
|
+
mockBufferExtensions.readableToArrayBuffer.mockReturnValue(() => Promise.resolve(E.left(bufferError)));
|
|
704
|
+
await expect(oauth2_token_1.default.run(['--grantType', 'device_code'])).rejects.toThrow('Buffer read failed');
|
|
705
|
+
});
|
|
706
|
+
it('should handle HTTP client error during device code polling', async () => {
|
|
707
|
+
const mockExecute = jest
|
|
708
|
+
.fn()
|
|
709
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
710
|
+
statusCode: 200,
|
|
711
|
+
headers: {},
|
|
712
|
+
body: {},
|
|
713
|
+
})))
|
|
714
|
+
.mockReturnValueOnce(() => Promise.resolve(E.left(new Error('Network error during poll'))));
|
|
715
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
716
|
+
mockBufferExtensions.readableToArrayBuffer.mockReset();
|
|
717
|
+
mockBufferExtensions.readableToArrayBuffer.mockReturnValue(() => Promise.resolve(E.right(createArrayBufferFromJson({
|
|
718
|
+
device_code: 'test-device-code',
|
|
719
|
+
user_code: 'ABC-123',
|
|
720
|
+
verification_uri: 'https://oauth.example.com/device',
|
|
721
|
+
expires_in: 900,
|
|
722
|
+
interval: 1,
|
|
723
|
+
}))));
|
|
724
|
+
await expect(oauth2_token_1.default.run(['--grantType', 'device_code'])).rejects.toThrow('Network error during poll');
|
|
725
|
+
});
|
|
726
|
+
it('should handle slow_down error from device code poll', async () => {
|
|
727
|
+
const mockExecute = jest
|
|
728
|
+
.fn()
|
|
729
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
730
|
+
statusCode: 200,
|
|
731
|
+
headers: {},
|
|
732
|
+
body: {},
|
|
733
|
+
})))
|
|
734
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
735
|
+
statusCode: 400,
|
|
736
|
+
headers: {},
|
|
737
|
+
body: {},
|
|
738
|
+
})))
|
|
739
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
740
|
+
statusCode: 200,
|
|
741
|
+
headers: {},
|
|
742
|
+
body: {},
|
|
743
|
+
})));
|
|
744
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
745
|
+
mockBufferExtensions.readableToArrayBuffer.mockReset();
|
|
746
|
+
mockBufferExtensions.readableToArrayBuffer
|
|
747
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson({
|
|
748
|
+
device_code: 'test-device-code',
|
|
749
|
+
user_code: 'ABC-123',
|
|
750
|
+
verification_uri: 'https://oauth.example.com/device',
|
|
751
|
+
expires_in: 900,
|
|
752
|
+
interval: 1,
|
|
753
|
+
}))))
|
|
754
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson({
|
|
755
|
+
error: 'slow_down',
|
|
756
|
+
}))))
|
|
757
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson(mockTokenResponse))));
|
|
758
|
+
await oauth2_token_1.default.run(['--grantType', 'device_code']);
|
|
759
|
+
expect(mockExecute).toHaveBeenCalledTimes(3);
|
|
760
|
+
});
|
|
761
|
+
it('should handle access_denied error from device code poll', async () => {
|
|
762
|
+
const mockExecute = jest
|
|
763
|
+
.fn()
|
|
764
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
765
|
+
statusCode: 200,
|
|
766
|
+
headers: {},
|
|
767
|
+
body: {},
|
|
768
|
+
})))
|
|
769
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
770
|
+
statusCode: 403,
|
|
771
|
+
headers: {},
|
|
772
|
+
body: {},
|
|
773
|
+
})));
|
|
774
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
775
|
+
mockBufferExtensions.readableToArrayBuffer.mockReset();
|
|
776
|
+
mockBufferExtensions.readableToArrayBuffer
|
|
777
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson({
|
|
778
|
+
device_code: 'test-device-code',
|
|
779
|
+
user_code: 'ABC-123',
|
|
780
|
+
verification_uri: 'https://oauth.example.com/device',
|
|
781
|
+
expires_in: 900,
|
|
782
|
+
interval: 1,
|
|
783
|
+
}))))
|
|
784
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson({
|
|
785
|
+
error: 'access_denied',
|
|
786
|
+
}))));
|
|
787
|
+
await expect(oauth2_token_1.default.run(['--grantType', 'device_code'])).rejects.toThrow('User denied the request');
|
|
788
|
+
});
|
|
789
|
+
it('should handle expired_token error from device code poll', async () => {
|
|
790
|
+
const mockExecute = jest
|
|
791
|
+
.fn()
|
|
792
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
793
|
+
statusCode: 200,
|
|
794
|
+
headers: {},
|
|
795
|
+
body: {},
|
|
796
|
+
})))
|
|
797
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
798
|
+
statusCode: 400,
|
|
799
|
+
headers: {},
|
|
800
|
+
body: {},
|
|
801
|
+
})));
|
|
802
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
803
|
+
mockBufferExtensions.readableToArrayBuffer.mockReset();
|
|
804
|
+
mockBufferExtensions.readableToArrayBuffer
|
|
805
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson({
|
|
806
|
+
device_code: 'test-device-code',
|
|
807
|
+
user_code: 'ABC-123',
|
|
808
|
+
verification_uri: 'https://oauth.example.com/device',
|
|
809
|
+
expires_in: 900,
|
|
810
|
+
interval: 1,
|
|
811
|
+
}))))
|
|
812
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson({
|
|
813
|
+
error: 'expired_token',
|
|
814
|
+
}))));
|
|
815
|
+
await expect(oauth2_token_1.default.run(['--grantType', 'device_code'])).rejects.toThrow('The device code has expired');
|
|
816
|
+
});
|
|
817
|
+
it('should handle expired error from device code poll', async () => {
|
|
818
|
+
const mockExecute = jest
|
|
819
|
+
.fn()
|
|
820
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
821
|
+
statusCode: 200,
|
|
822
|
+
headers: {},
|
|
823
|
+
body: {},
|
|
824
|
+
})))
|
|
825
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
826
|
+
statusCode: 400,
|
|
827
|
+
headers: {},
|
|
828
|
+
body: {},
|
|
829
|
+
})));
|
|
830
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
831
|
+
mockBufferExtensions.readableToArrayBuffer.mockReset();
|
|
832
|
+
mockBufferExtensions.readableToArrayBuffer
|
|
833
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson({
|
|
834
|
+
device_code: 'test-device-code',
|
|
835
|
+
user_code: 'ABC-123',
|
|
836
|
+
verification_uri: 'https://oauth.example.com/device',
|
|
837
|
+
expires_in: 900,
|
|
838
|
+
interval: 1,
|
|
839
|
+
}))))
|
|
840
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson({
|
|
841
|
+
error: 'expired',
|
|
842
|
+
}))));
|
|
843
|
+
await expect(oauth2_token_1.default.run(['--grantType', 'device_code'])).rejects.toThrow('The device code has expired');
|
|
844
|
+
});
|
|
845
|
+
it('should handle unknown error from device code poll', async () => {
|
|
846
|
+
const mockExecute = jest
|
|
847
|
+
.fn()
|
|
848
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
849
|
+
statusCode: 200,
|
|
850
|
+
headers: {},
|
|
851
|
+
body: {},
|
|
852
|
+
})))
|
|
853
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
854
|
+
statusCode: 500,
|
|
855
|
+
headers: {},
|
|
856
|
+
body: {},
|
|
857
|
+
})))
|
|
858
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
859
|
+
statusCode: 200,
|
|
860
|
+
headers: {},
|
|
861
|
+
body: {},
|
|
862
|
+
})));
|
|
863
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
864
|
+
mockBufferExtensions.readableToArrayBuffer.mockReset();
|
|
865
|
+
mockBufferExtensions.readableToArrayBuffer
|
|
866
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson({
|
|
867
|
+
device_code: 'test-device-code',
|
|
868
|
+
user_code: 'ABC-123',
|
|
869
|
+
verification_uri: 'https://oauth.example.com/device',
|
|
870
|
+
expires_in: 900,
|
|
871
|
+
interval: 1,
|
|
872
|
+
}))))
|
|
873
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson({
|
|
874
|
+
error: 'unknown_error',
|
|
875
|
+
}))))
|
|
876
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson(mockTokenResponse))));
|
|
877
|
+
await oauth2_token_1.default.run(['--grantType', 'device_code']);
|
|
878
|
+
expect(mockExecute).toHaveBeenCalledTimes(3);
|
|
879
|
+
});
|
|
880
|
+
it('should handle device code timeout', async () => {
|
|
881
|
+
const mockExecute = jest.fn().mockReturnValue(() => Promise.resolve(E.right({
|
|
882
|
+
statusCode: 400,
|
|
883
|
+
headers: {},
|
|
884
|
+
body: {},
|
|
885
|
+
})));
|
|
886
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
887
|
+
mockBufferExtensions.readableToArrayBuffer.mockReset();
|
|
888
|
+
mockBufferExtensions.readableToArrayBuffer
|
|
889
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson({
|
|
890
|
+
device_code: 'test-device-code',
|
|
891
|
+
user_code: 'ABC-123',
|
|
892
|
+
verification_uri: 'https://oauth.example.com/device',
|
|
893
|
+
expires_in: 1,
|
|
894
|
+
interval: 1,
|
|
895
|
+
}))))
|
|
896
|
+
.mockReturnValue(() => Promise.resolve(E.right(createArrayBufferFromJson({
|
|
897
|
+
error: 'authorization_pending',
|
|
898
|
+
}))));
|
|
899
|
+
await expect(oauth2_token_1.default.run(['--grantType', 'device_code'])).rejects.toThrow('Timed out waiting for device authorization');
|
|
900
|
+
});
|
|
901
|
+
it('should handle buffer read error during device code polling', async () => {
|
|
902
|
+
const mockExecute = jest
|
|
903
|
+
.fn()
|
|
904
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
905
|
+
statusCode: 200,
|
|
906
|
+
headers: {},
|
|
907
|
+
body: {},
|
|
908
|
+
})))
|
|
909
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right({
|
|
910
|
+
statusCode: 200,
|
|
911
|
+
headers: {},
|
|
912
|
+
body: {},
|
|
913
|
+
})));
|
|
914
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
915
|
+
const bufferError = new Error('Buffer read failed');
|
|
916
|
+
mockBufferExtensions.readableToArrayBuffer.mockReset();
|
|
917
|
+
mockBufferExtensions.readableToArrayBuffer
|
|
918
|
+
.mockReturnValueOnce(() => Promise.resolve(E.right(createArrayBufferFromJson({
|
|
919
|
+
device_code: 'test-device-code',
|
|
920
|
+
user_code: 'ABC-123',
|
|
921
|
+
verification_uri: 'https://oauth.example.com/device',
|
|
922
|
+
expires_in: 900,
|
|
923
|
+
interval: 1,
|
|
924
|
+
}))))
|
|
925
|
+
.mockReturnValueOnce(() => Promise.resolve(E.left(bufferError)));
|
|
926
|
+
await expect(oauth2_token_1.default.run(['--grantType', 'device_code'])).rejects.toThrow('Buffer read failed');
|
|
927
|
+
});
|
|
928
|
+
});
|
|
929
|
+
describe('Authorization Code Flow Error Handling', () => {
|
|
930
|
+
beforeEach(() => {
|
|
931
|
+
// Mock HTTP server for callback
|
|
932
|
+
const mockServer = {
|
|
933
|
+
listen: jest
|
|
934
|
+
.fn()
|
|
935
|
+
.mockImplementation((port, host, callback) => callback?.()),
|
|
936
|
+
close: jest.fn().mockImplementation((callback) => callback?.()),
|
|
937
|
+
};
|
|
938
|
+
mockHttp.createServer.mockImplementation((requestHandler) => {
|
|
939
|
+
setTimeout(() => {
|
|
940
|
+
const mockReq = {
|
|
941
|
+
url: '/callback?code=test-code&state=bW9jay1yYW5kb20tYnl0ZXMtZm9yLXRlc3RpbmctcHVycG9zZXM',
|
|
942
|
+
};
|
|
943
|
+
const mockRes = { writeHead: jest.fn(), end: jest.fn() };
|
|
944
|
+
requestHandler?.(mockReq, mockRes);
|
|
945
|
+
}, 100);
|
|
946
|
+
return mockServer;
|
|
947
|
+
});
|
|
948
|
+
});
|
|
949
|
+
it('should handle buffer read error during token exchange', async () => {
|
|
950
|
+
const mockExecute = jest.fn().mockReturnValue(() => Promise.resolve(E.right({
|
|
951
|
+
statusCode: 200,
|
|
952
|
+
headers: {},
|
|
953
|
+
body: {},
|
|
954
|
+
})));
|
|
955
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
956
|
+
const bufferError = new Error('Buffer read failed');
|
|
957
|
+
mockBufferExtensions.readableToArrayBuffer.mockReset();
|
|
958
|
+
mockBufferExtensions.readableToArrayBuffer.mockReturnValue(() => Promise.resolve(E.left(bufferError)));
|
|
959
|
+
await expect(oauth2_token_1.default.run(['--grantType', 'authorization_code'])).rejects.toThrow('Buffer read failed');
|
|
960
|
+
});
|
|
961
|
+
it('should handle missing access token in authorization code response', async () => {
|
|
962
|
+
const mockExecute = jest.fn().mockReturnValue(() => Promise.resolve(E.right({
|
|
963
|
+
statusCode: 200,
|
|
964
|
+
headers: {},
|
|
965
|
+
body: {},
|
|
966
|
+
})));
|
|
967
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
968
|
+
mockBufferExtensions.readableToArrayBuffer.mockReset();
|
|
969
|
+
mockBufferExtensions.readableToArrayBuffer.mockReturnValue(() => Promise.resolve(E.right(createArrayBufferFromJson({
|
|
970
|
+
error: 'invalid_grant',
|
|
971
|
+
}))));
|
|
972
|
+
await expect(oauth2_token_1.default.run(['--grantType', 'authorization_code'])).rejects.toThrow('No access_token in token response');
|
|
973
|
+
});
|
|
974
|
+
it('should handle missing tokenUrl in authorization code flow', async () => {
|
|
975
|
+
const invalidContext = {
|
|
976
|
+
client_id: 'test-client-id',
|
|
977
|
+
authorize_url: 'https://oauth.example.com/authorize',
|
|
978
|
+
// Missing token_url
|
|
979
|
+
};
|
|
980
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify(invalidContext));
|
|
981
|
+
const mockServer = {
|
|
982
|
+
listen: jest
|
|
983
|
+
.fn()
|
|
984
|
+
.mockImplementation((port, host, callback) => callback?.()),
|
|
985
|
+
close: jest.fn().mockImplementation((callback) => callback?.()),
|
|
986
|
+
};
|
|
987
|
+
mockHttp.createServer.mockImplementation((requestHandler) => {
|
|
988
|
+
setTimeout(() => {
|
|
989
|
+
const mockReq = {
|
|
990
|
+
url: '/callback?code=test-code&state=bW9jay1yYW5kb20tYnl0ZXMtZm9yLXRlc3RpbmctcHVycG9zZXM',
|
|
991
|
+
};
|
|
992
|
+
const mockRes = { writeHead: jest.fn(), end: jest.fn() };
|
|
993
|
+
requestHandler?.(mockReq, mockRes);
|
|
994
|
+
}, 100);
|
|
995
|
+
return mockServer;
|
|
996
|
+
});
|
|
997
|
+
await expect(oauth2_token_1.default.run(['--grantType', 'authorization_code'])).rejects.toThrow('Missing required parameters: tokenUrl, clientId, and authorizeUrl are required for authorization_code flow');
|
|
998
|
+
});
|
|
999
|
+
it('should handle invalid tokenUrl in authorization code flow', async () => {
|
|
1000
|
+
const invalidContext = {
|
|
1001
|
+
...mockContext,
|
|
1002
|
+
token_url: 'not-a-valid-url',
|
|
1003
|
+
};
|
|
1004
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify(invalidContext));
|
|
1005
|
+
const mockServer = {
|
|
1006
|
+
listen: jest
|
|
1007
|
+
.fn()
|
|
1008
|
+
.mockImplementation((port, host, callback) => callback?.()),
|
|
1009
|
+
close: jest.fn().mockImplementation((callback) => callback?.()),
|
|
1010
|
+
};
|
|
1011
|
+
mockHttp.createServer.mockImplementation((requestHandler) => {
|
|
1012
|
+
setTimeout(() => {
|
|
1013
|
+
const mockReq = {
|
|
1014
|
+
url: '/callback?code=test-code&state=bW9jay1yYW5kb20tYnl0ZXMtZm9yLXRlc3RpbmctcHVycG9zZXM',
|
|
1015
|
+
};
|
|
1016
|
+
const mockRes = { writeHead: jest.fn(), end: jest.fn() };
|
|
1017
|
+
requestHandler?.(mockReq, mockRes);
|
|
1018
|
+
}, 100);
|
|
1019
|
+
return mockServer;
|
|
1020
|
+
});
|
|
1021
|
+
await expect(oauth2_token_1.default.run(['--grantType', 'authorization_code'])).rejects.toThrow('Invalid tokenUrl');
|
|
1022
|
+
});
|
|
1023
|
+
it('should handle invalid authorizeUrl in authorization code flow', async () => {
|
|
1024
|
+
const invalidContext = {
|
|
1025
|
+
...mockContext,
|
|
1026
|
+
authorize_url: 'not-a-valid-url',
|
|
1027
|
+
};
|
|
1028
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify(invalidContext));
|
|
1029
|
+
const mockServer = {
|
|
1030
|
+
listen: jest
|
|
1031
|
+
.fn()
|
|
1032
|
+
.mockImplementation((port, host, callback) => callback?.()),
|
|
1033
|
+
close: jest.fn().mockImplementation((callback) => callback?.()),
|
|
1034
|
+
};
|
|
1035
|
+
mockHttp.createServer.mockImplementation((requestHandler) => {
|
|
1036
|
+
setTimeout(() => {
|
|
1037
|
+
const mockReq = {
|
|
1038
|
+
url: '/callback?code=test-code&state=bW9jay1yYW5kb20tYnl0ZXMtZm9yLXRlc3RpbmctcHVycG9zZXM',
|
|
1039
|
+
};
|
|
1040
|
+
const mockRes = { writeHead: jest.fn(), end: jest.fn() };
|
|
1041
|
+
requestHandler?.(mockReq, mockRes);
|
|
1042
|
+
}, 100);
|
|
1043
|
+
return mockServer;
|
|
1044
|
+
});
|
|
1045
|
+
await expect(oauth2_token_1.default.run(['--grantType', 'authorization_code'])).rejects.toThrow('Invalid authorizeUrl');
|
|
1046
|
+
});
|
|
1047
|
+
it('should handle state mismatch error in authorization code callback', async () => {
|
|
1048
|
+
const mockServer = {
|
|
1049
|
+
listen: jest
|
|
1050
|
+
.fn()
|
|
1051
|
+
.mockImplementation((port, host, callback) => callback?.()),
|
|
1052
|
+
close: jest.fn().mockImplementation((callback) => callback?.()),
|
|
1053
|
+
};
|
|
1054
|
+
mockHttp.createServer.mockImplementation((requestHandler) => {
|
|
1055
|
+
setTimeout(() => {
|
|
1056
|
+
const mockReq = {
|
|
1057
|
+
url: '/callback?code=test-code&state=wrong-state',
|
|
1058
|
+
};
|
|
1059
|
+
const mockRes = { writeHead: jest.fn(), end: jest.fn() };
|
|
1060
|
+
requestHandler?.(mockReq, mockRes);
|
|
1061
|
+
}, 100);
|
|
1062
|
+
return mockServer;
|
|
1063
|
+
});
|
|
1064
|
+
await expect(oauth2_token_1.default.run(['--grantType', 'authorization_code'])).rejects.toThrow('State mismatch');
|
|
1065
|
+
});
|
|
1066
|
+
it('should handle HTTP client error during token exchange', async () => {
|
|
1067
|
+
const mockError = new Error('Network error');
|
|
1068
|
+
const mockExecute = jest
|
|
1069
|
+
.fn()
|
|
1070
|
+
.mockReturnValue(() => Promise.resolve(E.left(mockError)));
|
|
1071
|
+
mockAxiosHttpClient.mockImplementation(() => ({ execute: mockExecute }));
|
|
1072
|
+
await expect(oauth2_token_1.default.run(['--grantType', 'authorization_code'])).rejects.toThrow('Network error');
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
describe('Environment variable support', () => {
|
|
1076
|
+
let mockExecute;
|
|
1077
|
+
beforeEach(() => {
|
|
1078
|
+
// Set up HTTP client mock for env tests
|
|
1079
|
+
mockExecute = jest.fn();
|
|
1080
|
+
const mockHttpInstance = {
|
|
1081
|
+
execute: mockExecute,
|
|
1082
|
+
};
|
|
1083
|
+
mockAxiosHttpClient.mockImplementation(() => mockHttpInstance);
|
|
1084
|
+
mockExecute.mockReturnValue(() => Promise.resolve(E.right({
|
|
1085
|
+
statusCode: 200,
|
|
1086
|
+
headers: {},
|
|
1087
|
+
body: {},
|
|
1088
|
+
})));
|
|
1089
|
+
mockBufferExtensions.readableToArrayBuffer.mockImplementation(() => () => Promise.resolve(E.right(createArrayBufferFromJson(mockTokenResponse))));
|
|
1090
|
+
});
|
|
1091
|
+
it('should read credentials from .env file with OAUTH2_ prefix', async () => {
|
|
1092
|
+
// Mock context file without sensitive data
|
|
1093
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify({
|
|
1094
|
+
token_url: 'https://example.com/token',
|
|
1095
|
+
}));
|
|
1096
|
+
// Mock .env file with OAUTH2_ prefixed variables
|
|
1097
|
+
mockDotenv.config.mockReturnValue({
|
|
1098
|
+
parsed: {
|
|
1099
|
+
OAUTH2_CLIENT_ID: 'env-client-id',
|
|
1100
|
+
OAUTH2_CLIENT_SECRET: 'env-client-secret',
|
|
1101
|
+
OAUTH2_SCOPE: 'read write',
|
|
1102
|
+
OAUTH2_AUDIENCE: 'https://api.example.com',
|
|
1103
|
+
},
|
|
1104
|
+
error: undefined,
|
|
1105
|
+
});
|
|
1106
|
+
await oauth2_token_1.default.run(['--grantType', 'client_credentials']);
|
|
1107
|
+
expect(mockExecute).toHaveBeenCalledTimes(1);
|
|
1108
|
+
const [method, url, request] = mockExecute.mock.calls[0];
|
|
1109
|
+
const requestBody = request.body.toString();
|
|
1110
|
+
expect(requestBody).toContain('client_id=env-client-id');
|
|
1111
|
+
expect(requestBody).toContain('client_secret=env-client-secret');
|
|
1112
|
+
expect(requestBody).toContain('scope=read+write');
|
|
1113
|
+
expect(requestBody).toContain('audience=https%3A%2F%2Fapi.example.com');
|
|
1114
|
+
});
|
|
1115
|
+
it('should read credentials from .env file without prefix', async () => {
|
|
1116
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify({
|
|
1117
|
+
token_url: 'https://example.com/token',
|
|
1118
|
+
}));
|
|
1119
|
+
// Mock .env file with unprefixed variables
|
|
1120
|
+
mockDotenv.config.mockReturnValue({
|
|
1121
|
+
parsed: {
|
|
1122
|
+
CLIENT_ID: 'env-client-id-no-prefix',
|
|
1123
|
+
CLIENT_SECRET: 'env-client-secret-no-prefix',
|
|
1124
|
+
},
|
|
1125
|
+
error: undefined,
|
|
1126
|
+
});
|
|
1127
|
+
await oauth2_token_1.default.run(['--grantType', 'client_credentials']);
|
|
1128
|
+
const [method, url, request] = mockExecute.mock.calls[0];
|
|
1129
|
+
const requestBody = request.body.toString();
|
|
1130
|
+
expect(requestBody).toContain('client_id=env-client-id-no-prefix');
|
|
1131
|
+
expect(requestBody).toContain('client_secret=env-client-secret-no-prefix');
|
|
1132
|
+
});
|
|
1133
|
+
it('should prioritize OAUTH2_ prefix over unprefixed variables', async () => {
|
|
1134
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify({
|
|
1135
|
+
token_url: 'https://example.com/token',
|
|
1136
|
+
}));
|
|
1137
|
+
mockDotenv.config.mockReturnValue({
|
|
1138
|
+
parsed: {
|
|
1139
|
+
OAUTH2_CLIENT_ID: 'prefixed-client-id',
|
|
1140
|
+
CLIENT_ID: 'unprefixed-client-id',
|
|
1141
|
+
OAUTH2_CLIENT_SECRET: 'prefixed-secret',
|
|
1142
|
+
CLIENT_SECRET: 'unprefixed-secret',
|
|
1143
|
+
},
|
|
1144
|
+
error: undefined,
|
|
1145
|
+
});
|
|
1146
|
+
await oauth2_token_1.default.run(['--grantType', 'client_credentials']);
|
|
1147
|
+
const [method, url, request] = mockExecute.mock.calls[0];
|
|
1148
|
+
const requestBody = request.body.toString();
|
|
1149
|
+
expect(requestBody).toContain('client_id=prefixed-client-id');
|
|
1150
|
+
expect(requestBody).toContain('client_secret=prefixed-secret');
|
|
1151
|
+
});
|
|
1152
|
+
it('should use correct precedence: flags > env > context', async () => {
|
|
1153
|
+
// Context file has some values
|
|
1154
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify({
|
|
1155
|
+
token_url: 'https://example.com/token',
|
|
1156
|
+
client_id: 'context-client-id',
|
|
1157
|
+
client_secret: 'context-client-secret',
|
|
1158
|
+
}));
|
|
1159
|
+
// .env file has some values
|
|
1160
|
+
mockDotenv.config.mockReturnValue({
|
|
1161
|
+
parsed: {
|
|
1162
|
+
OAUTH2_CLIENT_ID: 'env-client-id',
|
|
1163
|
+
OAUTH2_SCOPE: 'env-scope',
|
|
1164
|
+
},
|
|
1165
|
+
error: undefined,
|
|
1166
|
+
});
|
|
1167
|
+
// Flag overrides everything
|
|
1168
|
+
await oauth2_token_1.default.run([
|
|
1169
|
+
'--grantType',
|
|
1170
|
+
'client_credentials',
|
|
1171
|
+
'--clientId',
|
|
1172
|
+
'flag-client-id',
|
|
1173
|
+
]);
|
|
1174
|
+
const [method, url, request] = mockExecute.mock.calls[0];
|
|
1175
|
+
const requestBody = request.body.toString();
|
|
1176
|
+
// Flag takes precedence over env and context
|
|
1177
|
+
expect(requestBody).toContain('client_id=flag-client-id');
|
|
1178
|
+
// Env takes precedence over context
|
|
1179
|
+
expect(requestBody).toContain('scope=env-scope');
|
|
1180
|
+
// Context used when not in flag or env
|
|
1181
|
+
expect(requestBody).toContain('client_secret=context-client-secret');
|
|
1182
|
+
});
|
|
1183
|
+
it('should work with refresh token from .env file', async () => {
|
|
1184
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify({
|
|
1185
|
+
token_url: 'https://example.com/token',
|
|
1186
|
+
client_id: 'test-client-id',
|
|
1187
|
+
}));
|
|
1188
|
+
mockDotenv.config.mockReturnValue({
|
|
1189
|
+
parsed: {
|
|
1190
|
+
OAUTH2_REFRESH_TOKEN: 'env-refresh-token',
|
|
1191
|
+
OAUTH2_CLIENT_SECRET: 'env-client-secret',
|
|
1192
|
+
},
|
|
1193
|
+
error: undefined,
|
|
1194
|
+
});
|
|
1195
|
+
await oauth2_token_1.default.run(['--refresh']);
|
|
1196
|
+
const [method, url, request] = mockExecute.mock.calls[0];
|
|
1197
|
+
const requestBody = request.body.toString();
|
|
1198
|
+
expect(requestBody).toContain('grant_type=refresh_token');
|
|
1199
|
+
expect(requestBody).toContain('refresh_token=env-refresh-token');
|
|
1200
|
+
expect(requestBody).toContain('client_secret=env-client-secret');
|
|
1201
|
+
});
|
|
1202
|
+
it('should work when .env file does not exist', async () => {
|
|
1203
|
+
// Mock .env file not existing
|
|
1204
|
+
mockFs.access.mockRejectedValue(new Error('File not found'));
|
|
1205
|
+
mockDotenv.config.mockReturnValue({
|
|
1206
|
+
parsed: {},
|
|
1207
|
+
error: new Error('File not found'),
|
|
1208
|
+
});
|
|
1209
|
+
await oauth2_token_1.default.run(['--grantType', 'client_credentials']);
|
|
1210
|
+
// Should still work with context file values
|
|
1211
|
+
expect(mockExecute).toHaveBeenCalledTimes(1);
|
|
1212
|
+
});
|
|
1213
|
+
it('should use custom .env path when specified', async () => {
|
|
1214
|
+
await oauth2_token_1.default.run([
|
|
1215
|
+
'--grantType',
|
|
1216
|
+
'client_credentials',
|
|
1217
|
+
'--envPath',
|
|
1218
|
+
'custom/.env',
|
|
1219
|
+
]);
|
|
1220
|
+
expect(mockDotenv.config).toHaveBeenCalledWith({
|
|
1221
|
+
path: expect.stringContaining('custom/.env'),
|
|
1222
|
+
});
|
|
1223
|
+
});
|
|
1224
|
+
it('should read all OAuth2 URLs from .env file', async () => {
|
|
1225
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify({}));
|
|
1226
|
+
mockDotenv.config.mockReturnValue({
|
|
1227
|
+
parsed: {
|
|
1228
|
+
OAUTH2_TOKEN_URL: 'https://env.example.com/token',
|
|
1229
|
+
OAUTH2_AUTHORIZE_URL: 'https://env.example.com/authorize',
|
|
1230
|
+
OAUTH2_DEVICE_CODE_URL: 'https://env.example.com/device',
|
|
1231
|
+
OAUTH2_REDIRECT_URI: 'http://env.localhost:8080/callback',
|
|
1232
|
+
OAUTH2_CLIENT_ID: 'env-client-id',
|
|
1233
|
+
OAUTH2_CLIENT_SECRET: 'env-client-secret',
|
|
1234
|
+
},
|
|
1235
|
+
error: undefined,
|
|
1236
|
+
});
|
|
1237
|
+
await oauth2_token_1.default.run(['--grantType', 'client_credentials']);
|
|
1238
|
+
const [method, url, request] = mockExecute.mock.calls[0];
|
|
1239
|
+
const requestBody = request.body.toString();
|
|
1240
|
+
expect(url).toBe('https://env.example.com/token');
|
|
1241
|
+
expect(requestBody).toContain('client_id=env-client-id');
|
|
1242
|
+
});
|
|
1243
|
+
});
|
|
1244
|
+
});
|