@onlineapps/conn-orch-api-mapper 1.0.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,461 @@
1
+ 'use strict';
2
+
3
+ const ApiMapper = require('../../src/ApiMapper');
4
+ const axios = require('axios');
5
+
6
+ jest.mock('axios');
7
+
8
+ describe('ApiMapper Unit Tests', () => {
9
+ let apiMapper;
10
+ let mockLogger;
11
+ let mockOpenApiSpec;
12
+
13
+ beforeEach(() => {
14
+ jest.clearAllMocks();
15
+
16
+ mockLogger = {
17
+ info: jest.fn(),
18
+ error: jest.fn(),
19
+ debug: jest.fn(),
20
+ warn: jest.fn()
21
+ };
22
+
23
+ mockOpenApiSpec = {
24
+ openapi: '3.0.0',
25
+ info: { title: 'Test API', version: '1.0.0' },
26
+ servers: [{ url: 'http://localhost:3000' }],
27
+ paths: {
28
+ '/users/{userId}': {
29
+ get: {
30
+ operationId: 'getUser',
31
+ parameters: [
32
+ { name: 'userId', in: 'path', required: true, schema: { type: 'string' } }
33
+ ]
34
+ },
35
+ put: {
36
+ operationId: 'updateUser',
37
+ parameters: [
38
+ { name: 'userId', in: 'path', required: true }
39
+ ],
40
+ requestBody: {
41
+ content: {
42
+ 'application/json': {
43
+ schema: { type: 'object' }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ },
49
+ '/users': {
50
+ get: {
51
+ operationId: 'listUsers',
52
+ parameters: [
53
+ { name: 'limit', in: 'query', schema: { type: 'integer' } },
54
+ { name: 'offset', in: 'query', schema: { type: 'integer' } }
55
+ ]
56
+ },
57
+ post: {
58
+ operationId: 'createUser',
59
+ requestBody: {
60
+ required: true,
61
+ content: {
62
+ 'application/json': {
63
+ schema: { type: 'object' }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ };
71
+
72
+ apiMapper = new ApiMapper({
73
+ openApiSpec: mockOpenApiSpec,
74
+ serviceUrl: 'http://test-service:3000',
75
+ logger: mockLogger
76
+ });
77
+ });
78
+
79
+ describe('Constructor', () => {
80
+ it('should create instance with required config', () => {
81
+ expect(apiMapper).toBeInstanceOf(ApiMapper);
82
+ expect(apiMapper.serviceUrl).toBe('http://test-service:3000');
83
+ expect(apiMapper.openApiSpec).toEqual(mockOpenApiSpec);
84
+ });
85
+
86
+ it('should throw error without OpenAPI spec', () => {
87
+ expect(() => new ApiMapper({})).toThrow('OpenAPI specification is required');
88
+ });
89
+
90
+ it('should use default service URL if not provided', () => {
91
+ const mapper = new ApiMapper({
92
+ openApiSpec: mockOpenApiSpec
93
+ });
94
+ expect(mapper.serviceUrl).toBe('http://localhost:3000');
95
+ });
96
+
97
+ it('should parse operations from OpenAPI spec', () => {
98
+ expect(apiMapper.operations).toBeDefined();
99
+ expect(apiMapper.operations['getUser']).toBeDefined();
100
+ expect(apiMapper.operations['updateUser']).toBeDefined();
101
+ expect(apiMapper.operations['listUsers']).toBeDefined();
102
+ expect(apiMapper.operations['createUser']).toBeDefined();
103
+ });
104
+ });
105
+
106
+ describe('_parseOpenApiSpec', () => {
107
+ it('should extract operations with correct details', () => {
108
+ const operations = apiMapper._parseOpenApiSpec(mockOpenApiSpec);
109
+
110
+ expect(operations['getUser']).toEqual({
111
+ method: 'GET',
112
+ path: '/users/{userId}',
113
+ parameters: mockOpenApiSpec.paths['/users/{userId}'].get.parameters,
114
+ requestBody: undefined,
115
+ responses: undefined,
116
+ summary: undefined,
117
+ description: undefined
118
+ });
119
+
120
+ expect(operations['createUser']).toEqual({
121
+ method: 'POST',
122
+ path: '/users',
123
+ parameters: [],
124
+ requestBody: mockOpenApiSpec.paths['/users'].post.requestBody,
125
+ responses: undefined,
126
+ summary: undefined,
127
+ description: undefined
128
+ });
129
+ });
130
+
131
+ it('should generate operation ID if missing', () => {
132
+ const specWithoutId = {
133
+ paths: {
134
+ '/test': {
135
+ get: {
136
+ // No operationId
137
+ parameters: []
138
+ }
139
+ }
140
+ }
141
+ };
142
+
143
+ const operations = apiMapper._parseOpenApiSpec(specWithoutId);
144
+ expect(operations['get__test']).toBeDefined();
145
+ });
146
+
147
+ it('should handle empty spec', () => {
148
+ const operations = apiMapper._parseOpenApiSpec({});
149
+ expect(operations).toEqual({});
150
+ });
151
+ });
152
+
153
+ describe('mapOperationToEndpoint', () => {
154
+ it('should return operation details for valid operation', () => {
155
+ const endpoint = apiMapper.mapOperationToEndpoint('getUser');
156
+
157
+ expect(endpoint).toEqual({
158
+ method: 'GET',
159
+ path: '/users/{userId}',
160
+ parameters: expect.any(Array),
161
+ requestBody: undefined,
162
+ responses: undefined,
163
+ summary: undefined,
164
+ description: undefined
165
+ });
166
+ });
167
+
168
+ it('should throw error for unknown operation', () => {
169
+ expect(() => apiMapper.mapOperationToEndpoint('unknownOp'))
170
+ .toThrow('Operation not found: unknownOp');
171
+ });
172
+ });
173
+
174
+ describe('transformRequest', () => {
175
+ it('should separate parameters by type', () => {
176
+ const params = [
177
+ { name: 'userId', in: 'path' },
178
+ { name: 'limit', in: 'query' },
179
+ { name: 'auth', in: 'header' }
180
+ ];
181
+
182
+ const input = {
183
+ userId: '123',
184
+ limit: 10,
185
+ auth: 'Bearer token'
186
+ };
187
+
188
+ const result = apiMapper.transformRequest(input, params);
189
+
190
+ expect(result.params.userId).toBe('123');
191
+ expect(result.query.limit).toBe(10);
192
+ expect(result.headers.auth).toBe('Bearer token');
193
+ });
194
+
195
+ it('should handle missing optional parameters', () => {
196
+ const params = [
197
+ { name: 'required', in: 'query', required: true },
198
+ { name: 'optional', in: 'query' }
199
+ ];
200
+
201
+ const result = apiMapper.transformRequest({ required: 'value' }, params);
202
+
203
+ expect(result.query.required).toBe('value');
204
+ expect(result.query.optional).toBeUndefined();
205
+ });
206
+
207
+ it('should throw for missing required parameters', () => {
208
+ const params = [
209
+ { name: 'required', in: 'path', required: true }
210
+ ];
211
+
212
+ expect(() => apiMapper.transformRequest({}, params))
213
+ .toThrow('Required parameter missing: required');
214
+ });
215
+
216
+ it('should extract body from input', () => {
217
+ const input = {
218
+ userId: '123',
219
+ body: {
220
+ name: 'John',
221
+ email: 'john@example.com'
222
+ }
223
+ };
224
+
225
+ const params = [{ name: 'userId', in: 'path' }];
226
+ const result = apiMapper.transformRequest(input, params);
227
+
228
+ expect(result.body).toEqual({
229
+ name: 'John',
230
+ email: 'john@example.com'
231
+ });
232
+ });
233
+ });
234
+
235
+ describe('transformResponse', () => {
236
+ it('should extract data from response', () => {
237
+ const response = {
238
+ data: { id: '1', name: 'John' },
239
+ status: 200,
240
+ headers: {}
241
+ };
242
+
243
+ const result = apiMapper.transformResponse(response);
244
+ expect(result).toEqual({ id: '1', name: 'John' });
245
+ });
246
+
247
+ it('should handle null response', () => {
248
+ const response = {
249
+ data: null,
250
+ status: 204
251
+ };
252
+
253
+ const result = apiMapper.transformResponse(response);
254
+ expect(result).toEqual(response);
255
+ });
256
+
257
+ it('should pass through response without data property', () => {
258
+ const response = { status: 200 };
259
+ const result = apiMapper.transformResponse(response);
260
+ expect(result).toEqual(response);
261
+ });
262
+ });
263
+
264
+ describe('_buildRequest', () => {
265
+ it('should build request with path parameters', () => {
266
+ const operation = {
267
+ method: 'GET',
268
+ path: '/users/{userId}',
269
+ parameters: [
270
+ { name: 'userId', in: 'path' }
271
+ ]
272
+ };
273
+
274
+ const input = { userId: '123' };
275
+ const request = apiMapper._buildRequest(operation, input);
276
+
277
+ expect(request.method).toBe('GET');
278
+ expect(request.url).toBe('http://test-service:3000/users/123');
279
+ });
280
+
281
+ it('should add query parameters', () => {
282
+ const operation = {
283
+ method: 'GET',
284
+ path: '/users',
285
+ parameters: [
286
+ { name: 'limit', in: 'query' },
287
+ { name: 'offset', in: 'query' }
288
+ ]
289
+ };
290
+
291
+ const input = { limit: 10, offset: 20 };
292
+ const request = apiMapper._buildRequest(operation, input);
293
+
294
+ expect(request.params).toEqual({ limit: 10, offset: 20 });
295
+ });
296
+
297
+ it('should add headers', () => {
298
+ const operation = {
299
+ method: 'GET',
300
+ path: '/users',
301
+ parameters: [
302
+ { name: 'X-API-Key', in: 'header' }
303
+ ]
304
+ };
305
+
306
+ const input = { 'X-API-Key': 'secret' };
307
+ const request = apiMapper._buildRequest(operation, input);
308
+
309
+ expect(request.headers['X-API-Key']).toBe('secret');
310
+ });
311
+
312
+ it('should add request body', () => {
313
+ const operation = {
314
+ method: 'POST',
315
+ path: '/users',
316
+ parameters: [],
317
+ requestBody: {
318
+ content: {
319
+ 'application/json': {}
320
+ }
321
+ }
322
+ };
323
+
324
+ const input = {
325
+ body: { name: 'John', email: 'john@example.com' }
326
+ };
327
+ const request = apiMapper._buildRequest(operation, input);
328
+
329
+ expect(request.data).toEqual(input.body);
330
+ expect(request.headers['Content-Type']).toBe('application/json');
331
+ });
332
+ });
333
+
334
+ describe('_resolveVariables', () => {
335
+ it('should resolve simple variables', () => {
336
+ const input = {
337
+ userId: '${context.user.id}',
338
+ name: 'static'
339
+ };
340
+
341
+ const context = {
342
+ context: {
343
+ user: { id: '123' }
344
+ }
345
+ };
346
+
347
+ const result = apiMapper._resolveVariables(input, context);
348
+ expect(result.userId).toBe('123');
349
+ expect(result.name).toBe('static');
350
+ });
351
+
352
+ it('should handle missing context gracefully', () => {
353
+ const input = {
354
+ userId: '${context.user.id}'
355
+ };
356
+
357
+ const result = apiMapper._resolveVariables(input, {});
358
+ expect(result.userId).toBeUndefined();
359
+ });
360
+
361
+ it('should resolve nested objects', () => {
362
+ const input = {
363
+ user: {
364
+ id: '${context.userId}',
365
+ name: '${input.userName}'
366
+ }
367
+ };
368
+
369
+ const context = {
370
+ context: { userId: '123' },
371
+ input: { userName: 'John' }
372
+ };
373
+
374
+ const result = apiMapper._resolveVariables(input, context);
375
+ expect(result.user.id).toBe('123');
376
+ expect(result.user.name).toBe('John');
377
+ });
378
+ });
379
+
380
+ describe('callOperation', () => {
381
+ it('should execute operation successfully', async () => {
382
+ const mockResponse = {
383
+ data: { id: '1', name: 'John' },
384
+ status: 200
385
+ };
386
+ axios.mockResolvedValue(mockResponse);
387
+
388
+ const result = await apiMapper.callOperation('getUser', { userId: '1' });
389
+
390
+ expect(axios).toHaveBeenCalledWith({
391
+ method: 'GET',
392
+ url: 'http://test-service:3000/users/1',
393
+ params: {},
394
+ headers: {},
395
+ data: null
396
+ });
397
+
398
+ expect(result).toEqual({ id: '1', name: 'John' });
399
+ });
400
+
401
+ it('should handle operation errors', async () => {
402
+ const error = new Error('Network error');
403
+ axios.mockRejectedValue(error);
404
+
405
+ await expect(apiMapper.callOperation('getUser', { userId: '1' }))
406
+ .rejects.toThrow('Network error');
407
+
408
+ expect(mockLogger.error).toHaveBeenCalledWith(
409
+ 'API call failed for getUser',
410
+ expect.any(Object)
411
+ );
412
+ });
413
+
414
+ it('should throw for unknown operation', async () => {
415
+ await expect(apiMapper.callOperation('unknownOp', {}))
416
+ .rejects.toThrow('Operation not found: unknownOp');
417
+ });
418
+ });
419
+
420
+ describe('loadOpenApiSpec', () => {
421
+ it('should reload spec and update operations', () => {
422
+ const newSpec = {
423
+ paths: {
424
+ '/new': {
425
+ get: {
426
+ operationId: 'newOp'
427
+ }
428
+ }
429
+ }
430
+ };
431
+
432
+ apiMapper.loadOpenApiSpec(newSpec);
433
+
434
+ expect(apiMapper.openApiSpec).toEqual(newSpec);
435
+ expect(apiMapper.operations['newOp']).toBeDefined();
436
+ expect(apiMapper.operations['getUser']).toBeUndefined();
437
+ });
438
+ });
439
+
440
+ describe('Direct Call Mode', () => {
441
+ it('should use direct call when configured', async () => {
442
+ const mockApp = {};
443
+ const directMapper = new ApiMapper({
444
+ openApiSpec: mockOpenApiSpec,
445
+ service: mockApp,
446
+ directCall: true,
447
+ logger: mockLogger
448
+ });
449
+
450
+ directMapper._callDirectly = jest.fn().mockResolvedValue({
451
+ data: { id: '1' },
452
+ status: 200
453
+ });
454
+
455
+ const result = await directMapper.callOperation('getUser', { userId: '1' });
456
+
457
+ expect(directMapper._callDirectly).toHaveBeenCalled();
458
+ expect(result).toEqual({ id: '1' });
459
+ });
460
+ });
461
+ });