@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,506 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { ValidationProofCodec, FingerprintUtils } = require('@onlineapps/service-validator-core');
6
+ const { ServiceStructureValidator } = require('./validators/ServiceStructureValidator');
7
+ const ServiceReadinessValidator = require('./ServiceReadinessValidator');
8
+ const CookbookTestRunner = require('./CookbookTestRunner');
9
+
10
+ /**
11
+ * ValidationOrchestrator
12
+ *
13
+ * Orchestrates complete service validation (Tier 1 Pre-Validation)
14
+ * Runs 6-step validation process and generates validation proof.
15
+ *
16
+ * Called automatically by ServiceWrapper during initialization.
17
+ */
18
+ class ValidationOrchestrator {
19
+ constructor(options = {}) {
20
+ this.serviceRoot = options.serviceRoot;
21
+ this.serviceName = options.serviceName;
22
+ this.serviceVersion = options.serviceVersion;
23
+ this.logger = options.logger || console;
24
+
25
+ // Paths
26
+ this.configPath = path.join(this.serviceRoot, 'conn-config');
27
+ this.runtimePath = path.join(this.serviceRoot, 'conn-runtime');
28
+ this.proofPath = path.join(this.runtimePath, 'validation-proof.json');
29
+
30
+ // Validators
31
+ this.structureValidator = new ServiceStructureValidator(this.serviceRoot);
32
+ this.readinessValidator = new ServiceReadinessValidator();
33
+ this.cookbookRunner = new CookbookTestRunner({
34
+ servicePath: this.serviceRoot,
35
+ serviceName: this.serviceName
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Main validation entry point
41
+ * Checks if proof exists and is valid, or runs full validation
42
+ */
43
+ async validate() {
44
+ this.logger.info('[ValidationOrchestrator] Starting validation...');
45
+
46
+ // Check if proof exists and is valid
47
+ const existingProof = await this.loadExistingProof();
48
+ if (existingProof && await this.isProofValid(existingProof)) {
49
+ this.logger.info('[ValidationOrchestrator] ✓ Valid proof found, skipping validation');
50
+ return {
51
+ success: true,
52
+ proofExists: true,
53
+ proof: existingProof,
54
+ skipped: true
55
+ };
56
+ }
57
+
58
+ // Run full validation
59
+ this.logger.info('[ValidationOrchestrator] No valid proof, running validation...');
60
+ return await this.runFullValidation();
61
+ }
62
+
63
+ /**
64
+ * Load existing validation proof from conn-runtime/
65
+ */
66
+ async loadExistingProof() {
67
+ try {
68
+ if (!fs.existsSync(this.proofPath)) {
69
+ return null;
70
+ }
71
+
72
+ const content = fs.readFileSync(this.proofPath, 'utf8');
73
+ const proof = JSON.parse(content);
74
+
75
+ return proof;
76
+ } catch (error) {
77
+ this.logger.warn(`[ValidationOrchestrator] Failed to load proof: ${error.message}`);
78
+ return null;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Check if proof is still valid
84
+ * Valid = correct fingerprint + not expired
85
+ */
86
+ async isProofValid(proof) {
87
+ try {
88
+ // Calculate current fingerprint
89
+ const currentFingerprint = await this.calculateFingerprint();
90
+
91
+ // Check if fingerprint matches
92
+ if (proof.fingerprint !== currentFingerprint) {
93
+ this.logger.info('[ValidationOrchestrator] Fingerprint mismatch (service changed)');
94
+ return false;
95
+ }
96
+
97
+ // Check expiration (proof valid for 30 days)
98
+ const proofDate = new Date(proof.validatedAt);
99
+ const now = new Date();
100
+ const daysSinceValidation = (now - proofDate) / (1000 * 60 * 60 * 24);
101
+
102
+ if (daysSinceValidation > 30) {
103
+ this.logger.info('[ValidationOrchestrator] Proof expired (>30 days old)');
104
+ return false;
105
+ }
106
+
107
+ // Verify proof signature
108
+ const isValid = ValidationProofCodec.verify(proof);
109
+ if (!isValid) {
110
+ this.logger.warn('[ValidationOrchestrator] Proof signature invalid');
111
+ return false;
112
+ }
113
+
114
+ return true;
115
+ } catch (error) {
116
+ this.logger.error(`[ValidationOrchestrator] Proof validation error: ${error.message}`);
117
+ return false;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Calculate service fingerprint
123
+ * Based on: version + operations.json + dependencies + config
124
+ */
125
+ async calculateFingerprint() {
126
+ try {
127
+ const configFile = path.join(this.configPath, 'config.json');
128
+ const operationsFile = path.join(this.configPath, 'operations.json');
129
+ const packageFile = path.join(this.serviceRoot, 'package.json');
130
+
131
+ const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
132
+ const operations = JSON.parse(fs.readFileSync(operationsFile, 'utf8'));
133
+ const pkg = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
134
+
135
+ // Extract @onlineapps/* dependencies
136
+ const deps = {};
137
+ if (pkg.dependencies) {
138
+ Object.keys(pkg.dependencies).forEach(name => {
139
+ if (name.startsWith('@onlineapps/')) {
140
+ deps[name] = pkg.dependencies[name];
141
+ }
142
+ });
143
+ }
144
+
145
+ return FingerprintUtils.calculate({
146
+ serviceVersion: config.service?.version || pkg.version,
147
+ operations: operations,
148
+ dependencies: deps,
149
+ configHash: FingerprintUtils.hashObject(config)
150
+ });
151
+ } catch (error) {
152
+ throw new Error(`Failed to calculate fingerprint: ${error.message}`);
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Run full 6-step validation process
158
+ */
159
+ async runFullValidation() {
160
+ const startTime = Date.now();
161
+ const results = {
162
+ success: true,
163
+ steps: {},
164
+ errors: [],
165
+ warnings: [],
166
+ totalTests: 0,
167
+ passedTests: 0,
168
+ failedTests: 0
169
+ };
170
+
171
+ try {
172
+ // Step 1: Service Structure
173
+ this.logger.info('[ValidationOrchestrator] Step 1/6: Service Structure');
174
+ results.steps.structure = await this.validateStructure();
175
+ if (!results.steps.structure.valid) {
176
+ results.success = false;
177
+ results.errors.push(...results.steps.structure.errors);
178
+ // Fail fast - can't continue without proper structure
179
+ return this.finalizeResults(results, startTime);
180
+ }
181
+
182
+ // Step 2: Config Files
183
+ this.logger.info('[ValidationOrchestrator] Step 2/6: Config Files');
184
+ results.steps.config = await this.validateConfig();
185
+ if (!results.steps.config.valid) {
186
+ results.success = false;
187
+ results.errors.push(...results.steps.config.errors);
188
+ }
189
+
190
+ // Step 3: Operations Compliance
191
+ this.logger.info('[ValidationOrchestrator] Step 3/6: Operations Compliance');
192
+ results.steps.operations = await this.validateOperations();
193
+ if (!results.steps.operations.valid) {
194
+ results.success = false;
195
+ results.errors.push(...results.steps.operations.errors);
196
+ }
197
+
198
+ // Step 4: Cookbook Tests
199
+ this.logger.info('[ValidationOrchestrator] Step 4/6: Cookbook Tests');
200
+ results.steps.cookbooks = await this.runCookbookTests();
201
+ results.totalTests += results.steps.cookbooks.total || 0;
202
+ results.passedTests += results.steps.cookbooks.passed || 0;
203
+ results.failedTests += results.steps.cookbooks.failed || 0;
204
+ if (!results.steps.cookbooks.success) {
205
+ results.success = false;
206
+ results.errors.push(...(results.steps.cookbooks.errors || []));
207
+ }
208
+
209
+ // Step 5: Service Readiness
210
+ this.logger.info('[ValidationOrchestrator] Step 5/6: Service Readiness');
211
+ results.steps.readiness = await this.validateReadiness();
212
+ if (!results.steps.readiness.valid) {
213
+ results.success = false;
214
+ results.errors.push(...results.steps.readiness.errors);
215
+ }
216
+
217
+ // Step 6: Connector Integration
218
+ this.logger.info('[ValidationOrchestrator] Step 6/6: Connector Integration');
219
+ results.steps.connectors = this.validateConnectors();
220
+ if (!results.steps.connectors.valid) {
221
+ // Non-critical - just warnings
222
+ results.warnings.push(...results.steps.connectors.warnings);
223
+ }
224
+
225
+ // Finalize and generate proof if successful
226
+ return await this.finalizeResults(results, startTime);
227
+
228
+ } catch (error) {
229
+ this.logger.error(`[ValidationOrchestrator] Validation failed: ${error.message}`);
230
+ results.success = false;
231
+ results.errors.push(`Validation error: ${error.message}`);
232
+ return this.finalizeResults(results, startTime);
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Step 1: Validate service structure
238
+ */
239
+ async validateStructure() {
240
+ try {
241
+ const result = this.structureValidator.validate();
242
+
243
+ this.logger.info(`[ValidationOrchestrator] ✓ Service structure: ${result.valid ? 'PASS' : 'FAIL'}`);
244
+ return result;
245
+ } catch (error) {
246
+ return {
247
+ valid: false,
248
+ errors: [`Structure validation failed: ${error.message}`]
249
+ };
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Step 2: Validate config files
255
+ */
256
+ async validateConfig() {
257
+ const errors = [];
258
+
259
+ try {
260
+ // Validate config.json
261
+ const configFile = path.join(this.configPath, 'config.json');
262
+ if (!fs.existsSync(configFile)) {
263
+ errors.push('config.json not found');
264
+ } else {
265
+ const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
266
+ if (!config.service?.name) errors.push('config.json missing service.name');
267
+ if (!config.service?.version) errors.push('config.json missing service.version');
268
+ }
269
+
270
+ // Validate operations.json
271
+ const operationsFile = path.join(this.configPath, 'operations.json');
272
+ if (!fs.existsSync(operationsFile)) {
273
+ errors.push('operations.json not found');
274
+ } else {
275
+ const operations = JSON.parse(fs.readFileSync(operationsFile, 'utf8'));
276
+ if (!operations.operations || Object.keys(operations.operations).length === 0) {
277
+ errors.push('operations.json has no operations defined');
278
+ }
279
+ }
280
+
281
+ this.logger.info(`[ValidationOrchestrator] ✓ Config files: ${errors.length === 0 ? 'PASS' : 'FAIL'}`);
282
+ return {
283
+ valid: errors.length === 0,
284
+ errors: errors
285
+ };
286
+ } catch (error) {
287
+ return {
288
+ valid: false,
289
+ errors: [`Config validation failed: ${error.message}`]
290
+ };
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Step 3: Validate operations compliance
296
+ */
297
+ async validateOperations() {
298
+ try {
299
+ const operationsFile = path.join(this.configPath, 'operations.json');
300
+ const operations = JSON.parse(fs.readFileSync(operationsFile, 'utf8'));
301
+ const errors = [];
302
+
303
+ // Validate each operation
304
+ for (const [opName, opDef] of Object.entries(operations.operations || {})) {
305
+ if (!opDef.endpoint) {
306
+ errors.push(`Operation ${opName}: missing endpoint`);
307
+ } else if (!opDef.endpoint.startsWith('/')) {
308
+ errors.push(`Operation ${opName}: endpoint must start with /`);
309
+ }
310
+
311
+ if (!opDef.method) {
312
+ errors.push(`Operation ${opName}: missing method`);
313
+ } else if (!['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(opDef.method)) {
314
+ errors.push(`Operation ${opName}: invalid HTTP method`);
315
+ }
316
+
317
+ if (!opDef.input) {
318
+ errors.push(`Operation ${opName}: missing input schema`);
319
+ }
320
+ if (!opDef.output) {
321
+ errors.push(`Operation ${opName}: missing output schema`);
322
+ }
323
+ }
324
+
325
+ this.logger.info(`[ValidationOrchestrator] ✓ Operations compliance: ${errors.length === 0 ? 'PASS' : 'FAIL'}`);
326
+ return {
327
+ valid: errors.length === 0,
328
+ errors: errors
329
+ };
330
+ } catch (error) {
331
+ return {
332
+ valid: false,
333
+ errors: [`Operations validation failed: ${error.message}`]
334
+ };
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Step 4: Run cookbook tests
340
+ */
341
+ async runCookbookTests() {
342
+ try {
343
+ const cookbooksPath = path.join(this.serviceRoot, 'tests', 'cookbooks');
344
+
345
+ if (!fs.existsSync(cookbooksPath)) {
346
+ this.logger.warn('[ValidationOrchestrator] No cookbook tests found (tests/cookbooks/ missing)');
347
+ return {
348
+ success: true,
349
+ total: 0,
350
+ passed: 0,
351
+ failed: 0,
352
+ warnings: ['No cookbook tests found']
353
+ };
354
+ }
355
+
356
+ const result = await this.cookbookRunner.runCookbooks(cookbooksPath);
357
+
358
+ this.logger.info(`[ValidationOrchestrator] ✓ Cookbook tests: ${result.passed}/${result.total} passed`);
359
+ return {
360
+ success: result.failed === 0,
361
+ total: result.total,
362
+ passed: result.passed,
363
+ failed: result.failed,
364
+ errors: result.failed > 0 ? [`${result.failed} cookbook test(s) failed`] : []
365
+ };
366
+ } catch (error) {
367
+ return {
368
+ success: false,
369
+ total: 0,
370
+ passed: 0,
371
+ failed: 0,
372
+ errors: [`Cookbook tests failed: ${error.message}`]
373
+ };
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Step 5: Validate service readiness
379
+ */
380
+ async validateReadiness() {
381
+ try {
382
+ // Load operations.json
383
+ const operationsFile = path.join(this.configPath, 'operations.json');
384
+ const operations = JSON.parse(fs.readFileSync(operationsFile, 'utf8'));
385
+
386
+ const result = await this.readinessValidator.validateReadiness({
387
+ serviceRoot: this.serviceRoot,
388
+ serviceName: this.serviceName,
389
+ operations: operations.operations
390
+ });
391
+
392
+ this.logger.info(`[ValidationOrchestrator] ✓ Service readiness: ${result.ready ? 'PASS' : 'FAIL'}`);
393
+ return {
394
+ valid: result.ready,
395
+ errors: result.ready ? [] : result.errors || ['Service not ready']
396
+ };
397
+ } catch (error) {
398
+ return {
399
+ valid: false,
400
+ errors: [`Readiness validation failed: ${error.message}`]
401
+ };
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Step 6: Validate connector integration
407
+ */
408
+ validateConnectors() {
409
+ try {
410
+ // This is validated implicitly through cookbook tests
411
+ // which test ServiceWrapper + all connectors
412
+
413
+ this.logger.info('[ValidationOrchestrator] ✓ Connector integration: PASS (via cookbook tests)');
414
+ return {
415
+ valid: true,
416
+ warnings: []
417
+ };
418
+ } catch (error) {
419
+ return {
420
+ valid: false,
421
+ warnings: [`Connector validation warning: ${error.message}`]
422
+ };
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Finalize validation results and generate proof if successful
428
+ */
429
+ async finalizeResults(results, startTime) {
430
+ const duration = Date.now() - startTime;
431
+ results.durationMs = duration;
432
+
433
+ if (results.success) {
434
+ try {
435
+ // Generate validation proof
436
+ const fingerprint = await this.calculateFingerprint();
437
+ const proof = {
438
+ serviceName: this.serviceName,
439
+ serviceVersion: this.serviceVersion,
440
+ fingerprint: fingerprint,
441
+ validator: '@onlineapps/conn-orch-validator',
442
+ validatorVersion: require('../package.json').version,
443
+ validatedAt: new Date().toISOString(),
444
+ testsRun: results.totalTests,
445
+ testsPassed: results.passedTests,
446
+ testsFailed: results.failedTests,
447
+ durationMs: duration,
448
+ steps: {
449
+ structure: results.steps.structure?.valid || false,
450
+ config: results.steps.config?.valid || false,
451
+ operations: results.steps.operations?.valid || false,
452
+ cookbooks: results.steps.cookbooks?.success || false,
453
+ readiness: results.steps.readiness?.valid || false,
454
+ connectors: results.steps.connectors?.valid || false
455
+ }
456
+ };
457
+
458
+ // Encode proof using ValidationProofCodec
459
+ const encodedProof = ValidationProofCodec.encode(proof);
460
+
461
+ // Save proof to conn-runtime/
462
+ await this.saveProof(encodedProof);
463
+
464
+ results.proof = encodedProof;
465
+ results.fingerprint = fingerprint;
466
+
467
+ this.logger.info(`[ValidationOrchestrator] ✅ Validation PASSED (${duration}ms)`);
468
+ this.logger.info(`[ValidationOrchestrator] Proof saved to: ${this.proofPath}`);
469
+ } catch (error) {
470
+ this.logger.error(`[ValidationOrchestrator] Failed to generate proof: ${error.message}`);
471
+ results.success = false;
472
+ results.errors.push(`Proof generation failed: ${error.message}`);
473
+ }
474
+ } else {
475
+ this.logger.error(`[ValidationOrchestrator] ❌ Validation FAILED (${duration}ms)`);
476
+ this.logger.error(`[ValidationOrchestrator] Errors: ${results.errors.join(', ')}`);
477
+ }
478
+
479
+ return results;
480
+ }
481
+
482
+ /**
483
+ * Save validation proof to conn-runtime/validation-proof.json
484
+ */
485
+ async saveProof(proof) {
486
+ try {
487
+ // Ensure conn-runtime/ directory exists
488
+ if (!fs.existsSync(this.runtimePath)) {
489
+ fs.mkdirSync(this.runtimePath, { recursive: true });
490
+
491
+ // Create .gitkeep
492
+ const gitkeepPath = path.join(this.runtimePath, '.gitkeep');
493
+ fs.writeFileSync(gitkeepPath, '# Runtime data generated by connectors\n');
494
+ }
495
+
496
+ // Save proof
497
+ fs.writeFileSync(this.proofPath, JSON.stringify(proof, null, 2));
498
+
499
+ this.logger.info(`[ValidationOrchestrator] Proof saved: ${this.proofPath}`);
500
+ } catch (error) {
501
+ throw new Error(`Failed to save proof: ${error.message}`);
502
+ }
503
+ }
504
+ }
505
+
506
+ module.exports = ValidationOrchestrator;