@app-connect/core 0.0.2 → 1.5.8
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/.env.test +5 -0
- package/README.md +175 -8
- package/adapter/registry.js +137 -5
- package/handlers/log.js +4 -3
- package/index.js +5 -3
- package/jest.config.js +57 -0
- package/lib/callLogComposer.js +98 -64
- package/lib/constants.js +9 -0
- package/lib/oauth.js +60 -23
- package/package.json +7 -1
- package/releaseNotes.json +28 -0
- package/test/adapter/registry.test.js +271 -0
- package/test/handlers/auth.test.js +231 -0
- package/test/lib/jwt.test.js +161 -0
- package/test/setup.js +176 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
const adapterRegistry = require('../../adapter/registry');
|
|
2
|
+
|
|
3
|
+
describe('AdapterRegistry Interface Registration with Composition', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
// Clear the registry before each test
|
|
6
|
+
adapterRegistry.adapters.clear();
|
|
7
|
+
adapterRegistry.manifests.clear();
|
|
8
|
+
adapterRegistry.platformInterfaces.clear();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('should register interface functions for a platform', () => {
|
|
12
|
+
const mockFunction = jest.fn();
|
|
13
|
+
|
|
14
|
+
adapterRegistry.registerAdapterInterface('testPlatform', 'testInterface', mockFunction);
|
|
15
|
+
|
|
16
|
+
expect(adapterRegistry.hasPlatformInterface('testPlatform', 'testInterface')).toBe(true);
|
|
17
|
+
expect(adapterRegistry.getPlatformInterfaces('testPlatform').get('testInterface')).toBe(mockFunction);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('should throw error when registering non-function as interface', () => {
|
|
21
|
+
expect(() => {
|
|
22
|
+
adapterRegistry.registerAdapterInterface('testPlatform', 'testInterface', 'not a function');
|
|
23
|
+
}).toThrow('Interface function must be a function, got: string');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should return original adapter when no interfaces are registered', () => {
|
|
27
|
+
const mockAdapter = {
|
|
28
|
+
getAuthType: () => 'apiKey',
|
|
29
|
+
createCallLog: jest.fn(),
|
|
30
|
+
updateCallLog: jest.fn()
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
adapterRegistry.registerAdapter('testPlatform', mockAdapter);
|
|
34
|
+
|
|
35
|
+
const retrievedAdapter = adapterRegistry.getAdapter('testPlatform');
|
|
36
|
+
expect(retrievedAdapter).toBe(mockAdapter);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('should return composed adapter with interface functions when interfaces are registered', () => {
|
|
40
|
+
const mockInterface = jest.fn();
|
|
41
|
+
const mockAdapter = {
|
|
42
|
+
getAuthType: () => 'apiKey',
|
|
43
|
+
createCallLog: jest.fn(),
|
|
44
|
+
updateCallLog: jest.fn()
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Register interface function first
|
|
48
|
+
adapterRegistry.registerAdapterInterface('testPlatform', 'customMethod', mockInterface);
|
|
49
|
+
|
|
50
|
+
// Register adapter
|
|
51
|
+
adapterRegistry.registerAdapter('testPlatform', mockAdapter);
|
|
52
|
+
|
|
53
|
+
// Get composed adapter
|
|
54
|
+
const composedAdapter = adapterRegistry.getAdapter('testPlatform');
|
|
55
|
+
|
|
56
|
+
// Should be a different object (composed)
|
|
57
|
+
expect(composedAdapter).not.toBe(mockAdapter);
|
|
58
|
+
|
|
59
|
+
// Should have the interface function
|
|
60
|
+
expect(composedAdapter.customMethod).toBe(mockInterface);
|
|
61
|
+
|
|
62
|
+
// Should still have original methods
|
|
63
|
+
expect(composedAdapter.getAuthType).toBe(mockAdapter.getAuthType);
|
|
64
|
+
expect(composedAdapter.createCallLog).toBe(mockAdapter.createCallLog);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('should not override existing adapter methods when composing interfaces', () => {
|
|
68
|
+
const existingMethod = jest.fn();
|
|
69
|
+
const mockAdapter = {
|
|
70
|
+
getAuthType: () => 'apiKey',
|
|
71
|
+
createCallLog: jest.fn(),
|
|
72
|
+
updateCallLog: jest.fn(),
|
|
73
|
+
existingMethod: existingMethod
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Register adapter first
|
|
77
|
+
adapterRegistry.registerAdapter('testPlatform', mockAdapter);
|
|
78
|
+
|
|
79
|
+
// Try to register interface with same name as existing method
|
|
80
|
+
const newMethod = jest.fn();
|
|
81
|
+
adapterRegistry.registerAdapterInterface('testPlatform', 'existingMethod', newMethod);
|
|
82
|
+
|
|
83
|
+
// Get composed adapter
|
|
84
|
+
const composedAdapter = adapterRegistry.getAdapter('testPlatform');
|
|
85
|
+
|
|
86
|
+
// Should not override the existing method
|
|
87
|
+
expect(composedAdapter.existingMethod).toBe(existingMethod);
|
|
88
|
+
expect(composedAdapter.existingMethod).not.toBe(newMethod);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('should preserve original adapter when composing interfaces', () => {
|
|
92
|
+
const mockInterface = jest.fn();
|
|
93
|
+
const mockAdapter = {
|
|
94
|
+
getAuthType: () => 'apiKey',
|
|
95
|
+
createCallLog: jest.fn(),
|
|
96
|
+
updateCallLog: jest.fn()
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
adapterRegistry.registerAdapterInterface('testPlatform', 'customMethod', mockInterface);
|
|
100
|
+
adapterRegistry.registerAdapter('testPlatform', mockAdapter);
|
|
101
|
+
|
|
102
|
+
// Get original adapter
|
|
103
|
+
const originalAdapter = adapterRegistry.getOriginalAdapter('testPlatform');
|
|
104
|
+
|
|
105
|
+
// Original adapter should be unchanged
|
|
106
|
+
expect(originalAdapter).toBe(mockAdapter);
|
|
107
|
+
expect(originalAdapter.customMethod).toBeUndefined();
|
|
108
|
+
|
|
109
|
+
// Composed adapter should have the interface
|
|
110
|
+
const composedAdapter = adapterRegistry.getAdapter('testPlatform');
|
|
111
|
+
expect(composedAdapter.customMethod).toBe(mockInterface);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('should unregister interface functions', () => {
|
|
115
|
+
const mockFunction = jest.fn();
|
|
116
|
+
|
|
117
|
+
adapterRegistry.registerAdapterInterface('testPlatform', 'testInterface', mockFunction);
|
|
118
|
+
expect(adapterRegistry.hasPlatformInterface('testPlatform', 'testInterface')).toBe(true);
|
|
119
|
+
|
|
120
|
+
adapterRegistry.unregisterAdapterInterface('testPlatform', 'testInterface');
|
|
121
|
+
expect(adapterRegistry.hasPlatformInterface('testPlatform', 'testInterface')).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('should return empty map for non-existent platform interfaces', () => {
|
|
125
|
+
const interfaces = adapterRegistry.getPlatformInterfaces('nonExistentPlatform');
|
|
126
|
+
expect(interfaces).toBeInstanceOf(Map);
|
|
127
|
+
expect(interfaces.size).toBe(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('should return false for non-existent platform interface', () => {
|
|
131
|
+
expect(adapterRegistry.hasPlatformInterface('nonExistentPlatform', 'anyInterface')).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('should handle multiple interface functions for same platform', () => {
|
|
135
|
+
const mockFunction1 = jest.fn();
|
|
136
|
+
const mockFunction2 = jest.fn();
|
|
137
|
+
const mockAdapter = {
|
|
138
|
+
getAuthType: () => 'apiKey',
|
|
139
|
+
createCallLog: jest.fn(),
|
|
140
|
+
updateCallLog: jest.fn()
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
adapterRegistry.registerAdapterInterface('testPlatform', 'interface1', mockFunction1);
|
|
144
|
+
adapterRegistry.registerAdapterInterface('testPlatform', 'interface2', mockFunction2);
|
|
145
|
+
adapterRegistry.registerAdapter('testPlatform', mockAdapter);
|
|
146
|
+
|
|
147
|
+
const platformInterfaces = adapterRegistry.getPlatformInterfaces('testPlatform');
|
|
148
|
+
expect(platformInterfaces.size).toBe(2);
|
|
149
|
+
expect(platformInterfaces.get('interface1')).toBe(mockFunction1);
|
|
150
|
+
expect(platformInterfaces.get('interface2')).toBe(mockFunction2);
|
|
151
|
+
|
|
152
|
+
// Check composed adapter has both interfaces
|
|
153
|
+
const composedAdapter = adapterRegistry.getAdapter('testPlatform');
|
|
154
|
+
expect(composedAdapter.interface1).toBe(mockFunction1);
|
|
155
|
+
expect(composedAdapter.interface2).toBe(mockFunction2);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('should clean up platform interfaces when unregistering adapter', () => {
|
|
159
|
+
const mockFunction = jest.fn();
|
|
160
|
+
const mockAdapter = {
|
|
161
|
+
getAuthType: () => 'apiKey',
|
|
162
|
+
createCallLog: jest.fn(),
|
|
163
|
+
updateCallLog: jest.fn()
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
adapterRegistry.registerAdapterInterface('testPlatform', 'testInterface', mockFunction);
|
|
167
|
+
adapterRegistry.registerAdapter('testPlatform', mockAdapter);
|
|
168
|
+
|
|
169
|
+
expect(adapterRegistry.hasPlatformInterface('testPlatform', 'testInterface')).toBe(true);
|
|
170
|
+
|
|
171
|
+
adapterRegistry.unregisterAdapter('testPlatform');
|
|
172
|
+
|
|
173
|
+
expect(adapterRegistry.hasPlatformInterface('testPlatform', 'testInterface')).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('should get adapter capabilities correctly', () => {
|
|
177
|
+
const mockInterface = jest.fn();
|
|
178
|
+
const mockAdapter = {
|
|
179
|
+
getAuthType: () => 'apiKey',
|
|
180
|
+
createCallLog: jest.fn(),
|
|
181
|
+
updateCallLog: jest.fn()
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
adapterRegistry.registerAdapterInterface('testPlatform', 'customMethod', mockInterface);
|
|
185
|
+
adapterRegistry.registerAdapter('testPlatform', mockAdapter);
|
|
186
|
+
|
|
187
|
+
const capabilities = adapterRegistry.getAdapterCapabilities('testPlatform');
|
|
188
|
+
|
|
189
|
+
expect(capabilities.platform).toBe('testPlatform');
|
|
190
|
+
expect(capabilities.originalMethods).toContain('getAuthType');
|
|
191
|
+
expect(capabilities.originalMethods).toContain('createCallLog');
|
|
192
|
+
expect(capabilities.originalMethods).toContain('updateCallLog');
|
|
193
|
+
expect(capabilities.composedMethods).toContain('customMethod');
|
|
194
|
+
expect(capabilities.registeredInterfaces).toContain('customMethod');
|
|
195
|
+
expect(capabilities.authType).toBe('apiKey');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('should handle interface registration after adapter registration', () => {
|
|
199
|
+
const mockAdapter = {
|
|
200
|
+
getAuthType: () => 'apiKey',
|
|
201
|
+
createCallLog: jest.fn(),
|
|
202
|
+
updateCallLog: jest.fn()
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Register adapter first
|
|
206
|
+
adapterRegistry.registerAdapter('testPlatform', mockAdapter);
|
|
207
|
+
|
|
208
|
+
// Register interface function after
|
|
209
|
+
const mockInterface = jest.fn();
|
|
210
|
+
adapterRegistry.registerAdapterInterface('testPlatform', 'customMethod', mockInterface);
|
|
211
|
+
|
|
212
|
+
// Get composed adapter
|
|
213
|
+
const composedAdapter = adapterRegistry.getAdapter('testPlatform');
|
|
214
|
+
|
|
215
|
+
// Should have the interface function
|
|
216
|
+
expect(composedAdapter.customMethod).toBe(mockInterface);
|
|
217
|
+
|
|
218
|
+
// Original adapter should be unchanged
|
|
219
|
+
const originalAdapter = adapterRegistry.getOriginalAdapter('testPlatform');
|
|
220
|
+
expect(originalAdapter.customMethod).toBeUndefined();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('should return interface-only adapter when no base adapter is registered', () => {
|
|
224
|
+
const mockInterface1 = jest.fn();
|
|
225
|
+
const mockInterface2 = jest.fn();
|
|
226
|
+
|
|
227
|
+
// Register only interface functions, no base adapter
|
|
228
|
+
adapterRegistry.registerAdapterInterface('interfaceOnlyPlatform', 'method1', mockInterface1);
|
|
229
|
+
adapterRegistry.registerAdapterInterface('interfaceOnlyPlatform', 'method2', mockInterface2);
|
|
230
|
+
|
|
231
|
+
// Get adapter - should return interface-only object
|
|
232
|
+
const interfaceOnlyAdapter = adapterRegistry.getAdapter('interfaceOnlyPlatform');
|
|
233
|
+
|
|
234
|
+
// Should have interface functions
|
|
235
|
+
expect(interfaceOnlyAdapter.method1).toBe(mockInterface1);
|
|
236
|
+
expect(interfaceOnlyAdapter.method2).toBe(mockInterface2);
|
|
237
|
+
|
|
238
|
+
// Should not have base adapter methods
|
|
239
|
+
expect(interfaceOnlyAdapter.getAuthType).toBeUndefined();
|
|
240
|
+
|
|
241
|
+
// Should be a plain object, not inherited from any adapter
|
|
242
|
+
expect(Object.getPrototypeOf(interfaceOnlyAdapter)).toBe(Object.prototype);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('should throw error when no adapter and no interfaces are registered', () => {
|
|
246
|
+
expect(() => {
|
|
247
|
+
adapterRegistry.getAdapter('nonExistentPlatform');
|
|
248
|
+
}).toThrow('Adapter not found for platform: nonExistentPlatform');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('should handle mixed scenarios correctly', () => {
|
|
252
|
+
// Scenario 1: Only interfaces, no adapter
|
|
253
|
+
adapterRegistry.registerAdapterInterface('mixedPlatform', 'interfaceMethod', jest.fn());
|
|
254
|
+
const interfaceOnly = adapterRegistry.getAdapter('mixedPlatform');
|
|
255
|
+
expect(interfaceOnly.interfaceMethod).toBeDefined();
|
|
256
|
+
expect(interfaceOnly.getAuthType).toBeUndefined();
|
|
257
|
+
|
|
258
|
+
// Scenario 2: Add adapter later
|
|
259
|
+
const mockAdapter = {
|
|
260
|
+
getAuthType: () => 'apiKey',
|
|
261
|
+
createCallLog: jest.fn(),
|
|
262
|
+
updateCallLog: jest.fn()
|
|
263
|
+
};
|
|
264
|
+
adapterRegistry.registerAdapter('mixedPlatform', mockAdapter);
|
|
265
|
+
|
|
266
|
+
const composedAdapter = adapterRegistry.getAdapter('mixedPlatform');
|
|
267
|
+
expect(composedAdapter.interfaceMethod).toBeDefined();
|
|
268
|
+
expect(composedAdapter.getAuthType).toBeDefined();
|
|
269
|
+
expect(composedAdapter.getAuthType()).toBe('apiKey');
|
|
270
|
+
});
|
|
271
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
const authHandler = require('../../handlers/auth');
|
|
2
|
+
const adapterRegistry = require('../../adapter/registry');
|
|
3
|
+
|
|
4
|
+
// Mock the adapter registry
|
|
5
|
+
jest.mock('../../adapter/registry');
|
|
6
|
+
|
|
7
|
+
describe('Auth Handler', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// Reset mocks
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
global.testUtils.resetAdapterRegistry();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('onApiKeyLogin', () => {
|
|
15
|
+
test('should handle successful API key login', async () => {
|
|
16
|
+
// Arrange
|
|
17
|
+
const mockUserInfo = {
|
|
18
|
+
successful: true,
|
|
19
|
+
platformUserInfo: {
|
|
20
|
+
id: 'test-user-id',
|
|
21
|
+
name: 'Test User',
|
|
22
|
+
timezoneName: 'America/Los_Angeles',
|
|
23
|
+
timezoneOffset: 0,
|
|
24
|
+
platformAdditionalInfo: {}
|
|
25
|
+
},
|
|
26
|
+
returnMessage: {
|
|
27
|
+
messageType: 'success',
|
|
28
|
+
message: 'Login successful',
|
|
29
|
+
ttl: 1000
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const mockAdapter = global.testUtils.createMockAdapter({
|
|
34
|
+
getBasicAuth: jest.fn().mockReturnValue('dGVzdC1hcGkta2V5Og=='),
|
|
35
|
+
getUserInfo: jest.fn().mockResolvedValue(mockUserInfo)
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
adapterRegistry.getAdapter.mockReturnValue(mockAdapter);
|
|
39
|
+
|
|
40
|
+
const requestData = {
|
|
41
|
+
platform: 'testCRM',
|
|
42
|
+
hostname: 'test.example.com',
|
|
43
|
+
apiKey: 'test-api-key',
|
|
44
|
+
additionalInfo: {}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Act
|
|
48
|
+
const result = await authHandler.onApiKeyLogin(requestData);
|
|
49
|
+
|
|
50
|
+
// Assert
|
|
51
|
+
expect(result.userInfo).toBeDefined();
|
|
52
|
+
expect(result.userInfo.id).toBe('test-user-id');
|
|
53
|
+
expect(result.userInfo.name).toBe('Test User');
|
|
54
|
+
expect(result.returnMessage).toEqual(mockUserInfo.returnMessage);
|
|
55
|
+
expect(mockAdapter.getBasicAuth).toHaveBeenCalledWith({ apiKey: 'test-api-key' });
|
|
56
|
+
expect(mockAdapter.getUserInfo).toHaveBeenCalledWith({
|
|
57
|
+
authHeader: 'Basic dGVzdC1hcGkta2V5Og==',
|
|
58
|
+
hostname: 'test.example.com',
|
|
59
|
+
additionalInfo: {},
|
|
60
|
+
apiKey: 'test-api-key'
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('should handle failed API key login', async () => {
|
|
65
|
+
// Arrange
|
|
66
|
+
const mockUserInfo = {
|
|
67
|
+
successful: false,
|
|
68
|
+
platformUserInfo: null,
|
|
69
|
+
returnMessage: {
|
|
70
|
+
messageType: 'error',
|
|
71
|
+
message: 'Invalid API key',
|
|
72
|
+
ttl: 3000
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const mockAdapter = global.testUtils.createMockAdapter({
|
|
77
|
+
getBasicAuth: jest.fn().mockReturnValue('dGVzdC1hcGkta2V5Og=='),
|
|
78
|
+
getUserInfo: jest.fn().mockResolvedValue(mockUserInfo)
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
adapterRegistry.getAdapter.mockReturnValue(mockAdapter);
|
|
82
|
+
|
|
83
|
+
const requestData = {
|
|
84
|
+
platform: 'testCRM',
|
|
85
|
+
hostname: 'test.example.com',
|
|
86
|
+
apiKey: 'invalid-api-key',
|
|
87
|
+
additionalInfo: {}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Act
|
|
91
|
+
const result = await authHandler.onApiKeyLogin(requestData);
|
|
92
|
+
|
|
93
|
+
// Assert
|
|
94
|
+
expect(result.userInfo).toBeNull();
|
|
95
|
+
expect(result.returnMessage).toEqual(mockUserInfo.returnMessage);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('should throw error when adapter not found', async () => {
|
|
99
|
+
// Arrange
|
|
100
|
+
adapterRegistry.getAdapter.mockImplementation(() => {
|
|
101
|
+
throw new Error('Adapter not found for platform: testCRM');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const requestData = {
|
|
105
|
+
platform: 'testCRM',
|
|
106
|
+
hostname: 'test.example.com',
|
|
107
|
+
apiKey: 'test-api-key',
|
|
108
|
+
additionalInfo: {}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Act & Assert
|
|
112
|
+
await expect(authHandler.onApiKeyLogin(requestData))
|
|
113
|
+
.rejects.toThrow('Adapter not found for platform: testCRM');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('authValidation', () => {
|
|
118
|
+
test('should validate user authentication successfully', async () => {
|
|
119
|
+
// Arrange
|
|
120
|
+
const mockUser = global.testUtils.createMockUser();
|
|
121
|
+
const mockValidationResponse = {
|
|
122
|
+
successful: true,
|
|
123
|
+
returnMessage: {
|
|
124
|
+
messageType: 'success',
|
|
125
|
+
message: 'Authentication valid',
|
|
126
|
+
ttl: 1000
|
|
127
|
+
},
|
|
128
|
+
status: 200
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const mockAdapter = global.testUtils.createMockAdapter({
|
|
132
|
+
getOauthInfo: jest.fn().mockResolvedValue({}),
|
|
133
|
+
authValidation: jest.fn().mockResolvedValue(mockValidationResponse)
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
adapterRegistry.getAdapter.mockReturnValue(mockAdapter);
|
|
137
|
+
|
|
138
|
+
// Mock UserModel.findOne to return a user
|
|
139
|
+
const { UserModel } = require('../../models/userModel');
|
|
140
|
+
jest.spyOn(UserModel, 'findOne').mockResolvedValue(mockUser);
|
|
141
|
+
|
|
142
|
+
// Mock oauth.checkAndRefreshAccessToken
|
|
143
|
+
const oauth = require('../../lib/oauth');
|
|
144
|
+
jest.spyOn(oauth, 'checkAndRefreshAccessToken').mockResolvedValue(mockUser);
|
|
145
|
+
|
|
146
|
+
const requestData = {
|
|
147
|
+
platform: 'testCRM',
|
|
148
|
+
userId: 'test-user-id'
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Act
|
|
152
|
+
const result = await authHandler.authValidation(requestData);
|
|
153
|
+
|
|
154
|
+
// Assert
|
|
155
|
+
expect(result).toEqual({
|
|
156
|
+
...mockValidationResponse,
|
|
157
|
+
failReason: ''
|
|
158
|
+
});
|
|
159
|
+
expect(mockAdapter.authValidation).toHaveBeenCalledWith({ user: mockUser });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('should handle user not found in database', async () => {
|
|
163
|
+
// Arrange
|
|
164
|
+
const mockAdapter = global.testUtils.createMockAdapter();
|
|
165
|
+
adapterRegistry.getAdapter.mockReturnValue(mockAdapter);
|
|
166
|
+
|
|
167
|
+
// Mock UserModel.findOne to return null (user not found)
|
|
168
|
+
const { UserModel } = require('../../models/userModel');
|
|
169
|
+
jest.spyOn(UserModel, 'findOne').mockResolvedValue(null);
|
|
170
|
+
|
|
171
|
+
const requestData = {
|
|
172
|
+
platform: 'testCRM',
|
|
173
|
+
userId: 'non-existent-user'
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Act
|
|
177
|
+
const result = await authHandler.authValidation(requestData);
|
|
178
|
+
|
|
179
|
+
// Assert
|
|
180
|
+
expect(result).toEqual({
|
|
181
|
+
successful: false,
|
|
182
|
+
status: 404,
|
|
183
|
+
failReason: 'App Connect. User not found in database'
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('should handle validation failure', async () => {
|
|
188
|
+
// Arrange
|
|
189
|
+
const mockUser = global.testUtils.createMockUser();
|
|
190
|
+
const mockValidationResponse = {
|
|
191
|
+
successful: false,
|
|
192
|
+
returnMessage: {
|
|
193
|
+
messageType: 'error',
|
|
194
|
+
message: 'Authentication failed',
|
|
195
|
+
ttl: 3000
|
|
196
|
+
},
|
|
197
|
+
status: 401
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const mockAdapter = global.testUtils.createMockAdapter({
|
|
201
|
+
getOauthInfo: jest.fn().mockResolvedValue({}),
|
|
202
|
+
authValidation: jest.fn().mockResolvedValue(mockValidationResponse)
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
adapterRegistry.getAdapter.mockReturnValue(mockAdapter);
|
|
206
|
+
|
|
207
|
+
// Mock UserModel.findOne to return a user
|
|
208
|
+
const { UserModel } = require('../../models/userModel');
|
|
209
|
+
jest.spyOn(UserModel, 'findOne').mockResolvedValue(mockUser);
|
|
210
|
+
|
|
211
|
+
// Mock oauth.checkAndRefreshAccessToken
|
|
212
|
+
const oauth = require('../../lib/oauth');
|
|
213
|
+
jest.spyOn(oauth, 'checkAndRefreshAccessToken').mockResolvedValue(mockUser);
|
|
214
|
+
|
|
215
|
+
const requestData = {
|
|
216
|
+
platform: 'testCRM',
|
|
217
|
+
userId: 'test-user-id'
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Act
|
|
221
|
+
const result = await authHandler.authValidation(requestData);
|
|
222
|
+
|
|
223
|
+
// Assert
|
|
224
|
+
expect(result).toEqual({
|
|
225
|
+
...mockValidationResponse,
|
|
226
|
+
failReason: 'CRM. API failed'
|
|
227
|
+
});
|
|
228
|
+
expect(result.successful).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const jwt = require('../../lib/jwt');
|
|
2
|
+
|
|
3
|
+
describe('JWT Utility', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
// Reset environment
|
|
6
|
+
process.env.APP_SERVER_SECRET_KEY = 'test-secret-key';
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe('generateJwt', () => {
|
|
10
|
+
test('should generate JWT token from payload', () => {
|
|
11
|
+
// Arrange
|
|
12
|
+
const payload = {
|
|
13
|
+
id: 'test-user-id',
|
|
14
|
+
platform: 'testCRM'
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Act
|
|
18
|
+
const token = jwt.generateJwt(payload);
|
|
19
|
+
|
|
20
|
+
// Assert
|
|
21
|
+
expect(token).toBeDefined();
|
|
22
|
+
expect(typeof token).toBe('string');
|
|
23
|
+
expect(token.split('.')).toHaveLength(3); // JWT has 3 parts
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should generate different tokens for different payloads', () => {
|
|
27
|
+
// Arrange
|
|
28
|
+
const payload1 = { id: 'user1', platform: 'testCRM' };
|
|
29
|
+
const payload2 = { id: 'user2', platform: 'testCRM' };
|
|
30
|
+
|
|
31
|
+
// Act
|
|
32
|
+
const token1 = jwt.generateJwt(payload1);
|
|
33
|
+
const token2 = jwt.generateJwt(payload2);
|
|
34
|
+
|
|
35
|
+
// Assert
|
|
36
|
+
expect(token1).not.toBe(token2);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('decodeJwt', () => {
|
|
41
|
+
test('should decode valid JWT token', () => {
|
|
42
|
+
// Arrange
|
|
43
|
+
const payload = {
|
|
44
|
+
id: 'test-user-id',
|
|
45
|
+
platform: 'testCRM'
|
|
46
|
+
};
|
|
47
|
+
const token = jwt.generateJwt(payload);
|
|
48
|
+
|
|
49
|
+
// Act
|
|
50
|
+
const decoded = jwt.decodeJwt(token);
|
|
51
|
+
|
|
52
|
+
// Assert
|
|
53
|
+
expect(decoded).toMatchObject(payload);
|
|
54
|
+
expect(decoded).toHaveProperty('exp');
|
|
55
|
+
expect(decoded).toHaveProperty('iat');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('should return null for invalid token', () => {
|
|
59
|
+
// Arrange
|
|
60
|
+
const invalidToken = 'invalid.jwt.token';
|
|
61
|
+
|
|
62
|
+
// Act
|
|
63
|
+
const decoded = jwt.decodeJwt(invalidToken);
|
|
64
|
+
|
|
65
|
+
// Assert
|
|
66
|
+
expect(decoded).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('should return null for malformed token', () => {
|
|
70
|
+
// Arrange
|
|
71
|
+
const malformedToken = 'not-a-jwt-token';
|
|
72
|
+
|
|
73
|
+
// Act
|
|
74
|
+
const decoded = jwt.decodeJwt(malformedToken);
|
|
75
|
+
|
|
76
|
+
// Assert
|
|
77
|
+
expect(decoded).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('should return null for token with wrong secret', () => {
|
|
81
|
+
// Arrange
|
|
82
|
+
const payload = { id: 'test-user-id', platform: 'testCRM' };
|
|
83
|
+
const token = jwt.generateJwt(payload);
|
|
84
|
+
|
|
85
|
+
// Change secret temporarily
|
|
86
|
+
const originalSecret = process.env.APP_SERVER_SECRET_KEY;
|
|
87
|
+
process.env.APP_SERVER_SECRET_KEY = 'different-secret';
|
|
88
|
+
|
|
89
|
+
// Act
|
|
90
|
+
const decoded = jwt.decodeJwt(token);
|
|
91
|
+
|
|
92
|
+
// Restore secret
|
|
93
|
+
process.env.APP_SERVER_SECRET_KEY = originalSecret;
|
|
94
|
+
|
|
95
|
+
// Assert
|
|
96
|
+
expect(decoded).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('generateJwt and decodeJwt round trip', () => {
|
|
101
|
+
test('should successfully generate and decode complex payload', () => {
|
|
102
|
+
// Arrange
|
|
103
|
+
const complexPayload = {
|
|
104
|
+
id: 'test-user-id',
|
|
105
|
+
platform: 'testCRM',
|
|
106
|
+
timestamp: Date.now(),
|
|
107
|
+
metadata: {
|
|
108
|
+
timezone: 'America/Los_Angeles',
|
|
109
|
+
preferences: {
|
|
110
|
+
autoLog: true,
|
|
111
|
+
callPop: false
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Act
|
|
117
|
+
const token = jwt.generateJwt(complexPayload);
|
|
118
|
+
const decoded = jwt.decodeJwt(token);
|
|
119
|
+
|
|
120
|
+
// Assert
|
|
121
|
+
expect(decoded).toMatchObject(complexPayload);
|
|
122
|
+
expect(decoded).toHaveProperty('exp');
|
|
123
|
+
expect(decoded).toHaveProperty('iat');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('should handle empty payload', () => {
|
|
127
|
+
// Arrange
|
|
128
|
+
const emptyPayload = {};
|
|
129
|
+
|
|
130
|
+
// Act
|
|
131
|
+
const token = jwt.generateJwt(emptyPayload);
|
|
132
|
+
const decoded = jwt.decodeJwt(token);
|
|
133
|
+
|
|
134
|
+
// Assert
|
|
135
|
+
expect(decoded).toMatchObject(emptyPayload);
|
|
136
|
+
expect(decoded).toHaveProperty('exp');
|
|
137
|
+
expect(decoded).toHaveProperty('iat');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('error handling', () => {
|
|
142
|
+
test('should handle missing secret key', () => {
|
|
143
|
+
// Arrange
|
|
144
|
+
const payload = { id: 'test-user-id' };
|
|
145
|
+
delete process.env.APP_SERVER_SECRET_KEY;
|
|
146
|
+
|
|
147
|
+
// Act & Assert
|
|
148
|
+
expect(() => jwt.generateJwt(payload)).toThrow();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('should handle null payload', () => {
|
|
152
|
+
// Act & Assert
|
|
153
|
+
expect(() => jwt.generateJwt(null)).toThrow();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('should handle undefined payload', () => {
|
|
157
|
+
// Act & Assert
|
|
158
|
+
expect(() => jwt.generateJwt(undefined)).toThrow();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|