@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.
Files changed (37) hide show
  1. package/README.md +78 -0
  2. package/TESTING_STRATEGY.md +92 -0
  3. package/docs/DESIGN.md +134 -0
  4. package/examples/service-wrapper-usage.js +250 -0
  5. package/examples/three-tier-testing.js +144 -0
  6. package/jest.config.js +23 -0
  7. package/onlineapps-conn-e2e-testing-1.0.0.tgz +0 -0
  8. package/package.json +43 -0
  9. package/src/CookbookTestRunner.js +434 -0
  10. package/src/CookbookTestUtils.js +237 -0
  11. package/src/ServiceReadinessValidator.js +430 -0
  12. package/src/ServiceTestHarness.js +256 -0
  13. package/src/ServiceValidator.js +387 -0
  14. package/src/TestOrchestrator.js +727 -0
  15. package/src/ValidationOrchestrator.js +506 -0
  16. package/src/WorkflowTestRunner.js +396 -0
  17. package/src/helpers/README.md +235 -0
  18. package/src/helpers/createPreValidationTests.js +321 -0
  19. package/src/helpers/createServiceReadinessTests.js +245 -0
  20. package/src/index.js +62 -0
  21. package/src/mocks/MockMQClient.js +176 -0
  22. package/src/mocks/MockRegistry.js +164 -0
  23. package/src/mocks/MockStorage.js +186 -0
  24. package/src/validators/ServiceStructureValidator.js +487 -0
  25. package/src/validators/ValidationProofGenerator.js +79 -0
  26. package/test-mq-flow.js +72 -0
  27. package/test-orchestrator.js +95 -0
  28. package/tests/component/testing-framework-integration.test.js +313 -0
  29. package/tests/integration/ServiceReadiness.test.js +265 -0
  30. package/tests/monitoring-e2e.test.js +315 -0
  31. package/tests/run-example.js +257 -0
  32. package/tests/unit/CookbookTestRunner.test.js +353 -0
  33. package/tests/unit/MockMQClient.test.js +190 -0
  34. package/tests/unit/MockRegistry.test.js +233 -0
  35. package/tests/unit/MockStorage.test.js +257 -0
  36. package/tests/unit/ServiceValidator.test.js +429 -0
  37. package/tests/unit/WorkflowTestRunner.test.js +546 -0
@@ -0,0 +1,256 @@
1
+ 'use strict';
2
+
3
+ const MockMQClient = require('./mocks/MockMQClient');
4
+ const MockRegistry = require('./mocks/MockRegistry');
5
+ const MockStorage = require('./mocks/MockStorage');
6
+ const axios = require('axios');
7
+
8
+ /**
9
+ * ServiceTestHarness - Complete test environment for services
10
+ */
11
+ class ServiceTestHarness {
12
+ constructor(options = {}) {
13
+ this.service = options.service; // Express app
14
+ this.serviceName = options.serviceName || 'test-service';
15
+ this.openApiSpec = options.openApiSpec || {};
16
+ this.mockInfrastructure = options.mockInfrastructure !== false;
17
+
18
+ // Initialize mocks if requested
19
+ if (this.mockInfrastructure) {
20
+ this.mqClient = new MockMQClient();
21
+ this.registry = new MockRegistry();
22
+ this.storage = new MockStorage();
23
+ }
24
+
25
+ // Test state
26
+ this.server = null;
27
+ this.port = options.port || 0; // 0 = random port
28
+ this.baseUrl = null;
29
+ this.isRunning = false;
30
+ }
31
+
32
+ /**
33
+ * Start test harness
34
+ */
35
+ async start() {
36
+ if (this.isRunning) {
37
+ return;
38
+ }
39
+
40
+ // Start Express server if provided
41
+ if (this.service) {
42
+ await this.startServer();
43
+ }
44
+
45
+ // Connect mocks
46
+ if (this.mockInfrastructure) {
47
+ await this.mqClient.connect();
48
+ await this.registry.register({
49
+ name: this.serviceName,
50
+ url: this.baseUrl,
51
+ openapi: this.openApiSpec
52
+ });
53
+ }
54
+
55
+ this.isRunning = true;
56
+ }
57
+
58
+ /**
59
+ * Stop test harness
60
+ */
61
+ async stop() {
62
+ if (!this.isRunning) {
63
+ return;
64
+ }
65
+
66
+ // Stop server
67
+ if (this.server) {
68
+ await new Promise(resolve => {
69
+ this.server.close(resolve);
70
+ });
71
+ }
72
+
73
+ // Disconnect mocks
74
+ if (this.mockInfrastructure) {
75
+ await this.mqClient.disconnect();
76
+ await this.registry.unregister(this.serviceName);
77
+ }
78
+
79
+ this.isRunning = false;
80
+ }
81
+
82
+ /**
83
+ * Start Express server
84
+ * @private
85
+ */
86
+ async startServer() {
87
+ return new Promise((resolve) => {
88
+ this.server = this.service.listen(this.port, () => {
89
+ const actualPort = this.server.address().port;
90
+ this.baseUrl = `http://localhost:${actualPort}`;
91
+ resolve();
92
+ });
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Call API endpoint
98
+ */
99
+ async callApi(method, path, data = null, headers = {}) {
100
+ if (!this.baseUrl) {
101
+ throw new Error('Service not started');
102
+ }
103
+
104
+ const config = {
105
+ method,
106
+ url: `${this.baseUrl}${path}`,
107
+ headers,
108
+ timeout: 5000
109
+ };
110
+
111
+ if (data) {
112
+ if (method === 'GET') {
113
+ config.params = data;
114
+ } else {
115
+ config.data = data;
116
+ }
117
+ }
118
+
119
+ const response = await axios(config);
120
+ return response.data;
121
+ }
122
+
123
+ /**
124
+ * Simulate workflow execution
125
+ */
126
+ async simulateWorkflow(workflow) {
127
+ const results = [];
128
+
129
+ for (const step of workflow.steps) {
130
+ if (step.type === 'task' && step.service === this.serviceName) {
131
+ // Execute task step
132
+ const result = await this.executeStep(step);
133
+ results.push(result);
134
+
135
+ // Simulate publishing result to queue
136
+ if (this.mockInfrastructure) {
137
+ await this.mqClient.publish('workflow.completed', {
138
+ step: step.id,
139
+ result
140
+ });
141
+ }
142
+ }
143
+ }
144
+
145
+ return {
146
+ workflow: workflow,
147
+ results,
148
+ completed: true
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Execute a single workflow step
154
+ * @private
155
+ */
156
+ async executeStep(step) {
157
+ // Map step to API call based on operation
158
+ const operation = step.operation || step.id;
159
+ const endpoint = this.findEndpoint(operation);
160
+
161
+ if (!endpoint) {
162
+ throw new Error(`No endpoint found for operation: ${operation}`);
163
+ }
164
+
165
+ // Call the API
166
+ const response = await this.callApi(
167
+ endpoint.method,
168
+ endpoint.path,
169
+ step.input
170
+ );
171
+
172
+ return {
173
+ stepId: step.id,
174
+ operation,
175
+ response
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Find endpoint in OpenAPI spec
181
+ * @private
182
+ */
183
+ findEndpoint(operationId) {
184
+ if (!this.openApiSpec.paths) {
185
+ return null;
186
+ }
187
+
188
+ for (const [path, pathItem] of Object.entries(this.openApiSpec.paths)) {
189
+ for (const [method, operation] of Object.entries(pathItem)) {
190
+ if (operation.operationId === operationId) {
191
+ return { method: method.toUpperCase(), path };
192
+ }
193
+ }
194
+ }
195
+
196
+ return null;
197
+ }
198
+
199
+ /**
200
+ * Get published messages from mock MQ
201
+ */
202
+ getPublishedMessages(queue = null) {
203
+ if (!this.mockInfrastructure) {
204
+ throw new Error('Mock infrastructure not enabled');
205
+ }
206
+ return this.mqClient.getPublishedMessages(queue);
207
+ }
208
+
209
+ /**
210
+ * Get service registration from mock registry
211
+ */
212
+ getServiceRegistration() {
213
+ if (!this.mockInfrastructure) {
214
+ throw new Error('Mock infrastructure not enabled');
215
+ }
216
+ return this.registry.getService(this.serviceName);
217
+ }
218
+
219
+ /**
220
+ * Get storage contents from mock storage
221
+ */
222
+ async getStorageObject(bucket, key) {
223
+ if (!this.mockInfrastructure) {
224
+ throw new Error('Mock infrastructure not enabled');
225
+ }
226
+ return await this.storage.get(bucket, key);
227
+ }
228
+
229
+ /**
230
+ * Clear all test data
231
+ */
232
+ clear() {
233
+ if (this.mockInfrastructure) {
234
+ this.mqClient.clear();
235
+ this.registry.clear();
236
+ this.storage.clear();
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Get test statistics
242
+ */
243
+ getStats() {
244
+ if (!this.mockInfrastructure) {
245
+ return { mocks: 'disabled' };
246
+ }
247
+
248
+ return {
249
+ mq: this.mqClient.getStats(),
250
+ registry: this.registry.getStats(),
251
+ storage: this.storage.getStats()
252
+ };
253
+ }
254
+ }
255
+
256
+ module.exports = ServiceTestHarness;
@@ -0,0 +1,387 @@
1
+ 'use strict';
2
+
3
+ const axios = require('axios');
4
+ const Ajv = require('ajv');
5
+ const addFormats = require('ajv-formats');
6
+
7
+ /**
8
+ * ServiceValidator - Validates service compliance with OpenAPI spec
9
+ */
10
+ class ServiceValidator {
11
+ constructor(options = {}) {
12
+ this.ajv = new Ajv({ allErrors: true, strict: false });
13
+ addFormats(this.ajv);
14
+ this.timeout = options.timeout || 5000;
15
+ }
16
+
17
+ /**
18
+ * Validate service against OpenAPI specification
19
+ */
20
+ async validateService(serviceUrl, openApiSpec) {
21
+ const results = {
22
+ valid: true,
23
+ errors: [],
24
+ warnings: [],
25
+ testedEndpoints: [],
26
+ coverage: 0
27
+ };
28
+
29
+ if (!openApiSpec.paths) {
30
+ results.valid = false;
31
+ results.errors.push('No paths defined in OpenAPI spec');
32
+ return results;
33
+ }
34
+
35
+ const totalEndpoints = this.countEndpoints(openApiSpec.paths);
36
+ let testedCount = 0;
37
+
38
+ // Test each endpoint
39
+ for (const [path, pathItem] of Object.entries(openApiSpec.paths)) {
40
+ for (const [method, operation] of Object.entries(pathItem)) {
41
+ if (method === 'parameters' || method.startsWith('x-')) continue;
42
+
43
+ const endpointResult = await this.testEndpoint(
44
+ serviceUrl,
45
+ path,
46
+ method,
47
+ operation
48
+ );
49
+
50
+ results.testedEndpoints.push(endpointResult);
51
+ if (!endpointResult.valid) {
52
+ results.valid = false;
53
+ results.errors.push(...endpointResult.errors);
54
+ }
55
+ if (endpointResult.warnings) {
56
+ results.warnings.push(...endpointResult.warnings);
57
+ }
58
+ testedCount++;
59
+ }
60
+ }
61
+
62
+ results.coverage = (testedCount / totalEndpoints) * 100;
63
+ return results;
64
+ }
65
+
66
+ /**
67
+ * Test individual endpoint
68
+ */
69
+ async testEndpoint(serviceUrl, path, method, operation) {
70
+ const result = {
71
+ path,
72
+ method: method.toUpperCase(),
73
+ operationId: operation.operationId,
74
+ valid: true,
75
+ errors: [],
76
+ warnings: [],
77
+ response: null
78
+ };
79
+
80
+ try {
81
+ // Build test request
82
+ const testRequest = this.buildTestRequest(path, method, operation);
83
+ const url = `${serviceUrl}${testRequest.path}`;
84
+
85
+ // Execute request
86
+ const response = await axios({
87
+ method: method.toUpperCase(),
88
+ url,
89
+ data: testRequest.body,
90
+ params: testRequest.query,
91
+ headers: testRequest.headers,
92
+ timeout: this.timeout,
93
+ validateStatus: () => true // Don't throw on any status
94
+ });
95
+
96
+ result.response = {
97
+ status: response.status,
98
+ headers: response.headers,
99
+ data: response.data
100
+ };
101
+
102
+ // Validate response
103
+ this.validateResponse(response, operation, result);
104
+
105
+ } catch (error) {
106
+ result.valid = false;
107
+ result.errors.push(`Failed to call endpoint: ${error.message}`);
108
+ }
109
+
110
+ return result;
111
+ }
112
+
113
+ /**
114
+ * Build test request from OpenAPI operation
115
+ */
116
+ buildTestRequest(path, method, operation) {
117
+ const request = {
118
+ path,
119
+ headers: {},
120
+ query: {},
121
+ body: null
122
+ };
123
+
124
+ // Process parameters
125
+ if (operation.parameters) {
126
+ operation.parameters.forEach(param => {
127
+ const testValue = this.generateTestValue(param.schema);
128
+
129
+ switch (param.in) {
130
+ case 'path':
131
+ request.path = request.path.replace(`{${param.name}}`, testValue);
132
+ break;
133
+ case 'query':
134
+ if (!param.required && Math.random() > 0.5) break;
135
+ request.query[param.name] = testValue;
136
+ break;
137
+ case 'header':
138
+ if (!param.required && Math.random() > 0.5) break;
139
+ request.headers[param.name] = testValue;
140
+ break;
141
+ }
142
+ });
143
+ }
144
+
145
+ // Process request body
146
+ if (operation.requestBody && operation.requestBody.content) {
147
+ const contentType = Object.keys(operation.requestBody.content)[0];
148
+ const schema = operation.requestBody.content[contentType].schema;
149
+ request.headers['Content-Type'] = contentType;
150
+ request.body = this.generateTestValue(schema);
151
+ }
152
+
153
+ return request;
154
+ }
155
+
156
+ /**
157
+ * Generate test value based on schema
158
+ */
159
+ generateTestValue(schema) {
160
+ if (!schema) return null;
161
+
162
+ switch (schema.type) {
163
+ case 'string':
164
+ if (schema.enum) return schema.enum[0];
165
+ if (schema.format === 'email') return 'test@example.com';
166
+ if (schema.format === 'date') return '2024-01-01';
167
+ if (schema.format === 'date-time') return '2024-01-01T00:00:00Z';
168
+ if (schema.format === 'uuid') return '123e4567-e89b-12d3-a456-426614174000';
169
+ return schema.example || 'test';
170
+
171
+ case 'number':
172
+ case 'integer':
173
+ if (schema.minimum !== undefined) return schema.minimum;
174
+ if (schema.maximum !== undefined) return schema.maximum;
175
+ return schema.example || 1;
176
+
177
+ case 'boolean':
178
+ return schema.example !== undefined ? schema.example : true;
179
+
180
+ case 'array':
181
+ const itemValue = this.generateTestValue(schema.items);
182
+ return [itemValue];
183
+
184
+ case 'object':
185
+ const obj = {};
186
+ if (schema.properties) {
187
+ Object.entries(schema.properties).forEach(([key, prop]) => {
188
+ if (schema.required && schema.required.includes(key)) {
189
+ obj[key] = this.generateTestValue(prop);
190
+ } else if (Math.random() > 0.5) {
191
+ obj[key] = this.generateTestValue(prop);
192
+ }
193
+ });
194
+ }
195
+ return obj;
196
+
197
+ default:
198
+ return null;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Validate response against OpenAPI schema
204
+ */
205
+ validateResponse(response, operation, result) {
206
+ if (!operation.responses) {
207
+ result.warnings.push('No responses defined in OpenAPI spec');
208
+ return;
209
+ }
210
+
211
+ const statusCode = response.status.toString();
212
+ const responseSpec = operation.responses[statusCode] || operation.responses.default;
213
+
214
+ if (!responseSpec) {
215
+ result.valid = false;
216
+ result.errors.push(`Unexpected status code: ${statusCode}`);
217
+ return;
218
+ }
219
+
220
+ // Validate response body
221
+ if (responseSpec.content) {
222
+ const contentType = Object.keys(responseSpec.content)[0];
223
+ const schema = responseSpec.content[contentType].schema;
224
+
225
+ if (schema) {
226
+ const validate = this.ajv.compile(schema);
227
+ if (!validate(response.data)) {
228
+ result.valid = false;
229
+ result.errors.push(`Response validation failed: ${JSON.stringify(validate.errors)}`);
230
+ }
231
+ }
232
+ }
233
+
234
+ // Validate headers
235
+ if (responseSpec.headers) {
236
+ Object.entries(responseSpec.headers).forEach(([headerName, headerSpec]) => {
237
+ const headerValue = response.headers[headerName.toLowerCase()];
238
+ if (headerSpec.required && !headerValue) {
239
+ result.valid = false;
240
+ result.errors.push(`Required header missing: ${headerName}`);
241
+ }
242
+ });
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Count total endpoints in paths
248
+ */
249
+ countEndpoints(paths) {
250
+ let count = 0;
251
+ Object.values(paths).forEach(pathItem => {
252
+ Object.keys(pathItem).forEach(method => {
253
+ if (method !== 'parameters' && !method.startsWith('x-')) {
254
+ count++;
255
+ }
256
+ });
257
+ });
258
+ return count;
259
+ }
260
+
261
+ /**
262
+ * Validate cookbook structure and operations
263
+ */
264
+ async validateCookbook(cookbook, serviceRegistry) {
265
+ const results = {
266
+ valid: true,
267
+ errors: [],
268
+ warnings: [],
269
+ validatedSteps: []
270
+ };
271
+
272
+ if (!cookbook.steps || !Array.isArray(cookbook.steps)) {
273
+ results.valid = false;
274
+ results.errors.push('Cookbook must have steps array');
275
+ return results;
276
+ }
277
+
278
+ // Validate each step
279
+ for (const step of cookbook.steps) {
280
+ const stepResult = await this.validateStep(step, serviceRegistry);
281
+ results.validatedSteps.push(stepResult);
282
+
283
+ if (!stepResult.valid) {
284
+ results.valid = false;
285
+ results.errors.push(...stepResult.errors);
286
+ }
287
+ if (stepResult.warnings) {
288
+ results.warnings.push(...stepResult.warnings);
289
+ }
290
+ }
291
+
292
+ return results;
293
+ }
294
+
295
+ /**
296
+ * Validate individual cookbook step
297
+ */
298
+ async validateStep(step, serviceRegistry) {
299
+ const result = {
300
+ stepId: step.id,
301
+ valid: true,
302
+ errors: [],
303
+ warnings: []
304
+ };
305
+
306
+ // Basic validation
307
+ if (!step.id) {
308
+ result.valid = false;
309
+ result.errors.push('Step must have an id');
310
+ }
311
+
312
+ if (!step.type) {
313
+ result.valid = false;
314
+ result.errors.push('Step must have a type');
315
+ }
316
+
317
+ // Type-specific validation
318
+ switch (step.type) {
319
+ case 'task':
320
+ if (!step.service) {
321
+ result.valid = false;
322
+ result.errors.push('Task step must have a service');
323
+ } else {
324
+ // Check if service exists
325
+ const service = serviceRegistry.getService(step.service);
326
+ if (!service) {
327
+ result.valid = false;
328
+ result.errors.push(`Service not found: ${step.service}`);
329
+ } else if (step.operation) {
330
+ // Validate operation exists in service
331
+ const hasOperation = this.serviceHasOperation(
332
+ service.openapi,
333
+ step.operation
334
+ );
335
+ if (!hasOperation) {
336
+ result.valid = false;
337
+ result.errors.push(`Operation not found: ${step.operation}`);
338
+ }
339
+ }
340
+ }
341
+ break;
342
+
343
+ case 'foreach':
344
+ if (!step.items) {
345
+ result.valid = false;
346
+ result.errors.push('Foreach step must have items');
347
+ }
348
+ if (!step.body) {
349
+ result.valid = false;
350
+ result.errors.push('Foreach step must have body');
351
+ }
352
+ break;
353
+
354
+ case 'switch':
355
+ if (!step.condition) {
356
+ result.valid = false;
357
+ result.errors.push('Switch step must have condition');
358
+ }
359
+ if (!step.cases || !Array.isArray(step.cases)) {
360
+ result.valid = false;
361
+ result.errors.push('Switch step must have cases array');
362
+ }
363
+ break;
364
+ }
365
+
366
+ return result;
367
+ }
368
+
369
+ /**
370
+ * Check if service has operation
371
+ */
372
+ serviceHasOperation(openApiSpec, operationId) {
373
+ if (!openApiSpec || !openApiSpec.paths) return false;
374
+
375
+ for (const pathItem of Object.values(openApiSpec.paths)) {
376
+ for (const operation of Object.values(pathItem)) {
377
+ if (operation.operationId === operationId) {
378
+ return true;
379
+ }
380
+ }
381
+ }
382
+
383
+ return false;
384
+ }
385
+ }
386
+
387
+ module.exports = ServiceValidator;