@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,396 @@
1
+ 'use strict';
2
+
3
+ const EventEmitter = require('events');
4
+
5
+ /**
6
+ * WorkflowTestRunner - Executes and tests cookbook workflows
7
+ */
8
+ class WorkflowTestRunner extends EventEmitter {
9
+ constructor(options = {}) {
10
+ super();
11
+ this.mqClient = options.mqClient;
12
+ this.registry = options.registry;
13
+ this.storage = options.storage;
14
+ this.timeout = options.timeout || 30000;
15
+ this.debug = options.debug || false;
16
+ this.runningWorkflows = new Map();
17
+ }
18
+
19
+ /**
20
+ * Run workflow test
21
+ */
22
+ async runWorkflow(cookbook, initialContext = {}) {
23
+ const workflowId = `test-workflow-${Date.now()}`;
24
+
25
+ const workflow = {
26
+ id: workflowId,
27
+ cookbook,
28
+ context: {
29
+ api_input: initialContext,
30
+ steps: {}
31
+ },
32
+ status: 'running',
33
+ startedAt: Date.now(),
34
+ currentStep: 0,
35
+ results: [],
36
+ errors: []
37
+ };
38
+
39
+ this.runningWorkflows.set(workflowId, workflow);
40
+ this.emit('workflow:started', { workflowId, cookbook });
41
+
42
+ try {
43
+ // Execute steps sequentially
44
+ for (let i = 0; i < cookbook.steps.length; i++) {
45
+ const step = cookbook.steps[i];
46
+ workflow.currentStep = i;
47
+
48
+ const stepResult = await this.executeStep(step, workflow);
49
+ workflow.results.push(stepResult);
50
+
51
+ if (!stepResult.success) {
52
+ workflow.status = 'failed';
53
+ workflow.errors.push(stepResult.error);
54
+ this.emit('workflow:failed', { workflowId, error: stepResult.error });
55
+ break;
56
+ }
57
+
58
+ // Update context with step result
59
+ workflow.context.steps[step.id] = stepResult.data;
60
+ }
61
+
62
+ if (workflow.status !== 'failed') {
63
+ workflow.status = 'completed';
64
+ workflow.completedAt = Date.now();
65
+ this.emit('workflow:completed', { workflowId, results: workflow.results });
66
+ }
67
+
68
+ } catch (error) {
69
+ workflow.status = 'error';
70
+ workflow.errors.push(error.message);
71
+ this.emit('workflow:error', { workflowId, error: error.message });
72
+ }
73
+
74
+ return workflow;
75
+ }
76
+
77
+ /**
78
+ * Execute individual step
79
+ */
80
+ async executeStep(step, workflow) {
81
+ const startTime = Date.now();
82
+
83
+ this.emit('step:started', {
84
+ workflowId: workflow.id,
85
+ stepId: step.id,
86
+ type: step.type
87
+ });
88
+
89
+ const result = {
90
+ stepId: step.id,
91
+ type: step.type,
92
+ success: false,
93
+ data: null,
94
+ error: null,
95
+ duration: 0
96
+ };
97
+
98
+ try {
99
+ switch (step.type) {
100
+ case 'task':
101
+ result.data = await this.executeTaskStep(step, workflow);
102
+ break;
103
+
104
+ case 'foreach':
105
+ result.data = await this.executeForeachStep(step, workflow);
106
+ break;
107
+
108
+ case 'switch':
109
+ result.data = await this.executeSwitchStep(step, workflow);
110
+ break;
111
+
112
+ case 'fork_join':
113
+ result.data = await this.executeForkJoinStep(step, workflow);
114
+ break;
115
+
116
+ default:
117
+ throw new Error(`Unknown step type: ${step.type}`);
118
+ }
119
+
120
+ result.success = true;
121
+ result.duration = Date.now() - startTime;
122
+
123
+ this.emit('step:completed', {
124
+ workflowId: workflow.id,
125
+ stepId: step.id,
126
+ result
127
+ });
128
+
129
+ } catch (error) {
130
+ result.error = error.message;
131
+ result.duration = Date.now() - startTime;
132
+
133
+ this.emit('step:failed', {
134
+ workflowId: workflow.id,
135
+ stepId: step.id,
136
+ error: error.message
137
+ });
138
+ }
139
+
140
+ return result;
141
+ }
142
+
143
+ /**
144
+ * Execute task step
145
+ */
146
+ async executeTaskStep(step, workflow) {
147
+ const { service, operation, input } = step;
148
+
149
+ // Resolve input using JSONPath
150
+ const resolvedInput = this.resolveInput(input, workflow.context);
151
+
152
+ if (this.debug) {
153
+ console.log(`Executing task: ${service}.${operation}`, resolvedInput);
154
+ }
155
+
156
+ // Get service from registry
157
+ const serviceInfo = await this.registry.getService(service);
158
+ if (!serviceInfo) {
159
+ throw new Error(`Service not found: ${service}`);
160
+ }
161
+
162
+ // Simulate API call or send to queue
163
+ if (this.mqClient) {
164
+ // Send to service queue
165
+ const queueName = `service.${service}.request`;
166
+ await this.mqClient.publish(queueName, {
167
+ workflow_id: workflow.id,
168
+ step_id: step.id,
169
+ operation,
170
+ input: resolvedInput
171
+ });
172
+
173
+ // Wait for response (with timeout)
174
+ const response = await this.waitForResponse(
175
+ workflow.id,
176
+ step.id,
177
+ this.timeout
178
+ );
179
+
180
+ return response;
181
+ } else {
182
+ // Simulate response
183
+ return {
184
+ processed: true,
185
+ operation,
186
+ input: resolvedInput,
187
+ output: { simulated: true }
188
+ };
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Execute foreach step
194
+ */
195
+ async executeForeachStep(step, workflow) {
196
+ const { items, body } = step;
197
+
198
+ // Resolve items array
199
+ const itemsArray = this.resolveInput(items, workflow.context);
200
+ if (!Array.isArray(itemsArray)) {
201
+ throw new Error('Foreach items must resolve to an array');
202
+ }
203
+
204
+ const results = [];
205
+ for (const item of itemsArray) {
206
+ // Create context with current item
207
+ const itemContext = {
208
+ ...workflow.context,
209
+ current_item: item
210
+ };
211
+
212
+ // Execute body step for each item
213
+ const itemWorkflow = { ...workflow, context: itemContext };
214
+ const result = await this.executeStep(body, itemWorkflow);
215
+ results.push(result);
216
+ }
217
+
218
+ return { items: itemsArray.length, results };
219
+ }
220
+
221
+ /**
222
+ * Execute switch step
223
+ */
224
+ async executeSwitchStep(step, workflow) {
225
+ const { condition, cases, default: defaultCase } = step;
226
+
227
+ // Resolve condition value
228
+ const conditionValue = this.resolveInput(condition, workflow.context);
229
+
230
+ // Find matching case
231
+ const matchingCase = cases.find(c => c.value === conditionValue);
232
+ const stepToExecute = matchingCase ? matchingCase.step : defaultCase;
233
+
234
+ if (!stepToExecute) {
235
+ throw new Error(`No matching case for condition value: ${conditionValue}`);
236
+ }
237
+
238
+ // Execute the selected step
239
+ const result = await this.executeStep(stepToExecute, workflow);
240
+
241
+ return {
242
+ condition: conditionValue,
243
+ executed: stepToExecute.id,
244
+ result
245
+ };
246
+ }
247
+
248
+ /**
249
+ * Execute fork-join step
250
+ */
251
+ async executeForkJoinStep(step, workflow) {
252
+ const { branches, join } = step;
253
+
254
+ // Execute all branches in parallel
255
+ const branchPromises = branches.map(branch =>
256
+ this.executeStep(branch, workflow)
257
+ );
258
+
259
+ const results = await Promise.all(branchPromises);
260
+
261
+ // Apply join strategy
262
+ let joinedResult;
263
+ switch (join.strategy) {
264
+ case 'merge':
265
+ joinedResult = this.mergeResults(results);
266
+ break;
267
+ case 'first':
268
+ joinedResult = results[0];
269
+ break;
270
+ case 'all':
271
+ joinedResult = results;
272
+ break;
273
+ default:
274
+ joinedResult = results;
275
+ }
276
+
277
+ return {
278
+ branches: branches.length,
279
+ strategy: join.strategy,
280
+ result: joinedResult
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Resolve input using JSONPath or direct value
286
+ */
287
+ resolveInput(input, context) {
288
+ if (!input) return null;
289
+
290
+ if (typeof input === 'string' && input.startsWith('$')) {
291
+ // JSONPath resolution
292
+ return this.resolveJsonPath(input, context);
293
+ }
294
+
295
+ if (typeof input === 'object' && input !== null) {
296
+ // Recursively resolve object properties
297
+ const resolved = {};
298
+ for (const [key, value] of Object.entries(input)) {
299
+ resolved[key] = this.resolveInput(value, context);
300
+ }
301
+ return resolved;
302
+ }
303
+
304
+ return input;
305
+ }
306
+
307
+ /**
308
+ * Simple JSONPath resolution
309
+ */
310
+ resolveJsonPath(path, context) {
311
+ // Remove leading $
312
+ const pathParts = path.substring(1).split('.');
313
+
314
+ let current = context;
315
+ for (const part of pathParts) {
316
+ if (current === null || current === undefined) {
317
+ return null;
318
+ }
319
+ current = current[part];
320
+ }
321
+
322
+ return current;
323
+ }
324
+
325
+ /**
326
+ * Wait for step response from queue
327
+ */
328
+ async waitForResponse(workflowId, stepId, timeout) {
329
+ return new Promise((resolve, reject) => {
330
+ const timeoutId = setTimeout(() => {
331
+ reject(new Error(`Timeout waiting for response: ${stepId}`));
332
+ }, timeout);
333
+
334
+ // Listen for response on response queue
335
+ const responseQueue = `workflow.${workflowId}.response`;
336
+
337
+ this.mqClient.consume(responseQueue, (msg) => {
338
+ const message = JSON.parse(msg.content.toString());
339
+
340
+ if (message.step_id === stepId) {
341
+ clearTimeout(timeoutId);
342
+ this.mqClient.ack(msg);
343
+ resolve(message.result);
344
+ }
345
+ });
346
+ });
347
+ }
348
+
349
+ /**
350
+ * Merge results from parallel branches
351
+ */
352
+ mergeResults(results) {
353
+ const merged = {};
354
+
355
+ results.forEach((result, index) => {
356
+ if (result.data && typeof result.data === 'object') {
357
+ Object.assign(merged, result.data);
358
+ } else {
359
+ merged[`branch_${index}`] = result.data;
360
+ }
361
+ });
362
+
363
+ return merged;
364
+ }
365
+
366
+ /**
367
+ * Get workflow status
368
+ */
369
+ getWorkflowStatus(workflowId) {
370
+ return this.runningWorkflows.get(workflowId);
371
+ }
372
+
373
+ /**
374
+ * Stop workflow
375
+ */
376
+ stopWorkflow(workflowId) {
377
+ const workflow = this.runningWorkflows.get(workflowId);
378
+ if (workflow && workflow.status === 'running') {
379
+ workflow.status = 'stopped';
380
+ this.emit('workflow:stopped', { workflowId });
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Clear completed workflows
386
+ */
387
+ clearCompleted() {
388
+ for (const [id, workflow] of this.runningWorkflows.entries()) {
389
+ if (workflow.status === 'completed' || workflow.status === 'failed') {
390
+ this.runningWorkflows.delete(id);
391
+ }
392
+ }
393
+ }
394
+ }
395
+
396
+ module.exports = WorkflowTestRunner;
@@ -0,0 +1,235 @@
1
+ # Test Helpers - Generic Test Suite Creators
2
+
3
+ This directory contains **generic test helpers** that create complete test suites for business services.
4
+
5
+ ## Philosophy
6
+
7
+ Instead of **copying test code** between services, we provide **reusable test helpers** that:
8
+ - ✅ Work for **ALL business services** without modification
9
+ - ✅ Automatically validate service structure
10
+ - ✅ Provide clear error messages when something is wrong
11
+ - ✅ Follow SPOT (Single Point Of Truth) principles
12
+ - ✅ Stay up-to-date with latest testing standards
13
+
14
+ ## Available Helpers
15
+
16
+ ### 1. createServiceReadinessTests
17
+
18
+ **Purpose:** Integration test for service HTTP API readiness
19
+
20
+ **File:** `createServiceReadinessTests.js`
21
+
22
+ **What it does:**
23
+ - Validates service structure (conn-config/, src/app.js, package.json)
24
+ - Validates configuration files (config.json, operations.json)
25
+ - Tests all operations endpoints
26
+ - Validates health endpoint
27
+ - Auto-generates cookbook tests (with mocks)
28
+ - Tests registry compatibility (with mocks)
29
+ - Scores 100/100 with all checks
30
+
31
+ **Usage:**
32
+ ```javascript
33
+ // services/my-service/tests/integration/service-readiness.test.js
34
+ const { createServiceReadinessTests } = require('@onlineapps/conn-orch-validator');
35
+
36
+ createServiceReadinessTests(__dirname);
37
+ ```
38
+
39
+ **Options:**
40
+ ```javascript
41
+ createServiceReadinessTests(__dirname, {
42
+ testPort: 5556, // Test server port (default: 5556)
43
+ includeOptionalChecks: true, // Cookbook & registry checks (default: true)
44
+ timeout: 15000 // Test timeout in ms (default: 15000)
45
+ });
46
+ ```
47
+
48
+ **Score Breakdown:**
49
+ - operations: 30 points (required)
50
+ - endpoints: 30 points (required)
51
+ - health: 20 points (required)
52
+ - cookbook: 15 points (optional, with mocks)
53
+ - registry: 5 points (optional, with mocks)
54
+ - **Total: 100/100**
55
+
56
+ ---
57
+
58
+ ### 2. createPreValidationTests
59
+
60
+ **Purpose:** Tier 1 pre-validation process tests
61
+
62
+ **File:** `createPreValidationTests.js`
63
+
64
+ **What it does:**
65
+ - Validates pre-requisites (cookbooks/, operations.json, scripts)
66
+ - Runs cookbook tests with mocked infrastructure
67
+ - Validates ValidationProof generation
68
+ - Tests proof structure and integrity
69
+ - Verifies SHA256 hash correctness
70
+ - Tests tamper detection
71
+ - Validates dependencies tracking
72
+
73
+ **Usage:**
74
+ ```javascript
75
+ // services/my-service/tests/integration/pre-validation-process.test.js
76
+ const { createPreValidationTests } = require('@onlineapps/conn-orch-validator');
77
+
78
+ createPreValidationTests(__dirname);
79
+ ```
80
+
81
+ **Options:**
82
+ ```javascript
83
+ createPreValidationTests(__dirname, {
84
+ timeout: 60000 // Test timeout in ms (default: 60000)
85
+ });
86
+ ```
87
+
88
+ **Test Structure:**
89
+ 1. Pre-requisites Check
90
+ - cookbooks/ directory exists
91
+ - operations.json exists
92
+ - Pre-validation script configured
93
+ 2. Cookbook Test Execution
94
+ - Runs npm run test:cookbooks
95
+ - Verifies infrastructure mocking
96
+ 3. ValidationProof Generation
97
+ - Creates .validation-proof.json
98
+ - Validates proof structure
99
+ - Checks SHA256 hash
100
+ 4. Proof Integrity Verification
101
+ - ValidationProofCodec decode
102
+ - Hash matching
103
+ - Age verification
104
+ - Tamper detection
105
+ 5. Dependencies Tracking
106
+ - Proof includes @onlineapps/* versions
107
+
108
+ ---
109
+
110
+ ## How It Works
111
+
112
+ ### Automatic Structure Validation
113
+
114
+ Both helpers use `ServiceStructureValidator` to validate service structure BEFORE running tests:
115
+
116
+ ```
117
+ 🔍 Validating service structure...
118
+
119
+ ═══════════════════════════════════════════════════════════════
120
+ SERVICE STRUCTURE VALIDATION
121
+ ═══════════════════════════════════════════════════════════════
122
+
123
+ ✅ ALL CHECKS PASSED
124
+
125
+ ✓ Found Configuration directory: conn-config
126
+ ✓ Found Source code directory: src
127
+ ✓ Found Tests directory: tests
128
+ ✓ Found valid config.json
129
+ ✓ Found valid operations.json
130
+ ✓ Found src/app.js
131
+ ✓ Found 3 cookbook test(s)
132
+
133
+ ═══════════════════════════════════════════════════════════════
134
+ ```
135
+
136
+ If validation fails, clear error messages are shown:
137
+
138
+ ```
139
+ ❌ ERRORS (2):
140
+
141
+ ✗ Required directory missing: conn-config
142
+ Type: MISSING_DIRECTORY
143
+ File: conn-config
144
+ Fix: Create directory: mkdir -p conn-config
145
+
146
+ ✗ Service configuration missing: conn-config/config.json
147
+ Type: MISSING_CONFIG
148
+ File: conn-config/config.json
149
+ Fix: Create config.json with service metadata. See: /docs/standards/SERVICE_TEMPLATE.md
150
+
151
+ ⚠️ WARNINGS (1):
152
+
153
+ ⚠ Recommended npm script missing: test:cookbooks
154
+ Suggestion: Add to package.json scripts: "test:cookbooks": "..."
155
+ ```
156
+
157
+ ### Zero Configuration
158
+
159
+ Helpers automatically detect and load:
160
+
161
+ ```javascript
162
+ // Service root (2 levels up from tests/integration/)
163
+ const serviceRoot = path.resolve(__dirname, '../..');
164
+
165
+ // Standard file locations
166
+ const config = require(path.join(serviceRoot, 'conn-config/config.json'));
167
+ const operations = require(path.join(serviceRoot, 'conn-config/operations.json'));
168
+ const app = require(path.join(serviceRoot, 'src/app.js'));
169
+
170
+ // Extract metadata
171
+ const serviceName = config.service.name;
172
+ const serviceVersion = config.service.version;
173
+ const healthEndpoint = config.wrapper?.health?.endpoint || '/health';
174
+ ```
175
+
176
+ No service-specific code needed!
177
+
178
+ ---
179
+
180
+ ## Adding New Helpers
181
+
182
+ When creating new generic test helpers:
183
+
184
+ 1. **Follow naming convention:** `create[Purpose]Tests.js`
185
+ 2. **Accept testsDir as first parameter:** `function create...(testsDir, options)`
186
+ 3. **Calculate service root:** `const serviceRoot = path.resolve(testsDir, '../..');`
187
+ 4. **Validate structure first:** Use `ServiceStructureValidator`
188
+ 5. **Load standard files:** config.json, operations.json, app.js
189
+ 6. **Provide clear output:** Log validation results, test progress
190
+ 7. **Export single function:** `module.exports = { create...Tests };`
191
+ 8. **Update index.js:** Add to exports
192
+ 9. **Document in README.md:** Add to "Available Helpers" section
193
+
194
+ **Example skeleton:**
195
+ ```javascript
196
+ const path = require('path');
197
+ const { ServiceStructureValidator } = require('../validators/ServiceStructureValidator');
198
+
199
+ function createMyTests(testsDir, options = {}) {
200
+ const serviceRoot = path.resolve(testsDir, '../..');
201
+
202
+ // 1. Validate structure
203
+ const validator = new ServiceStructureValidator(serviceRoot);
204
+ const result = validator.validate();
205
+ console.log(ServiceStructureValidator.formatResult(result));
206
+
207
+ if (!result.valid) {
208
+ throw new Error('Structure validation failed');
209
+ }
210
+
211
+ // 2. Load config
212
+ const config = require(path.join(serviceRoot, 'conn-config/config.json'));
213
+
214
+ // 3. Create test suite
215
+ describe('My Test Suite @integration', () => {
216
+ // tests...
217
+ });
218
+ }
219
+
220
+ module.exports = { createMyTests };
221
+ ```
222
+
223
+ ---
224
+
225
+ ## Related Documentation
226
+
227
+ - [/docs/standards/TESTING.md](/docs/standards/TESTING.md) - Testing standards
228
+ - [/tests/TESTING.md](/tests/TESTING.md) - SPOT principles
229
+ - [/shared/connector/conn-orch-validator/README.md](/shared/connector/conn-orch-validator/README.md) - Package documentation
230
+ - [/shared/connector/conn-orch-validator/docs/DESIGN.md](/shared/connector/conn-orch-validator/docs/DESIGN.md) - Design principles
231
+
232
+ ---
233
+
234
+ *Last updated: 2025-10-21*
235
+ *Maintained by: OA Drive Core Team*