@objectstack/core 0.8.2 → 0.9.1
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/API_REGISTRY.md +392 -0
- package/CHANGELOG.md +8 -0
- package/README.md +36 -0
- package/dist/api-registry-plugin.d.ts +54 -0
- package/dist/api-registry-plugin.d.ts.map +1 -0
- package/dist/api-registry-plugin.js +53 -0
- package/dist/api-registry-plugin.test.d.ts +2 -0
- package/dist/api-registry-plugin.test.d.ts.map +1 -0
- package/dist/api-registry-plugin.test.js +332 -0
- package/dist/api-registry.d.ts +259 -0
- package/dist/api-registry.d.ts.map +1 -0
- package/dist/api-registry.js +599 -0
- package/dist/api-registry.test.d.ts +2 -0
- package/dist/api-registry.test.d.ts.map +1 -0
- package/dist/api-registry.test.js +957 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +35 -11
- package/dist/plugin-loader.d.ts +3 -2
- package/dist/plugin-loader.d.ts.map +1 -1
- package/dist/plugin-loader.js +13 -11
- package/dist/qa/adapter.d.ts +14 -0
- package/dist/qa/adapter.d.ts.map +1 -0
- package/dist/qa/adapter.js +1 -0
- package/dist/qa/http-adapter.d.ts +16 -0
- package/dist/qa/http-adapter.d.ts.map +1 -0
- package/dist/qa/http-adapter.js +107 -0
- package/dist/qa/index.d.ts +4 -0
- package/dist/qa/index.d.ts.map +1 -0
- package/dist/qa/index.js +3 -0
- package/dist/qa/runner.d.ts +27 -0
- package/dist/qa/runner.d.ts.map +1 -0
- package/dist/qa/runner.js +157 -0
- package/dist/security/index.d.ts +14 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +13 -0
- package/dist/security/plugin-config-validator.d.ts +79 -0
- package/dist/security/plugin-config-validator.d.ts.map +1 -0
- package/dist/security/plugin-config-validator.js +166 -0
- package/dist/security/plugin-config-validator.test.d.ts +2 -0
- package/dist/security/plugin-config-validator.test.d.ts.map +1 -0
- package/dist/security/plugin-config-validator.test.js +223 -0
- package/dist/security/plugin-permission-enforcer.d.ts +154 -0
- package/dist/security/plugin-permission-enforcer.d.ts.map +1 -0
- package/dist/security/plugin-permission-enforcer.js +323 -0
- package/dist/security/plugin-permission-enforcer.test.d.ts +2 -0
- package/dist/security/plugin-permission-enforcer.test.d.ts.map +1 -0
- package/dist/security/plugin-permission-enforcer.test.js +205 -0
- package/dist/security/plugin-signature-verifier.d.ts +96 -0
- package/dist/security/plugin-signature-verifier.d.ts.map +1 -0
- package/dist/security/plugin-signature-verifier.js +250 -0
- package/examples/api-registry-example.ts +557 -0
- package/package.json +2 -2
- package/src/api-registry-plugin.test.ts +391 -0
- package/src/api-registry-plugin.ts +86 -0
- package/src/api-registry.test.ts +1089 -0
- package/src/api-registry.ts +736 -0
- package/src/index.ts +6 -0
- package/src/logger.ts +36 -11
- package/src/plugin-loader.ts +17 -13
- package/src/qa/adapter.ts +14 -0
- package/src/qa/http-adapter.ts +114 -0
- package/src/qa/index.ts +3 -0
- package/src/qa/runner.ts +179 -0
- package/src/security/index.ts +29 -0
- package/src/security/plugin-config-validator.test.ts +276 -0
- package/src/security/plugin-config-validator.ts +191 -0
- package/src/security/plugin-permission-enforcer.test.ts +251 -0
- package/src/security/plugin-permission-enforcer.ts +408 -0
- package/src/security/plugin-signature-verifier.ts +359 -0
|
@@ -0,0 +1,1089 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { ApiRegistry } from './api-registry';
|
|
3
|
+
import type {
|
|
4
|
+
ApiRegistryEntryInput,
|
|
5
|
+
} from '@objectstack/spec/api';
|
|
6
|
+
import type { Logger } from '@objectstack/spec/contracts';
|
|
7
|
+
|
|
8
|
+
// Mock logger
|
|
9
|
+
const createMockLogger = (): Logger => ({
|
|
10
|
+
debug: vi.fn(),
|
|
11
|
+
info: vi.fn(),
|
|
12
|
+
warn: vi.fn(),
|
|
13
|
+
error: vi.fn(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('ApiRegistry', () => {
|
|
17
|
+
let registry: ApiRegistry;
|
|
18
|
+
let logger: Logger;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
logger = createMockLogger();
|
|
22
|
+
registry = new ApiRegistry(logger, 'error', '1.0.0');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('Constructor', () => {
|
|
26
|
+
it('should create registry with default conflict resolution', () => {
|
|
27
|
+
const reg = new ApiRegistry(logger);
|
|
28
|
+
const snapshot = reg.getRegistry();
|
|
29
|
+
expect(snapshot.conflictResolution).toBe('error');
|
|
30
|
+
expect(snapshot.version).toBe('1.0.0');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should create registry with custom conflict resolution', () => {
|
|
34
|
+
const reg = new ApiRegistry(logger, 'priority', '2.0.0');
|
|
35
|
+
const snapshot = reg.getRegistry();
|
|
36
|
+
expect(snapshot.conflictResolution).toBe('priority');
|
|
37
|
+
expect(snapshot.version).toBe('2.0.0');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('registerApi', () => {
|
|
42
|
+
it('should register a simple REST API', () => {
|
|
43
|
+
const api: ApiRegistryEntryInput = {
|
|
44
|
+
id: 'customer_api',
|
|
45
|
+
name: 'Customer API',
|
|
46
|
+
type: 'rest',
|
|
47
|
+
version: 'v1',
|
|
48
|
+
basePath: '/api/v1/customers',
|
|
49
|
+
endpoints: [
|
|
50
|
+
{
|
|
51
|
+
id: 'get_customer',
|
|
52
|
+
method: 'GET',
|
|
53
|
+
path: '/api/v1/customers/:id',
|
|
54
|
+
summary: 'Get customer by ID',
|
|
55
|
+
responses: [
|
|
56
|
+
{
|
|
57
|
+
statusCode: 200,
|
|
58
|
+
description: 'Success',
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
registry.registerApi(api);
|
|
66
|
+
|
|
67
|
+
const retrieved = registry.getApi('customer_api');
|
|
68
|
+
expect(retrieved).toBeDefined();
|
|
69
|
+
expect(retrieved?.name).toBe('Customer API');
|
|
70
|
+
expect(retrieved?.endpoints.length).toBe(1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should throw error when registering duplicate API', () => {
|
|
74
|
+
const api: ApiRegistryEntryInput = {
|
|
75
|
+
id: 'test_api',
|
|
76
|
+
name: 'Test API',
|
|
77
|
+
type: 'rest',
|
|
78
|
+
version: 'v1',
|
|
79
|
+
basePath: '/api/test',
|
|
80
|
+
endpoints: [],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
registry.registerApi(api);
|
|
84
|
+
|
|
85
|
+
expect(() => registry.registerApi(api)).toThrow(
|
|
86
|
+
"API 'test_api' already registered"
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should register API with multiple endpoints', () => {
|
|
91
|
+
const api: ApiRegistryEntryInput = {
|
|
92
|
+
id: 'crud_api',
|
|
93
|
+
name: 'CRUD API',
|
|
94
|
+
type: 'rest',
|
|
95
|
+
version: 'v1',
|
|
96
|
+
basePath: '/api/v1/data',
|
|
97
|
+
endpoints: [
|
|
98
|
+
{
|
|
99
|
+
id: 'create',
|
|
100
|
+
method: 'POST',
|
|
101
|
+
path: '/api/v1/data',
|
|
102
|
+
summary: 'Create record',
|
|
103
|
+
responses: [],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'read',
|
|
107
|
+
method: 'GET',
|
|
108
|
+
path: '/api/v1/data/:id',
|
|
109
|
+
summary: 'Read record',
|
|
110
|
+
responses: [],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'update',
|
|
114
|
+
method: 'PUT',
|
|
115
|
+
path: '/api/v1/data/:id',
|
|
116
|
+
summary: 'Update record',
|
|
117
|
+
responses: [],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'delete',
|
|
121
|
+
method: 'DELETE',
|
|
122
|
+
path: '/api/v1/data/:id',
|
|
123
|
+
summary: 'Delete record',
|
|
124
|
+
responses: [],
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
registry.registerApi(api);
|
|
130
|
+
|
|
131
|
+
const stats = registry.getStats();
|
|
132
|
+
expect(stats.totalApis).toBe(1);
|
|
133
|
+
expect(stats.totalEndpoints).toBe(4);
|
|
134
|
+
expect(stats.totalRoutes).toBe(4);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should register API with RBAC permissions', () => {
|
|
138
|
+
const api: ApiRegistryEntryInput = {
|
|
139
|
+
id: 'protected_api',
|
|
140
|
+
name: 'Protected API',
|
|
141
|
+
type: 'rest',
|
|
142
|
+
version: 'v1',
|
|
143
|
+
basePath: '/api/protected',
|
|
144
|
+
endpoints: [
|
|
145
|
+
{
|
|
146
|
+
id: 'admin_only',
|
|
147
|
+
method: 'POST',
|
|
148
|
+
path: '/api/protected/admin',
|
|
149
|
+
summary: 'Admin endpoint',
|
|
150
|
+
requiredPermissions: ['admin.access', 'api_enabled'],
|
|
151
|
+
responses: [],
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
registry.registerApi(api);
|
|
157
|
+
|
|
158
|
+
const endpoint = registry.getEndpoint('protected_api', 'admin_only');
|
|
159
|
+
expect(endpoint?.requiredPermissions).toEqual(['admin.access', 'api_enabled']);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('unregisterApi', () => {
|
|
164
|
+
it('should unregister an API', () => {
|
|
165
|
+
const api: ApiRegistryEntryInput = {
|
|
166
|
+
id: 'temp_api',
|
|
167
|
+
name: 'Temporary API',
|
|
168
|
+
type: 'rest',
|
|
169
|
+
version: 'v1',
|
|
170
|
+
basePath: '/api/temp',
|
|
171
|
+
endpoints: [
|
|
172
|
+
{
|
|
173
|
+
id: 'test',
|
|
174
|
+
method: 'GET',
|
|
175
|
+
path: '/api/temp/test',
|
|
176
|
+
responses: [],
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
registry.registerApi(api);
|
|
182
|
+
expect(registry.getApi('temp_api')).toBeDefined();
|
|
183
|
+
|
|
184
|
+
registry.unregisterApi('temp_api');
|
|
185
|
+
expect(registry.getApi('temp_api')).toBeUndefined();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should throw error when unregistering non-existent API', () => {
|
|
189
|
+
expect(() => registry.unregisterApi('nonexistent')).toThrow(
|
|
190
|
+
"API 'nonexistent' not found"
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('Route Conflict Detection', () => {
|
|
196
|
+
describe('error strategy', () => {
|
|
197
|
+
it('should throw error on route conflict', () => {
|
|
198
|
+
const api1: ApiRegistryEntryInput = {
|
|
199
|
+
id: 'api1',
|
|
200
|
+
name: 'API 1',
|
|
201
|
+
type: 'rest',
|
|
202
|
+
version: 'v1',
|
|
203
|
+
basePath: '/api/v1',
|
|
204
|
+
endpoints: [
|
|
205
|
+
{
|
|
206
|
+
id: 'endpoint1',
|
|
207
|
+
method: 'GET',
|
|
208
|
+
path: '/api/v1/test',
|
|
209
|
+
responses: [],
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const api2: ApiRegistryEntryInput = {
|
|
215
|
+
id: 'api2',
|
|
216
|
+
name: 'API 2',
|
|
217
|
+
type: 'rest',
|
|
218
|
+
version: 'v1',
|
|
219
|
+
basePath: '/api/v1',
|
|
220
|
+
endpoints: [
|
|
221
|
+
{
|
|
222
|
+
id: 'endpoint2',
|
|
223
|
+
method: 'GET',
|
|
224
|
+
path: '/api/v1/test', // Same route!
|
|
225
|
+
responses: [],
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
registry.registerApi(api1);
|
|
231
|
+
expect(() => registry.registerApi(api2)).toThrow(/Route conflict detected/);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should allow same path with different methods', () => {
|
|
235
|
+
const api: ApiRegistryEntryInput = {
|
|
236
|
+
id: 'multi_method',
|
|
237
|
+
name: 'Multi Method API',
|
|
238
|
+
type: 'rest',
|
|
239
|
+
version: 'v1',
|
|
240
|
+
basePath: '/api/v1',
|
|
241
|
+
endpoints: [
|
|
242
|
+
{
|
|
243
|
+
id: 'get',
|
|
244
|
+
method: 'GET',
|
|
245
|
+
path: '/api/v1/resource',
|
|
246
|
+
responses: [],
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: 'post',
|
|
250
|
+
method: 'POST',
|
|
251
|
+
path: '/api/v1/resource',
|
|
252
|
+
responses: [],
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: 'put',
|
|
256
|
+
method: 'PUT',
|
|
257
|
+
path: '/api/v1/resource',
|
|
258
|
+
responses: [],
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
expect(() => registry.registerApi(api)).not.toThrow();
|
|
264
|
+
expect(registry.getStats().totalRoutes).toBe(3);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('priority strategy', () => {
|
|
269
|
+
beforeEach(() => {
|
|
270
|
+
registry = new ApiRegistry(logger, 'priority');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should prefer higher priority endpoint', () => {
|
|
274
|
+
const api1: ApiRegistryEntryInput = {
|
|
275
|
+
id: 'low_priority',
|
|
276
|
+
name: 'Low Priority API',
|
|
277
|
+
type: 'rest',
|
|
278
|
+
version: 'v1',
|
|
279
|
+
basePath: '/api',
|
|
280
|
+
endpoints: [
|
|
281
|
+
{
|
|
282
|
+
id: 'low',
|
|
283
|
+
method: 'GET',
|
|
284
|
+
path: '/api/test',
|
|
285
|
+
priority: 100,
|
|
286
|
+
responses: [],
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const api2: ApiRegistryEntryInput = {
|
|
292
|
+
id: 'high_priority',
|
|
293
|
+
name: 'High Priority API',
|
|
294
|
+
type: 'rest',
|
|
295
|
+
version: 'v1',
|
|
296
|
+
basePath: '/api',
|
|
297
|
+
endpoints: [
|
|
298
|
+
{
|
|
299
|
+
id: 'high',
|
|
300
|
+
method: 'GET',
|
|
301
|
+
path: '/api/test',
|
|
302
|
+
priority: 500,
|
|
303
|
+
responses: [],
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
registry.registerApi(api1);
|
|
309
|
+
registry.registerApi(api2); // Should replace low priority
|
|
310
|
+
|
|
311
|
+
const result = registry.findEndpointByRoute('GET', '/api/test');
|
|
312
|
+
expect(result?.api.id).toBe('high_priority');
|
|
313
|
+
expect(result?.endpoint.id).toBe('high');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should keep higher priority when registering lower priority', () => {
|
|
317
|
+
const api1: ApiRegistryEntryInput = {
|
|
318
|
+
id: 'high_priority',
|
|
319
|
+
name: 'High Priority API',
|
|
320
|
+
type: 'rest',
|
|
321
|
+
version: 'v1',
|
|
322
|
+
basePath: '/api',
|
|
323
|
+
endpoints: [
|
|
324
|
+
{
|
|
325
|
+
id: 'high',
|
|
326
|
+
method: 'GET',
|
|
327
|
+
path: '/api/test',
|
|
328
|
+
priority: 900,
|
|
329
|
+
responses: [],
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const api2: ApiRegistryEntryInput = {
|
|
335
|
+
id: 'low_priority',
|
|
336
|
+
name: 'Low Priority API',
|
|
337
|
+
type: 'rest',
|
|
338
|
+
version: 'v1',
|
|
339
|
+
basePath: '/api',
|
|
340
|
+
endpoints: [
|
|
341
|
+
{
|
|
342
|
+
id: 'low',
|
|
343
|
+
method: 'GET',
|
|
344
|
+
path: '/api/test',
|
|
345
|
+
priority: 100,
|
|
346
|
+
responses: [],
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
registry.registerApi(api1);
|
|
352
|
+
registry.registerApi(api2); // Should NOT replace
|
|
353
|
+
|
|
354
|
+
const result = registry.findEndpointByRoute('GET', '/api/test');
|
|
355
|
+
expect(result?.api.id).toBe('high_priority');
|
|
356
|
+
expect(result?.endpoint.id).toBe('high');
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe('first-wins strategy', () => {
|
|
361
|
+
beforeEach(() => {
|
|
362
|
+
registry = new ApiRegistry(logger, 'first-wins');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should keep first registered endpoint', () => {
|
|
366
|
+
const api1: ApiRegistryEntryInput = {
|
|
367
|
+
id: 'first',
|
|
368
|
+
name: 'First API',
|
|
369
|
+
type: 'rest',
|
|
370
|
+
version: 'v1',
|
|
371
|
+
basePath: '/api',
|
|
372
|
+
endpoints: [
|
|
373
|
+
{
|
|
374
|
+
id: 'first_endpoint',
|
|
375
|
+
method: 'GET',
|
|
376
|
+
path: '/api/test',
|
|
377
|
+
responses: [],
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const api2: ApiRegistryEntryInput = {
|
|
383
|
+
id: 'second',
|
|
384
|
+
name: 'Second API',
|
|
385
|
+
type: 'rest',
|
|
386
|
+
version: 'v1',
|
|
387
|
+
basePath: '/api',
|
|
388
|
+
endpoints: [
|
|
389
|
+
{
|
|
390
|
+
id: 'second_endpoint',
|
|
391
|
+
method: 'GET',
|
|
392
|
+
path: '/api/test',
|
|
393
|
+
responses: [],
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
registry.registerApi(api1);
|
|
399
|
+
registry.registerApi(api2);
|
|
400
|
+
|
|
401
|
+
const result = registry.findEndpointByRoute('GET', '/api/test');
|
|
402
|
+
expect(result?.api.id).toBe('first');
|
|
403
|
+
expect(result?.endpoint.id).toBe('first_endpoint');
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
describe('last-wins strategy', () => {
|
|
408
|
+
beforeEach(() => {
|
|
409
|
+
registry = new ApiRegistry(logger, 'last-wins');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('should use last registered endpoint', () => {
|
|
413
|
+
const api1: ApiRegistryEntryInput = {
|
|
414
|
+
id: 'first',
|
|
415
|
+
name: 'First API',
|
|
416
|
+
type: 'rest',
|
|
417
|
+
version: 'v1',
|
|
418
|
+
basePath: '/api',
|
|
419
|
+
endpoints: [
|
|
420
|
+
{
|
|
421
|
+
id: 'first_endpoint',
|
|
422
|
+
method: 'GET',
|
|
423
|
+
path: '/api/test',
|
|
424
|
+
responses: [],
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const api2: ApiRegistryEntryInput = {
|
|
430
|
+
id: 'second',
|
|
431
|
+
name: 'Second API',
|
|
432
|
+
type: 'rest',
|
|
433
|
+
version: 'v1',
|
|
434
|
+
basePath: '/api',
|
|
435
|
+
endpoints: [
|
|
436
|
+
{
|
|
437
|
+
id: 'second_endpoint',
|
|
438
|
+
method: 'GET',
|
|
439
|
+
path: '/api/test',
|
|
440
|
+
responses: [],
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
registry.registerApi(api1);
|
|
446
|
+
registry.registerApi(api2);
|
|
447
|
+
|
|
448
|
+
const result = registry.findEndpointByRoute('GET', '/api/test');
|
|
449
|
+
expect(result?.api.id).toBe('second');
|
|
450
|
+
expect(result?.endpoint.id).toBe('second_endpoint');
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
describe('findApis', () => {
|
|
456
|
+
beforeEach(() => {
|
|
457
|
+
// Register multiple APIs for testing
|
|
458
|
+
registry.registerApi({
|
|
459
|
+
id: 'rest_api',
|
|
460
|
+
name: 'REST API',
|
|
461
|
+
type: 'rest',
|
|
462
|
+
version: 'v1',
|
|
463
|
+
basePath: '/api/v1/rest',
|
|
464
|
+
endpoints: [],
|
|
465
|
+
metadata: {
|
|
466
|
+
status: 'active',
|
|
467
|
+
tags: ['data', 'crud'],
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
registry.registerApi({
|
|
472
|
+
id: 'graphql_api',
|
|
473
|
+
name: 'GraphQL API',
|
|
474
|
+
type: 'graphql',
|
|
475
|
+
version: 'v1',
|
|
476
|
+
basePath: '/graphql',
|
|
477
|
+
endpoints: [],
|
|
478
|
+
metadata: {
|
|
479
|
+
status: 'active',
|
|
480
|
+
tags: ['query', 'data'],
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
registry.registerApi({
|
|
485
|
+
id: 'deprecated_api',
|
|
486
|
+
name: 'Deprecated API',
|
|
487
|
+
type: 'rest',
|
|
488
|
+
version: 'v0',
|
|
489
|
+
basePath: '/api/v0/old',
|
|
490
|
+
endpoints: [],
|
|
491
|
+
metadata: {
|
|
492
|
+
status: 'deprecated',
|
|
493
|
+
tags: ['legacy'],
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('should find all APIs with empty query', () => {
|
|
499
|
+
const result = registry.findApis({});
|
|
500
|
+
expect(result.total).toBe(3);
|
|
501
|
+
expect(result.apis.length).toBe(3);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('should filter by type', () => {
|
|
505
|
+
const result = registry.findApis({ type: 'rest' });
|
|
506
|
+
expect(result.total).toBe(2);
|
|
507
|
+
expect(result.apis.every((api) => api.type === 'rest')).toBe(true);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('should filter by status', () => {
|
|
511
|
+
const result = registry.findApis({ status: 'active' });
|
|
512
|
+
expect(result.total).toBe(2);
|
|
513
|
+
expect(result.apis.every((api) => api.metadata?.status === 'active')).toBe(true);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('should filter by version', () => {
|
|
517
|
+
const result = registry.findApis({ version: 'v1' });
|
|
518
|
+
expect(result.total).toBe(2);
|
|
519
|
+
expect(result.apis.every((api) => api.version === 'v1')).toBe(true);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('should filter by tags (ANY match)', () => {
|
|
523
|
+
const result = registry.findApis({ tags: ['data'] });
|
|
524
|
+
expect(result.total).toBe(2);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('should search in name and description', () => {
|
|
528
|
+
const result = registry.findApis({ search: 'graphql' });
|
|
529
|
+
expect(result.total).toBe(1);
|
|
530
|
+
expect(result.apis[0].id).toBe('graphql_api');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('should combine multiple filters', () => {
|
|
534
|
+
const result = registry.findApis({
|
|
535
|
+
type: 'rest',
|
|
536
|
+
status: 'active',
|
|
537
|
+
tags: ['crud'],
|
|
538
|
+
});
|
|
539
|
+
expect(result.total).toBe(1);
|
|
540
|
+
expect(result.apis[0].id).toBe('rest_api');
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
describe('getEndpoint', () => {
|
|
545
|
+
it('should get endpoint by API and endpoint ID', () => {
|
|
546
|
+
const api: ApiRegistryEntryInput = {
|
|
547
|
+
id: 'test_api',
|
|
548
|
+
name: 'Test API',
|
|
549
|
+
type: 'rest',
|
|
550
|
+
version: 'v1',
|
|
551
|
+
basePath: '/api/test',
|
|
552
|
+
endpoints: [
|
|
553
|
+
{
|
|
554
|
+
id: 'test_endpoint',
|
|
555
|
+
method: 'GET',
|
|
556
|
+
path: '/api/test/hello',
|
|
557
|
+
summary: 'Test endpoint',
|
|
558
|
+
responses: [],
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
registry.registerApi(api);
|
|
564
|
+
|
|
565
|
+
const endpoint = registry.getEndpoint('test_api', 'test_endpoint');
|
|
566
|
+
expect(endpoint).toBeDefined();
|
|
567
|
+
expect(endpoint?.summary).toBe('Test endpoint');
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('should return undefined for non-existent endpoint', () => {
|
|
571
|
+
const endpoint = registry.getEndpoint('nonexistent', 'also_nonexistent');
|
|
572
|
+
expect(endpoint).toBeUndefined();
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
describe('findEndpointByRoute', () => {
|
|
577
|
+
it('should find endpoint by method and path', () => {
|
|
578
|
+
const api: ApiRegistryEntryInput = {
|
|
579
|
+
id: 'route_api',
|
|
580
|
+
name: 'Route API',
|
|
581
|
+
type: 'rest',
|
|
582
|
+
version: 'v1',
|
|
583
|
+
basePath: '/api',
|
|
584
|
+
endpoints: [
|
|
585
|
+
{
|
|
586
|
+
id: 'get_users',
|
|
587
|
+
method: 'GET',
|
|
588
|
+
path: '/api/users',
|
|
589
|
+
responses: [],
|
|
590
|
+
},
|
|
591
|
+
],
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
registry.registerApi(api);
|
|
595
|
+
|
|
596
|
+
const result = registry.findEndpointByRoute('GET', '/api/users');
|
|
597
|
+
expect(result).toBeDefined();
|
|
598
|
+
expect(result?.api.id).toBe('route_api');
|
|
599
|
+
expect(result?.endpoint.id).toBe('get_users');
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('should return undefined for non-existent route', () => {
|
|
603
|
+
const result = registry.findEndpointByRoute('POST', '/nonexistent');
|
|
604
|
+
expect(result).toBeUndefined();
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
describe('getRegistry', () => {
|
|
609
|
+
it('should return complete registry snapshot', () => {
|
|
610
|
+
registry.registerApi({
|
|
611
|
+
id: 'api1',
|
|
612
|
+
name: 'API 1',
|
|
613
|
+
type: 'rest',
|
|
614
|
+
version: 'v1',
|
|
615
|
+
basePath: '/api/v1',
|
|
616
|
+
endpoints: [
|
|
617
|
+
{ id: 'e1', path: '/api/v1/test', responses: [] },
|
|
618
|
+
],
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
const snapshot = registry.getRegistry();
|
|
622
|
+
expect(snapshot.version).toBe('1.0.0');
|
|
623
|
+
expect(snapshot.conflictResolution).toBe('error');
|
|
624
|
+
expect(snapshot.totalApis).toBe(1);
|
|
625
|
+
expect(snapshot.totalEndpoints).toBe(1);
|
|
626
|
+
expect(snapshot.byType).toBeDefined();
|
|
627
|
+
expect(snapshot.byStatus).toBeDefined();
|
|
628
|
+
expect(snapshot.updatedAt).toBeDefined();
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('should group APIs by type', () => {
|
|
632
|
+
registry.registerApi({
|
|
633
|
+
id: 'rest1',
|
|
634
|
+
name: 'REST 1',
|
|
635
|
+
type: 'rest',
|
|
636
|
+
version: 'v1',
|
|
637
|
+
basePath: '/api/rest1',
|
|
638
|
+
endpoints: [],
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
registry.registerApi({
|
|
642
|
+
id: 'rest2',
|
|
643
|
+
name: 'REST 2',
|
|
644
|
+
type: 'rest',
|
|
645
|
+
version: 'v1',
|
|
646
|
+
basePath: '/api/rest2',
|
|
647
|
+
endpoints: [],
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
registry.registerApi({
|
|
651
|
+
id: 'graphql1',
|
|
652
|
+
name: 'GraphQL 1',
|
|
653
|
+
type: 'graphql',
|
|
654
|
+
version: 'v1',
|
|
655
|
+
basePath: '/graphql',
|
|
656
|
+
endpoints: [],
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
const snapshot = registry.getRegistry();
|
|
660
|
+
expect(snapshot.byType?.rest.length).toBe(2);
|
|
661
|
+
expect(snapshot.byType?.graphql.length).toBe(1);
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
describe('clear', () => {
|
|
666
|
+
it('should clear all registered APIs', () => {
|
|
667
|
+
registry.registerApi({
|
|
668
|
+
id: 'test',
|
|
669
|
+
name: 'Test',
|
|
670
|
+
type: 'rest',
|
|
671
|
+
version: 'v1',
|
|
672
|
+
basePath: '/test',
|
|
673
|
+
endpoints: [{ id: 'e1', path: '/test', responses: [] }],
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
expect(registry.getStats().totalApis).toBe(1);
|
|
677
|
+
|
|
678
|
+
registry.clear();
|
|
679
|
+
|
|
680
|
+
expect(registry.getStats().totalApis).toBe(0);
|
|
681
|
+
expect(registry.getStats().totalEndpoints).toBe(0);
|
|
682
|
+
expect(registry.getStats().totalRoutes).toBe(0);
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
describe('getStats', () => {
|
|
687
|
+
it('should return accurate statistics', () => {
|
|
688
|
+
registry.registerApi({
|
|
689
|
+
id: 'api1',
|
|
690
|
+
name: 'API 1',
|
|
691
|
+
type: 'rest',
|
|
692
|
+
version: 'v1',
|
|
693
|
+
basePath: '/api1',
|
|
694
|
+
endpoints: [
|
|
695
|
+
{ id: 'e1', path: '/api1/e1', responses: [] },
|
|
696
|
+
{ id: 'e2', path: '/api1/e2', responses: [] },
|
|
697
|
+
],
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
registry.registerApi({
|
|
701
|
+
id: 'api2',
|
|
702
|
+
name: 'API 2',
|
|
703
|
+
type: 'graphql',
|
|
704
|
+
version: 'v1',
|
|
705
|
+
basePath: '/graphql',
|
|
706
|
+
endpoints: [
|
|
707
|
+
{ id: 'query', path: '/graphql', responses: [] },
|
|
708
|
+
],
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
const stats = registry.getStats();
|
|
712
|
+
expect(stats.totalApis).toBe(2);
|
|
713
|
+
expect(stats.totalEndpoints).toBe(3);
|
|
714
|
+
expect(stats.totalRoutes).toBe(3);
|
|
715
|
+
expect(stats.apisByType.rest).toBe(1);
|
|
716
|
+
expect(stats.apisByType.graphql).toBe(1);
|
|
717
|
+
expect(stats.endpointsByApi.api1).toBe(2);
|
|
718
|
+
expect(stats.endpointsByApi.api2).toBe(1);
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
describe('Multi-protocol Support', () => {
|
|
723
|
+
it('should register GraphQL API', () => {
|
|
724
|
+
const api: ApiRegistryEntryInput = {
|
|
725
|
+
id: 'graphql',
|
|
726
|
+
name: 'GraphQL API',
|
|
727
|
+
type: 'graphql',
|
|
728
|
+
version: 'v1',
|
|
729
|
+
basePath: '/graphql',
|
|
730
|
+
endpoints: [
|
|
731
|
+
{
|
|
732
|
+
id: 'query',
|
|
733
|
+
path: '/graphql',
|
|
734
|
+
summary: 'GraphQL Query',
|
|
735
|
+
responses: [],
|
|
736
|
+
},
|
|
737
|
+
],
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
registry.registerApi(api);
|
|
741
|
+
expect(registry.getApi('graphql')?.type).toBe('graphql');
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it('should register WebSocket API', () => {
|
|
745
|
+
const api: ApiRegistryEntryInput = {
|
|
746
|
+
id: 'websocket',
|
|
747
|
+
name: 'WebSocket API',
|
|
748
|
+
type: 'websocket',
|
|
749
|
+
version: 'v1',
|
|
750
|
+
basePath: '/ws',
|
|
751
|
+
endpoints: [
|
|
752
|
+
{
|
|
753
|
+
id: 'subscribe',
|
|
754
|
+
path: '/ws/events',
|
|
755
|
+
summary: 'Subscribe to events',
|
|
756
|
+
protocolConfig: {
|
|
757
|
+
subProtocol: 'websocket',
|
|
758
|
+
eventName: 'data.updated',
|
|
759
|
+
direction: 'server-to-client',
|
|
760
|
+
},
|
|
761
|
+
responses: [],
|
|
762
|
+
},
|
|
763
|
+
],
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
registry.registerApi(api);
|
|
767
|
+
const endpoint = registry.getEndpoint('websocket', 'subscribe');
|
|
768
|
+
expect(endpoint?.protocolConfig?.subProtocol).toBe('websocket');
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it('should register Plugin API', () => {
|
|
772
|
+
const api: ApiRegistryEntryInput = {
|
|
773
|
+
id: 'custom_plugin',
|
|
774
|
+
name: 'Custom Plugin API',
|
|
775
|
+
type: 'plugin',
|
|
776
|
+
version: '1.0.0',
|
|
777
|
+
basePath: '/plugins/custom',
|
|
778
|
+
endpoints: [
|
|
779
|
+
{
|
|
780
|
+
id: 'custom_action',
|
|
781
|
+
method: 'POST',
|
|
782
|
+
path: '/plugins/custom/action',
|
|
783
|
+
summary: 'Custom plugin action',
|
|
784
|
+
responses: [],
|
|
785
|
+
},
|
|
786
|
+
],
|
|
787
|
+
metadata: {
|
|
788
|
+
pluginSource: 'custom_plugin_package',
|
|
789
|
+
status: 'active',
|
|
790
|
+
},
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
registry.registerApi(api);
|
|
794
|
+
const result = registry.findApis({ pluginSource: 'custom_plugin_package' });
|
|
795
|
+
expect(result.total).toBe(1);
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
describe('Performance Optimizations', () => {
|
|
800
|
+
it('should use indices for fast type-based lookups', () => {
|
|
801
|
+
// Register multiple APIs with different types
|
|
802
|
+
registry.registerApi({
|
|
803
|
+
id: 'rest_api_1',
|
|
804
|
+
name: 'REST API 1',
|
|
805
|
+
type: 'rest',
|
|
806
|
+
version: 'v1',
|
|
807
|
+
basePath: '/api/rest1',
|
|
808
|
+
endpoints: [{ id: 'e1', path: '/api/rest1', responses: [] }],
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
registry.registerApi({
|
|
812
|
+
id: 'rest_api_2',
|
|
813
|
+
name: 'REST API 2',
|
|
814
|
+
type: 'rest',
|
|
815
|
+
version: 'v1',
|
|
816
|
+
basePath: '/api/rest2',
|
|
817
|
+
endpoints: [{ id: 'e2', path: '/api/rest2', responses: [] }],
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
registry.registerApi({
|
|
821
|
+
id: 'graphql_api',
|
|
822
|
+
name: 'GraphQL API',
|
|
823
|
+
type: 'graphql',
|
|
824
|
+
version: 'v1',
|
|
825
|
+
basePath: '/graphql',
|
|
826
|
+
endpoints: [{ id: 'e3', path: '/graphql', responses: [] }],
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// Should efficiently find all REST APIs
|
|
830
|
+
const restApis = registry.findApis({ type: 'rest' });
|
|
831
|
+
expect(restApis.total).toBe(2);
|
|
832
|
+
expect(restApis.apis.every(api => api.type === 'rest')).toBe(true);
|
|
833
|
+
|
|
834
|
+
// Should efficiently find GraphQL APIs
|
|
835
|
+
const graphqlApis = registry.findApis({ type: 'graphql' });
|
|
836
|
+
expect(graphqlApis.total).toBe(1);
|
|
837
|
+
expect(graphqlApis.apis[0].id).toBe('graphql_api');
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it('should use indices for fast tag-based lookups', () => {
|
|
841
|
+
registry.registerApi({
|
|
842
|
+
id: 'api_1',
|
|
843
|
+
name: 'API 1',
|
|
844
|
+
type: 'rest',
|
|
845
|
+
version: 'v1',
|
|
846
|
+
basePath: '/api1',
|
|
847
|
+
endpoints: [{ id: 'e1', path: '/api1', responses: [] }],
|
|
848
|
+
metadata: { tags: ['customer', 'crm'] },
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
registry.registerApi({
|
|
852
|
+
id: 'api_2',
|
|
853
|
+
name: 'API 2',
|
|
854
|
+
type: 'rest',
|
|
855
|
+
version: 'v1',
|
|
856
|
+
basePath: '/api2',
|
|
857
|
+
endpoints: [{ id: 'e2', path: '/api2', responses: [] }],
|
|
858
|
+
metadata: { tags: ['order', 'sales'] },
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
registry.registerApi({
|
|
862
|
+
id: 'api_3',
|
|
863
|
+
name: 'API 3',
|
|
864
|
+
type: 'rest',
|
|
865
|
+
version: 'v1',
|
|
866
|
+
basePath: '/api3',
|
|
867
|
+
endpoints: [{ id: 'e3', path: '/api3', responses: [] }],
|
|
868
|
+
metadata: { tags: ['customer', 'analytics'] },
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
// Should efficiently find APIs by tag
|
|
872
|
+
const customerApis = registry.findApis({ tags: ['customer'] });
|
|
873
|
+
expect(customerApis.total).toBe(2);
|
|
874
|
+
expect(customerApis.apis.map(a => a.id).sort()).toEqual(['api_1', 'api_3']);
|
|
875
|
+
|
|
876
|
+
// Should support multiple tags (ANY match)
|
|
877
|
+
const multiTagApis = registry.findApis({ tags: ['crm', 'sales'] });
|
|
878
|
+
expect(multiTagApis.total).toBe(2);
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
it('should use indices for fast status-based lookups', () => {
|
|
882
|
+
registry.registerApi({
|
|
883
|
+
id: 'active_api',
|
|
884
|
+
name: 'Active API',
|
|
885
|
+
type: 'rest',
|
|
886
|
+
version: 'v1',
|
|
887
|
+
basePath: '/active',
|
|
888
|
+
endpoints: [{ id: 'e1', path: '/active', responses: [] }],
|
|
889
|
+
metadata: { status: 'active' },
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
registry.registerApi({
|
|
893
|
+
id: 'beta_api',
|
|
894
|
+
name: 'Beta API',
|
|
895
|
+
type: 'rest',
|
|
896
|
+
version: 'v1',
|
|
897
|
+
basePath: '/beta',
|
|
898
|
+
endpoints: [{ id: 'e2', path: '/beta', responses: [] }],
|
|
899
|
+
metadata: { status: 'beta' },
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
registry.registerApi({
|
|
903
|
+
id: 'deprecated_api',
|
|
904
|
+
name: 'Deprecated API',
|
|
905
|
+
type: 'rest',
|
|
906
|
+
version: 'v1',
|
|
907
|
+
basePath: '/deprecated',
|
|
908
|
+
endpoints: [{ id: 'e3', path: '/deprecated', responses: [] }],
|
|
909
|
+
metadata: { status: 'deprecated' },
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// Should efficiently find by status
|
|
913
|
+
const activeApis = registry.findApis({ status: 'active' });
|
|
914
|
+
expect(activeApis.total).toBe(1);
|
|
915
|
+
expect(activeApis.apis[0].id).toBe('active_api');
|
|
916
|
+
|
|
917
|
+
const betaApis = registry.findApis({ status: 'beta' });
|
|
918
|
+
expect(betaApis.total).toBe(1);
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it('should combine multiple indexed filters efficiently', () => {
|
|
922
|
+
registry.registerApi({
|
|
923
|
+
id: 'rest_crm_active',
|
|
924
|
+
name: 'REST CRM Active',
|
|
925
|
+
type: 'rest',
|
|
926
|
+
version: 'v1',
|
|
927
|
+
basePath: '/crm',
|
|
928
|
+
endpoints: [{ id: 'e1', path: '/crm', responses: [] }],
|
|
929
|
+
metadata: { status: 'active', tags: ['crm', 'customer'] },
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
registry.registerApi({
|
|
933
|
+
id: 'rest_crm_beta',
|
|
934
|
+
name: 'REST CRM Beta',
|
|
935
|
+
type: 'rest',
|
|
936
|
+
version: 'v1',
|
|
937
|
+
basePath: '/crm-beta',
|
|
938
|
+
endpoints: [{ id: 'e2', path: '/crm-beta', responses: [] }],
|
|
939
|
+
metadata: { status: 'beta', tags: ['crm'] },
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
registry.registerApi({
|
|
943
|
+
id: 'graphql_crm_active',
|
|
944
|
+
name: 'GraphQL CRM Active',
|
|
945
|
+
type: 'graphql',
|
|
946
|
+
version: 'v1',
|
|
947
|
+
basePath: '/graphql',
|
|
948
|
+
endpoints: [{ id: 'e3', path: '/graphql', responses: [] }],
|
|
949
|
+
metadata: { status: 'active', tags: ['crm'] },
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
// Combine type + status + tags filters
|
|
953
|
+
const result = registry.findApis({
|
|
954
|
+
type: 'rest',
|
|
955
|
+
status: 'active',
|
|
956
|
+
tags: ['crm'],
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
expect(result.total).toBe(1);
|
|
960
|
+
expect(result.apis[0].id).toBe('rest_crm_active');
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
it('should maintain indices when APIs are unregistered', () => {
|
|
964
|
+
registry.registerApi({
|
|
965
|
+
id: 'temp_api',
|
|
966
|
+
name: 'Temporary API',
|
|
967
|
+
type: 'rest',
|
|
968
|
+
version: 'v1',
|
|
969
|
+
basePath: '/temp',
|
|
970
|
+
endpoints: [{ id: 'e1', path: '/temp', responses: [] }],
|
|
971
|
+
metadata: { status: 'beta', tags: ['temp', 'test'] },
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
// Verify it's in indices
|
|
975
|
+
expect(registry.findApis({ type: 'rest' }).total).toBe(1);
|
|
976
|
+
expect(registry.findApis({ status: 'beta' }).total).toBe(1);
|
|
977
|
+
expect(registry.findApis({ tags: ['temp'] }).total).toBe(1);
|
|
978
|
+
|
|
979
|
+
// Unregister
|
|
980
|
+
registry.unregisterApi('temp_api');
|
|
981
|
+
|
|
982
|
+
// Verify removed from indices
|
|
983
|
+
expect(registry.findApis({ type: 'rest' }).total).toBe(0);
|
|
984
|
+
expect(registry.findApis({ status: 'beta' }).total).toBe(0);
|
|
985
|
+
expect(registry.findApis({ tags: ['temp'] }).total).toBe(0);
|
|
986
|
+
});
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
describe('Safety Guards', () => {
|
|
990
|
+
it('should allow clear() in non-production environment', () => {
|
|
991
|
+
const originalEnv = process.env.NODE_ENV;
|
|
992
|
+
try {
|
|
993
|
+
process.env.NODE_ENV = 'test';
|
|
994
|
+
|
|
995
|
+
registry.registerApi({
|
|
996
|
+
id: 'test_api',
|
|
997
|
+
name: 'Test API',
|
|
998
|
+
type: 'rest',
|
|
999
|
+
version: 'v1',
|
|
1000
|
+
basePath: '/test',
|
|
1001
|
+
endpoints: [{ id: 'e1', path: '/test', responses: [] }],
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
expect(registry.getStats().totalApis).toBe(1);
|
|
1005
|
+
|
|
1006
|
+
// Should work without force flag in non-production
|
|
1007
|
+
registry.clear();
|
|
1008
|
+
expect(registry.getStats().totalApis).toBe(0);
|
|
1009
|
+
} finally {
|
|
1010
|
+
process.env.NODE_ENV = originalEnv;
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it('should prevent clear() in production without force flag', () => {
|
|
1015
|
+
const originalEnv = process.env.NODE_ENV;
|
|
1016
|
+
try {
|
|
1017
|
+
process.env.NODE_ENV = 'production';
|
|
1018
|
+
|
|
1019
|
+
registry.registerApi({
|
|
1020
|
+
id: 'prod_api',
|
|
1021
|
+
name: 'Production API',
|
|
1022
|
+
type: 'rest',
|
|
1023
|
+
version: 'v1',
|
|
1024
|
+
basePath: '/prod',
|
|
1025
|
+
endpoints: [{ id: 'e1', path: '/prod', responses: [] }],
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
// Should throw error in production without force flag
|
|
1029
|
+
expect(() => registry.clear()).toThrow(
|
|
1030
|
+
'Cannot clear registry in production environment without force flag'
|
|
1031
|
+
);
|
|
1032
|
+
|
|
1033
|
+
// API should still exist
|
|
1034
|
+
expect(registry.getStats().totalApis).toBe(1);
|
|
1035
|
+
} finally {
|
|
1036
|
+
process.env.NODE_ENV = originalEnv;
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
it('should allow clear() in production with force flag', () => {
|
|
1041
|
+
const originalEnv = process.env.NODE_ENV;
|
|
1042
|
+
try {
|
|
1043
|
+
process.env.NODE_ENV = 'production';
|
|
1044
|
+
|
|
1045
|
+
registry.registerApi({
|
|
1046
|
+
id: 'prod_api',
|
|
1047
|
+
name: 'Production API',
|
|
1048
|
+
type: 'rest',
|
|
1049
|
+
version: 'v1',
|
|
1050
|
+
basePath: '/prod',
|
|
1051
|
+
endpoints: [{ id: 'e1', path: '/prod', responses: [] }],
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
expect(registry.getStats().totalApis).toBe(1);
|
|
1055
|
+
|
|
1056
|
+
// Should work with force flag
|
|
1057
|
+
registry.clear({ force: true });
|
|
1058
|
+
expect(registry.getStats().totalApis).toBe(0);
|
|
1059
|
+
|
|
1060
|
+
// Verify logger warned about forced clear
|
|
1061
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
1062
|
+
'API registry forcefully cleared in production',
|
|
1063
|
+
{ force: true }
|
|
1064
|
+
);
|
|
1065
|
+
} finally {
|
|
1066
|
+
process.env.NODE_ENV = originalEnv;
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
it('should clear all indices when clear() is called', () => {
|
|
1071
|
+
registry.registerApi({
|
|
1072
|
+
id: 'api_1',
|
|
1073
|
+
name: 'API 1',
|
|
1074
|
+
type: 'rest',
|
|
1075
|
+
version: 'v1',
|
|
1076
|
+
basePath: '/api1',
|
|
1077
|
+
endpoints: [{ id: 'e1', path: '/api1', responses: [] }],
|
|
1078
|
+
metadata: { status: 'active', tags: ['test'] },
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
registry.clear();
|
|
1082
|
+
|
|
1083
|
+
// All lookups should return empty
|
|
1084
|
+
expect(registry.findApis({ type: 'rest' }).total).toBe(0);
|
|
1085
|
+
expect(registry.findApis({ status: 'active' }).total).toBe(0);
|
|
1086
|
+
expect(registry.findApis({ tags: ['test'] }).total).toBe(0);
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
1089
|
+
});
|