@lanonasis/cli 3.1.13 → 3.3.15

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,243 @@
1
+ import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
2
+ import { CLIConfig } from '../utils/config.js';
3
+ import * as fs from 'fs/promises';
4
+ import * as path from 'path';
5
+ import * as os from 'os';
6
+ // Mock axios for network calls
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ const mockAxios = {
9
+ get: jest.fn(),
10
+ post: jest.fn()
11
+ };
12
+ jest.mock('axios', () => ({
13
+ default: mockAxios,
14
+ get: mockAxios.get,
15
+ post: mockAxios.post
16
+ }));
17
+ describe('Authentication Persistence Tests', () => {
18
+ let testConfigDir;
19
+ let config;
20
+ beforeEach(async () => {
21
+ // Create a temporary test directory for each test
22
+ testConfigDir = path.join(os.tmpdir(), `test-auth-persistence-${Date.now()}-${Math.random()}`);
23
+ await fs.mkdir(testConfigDir, { recursive: true });
24
+ // Create a new config instance with test directory
25
+ config = new CLIConfig();
26
+ config.configDir = testConfigDir;
27
+ config.configPath = path.join(testConfigDir, 'config.json');
28
+ config.lockFile = path.join(testConfigDir, 'config.lock');
29
+ await config.init();
30
+ // Clear axios mocks
31
+ mockAxios.get.mockClear();
32
+ mockAxios.post.mockClear();
33
+ });
34
+ afterEach(async () => {
35
+ // Clean up test directory
36
+ try {
37
+ await fs.rm(testConfigDir, { recursive: true, force: true });
38
+ }
39
+ catch {
40
+ // Ignore cleanup errors
41
+ }
42
+ });
43
+ describe('Credential Storage and Retrieval', () => {
44
+ it('should store and retrieve vendor key credentials across CLI sessions', async () => {
45
+ const testVendorKey = 'pk_test123456789.sk_test123456789012345';
46
+ // Mock successful server validation
47
+ mockAxios.get.mockResolvedValueOnce({ status: 200, data: { status: 'ok' } });
48
+ // Store vendor key
49
+ await config.setVendorKey(testVendorKey);
50
+ // Verify storage
51
+ expect(config.getVendorKey()).toBe(testVendorKey);
52
+ expect(config.get('authMethod')).toBe('vendor_key');
53
+ expect(config.get('lastValidated')).toBeDefined();
54
+ // Simulate new CLI session by creating new config instance
55
+ const newConfig = new CLIConfig();
56
+ newConfig.configDir = testConfigDir;
57
+ newConfig.configPath = path.join(testConfigDir, 'config.json');
58
+ newConfig.lockFile = path.join(testConfigDir, 'config.lock');
59
+ await newConfig.init();
60
+ // Verify credentials persist across sessions
61
+ expect(newConfig.getVendorKey()).toBe(testVendorKey);
62
+ expect(newConfig.get('authMethod')).toBe('vendor_key');
63
+ });
64
+ it('should store and retrieve JWT token credentials across CLI sessions', async () => {
65
+ const testToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
66
+ // Store JWT token
67
+ await config.setToken(testToken);
68
+ // Verify storage
69
+ expect(config.getToken()).toBe(testToken);
70
+ expect(config.get('authMethod')).toBe('jwt');
71
+ expect(config.get('lastValidated')).toBeDefined();
72
+ // Simulate new CLI session
73
+ const newConfig = new CLIConfig();
74
+ newConfig.configDir = testConfigDir;
75
+ newConfig.configPath = path.join(testConfigDir, 'config.json');
76
+ newConfig.lockFile = path.join(testConfigDir, 'config.lock');
77
+ await newConfig.init();
78
+ // Verify token persists across sessions
79
+ expect(newConfig.getToken()).toBe(testToken);
80
+ expect(newConfig.get('authMethod')).toBe('jwt');
81
+ });
82
+ });
83
+ describe('Authentication Failure Tracking', () => {
84
+ it('should track authentication failure count and last failure time', async () => {
85
+ // Initially no failures
86
+ expect(config.getFailureCount()).toBe(0);
87
+ expect(config.getLastAuthFailure()).toBeUndefined();
88
+ // Increment failure count
89
+ await config.incrementFailureCount();
90
+ // Verify failure count increased
91
+ expect(config.getFailureCount()).toBe(1);
92
+ expect(config.getLastAuthFailure()).toBeDefined();
93
+ // Increment again
94
+ await config.incrementFailureCount();
95
+ // Verify count is now 2
96
+ expect(config.getFailureCount()).toBe(2);
97
+ });
98
+ it('should reset failure count when authentication succeeds', async () => {
99
+ // Add some failures
100
+ await config.incrementFailureCount();
101
+ await config.incrementFailureCount();
102
+ expect(config.getFailureCount()).toBe(2);
103
+ // Reset failure count (simulating successful auth)
104
+ await config.resetFailureCount();
105
+ // Verify reset
106
+ expect(config.getFailureCount()).toBe(0);
107
+ expect(config.getLastAuthFailure()).toBeUndefined();
108
+ });
109
+ it('should apply progressive delays based on failure count', async () => {
110
+ // Initially no delay
111
+ expect(config.shouldDelayAuth()).toBe(false);
112
+ expect(config.getAuthDelayMs()).toBe(0);
113
+ // After 3 failures, should delay
114
+ await config.incrementFailureCount(); // 1
115
+ await config.incrementFailureCount(); // 2
116
+ await config.incrementFailureCount(); // 3
117
+ expect(config.shouldDelayAuth()).toBe(true);
118
+ expect(config.getAuthDelayMs()).toBeGreaterThanOrEqual(1500); // 2000ms ± 25%
119
+ expect(config.getAuthDelayMs()).toBeLessThanOrEqual(2500);
120
+ // After 4 failures, delay should increase
121
+ await config.incrementFailureCount(); // 4
122
+ expect(config.getAuthDelayMs()).toBeGreaterThanOrEqual(3000); // 4000ms ± 25%
123
+ expect(config.getAuthDelayMs()).toBeLessThanOrEqual(5000);
124
+ });
125
+ });
126
+ describe('Vendor Key Validation', () => {
127
+ it('should validate correct vendor key format', () => {
128
+ const validKey = 'pk_test123456789.sk_test123456789012345';
129
+ const result = config.validateVendorKeyFormat(validKey);
130
+ expect(result).toBe(true);
131
+ });
132
+ it('should reject invalid vendor key formats', () => {
133
+ const invalidKeys = [
134
+ 'invalid-key', // no dot
135
+ 'pk_.sk_test123456789012345', // empty public part
136
+ 'pk_test123456789.sk_', // empty secret part
137
+ 'pk_test.sk_test', // too short
138
+ 'pk_test123456789', // missing secret part
139
+ 'sk_test123456789.pk_test123456789', // wrong order
140
+ 'pk_test@invalid.sk_test123456789012345', // invalid chars
141
+ ];
142
+ invalidKeys.forEach(key => {
143
+ const result = config.validateVendorKeyFormat(key);
144
+ expect(result).not.toBe(true);
145
+ expect(typeof result).toBe('string'); // Should return error message
146
+ });
147
+ });
148
+ it('should accept valid vendor key formats', () => {
149
+ const validKeys = [
150
+ 'pk_123456789ABCDEF.sk_1234567890123456789012345',
151
+ 'pk_A0123456789ABC.sk_XYZ123456789012345',
152
+ ];
153
+ validKeys.forEach(key => {
154
+ const result = config.validateVendorKeyFormat(key);
155
+ expect(result).toBe(true);
156
+ });
157
+ });
158
+ });
159
+ describe('Token Expiry Handling', () => {
160
+ it('should detect if JWT token is authenticated', async () => {
161
+ // Valid token (in the future)
162
+ const validToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk5OTk5OTk5OTl9.03fBznX7YvIa8e1GjN0dYF1zR2vZ3xP4wQ5rE6sT7uA';
163
+ await config.setToken(validToken);
164
+ const isAuthenticated = await config.isAuthenticated();
165
+ expect(isAuthenticated).toBe(true);
166
+ });
167
+ it('should detect if JWT token is expired', async () => {
168
+ // Expired token (in the past)
169
+ const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
170
+ await config.setToken(expiredToken);
171
+ const isAuthenticated = await config.isAuthenticated();
172
+ expect(isAuthenticated).toBe(false);
173
+ });
174
+ });
175
+ describe('Device ID Management', () => {
176
+ it('should generate and maintain consistent device ID across sessions', async () => {
177
+ // Get first device ID
178
+ const deviceId1 = await config.getDeviceId();
179
+ expect(deviceId1).toBeDefined();
180
+ expect(typeof deviceId1).toBe('string');
181
+ expect(deviceId1.length).toBeGreaterThan(0);
182
+ // Get device ID again - should be the same
183
+ const deviceId2 = await config.getDeviceId();
184
+ expect(deviceId1).toBe(deviceId2);
185
+ // Simulate new session
186
+ const newConfig = new CLIConfig();
187
+ newConfig.configDir = testConfigDir;
188
+ newConfig.configPath = path.join(testConfigDir, 'config.json');
189
+ newConfig.lockFile = path.join(testConfigDir, 'config.lock');
190
+ await newConfig.init();
191
+ // Device ID should persist across sessions
192
+ const deviceId3 = await newConfig.getDeviceId();
193
+ expect(deviceId1).toBe(deviceId3);
194
+ });
195
+ });
196
+ describe('Configuration Versioning and Migration', () => {
197
+ it('should maintain configuration version compatibility', async () => {
198
+ // Check that config has version
199
+ const version = config.get('version');
200
+ expect(version).toBeDefined();
201
+ expect(version).toBe('1.0.0');
202
+ });
203
+ it('should handle atomic configuration saves', async () => {
204
+ // Set some data
205
+ await config.setAndSave('testKey', 'testValue');
206
+ // Verify it was saved
207
+ expect(config.get('testKey')).toBe('testValue');
208
+ // Create new config instance and verify data persists
209
+ const newConfig = new CLIConfig();
210
+ newConfig.configDir = testConfigDir;
211
+ newConfig.configPath = path.join(testConfigDir, 'config.json');
212
+ newConfig.lockFile = path.join(testConfigDir, 'config.lock');
213
+ await newConfig.init();
214
+ expect(newConfig.get('testKey')).toBe('testValue');
215
+ });
216
+ });
217
+ describe('Credential Validation Against Server', () => {
218
+ it('should validate stored credentials against server', async () => {
219
+ const testVendorKey = 'pk_test123456789.sk_test123456789012345';
220
+ // Mock successful server validation
221
+ mockAxios.get.mockResolvedValue({ status: 200, data: { status: 'ok' } });
222
+ // Set vendor key
223
+ await config.setVendorKey(testVendorKey);
224
+ // Validate credentials
225
+ const isValid = await config.validateStoredCredentials();
226
+ expect(isValid).toBe(true);
227
+ // Verify server was called
228
+ expect(mockAxios.get).toHaveBeenCalledTimes(1);
229
+ });
230
+ it('should return false when credentials are invalid', async () => {
231
+ const testVendorKey = 'pk_test123456789.sk_test123456789012345';
232
+ // Mock failed server validation
233
+ mockAxios.get.mockRejectedValue({ response: { status: 401 } });
234
+ // Set vendor key
235
+ await config.setVendorKey(testVendorKey);
236
+ // Validate credentials
237
+ const isValid = await config.validateStoredCredentials();
238
+ expect(isValid).toBe(false);
239
+ // Verify server was called
240
+ expect(mockAxios.get).toHaveBeenCalledTimes(1);
241
+ });
242
+ });
243
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,305 @@
1
+ import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
2
+ import { CLIConfig } from '../utils/config.js';
3
+ import * as fs from 'fs/promises';
4
+ import * as path from 'path';
5
+ import * as os from 'os';
6
+ // Mock dependencies
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ const mockAxios = {
9
+ get: jest.fn(),
10
+ post: jest.fn()
11
+ };
12
+ jest.mock('axios', () => ({
13
+ default: mockAxios,
14
+ get: mockAxios.get,
15
+ post: mockAxios.post
16
+ }));
17
+ jest.mock('eventsource');
18
+ jest.mock('ws');
19
+ jest.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
20
+ Client: jest.fn().mockImplementation(() => ({
21
+ connect: jest.fn(),
22
+ close: jest.fn(),
23
+ callTool: jest.fn(),
24
+ listTools: jest.fn()
25
+ }))
26
+ }));
27
+ jest.mock('@modelcontextprotocol/sdk/client/stdio.js', () => ({
28
+ StdioClientTransport: jest.fn()
29
+ }));
30
+ describe('Cross-Device Integration Tests', () => {
31
+ let device1Config;
32
+ let device2Config;
33
+ let device3Config;
34
+ let device1Dir;
35
+ let device2Dir;
36
+ let device3Dir;
37
+ beforeEach(async () => {
38
+ // Create separate test directories for each "device"
39
+ device1Dir = path.join(os.tmpdir(), `test-device1-${Date.now()}-${Math.random()}`);
40
+ device2Dir = path.join(os.tmpdir(), `test-device2-${Date.now()}-${Math.random()}`);
41
+ device3Dir = path.join(os.tmpdir(), `test-device3-${Date.now()}-${Math.random()}`);
42
+ await fs.mkdir(device1Dir, { recursive: true });
43
+ await fs.mkdir(device2Dir, { recursive: true });
44
+ await fs.mkdir(device3Dir, { recursive: true });
45
+ // Create config instances for each "device"
46
+ device1Config = new CLIConfig();
47
+ device1Config.configDir = device1Dir;
48
+ device1Config.configPath = path.join(device1Dir, 'config.json');
49
+ device1Config.lockFile = path.join(device1Dir, 'config.lock');
50
+ device2Config = new CLIConfig();
51
+ device2Config.configDir = device2Dir;
52
+ device2Config.configPath = path.join(device2Dir, 'config.json');
53
+ device2Config.lockFile = path.join(device2Dir, 'config.lock');
54
+ device3Config = new CLIConfig();
55
+ device3Config.configDir = device3Dir;
56
+ device3Config.configPath = path.join(device3Dir, 'config.json');
57
+ device3Config.lockFile = path.join(device3Dir, 'config.lock');
58
+ // Initialize all configs
59
+ await device1Config.init();
60
+ await device2Config.init();
61
+ await device3Config.init();
62
+ // Clear axios mocks
63
+ mockAxios.get.mockClear();
64
+ mockAxios.post.mockClear();
65
+ });
66
+ afterEach(async () => {
67
+ // Clean up all test directories
68
+ try {
69
+ await fs.rm(device1Dir, { recursive: true, force: true });
70
+ await fs.rm(device2Dir, { recursive: true, force: true });
71
+ await fs.rm(device3Dir, { recursive: true, force: true });
72
+ }
73
+ catch {
74
+ // Ignore cleanup errors
75
+ }
76
+ });
77
+ describe('Same Credentials Working on Multiple Simulated Devices', () => {
78
+ it('should allow same vendor key to work on multiple devices', async () => {
79
+ const sharedVendorKey = 'pk_shared123456789.sk_shared123456789012345';
80
+ // Mock successful server validation for all devices
81
+ mockAxios.get.mockResolvedValue({ status: 200, data: { status: 'ok' } });
82
+ // Set same vendor key on all devices
83
+ await device1Config.setVendorKey(sharedVendorKey);
84
+ await device2Config.setVendorKey(sharedVendorKey);
85
+ await device3Config.setVendorKey(sharedVendorKey);
86
+ // Verify all devices have the same vendor key
87
+ expect(device1Config.getVendorKey()).toBe(sharedVendorKey);
88
+ expect(device2Config.getVendorKey()).toBe(sharedVendorKey);
89
+ expect(device3Config.getVendorKey()).toBe(sharedVendorKey);
90
+ // Verify all devices have same auth method
91
+ expect(device1Config.get('authMethod')).toBe('vendor_key');
92
+ expect(device2Config.get('authMethod')).toBe('vendor_key');
93
+ expect(device3Config.get('authMethod')).toBe('vendor_key');
94
+ // Verify server validation was called for each device
95
+ expect(mockAxios.get).toHaveBeenCalledTimes(3);
96
+ });
97
+ it('should maintain separate device IDs while sharing credentials', async () => {
98
+ const sharedVendorKey = 'pk_shared123456789.sk_shared123456789012345';
99
+ // Mock successful server validation
100
+ mockAxios.get.mockResolvedValue({ status: 200, data: { status: 'ok' } });
101
+ // Set same vendor key on all devices
102
+ await device1Config.setVendorKey(sharedVendorKey);
103
+ await device2Config.setVendorKey(sharedVendorKey);
104
+ await device3Config.setVendorKey(sharedVendorKey);
105
+ // Get device IDs
106
+ const deviceId1 = await device1Config.getDeviceId();
107
+ const deviceId2 = await device2Config.getDeviceId();
108
+ const deviceId3 = await device3Config.getDeviceId();
109
+ // Device IDs should be different
110
+ expect(deviceId1).not.toBe(deviceId2);
111
+ expect(deviceId1).not.toBe(deviceId3);
112
+ expect(deviceId2).not.toBe(deviceId3);
113
+ // But credentials should be the same
114
+ expect(device1Config.getVendorKey()).toBe(sharedVendorKey);
115
+ expect(device2Config.getVendorKey()).toBe(sharedVendorKey);
116
+ expect(device3Config.getVendorKey()).toBe(sharedVendorKey);
117
+ });
118
+ });
119
+ describe('Service Discovery Consistency Across Environments', () => {
120
+ it('should discover same service endpoints from all devices', async () => {
121
+ // Mock service discovery response
122
+ const mockDiscoveryResponse = {
123
+ auth: { login: 'https://api.lanonasis.com/auth/login' },
124
+ endpoints: {
125
+ http: 'https://mcp.lanonasis.com/api/v1',
126
+ websocket: 'wss://mcp.lanonasis.com/ws',
127
+ sse: 'https://mcp.lanonasis.com/api/v1/events'
128
+ }
129
+ };
130
+ mockAxios.get.mockResolvedValue({ data: mockDiscoveryResponse });
131
+ // Perform service discovery on all devices
132
+ await device1Config.discoverServices();
133
+ await device2Config.discoverServices();
134
+ await device3Config.discoverServices();
135
+ // All devices should have discovered the same endpoints
136
+ const services1 = device1Config.get('discoveredServices');
137
+ const services2 = device2Config.get('discoveredServices');
138
+ const services3 = device3Config.get('discoveredServices');
139
+ expect(services1).toEqual(services2);
140
+ expect(services1).toEqual(services3);
141
+ expect(services2).toEqual(services3);
142
+ // Verify specific endpoints
143
+ expect(services1.mcp_base).toBe('https://mcp.lanonasis.com/api/v1');
144
+ expect(services1.mcp_ws_base).toBe('wss://mcp.lanonasis.com/ws');
145
+ expect(services1.mcp_sse_base).toBe('https://mcp.lanonasis.com/api/v1/events');
146
+ });
147
+ it('should handle service discovery failures consistently', async () => {
148
+ // Mock service discovery failure
149
+ mockAxios.get.mockRejectedValue(new Error('Service discovery failed'));
150
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
151
+ // Attempt service discovery on all devices
152
+ await device1Config.discoverServices(true);
153
+ await device2Config.discoverServices(true);
154
+ await device3Config.discoverServices(true);
155
+ // All devices should fall back to same default endpoints
156
+ const services1 = device1Config.get('discoveredServices');
157
+ const services2 = device2Config.get('discoveredServices');
158
+ const services3 = device3Config.get('discoveredServices');
159
+ expect(services1).toEqual(services2);
160
+ expect(services1).toEqual(services3);
161
+ // Should use fallback endpoints
162
+ expect(services1.auth_base).toBe('https://api.lanonasis.com');
163
+ expect(services1.mcp_base).toBe('https://mcp.lanonasis.com/api/v1');
164
+ expect(services1.mcp_ws_base).toBe('wss://mcp.lanonasis.com/ws');
165
+ consoleSpy.mockRestore();
166
+ });
167
+ });
168
+ describe('Configuration Synchronization and Compatibility', () => {
169
+ it('should maintain configuration version compatibility across devices', async () => {
170
+ // All devices should have the same config version
171
+ expect(device1Config.get('version')).toBe('1.0.0');
172
+ expect(device2Config.get('version')).toBe('1.0.0');
173
+ expect(device3Config.get('version')).toBe('1.0.0');
174
+ // Set some data on device 1
175
+ await device1Config.setAndSave('testData', 'test-value');
176
+ // Read config file directly and apply to device 2
177
+ const configPath1 = device1Config.configPath;
178
+ const configData = JSON.parse(await fs.readFile(configPath1, 'utf-8'));
179
+ const configPath2 = device2Config.configPath;
180
+ await fs.writeFile(configPath2, JSON.stringify(configData, null, 2));
181
+ // Reload device 2 config
182
+ await device2Config.load();
183
+ // Device 2 should have the same data and version
184
+ expect(device2Config.get('testData')).toBe('test-value');
185
+ expect(device2Config.get('version')).toBe('1.0.0');
186
+ });
187
+ it('should create consistent backup files across devices', async () => {
188
+ const testData = { test: 'backup-data', timestamp: Date.now() };
189
+ // Set data on all devices
190
+ await device1Config.setAndSave('backupTest', testData);
191
+ await device2Config.setAndSave('backupTest', testData);
192
+ await device3Config.setAndSave('backupTest', testData);
193
+ // Create backups on all devices
194
+ const backup1 = await device1Config.backupConfig();
195
+ const backup2 = await device2Config.backupConfig();
196
+ const backup3 = await device3Config.backupConfig();
197
+ // All backups should exist
198
+ expect(await fs.access(backup1).then(() => true).catch(() => false)).toBe(true);
199
+ expect(await fs.access(backup2).then(() => true).catch(() => false)).toBe(true);
200
+ expect(await fs.access(backup3).then(() => true).catch(() => false)).toBe(true);
201
+ // All backups should contain the same data
202
+ const backupData1 = JSON.parse(await fs.readFile(backup1, 'utf-8'));
203
+ const backupData2 = JSON.parse(await fs.readFile(backup2, 'utf-8'));
204
+ const backupData3 = JSON.parse(await fs.readFile(backup3, 'utf-8'));
205
+ expect(backupData1.backupTest).toEqual(testData);
206
+ expect(backupData2.backupTest).toEqual(testData);
207
+ expect(backupData3.backupTest).toEqual(testData);
208
+ // All should have same version
209
+ expect(backupData1.version).toBe('1.0.0');
210
+ expect(backupData2.version).toBe('1.0.0');
211
+ expect(backupData3.version).toBe('1.0.0');
212
+ });
213
+ });
214
+ describe('Error Message Consistency Across Different Failure Scenarios', () => {
215
+ it('should provide consistent error messages for authentication failures', async () => {
216
+ const invalidVendorKey = 'pk_invalid.sk_invalid';
217
+ // Mock authentication failure
218
+ mockAxios.get.mockRejectedValue({
219
+ response: { status: 401, data: { error: 'invalid vendor key' } }
220
+ });
221
+ // Attempt to set invalid vendor key on all devices
222
+ const errors = [];
223
+ try {
224
+ await device1Config.setVendorKey(invalidVendorKey);
225
+ }
226
+ catch (error) {
227
+ errors.push(error.message);
228
+ }
229
+ try {
230
+ await device2Config.setVendorKey(invalidVendorKey);
231
+ }
232
+ catch (error) {
233
+ errors.push(error.message);
234
+ }
235
+ try {
236
+ await device3Config.setVendorKey(invalidVendorKey);
237
+ }
238
+ catch (error) {
239
+ errors.push(error.message);
240
+ }
241
+ // All devices should get the same error message
242
+ expect(errors).toHaveLength(3);
243
+ expect(errors[0]).toBe(errors[1]);
244
+ expect(errors[0]).toBe(errors[2]);
245
+ expect(errors[0]).toContain('Vendor key authentication failed');
246
+ });
247
+ it('should provide consistent validation error messages', async () => {
248
+ const invalidFormats = [
249
+ 'invalid-key',
250
+ 'pk_short.sk_short',
251
+ 'pk_.sk_test123456789012345',
252
+ 'pk_test123456789.sk_'
253
+ ];
254
+ for (const invalidKey of invalidFormats) {
255
+ const validationResults = [];
256
+ // Test validation on all devices
257
+ validationResults.push(device1Config.validateVendorKeyFormat(invalidKey));
258
+ validationResults.push(device2Config.validateVendorKeyFormat(invalidKey));
259
+ validationResults.push(device3Config.validateVendorKeyFormat(invalidKey));
260
+ // All devices should return the same validation error
261
+ expect(validationResults[0]).toBe(validationResults[1]);
262
+ expect(validationResults[0]).toBe(validationResults[2]);
263
+ expect(typeof validationResults[0]).toBe('string'); // Should be error message
264
+ }
265
+ });
266
+ it('should maintain consistent failure tracking across devices', async () => {
267
+ const sharedVendorKey = 'pk_shared123456789.sk_shared123456789012345';
268
+ // Mock successful initial setup
269
+ mockAxios.get.mockResolvedValue({ status: 200, data: { status: 'ok' } });
270
+ // Set vendor key on all devices
271
+ await device1Config.setVendorKey(sharedVendorKey);
272
+ await device2Config.setVendorKey(sharedVendorKey);
273
+ await device3Config.setVendorKey(sharedVendorKey);
274
+ // Mock validation failure
275
+ mockAxios.get.mockRejectedValue({
276
+ response: { status: 401, data: { error: 'invalid credentials' } }
277
+ });
278
+ // Validate credentials on all devices (should fail)
279
+ await device1Config.validateStoredCredentials();
280
+ await device2Config.validateStoredCredentials();
281
+ await device3Config.validateStoredCredentials();
282
+ // All devices should have incremented failure count
283
+ expect(device1Config.getFailureCount()).toBe(1);
284
+ expect(device2Config.getFailureCount()).toBe(1);
285
+ expect(device3Config.getFailureCount()).toBe(1);
286
+ // Add more failures to test delay calculation consistency
287
+ await device1Config.incrementFailureCount();
288
+ await device1Config.incrementFailureCount();
289
+ await device2Config.incrementFailureCount();
290
+ await device2Config.incrementFailureCount();
291
+ await device3Config.incrementFailureCount();
292
+ await device3Config.incrementFailureCount();
293
+ // All devices should have same failure count and delay
294
+ expect(device1Config.getFailureCount()).toBe(3);
295
+ expect(device2Config.getFailureCount()).toBe(3);
296
+ expect(device3Config.getFailureCount()).toBe(3);
297
+ expect(device1Config.shouldDelayAuth()).toBe(true);
298
+ expect(device2Config.shouldDelayAuth()).toBe(true);
299
+ expect(device3Config.shouldDelayAuth()).toBe(true);
300
+ expect(device1Config.getAuthDelayMs()).toBe(2000);
301
+ expect(device2Config.getAuthDelayMs()).toBe(2000);
302
+ expect(device3Config.getAuthDelayMs()).toBe(2000);
303
+ });
304
+ });
305
+ });