@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.
- package/README.md +78 -0
- package/TESTING_STRATEGY.md +92 -0
- package/docs/DESIGN.md +134 -0
- package/examples/service-wrapper-usage.js +250 -0
- package/examples/three-tier-testing.js +144 -0
- package/jest.config.js +23 -0
- package/onlineapps-conn-e2e-testing-1.0.0.tgz +0 -0
- package/package.json +43 -0
- package/src/CookbookTestRunner.js +434 -0
- package/src/CookbookTestUtils.js +237 -0
- package/src/ServiceReadinessValidator.js +430 -0
- package/src/ServiceTestHarness.js +256 -0
- package/src/ServiceValidator.js +387 -0
- package/src/TestOrchestrator.js +727 -0
- package/src/ValidationOrchestrator.js +506 -0
- package/src/WorkflowTestRunner.js +396 -0
- package/src/helpers/README.md +235 -0
- package/src/helpers/createPreValidationTests.js +321 -0
- package/src/helpers/createServiceReadinessTests.js +245 -0
- package/src/index.js +62 -0
- package/src/mocks/MockMQClient.js +176 -0
- package/src/mocks/MockRegistry.js +164 -0
- package/src/mocks/MockStorage.js +186 -0
- package/src/validators/ServiceStructureValidator.js +487 -0
- package/src/validators/ValidationProofGenerator.js +79 -0
- package/test-mq-flow.js +72 -0
- package/test-orchestrator.js +95 -0
- package/tests/component/testing-framework-integration.test.js +313 -0
- package/tests/integration/ServiceReadiness.test.js +265 -0
- package/tests/monitoring-e2e.test.js +315 -0
- package/tests/run-example.js +257 -0
- package/tests/unit/CookbookTestRunner.test.js +353 -0
- package/tests/unit/MockMQClient.test.js +190 -0
- package/tests/unit/MockRegistry.test.js +233 -0
- package/tests/unit/MockStorage.test.js +257 -0
- package/tests/unit/ServiceValidator.test.js +429 -0
- 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
|
+
};
|
|
Binary file
|
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;
|