@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.
- package/dist/__tests__/auth-persistence.test.d.ts +1 -0
- package/dist/__tests__/auth-persistence.test.js +243 -0
- package/dist/__tests__/cross-device-integration.test.d.ts +1 -0
- package/dist/__tests__/cross-device-integration.test.js +305 -0
- package/dist/__tests__/mcp-connection-reliability.test.d.ts +1 -0
- package/dist/__tests__/mcp-connection-reliability.test.js +489 -0
- package/dist/__tests__/setup.d.ts +1 -0
- package/dist/__tests__/setup.js +26 -0
- package/dist/commands/api-keys.js +12 -6
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +420 -50
- package/dist/commands/config.js +519 -1
- package/dist/commands/mcp.js +299 -0
- package/dist/index.js +5 -1
- package/dist/mcp/server/lanonasis-server.d.ts +161 -6
- package/dist/mcp/server/lanonasis-server.js +813 -17
- package/dist/mcp/server/mcp/server/lanonasis-server.js +911 -0
- package/dist/mcp/server/utils/api.js +431 -0
- package/dist/mcp/server/utils/config.js +855 -0
- package/dist/utils/config.d.ts +40 -1
- package/dist/utils/config.js +273 -36
- package/dist/utils/mcp-client.d.ts +83 -2
- package/dist/utils/mcp-client.js +414 -15
- package/package.json +8 -4
|
@@ -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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|