@optimizely-opal/opal-tool-ocp-sdk 0.0.0-OCP-1487.4 → 0.0.0-OCP-1487.6

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.
@@ -1,7 +1,7 @@
1
- import { AuthUtils } from './AuthUtils';
1
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
2
2
  import { getAppContext, logger } from '@zaiusinc/app-sdk';
3
3
  import { getTokenVerifier } from './TokenVerifier';
4
- import { OptiIdAuthData } from '../types/Models';
4
+ import { authorizeRegularRequest, authorizeGlobalRequest } from './AuthUtils';
5
5
 
6
6
  // Mock the dependencies
7
7
  jest.mock('./TokenVerifier', () => ({
@@ -19,568 +19,711 @@ jest.mock('@zaiusinc/app-sdk', () => ({
19
19
  }));
20
20
 
21
21
  describe('AuthUtils', () => {
22
+ let mockTokenVerifier: any;
22
23
  let mockGetTokenVerifier: jest.MockedFunction<typeof getTokenVerifier>;
23
24
  let mockGetAppContext: jest.MockedFunction<typeof getAppContext>;
24
- let mockTokenVerifier: jest.Mocked<{
25
- verify: (token: string) => Promise<any>;
26
- }>;
27
25
 
28
26
  beforeEach(() => {
29
27
  jest.clearAllMocks();
30
28
 
31
- // Create mock token verifier
29
+ // Setup token verifier mock
32
30
  mockTokenVerifier = {
33
31
  verify: jest.fn(),
34
32
  };
35
33
 
36
- // Setup the mocks
37
- mockGetTokenVerifier = jest.mocked(getTokenVerifier);
38
- mockGetAppContext = jest.mocked(getAppContext);
34
+ mockGetTokenVerifier = getTokenVerifier as jest.MockedFunction<typeof getTokenVerifier>;
35
+ mockGetTokenVerifier.mockResolvedValue(mockTokenVerifier);
39
36
 
40
- mockGetTokenVerifier.mockResolvedValue(mockTokenVerifier as any);
37
+ // Setup app context mock with default organization
38
+ mockGetAppContext = getAppContext as jest.MockedFunction<typeof getAppContext>;
41
39
  mockGetAppContext.mockReturnValue({
42
40
  account: {
43
- organizationId: 'app-org-123'
41
+ organizationId: 'test-org-123'
44
42
  }
45
43
  } as any);
46
44
  });
47
45
 
48
- describe('validateAccessToken', () => {
49
- it('should return true for valid token', async () => {
50
- // Arrange
51
- const validToken = 'valid-access-token';
46
+ describe('authorizeRegularRequest', () => {
47
+ beforeEach(() => {
48
+ // Default to successful token verification
52
49
  mockTokenVerifier.verify.mockResolvedValue(true);
53
-
54
- // Act
55
- const result = await AuthUtils.validateAccessToken(validToken);
56
-
57
- // Assert
58
- expect(result).toBe(true);
59
- expect(mockGetTokenVerifier).toHaveBeenCalledTimes(1);
60
- expect(mockTokenVerifier.verify).toHaveBeenCalledWith(validToken);
61
- expect(logger.error).not.toHaveBeenCalled();
62
- });
63
-
64
- it('should return false for invalid token', async () => {
65
- // Arrange
66
- const invalidToken = 'invalid-access-token';
67
- mockTokenVerifier.verify.mockResolvedValue(false);
68
-
69
- // Act
70
- const result = await AuthUtils.validateAccessToken(invalidToken);
71
-
72
- // Assert
73
- expect(result).toBe(false);
74
- expect(mockGetTokenVerifier).toHaveBeenCalledTimes(1);
75
- expect(mockTokenVerifier.verify).toHaveBeenCalledWith(invalidToken);
76
- expect(logger.error).not.toHaveBeenCalled();
77
- });
78
-
79
- it('should return false for undefined token', async () => {
80
- // Act
81
- const result = await AuthUtils.validateAccessToken(undefined);
82
-
83
- // Assert
84
- expect(result).toBe(false);
85
- expect(mockGetTokenVerifier).not.toHaveBeenCalled();
86
- expect(mockTokenVerifier.verify).not.toHaveBeenCalled();
87
- expect(logger.error).not.toHaveBeenCalled();
88
- });
89
-
90
- it('should return false for null token', async () => {
91
- // Act
92
- const result = await AuthUtils.validateAccessToken(null as any);
93
-
94
- // Assert
95
- expect(result).toBe(false);
96
- expect(mockGetTokenVerifier).not.toHaveBeenCalled();
97
- expect(mockTokenVerifier.verify).not.toHaveBeenCalled();
98
- expect(logger.error).not.toHaveBeenCalled();
99
- });
100
-
101
- it('should return false for empty string token', async () => {
102
- // Act
103
- const result = await AuthUtils.validateAccessToken('');
104
-
105
- // Assert
106
- expect(result).toBe(false);
107
- expect(mockGetTokenVerifier).not.toHaveBeenCalled();
108
- expect(mockTokenVerifier.verify).not.toHaveBeenCalled();
109
- expect(logger.error).not.toHaveBeenCalled();
110
50
  });
111
51
 
112
- it('should return false and log error when getTokenVerifier fails', async () => {
113
- // Arrange
114
- const validToken = 'valid-access-token';
115
- const error = new Error('Failed to get token verifier');
116
- mockGetTokenVerifier.mockRejectedValue(error);
52
+ describe('when request is for discovery endpoint', () => {
53
+ it('should return true without authentication', async () => {
54
+ const request = { path: '/discovery' };
117
55
 
118
- // Act
119
- const result = await AuthUtils.validateAccessToken(validToken);
56
+ const result = await authorizeRegularRequest(request);
120
57
 
121
- // Assert
122
- expect(result).toBe(false);
123
- expect(mockGetTokenVerifier).toHaveBeenCalledTimes(1);
124
- expect(mockTokenVerifier.verify).not.toHaveBeenCalled();
125
- expect(logger.error).toHaveBeenCalledWith('OptiID token validation failed:', error);
58
+ expect(result).toBe(true);
59
+ expect(mockGetTokenVerifier).not.toHaveBeenCalled();
60
+ expect(mockTokenVerifier.verify).not.toHaveBeenCalled();
61
+ });
126
62
  });
127
63
 
128
- it('should return false and log error when token verification throws', async () => {
129
- // Arrange
130
- const validToken = 'valid-access-token';
131
- const error = new Error('Token verification failed');
132
- mockTokenVerifier.verify.mockRejectedValue(error);
64
+ describe('when request is for ready endpoint', () => {
65
+ it('should return true without authentication', async () => {
66
+ const request = { path: '/ready' };
133
67
 
134
- // Act
135
- const result = await AuthUtils.validateAccessToken(validToken);
68
+ const result = await authorizeRegularRequest(request);
136
69
 
137
- // Assert
138
- expect(result).toBe(false);
139
- expect(mockGetTokenVerifier).toHaveBeenCalledTimes(1);
140
- expect(mockTokenVerifier.verify).toHaveBeenCalledWith(validToken);
141
- expect(logger.error).toHaveBeenCalledWith('OptiID token validation failed:', error);
70
+ expect(result).toBe(true);
71
+ expect(mockGetTokenVerifier).not.toHaveBeenCalled();
72
+ expect(mockTokenVerifier.verify).not.toHaveBeenCalled();
73
+ });
142
74
  });
143
75
 
144
- it('should handle whitespace-only token', async () => {
145
- // Arrange
146
- mockTokenVerifier.verify.mockResolvedValue(false);
147
-
148
- // Act
149
- const result = await AuthUtils.validateAccessToken(' ');
150
-
151
- // Assert - whitespace-only string should be treated as truthy and passed to verifier
152
- expect(result).toBe(false);
153
- expect(mockGetTokenVerifier).toHaveBeenCalledTimes(1);
154
- expect(mockTokenVerifier.verify).toHaveBeenCalledWith(' ');
155
- });
156
- });
157
-
158
- describe('extractAuthData', () => {
159
- const createValidRequest = (): any => ({
160
- bodyJSON: {
161
- auth: {
162
- provider: 'OptiID',
163
- credentials: {
164
- access_token: 'valid-access-token',
165
- customer_id: 'org-123',
166
- instance_id: 'instance-456',
167
- product_sku: 'OPAL'
76
+ describe('when request has valid authentication', () => {
77
+ it('should return true for valid OptiID token with matching organization', async () => {
78
+ const request = {
79
+ path: '/some-tool',
80
+ bodyJSON: {
81
+ auth: {
82
+ provider: 'OptiID',
83
+ credentials: {
84
+ customer_id: 'test-org-123',
85
+ access_token: 'valid-token-123'
86
+ }
87
+ }
168
88
  }
169
- } as OptiIdAuthData
170
- }
171
- });
172
-
173
- it('should extract auth data successfully from valid request', () => {
174
- // Arrange
175
- const request = createValidRequest();
176
-
177
- // Act
178
- const result = AuthUtils.extractAuthData(request);
179
-
180
- // Assert
181
- expect(result).not.toBeNull();
182
- expect(result?.authData).toBe(request.bodyJSON.auth);
183
- expect(result?.accessToken).toBe('valid-access-token');
184
- expect(logger.error).not.toHaveBeenCalled();
185
- });
186
-
187
- it('should handle case-insensitive provider name', () => {
188
- // Arrange
189
- const request = createValidRequest();
190
- request.bodyJSON.auth.provider = 'optiid'; // lowercase
191
-
192
- // Act
193
- const result = AuthUtils.extractAuthData(request);
194
-
195
- // Assert
196
- expect(result).not.toBeNull();
197
- expect(result?.authData).toBe(request.bodyJSON.auth);
198
- expect(result?.accessToken).toBe('valid-access-token');
199
- expect(logger.error).not.toHaveBeenCalled();
200
- });
201
-
202
- it('should handle mixed case provider name', () => {
203
- // Arrange
204
- const request = createValidRequest();
205
- request.bodyJSON.auth.provider = 'OpTiId'; // mixed case
206
-
207
- // Act
208
- const result = AuthUtils.extractAuthData(request);
209
-
210
- // Assert
211
- expect(result).not.toBeNull();
212
- expect(result?.authData).toBe(request.bodyJSON.auth);
213
- expect(result?.accessToken).toBe('valid-access-token');
214
- expect(logger.error).not.toHaveBeenCalled();
215
- });
216
-
217
- it('should return null when access token is missing', () => {
218
- // Arrange
219
- const request = createValidRequest();
220
- delete request.bodyJSON.auth.credentials.access_token;
221
-
222
- // Act
223
- const result = AuthUtils.extractAuthData(request);
224
-
225
- // Assert
226
- expect(result).toBeNull();
227
- expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
228
- });
229
-
230
- it('should return null when access token is undefined', () => {
231
- // Arrange
232
- const request = createValidRequest();
233
- request.bodyJSON.auth.credentials.access_token = undefined;
234
-
235
- // Act
236
- const result = AuthUtils.extractAuthData(request);
237
-
238
- // Assert
239
- expect(result).toBeNull();
240
- expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
241
- });
242
-
243
- it('should return null when access token is empty string', () => {
244
- // Arrange
245
- const request = createValidRequest();
246
- request.bodyJSON.auth.credentials.access_token = '';
247
-
248
- // Act
249
- const result = AuthUtils.extractAuthData(request);
250
-
251
- // Assert
252
- expect(result).toBeNull();
253
- expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
254
- });
255
-
256
- it('should return null when provider is not OptiID', () => {
257
- // Arrange
258
- const request = createValidRequest();
259
- request.bodyJSON.auth.provider = 'SomeOtherProvider';
260
-
261
- // Act
262
- const result = AuthUtils.extractAuthData(request);
263
-
264
- // Assert
265
- expect(result).toBeNull();
266
- expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
267
- });
268
-
269
- it('should return null when provider is missing', () => {
270
- // Arrange
271
- const request = createValidRequest();
272
- delete request.bodyJSON.auth.provider;
273
-
274
- // Act
275
- const result = AuthUtils.extractAuthData(request);
276
-
277
- // Assert
278
- expect(result).toBeNull();
279
- expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
280
- });
281
-
282
- it('should return null when auth structure is missing', () => {
283
- // Arrange
284
- const request = {
285
- bodyJSON: {
286
- parameters: { some: 'data' }
287
- }
288
- };
289
-
290
- // Act
291
- const result = AuthUtils.extractAuthData(request);
292
-
293
- // Assert
294
- expect(result).toBeNull();
295
- expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
296
- });
297
-
298
- it('should return null when credentials structure is missing', () => {
299
- // Arrange
300
- const request = {
301
- bodyJSON: {
302
- auth: {
303
- provider: 'OptiID'
89
+ };
90
+
91
+ const result = await authorizeRegularRequest(request);
92
+
93
+ expect(result).toBe(true);
94
+ expect(mockGetTokenVerifier).toHaveBeenCalled();
95
+ expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-token-123');
96
+ });
97
+
98
+ it('should handle case-insensitive provider names', async () => {
99
+ const request = {
100
+ path: '/some-tool',
101
+ bodyJSON: {
102
+ auth: {
103
+ provider: 'optiid', // lowercase
104
+ credentials: {
105
+ customer_id: 'test-org-123',
106
+ access_token: 'valid-token-123'
107
+ }
108
+ }
304
109
  }
305
- }
306
- };
307
-
308
- // Act
309
- const result = AuthUtils.extractAuthData(request);
310
-
311
- // Assert
312
- expect(result).toBeNull();
313
- expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
314
- });
315
-
316
- it('should return null when bodyJSON is missing', () => {
317
- // Arrange
318
- const request = {};
319
-
320
- // Act
321
- const result = AuthUtils.extractAuthData(request);
322
-
323
- // Assert
324
- expect(result).toBeNull();
325
- expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
326
- });
327
-
328
- it('should return null when request is null', () => {
329
- // Act
330
- const result = AuthUtils.extractAuthData(null);
331
-
332
- // Assert
333
- expect(result).toBeNull();
334
- expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
335
- });
110
+ };
111
+
112
+ const result = await authorizeRegularRequest(request);
113
+
114
+ expect(result).toBe(true);
115
+ expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-token-123');
116
+ });
117
+
118
+ it('should handle mixed case provider names', async () => {
119
+ const request = {
120
+ path: '/some-tool',
121
+ bodyJSON: {
122
+ auth: {
123
+ provider: 'OpTiId', // mixed case
124
+ credentials: {
125
+ customer_id: 'test-org-123',
126
+ access_token: 'valid-token-123'
127
+ }
128
+ }
129
+ }
130
+ };
131
+
132
+ const result = await authorizeRegularRequest(request);
133
+
134
+ expect(result).toBe(true);
135
+ expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-token-123');
136
+ });
137
+ });
138
+
139
+ describe('when authentication fails', () => {
140
+ it('should return false when auth data is missing', async () => {
141
+ const request = {
142
+ path: '/some-tool',
143
+ bodyJSON: {}
144
+ };
145
+
146
+ const result = await authorizeRegularRequest(request);
147
+
148
+ expect(result).toBe(false);
149
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
150
+ expect(mockTokenVerifier.verify).not.toHaveBeenCalled();
151
+ });
152
+
153
+ it('should return false when bodyJSON is missing', async () => {
154
+ const request = {
155
+ path: '/some-tool'
156
+ };
157
+
158
+ const result = await authorizeRegularRequest(request);
159
+
160
+ expect(result).toBe(false);
161
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
162
+ });
163
+
164
+ it('should return false when provider is not OptiID', async () => {
165
+ const request = {
166
+ path: '/some-tool',
167
+ bodyJSON: {
168
+ auth: {
169
+ provider: 'SomeOtherProvider',
170
+ credentials: {
171
+ customer_id: 'test-org-123',
172
+ access_token: 'some-token'
173
+ }
174
+ }
175
+ }
176
+ };
177
+
178
+ const result = await authorizeRegularRequest(request);
179
+
180
+ expect(result).toBe(false);
181
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
182
+ });
183
+
184
+ it('should return false when access token is missing', async () => {
185
+ const request = {
186
+ path: '/some-tool',
187
+ bodyJSON: {
188
+ auth: {
189
+ provider: 'OptiID',
190
+ credentials: {
191
+ customer_id: 'test-org-123'
192
+ // access_token missing
193
+ }
194
+ }
195
+ }
196
+ };
197
+
198
+ const result = await authorizeRegularRequest(request);
199
+
200
+ expect(result).toBe(false);
201
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
202
+ });
203
+
204
+ it('should return false when access token is empty string', async () => {
205
+ const request = {
206
+ path: '/some-tool',
207
+ bodyJSON: {
208
+ auth: {
209
+ provider: 'OptiID',
210
+ credentials: {
211
+ customer_id: 'test-org-123',
212
+ access_token: ''
213
+ }
214
+ }
215
+ }
216
+ };
217
+
218
+ const result = await authorizeRegularRequest(request);
219
+
220
+ expect(result).toBe(false);
221
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
222
+ });
223
+
224
+ it('should return false when access token is undefined', async () => {
225
+ const request = {
226
+ path: '/some-tool',
227
+ bodyJSON: {
228
+ auth: {
229
+ provider: 'OptiID',
230
+ credentials: {
231
+ customer_id: 'test-org-123',
232
+ access_token: undefined
233
+ }
234
+ }
235
+ }
236
+ };
237
+
238
+ const result = await authorizeRegularRequest(request);
239
+
240
+ expect(result).toBe(false);
241
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
242
+ });
243
+ });
244
+
245
+ describe('when organization validation fails', () => {
246
+ it('should return false when customer_id does not match app organization', async () => {
247
+ const request = {
248
+ path: '/some-tool',
249
+ bodyJSON: {
250
+ auth: {
251
+ provider: 'OptiID',
252
+ credentials: {
253
+ customer_id: 'different-org-456',
254
+ access_token: 'valid-token-123'
255
+ }
256
+ }
257
+ }
258
+ };
259
+
260
+ const result = await authorizeRegularRequest(request);
261
+
262
+ expect(result).toBe(false);
263
+ expect(logger.error).toHaveBeenCalledWith(
264
+ 'Invalid organisation ID: expected test-org-123, received different-org-456'
265
+ );
266
+ expect(mockTokenVerifier.verify).not.toHaveBeenCalled();
267
+ });
268
+
269
+ it('should return false when customer_id is missing', async () => {
270
+ const request = {
271
+ path: '/some-tool',
272
+ bodyJSON: {
273
+ auth: {
274
+ provider: 'OptiID',
275
+ credentials: {
276
+ access_token: 'valid-token-123'
277
+ // customer_id missing
278
+ }
279
+ }
280
+ }
281
+ };
282
+
283
+ const result = await authorizeRegularRequest(request);
284
+
285
+ expect(result).toBe(false);
286
+ expect(logger.error).toHaveBeenCalledWith('Organisation ID is required but not provided');
287
+ });
288
+
289
+ it('should return false when customer_id is empty string', async () => {
290
+ const request = {
291
+ path: '/some-tool',
292
+ bodyJSON: {
293
+ auth: {
294
+ provider: 'OptiID',
295
+ credentials: {
296
+ customer_id: '',
297
+ access_token: 'valid-token-123'
298
+ }
299
+ }
300
+ }
301
+ };
302
+
303
+ const result = await authorizeRegularRequest(request);
304
+
305
+ expect(result).toBe(false);
306
+ expect(logger.error).toHaveBeenCalledWith('Organisation ID is required but not provided');
307
+ });
308
+
309
+ it('should handle case when app context has no account', async () => {
310
+ mockGetAppContext.mockReturnValue({} as any);
311
+
312
+ const request = {
313
+ path: '/some-tool',
314
+ bodyJSON: {
315
+ auth: {
316
+ provider: 'OptiID',
317
+ credentials: {
318
+ customer_id: 'some-org-123',
319
+ access_token: 'valid-token-123'
320
+ }
321
+ }
322
+ }
323
+ };
324
+
325
+ const result = await authorizeRegularRequest(request);
326
+
327
+ expect(result).toBe(false);
328
+ expect(logger.error).toHaveBeenCalledWith(
329
+ 'Invalid organisation ID: expected undefined, received some-org-123'
330
+ );
331
+ });
332
+
333
+ it('should handle case when app context is null', async () => {
334
+ mockGetAppContext.mockReturnValue(null as any);
335
+
336
+ const request = {
337
+ path: '/some-tool',
338
+ bodyJSON: {
339
+ auth: {
340
+ provider: 'OptiID',
341
+ credentials: {
342
+ customer_id: 'some-org-123',
343
+ access_token: 'valid-token-123'
344
+ }
345
+ }
346
+ }
347
+ };
348
+
349
+ const result = await authorizeRegularRequest(request);
350
+
351
+ expect(result).toBe(false);
352
+ expect(logger.error).toHaveBeenCalledWith(
353
+ 'Invalid organisation ID: expected undefined, received some-org-123'
354
+ );
355
+ });
356
+ });
357
+
358
+ describe('when token validation fails', () => {
359
+ it('should return false when token verifier returns false', async () => {
360
+ mockTokenVerifier.verify.mockResolvedValue(false);
361
+
362
+ const request = {
363
+ path: '/some-tool',
364
+ bodyJSON: {
365
+ auth: {
366
+ provider: 'OptiID',
367
+ credentials: {
368
+ customer_id: 'test-org-123',
369
+ access_token: 'invalid-token'
370
+ }
371
+ }
372
+ }
373
+ };
374
+
375
+ const result = await authorizeRegularRequest(request);
376
+
377
+ expect(result).toBe(false);
378
+ expect(mockTokenVerifier.verify).toHaveBeenCalledWith('invalid-token');
379
+ });
380
+
381
+ it('should return false when token verification throws an error', async () => {
382
+ const verificationError = new Error('Token verification failed');
383
+ mockTokenVerifier.verify.mockRejectedValue(verificationError);
384
+
385
+ const request = {
386
+ path: '/some-tool',
387
+ bodyJSON: {
388
+ auth: {
389
+ provider: 'OptiID',
390
+ credentials: {
391
+ customer_id: 'test-org-123',
392
+ access_token: 'error-token'
393
+ }
394
+ }
395
+ }
396
+ };
397
+
398
+ const result = await authorizeRegularRequest(request);
399
+
400
+ expect(result).toBe(false);
401
+ expect(logger.error).toHaveBeenCalledWith('OptiID token validation failed:', verificationError);
402
+ });
403
+
404
+ it('should return false when getTokenVerifier throws an error', async () => {
405
+ const verifierError = new Error('Failed to get token verifier');
406
+ mockGetTokenVerifier.mockRejectedValue(verifierError);
407
+
408
+ const request = {
409
+ path: '/some-tool',
410
+ bodyJSON: {
411
+ auth: {
412
+ provider: 'OptiID',
413
+ credentials: {
414
+ customer_id: 'test-org-123',
415
+ access_token: 'some-token'
416
+ }
417
+ }
418
+ }
419
+ };
336
420
 
337
- it('should return null when request is undefined', () => {
338
- // Act
339
- const result = AuthUtils.extractAuthData(undefined);
421
+ const result = await authorizeRegularRequest(request);
340
422
 
341
- // Assert
342
- expect(result).toBeNull();
343
- expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
423
+ expect(result).toBe(false);
424
+ expect(logger.error).toHaveBeenCalledWith('OptiID token validation failed:', verifierError);
425
+ });
344
426
  });
345
427
  });
346
428
 
347
- describe('validateOrganizationId', () => {
429
+ describe('authorizeGlobalRequest', () => {
348
430
  beforeEach(() => {
349
- mockGetAppContext.mockReturnValue({
350
- account: {
351
- organizationId: 'app-org-123'
352
- }
353
- } as any);
354
- });
355
-
356
- it('should return true when customer ID matches app organization ID', () => {
357
- // Act
358
- const result = AuthUtils.validateOrganizationId('app-org-123');
359
-
360
- // Assert
361
- expect(result).toBe(true);
362
- expect(mockGetAppContext).toHaveBeenCalledTimes(1);
363
- expect(logger.error).not.toHaveBeenCalled();
364
- });
365
-
366
- it('should return false when customer ID does not match app organization ID', () => {
367
- // Act
368
- const result = AuthUtils.validateOrganizationId('different-org-456');
369
-
370
- // Assert
371
- expect(result).toBe(false);
372
- expect(mockGetAppContext).toHaveBeenCalledTimes(1);
373
- expect(logger.error).toHaveBeenCalledWith(
374
- 'Invalid organisation ID: expected app-org-123, received different-org-456'
375
- );
376
- });
377
-
378
- it('should return false when customer ID is undefined', () => {
379
- // Act
380
- const result = AuthUtils.validateOrganizationId(undefined);
381
-
382
- // Assert
383
- expect(result).toBe(false);
384
- expect(mockGetAppContext).not.toHaveBeenCalled();
385
- expect(logger.error).toHaveBeenCalledWith('Organisation ID is required but not provided');
431
+ // Default to successful token verification
432
+ mockTokenVerifier.verify.mockResolvedValue(true);
386
433
  });
387
434
 
388
- it('should return false when customer ID is null', () => {
389
- // Act
390
- const result = AuthUtils.validateOrganizationId(null as any);
391
-
392
- // Assert
393
- expect(result).toBe(false);
394
- expect(mockGetAppContext).not.toHaveBeenCalled();
395
- expect(logger.error).toHaveBeenCalledWith('Organisation ID is required but not provided');
396
- });
435
+ describe('when request is for discovery endpoint', () => {
436
+ it('should return true without authentication', async () => {
437
+ const request = { path: '/discovery' };
397
438
 
398
- it('should return false when customer ID is empty string', () => {
399
- // Act
400
- const result = AuthUtils.validateOrganizationId('');
439
+ const result = await authorizeGlobalRequest(request);
401
440
 
402
- // Assert
403
- expect(result).toBe(false);
404
- expect(mockGetAppContext).not.toHaveBeenCalled();
405
- expect(logger.error).toHaveBeenCalledWith('Organisation ID is required but not provided');
441
+ expect(result).toBe(true);
442
+ expect(mockGetTokenVerifier).not.toHaveBeenCalled();
443
+ expect(mockTokenVerifier.verify).not.toHaveBeenCalled();
444
+ });
406
445
  });
407
446
 
408
- it('should handle case when app context has no account', () => {
409
- // Arrange
410
- mockGetAppContext.mockReturnValue({} as any);
447
+ describe('when request is for ready endpoint', () => {
448
+ it('should return true without authentication', async () => {
449
+ const request = { path: '/ready' };
411
450
 
412
- // Act
413
- const result = AuthUtils.validateOrganizationId('some-org-123');
451
+ const result = await authorizeGlobalRequest(request);
414
452
 
415
- // Assert
416
- expect(result).toBe(false);
417
- expect(mockGetAppContext).toHaveBeenCalledTimes(1);
418
- expect(logger.error).toHaveBeenCalledWith(
419
- 'Invalid organisation ID: expected undefined, received some-org-123'
420
- );
453
+ expect(result).toBe(true);
454
+ expect(mockGetTokenVerifier).not.toHaveBeenCalled();
455
+ expect(mockTokenVerifier.verify).not.toHaveBeenCalled();
456
+ });
421
457
  });
422
458
 
423
- it('should handle case when app context account has no organizationId', () => {
424
- // Arrange
425
- mockGetAppContext.mockReturnValue({
426
- account: {}
427
- } as any);
428
-
429
- // Act
430
- const result = AuthUtils.validateOrganizationId('some-org-123');
431
-
432
- // Assert
433
- expect(result).toBe(false);
434
- expect(mockGetAppContext).toHaveBeenCalledTimes(1);
435
- expect(logger.error).toHaveBeenCalledWith(
436
- 'Invalid organisation ID: expected undefined, received some-org-123'
437
- );
438
- });
439
-
440
- it('should handle case when app context is null', () => {
441
- // Arrange
442
- mockGetAppContext.mockReturnValue(null as any);
459
+ describe('when request has valid authentication', () => {
460
+ it('should return true for valid OptiID token regardless of organization', async () => {
461
+ const request = {
462
+ path: '/global-tool',
463
+ bodyJSON: {
464
+ auth: {
465
+ provider: 'OptiID',
466
+ credentials: {
467
+ customer_id: 'different-org-456', // Different from app org
468
+ access_token: 'valid-token-123'
469
+ }
470
+ }
471
+ }
472
+ };
473
+
474
+ const result = await authorizeGlobalRequest(request);
475
+
476
+ expect(result).toBe(true);
477
+ expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-token-123');
478
+ // Should not log organization validation errors for global requests
479
+ expect(logger.error).not.toHaveBeenCalledWith(
480
+ expect.stringContaining('Invalid organisation ID')
481
+ );
482
+ });
483
+
484
+ it('should return true even without customer_id', async () => {
485
+ const request = {
486
+ path: '/global-tool',
487
+ bodyJSON: {
488
+ auth: {
489
+ provider: 'OptiID',
490
+ credentials: {
491
+ access_token: 'valid-token-123'
492
+ // No customer_id - should be fine for global functions
493
+ }
494
+ }
495
+ }
496
+ };
497
+
498
+ const result = await authorizeGlobalRequest(request);
499
+
500
+ expect(result).toBe(true);
501
+ expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-token-123');
502
+ });
503
+
504
+ it('should handle case-insensitive provider names', async () => {
505
+ const request = {
506
+ path: '/global-tool',
507
+ bodyJSON: {
508
+ auth: {
509
+ provider: 'optiid',
510
+ credentials: {
511
+ customer_id: 'any-org',
512
+ access_token: 'valid-token-123'
513
+ }
514
+ }
515
+ }
516
+ };
517
+
518
+ const result = await authorizeGlobalRequest(request);
519
+
520
+ expect(result).toBe(true);
521
+ expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-token-123');
522
+ });
523
+ });
524
+
525
+ describe('when authentication fails', () => {
526
+ it('should return false when auth data is missing', async () => {
527
+ const request = {
528
+ path: '/global-tool',
529
+ bodyJSON: {}
530
+ };
531
+
532
+ const result = await authorizeGlobalRequest(request);
533
+
534
+ expect(result).toBe(false);
535
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
536
+ });
537
+
538
+ it('should return false when provider is not OptiID', async () => {
539
+ const request = {
540
+ path: '/global-tool',
541
+ bodyJSON: {
542
+ auth: {
543
+ provider: 'SomeOtherProvider',
544
+ credentials: {
545
+ customer_id: 'any-org',
546
+ access_token: 'some-token'
547
+ }
548
+ }
549
+ }
550
+ };
551
+
552
+ const result = await authorizeGlobalRequest(request);
553
+
554
+ expect(result).toBe(false);
555
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
556
+ });
557
+
558
+ it('should return false when access token is missing', async () => {
559
+ const request = {
560
+ path: '/global-tool',
561
+ bodyJSON: {
562
+ auth: {
563
+ provider: 'OptiID',
564
+ credentials: {
565
+ customer_id: 'any-org'
566
+ // access_token missing
567
+ }
568
+ }
569
+ }
570
+ };
571
+
572
+ const result = await authorizeGlobalRequest(request);
573
+
574
+ expect(result).toBe(false);
575
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
576
+ });
577
+
578
+ it('should return false when access token is empty', async () => {
579
+ const request = {
580
+ path: '/global-tool',
581
+ bodyJSON: {
582
+ auth: {
583
+ provider: 'OptiID',
584
+ credentials: {
585
+ customer_id: 'any-org',
586
+ access_token: ''
587
+ }
588
+ }
589
+ }
590
+ };
443
591
 
444
- // Act
445
- const result = AuthUtils.validateOrganizationId('some-org-123');
592
+ const result = await authorizeGlobalRequest(request);
446
593
 
447
- // Assert
448
- expect(result).toBe(false);
449
- expect(mockGetAppContext).toHaveBeenCalledTimes(1);
450
- expect(logger.error).toHaveBeenCalledWith(
451
- 'Invalid organisation ID: expected undefined, received some-org-123'
452
- );
594
+ expect(result).toBe(false);
595
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
596
+ });
453
597
  });
454
598
 
455
- it('should be case-sensitive for organization ID matching', () => {
456
- // Arrange
457
- mockGetAppContext.mockReturnValue({
458
- account: {
459
- organizationId: 'App-Org-123' // different case
460
- }
461
- } as any);
462
-
463
- // Act
464
- const result = AuthUtils.validateOrganizationId('app-org-123');
599
+ describe('when token validation fails', () => {
600
+ it('should return false when token verifier returns false', async () => {
601
+ mockTokenVerifier.verify.mockResolvedValue(false);
465
602
 
466
- // Assert
467
- expect(result).toBe(false);
468
- expect(mockGetAppContext).toHaveBeenCalledTimes(1);
469
- expect(logger.error).toHaveBeenCalledWith(
470
- 'Invalid organisation ID: expected App-Org-123, received app-org-123'
471
- );
472
- });
603
+ const request = {
604
+ path: '/global-tool',
605
+ bodyJSON: {
606
+ auth: {
607
+ provider: 'OptiID',
608
+ credentials: {
609
+ customer_id: 'any-org',
610
+ access_token: 'invalid-token'
611
+ }
612
+ }
613
+ }
614
+ };
615
+
616
+ const result = await authorizeGlobalRequest(request);
617
+
618
+ expect(result).toBe(false);
619
+ expect(mockTokenVerifier.verify).toHaveBeenCalledWith('invalid-token');
620
+ });
621
+
622
+ it('should return false when token verification throws an error', async () => {
623
+ const verificationError = new Error('Global token verification failed');
624
+ mockTokenVerifier.verify.mockRejectedValue(verificationError);
625
+
626
+ const request = {
627
+ path: '/global-tool',
628
+ bodyJSON: {
629
+ auth: {
630
+ provider: 'OptiID',
631
+ credentials: {
632
+ customer_id: 'any-org',
633
+ access_token: 'error-token'
634
+ }
635
+ }
636
+ }
637
+ };
638
+
639
+ const result = await authorizeGlobalRequest(request);
640
+
641
+ expect(result).toBe(false);
642
+ expect(logger.error).toHaveBeenCalledWith('OptiID token validation failed:', verificationError);
643
+ });
644
+ });
645
+
646
+ describe('organization validation differences from authorizeRegularRequest', () => {
647
+ it('should NOT validate organization ID and allow any customer_id', async () => {
648
+ const request = {
649
+ path: '/global-tool',
650
+ bodyJSON: {
651
+ auth: {
652
+ provider: 'OptiID',
653
+ credentials: {
654
+ customer_id: 'completely-different-org',
655
+ access_token: 'valid-token-123'
656
+ }
657
+ }
658
+ }
659
+ };
660
+
661
+ const result = await authorizeGlobalRequest(request);
662
+
663
+ expect(result).toBe(true);
664
+ expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-token-123');
665
+ // Should NOT log organization validation errors
666
+ expect(logger.error).not.toHaveBeenCalledWith(
667
+ expect.stringContaining('Invalid organisation ID')
668
+ );
669
+ expect(logger.error).not.toHaveBeenCalledWith(
670
+ expect.stringContaining('Organisation ID is required')
671
+ );
672
+ });
673
+
674
+ it('should work even when app context has no organization', async () => {
675
+ mockGetAppContext.mockReturnValue({} as any);
676
+
677
+ const request = {
678
+ path: '/global-tool',
679
+ bodyJSON: {
680
+ auth: {
681
+ provider: 'OptiID',
682
+ credentials: {
683
+ customer_id: 'any-org',
684
+ access_token: 'valid-token-123'
685
+ }
686
+ }
687
+ }
688
+ };
473
689
 
474
- it('should handle whitespace-only customer ID', () => {
475
- // Act
476
- const result = AuthUtils.validateOrganizationId(' ');
690
+ const result = await authorizeGlobalRequest(request);
477
691
 
478
- // Assert
479
- expect(result).toBe(false);
480
- expect(mockGetAppContext).toHaveBeenCalledTimes(1);
481
- expect(logger.error).toHaveBeenCalledWith(
482
- 'Invalid organisation ID: expected app-org-123, received '
483
- );
692
+ expect(result).toBe(true);
693
+ expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-token-123');
694
+ });
484
695
  });
485
696
  });
486
697
 
487
- describe('integration scenarios', () => {
488
- it('should handle complete authentication flow for valid request', async () => {
489
- // Arrange
698
+ describe('edge cases and error handling', () => {
699
+ it('should handle requests with null bodyJSON', async () => {
490
700
  const request = {
491
- bodyJSON: {
492
- auth: {
493
- provider: 'OptiID',
494
- credentials: {
495
- access_token: 'valid-access-token',
496
- customer_id: 'app-org-123',
497
- instance_id: 'instance-456',
498
- product_sku: 'OPAL'
499
- }
500
- } as OptiIdAuthData
501
- }
701
+ path: '/some-tool',
702
+ bodyJSON: null
502
703
  };
503
704
 
504
- mockGetAppContext.mockReturnValue({
505
- account: {
506
- organizationId: 'app-org-123'
507
- }
508
- } as any);
705
+ const result1 = await authorizeRegularRequest(request);
706
+ const result2 = await authorizeGlobalRequest(request);
509
707
 
510
- mockTokenVerifier.verify.mockResolvedValue(true);
511
-
512
- // Act
513
- const authInfo = AuthUtils.extractAuthData(request);
514
- const isValidOrg = authInfo
515
- ? AuthUtils.validateOrganizationId(authInfo.authData.credentials?.customer_id)
516
- : false;
517
- const isValidToken = authInfo ? await AuthUtils.validateAccessToken(authInfo.accessToken) : false;
518
-
519
- // Assert
520
- expect(authInfo).not.toBeNull();
521
- expect(isValidOrg).toBe(true);
522
- expect(isValidToken).toBe(true);
523
- expect(logger.error).not.toHaveBeenCalled();
708
+ expect(result1).toBe(false);
709
+ expect(result2).toBe(false);
710
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
524
711
  });
525
712
 
526
- it('should handle complete authentication flow for invalid provider', async () => {
527
- // Arrange
713
+ it('should handle malformed auth objects', async () => {
528
714
  const request = {
715
+ path: '/some-tool',
529
716
  bodyJSON: {
530
- auth: {
531
- provider: 'SomeOtherProvider',
532
- credentials: {
533
- access_token: 'valid-access-token',
534
- customer_id: 'app-org-123',
535
- instance_id: 'instance-456',
536
- product_sku: 'OPAL'
537
- }
538
- } as OptiIdAuthData
717
+ auth: 'invalid-auth-format'
539
718
  }
540
719
  };
541
720
 
542
- // Act
543
- const authInfo = AuthUtils.extractAuthData(request);
721
+ const result1 = await authorizeRegularRequest(request);
722
+ const result2 = await authorizeGlobalRequest(request);
544
723
 
545
- // Assert
546
- expect(authInfo).toBeNull();
724
+ expect(result1).toBe(false);
725
+ expect(result2).toBe(false);
547
726
  expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
548
727
  });
549
-
550
- it('should handle complete authentication flow for organization mismatch', async () => {
551
- // Arrange
552
- const request = {
553
- bodyJSON: {
554
- auth: {
555
- provider: 'OptiID',
556
- credentials: {
557
- access_token: 'valid-access-token',
558
- customer_id: 'different-org-456',
559
- instance_id: 'instance-456',
560
- product_sku: 'OPAL'
561
- }
562
- } as OptiIdAuthData
563
- }
564
- };
565
-
566
- mockGetAppContext.mockReturnValue({
567
- account: {
568
- organizationId: 'app-org-123'
569
- }
570
- } as any);
571
-
572
- // Act
573
- const authInfo = AuthUtils.extractAuthData(request);
574
- const isValidOrg = authInfo
575
- ? AuthUtils.validateOrganizationId(authInfo.authData.credentials?.customer_id)
576
- : false;
577
-
578
- // Assert
579
- expect(authInfo).not.toBeNull();
580
- expect(isValidOrg).toBe(false);
581
- expect(logger.error).toHaveBeenCalledWith(
582
- 'Invalid organisation ID: expected app-org-123, received different-org-456'
583
- );
584
- });
585
728
  });
586
729
  });