@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
package/jest.config.js ADDED
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ testEnvironment: 'node',
5
+ coverageDirectory: 'coverage',
6
+ collectCoverageFrom: [
7
+ 'src/**/*.js',
8
+ '!src/index.js'
9
+ ],
10
+ testMatch: [
11
+ '**/tests/**/*.test.js'
12
+ ],
13
+ coverageThreshold: {
14
+ global: {
15
+ branches: 80,
16
+ functions: 80,
17
+ lines: 80,
18
+ statements: 80
19
+ }
20
+ },
21
+ verbose: true,
22
+ testTimeout: 10000
23
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@onlineapps/conn-orch-validator",
3
+ "version": "2.0.0",
4
+ "description": "Validation orchestrator for OA Drive microservices - coordinates validation across all layers (base, infra, orch, business)",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "test": "jest",
8
+ "test:unit": "jest tests/unit",
9
+ "test:component": "jest tests/component",
10
+ "test:integration": "jest tests/integration",
11
+ "test:coverage": "jest --coverage"
12
+ },
13
+ "keywords": [
14
+ "validation",
15
+ "orchestration",
16
+ "microservices",
17
+ "production-validation",
18
+ "tier1",
19
+ "pre-validation"
20
+ ],
21
+ "author": "OnlineApps",
22
+ "license": "PROPRIETARY",
23
+ "dependencies": {
24
+ "@onlineapps/service-validator-core": "^1.0.4",
25
+ "ajv": "^8.12.0",
26
+ "ajv-formats": "^2.1.1",
27
+ "amqplib": "^0.10.9",
28
+ "axios": "^1.4.0",
29
+ "joi": "^17.9.0"
30
+ },
31
+ "devDependencies": {
32
+ "express": "^4.18.0",
33
+ "jest": "^29.5.0"
34
+ },
35
+ "engines": {
36
+ "node": ">=14.0.0"
37
+ },
38
+ "directories": {
39
+ "doc": "docs",
40
+ "example": "examples",
41
+ "test": "test"
42
+ }
43
+ }
@@ -0,0 +1,434 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const axios = require('axios');
6
+ const MockMQClient = require('./mocks/MockMQClient');
7
+ const MockRegistry = require('./mocks/MockRegistry');
8
+
9
+ /**
10
+ * CookbookTestRunner - Executes cookbook tests offline with mocked infrastructure
11
+ *
12
+ * Supports unified cookbook format for both testing (with expect) and production (without expect)
13
+ */
14
+ class CookbookTestRunner {
15
+ constructor(options = {}) {
16
+ this.serviceName = options.serviceName;
17
+ this.serviceUrl = options.serviceUrl || 'http://localhost:3000';
18
+ this.servicePath = options.servicePath;
19
+ this.mockInfrastructure = options.mockInfrastructure !== false; // default true
20
+ this.timeout = options.timeout || 30000;
21
+ this.logger = options.logger || console;
22
+
23
+ // Initialize mocked infrastructure
24
+ if (this.mockInfrastructure) {
25
+ this.mqClient = new MockMQClient();
26
+ this.registry = new MockRegistry();
27
+ }
28
+
29
+ // Test results
30
+ this.results = {
31
+ total: 0,
32
+ passed: 0,
33
+ failed: 0,
34
+ duration: 0,
35
+ steps: []
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Run single cookbook test
41
+ *
42
+ * @param {string|Object} cookbook - Path to cookbook file or cookbook object
43
+ * @returns {Object} Test results
44
+ */
45
+ async runCookbook(cookbook) {
46
+ const startTime = Date.now();
47
+
48
+ // Load cookbook
49
+ const cookbookData = typeof cookbook === 'string'
50
+ ? this.loadCookbook(cookbook)
51
+ : cookbook;
52
+
53
+ // Validate cookbook format
54
+ this.validateCookbook(cookbookData);
55
+
56
+ // Get test configuration
57
+ const testConfig = cookbookData.test || { mode: 'production' };
58
+
59
+ // Check if this is a test cookbook (has expect clauses)
60
+ const isTestMode = testConfig.mode === 'offline' || this.hasExpectClauses(cookbookData);
61
+
62
+ this.logger.info(`Running cookbook: ${cookbookData.description || 'Unnamed'}`);
63
+ this.logger.info(`Mode: ${testConfig.mode || 'production'}, Test: ${isTestMode}`);
64
+
65
+ // Execute steps
66
+ const stepResults = [];
67
+ for (const step of cookbookData.steps) {
68
+ const stepResult = await this.executeStep(step, testConfig);
69
+ stepResults.push(stepResult);
70
+
71
+ // If step failed and has expect, fail immediately
72
+ if (!stepResult.passed && step.expect) {
73
+ break;
74
+ }
75
+ }
76
+
77
+ // Calculate results
78
+ const duration = Date.now() - startTime;
79
+ const passed = stepResults.every(r => r.passed);
80
+ const failed = stepResults.filter(r => !r.passed).length;
81
+
82
+ const results = {
83
+ cookbook: cookbookData.description || 'Unnamed',
84
+ passed,
85
+ total: stepResults.length,
86
+ passed: stepResults.filter(r => r.passed).length,
87
+ failed,
88
+ duration,
89
+ steps: stepResults
90
+ };
91
+
92
+ // Update aggregate results
93
+ this.results.total += results.total;
94
+ this.results.passed += results.passed;
95
+ this.results.failed += results.failed;
96
+ this.results.duration += results.duration;
97
+ this.results.steps.push(...results.steps);
98
+
99
+ return results;
100
+ }
101
+
102
+ /**
103
+ * Run all cookbooks in a directory
104
+ *
105
+ * @param {string} cookbooksDir - Path to cookbooks directory
106
+ * @returns {Object} Aggregate results
107
+ */
108
+ async runCookbooks(cookbooksDir) {
109
+ const files = fs.readdirSync(cookbooksDir).filter(f => f.endsWith('.json'));
110
+
111
+ this.logger.info(`Found ${files.length} cookbook(s) in ${cookbooksDir}`);
112
+
113
+ for (const file of files) {
114
+ const cookbookPath = path.join(cookbooksDir, file);
115
+ try {
116
+ await this.runCookbook(cookbookPath);
117
+ } catch (error) {
118
+ this.logger.error(`Failed to run cookbook ${file}:`, error.message);
119
+ this.results.failed++;
120
+ this.results.steps.push({
121
+ cookbook: file,
122
+ passed: false,
123
+ error: error.message
124
+ });
125
+ }
126
+ }
127
+
128
+ return this.results;
129
+ }
130
+
131
+ /**
132
+ * Execute single step
133
+ */
134
+ async executeStep(step, testConfig) {
135
+ const startTime = Date.now();
136
+
137
+ this.logger.info(`Executing step: ${step.id || 'unnamed'} (${step.operation})`);
138
+
139
+ const result = {
140
+ id: step.id,
141
+ operation: step.operation,
142
+ passed: false,
143
+ duration: 0,
144
+ request: null,
145
+ response: null,
146
+ expected: step.expect || null,
147
+ actual: null,
148
+ error: null,
149
+ validationErrors: []
150
+ };
151
+
152
+ try {
153
+ // Resolve operation endpoint from operations.json
154
+ const endpoint = await this.resolveOperation(step.service, step.operation);
155
+
156
+ // Build request
157
+ const request = {
158
+ method: endpoint.method,
159
+ url: `${this.serviceUrl}${endpoint.path}`,
160
+ data: step.input,
161
+ timeout: testConfig.timeout || this.timeout
162
+ };
163
+
164
+ result.request = request;
165
+
166
+ // Execute request
167
+ const response = await axios(request);
168
+
169
+ result.response = {
170
+ status: response.status,
171
+ statusText: response.statusText,
172
+ data: response.data
173
+ };
174
+
175
+ result.actual = response.data;
176
+ result.duration = Date.now() - startTime;
177
+
178
+ // Validate expectations
179
+ if (step.expect) {
180
+ const validation = this.validateExpectations(step.expect, result);
181
+ result.passed = validation.passed;
182
+ result.validationErrors = validation.errors;
183
+ } else {
184
+ // No expectations = just check if request succeeded
185
+ result.passed = response.status >= 200 && response.status < 300;
186
+ }
187
+
188
+ this.logger.info(`Step ${step.id}: ${result.passed ? 'PASSED' : 'FAILED'} (${result.duration}ms)`);
189
+
190
+ } catch (error) {
191
+ result.error = error.message;
192
+ result.duration = Date.now() - startTime;
193
+ result.passed = false;
194
+
195
+ this.logger.error(`Step ${step.id}: ERROR - ${error.message}`);
196
+ }
197
+
198
+ return result;
199
+ }
200
+
201
+ /**
202
+ * Validate expectations against actual results
203
+ */
204
+ validateExpectations(expect, result) {
205
+ const errors = [];
206
+
207
+ // Validate status
208
+ if (expect.status) {
209
+ const expectedSuccess = expect.status === 'success';
210
+ const actualSuccess = result.response && result.response.status >= 200 && result.response.status < 300;
211
+
212
+ if (expectedSuccess !== actualSuccess) {
213
+ errors.push(`Expected status: ${expect.status}, got: ${actualSuccess ? 'success' : 'error'}`);
214
+ }
215
+ }
216
+
217
+ // Validate status code
218
+ if (expect.statusCode && result.response) {
219
+ if (result.response.status !== expect.statusCode) {
220
+ errors.push(`Expected status code: ${expect.statusCode}, got: ${result.response.status}`);
221
+ }
222
+ }
223
+
224
+ // Validate duration
225
+ if (expect.duration && expect.duration.max) {
226
+ if (result.duration > expect.duration.max) {
227
+ errors.push(`Duration ${result.duration}ms exceeds max ${expect.duration.max}ms`);
228
+ }
229
+ }
230
+
231
+ // Validate output
232
+ if (expect.output && result.actual) {
233
+ const outputErrors = this.validateOutput(expect.output, result.actual);
234
+ errors.push(...outputErrors);
235
+ }
236
+
237
+ // Validate error
238
+ if (expect.error && result.error) {
239
+ const errorErrors = this.validateError(expect.error, result.error);
240
+ errors.push(...errorErrors);
241
+ }
242
+
243
+ return {
244
+ passed: errors.length === 0,
245
+ errors
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Validate output fields
251
+ */
252
+ validateOutput(expected, actual) {
253
+ const errors = [];
254
+
255
+ for (const [field, expectation] of Object.entries(expected)) {
256
+ const actualValue = actual[field];
257
+
258
+ // Check existence
259
+ if (expectation.exists !== undefined) {
260
+ const exists = actualValue !== undefined && actualValue !== null;
261
+ if (expectation.exists !== exists) {
262
+ errors.push(`Field ${field}: expected to ${expectation.exists ? 'exist' : 'not exist'}`);
263
+ }
264
+ // If we're only checking existence, skip other validations
265
+ if (!exists) {
266
+ continue;
267
+ }
268
+ }
269
+
270
+ // If field doesn't exist and we're not explicitly checking existence, error
271
+ if (actualValue === undefined || actualValue === null) {
272
+ if (expectation.exists === undefined) {
273
+ errors.push(`Field ${field}: missing in output`);
274
+ }
275
+ continue;
276
+ }
277
+
278
+ // Type check
279
+ if (expectation.type) {
280
+ const actualType = typeof actualValue;
281
+ if (actualType !== expectation.type) {
282
+ errors.push(`Field ${field}: expected type ${expectation.type}, got ${actualType}`);
283
+ }
284
+ }
285
+
286
+ // Equals check
287
+ if (expectation.equals !== undefined) {
288
+ if (actualValue !== expectation.equals) {
289
+ errors.push(`Field ${field}: expected ${expectation.equals}, got ${actualValue}`);
290
+ }
291
+ }
292
+
293
+ // Contains check (for strings)
294
+ if (expectation.contains) {
295
+ if (typeof actualValue !== 'string' || !actualValue.includes(expectation.contains)) {
296
+ errors.push(`Field ${field}: expected to contain "${expectation.contains}"`);
297
+ }
298
+ }
299
+
300
+ // Pattern check (regex)
301
+ if (expectation.pattern) {
302
+ const regex = new RegExp(expectation.pattern);
303
+ if (!regex.test(String(actualValue))) {
304
+ errors.push(`Field ${field}: does not match pattern ${expectation.pattern}`);
305
+ }
306
+ }
307
+
308
+ // Range checks
309
+ if (expectation.min !== undefined && actualValue < expectation.min) {
310
+ errors.push(`Field ${field}: ${actualValue} is less than min ${expectation.min}`);
311
+ }
312
+
313
+ if (expectation.max !== undefined && actualValue > expectation.max) {
314
+ errors.push(`Field ${field}: ${actualValue} exceeds max ${expectation.max}`);
315
+ }
316
+
317
+ // Length check
318
+ if (expectation.length !== undefined) {
319
+ const length = Array.isArray(actualValue) ? actualValue.length : String(actualValue).length;
320
+ if (length !== expectation.length) {
321
+ errors.push(`Field ${field}: expected length ${expectation.length}, got ${length}`);
322
+ }
323
+ }
324
+ }
325
+
326
+ return errors;
327
+ }
328
+
329
+ /**
330
+ * Validate error expectations
331
+ */
332
+ validateError(expected, actual) {
333
+ const errors = [];
334
+
335
+ if (expected.code && actual.code !== expected.code) {
336
+ errors.push(`Expected error code: ${expected.code}, got: ${actual.code}`);
337
+ }
338
+
339
+ if (expected.message) {
340
+ if (expected.message.contains && !actual.message.includes(expected.message.contains)) {
341
+ errors.push(`Expected error message to contain: "${expected.message.contains}"`);
342
+ }
343
+ }
344
+
345
+ return errors;
346
+ }
347
+
348
+ /**
349
+ * Resolve operation to HTTP endpoint
350
+ */
351
+ async resolveOperation(serviceName, operationName) {
352
+ // Load operations.json from service path
353
+ const operationsPath = this.servicePath
354
+ ? path.join(this.servicePath, 'conn-config', 'operations.json')
355
+ : null;
356
+
357
+ if (!operationsPath || !fs.existsSync(operationsPath)) {
358
+ throw new Error(`Operations file not found for service: ${serviceName}`);
359
+ }
360
+
361
+ const operations = JSON.parse(fs.readFileSync(operationsPath, 'utf8'));
362
+ const operation = operations.operations[operationName];
363
+
364
+ if (!operation) {
365
+ throw new Error(`Operation not found: ${operationName}`);
366
+ }
367
+
368
+ return {
369
+ path: operation.endpoint,
370
+ method: operation.method
371
+ };
372
+ }
373
+
374
+ /**
375
+ * Load cookbook from file
376
+ */
377
+ loadCookbook(cookbookPath) {
378
+ if (!fs.existsSync(cookbookPath)) {
379
+ throw new Error(`Cookbook not found: ${cookbookPath}`);
380
+ }
381
+
382
+ const content = fs.readFileSync(cookbookPath, 'utf8');
383
+ return JSON.parse(content);
384
+ }
385
+
386
+ /**
387
+ * Validate cookbook format
388
+ */
389
+ validateCookbook(cookbook) {
390
+ if (!cookbook.steps || !Array.isArray(cookbook.steps)) {
391
+ throw new Error('Cookbook must have steps array');
392
+ }
393
+
394
+ if (cookbook.steps.length === 0) {
395
+ throw new Error('Cookbook must have at least one step');
396
+ }
397
+
398
+ for (const step of cookbook.steps) {
399
+ if (!step.service) throw new Error('Step must have service');
400
+ if (!step.operation) throw new Error('Step must have operation');
401
+ }
402
+
403
+ return true;
404
+ }
405
+
406
+ /**
407
+ * Check if cookbook has expect clauses
408
+ */
409
+ hasExpectClauses(cookbook) {
410
+ return cookbook.steps.some(step => step.expect !== undefined);
411
+ }
412
+
413
+ /**
414
+ * Get aggregate results
415
+ */
416
+ getResults() {
417
+ return this.results;
418
+ }
419
+
420
+ /**
421
+ * Reset results
422
+ */
423
+ resetResults() {
424
+ this.results = {
425
+ total: 0,
426
+ passed: 0,
427
+ failed: 0,
428
+ duration: 0,
429
+ steps: []
430
+ };
431
+ }
432
+ }
433
+
434
+ module.exports = CookbookTestRunner;