@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
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CookbookTestUtils - Utilities for cookbook testing
|
|
5
|
+
*/
|
|
6
|
+
class CookbookTestUtils {
|
|
7
|
+
/**
|
|
8
|
+
* Create a mock workflow message
|
|
9
|
+
*/
|
|
10
|
+
static createWorkflowMessage(options = {}) {
|
|
11
|
+
return {
|
|
12
|
+
workflow_id: options.workflow_id || `test-workflow-${Date.now()}`,
|
|
13
|
+
cookbook: options.cookbook || {
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
steps: options.steps || [
|
|
16
|
+
{
|
|
17
|
+
id: 'step1',
|
|
18
|
+
type: 'task',
|
|
19
|
+
service: options.serviceName || 'test-service',
|
|
20
|
+
operation: 'testOperation',
|
|
21
|
+
input: options.input || {},
|
|
22
|
+
output: options.output || {}
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
current_step: options.current_step || 'step1',
|
|
27
|
+
context: options.context || {
|
|
28
|
+
api_input: options.api_input || {},
|
|
29
|
+
steps: options.previous_steps || {}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate test cookbook with multiple steps
|
|
36
|
+
*/
|
|
37
|
+
static generateTestCookbook(options = {}) {
|
|
38
|
+
const stepCount = options.steps || 3;
|
|
39
|
+
const services = options.services || ['service1', 'service2'];
|
|
40
|
+
const steps = [];
|
|
41
|
+
|
|
42
|
+
for (let i = 1; i <= stepCount; i++) {
|
|
43
|
+
steps.push({
|
|
44
|
+
id: `step${i}`,
|
|
45
|
+
type: 'task',
|
|
46
|
+
service: services[i % services.length],
|
|
47
|
+
operation: `operation${i}`,
|
|
48
|
+
input: { data: `input${i}` },
|
|
49
|
+
output: { result: `$.result${i}` }
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
version: '1.0.0',
|
|
55
|
+
api_input: options.api_input || { test: 'data' },
|
|
56
|
+
steps
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validate cookbook structure (basic validation)
|
|
62
|
+
*/
|
|
63
|
+
static validateCookbook(cookbook) {
|
|
64
|
+
const errors = [];
|
|
65
|
+
|
|
66
|
+
if (!cookbook) {
|
|
67
|
+
errors.push('Cookbook is required');
|
|
68
|
+
return { valid: false, errors };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!cookbook.version) {
|
|
72
|
+
errors.push('Version is required');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!cookbook.steps || !Array.isArray(cookbook.steps)) {
|
|
76
|
+
errors.push('Steps array is required');
|
|
77
|
+
} else {
|
|
78
|
+
cookbook.steps.forEach((step, index) => {
|
|
79
|
+
if (!step.id) {
|
|
80
|
+
errors.push(`Step ${index}: id is required`);
|
|
81
|
+
}
|
|
82
|
+
if (!step.type) {
|
|
83
|
+
errors.push(`Step ${index}: type is required`);
|
|
84
|
+
}
|
|
85
|
+
if (step.type === 'task' && !step.service) {
|
|
86
|
+
errors.push(`Step ${index}: service is required for task steps`);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
valid: errors.length === 0,
|
|
93
|
+
errors
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Compare two cookbooks and find differences
|
|
99
|
+
*/
|
|
100
|
+
static compareCookbooks(cookbook1, cookbook2) {
|
|
101
|
+
const differences = [];
|
|
102
|
+
|
|
103
|
+
// Compare basic properties
|
|
104
|
+
if (cookbook1.version !== cookbook2.version) {
|
|
105
|
+
differences.push({
|
|
106
|
+
field: 'version',
|
|
107
|
+
cookbook1: cookbook1.version,
|
|
108
|
+
cookbook2: cookbook2.version
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Compare steps
|
|
113
|
+
const steps1 = cookbook1.steps || [];
|
|
114
|
+
const steps2 = cookbook2.steps || [];
|
|
115
|
+
|
|
116
|
+
if (steps1.length !== steps2.length) {
|
|
117
|
+
differences.push({
|
|
118
|
+
field: 'steps.length',
|
|
119
|
+
cookbook1: steps1.length,
|
|
120
|
+
cookbook2: steps2.length
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Compare individual steps
|
|
125
|
+
const minLength = Math.min(steps1.length, steps2.length);
|
|
126
|
+
for (let i = 0; i < minLength; i++) {
|
|
127
|
+
const step1 = steps1[i];
|
|
128
|
+
const step2 = steps2[i];
|
|
129
|
+
|
|
130
|
+
if (step1.id !== step2.id) {
|
|
131
|
+
differences.push({
|
|
132
|
+
field: `steps[${i}].id`,
|
|
133
|
+
cookbook1: step1.id,
|
|
134
|
+
cookbook2: step2.id
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (step1.type !== step2.type) {
|
|
139
|
+
differences.push({
|
|
140
|
+
field: `steps[${i}].type`,
|
|
141
|
+
cookbook1: step1.type,
|
|
142
|
+
cookbook2: step2.type
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (step1.service !== step2.service) {
|
|
147
|
+
differences.push({
|
|
148
|
+
field: `steps[${i}].service`,
|
|
149
|
+
cookbook1: step1.service,
|
|
150
|
+
cookbook2: step2.service
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return differences;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Create test context with previous step results
|
|
160
|
+
*/
|
|
161
|
+
static createContext(previousSteps = {}) {
|
|
162
|
+
return {
|
|
163
|
+
api_input: {
|
|
164
|
+
test_data: 'test',
|
|
165
|
+
timestamp: Date.now()
|
|
166
|
+
},
|
|
167
|
+
steps: previousSteps,
|
|
168
|
+
metadata: {
|
|
169
|
+
workflow_id: `test-${Date.now()}`,
|
|
170
|
+
started_at: new Date().toISOString()
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Simulate step execution result
|
|
177
|
+
*/
|
|
178
|
+
static createStepResult(stepId, success = true, data = {}) {
|
|
179
|
+
return {
|
|
180
|
+
stepId,
|
|
181
|
+
success,
|
|
182
|
+
data: data || { processed: true },
|
|
183
|
+
timestamp: Date.now(),
|
|
184
|
+
duration: Math.floor(Math.random() * 1000)
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Generate control flow step (foreach, switch, etc.)
|
|
190
|
+
*/
|
|
191
|
+
static createControlFlowStep(type, options = {}) {
|
|
192
|
+
const baseStep = {
|
|
193
|
+
id: options.id || `${type}-step-${Date.now()}`,
|
|
194
|
+
type
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
switch (type) {
|
|
198
|
+
case 'foreach':
|
|
199
|
+
return {
|
|
200
|
+
...baseStep,
|
|
201
|
+
items: options.items || '$api_input.items',
|
|
202
|
+
body: options.body || {
|
|
203
|
+
id: 'foreach-body',
|
|
204
|
+
type: 'task',
|
|
205
|
+
service: 'test-service',
|
|
206
|
+
operation: 'processItem'
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
case 'switch':
|
|
211
|
+
return {
|
|
212
|
+
...baseStep,
|
|
213
|
+
condition: options.condition || '$api_input.type',
|
|
214
|
+
cases: options.cases || [
|
|
215
|
+
{ value: 'type1', step: { id: 'case1', type: 'task', service: 'service1' } },
|
|
216
|
+
{ value: 'type2', step: { id: 'case2', type: 'task', service: 'service2' } }
|
|
217
|
+
],
|
|
218
|
+
default: options.default || { id: 'default', type: 'task', service: 'default-service' }
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
case 'fork_join':
|
|
222
|
+
return {
|
|
223
|
+
...baseStep,
|
|
224
|
+
branches: options.branches || [
|
|
225
|
+
{ id: 'branch1', type: 'task', service: 'service1' },
|
|
226
|
+
{ id: 'branch2', type: 'task', service: 'service2' }
|
|
227
|
+
],
|
|
228
|
+
join: options.join || { strategy: 'merge' }
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
default:
|
|
232
|
+
return baseStep;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = CookbookTestUtils;
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ServiceValidator = require('./ServiceValidator');
|
|
4
|
+
const CookbookTestUtils = require('./CookbookTestUtils');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ServiceReadinessValidator - Orchestrates complete service validation
|
|
8
|
+
* Used by service-wrapper to verify service is ready for production
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT: Only supports operations.json format. OpenAPI is deprecated.
|
|
11
|
+
*/
|
|
12
|
+
class ServiceReadinessValidator {
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.validator = new ServiceValidator(options);
|
|
15
|
+
this.logger = options.logger || console;
|
|
16
|
+
|
|
17
|
+
// Readiness checks: core (80 points) + optional (20 points) = 100 points max
|
|
18
|
+
// Core checks ALWAYS run, optional checks run if testCookbook/registry provided
|
|
19
|
+
// See: /shared/connector/conn-orch-validator/README.md for usage pattern
|
|
20
|
+
this.checks = {
|
|
21
|
+
operations: { weight: 30, required: true }, // operations.json structure valid
|
|
22
|
+
endpoints: { weight: 30, required: true }, // all endpoints respond correctly
|
|
23
|
+
health: { weight: 20, required: true }, // health check works
|
|
24
|
+
cookbook: { weight: 15, required: false }, // OPTIONAL - cookbook valid (with mocks)
|
|
25
|
+
registry: { weight: 5, required: false } // OPTIONAL - registry compatible (MockRegistry)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Perform complete readiness check
|
|
31
|
+
* @param {Object} service - Service configuration
|
|
32
|
+
* @returns {Object} Validation results with score
|
|
33
|
+
*/
|
|
34
|
+
async validateReadiness(service) {
|
|
35
|
+
const {
|
|
36
|
+
url,
|
|
37
|
+
operations,
|
|
38
|
+
registry,
|
|
39
|
+
testCookbook,
|
|
40
|
+
healthEndpoint = '/health'
|
|
41
|
+
} = service;
|
|
42
|
+
|
|
43
|
+
const results = {
|
|
44
|
+
timestamp: Date.now(),
|
|
45
|
+
serviceName: service.name,
|
|
46
|
+
serviceUrl: url,
|
|
47
|
+
checks: {},
|
|
48
|
+
score: 0,
|
|
49
|
+
maxScore: 100,
|
|
50
|
+
ready: false,
|
|
51
|
+
errors: [],
|
|
52
|
+
warnings: []
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// 1. Validate operations.json structure
|
|
56
|
+
if (operations) {
|
|
57
|
+
results.checks.operations = await this.checkOperationsCompliance(operations);
|
|
58
|
+
if (results.checks.operations.passed) {
|
|
59
|
+
results.score += this.checks.operations.weight;
|
|
60
|
+
} else if (this.checks.operations.required) {
|
|
61
|
+
results.errors.push('Operations validation failed');
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
results.errors.push('No operations.json provided - required for all services');
|
|
65
|
+
results.checks.operations = { passed: false, error: 'Missing operations.json' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. Test all declared operations endpoints
|
|
69
|
+
if (operations && results.checks.operations?.passed) {
|
|
70
|
+
results.checks.endpoints = await this.checkOperationsEndpoints(url, operations);
|
|
71
|
+
if (results.checks.endpoints.passed) {
|
|
72
|
+
results.score += this.checks.endpoints.weight;
|
|
73
|
+
} else if (this.checks.endpoints.required) {
|
|
74
|
+
results.errors.push('Endpoint validation failed');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 3. Verify health check
|
|
79
|
+
results.checks.health = await this.checkHealth(url + healthEndpoint);
|
|
80
|
+
if (results.checks.health.passed) {
|
|
81
|
+
results.score += this.checks.health.weight;
|
|
82
|
+
} else if (this.checks.health.required) {
|
|
83
|
+
results.errors.push('Health check failed');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 4. Test cookbook execution (if provided)
|
|
87
|
+
if (testCookbook) {
|
|
88
|
+
results.checks.cookbook = await this.checkCookbookExecution(
|
|
89
|
+
testCookbook
|
|
90
|
+
);
|
|
91
|
+
if (results.checks.cookbook.passed) {
|
|
92
|
+
results.score += this.checks.cookbook.weight;
|
|
93
|
+
} else if (this.checks.cookbook.required) {
|
|
94
|
+
results.errors.push('Cookbook validation failed');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 5. Verify registry compatibility
|
|
99
|
+
if (registry) {
|
|
100
|
+
results.checks.registry = await this.checkRegistryCompatibility(
|
|
101
|
+
service,
|
|
102
|
+
registry
|
|
103
|
+
);
|
|
104
|
+
if (results.checks.registry.passed) {
|
|
105
|
+
results.score += this.checks.registry.weight;
|
|
106
|
+
} else if (this.checks.registry.required) {
|
|
107
|
+
results.errors.push('Registry compatibility check failed');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Calculate readiness
|
|
112
|
+
const requiredChecks = Object.entries(this.checks)
|
|
113
|
+
.filter(([_, config]) => config.required)
|
|
114
|
+
.map(([name, _]) => name);
|
|
115
|
+
|
|
116
|
+
const requiredPassed = requiredChecks.every(
|
|
117
|
+
check => !results.checks[check] || results.checks[check].passed
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
results.ready = requiredPassed && results.score >= 60;
|
|
121
|
+
results.recommendation = this.getRecommendation(results);
|
|
122
|
+
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check operations.json compliance
|
|
128
|
+
*/
|
|
129
|
+
async checkOperationsCompliance(operations) {
|
|
130
|
+
try {
|
|
131
|
+
const errors = [];
|
|
132
|
+
const warnings = [];
|
|
133
|
+
|
|
134
|
+
// Validate operations structure
|
|
135
|
+
if (!operations || typeof operations !== 'object') {
|
|
136
|
+
return {
|
|
137
|
+
passed: false,
|
|
138
|
+
error: 'Operations must be an object'
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const operationCount = Object.keys(operations).length;
|
|
143
|
+
if (operationCount === 0) {
|
|
144
|
+
return {
|
|
145
|
+
passed: false,
|
|
146
|
+
error: 'No operations defined'
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Validate each operation
|
|
151
|
+
for (const [name, operation] of Object.entries(operations)) {
|
|
152
|
+
// Required fields
|
|
153
|
+
if (!operation.description) {
|
|
154
|
+
errors.push(`Operation '${name}' missing description`);
|
|
155
|
+
}
|
|
156
|
+
if (!operation.endpoint) {
|
|
157
|
+
errors.push(`Operation '${name}' missing endpoint`);
|
|
158
|
+
}
|
|
159
|
+
if (!operation.method) {
|
|
160
|
+
errors.push(`Operation '${name}' missing method`);
|
|
161
|
+
}
|
|
162
|
+
if (!operation.input) {
|
|
163
|
+
errors.push(`Operation '${name}' missing input schema`);
|
|
164
|
+
}
|
|
165
|
+
if (!operation.output) {
|
|
166
|
+
errors.push(`Operation '${name}' missing output schema`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Validate method
|
|
170
|
+
const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
|
|
171
|
+
if (operation.method && !validMethods.includes(operation.method)) {
|
|
172
|
+
errors.push(`Operation '${name}' has invalid method: ${operation.method}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Validate endpoint format
|
|
176
|
+
if (operation.endpoint && !operation.endpoint.startsWith('/')) {
|
|
177
|
+
warnings.push(`Operation '${name}' endpoint should start with /`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
passed: errors.length === 0,
|
|
183
|
+
operationCount,
|
|
184
|
+
errors,
|
|
185
|
+
warnings
|
|
186
|
+
};
|
|
187
|
+
} catch (error) {
|
|
188
|
+
return {
|
|
189
|
+
passed: false,
|
|
190
|
+
error: error.message
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Check all operations endpoints respond correctly
|
|
197
|
+
*/
|
|
198
|
+
async checkOperationsEndpoints(serviceUrl, operations) {
|
|
199
|
+
const axios = require('axios');
|
|
200
|
+
const results = {
|
|
201
|
+
passed: true,
|
|
202
|
+
total: Object.keys(operations).length,
|
|
203
|
+
successful: 0,
|
|
204
|
+
failed: 0,
|
|
205
|
+
details: []
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
for (const [operationName, operation] of Object.entries(operations)) {
|
|
209
|
+
try {
|
|
210
|
+
const endpoint = operation.endpoint;
|
|
211
|
+
const method = operation.method.toLowerCase();
|
|
212
|
+
const url = `${serviceUrl}${endpoint}`;
|
|
213
|
+
|
|
214
|
+
// Generate test input based on schema
|
|
215
|
+
const testInput = this.generateTestInput(operation.input);
|
|
216
|
+
|
|
217
|
+
// Make request
|
|
218
|
+
const response = await axios({
|
|
219
|
+
method,
|
|
220
|
+
url,
|
|
221
|
+
data: method !== 'get' ? testInput : undefined,
|
|
222
|
+
params: method === 'get' ? testInput : undefined,
|
|
223
|
+
timeout: 5000,
|
|
224
|
+
validateStatus: () => true // Accept any status for validation
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Check response
|
|
228
|
+
const isValid = response.status >= 200 && response.status < 300;
|
|
229
|
+
|
|
230
|
+
if (isValid) {
|
|
231
|
+
results.successful++;
|
|
232
|
+
} else {
|
|
233
|
+
results.failed++;
|
|
234
|
+
results.passed = false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
results.details.push({
|
|
238
|
+
operation: operationName,
|
|
239
|
+
endpoint,
|
|
240
|
+
method: method.toUpperCase(),
|
|
241
|
+
status: response.status,
|
|
242
|
+
valid: isValid
|
|
243
|
+
});
|
|
244
|
+
} catch (error) {
|
|
245
|
+
results.failed++;
|
|
246
|
+
results.passed = false;
|
|
247
|
+
results.details.push({
|
|
248
|
+
operation: operationName,
|
|
249
|
+
endpoint: operation.endpoint,
|
|
250
|
+
method: operation.method,
|
|
251
|
+
valid: false,
|
|
252
|
+
error: error.message
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return results;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Check health endpoint
|
|
262
|
+
*/
|
|
263
|
+
async checkHealth(healthUrl) {
|
|
264
|
+
try {
|
|
265
|
+
const axios = require('axios');
|
|
266
|
+
const response = await axios.get(healthUrl, { timeout: 5000 });
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
passed: response.status === 200,
|
|
270
|
+
status: response.status,
|
|
271
|
+
data: response.data
|
|
272
|
+
};
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return {
|
|
275
|
+
passed: false,
|
|
276
|
+
error: error.message
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Check cookbook structure validity
|
|
283
|
+
* Simple structural validation - checks cookbook has required fields and valid structure
|
|
284
|
+
*/
|
|
285
|
+
async checkCookbookExecution(cookbook) {
|
|
286
|
+
try {
|
|
287
|
+
// Validate cookbook structure using CookbookTestUtils
|
|
288
|
+
const structureValidation = CookbookTestUtils.validateCookbook(cookbook);
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
passed: structureValidation.valid,
|
|
292
|
+
errors: structureValidation.errors || [],
|
|
293
|
+
stepsValidated: cookbook.steps ? cookbook.steps.length : 0
|
|
294
|
+
};
|
|
295
|
+
} catch (error) {
|
|
296
|
+
return {
|
|
297
|
+
passed: false,
|
|
298
|
+
error: error.message
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Check registry compatibility
|
|
305
|
+
*/
|
|
306
|
+
async checkRegistryCompatibility(service, registry) {
|
|
307
|
+
try {
|
|
308
|
+
// Check if service can be registered
|
|
309
|
+
const canRegister = !!(
|
|
310
|
+
service.name &&
|
|
311
|
+
service.version &&
|
|
312
|
+
service.operations
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// Check if service operations match registry expectations
|
|
316
|
+
const registeredService = registry.getService(service.name);
|
|
317
|
+
const compatible = !registeredService ||
|
|
318
|
+
registeredService.version === service.version;
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
passed: canRegister && compatible,
|
|
322
|
+
canRegister,
|
|
323
|
+
compatible,
|
|
324
|
+
existingVersion: registeredService?.version
|
|
325
|
+
};
|
|
326
|
+
} catch (error) {
|
|
327
|
+
return {
|
|
328
|
+
passed: false,
|
|
329
|
+
error: error.message
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Generate test input based on operation input schema
|
|
336
|
+
*/
|
|
337
|
+
generateTestInput(inputSchema) {
|
|
338
|
+
const testData = {};
|
|
339
|
+
|
|
340
|
+
for (const [fieldName, fieldSpec] of Object.entries(inputSchema)) {
|
|
341
|
+
if (fieldSpec.required) {
|
|
342
|
+
// Generate appropriate test value based on type
|
|
343
|
+
switch (fieldSpec.type) {
|
|
344
|
+
case 'string':
|
|
345
|
+
testData[fieldName] = fieldSpec.default || 'Test';
|
|
346
|
+
break;
|
|
347
|
+
case 'number':
|
|
348
|
+
testData[fieldName] = fieldSpec.default || 0;
|
|
349
|
+
break;
|
|
350
|
+
case 'boolean':
|
|
351
|
+
testData[fieldName] = fieldSpec.default || false;
|
|
352
|
+
break;
|
|
353
|
+
case 'array':
|
|
354
|
+
testData[fieldName] = fieldSpec.default || [];
|
|
355
|
+
break;
|
|
356
|
+
case 'object':
|
|
357
|
+
testData[fieldName] = fieldSpec.default || {};
|
|
358
|
+
break;
|
|
359
|
+
default:
|
|
360
|
+
testData[fieldName] = null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return testData;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get recommendation based on results
|
|
370
|
+
*/
|
|
371
|
+
getRecommendation(results) {
|
|
372
|
+
if (results.ready) {
|
|
373
|
+
if (results.score >= 90) {
|
|
374
|
+
return 'Service is fully ready for production deployment';
|
|
375
|
+
} else if (results.score >= 75) {
|
|
376
|
+
return 'Service is ready but consider addressing warnings';
|
|
377
|
+
} else {
|
|
378
|
+
return 'Service meets minimum requirements but needs improvement';
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
if (results.errors.length > 0) {
|
|
382
|
+
return `Service is not ready: ${results.errors.join(', ')}`;
|
|
383
|
+
} else {
|
|
384
|
+
return 'Service does not meet minimum readiness requirements';
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Generate readiness report
|
|
391
|
+
*/
|
|
392
|
+
generateReport(results) {
|
|
393
|
+
const report = [];
|
|
394
|
+
|
|
395
|
+
report.push('=== Service Readiness Report ===');
|
|
396
|
+
report.push(`Service: ${results.serviceName}`);
|
|
397
|
+
report.push(`URL: ${results.serviceUrl}`);
|
|
398
|
+
report.push(`Score: ${results.score}/${results.maxScore}`);
|
|
399
|
+
report.push(`Status: ${results.ready ? 'READY' : 'NOT READY'}`);
|
|
400
|
+
report.push('');
|
|
401
|
+
|
|
402
|
+
report.push('Checks:');
|
|
403
|
+
for (const [name, check] of Object.entries(results.checks)) {
|
|
404
|
+
const status = check.passed ? '✓' : '✗';
|
|
405
|
+
report.push(` ${status} ${name}: ${check.passed ? 'Passed' : 'Failed'}`);
|
|
406
|
+
if (check.error) {
|
|
407
|
+
report.push(` Error: ${check.error}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
report.push('');
|
|
412
|
+
report.push(`Recommendation: ${results.recommendation}`);
|
|
413
|
+
|
|
414
|
+
if (results.errors.length > 0) {
|
|
415
|
+
report.push('');
|
|
416
|
+
report.push('Errors:');
|
|
417
|
+
results.errors.forEach(err => report.push(` - ${err}`));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (results.warnings.length > 0) {
|
|
421
|
+
report.push('');
|
|
422
|
+
report.push('Warnings:');
|
|
423
|
+
results.warnings.forEach(warn => report.push(` - ${warn}`));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return report.join('\n');
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
module.exports = ServiceReadinessValidator;
|