@optimizely-opal/opal-tool-ocp-sdk 0.0.0-OCP-1487.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +631 -0
- package/dist/auth/AuthUtils.d.ts +31 -0
- package/dist/auth/AuthUtils.d.ts.map +1 -0
- package/dist/auth/AuthUtils.js +64 -0
- package/dist/auth/AuthUtils.js.map +1 -0
- package/dist/auth/AuthUtils.test.d.ts +2 -0
- package/dist/auth/AuthUtils.test.d.ts.map +1 -0
- package/dist/auth/AuthUtils.test.js +469 -0
- package/dist/auth/AuthUtils.test.js.map +1 -0
- package/dist/auth/TokenVerifier.d.ts +31 -0
- package/dist/auth/TokenVerifier.d.ts.map +1 -0
- package/dist/auth/TokenVerifier.js +127 -0
- package/dist/auth/TokenVerifier.js.map +1 -0
- package/dist/auth/TokenVerifier.test.d.ts +2 -0
- package/dist/auth/TokenVerifier.test.d.ts.map +1 -0
- package/dist/auth/TokenVerifier.test.js +125 -0
- package/dist/auth/TokenVerifier.test.js.map +1 -0
- package/dist/decorator/Decorator.d.ts +48 -0
- package/dist/decorator/Decorator.d.ts.map +1 -0
- package/dist/decorator/Decorator.js +53 -0
- package/dist/decorator/Decorator.js.map +1 -0
- package/dist/decorator/Decorator.test.d.ts +2 -0
- package/dist/decorator/Decorator.test.d.ts.map +1 -0
- package/dist/decorator/Decorator.test.js +528 -0
- package/dist/decorator/Decorator.test.js.map +1 -0
- package/dist/function/GlobalToolFunction.d.ts +28 -0
- package/dist/function/GlobalToolFunction.d.ts.map +1 -0
- package/dist/function/GlobalToolFunction.js +56 -0
- package/dist/function/GlobalToolFunction.js.map +1 -0
- package/dist/function/GlobalToolFunction.test.d.ts +2 -0
- package/dist/function/GlobalToolFunction.test.d.ts.map +1 -0
- package/dist/function/GlobalToolFunction.test.js +425 -0
- package/dist/function/GlobalToolFunction.test.js.map +1 -0
- package/dist/function/ToolFunction.d.ts +28 -0
- package/dist/function/ToolFunction.d.ts.map +1 -0
- package/dist/function/ToolFunction.js +60 -0
- package/dist/function/ToolFunction.js.map +1 -0
- package/dist/function/ToolFunction.test.d.ts +2 -0
- package/dist/function/ToolFunction.test.d.ts.map +1 -0
- package/dist/function/ToolFunction.test.js +314 -0
- package/dist/function/ToolFunction.test.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/service/Service.d.ts +80 -0
- package/dist/service/Service.d.ts.map +1 -0
- package/dist/service/Service.js +210 -0
- package/dist/service/Service.js.map +1 -0
- package/dist/service/Service.test.d.ts +2 -0
- package/dist/service/Service.test.d.ts.map +1 -0
- package/dist/service/Service.test.js +427 -0
- package/dist/service/Service.test.js.map +1 -0
- package/dist/types/Models.d.ts +126 -0
- package/dist/types/Models.d.ts.map +1 -0
- package/dist/types/Models.js +181 -0
- package/dist/types/Models.js.map +1 -0
- package/package.json +64 -0
- package/src/auth/AuthUtils.test.ts +586 -0
- package/src/auth/AuthUtils.ts +66 -0
- package/src/auth/TokenVerifier.test.ts +165 -0
- package/src/auth/TokenVerifier.ts +145 -0
- package/src/decorator/Decorator.test.ts +649 -0
- package/src/decorator/Decorator.ts +111 -0
- package/src/function/GlobalToolFunction.test.ts +505 -0
- package/src/function/GlobalToolFunction.ts +61 -0
- package/src/function/ToolFunction.test.ts +374 -0
- package/src/function/ToolFunction.ts +64 -0
- package/src/index.ts +5 -0
- package/src/service/Service.test.ts +661 -0
- package/src/service/Service.ts +213 -0
- package/src/types/Models.ts +163 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import { AuthUtils } from './AuthUtils';
|
|
2
|
+
import { getAppContext, logger } from '@zaiusinc/app-sdk';
|
|
3
|
+
import { getTokenVerifier } from './TokenVerifier';
|
|
4
|
+
import { OptiIdAuthData } from '../types/Models';
|
|
5
|
+
|
|
6
|
+
// Mock the dependencies
|
|
7
|
+
jest.mock('./TokenVerifier', () => ({
|
|
8
|
+
getTokenVerifier: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
jest.mock('@zaiusinc/app-sdk', () => ({
|
|
12
|
+
getAppContext: jest.fn(),
|
|
13
|
+
logger: {
|
|
14
|
+
info: jest.fn(),
|
|
15
|
+
error: jest.fn(),
|
|
16
|
+
warn: jest.fn(),
|
|
17
|
+
debug: jest.fn(),
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
describe('AuthUtils', () => {
|
|
22
|
+
let mockGetTokenVerifier: jest.MockedFunction<typeof getTokenVerifier>;
|
|
23
|
+
let mockGetAppContext: jest.MockedFunction<typeof getAppContext>;
|
|
24
|
+
let mockTokenVerifier: jest.Mocked<{
|
|
25
|
+
verify: (token: string) => Promise<any>;
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
jest.clearAllMocks();
|
|
30
|
+
|
|
31
|
+
// Create mock token verifier
|
|
32
|
+
mockTokenVerifier = {
|
|
33
|
+
verify: jest.fn(),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Setup the mocks
|
|
37
|
+
mockGetTokenVerifier = jest.mocked(getTokenVerifier);
|
|
38
|
+
mockGetAppContext = jest.mocked(getAppContext);
|
|
39
|
+
|
|
40
|
+
mockGetTokenVerifier.mockResolvedValue(mockTokenVerifier as any);
|
|
41
|
+
mockGetAppContext.mockReturnValue({
|
|
42
|
+
account: {
|
|
43
|
+
organizationId: 'app-org-123'
|
|
44
|
+
}
|
|
45
|
+
} as any);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('validateAccessToken', () => {
|
|
49
|
+
it('should return true for valid token', async () => {
|
|
50
|
+
// Arrange
|
|
51
|
+
const validToken = 'valid-access-token';
|
|
52
|
+
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
|
+
});
|
|
111
|
+
|
|
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);
|
|
117
|
+
|
|
118
|
+
// Act
|
|
119
|
+
const result = await AuthUtils.validateAccessToken(validToken);
|
|
120
|
+
|
|
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);
|
|
126
|
+
});
|
|
127
|
+
|
|
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);
|
|
133
|
+
|
|
134
|
+
// Act
|
|
135
|
+
const result = await AuthUtils.validateAccessToken(validToken);
|
|
136
|
+
|
|
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);
|
|
142
|
+
});
|
|
143
|
+
|
|
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'
|
|
168
|
+
}
|
|
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'
|
|
304
|
+
}
|
|
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
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should return null when request is undefined', () => {
|
|
338
|
+
// Act
|
|
339
|
+
const result = AuthUtils.extractAuthData(undefined);
|
|
340
|
+
|
|
341
|
+
// Assert
|
|
342
|
+
expect(result).toBeNull();
|
|
343
|
+
expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe('validateOrganizationId', () => {
|
|
348
|
+
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');
|
|
386
|
+
});
|
|
387
|
+
|
|
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
|
+
});
|
|
397
|
+
|
|
398
|
+
it('should return false when customer ID is empty string', () => {
|
|
399
|
+
// Act
|
|
400
|
+
const result = AuthUtils.validateOrganizationId('');
|
|
401
|
+
|
|
402
|
+
// Assert
|
|
403
|
+
expect(result).toBe(false);
|
|
404
|
+
expect(mockGetAppContext).not.toHaveBeenCalled();
|
|
405
|
+
expect(logger.error).toHaveBeenCalledWith('Organisation ID is required but not provided');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('should handle case when app context has no account', () => {
|
|
409
|
+
// Arrange
|
|
410
|
+
mockGetAppContext.mockReturnValue({} as any);
|
|
411
|
+
|
|
412
|
+
// Act
|
|
413
|
+
const result = AuthUtils.validateOrganizationId('some-org-123');
|
|
414
|
+
|
|
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
|
+
);
|
|
421
|
+
});
|
|
422
|
+
|
|
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);
|
|
443
|
+
|
|
444
|
+
// Act
|
|
445
|
+
const result = AuthUtils.validateOrganizationId('some-org-123');
|
|
446
|
+
|
|
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
|
+
);
|
|
453
|
+
});
|
|
454
|
+
|
|
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');
|
|
465
|
+
|
|
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
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should handle whitespace-only customer ID', () => {
|
|
475
|
+
// Act
|
|
476
|
+
const result = AuthUtils.validateOrganizationId(' ');
|
|
477
|
+
|
|
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
|
+
);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
describe('integration scenarios', () => {
|
|
488
|
+
it('should handle complete authentication flow for valid request', async () => {
|
|
489
|
+
// Arrange
|
|
490
|
+
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
|
+
}
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
mockGetAppContext.mockReturnValue({
|
|
505
|
+
account: {
|
|
506
|
+
organizationId: 'app-org-123'
|
|
507
|
+
}
|
|
508
|
+
} as any);
|
|
509
|
+
|
|
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();
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('should handle complete authentication flow for invalid provider', async () => {
|
|
527
|
+
// Arrange
|
|
528
|
+
const request = {
|
|
529
|
+
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
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// Act
|
|
543
|
+
const authInfo = AuthUtils.extractAuthData(request);
|
|
544
|
+
|
|
545
|
+
// Assert
|
|
546
|
+
expect(authInfo).toBeNull();
|
|
547
|
+
expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
|
|
548
|
+
});
|
|
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
|
+
});
|
|
586
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { getAppContext, logger } from '@zaiusinc/app-sdk';
|
|
2
|
+
import { getTokenVerifier } from './TokenVerifier';
|
|
3
|
+
import { OptiIdAuthData } from '../types/Models';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Common authentication utilities for all function types
|
|
7
|
+
*/
|
|
8
|
+
export class AuthUtils {
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validate the OptiID access token
|
|
12
|
+
*
|
|
13
|
+
* @param accessToken - The access token to validate
|
|
14
|
+
* @returns true if the token is valid
|
|
15
|
+
*/
|
|
16
|
+
public static async validateAccessToken(accessToken: string | undefined): Promise<boolean> {
|
|
17
|
+
try {
|
|
18
|
+
if (!accessToken) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const tokenVerifier = await getTokenVerifier();
|
|
22
|
+
return await tokenVerifier.verify(accessToken);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
logger.error('OptiID token validation failed:', error);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extract and validate basic OptiID authentication data from request
|
|
31
|
+
*
|
|
32
|
+
* @param request - The incoming request
|
|
33
|
+
* @returns object with authData and accessToken, or null if invalid
|
|
34
|
+
*/
|
|
35
|
+
public static extractAuthData(request: any): { authData: OptiIdAuthData; accessToken: string } | null {
|
|
36
|
+
const authData = request?.bodyJSON?.auth as OptiIdAuthData;
|
|
37
|
+
const accessToken = authData?.credentials?.access_token;
|
|
38
|
+
if (!accessToken || authData?.provider?.toLowerCase() !== 'optiid') {
|
|
39
|
+
logger.error('OptiID token is required but not provided');
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { authData, accessToken };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validate organization ID matches the app context
|
|
48
|
+
*
|
|
49
|
+
* @param customerId - The customer ID from the auth data
|
|
50
|
+
* @returns true if the organization ID is valid
|
|
51
|
+
*/
|
|
52
|
+
public static validateOrganizationId(customerId: string | undefined): boolean {
|
|
53
|
+
if (!customerId) {
|
|
54
|
+
logger.error('Organisation ID is required but not provided');
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const appOrganisationId = getAppContext()?.account?.organizationId;
|
|
59
|
+
if (customerId !== appOrganisationId) {
|
|
60
|
+
logger.error(`Invalid organisation ID: expected ${appOrganisationId}, received ${customerId}`);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|