@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,237 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CookbookTestUtils - Utilities for cookbook testing
5
+ */
6
+ class CookbookTestUtils {
7
+ /**
8
+ * Create a mock workflow message
9
+ */
10
+ static createWorkflowMessage(options = {}) {
11
+ return {
12
+ workflow_id: options.workflow_id || `test-workflow-${Date.now()}`,
13
+ cookbook: options.cookbook || {
14
+ version: '1.0.0',
15
+ steps: options.steps || [
16
+ {
17
+ id: 'step1',
18
+ type: 'task',
19
+ service: options.serviceName || 'test-service',
20
+ operation: 'testOperation',
21
+ input: options.input || {},
22
+ output: options.output || {}
23
+ }
24
+ ]
25
+ },
26
+ current_step: options.current_step || 'step1',
27
+ context: options.context || {
28
+ api_input: options.api_input || {},
29
+ steps: options.previous_steps || {}
30
+ }
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Generate test cookbook with multiple steps
36
+ */
37
+ static generateTestCookbook(options = {}) {
38
+ const stepCount = options.steps || 3;
39
+ const services = options.services || ['service1', 'service2'];
40
+ const steps = [];
41
+
42
+ for (let i = 1; i <= stepCount; i++) {
43
+ steps.push({
44
+ id: `step${i}`,
45
+ type: 'task',
46
+ service: services[i % services.length],
47
+ operation: `operation${i}`,
48
+ input: { data: `input${i}` },
49
+ output: { result: `$.result${i}` }
50
+ });
51
+ }
52
+
53
+ return {
54
+ version: '1.0.0',
55
+ api_input: options.api_input || { test: 'data' },
56
+ steps
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Validate cookbook structure (basic validation)
62
+ */
63
+ static validateCookbook(cookbook) {
64
+ const errors = [];
65
+
66
+ if (!cookbook) {
67
+ errors.push('Cookbook is required');
68
+ return { valid: false, errors };
69
+ }
70
+
71
+ if (!cookbook.version) {
72
+ errors.push('Version is required');
73
+ }
74
+
75
+ if (!cookbook.steps || !Array.isArray(cookbook.steps)) {
76
+ errors.push('Steps array is required');
77
+ } else {
78
+ cookbook.steps.forEach((step, index) => {
79
+ if (!step.id) {
80
+ errors.push(`Step ${index}: id is required`);
81
+ }
82
+ if (!step.type) {
83
+ errors.push(`Step ${index}: type is required`);
84
+ }
85
+ if (step.type === 'task' && !step.service) {
86
+ errors.push(`Step ${index}: service is required for task steps`);
87
+ }
88
+ });
89
+ }
90
+
91
+ return {
92
+ valid: errors.length === 0,
93
+ errors
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Compare two cookbooks and find differences
99
+ */
100
+ static compareCookbooks(cookbook1, cookbook2) {
101
+ const differences = [];
102
+
103
+ // Compare basic properties
104
+ if (cookbook1.version !== cookbook2.version) {
105
+ differences.push({
106
+ field: 'version',
107
+ cookbook1: cookbook1.version,
108
+ cookbook2: cookbook2.version
109
+ });
110
+ }
111
+
112
+ // Compare steps
113
+ const steps1 = cookbook1.steps || [];
114
+ const steps2 = cookbook2.steps || [];
115
+
116
+ if (steps1.length !== steps2.length) {
117
+ differences.push({
118
+ field: 'steps.length',
119
+ cookbook1: steps1.length,
120
+ cookbook2: steps2.length
121
+ });
122
+ }
123
+
124
+ // Compare individual steps
125
+ const minLength = Math.min(steps1.length, steps2.length);
126
+ for (let i = 0; i < minLength; i++) {
127
+ const step1 = steps1[i];
128
+ const step2 = steps2[i];
129
+
130
+ if (step1.id !== step2.id) {
131
+ differences.push({
132
+ field: `steps[${i}].id`,
133
+ cookbook1: step1.id,
134
+ cookbook2: step2.id
135
+ });
136
+ }
137
+
138
+ if (step1.type !== step2.type) {
139
+ differences.push({
140
+ field: `steps[${i}].type`,
141
+ cookbook1: step1.type,
142
+ cookbook2: step2.type
143
+ });
144
+ }
145
+
146
+ if (step1.service !== step2.service) {
147
+ differences.push({
148
+ field: `steps[${i}].service`,
149
+ cookbook1: step1.service,
150
+ cookbook2: step2.service
151
+ });
152
+ }
153
+ }
154
+
155
+ return differences;
156
+ }
157
+
158
+ /**
159
+ * Create test context with previous step results
160
+ */
161
+ static createContext(previousSteps = {}) {
162
+ return {
163
+ api_input: {
164
+ test_data: 'test',
165
+ timestamp: Date.now()
166
+ },
167
+ steps: previousSteps,
168
+ metadata: {
169
+ workflow_id: `test-${Date.now()}`,
170
+ started_at: new Date().toISOString()
171
+ }
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Simulate step execution result
177
+ */
178
+ static createStepResult(stepId, success = true, data = {}) {
179
+ return {
180
+ stepId,
181
+ success,
182
+ data: data || { processed: true },
183
+ timestamp: Date.now(),
184
+ duration: Math.floor(Math.random() * 1000)
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Generate control flow step (foreach, switch, etc.)
190
+ */
191
+ static createControlFlowStep(type, options = {}) {
192
+ const baseStep = {
193
+ id: options.id || `${type}-step-${Date.now()}`,
194
+ type
195
+ };
196
+
197
+ switch (type) {
198
+ case 'foreach':
199
+ return {
200
+ ...baseStep,
201
+ items: options.items || '$api_input.items',
202
+ body: options.body || {
203
+ id: 'foreach-body',
204
+ type: 'task',
205
+ service: 'test-service',
206
+ operation: 'processItem'
207
+ }
208
+ };
209
+
210
+ case 'switch':
211
+ return {
212
+ ...baseStep,
213
+ condition: options.condition || '$api_input.type',
214
+ cases: options.cases || [
215
+ { value: 'type1', step: { id: 'case1', type: 'task', service: 'service1' } },
216
+ { value: 'type2', step: { id: 'case2', type: 'task', service: 'service2' } }
217
+ ],
218
+ default: options.default || { id: 'default', type: 'task', service: 'default-service' }
219
+ };
220
+
221
+ case 'fork_join':
222
+ return {
223
+ ...baseStep,
224
+ branches: options.branches || [
225
+ { id: 'branch1', type: 'task', service: 'service1' },
226
+ { id: 'branch2', type: 'task', service: 'service2' }
227
+ ],
228
+ join: options.join || { strategy: 'merge' }
229
+ };
230
+
231
+ default:
232
+ return baseStep;
233
+ }
234
+ }
235
+ }
236
+
237
+ module.exports = CookbookTestUtils;
@@ -0,0 +1,430 @@
1
+ 'use strict';
2
+
3
+ const ServiceValidator = require('./ServiceValidator');
4
+ const CookbookTestUtils = require('./CookbookTestUtils');
5
+
6
+ /**
7
+ * ServiceReadinessValidator - Orchestrates complete service validation
8
+ * Used by service-wrapper to verify service is ready for production
9
+ *
10
+ * IMPORTANT: Only supports operations.json format. OpenAPI is deprecated.
11
+ */
12
+ class ServiceReadinessValidator {
13
+ constructor(options = {}) {
14
+ this.validator = new ServiceValidator(options);
15
+ this.logger = options.logger || console;
16
+
17
+ // Readiness checks: core (80 points) + optional (20 points) = 100 points max
18
+ // Core checks ALWAYS run, optional checks run if testCookbook/registry provided
19
+ // See: /shared/connector/conn-orch-validator/README.md for usage pattern
20
+ this.checks = {
21
+ operations: { weight: 30, required: true }, // operations.json structure valid
22
+ endpoints: { weight: 30, required: true }, // all endpoints respond correctly
23
+ health: { weight: 20, required: true }, // health check works
24
+ cookbook: { weight: 15, required: false }, // OPTIONAL - cookbook valid (with mocks)
25
+ registry: { weight: 5, required: false } // OPTIONAL - registry compatible (MockRegistry)
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Perform complete readiness check
31
+ * @param {Object} service - Service configuration
32
+ * @returns {Object} Validation results with score
33
+ */
34
+ async validateReadiness(service) {
35
+ const {
36
+ url,
37
+ operations,
38
+ registry,
39
+ testCookbook,
40
+ healthEndpoint = '/health'
41
+ } = service;
42
+
43
+ const results = {
44
+ timestamp: Date.now(),
45
+ serviceName: service.name,
46
+ serviceUrl: url,
47
+ checks: {},
48
+ score: 0,
49
+ maxScore: 100,
50
+ ready: false,
51
+ errors: [],
52
+ warnings: []
53
+ };
54
+
55
+ // 1. Validate operations.json structure
56
+ if (operations) {
57
+ results.checks.operations = await this.checkOperationsCompliance(operations);
58
+ if (results.checks.operations.passed) {
59
+ results.score += this.checks.operations.weight;
60
+ } else if (this.checks.operations.required) {
61
+ results.errors.push('Operations validation failed');
62
+ }
63
+ } else {
64
+ results.errors.push('No operations.json provided - required for all services');
65
+ results.checks.operations = { passed: false, error: 'Missing operations.json' };
66
+ }
67
+
68
+ // 2. Test all declared operations endpoints
69
+ if (operations && results.checks.operations?.passed) {
70
+ results.checks.endpoints = await this.checkOperationsEndpoints(url, operations);
71
+ if (results.checks.endpoints.passed) {
72
+ results.score += this.checks.endpoints.weight;
73
+ } else if (this.checks.endpoints.required) {
74
+ results.errors.push('Endpoint validation failed');
75
+ }
76
+ }
77
+
78
+ // 3. Verify health check
79
+ results.checks.health = await this.checkHealth(url + healthEndpoint);
80
+ if (results.checks.health.passed) {
81
+ results.score += this.checks.health.weight;
82
+ } else if (this.checks.health.required) {
83
+ results.errors.push('Health check failed');
84
+ }
85
+
86
+ // 4. Test cookbook execution (if provided)
87
+ if (testCookbook) {
88
+ results.checks.cookbook = await this.checkCookbookExecution(
89
+ testCookbook
90
+ );
91
+ if (results.checks.cookbook.passed) {
92
+ results.score += this.checks.cookbook.weight;
93
+ } else if (this.checks.cookbook.required) {
94
+ results.errors.push('Cookbook validation failed');
95
+ }
96
+ }
97
+
98
+ // 5. Verify registry compatibility
99
+ if (registry) {
100
+ results.checks.registry = await this.checkRegistryCompatibility(
101
+ service,
102
+ registry
103
+ );
104
+ if (results.checks.registry.passed) {
105
+ results.score += this.checks.registry.weight;
106
+ } else if (this.checks.registry.required) {
107
+ results.errors.push('Registry compatibility check failed');
108
+ }
109
+ }
110
+
111
+ // Calculate readiness
112
+ const requiredChecks = Object.entries(this.checks)
113
+ .filter(([_, config]) => config.required)
114
+ .map(([name, _]) => name);
115
+
116
+ const requiredPassed = requiredChecks.every(
117
+ check => !results.checks[check] || results.checks[check].passed
118
+ );
119
+
120
+ results.ready = requiredPassed && results.score >= 60;
121
+ results.recommendation = this.getRecommendation(results);
122
+
123
+ return results;
124
+ }
125
+
126
+ /**
127
+ * Check operations.json compliance
128
+ */
129
+ async checkOperationsCompliance(operations) {
130
+ try {
131
+ const errors = [];
132
+ const warnings = [];
133
+
134
+ // Validate operations structure
135
+ if (!operations || typeof operations !== 'object') {
136
+ return {
137
+ passed: false,
138
+ error: 'Operations must be an object'
139
+ };
140
+ }
141
+
142
+ const operationCount = Object.keys(operations).length;
143
+ if (operationCount === 0) {
144
+ return {
145
+ passed: false,
146
+ error: 'No operations defined'
147
+ };
148
+ }
149
+
150
+ // Validate each operation
151
+ for (const [name, operation] of Object.entries(operations)) {
152
+ // Required fields
153
+ if (!operation.description) {
154
+ errors.push(`Operation '${name}' missing description`);
155
+ }
156
+ if (!operation.endpoint) {
157
+ errors.push(`Operation '${name}' missing endpoint`);
158
+ }
159
+ if (!operation.method) {
160
+ errors.push(`Operation '${name}' missing method`);
161
+ }
162
+ if (!operation.input) {
163
+ errors.push(`Operation '${name}' missing input schema`);
164
+ }
165
+ if (!operation.output) {
166
+ errors.push(`Operation '${name}' missing output schema`);
167
+ }
168
+
169
+ // Validate method
170
+ const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
171
+ if (operation.method && !validMethods.includes(operation.method)) {
172
+ errors.push(`Operation '${name}' has invalid method: ${operation.method}`);
173
+ }
174
+
175
+ // Validate endpoint format
176
+ if (operation.endpoint && !operation.endpoint.startsWith('/')) {
177
+ warnings.push(`Operation '${name}' endpoint should start with /`);
178
+ }
179
+ }
180
+
181
+ return {
182
+ passed: errors.length === 0,
183
+ operationCount,
184
+ errors,
185
+ warnings
186
+ };
187
+ } catch (error) {
188
+ return {
189
+ passed: false,
190
+ error: error.message
191
+ };
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Check all operations endpoints respond correctly
197
+ */
198
+ async checkOperationsEndpoints(serviceUrl, operations) {
199
+ const axios = require('axios');
200
+ const results = {
201
+ passed: true,
202
+ total: Object.keys(operations).length,
203
+ successful: 0,
204
+ failed: 0,
205
+ details: []
206
+ };
207
+
208
+ for (const [operationName, operation] of Object.entries(operations)) {
209
+ try {
210
+ const endpoint = operation.endpoint;
211
+ const method = operation.method.toLowerCase();
212
+ const url = `${serviceUrl}${endpoint}`;
213
+
214
+ // Generate test input based on schema
215
+ const testInput = this.generateTestInput(operation.input);
216
+
217
+ // Make request
218
+ const response = await axios({
219
+ method,
220
+ url,
221
+ data: method !== 'get' ? testInput : undefined,
222
+ params: method === 'get' ? testInput : undefined,
223
+ timeout: 5000,
224
+ validateStatus: () => true // Accept any status for validation
225
+ });
226
+
227
+ // Check response
228
+ const isValid = response.status >= 200 && response.status < 300;
229
+
230
+ if (isValid) {
231
+ results.successful++;
232
+ } else {
233
+ results.failed++;
234
+ results.passed = false;
235
+ }
236
+
237
+ results.details.push({
238
+ operation: operationName,
239
+ endpoint,
240
+ method: method.toUpperCase(),
241
+ status: response.status,
242
+ valid: isValid
243
+ });
244
+ } catch (error) {
245
+ results.failed++;
246
+ results.passed = false;
247
+ results.details.push({
248
+ operation: operationName,
249
+ endpoint: operation.endpoint,
250
+ method: operation.method,
251
+ valid: false,
252
+ error: error.message
253
+ });
254
+ }
255
+ }
256
+
257
+ return results;
258
+ }
259
+
260
+ /**
261
+ * Check health endpoint
262
+ */
263
+ async checkHealth(healthUrl) {
264
+ try {
265
+ const axios = require('axios');
266
+ const response = await axios.get(healthUrl, { timeout: 5000 });
267
+
268
+ return {
269
+ passed: response.status === 200,
270
+ status: response.status,
271
+ data: response.data
272
+ };
273
+ } catch (error) {
274
+ return {
275
+ passed: false,
276
+ error: error.message
277
+ };
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Check cookbook structure validity
283
+ * Simple structural validation - checks cookbook has required fields and valid structure
284
+ */
285
+ async checkCookbookExecution(cookbook) {
286
+ try {
287
+ // Validate cookbook structure using CookbookTestUtils
288
+ const structureValidation = CookbookTestUtils.validateCookbook(cookbook);
289
+
290
+ return {
291
+ passed: structureValidation.valid,
292
+ errors: structureValidation.errors || [],
293
+ stepsValidated: cookbook.steps ? cookbook.steps.length : 0
294
+ };
295
+ } catch (error) {
296
+ return {
297
+ passed: false,
298
+ error: error.message
299
+ };
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Check registry compatibility
305
+ */
306
+ async checkRegistryCompatibility(service, registry) {
307
+ try {
308
+ // Check if service can be registered
309
+ const canRegister = !!(
310
+ service.name &&
311
+ service.version &&
312
+ service.operations
313
+ );
314
+
315
+ // Check if service operations match registry expectations
316
+ const registeredService = registry.getService(service.name);
317
+ const compatible = !registeredService ||
318
+ registeredService.version === service.version;
319
+
320
+ return {
321
+ passed: canRegister && compatible,
322
+ canRegister,
323
+ compatible,
324
+ existingVersion: registeredService?.version
325
+ };
326
+ } catch (error) {
327
+ return {
328
+ passed: false,
329
+ error: error.message
330
+ };
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Generate test input based on operation input schema
336
+ */
337
+ generateTestInput(inputSchema) {
338
+ const testData = {};
339
+
340
+ for (const [fieldName, fieldSpec] of Object.entries(inputSchema)) {
341
+ if (fieldSpec.required) {
342
+ // Generate appropriate test value based on type
343
+ switch (fieldSpec.type) {
344
+ case 'string':
345
+ testData[fieldName] = fieldSpec.default || 'Test';
346
+ break;
347
+ case 'number':
348
+ testData[fieldName] = fieldSpec.default || 0;
349
+ break;
350
+ case 'boolean':
351
+ testData[fieldName] = fieldSpec.default || false;
352
+ break;
353
+ case 'array':
354
+ testData[fieldName] = fieldSpec.default || [];
355
+ break;
356
+ case 'object':
357
+ testData[fieldName] = fieldSpec.default || {};
358
+ break;
359
+ default:
360
+ testData[fieldName] = null;
361
+ }
362
+ }
363
+ }
364
+
365
+ return testData;
366
+ }
367
+
368
+ /**
369
+ * Get recommendation based on results
370
+ */
371
+ getRecommendation(results) {
372
+ if (results.ready) {
373
+ if (results.score >= 90) {
374
+ return 'Service is fully ready for production deployment';
375
+ } else if (results.score >= 75) {
376
+ return 'Service is ready but consider addressing warnings';
377
+ } else {
378
+ return 'Service meets minimum requirements but needs improvement';
379
+ }
380
+ } else {
381
+ if (results.errors.length > 0) {
382
+ return `Service is not ready: ${results.errors.join(', ')}`;
383
+ } else {
384
+ return 'Service does not meet minimum readiness requirements';
385
+ }
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Generate readiness report
391
+ */
392
+ generateReport(results) {
393
+ const report = [];
394
+
395
+ report.push('=== Service Readiness Report ===');
396
+ report.push(`Service: ${results.serviceName}`);
397
+ report.push(`URL: ${results.serviceUrl}`);
398
+ report.push(`Score: ${results.score}/${results.maxScore}`);
399
+ report.push(`Status: ${results.ready ? 'READY' : 'NOT READY'}`);
400
+ report.push('');
401
+
402
+ report.push('Checks:');
403
+ for (const [name, check] of Object.entries(results.checks)) {
404
+ const status = check.passed ? '✓' : '✗';
405
+ report.push(` ${status} ${name}: ${check.passed ? 'Passed' : 'Failed'}`);
406
+ if (check.error) {
407
+ report.push(` Error: ${check.error}`);
408
+ }
409
+ }
410
+
411
+ report.push('');
412
+ report.push(`Recommendation: ${results.recommendation}`);
413
+
414
+ if (results.errors.length > 0) {
415
+ report.push('');
416
+ report.push('Errors:');
417
+ results.errors.forEach(err => report.push(` - ${err}`));
418
+ }
419
+
420
+ if (results.warnings.length > 0) {
421
+ report.push('');
422
+ report.push('Warnings:');
423
+ results.warnings.forEach(warn => report.push(` - ${warn}`));
424
+ }
425
+
426
+ return report.join('\n');
427
+ }
428
+ }
429
+
430
+ module.exports = ServiceReadinessValidator;