@barndoor-ai/sdk 0.2.0

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,381 @@
1
+ /**
2
+ * Tests for the main BarndoorSDK client.
3
+ */
4
+
5
+ import * as SDK from '../dist/index.esm.js';
6
+ const { BarndoorSDK, ServerSummary, ServerDetail, HTTPError, ConfigurationError, TokenError } = SDK;
7
+
8
+ // Mock fetch
9
+ const mockFetch = {
10
+ fn: () => {},
11
+ mockResolvedValueOnce: value => {
12
+ mockFetch.fn = () => Promise.resolve(value);
13
+ return mockFetch;
14
+ },
15
+ mockRejectedValueOnce: error => {
16
+ mockFetch.fn = () => Promise.reject(error);
17
+ return mockFetch;
18
+ },
19
+ mockClear: () => {
20
+ mockFetch.fn = () => {};
21
+ mockFetch.calls = [];
22
+ },
23
+ calls: [],
24
+ };
25
+
26
+ global.fetch = (...args) => {
27
+ mockFetch.calls.push(args);
28
+ return mockFetch.fn(...args);
29
+ };
30
+
31
+ beforeEach(() => {
32
+ mockFetch.mockClear();
33
+ });
34
+
35
+ describe('BarndoorSDK Constructor', () => {
36
+ const validToken =
37
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
38
+
39
+ test('creates SDK with valid parameters', () => {
40
+ const sdk = new BarndoorSDK('https://api.example.com', { token: validToken });
41
+
42
+ expect(sdk.base).toBe('https://api.example.com');
43
+ expect(sdk.token).toBe(validToken);
44
+ expect(sdk._closed).toBe(false);
45
+ });
46
+
47
+ test('strips trailing slash from base URL', () => {
48
+ const sdk = new BarndoorSDK('https://api.example.com/', { token: validToken });
49
+ expect(sdk.base).toBe('https://api.example.com');
50
+ });
51
+
52
+ test('methods throw when token not provided', async () => {
53
+ const sdk = new BarndoorSDK('https://api.example.com');
54
+ await expect(sdk.listServers()).rejects.toThrow('No token available');
55
+ });
56
+
57
+ test('throws when empty token is provided', () => {
58
+ expect(() => new BarndoorSDK('https://api.example.com', { token: '' })).toThrow(TokenError);
59
+ });
60
+
61
+ test('validates URL format', () => {
62
+ expect(() => new BarndoorSDK('invalid-url', { token: validToken })).toThrow(ConfigurationError);
63
+
64
+ expect(() => new BarndoorSDK('', { token: validToken })).toThrow(ConfigurationError);
65
+ });
66
+
67
+ test('validates token format', () => {
68
+ expect(() => new BarndoorSDK('https://api.example.com', { token: '' })).toThrow(TokenError);
69
+
70
+ expect(() => new BarndoorSDK('https://api.example.com', { token: 'invalid-jwt' })).toThrow(
71
+ TokenError
72
+ );
73
+ });
74
+
75
+ test('validates timeout parameter', () => {
76
+ expect(
77
+ () =>
78
+ new BarndoorSDK('https://api.example.com', {
79
+ token: validToken,
80
+ timeout: -1,
81
+ })
82
+ ).toThrow(ConfigurationError);
83
+
84
+ expect(
85
+ () =>
86
+ new BarndoorSDK('https://api.example.com', {
87
+ token: validToken,
88
+ timeout: 'invalid',
89
+ })
90
+ ).toThrow(ConfigurationError);
91
+ });
92
+
93
+ test('validates maxRetries parameter', () => {
94
+ expect(
95
+ () =>
96
+ new BarndoorSDK('https://api.example.com', {
97
+ token: validToken,
98
+ maxRetries: -1,
99
+ })
100
+ ).toThrow(ConfigurationError);
101
+
102
+ expect(
103
+ () =>
104
+ new BarndoorSDK('https://api.example.com', {
105
+ token: validToken,
106
+ maxRetries: 1.5,
107
+ })
108
+ ).toThrow(ConfigurationError);
109
+ });
110
+ });
111
+
112
+ describe('BarndoorSDK Methods', () => {
113
+ const validToken =
114
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
115
+ let sdk;
116
+
117
+ beforeEach(() => {
118
+ // Mock environment to skip token validation
119
+ process.env.BARNDOOR_ENV = 'test';
120
+ sdk = new BarndoorSDK('https://api.example.com', { token: validToken });
121
+ });
122
+
123
+ afterEach(async () => {
124
+ if (sdk && typeof sdk.close === 'function') {
125
+ await sdk.close();
126
+ }
127
+ delete process.env.BARNDOOR_ENV;
128
+ });
129
+
130
+ describe('listServers', () => {
131
+ test('returns array of ServerSummary objects', async () => {
132
+ const mockServers = [
133
+ {
134
+ id: '123e4567-e89b-12d3-a456-426614174000',
135
+ name: 'Test Server 1',
136
+ slug: 'test-server-1',
137
+ provider: 'github',
138
+ connection_status: 'connected',
139
+ },
140
+ {
141
+ id: '123e4567-e89b-12d3-a456-426614174001',
142
+ name: 'Test Server 2',
143
+ slug: 'test-server-2',
144
+ provider: null,
145
+ connection_status: 'available',
146
+ },
147
+ ];
148
+
149
+ const mockPaginatedResponse = {
150
+ data: mockServers,
151
+ pagination: {
152
+ page: 1,
153
+ limit: 10,
154
+ total: 2,
155
+ pages: 1,
156
+ previous_page: null,
157
+ next_page: null,
158
+ },
159
+ };
160
+
161
+ mockFetch.mockResolvedValueOnce({
162
+ ok: true,
163
+ json: () => Promise.resolve(mockPaginatedResponse),
164
+ });
165
+
166
+ const servers = await sdk.listServers();
167
+
168
+ expect(servers).toHaveLength(2);
169
+ expect(servers[0]).toBeInstanceOf(ServerSummary);
170
+ expect(servers[0].id).toBe(mockServers[0].id);
171
+ expect(servers[1]).toBeInstanceOf(ServerSummary);
172
+ expect(servers[1].provider).toBeNull();
173
+
174
+ expect(mockFetch.calls.length).toBe(1);
175
+ expect(mockFetch.calls[0][0]).toBe('https://api.example.com/servers');
176
+ expect(mockFetch.calls[0][1]).toEqual(
177
+ expect.objectContaining({
178
+ method: 'GET',
179
+ headers: expect.objectContaining({
180
+ Authorization: `Bearer ${validToken}`,
181
+ }),
182
+ })
183
+ );
184
+ });
185
+
186
+ test('handles empty server list', async () => {
187
+ const mockEmptyResponse = {
188
+ data: [],
189
+ pagination: {
190
+ page: 1,
191
+ limit: 10,
192
+ total: 0,
193
+ pages: 0,
194
+ previous_page: null,
195
+ next_page: null,
196
+ },
197
+ };
198
+
199
+ mockFetch.mockResolvedValueOnce({
200
+ ok: true,
201
+ json: () => Promise.resolve(mockEmptyResponse),
202
+ });
203
+
204
+ const servers = await sdk.listServers();
205
+ expect(servers).toEqual([]);
206
+ });
207
+ });
208
+
209
+ describe('getServer', () => {
210
+ test('returns ServerDetail object', async () => {
211
+ const serverId = '123e4567-e89b-12d3-a456-426614174000';
212
+ const mockServer = {
213
+ id: serverId,
214
+ name: 'Test Server',
215
+ slug: 'test-server',
216
+ provider: 'github',
217
+ connection_status: 'connected',
218
+ url: 'https://api.example.com/mcp',
219
+ };
220
+
221
+ mockFetch.mockResolvedValueOnce({
222
+ ok: true,
223
+ json: () => Promise.resolve(mockServer),
224
+ });
225
+
226
+ const server = await sdk.getServer(serverId);
227
+
228
+ expect(server).toBeInstanceOf(ServerDetail);
229
+ expect(server.id).toBe(serverId);
230
+ expect(server.url).toBe(mockServer.url);
231
+
232
+ expect(mockFetch.calls[0][0]).toBe(`https://api.example.com/servers/${serverId}`);
233
+ expect(mockFetch.calls[0][1]).toEqual(expect.objectContaining({ method: 'GET' }));
234
+ });
235
+
236
+ test('validates server ID format', async () => {
237
+ await expect(sdk.getServer('invalid_uuid!')).rejects.toThrow(
238
+ 'Server ID must be a valid UUID or slug'
239
+ );
240
+
241
+ await expect(sdk.getServer('')).rejects.toThrow('Server ID must be a non-empty string');
242
+ });
243
+ });
244
+
245
+ describe('initiateConnection', () => {
246
+ const serverId = '123e4567-e89b-12d3-a456-426614174000';
247
+
248
+ test('initiates connection without return URL', async () => {
249
+ const mockResponse = {
250
+ connection_id: 'conn-123',
251
+ auth_url: 'https://auth.example.com/oauth/authorize?...',
252
+ state: 'random-state',
253
+ };
254
+
255
+ mockFetch.mockResolvedValueOnce({
256
+ ok: true,
257
+ json: () => Promise.resolve(mockResponse),
258
+ });
259
+
260
+ const result = await sdk.initiateConnection(serverId);
261
+
262
+ expect(result).toEqual(mockResponse);
263
+ expect(mockFetch.calls.length).toBe(1);
264
+ expect(mockFetch.calls[0][0]).toBe(`https://api.example.com/servers/${serverId}/connect`);
265
+ expect(mockFetch.calls[0][1]).toEqual(
266
+ expect.objectContaining({
267
+ method: 'POST',
268
+ body: JSON.stringify({}),
269
+ })
270
+ );
271
+ });
272
+
273
+ test('initiates connection with return URL', async () => {
274
+ const returnUrl = 'https://app.example.com/callback';
275
+ const mockResponse = { auth_url: 'https://auth.example.com/...' };
276
+
277
+ mockFetch.mockResolvedValueOnce({
278
+ ok: true,
279
+ json: () => Promise.resolve(mockResponse),
280
+ });
281
+
282
+ await sdk.initiateConnection(serverId, returnUrl);
283
+
284
+ expect(mockFetch.calls.length).toBe(1);
285
+ expect(mockFetch.calls[0][0]).toBe(
286
+ `https://api.example.com/servers/${serverId}/connect?return_url=${encodeURIComponent(returnUrl)}`
287
+ );
288
+ expect(mockFetch.calls[0][1]).toEqual(expect.any(Object));
289
+ });
290
+
291
+ test('handles OAuth configuration error', async () => {
292
+ mockFetch.mockResolvedValueOnce({
293
+ ok: false,
294
+ status: 500,
295
+ statusText: 'Internal Server Error',
296
+ text: () => Promise.resolve('OAuth server configuration not found'),
297
+ });
298
+
299
+ await expect(sdk.initiateConnection(serverId)).rejects.toThrow(
300
+ 'Server is missing OAuth configuration'
301
+ );
302
+ });
303
+ });
304
+
305
+ describe('getConnectionStatus', () => {
306
+ test('returns connection status', async () => {
307
+ const serverId = '123e4567-e89b-12d3-a456-426614174000';
308
+ const mockResponse = { status: 'connected' };
309
+
310
+ mockFetch.mockResolvedValueOnce({
311
+ ok: true,
312
+ json: () => Promise.resolve(mockResponse),
313
+ });
314
+
315
+ const status = await sdk.getConnectionStatus(serverId);
316
+
317
+ expect(status).toBe('connected');
318
+ expect(mockFetch.calls.length).toBe(1);
319
+ expect(mockFetch.calls[0][0]).toBe(`https://api.example.com/servers/${serverId}/connection`);
320
+ expect(mockFetch.calls[0][1]).toEqual(
321
+ expect.objectContaining({
322
+ method: 'GET',
323
+ })
324
+ );
325
+ });
326
+ });
327
+
328
+ describe('disconnectServer', () => {
329
+ const serverId = '123e4567-e89b-12d3-a456-426614174000';
330
+
331
+ test('successfully disconnects from server', async () => {
332
+ mockFetch.mockResolvedValueOnce({
333
+ ok: true,
334
+ status: 204,
335
+ });
336
+
337
+ await sdk.disconnectServer(serverId);
338
+
339
+ expect(mockFetch.calls.length).toBe(1);
340
+ expect(mockFetch.calls[0][0]).toBe(`https://api.example.com/servers/${serverId}/connection`);
341
+ expect(mockFetch.calls[0][1]).toEqual(
342
+ expect.objectContaining({
343
+ method: 'DELETE',
344
+ })
345
+ );
346
+ });
347
+
348
+ test('throws error when connection not found', async () => {
349
+ mockFetch.mockResolvedValueOnce({
350
+ ok: false,
351
+ status: 404,
352
+ json: () =>
353
+ Promise.resolve({
354
+ error: 'ConnectionNotFound',
355
+ message: 'Connection not found',
356
+ }),
357
+ });
358
+
359
+ await expect(sdk.disconnectServer(serverId)).rejects.toThrow(/Connection not found/i);
360
+ });
361
+
362
+ test('validates server ID format', async () => {
363
+ await expect(sdk.disconnectServer('invalid_server_id!')).rejects.toThrow(
364
+ 'Server ID must be a valid UUID or slug'
365
+ );
366
+ });
367
+ });
368
+
369
+ describe('cleanup', () => {
370
+ test('close() prevents further requests', async () => {
371
+ await sdk.close();
372
+
373
+ await expect(sdk.listServers()).rejects.toThrow('SDK has been closed');
374
+ });
375
+
376
+ test('aclose() is alias for close()', async () => {
377
+ await sdk.aclose();
378
+ expect(sdk._closed).toBe(true);
379
+ });
380
+ });
381
+ });
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Tests for configuration management.
3
+ */
4
+
5
+ import {
6
+ BarndoorConfig,
7
+ getStaticConfig,
8
+ getDynamicConfig,
9
+ ConfigurationError,
10
+ } from '../dist/index.esm.js';
11
+
12
+ // Mock process.env for testing
13
+ const originalEnv = process.env;
14
+
15
+ beforeEach(() => {
16
+ // Reset environment variables
17
+ process.env = { ...originalEnv };
18
+ });
19
+
20
+ afterAll(() => {
21
+ process.env = originalEnv;
22
+ });
23
+
24
+ describe('BarndoorConfig', () => {
25
+ test('creates config with default values', () => {
26
+ const config = new BarndoorConfig();
27
+
28
+ expect(config.authDomain).toBe('auth.barndoor.ai');
29
+ expect(config.clientId).toBe('');
30
+ expect(config.clientSecret).toBe('');
31
+ expect(config.apiAudience).toBe('https://barndoor.ai/');
32
+ expect(config.environment).toBe('production');
33
+ expect(config.promptForLogin).toBe(false);
34
+ expect(config.skipLoginLocal).toBe(false);
35
+ });
36
+
37
+ test('uses environment variables when available', () => {
38
+ process.env.AUTH_DOMAIN = 'custom.auth0.com';
39
+ process.env.AGENT_CLIENT_ID = 'test-client-id';
40
+ process.env.AGENT_CLIENT_SECRET = 'test-client-secret';
41
+ process.env.API_AUDIENCE = 'https://custom.api/';
42
+ process.env.BARNDOOR_ENV = 'development';
43
+
44
+ const config = new BarndoorConfig();
45
+
46
+ expect(config.authDomain).toBe('custom.auth0.com');
47
+ expect(config.clientId).toBe('test-client-id');
48
+ expect(config.clientSecret).toBe('test-client-secret');
49
+ expect(config.apiAudience).toBe('https://custom.api/');
50
+ expect(config.environment).toBe('development');
51
+ });
52
+
53
+ test('constructor options override environment variables', () => {
54
+ process.env.AUTH_DOMAIN = 'env.auth0.com';
55
+
56
+ const config = new BarndoorConfig({
57
+ authDomain: 'override.auth0.com',
58
+ });
59
+
60
+ expect(config.authDomain).toBe('override.auth0.com');
61
+ });
62
+
63
+ test('sets environment-specific defaults for localdev', () => {
64
+ const config = new BarndoorConfig({ environment: 'localdev' });
65
+
66
+ expect(config.apiBaseUrl).toBe('http://localhost:8000');
67
+ expect(config.mcpBaseUrl).toBe('http://localhost:8000');
68
+ });
69
+
70
+ test('sets environment-specific defaults for development', () => {
71
+ const config = new BarndoorConfig({ environment: 'development' });
72
+
73
+ expect(config.apiBaseUrl).toBe('https://api.barndoordev.com');
74
+ expect(config.mcpBaseUrl).toBe('https://{organization_id}.mcp.barndoordev.com');
75
+ });
76
+
77
+ test('sets environment-specific defaults for production', () => {
78
+ const config = new BarndoorConfig({ environment: 'production' });
79
+
80
+ expect(config.apiBaseUrl).toBe('https://api.barndoor.ai');
81
+ expect(config.mcpBaseUrl).toBe('https://{organization_id}.mcp.barndoor.ai');
82
+ });
83
+
84
+ test('respects custom URLs even in specific environments', () => {
85
+ process.env.BARNDOOR_API = 'https://custom.api.com';
86
+ process.env.BARNDOOR_URL = 'https://custom.mcp.com';
87
+
88
+ const config = new BarndoorConfig({ environment: 'localdev' });
89
+
90
+ expect(config.apiBaseUrl).toBe('https://custom.api.com');
91
+ expect(config.mcpBaseUrl).toBe('https://custom.mcp.com');
92
+ });
93
+
94
+ test('MCP env var precedence prefers BARNDOOR_MCP over BARNDOOR_URL', () => {
95
+ process.env.BARNDOOR_MCP = 'https://mcp.preferred.com';
96
+ process.env.BARNDOOR_URL = 'https://mcp.legacy.com';
97
+
98
+ const config = new BarndoorConfig({ environment: 'development' });
99
+ expect(config.mcpBaseUrl).toBe('https://mcp.preferred.com');
100
+ });
101
+
102
+ test('options override API/MCP env vars', () => {
103
+ process.env.BARNDOOR_API = 'https://api.env.com';
104
+ process.env.BARNDOOR_MCP = 'https://mcp.env.com';
105
+
106
+ const config = new BarndoorConfig({
107
+ environment: 'production',
108
+ apiBaseUrl: 'https://api.options.com',
109
+ mcpBaseUrl: 'https://mcp.options.com',
110
+ });
111
+
112
+ expect(config.apiBaseUrl).toBe('https://api.options.com');
113
+ expect(config.mcpBaseUrl).toBe('https://mcp.options.com');
114
+ });
115
+
116
+ test('validation passes for valid config', () => {
117
+ const config = new BarndoorConfig();
118
+ expect(() => config.validate()).not.toThrow();
119
+ });
120
+
121
+ test('validation fails for missing required fields', () => {
122
+ const config = new BarndoorConfig({ authDomain: '' });
123
+ expect(() => config.validate()).toThrow(ConfigurationError);
124
+ expect(() => config.validate()).toThrow('authDomain is required');
125
+ });
126
+ });
127
+
128
+ describe('Static Configuration', () => {
129
+ test('getStaticConfig returns BarndoorConfig instance', () => {
130
+ const config = getStaticConfig();
131
+ expect(config).toBeInstanceOf(BarndoorConfig);
132
+ });
133
+
134
+ test('BarndoorConfig.getStaticConfig returns same as function', () => {
135
+ const config1 = getStaticConfig();
136
+ const config2 = BarndoorConfig.getStaticConfig();
137
+
138
+ expect(config1.authDomain).toBe(config2.authDomain);
139
+ expect(config1.apiAudience).toBe(config2.apiAudience);
140
+ });
141
+ });
142
+
143
+ describe('Dynamic Configuration', () => {
144
+ const mockJwtToken =
145
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwczovL2Jhcm5kb29yLmFpL29yZ2FuaXphdGlvbl9pZCI6InRlc3Qtb3JnIiwiaWF0IjoxNjAwMDAwMDAwLCJleHAiOjE2MDAwMDM2MDB9.signature';
146
+
147
+ test('getDynamicConfig substitutes organization ID', () => {
148
+ const config = getDynamicConfig(mockJwtToken);
149
+
150
+ expect(config.apiBaseUrl).toBe('https://api.barndoor.ai');
151
+ expect(config.mcpBaseUrl).toBe('https://test-org.mcp.barndoor.ai');
152
+ });
153
+
154
+ test('BarndoorConfig.getDynamicConfig works the same', () => {
155
+ const config1 = getDynamicConfig(mockJwtToken);
156
+ const config2 = BarndoorConfig.getDynamicConfig(mockJwtToken);
157
+
158
+ expect(config1.apiBaseUrl).toBe(config2.apiBaseUrl);
159
+ expect(config1.mcpBaseUrl).toBe(config2.mcpBaseUrl);
160
+ });
161
+
162
+ test('throws error for invalid JWT token', () => {
163
+ expect(() => getDynamicConfig('invalid-token')).toThrow(ConfigurationError);
164
+ expect(() => getDynamicConfig('invalid-token')).toThrow('Failed to extract organization ID');
165
+ });
166
+
167
+ test('throws error for JWT without organization ID', () => {
168
+ const tokenWithoutOrgId =
169
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MDAwMDAwMDAsImV4cCI6MTYwMDAwMzYwMH0.signature';
170
+
171
+ expect(() => getDynamicConfig(tokenWithoutOrgId)).toThrow(ConfigurationError);
172
+ expect(() => getDynamicConfig(tokenWithoutOrgId)).toThrow(
173
+ 'No organization information found in token'
174
+ );
175
+ });
176
+ });
177
+
178
+ describe('Environment Detection', () => {
179
+ test('environment priority: MODE > BARNDOOR_ENV > default', () => {
180
+ process.env.MODE = 'test-mode';
181
+ process.env.BARNDOOR_ENV = 'test-barndoor-env';
182
+
183
+ const config = new BarndoorConfig();
184
+ expect(config.environment).toBe('test-mode');
185
+
186
+ delete process.env.MODE;
187
+ const config2 = new BarndoorConfig();
188
+ expect(config2.environment).toBe('test-barndoor-env');
189
+
190
+ delete process.env.BARNDOOR_ENV;
191
+ const config3 = new BarndoorConfig();
192
+ expect(config3.environment).toBe('production');
193
+ });
194
+
195
+ test('case insensitive environment matching', () => {
196
+ const localConfig = new BarndoorConfig({ environment: 'LOCALDEV' });
197
+ expect(localConfig.apiBaseUrl).toBe('http://localhost:8000');
198
+
199
+ const devConfig = new BarndoorConfig({ environment: 'DEV' });
200
+ expect(devConfig.apiBaseUrl).toBe('https://api.barndoordev.com');
201
+ });
202
+ });