@onlineapps/conn-orch-validator 3.1.2 → 3.2.1
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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/conn-orch-validator",
|
|
3
|
-
"version": "3.1
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"description": "Validation orchestrator for OA Drive microservices - coordinates validation across all layers (base, infra, orch, business)",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
"ajv": "^8.12.0",
|
|
30
30
|
"ajv-formats": "^2.1.1",
|
|
31
31
|
"amqplib": "^0.10.9",
|
|
32
|
-
"axios": "^1.4.0",
|
|
33
32
|
"joi": "^17.9.0"
|
|
34
33
|
},
|
|
35
34
|
"devDependencies": {
|
|
@@ -3,47 +3,39 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
|
-
const axios = require('axios');
|
|
7
6
|
const MockMQClient = require('./mocks/MockMQClient');
|
|
8
7
|
const MockRegistry = require('./mocks/MockRegistry');
|
|
9
8
|
const { resolveHeaders } = require('./utils/resolveHeaders');
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
|
-
* CookbookTestRunner
|
|
11
|
+
* CookbookTestRunner — executes cookbook tests offline with mocked infrastructure.
|
|
13
12
|
*
|
|
14
|
-
* Dispatch
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* → dispatch via axios HTTP request against serviceUrl. Retained for
|
|
19
|
-
* backward-compat with any pre-v3 services that still expose /api/v1/*.
|
|
20
|
-
* - operation with neither `handler` nor `endpoint` → explicit error.
|
|
13
|
+
* Dispatch is exclusively v3 handler-registry: each operation MUST declare
|
|
14
|
+
* `handler: "path#exportName"` in operations.json. The runner loads the
|
|
15
|
+
* module via `require()` and calls `handler(input, ctx)` directly in-process
|
|
16
|
+
* (no HTTP). Operations without a `handler` field are an explicit error.
|
|
21
17
|
*
|
|
22
|
-
* The
|
|
18
|
+
* The handler dispatch builds a minimal real ctx:
|
|
23
19
|
* { logger, tenant_id, workspace_id, correlation_id, db: null, cache: null,
|
|
24
|
-
* httpClient: null, stream: null, abortSignal: null }
|
|
25
|
-
*
|
|
20
|
+
* httpClient: null, secrets: null, stream: null, abortSignal: null }
|
|
21
|
+
* matching the shape ServiceWrapper.ContextBuilder produces in production
|
|
26
22
|
* (see api/docs/architecture/biz-service-invocation-model.md §5). Handlers
|
|
27
|
-
* that
|
|
23
|
+
* that need DB access import sequelize directly from their own
|
|
28
24
|
* src/config/database.js module (see standards/biz-service-onboarding.md §9a);
|
|
29
|
-
* connector
|
|
25
|
+
* connector slots stay null and the handler surfaces its own failure if
|
|
30
26
|
* something it genuinely needs is missing.
|
|
31
27
|
*
|
|
32
|
-
* Supports unified cookbook format for both testing (with expect) and production (without expect)
|
|
28
|
+
* Supports unified cookbook format for both testing (with expect) and production (without expect).
|
|
33
29
|
*/
|
|
34
30
|
class CookbookTestRunner {
|
|
35
31
|
constructor(options = {}) {
|
|
36
32
|
if (!options.serviceName) {
|
|
37
33
|
throw new Error('[CookbookTestRunner] serviceName is required');
|
|
38
34
|
}
|
|
39
|
-
if (!options.serviceUrl) {
|
|
40
|
-
throw new Error('[CookbookTestRunner] serviceUrl is required');
|
|
41
|
-
}
|
|
42
35
|
if (!options.logger || typeof options.logger.warn !== 'function') {
|
|
43
36
|
throw new Error('[CookbookTestRunner] Logger is required — Expected object with warn() method');
|
|
44
37
|
}
|
|
45
38
|
this.serviceName = options.serviceName;
|
|
46
|
-
this.serviceUrl = options.serviceUrl;
|
|
47
39
|
this.servicePath = options.servicePath;
|
|
48
40
|
this.mockInfrastructure = options.mockInfrastructure !== false;
|
|
49
41
|
this.timeout = options.timeout;
|
|
@@ -189,38 +181,31 @@ class CookbookTestRunner {
|
|
|
189
181
|
};
|
|
190
182
|
|
|
191
183
|
try {
|
|
192
|
-
// Resolve operation spec from operations.json — returns
|
|
193
|
-
// {
|
|
194
|
-
// { dispatchMode: 'http', path, method, headers } (legacy).
|
|
184
|
+
// Resolve operation spec from operations.json — always returns
|
|
185
|
+
// { modulePath, exportName } (v3 handler dispatch is the only mode).
|
|
195
186
|
const spec = await this.resolveOperation(step.service, step.operation);
|
|
196
187
|
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
if (!
|
|
188
|
+
const rawTenantId = process.env.OA_VALIDATION_TENANT_ID;
|
|
189
|
+
const rawWorkspaceId = process.env.OA_VALIDATION_WORKSPACE_ID;
|
|
190
|
+
if (!rawTenantId || !rawWorkspaceId) {
|
|
200
191
|
throw new Error('[CookbookTestRunner] Missing required environment variables OA_VALIDATION_TENANT_ID and/or OA_VALIDATION_WORKSPACE_ID');
|
|
201
192
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
validationTenantId,
|
|
219
|
-
validationWorkspaceId,
|
|
220
|
-
result,
|
|
221
|
-
startTime
|
|
222
|
-
});
|
|
223
|
-
}
|
|
193
|
+
// Coerce numeric env vars to integers so ctx matches production
|
|
194
|
+
// ContextBuilder (which receives already-typed values from the MQ envelope).
|
|
195
|
+
// Handlers that check `Number.isInteger(ctx.tenant_id)` would otherwise
|
|
196
|
+
// reject the string form and every workspace-scoped cookbook would fail.
|
|
197
|
+
const validationTenantId = /^\d+$/.test(rawTenantId) ? parseInt(rawTenantId, 10) : rawTenantId;
|
|
198
|
+
const validationWorkspaceId = /^\d+$/.test(rawWorkspaceId) ? parseInt(rawWorkspaceId, 10) : rawWorkspaceId;
|
|
199
|
+
|
|
200
|
+
await this._dispatchViaHandler({
|
|
201
|
+
step,
|
|
202
|
+
spec,
|
|
203
|
+
testConfig,
|
|
204
|
+
validationTenantId,
|
|
205
|
+
validationWorkspaceId,
|
|
206
|
+
result,
|
|
207
|
+
startTime
|
|
208
|
+
});
|
|
224
209
|
|
|
225
210
|
this.logger.info(`Step ${step.id}: ${result.passed ? 'PASSED' : 'FAILED'} (${result.duration}ms)`);
|
|
226
211
|
|
|
@@ -236,7 +221,7 @@ class CookbookTestRunner {
|
|
|
236
221
|
}
|
|
237
222
|
|
|
238
223
|
/**
|
|
239
|
-
*
|
|
224
|
+
* Handler dispatch: require handler module, build minimal real ctx, call
|
|
240
225
|
* handler(input, ctx). Mirrors the production ContextBuilder shape
|
|
241
226
|
* (see api/docs/architecture/biz-service-invocation-model.md §5) with
|
|
242
227
|
* all connector slots set to null — handlers that need DB access use
|
|
@@ -348,47 +333,6 @@ class CookbookTestRunner {
|
|
|
348
333
|
}
|
|
349
334
|
}
|
|
350
335
|
|
|
351
|
-
/**
|
|
352
|
-
* Legacy dispatch: HTTP request against serviceUrl + endpoint. Retained
|
|
353
|
-
* verbatim (modulo wrapping) for compatibility with pre-v3 services that
|
|
354
|
-
* still expose /api/v1/* business routes.
|
|
355
|
-
*/
|
|
356
|
-
async _dispatchViaHttp({ step, spec, testConfig, validationTenantId, validationWorkspaceId, result, startTime }) {
|
|
357
|
-
const request = {
|
|
358
|
-
method: spec.method,
|
|
359
|
-
url: `${this.serviceUrl}${spec.path}`,
|
|
360
|
-
data: step.input,
|
|
361
|
-
timeout: testConfig.timeout || this.timeout,
|
|
362
|
-
headers: {
|
|
363
|
-
'x-validation-request': 'true',
|
|
364
|
-
'x-tenant-id': validationTenantId,
|
|
365
|
-
'x-workspace-id': validationWorkspaceId,
|
|
366
|
-
...resolveHeaders(spec.headers),
|
|
367
|
-
...resolveHeaders(step.headers)
|
|
368
|
-
}
|
|
369
|
-
};
|
|
370
|
-
result.request = request;
|
|
371
|
-
|
|
372
|
-
const response = await axios(request);
|
|
373
|
-
|
|
374
|
-
result.response = {
|
|
375
|
-
status: response.status,
|
|
376
|
-
statusText: response.statusText,
|
|
377
|
-
data: response.data
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
result.actual = response.data;
|
|
381
|
-
result.duration = Date.now() - startTime;
|
|
382
|
-
|
|
383
|
-
if (step.expect) {
|
|
384
|
-
const validation = this.validateExpectations(step.expect, result);
|
|
385
|
-
result.passed = validation.passed;
|
|
386
|
-
result.validationErrors = validation.errors;
|
|
387
|
-
} else {
|
|
388
|
-
result.passed = response.status >= 200 && response.status < 300;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
336
|
/**
|
|
393
337
|
* Validate expectations against actual results
|
|
394
338
|
*/
|
|
@@ -537,16 +481,9 @@ class CookbookTestRunner {
|
|
|
537
481
|
}
|
|
538
482
|
|
|
539
483
|
/**
|
|
540
|
-
* Resolve operation spec from operations.json. Returns
|
|
541
|
-
* descriptor
|
|
542
|
-
*
|
|
543
|
-
* v3 (handler-based, post-2026-04):
|
|
544
|
-
* { dispatchMode: 'handler', modulePath, exportName }
|
|
545
|
-
*
|
|
546
|
-
* legacy (HTTP endpoint, pre-v3):
|
|
547
|
-
* { dispatchMode: 'http', path, method, headers }
|
|
548
|
-
*
|
|
549
|
-
* Throws if the operation declares neither.
|
|
484
|
+
* Resolve operation spec from operations.json. Returns the handler
|
|
485
|
+
* dispatch descriptor: { modulePath, exportName, headers }.
|
|
486
|
+
* Throws if the operation does not declare a v3 `handler`.
|
|
550
487
|
*/
|
|
551
488
|
async resolveOperation(serviceName, operationName) {
|
|
552
489
|
const serviceRoot = this._resolveServiceRoot(serviceName);
|
|
@@ -565,30 +502,20 @@ class CookbookTestRunner {
|
|
|
565
502
|
throw new Error(`Operation not found: ${operationName}`);
|
|
566
503
|
}
|
|
567
504
|
|
|
568
|
-
if (operation.handler) {
|
|
569
|
-
|
|
570
|
-
if (!modRel || !exportName) {
|
|
571
|
-
throw new Error(`[CookbookTestRunner] Invalid handler spec "${operation.handler}" for ${serviceName}.${operationName}; expected "path#exportName"`);
|
|
572
|
-
}
|
|
573
|
-
const modulePath = require.resolve(path.join(serviceRoot, 'src', modRel));
|
|
574
|
-
return {
|
|
575
|
-
dispatchMode: 'handler',
|
|
576
|
-
modulePath,
|
|
577
|
-
exportName,
|
|
578
|
-
headers: operation.headers || {}
|
|
579
|
-
};
|
|
505
|
+
if (!operation.handler) {
|
|
506
|
+
throw new Error(`[CookbookTestRunner] Operation ${serviceName}.${operationName} in ${operationsPath} is missing required v3 field "handler" ("path#exportName"). Fix: declare handler in operations.json per RFC §5.3.`);
|
|
580
507
|
}
|
|
581
508
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
path: operation.endpoint,
|
|
586
|
-
method: operation.method,
|
|
587
|
-
headers: operation.headers || {}
|
|
588
|
-
};
|
|
509
|
+
const [modRel, exportName] = String(operation.handler).split('#');
|
|
510
|
+
if (!modRel || !exportName) {
|
|
511
|
+
throw new Error(`[CookbookTestRunner] Invalid handler spec "${operation.handler}" for ${serviceName}.${operationName}; expected "path#exportName"`);
|
|
589
512
|
}
|
|
590
|
-
|
|
591
|
-
|
|
513
|
+
const modulePath = require.resolve(path.join(serviceRoot, 'src', modRel));
|
|
514
|
+
return {
|
|
515
|
+
modulePath,
|
|
516
|
+
exportName,
|
|
517
|
+
headers: operation.headers || {}
|
|
518
|
+
};
|
|
592
519
|
}
|
|
593
520
|
|
|
594
521
|
/**
|
|
@@ -197,17 +197,26 @@ class ServiceReadinessValidator {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
/**
|
|
200
|
-
* Check health endpoint
|
|
200
|
+
* Check health endpoint via Node 18+ global fetch (no external HTTP client).
|
|
201
201
|
*/
|
|
202
202
|
async checkHealth(healthUrl) {
|
|
203
203
|
try {
|
|
204
|
-
const
|
|
205
|
-
|
|
204
|
+
const response = await fetch(healthUrl, {
|
|
205
|
+
method: 'GET',
|
|
206
|
+
signal: AbortSignal.timeout(5000)
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
let data = null;
|
|
210
|
+
try {
|
|
211
|
+
data = await response.json();
|
|
212
|
+
} catch (_) {
|
|
213
|
+
// health endpoint may return non-JSON; keep data null and rely on status.
|
|
214
|
+
}
|
|
206
215
|
|
|
207
216
|
return {
|
|
208
217
|
passed: response.status === 200,
|
|
209
218
|
status: response.status,
|
|
210
|
-
data
|
|
219
|
+
data
|
|
211
220
|
};
|
|
212
221
|
} catch (error) {
|
|
213
222
|
return {
|
|
@@ -45,7 +45,6 @@ class ValidationOrchestrator {
|
|
|
45
45
|
this.cookbookRunner = new CookbookTestRunner({
|
|
46
46
|
servicePath: this.serviceRoot,
|
|
47
47
|
serviceName: this.serviceName,
|
|
48
|
-
serviceUrl: this.serviceUrl,
|
|
49
48
|
logger: this.logger,
|
|
50
49
|
timeout: 30000
|
|
51
50
|
});
|
|
@@ -384,12 +383,10 @@ class ValidationOrchestrator {
|
|
|
384
383
|
}
|
|
385
384
|
|
|
386
385
|
/**
|
|
387
|
-
* Step 4: Run cookbook tests
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
* are skipped here with a warning. Handler-registry-based cookbook dispatch
|
|
392
|
-
* is tracked separately (see RFC §5.3, §5.9).
|
|
386
|
+
* Step 4: Run cookbook tests via CookbookTestRunner (v3 handler dispatch).
|
|
387
|
+
* Operations must declare a `handler` in operations.json; the runner
|
|
388
|
+
* loads the module via `require()` and calls the exported function
|
|
389
|
+
* in-process. Failures fail this validation step.
|
|
393
390
|
*/
|
|
394
391
|
async runCookbookTests() {
|
|
395
392
|
try {
|
|
@@ -406,36 +403,6 @@ class ValidationOrchestrator {
|
|
|
406
403
|
};
|
|
407
404
|
}
|
|
408
405
|
|
|
409
|
-
const operationsFile = path.join(this.configPath, 'operations.json');
|
|
410
|
-
let isV3 = false;
|
|
411
|
-
try {
|
|
412
|
-
const ops = JSON.parse(fs.readFileSync(operationsFile, 'utf8'));
|
|
413
|
-
const opValues = Object.values(ops.operations || {});
|
|
414
|
-
isV3 = ops.schema_version === '3.0' || opValues.some(o => o && typeof o === 'object' && 'handler' in o);
|
|
415
|
-
} catch (_) { /* fall through to runner */ }
|
|
416
|
-
|
|
417
|
-
if (isV3) {
|
|
418
|
-
// v3 per-op HTTP dispatch is retired (RFC §5.3). Cookbook HTTP execution
|
|
419
|
-
// is skipped until CookbookTestRunner gains handler-registry dispatch.
|
|
420
|
-
// We still report each operation as a structural-validation "test" so
|
|
421
|
-
// the proof codec's `testsRun > 0` invariant is satisfied — structural
|
|
422
|
-
// validation of all operations has already passed at Steps 1 and 3.
|
|
423
|
-
let opCount = 0;
|
|
424
|
-
try {
|
|
425
|
-
const ops = JSON.parse(fs.readFileSync(operationsFile, 'utf8'));
|
|
426
|
-
opCount = Object.keys(ops.operations || {}).length;
|
|
427
|
-
} catch (_) { opCount = 0; }
|
|
428
|
-
|
|
429
|
-
console.warn(`[ValidationOrchestrator] Step 4: cookbook HTTP dispatch is v2-only. Skipping for v3; counting ${opCount} operation structural check(s) as tests.`);
|
|
430
|
-
return {
|
|
431
|
-
success: true,
|
|
432
|
-
total: opCount,
|
|
433
|
-
passed: opCount,
|
|
434
|
-
failed: 0,
|
|
435
|
-
warnings: ['Cookbook HTTP execution skipped — v3 handler-registry dispatch not yet implemented in CookbookTestRunner']
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
|
|
439
406
|
const result = await this.cookbookRunner.runCookbooks(cookbooksPath);
|
|
440
407
|
|
|
441
408
|
console.log(`[ValidationOrchestrator] ✓ Cookbook tests: ${result.passed}/${result.total} passed`);
|