@app-connect/core 0.0.1 → 0.0.3

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 ADDED
@@ -0,0 +1,5 @@
1
+ NODE_ENV=test
2
+ APP_SERVER_SECRET_KEY='test-secret-key'
3
+ HASH_KEY='test-hash-key'
4
+ DISABLE_SYNC_DB_TABLE='true'
5
+ DATABASE_URL='sqlite::memory:'
package/README.md CHANGED
@@ -10,7 +10,7 @@ Core package for RingCentral App Connect project providing modular APIs for CRM
10
10
  - **Contact Management**: Find, create, and manage contacts across CRM platforms
11
11
  - **Call Logging**: Comprehensive call and message logging capabilities
12
12
  - **Analytics**: Built-in analytics tracking for all operations
13
- - **Database Integration**: DynamoDB integration with automatic table management
13
+ - **Database Integration**: Sequelize.js ORM with automatic table management
14
14
 
15
15
  ## Installation
16
16
 
@@ -39,6 +39,69 @@ app.get('/my-custom-route', (req, res) => {
39
39
  exports.getServer = () => app;
40
40
  ```
41
41
 
42
+ ### Adapter Interface Registration
43
+
44
+ The adapter registry supports dynamic interface registration, allowing you to extend adapter functionality without modifying the original adapter:
45
+
46
+ ```javascript
47
+ const { adapterRegistry } = require('@app-connect/core');
48
+
49
+ // Register interface functions for a platform
50
+ async function customCreateCallLog({ user, contactInfo, authHeader, callLog, note }) {
51
+ // Custom implementation
52
+ return {
53
+ logId: 'custom-log-id',
54
+ returnMessage: {
55
+ message: 'Call logged with custom implementation',
56
+ messageType: 'success',
57
+ ttl: 2000
58
+ }
59
+ };
60
+ }
61
+
62
+ async function customFindContact({ user, authHeader, phoneNumber }) {
63
+ // Custom implementation
64
+ return [
65
+ {
66
+ id: 'custom-contact-id',
67
+ name: 'Custom Contact',
68
+ type: 'Contact',
69
+ phone: phoneNumber,
70
+ additionalInfo: null
71
+ }
72
+ ];
73
+ }
74
+
75
+ // Register interface functions
76
+ adapterRegistry.registerAdapterInterface('myCRM', 'createCallLog', customCreateCallLog);
77
+ adapterRegistry.registerAdapterInterface('myCRM', 'findContact', customFindContact);
78
+
79
+ // Register the base adapter
80
+ adapterRegistry.registerAdapter('myCRM', myCRMAdapter);
81
+
82
+ // Get composed adapter with interfaces
83
+ const composedAdapter = adapterRegistry.getAdapter('myCRM');
84
+ ```
85
+
86
+ **Interface-Only Adapters (No Base Adapter):**
87
+
88
+ ```javascript
89
+ // Register only interface functions, no base adapter
90
+ adapterRegistry.registerAdapterInterface('interfaceOnlyCRM', 'createCallLog', customCreateCallLog);
91
+ adapterRegistry.registerAdapterInterface('interfaceOnlyCRM', 'findContact', customFindContact);
92
+
93
+ // Get interface-only adapter
94
+ const interfaceOnlyAdapter = adapterRegistry.getAdapter('interfaceOnlyCRM');
95
+ console.log('Interface-only methods:', Object.keys(interfaceOnlyAdapter));
96
+ // Output: ['createCallLog', 'findContact']
97
+
98
+ // Later, you can add a base adapter
99
+ adapterRegistry.registerAdapter('interfaceOnlyCRM', myCRMAdapter);
100
+ const fullAdapter = adapterRegistry.getAdapter('interfaceOnlyCRM');
101
+ console.log('Full adapter methods:', Object.keys(fullAdapter));
102
+ // Output: ['getAuthType', 'getUserInfo', 'updateCallLog', 'unAuthorize', 'createContact', 'createCallLog', 'findContact']
103
+ ```
104
+
42
105
  ### Advanced Usage with Custom Middleware
43
106
 
44
107
  ```javascript
@@ -125,13 +188,84 @@ Registers a CRM adapter.
125
188
  - `adapter` (Object): Adapter implementation
126
189
  - `manifest` (Object, optional): Adapter manifest
127
190
 
191
+ #### `adapterRegistry.registerAdapterInterface(platformName, interfaceName, interfaceFunction)`
192
+ Registers an interface function for a specific platform that will be composed with the adapter at retrieval time.
193
+
194
+ **Parameters:**
195
+ - `platformName` (String): Platform identifier (e.g., 'pipedrive', 'salesforce')
196
+ - `interfaceName` (String): Interface function name (e.g., 'createCallLog', 'findContact')
197
+ - `interfaceFunction` (Function): The interface function to register
198
+
199
+ **Example:**
200
+ ```javascript
201
+ async function customCreateCallLog({ user, contactInfo, authHeader, callLog, note }) {
202
+ // Custom implementation
203
+ return {
204
+ logId: 'custom-log-id',
205
+ returnMessage: {
206
+ message: 'Call logged with custom implementation',
207
+ messageType: 'success',
208
+ ttl: 2000
209
+ }
210
+ };
211
+ }
212
+
213
+ adapterRegistry.registerAdapterInterface('myCRM', 'createCallLog', customCreateCallLog);
214
+ ```
215
+
128
216
  #### `adapterRegistry.getAdapter(name)`
129
- Retrieves a registered adapter.
217
+ Retrieves a registered adapter with composed interfaces.
218
+
219
+ **Parameters:**
220
+ - `name` (String): Adapter name
221
+
222
+ **Returns:** Composed adapter object or interface-only object
223
+
224
+ **Behavior:**
225
+ - If adapter exists and interfaces exist: Returns composed adapter with both
226
+ - If adapter exists but no interfaces: Returns original adapter
227
+ - If no adapter but interfaces exist: Returns object with just interface functions
228
+ - If no adapter and no interfaces: Throws error
229
+
230
+ #### `adapterRegistry.getOriginalAdapter(name)`
231
+ Retrieves the original adapter without any composed interface functions.
130
232
 
131
233
  **Parameters:**
132
234
  - `name` (String): Adapter name
133
235
 
134
- **Returns:** Adapter instance
236
+ **Returns:** Original adapter object
237
+
238
+ #### `adapterRegistry.getPlatformInterfaces(platformName)`
239
+ Returns a Map of registered interface functions for a platform.
240
+
241
+ **Parameters:**
242
+ - `platformName` (String): Platform identifier
243
+
244
+ **Returns:** Map of interface functions
245
+
246
+ #### `adapterRegistry.hasPlatformInterface(platformName, interfaceName)`
247
+ Checks if a specific interface function is registered for a platform.
248
+
249
+ **Parameters:**
250
+ - `platformName` (String): Platform identifier
251
+ - `interfaceName` (String): Interface function name
252
+
253
+ **Returns:** Boolean indicating if interface exists
254
+
255
+ #### `adapterRegistry.unregisterAdapterInterface(platformName, interfaceName)`
256
+ Unregisters an interface function for a platform.
257
+
258
+ **Parameters:**
259
+ - `platformName` (String): Platform identifier
260
+ - `interfaceName` (String): Interface function name
261
+
262
+ #### `adapterRegistry.getAdapterCapabilities(platformName)`
263
+ Gets comprehensive information about an adapter including its capabilities and registered interfaces.
264
+
265
+ **Parameters:**
266
+ - `platformName` (String): Platform identifier
267
+
268
+ **Returns:** Object with adapter capabilities information
135
269
 
136
270
  ### Exported Components
137
271
 
@@ -226,16 +360,41 @@ The core package provides the following API endpoints:
226
360
 
227
361
  The core package uses the following environment variables:
228
362
 
229
- - `DYNAMODB_LOCALHOST` - Local DynamoDB endpoint for development
363
+ - `DATABASE_URL` - Database connection string for Sequelize ORM
230
364
  - `DISABLE_SYNC_DB_TABLE` - Skip database table synchronization
231
365
  - `OVERRIDE_APP_SERVER` - Override app server URL in manifests
232
366
  - `HASH_KEY` - Key for hashing user information
233
367
  - `APP_SERVER_SECRET_KEY` - Server secret key
234
368
  - `IS_PROD` - Production environment flag
369
+ - `DYNAMODB_LOCALHOST` - Local DynamoDB endpoint for development, used for lock cache
235
370
 
236
- ## Architecture
371
+ ## Adapter Interface Registration Benefits
372
+
373
+ ### Key Features
374
+
375
+ - **Composition over Mutation**: Interface functions are composed with adapters at retrieval time, preserving the original adapter
376
+ - **Dynamic Registration**: Register interface functions before or after adapter registration
377
+ - **Immutability**: Original adapter objects remain unchanged
378
+ - **Clean Separation**: Interface functions are kept separate from core adapter logic
379
+ - **Flexibility**: Support for interface-only adapters (no base adapter required)
237
380
 
238
- The core package follows a modular architecture:
381
+ ### Best Practices
382
+
383
+ 1. **Register Required Interfaces**: Register all required interface functions before using the adapter
384
+ 2. **Use Descriptive Names**: Use clear, descriptive names for interface functions
385
+ 3. **Handle Errors**: Implement proper error handling in interface functions
386
+ 4. **Test Composed Adapters**: Test the final composed adapter to ensure interfaces work correctly
387
+ 5. **Document Interfaces**: Document what each interface function does and expects
388
+
389
+ ### Use Cases
390
+
391
+ - **Extending Existing Adapters**: Add new functionality to existing adapters without modification
392
+ - **Progressive Enhancement**: Start with interfaces, add base adapter later
393
+ - **Testing**: Test interface functions separately from base adapters
394
+ - **Modular Development**: Develop interface functions independently
395
+ - **Plugin Architecture**: Create pluggable interface functions for different scenarios
396
+
397
+ ## Architecture
239
398
 
240
399
  ```
241
400
  Core Package
@@ -4,12 +4,67 @@ class AdapterRegistry {
4
4
  this.adapters = new Map();
5
5
  this.manifests = new Map();
6
6
  this.releaseNotes = {};
7
+ this.platformInterfaces = new Map(); // Store interface functions per platform
7
8
  }
8
9
 
9
10
  setDefaultManifest(manifest) {
10
11
  this.manifests.set('default', manifest);
11
12
  }
12
13
 
14
+ /**
15
+ * Register an interface function for a specific platform
16
+ * @param {string} platformName - Platform identifier (e.g., 'pipedrive', 'salesforce')
17
+ * @param {string} interfaceName - Interface function name (e.g., 'createCallLog', 'findContact')
18
+ * @param {Function} interfaceFunction - The interface function to register
19
+ */
20
+ registerAdapterInterface(platformName, interfaceName, interfaceFunction) {
21
+ if (typeof interfaceFunction !== 'function') {
22
+ throw new Error(`Interface function must be a function, got: ${typeof interfaceFunction}`);
23
+ }
24
+
25
+ if (!this.platformInterfaces.has(platformName)) {
26
+ this.platformInterfaces.set(platformName, new Map());
27
+ }
28
+
29
+ const platformInterfaceMap = this.platformInterfaces.get(platformName);
30
+ platformInterfaceMap.set(interfaceName, interfaceFunction);
31
+
32
+ console.log(`Registered interface function: ${platformName}.${interfaceName}`);
33
+ }
34
+
35
+ /**
36
+ * Get registered interface functions for a platform
37
+ * @param {string} platformName - Platform identifier
38
+ * @returns {Map} Map of interface functions
39
+ */
40
+ getPlatformInterfaces(platformName) {
41
+ return this.platformInterfaces.get(platformName) || new Map();
42
+ }
43
+
44
+ /**
45
+ * Check if an interface function is registered for a platform
46
+ * @param {string} platformName - Platform identifier
47
+ * @param {string} interfaceName - Interface function name
48
+ * @returns {boolean} True if interface is registered
49
+ */
50
+ hasPlatformInterface(platformName, interfaceName) {
51
+ const platformInterfaceMap = this.platformInterfaces.get(platformName);
52
+ return platformInterfaceMap ? platformInterfaceMap.has(interfaceName) : false;
53
+ }
54
+
55
+ /**
56
+ * Unregister an interface function for a platform
57
+ * @param {string} platformName - Platform identifier
58
+ * @param {string} interfaceName - Interface function name
59
+ */
60
+ unregisterAdapterInterface(platformName, interfaceName) {
61
+ const platformInterfaceMap = this.platformInterfaces.get(platformName);
62
+ if (platformInterfaceMap && platformInterfaceMap.has(interfaceName)) {
63
+ platformInterfaceMap.delete(interfaceName);
64
+ console.log(`Unregistered interface function: ${platformName}.${interfaceName}`);
65
+ }
66
+ }
67
+
13
68
  /**
14
69
  * Register an adapter with the core system
15
70
  * @param {string} platform - Platform identifier (e.g., 'pipedrive', 'salesforce')
@@ -29,16 +84,62 @@ class AdapterRegistry {
29
84
  }
30
85
 
31
86
  /**
32
- * Get adapter by platform name
87
+ * Get adapter by platform name with composed interfaces
33
88
  * @param {string} platform - Platform identifier
34
- * @returns {Object} Adapter implementation
89
+ * @returns {Object} Composed adapter with interface functions
35
90
  */
36
91
  getAdapter(platform) {
37
92
  const adapter = this.adapters.get(platform);
38
- if (!adapter) {
93
+ const platformInterfaceMap = this.platformInterfaces.get(platform);
94
+
95
+ // If no adapter and no interfaces, throw error
96
+ if (!adapter && (!platformInterfaceMap || platformInterfaceMap.size === 0)) {
39
97
  throw new Error(`Adapter not found for platform: ${platform}`);
40
98
  }
41
- return adapter;
99
+
100
+ // If no adapter but interfaces exist, create a composed object with just interfaces
101
+ if (!adapter && platformInterfaceMap && platformInterfaceMap.size > 0) {
102
+ const composedAdapter = {};
103
+
104
+ // Add interface functions to the composed adapter
105
+ for (const [interfaceName, interfaceFunction] of platformInterfaceMap) {
106
+ composedAdapter[interfaceName] = interfaceFunction;
107
+ }
108
+
109
+ console.log(`Returning interface-only adapter for platform: ${platform}`);
110
+ return composedAdapter;
111
+ }
112
+
113
+ // If adapter exists but no interfaces, return original adapter
114
+ if (adapter && (!platformInterfaceMap || platformInterfaceMap.size === 0)) {
115
+ return adapter;
116
+ }
117
+
118
+ // If both adapter and interfaces exist, create a composed object
119
+ const composedAdapter = Object.create(adapter);
120
+
121
+ // Add interface functions to the composed adapter
122
+ for (const [interfaceName, interfaceFunction] of platformInterfaceMap) {
123
+ // Only add if the interface doesn't already exist in the adapter
124
+ if (!Object.prototype.hasOwnProperty.call(adapter, interfaceName)) {
125
+ composedAdapter[interfaceName] = interfaceFunction;
126
+ }
127
+ }
128
+
129
+ return composedAdapter;
130
+ }
131
+
132
+ /**
133
+ * Get the original adapter without composed interfaces
134
+ * @param {string} platform - Platform identifier
135
+ * @returns {Object} Original adapter implementation
136
+ */
137
+ getOriginalAdapter(platform) {
138
+ const adapter = this.adapters.get(platform);
139
+ if (!adapter) {
140
+ throw new Error(`Adapter not found for platform: ${platform}`);
141
+ }
142
+ return adapter;
42
143
  }
43
144
 
44
145
  /**
@@ -98,6 +199,7 @@ class AdapterRegistry {
98
199
  unregisterAdapter(platform) {
99
200
  this.adapters.delete(platform);
100
201
  this.manifests.delete(platform);
202
+ this.platformInterfaces.delete(platform);
101
203
  console.log(`Unregistered adapter: ${platform}`);
102
204
  }
103
205
 
@@ -108,8 +210,38 @@ class AdapterRegistry {
108
210
  getReleaseNotes(platform) {
109
211
  return this.releaseNotes;
110
212
  }
213
+
214
+ /**
215
+ * Get adapter capabilities summary including composed interfaces
216
+ * @param {string} platform - Platform identifier
217
+ * @returns {Object} Adapter capabilities
218
+ */
219
+ getAdapterCapabilities(platform) {
220
+ const originalAdapter = this.getOriginalAdapter(platform);
221
+ const composedAdapter = this.getAdapter(platform);
222
+ const platformInterfaceMap = this.getPlatformInterfaces(platform);
223
+
224
+ const capabilities = {
225
+ platform,
226
+ originalMethods: Object.keys(originalAdapter),
227
+ composedMethods: Object.keys(composedAdapter),
228
+ registeredInterfaces: Array.from(platformInterfaceMap.keys()),
229
+ authType: null
230
+ };
231
+
232
+ // Get auth type if available
233
+ if (typeof originalAdapter.getAuthType === 'function') {
234
+ try {
235
+ capabilities.authType = originalAdapter.getAuthType();
236
+ } catch (error) {
237
+ capabilities.authType = 'unknown';
238
+ }
239
+ }
240
+
241
+ return capabilities;
242
+ }
111
243
  }
112
244
 
113
245
  // Export singleton instance
114
246
  const adapterRegistry = new AdapterRegistry();
115
- module.exports = adapterRegistry;
247
+ module.exports = adapterRegistry;
package/jest.config.js ADDED
@@ -0,0 +1,57 @@
1
+ const path = require('path');
2
+
3
+ module.exports = {
4
+ // Test environment
5
+ testEnvironment: 'node',
6
+
7
+ // Test file patterns
8
+ testMatch: [
9
+ '<rootDir>/test/**/*.test.js',
10
+ '<rootDir>/**/*.test.js'
11
+ ],
12
+
13
+ // Setup files
14
+ setupFilesAfterEnv: [
15
+ '<rootDir>/test/setup.js'
16
+ ],
17
+
18
+ // Coverage configuration
19
+ collectCoverage: true,
20
+ coverageDirectory: '<rootDir>/coverage',
21
+ coverageReporters: ['text', 'lcov', 'html'],
22
+ coveragePathIgnorePatterns: [
23
+ '/node_modules/',
24
+ '/test/',
25
+ '/coverage/',
26
+ 'jest.config.js',
27
+ 'setup.js'
28
+ ],
29
+
30
+ // Module resolution
31
+ moduleDirectories: ['node_modules', '<rootDir>'],
32
+ moduleNameMapper: {
33
+ '^@app-connect/core/(.*)$': '<rootDir>/$1'
34
+ },
35
+
36
+ // Test timeout
37
+ testTimeout: 30000,
38
+
39
+ // Reporters
40
+ reporters: ['default'],
41
+
42
+ // Ignore patterns
43
+ modulePathIgnorePatterns: [
44
+ '<rootDir>/node_modules/',
45
+ '<rootDir>/coverage/',
46
+ '<rootDir>/test-results/'
47
+ ],
48
+
49
+ // Clear mocks between tests
50
+ clearMocks: true,
51
+
52
+ // Restore mocks between tests
53
+ restoreMocks: true,
54
+
55
+ // Verbose output
56
+ verbose: true
57
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@app-connect/core",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "RingCentral App Connect Core",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -35,6 +35,12 @@
35
35
  "tz-lookup": "^6.1.25",
36
36
  "ua-parser-js": "^1.0.38"
37
37
  },
38
+ "scripts": {
39
+ "test": "jest",
40
+ "test:watch": "jest --watch",
41
+ "test:coverage": "jest --coverage",
42
+ "test:ci": "jest --ci --coverage --watchAll=false"
43
+ },
38
44
  "devDependencies": {
39
45
  "@eslint/js": "^9.22.0",
40
46
  "@octokit/rest": "^19.0.5",
@@ -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
+ });
package/test/setup.js ADDED
@@ -0,0 +1,176 @@
1
+ // Test setup for @app-connect/core package
2
+ const path = require('path');
3
+ require('dotenv').config({ path: path.resolve(__dirname, '../.env.test') });
4
+
5
+ // Set test timeout
6
+ jest.setTimeout(30000);
7
+
8
+ // Mock console methods to reduce noise in tests
9
+ global.console = {
10
+ ...console,
11
+ log: jest.fn(),
12
+ debug: jest.fn(),
13
+ info: jest.fn(),
14
+ warn: jest.fn(),
15
+ error: jest.fn(),
16
+ };
17
+
18
+ // Setup database models for testing
19
+ beforeAll(async () => {
20
+ try {
21
+ // Set up test database URL if not provided
22
+ if (!process.env.DATABASE_URL) {
23
+ process.env.DATABASE_URL = 'sqlite::memory:';
24
+ }
25
+
26
+ // Import models
27
+ const { CallLogModel } = require('../models/callLogModel');
28
+ const { MessageLogModel } = require('../models/messageLogModel');
29
+ const { UserModel } = require('../models/userModel');
30
+ const { CacheModel } = require('../models/cacheModel');
31
+ const { AdminConfigModel } = require('../models/adminConfigModel');
32
+
33
+ // Sync database models
34
+ await CallLogModel.sync({ force: true });
35
+ await MessageLogModel.sync({ force: true });
36
+ await UserModel.sync({ force: true });
37
+ await CacheModel.sync({ force: true });
38
+ await AdminConfigModel.sync({ force: true });
39
+
40
+ console.log('Database models synced for testing');
41
+ } catch (error) {
42
+ console.error('Error setting up test database:', error);
43
+ // Don't fail the setup, some tests might not need database
44
+ }
45
+ });
46
+
47
+ // Clean up after all tests
48
+ afterAll(async () => {
49
+ try {
50
+ // Close database connections
51
+ const { sequelize } = require('../models/sequelize');
52
+ if (sequelize) {
53
+ await sequelize.close();
54
+ }
55
+ } catch (error) {
56
+ console.error('Error closing database connection:', error);
57
+ }
58
+ });
59
+
60
+ // Global test utilities
61
+ global.testUtils = {
62
+ // Helper to create mock user
63
+ createMockUser: (overrides = {}) => ({
64
+ id: 'test-user-id',
65
+ platform: 'testCRM',
66
+ accessToken: 'test-access-token',
67
+ refreshToken: 'test-refresh-token',
68
+ tokenExpiry: new Date(Date.now() + 3600000), // 1 hour from now
69
+ platformUserInfo: {
70
+ id: 'test-platform-user-id',
71
+ name: 'Test User',
72
+ timezoneName: 'America/Los_Angeles',
73
+ timezoneOffset: 0,
74
+ platformAdditionalInfo: {}
75
+ },
76
+ ...overrides
77
+ }),
78
+
79
+ // Helper to create mock call log
80
+ createMockCallLog: (overrides = {}) => ({
81
+ id: 'test-call-log-id',
82
+ userId: 'test-user-id',
83
+ platform: 'testCRM',
84
+ thirdPartyLogId: 'test-third-party-id',
85
+ contactId: 'test-contact-id',
86
+ contactType: 'Contact',
87
+ phoneNumber: '+1234567890',
88
+ callDirection: 'Inbound',
89
+ callResult: 'Answered',
90
+ callDuration: 120,
91
+ callStartTime: new Date(),
92
+ callEndTime: new Date(Date.now() + 120000),
93
+ recordingLink: 'https://example.com/recording.mp3',
94
+ subject: 'Test Call',
95
+ note: 'Test call note',
96
+ ...overrides
97
+ }),
98
+
99
+ // Helper to create mock contact
100
+ createMockContact: (overrides = {}) => ({
101
+ id: 'test-contact-id',
102
+ name: 'Test Contact',
103
+ type: 'Contact',
104
+ phone: '+1234567890',
105
+ additionalInfo: null,
106
+ ...overrides
107
+ }),
108
+
109
+ // Helper to reset adapter registry
110
+ resetAdapterRegistry: () => {
111
+ const adapterRegistry = require('../adapter/registry');
112
+ adapterRegistry.adapters.clear();
113
+ adapterRegistry.manifests.clear();
114
+ adapterRegistry.platformInterfaces.clear();
115
+ adapterRegistry.releaseNotes = {};
116
+ },
117
+
118
+ // Helper to create mock adapter
119
+ createMockAdapter: (overrides = {}) => ({
120
+ getAuthType: jest.fn().mockReturnValue('apiKey'),
121
+ getUserInfo: jest.fn().mockResolvedValue({
122
+ successful: true,
123
+ platformUserInfo: {
124
+ id: 'test-user-id',
125
+ name: 'Test User',
126
+ timezoneName: 'America/Los_Angeles',
127
+ timezoneOffset: 0,
128
+ platformAdditionalInfo: {}
129
+ }
130
+ }),
131
+ createCallLog: jest.fn().mockResolvedValue({
132
+ logId: 'test-log-id',
133
+ returnMessage: {
134
+ message: 'Call logged successfully',
135
+ messageType: 'success',
136
+ ttl: 2000
137
+ }
138
+ }),
139
+ updateCallLog: jest.fn().mockResolvedValue({
140
+ updatedNote: 'Call log updated',
141
+ returnMessage: {
142
+ message: 'Call log updated successfully',
143
+ messageType: 'success',
144
+ ttl: 2000
145
+ }
146
+ }),
147
+ unAuthorize: jest.fn().mockResolvedValue({
148
+ returnMessage: {
149
+ messageType: 'success',
150
+ message: 'Logged out successfully',
151
+ ttl: 1000
152
+ }
153
+ }),
154
+ findContact: jest.fn().mockResolvedValue([
155
+ {
156
+ id: 'test-contact-id',
157
+ name: 'Test Contact',
158
+ type: 'Contact',
159
+ phone: '+1234567890',
160
+ additionalInfo: null
161
+ }
162
+ ]),
163
+ createContact: jest.fn().mockResolvedValue({
164
+ contactInfo: {
165
+ id: 'new-contact-id',
166
+ name: 'New Contact'
167
+ },
168
+ returnMessage: {
169
+ message: 'Contact created successfully',
170
+ messageType: 'success',
171
+ ttl: 2000
172
+ }
173
+ }),
174
+ ...overrides
175
+ })
176
+ };