@onlineapps/conn-orch-validator 2.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.
- package/README.md +78 -0
- package/TESTING_STRATEGY.md +92 -0
- package/docs/DESIGN.md +134 -0
- package/examples/service-wrapper-usage.js +250 -0
- package/examples/three-tier-testing.js +144 -0
- package/jest.config.js +23 -0
- package/onlineapps-conn-e2e-testing-1.0.0.tgz +0 -0
- package/package.json +43 -0
- package/src/CookbookTestRunner.js +434 -0
- package/src/CookbookTestUtils.js +237 -0
- package/src/ServiceReadinessValidator.js +430 -0
- package/src/ServiceTestHarness.js +256 -0
- package/src/ServiceValidator.js +387 -0
- package/src/TestOrchestrator.js +727 -0
- package/src/ValidationOrchestrator.js +506 -0
- package/src/WorkflowTestRunner.js +396 -0
- package/src/helpers/README.md +235 -0
- package/src/helpers/createPreValidationTests.js +321 -0
- package/src/helpers/createServiceReadinessTests.js +245 -0
- package/src/index.js +62 -0
- package/src/mocks/MockMQClient.js +176 -0
- package/src/mocks/MockRegistry.js +164 -0
- package/src/mocks/MockStorage.js +186 -0
- package/src/validators/ServiceStructureValidator.js +487 -0
- package/src/validators/ValidationProofGenerator.js +79 -0
- package/test-mq-flow.js +72 -0
- package/test-orchestrator.js +95 -0
- package/tests/component/testing-framework-integration.test.js +313 -0
- package/tests/integration/ServiceReadiness.test.js +265 -0
- package/tests/monitoring-e2e.test.js +315 -0
- package/tests/run-example.js +257 -0
- package/tests/unit/CookbookTestRunner.test.js +353 -0
- package/tests/unit/MockMQClient.test.js +190 -0
- package/tests/unit/MockRegistry.test.js +233 -0
- package/tests/unit/MockStorage.test.js +257 -0
- package/tests/unit/ServiceValidator.test.js +429 -0
- package/tests/unit/WorkflowTestRunner.test.js +546 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ServiceValidator = require('../../src/ServiceValidator');
|
|
4
|
+
const MockRegistry = require('../../src/mocks/MockRegistry');
|
|
5
|
+
const axios = require('axios');
|
|
6
|
+
|
|
7
|
+
jest.mock('axios');
|
|
8
|
+
|
|
9
|
+
describe('ServiceValidator @unit', () => {
|
|
10
|
+
let validator;
|
|
11
|
+
let registry;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
validator = new ServiceValidator();
|
|
15
|
+
registry = new MockRegistry();
|
|
16
|
+
axios.mockClear();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('Test Value Generation', () => {
|
|
20
|
+
it('should generate string values', () => {
|
|
21
|
+
expect(validator.generateTestValue({ type: 'string' })).toBe('test');
|
|
22
|
+
expect(validator.generateTestValue({ type: 'string', example: 'custom' })).toBe('custom');
|
|
23
|
+
expect(validator.generateTestValue({ type: 'string', enum: ['opt1', 'opt2'] })).toBe('opt1');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should generate formatted strings', () => {
|
|
27
|
+
expect(validator.generateTestValue({ type: 'string', format: 'email' })).toBe('test@example.com');
|
|
28
|
+
expect(validator.generateTestValue({ type: 'string', format: 'date' })).toBe('2024-01-01');
|
|
29
|
+
expect(validator.generateTestValue({ type: 'string', format: 'date-time' })).toBe('2024-01-01T00:00:00Z');
|
|
30
|
+
expect(validator.generateTestValue({ type: 'string', format: 'uuid' }))
|
|
31
|
+
.toBe('123e4567-e89b-12d3-a456-426614174000');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should generate number values', () => {
|
|
35
|
+
expect(validator.generateTestValue({ type: 'number' })).toBe(1);
|
|
36
|
+
expect(validator.generateTestValue({ type: 'integer' })).toBe(1);
|
|
37
|
+
expect(validator.generateTestValue({ type: 'number', minimum: 10 })).toBe(10);
|
|
38
|
+
expect(validator.generateTestValue({ type: 'number', maximum: 100 })).toBe(100);
|
|
39
|
+
expect(validator.generateTestValue({ type: 'number', example: 42 })).toBe(42);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should generate boolean values', () => {
|
|
43
|
+
expect(validator.generateTestValue({ type: 'boolean' })).toBe(true);
|
|
44
|
+
expect(validator.generateTestValue({ type: 'boolean', example: false })).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should generate array values', () => {
|
|
48
|
+
const result = validator.generateTestValue({
|
|
49
|
+
type: 'array',
|
|
50
|
+
items: { type: 'string' }
|
|
51
|
+
});
|
|
52
|
+
expect(Array.isArray(result)).toBe(true);
|
|
53
|
+
expect(result).toEqual(['test']);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should generate object values', () => {
|
|
57
|
+
const schema = {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
name: { type: 'string' },
|
|
61
|
+
age: { type: 'number' }
|
|
62
|
+
},
|
|
63
|
+
required: ['name']
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const result = validator.generateTestValue(schema);
|
|
67
|
+
expect(result).toHaveProperty('name', 'test');
|
|
68
|
+
// Age might or might not be included (random)
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should handle null schema', () => {
|
|
72
|
+
expect(validator.generateTestValue(null)).toBeNull();
|
|
73
|
+
expect(validator.generateTestValue(undefined)).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('Request Building', () => {
|
|
78
|
+
const operation = {
|
|
79
|
+
parameters: [
|
|
80
|
+
{ name: 'id', in: 'path', schema: { type: 'string' }, required: true },
|
|
81
|
+
{ name: 'filter', in: 'query', schema: { type: 'string' } },
|
|
82
|
+
{ name: 'x-api-key', in: 'header', schema: { type: 'string' } }
|
|
83
|
+
],
|
|
84
|
+
requestBody: {
|
|
85
|
+
content: {
|
|
86
|
+
'application/json': {
|
|
87
|
+
schema: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: { data: { type: 'string' } }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
it('should build request with path parameters', () => {
|
|
97
|
+
const request = validator.buildTestRequest('/users/{id}', 'get', operation);
|
|
98
|
+
expect(request.path).toBe('/users/test');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should build request with query parameters', () => {
|
|
102
|
+
const request = validator.buildTestRequest('/users', 'get', operation);
|
|
103
|
+
// Query params are optional and added randomly
|
|
104
|
+
expect(request.query).toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should build request with headers', () => {
|
|
108
|
+
const request = validator.buildTestRequest('/users', 'get', operation);
|
|
109
|
+
expect(request.headers).toBeDefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should build request with body', () => {
|
|
113
|
+
const request = validator.buildTestRequest('/users', 'post', operation);
|
|
114
|
+
expect(request.headers['Content-Type']).toBe('application/json');
|
|
115
|
+
expect(request.body).toHaveProperty('data');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should handle operations without parameters', () => {
|
|
119
|
+
const request = validator.buildTestRequest('/users', 'get', {});
|
|
120
|
+
expect(request.path).toBe('/users');
|
|
121
|
+
expect(request.query).toEqual({});
|
|
122
|
+
expect(request.headers).toEqual({});
|
|
123
|
+
expect(request.body).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('Endpoint Testing', () => {
|
|
128
|
+
const operation = {
|
|
129
|
+
operationId: 'getUser',
|
|
130
|
+
responses: {
|
|
131
|
+
'200': {
|
|
132
|
+
content: {
|
|
133
|
+
'application/json': {
|
|
134
|
+
schema: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
properties: {
|
|
137
|
+
id: { type: 'string' },
|
|
138
|
+
name: { type: 'string' }
|
|
139
|
+
},
|
|
140
|
+
required: ['id', 'name']
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
it('should test endpoint successfully', async () => {
|
|
149
|
+
axios.mockResolvedValue({
|
|
150
|
+
status: 200,
|
|
151
|
+
headers: {},
|
|
152
|
+
data: { id: '123', name: 'Test' }
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const result = await validator.testEndpoint(
|
|
156
|
+
'http://localhost:3000',
|
|
157
|
+
'/users',
|
|
158
|
+
'get',
|
|
159
|
+
operation
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
expect(result.valid).toBe(true);
|
|
163
|
+
expect(result.path).toBe('/users');
|
|
164
|
+
expect(result.method).toBe('GET');
|
|
165
|
+
expect(result.operationId).toBe('getUser');
|
|
166
|
+
expect(result.response.status).toBe(200);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should handle endpoint errors', async () => {
|
|
170
|
+
axios.mockRejectedValue(new Error('Connection refused'));
|
|
171
|
+
|
|
172
|
+
const result = await validator.testEndpoint(
|
|
173
|
+
'http://localhost:3000',
|
|
174
|
+
'/users',
|
|
175
|
+
'get',
|
|
176
|
+
operation
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
expect(result.valid).toBe(false);
|
|
180
|
+
expect(result.errors).toContain('Failed to call endpoint: Connection refused');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should validate response against schema', async () => {
|
|
184
|
+
axios.mockResolvedValue({
|
|
185
|
+
status: 200,
|
|
186
|
+
headers: {},
|
|
187
|
+
data: { invalid: 'response' }
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const result = await validator.testEndpoint(
|
|
191
|
+
'http://localhost:3000',
|
|
192
|
+
'/users',
|
|
193
|
+
'get',
|
|
194
|
+
operation
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(result.valid).toBe(false);
|
|
198
|
+
expect(result.errors[0]).toContain('Response validation failed');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should handle unexpected status codes', async () => {
|
|
202
|
+
axios.mockResolvedValue({
|
|
203
|
+
status: 404,
|
|
204
|
+
headers: {},
|
|
205
|
+
data: { error: 'Not found' }
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const result = await validator.testEndpoint(
|
|
209
|
+
'http://localhost:3000',
|
|
210
|
+
'/users',
|
|
211
|
+
'get',
|
|
212
|
+
operation
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
expect(result.valid).toBe(false);
|
|
216
|
+
expect(result.errors).toContain('Unexpected status code: 404');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('Service Validation', () => {
|
|
221
|
+
const openApiSpec = {
|
|
222
|
+
paths: {
|
|
223
|
+
'/users': {
|
|
224
|
+
get: {
|
|
225
|
+
operationId: 'getUsers',
|
|
226
|
+
responses: {
|
|
227
|
+
'200': {
|
|
228
|
+
content: {
|
|
229
|
+
'application/json': {
|
|
230
|
+
schema: { type: 'array' }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
post: {
|
|
237
|
+
operationId: 'createUser',
|
|
238
|
+
responses: {
|
|
239
|
+
'201': {
|
|
240
|
+
content: {
|
|
241
|
+
'application/json': {
|
|
242
|
+
schema: { type: 'object' }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
it('should validate service with all endpoints passing', async () => {
|
|
253
|
+
// Return different responses based on method
|
|
254
|
+
axios.mockImplementation((config) => {
|
|
255
|
+
if (config.method === 'GET') {
|
|
256
|
+
return Promise.resolve({
|
|
257
|
+
status: 200,
|
|
258
|
+
headers: {},
|
|
259
|
+
data: []
|
|
260
|
+
});
|
|
261
|
+
} else if (config.method === 'POST') {
|
|
262
|
+
return Promise.resolve({
|
|
263
|
+
status: 201,
|
|
264
|
+
headers: {},
|
|
265
|
+
data: {}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const results = await validator.validateService(
|
|
271
|
+
'http://localhost:3000',
|
|
272
|
+
openApiSpec
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
expect(results.valid).toBe(true);
|
|
276
|
+
expect(results.errors).toHaveLength(0);
|
|
277
|
+
expect(results.testedEndpoints).toHaveLength(2);
|
|
278
|
+
expect(results.coverage).toBe(100);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should mark service invalid if any endpoint fails', async () => {
|
|
282
|
+
axios
|
|
283
|
+
.mockResolvedValueOnce({ status: 200, headers: {}, data: [] })
|
|
284
|
+
.mockRejectedValueOnce(new Error('Failed'));
|
|
285
|
+
|
|
286
|
+
const results = await validator.validateService(
|
|
287
|
+
'http://localhost:3000',
|
|
288
|
+
openApiSpec
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
expect(results.valid).toBe(false);
|
|
292
|
+
expect(results.errors.length).toBeGreaterThan(0);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should handle spec without paths', async () => {
|
|
296
|
+
const results = await validator.validateService(
|
|
297
|
+
'http://localhost:3000',
|
|
298
|
+
{}
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
expect(results.valid).toBe(false);
|
|
302
|
+
expect(results.errors).toContain('No paths defined in OpenAPI spec');
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('Cookbook Validation', () => {
|
|
307
|
+
beforeEach(async () => {
|
|
308
|
+
await registry.register({
|
|
309
|
+
name: 'service1',
|
|
310
|
+
openapi: {
|
|
311
|
+
paths: {
|
|
312
|
+
'/test': {
|
|
313
|
+
get: { operationId: 'testOp' }
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should validate cookbook with valid steps', async () => {
|
|
321
|
+
const cookbook = {
|
|
322
|
+
steps: [
|
|
323
|
+
{
|
|
324
|
+
id: 'step1',
|
|
325
|
+
type: 'task',
|
|
326
|
+
service: 'service1',
|
|
327
|
+
operation: 'testOp'
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const results = await validator.validateCookbook(cookbook, registry);
|
|
333
|
+
|
|
334
|
+
expect(results.valid).toBe(true);
|
|
335
|
+
expect(results.errors).toHaveLength(0);
|
|
336
|
+
expect(results.validatedSteps).toHaveLength(1);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should detect missing step fields', async () => {
|
|
340
|
+
const cookbook = {
|
|
341
|
+
steps: [
|
|
342
|
+
{ type: 'task' }, // Missing id and service
|
|
343
|
+
{ id: 'step2' } // Missing type
|
|
344
|
+
]
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const results = await validator.validateCookbook(cookbook, registry);
|
|
348
|
+
|
|
349
|
+
expect(results.valid).toBe(false);
|
|
350
|
+
expect(results.errors.length).toBeGreaterThan(0);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should validate foreach steps', async () => {
|
|
354
|
+
const cookbook = {
|
|
355
|
+
steps: [
|
|
356
|
+
{
|
|
357
|
+
id: 'foreach1',
|
|
358
|
+
type: 'foreach',
|
|
359
|
+
items: '$api_input.items',
|
|
360
|
+
body: { id: 'body', type: 'task', service: 'service1' }
|
|
361
|
+
}
|
|
362
|
+
]
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const results = await validator.validateCookbook(cookbook, registry);
|
|
366
|
+
expect(results.valid).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should validate switch steps', async () => {
|
|
370
|
+
const cookbook = {
|
|
371
|
+
steps: [
|
|
372
|
+
{
|
|
373
|
+
id: 'switch1',
|
|
374
|
+
type: 'switch',
|
|
375
|
+
condition: '$api_input.type',
|
|
376
|
+
cases: [
|
|
377
|
+
{ value: 'a', step: { id: 's1', type: 'task', service: 'service1' } }
|
|
378
|
+
]
|
|
379
|
+
}
|
|
380
|
+
]
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const results = await validator.validateCookbook(cookbook, registry);
|
|
384
|
+
expect(results.valid).toBe(true);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should detect non-existent service', async () => {
|
|
388
|
+
const cookbook = {
|
|
389
|
+
steps: [
|
|
390
|
+
{
|
|
391
|
+
id: 'step1',
|
|
392
|
+
type: 'task',
|
|
393
|
+
service: 'non-existent'
|
|
394
|
+
}
|
|
395
|
+
]
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const results = await validator.validateCookbook(cookbook, registry);
|
|
399
|
+
|
|
400
|
+
expect(results.valid).toBe(false);
|
|
401
|
+
expect(results.errors).toContain('Service not found: non-existent');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('should detect non-existent operation', async () => {
|
|
405
|
+
const cookbook = {
|
|
406
|
+
steps: [
|
|
407
|
+
{
|
|
408
|
+
id: 'step1',
|
|
409
|
+
type: 'task',
|
|
410
|
+
service: 'service1',
|
|
411
|
+
operation: 'nonExistentOp'
|
|
412
|
+
}
|
|
413
|
+
]
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const results = await validator.validateCookbook(cookbook, registry);
|
|
417
|
+
|
|
418
|
+
expect(results.valid).toBe(false);
|
|
419
|
+
expect(results.errors).toContain('Operation not found: nonExistentOp');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('should handle cookbook without steps', async () => {
|
|
423
|
+
const results = await validator.validateCookbook({}, registry);
|
|
424
|
+
|
|
425
|
+
expect(results.valid).toBe(false);
|
|
426
|
+
expect(results.errors).toContain('Cookbook must have steps array');
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
});
|