@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,256 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MockMQClient = require('./mocks/MockMQClient');
|
|
4
|
+
const MockRegistry = require('./mocks/MockRegistry');
|
|
5
|
+
const MockStorage = require('./mocks/MockStorage');
|
|
6
|
+
const axios = require('axios');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ServiceTestHarness - Complete test environment for services
|
|
10
|
+
*/
|
|
11
|
+
class ServiceTestHarness {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.service = options.service; // Express app
|
|
14
|
+
this.serviceName = options.serviceName || 'test-service';
|
|
15
|
+
this.openApiSpec = options.openApiSpec || {};
|
|
16
|
+
this.mockInfrastructure = options.mockInfrastructure !== false;
|
|
17
|
+
|
|
18
|
+
// Initialize mocks if requested
|
|
19
|
+
if (this.mockInfrastructure) {
|
|
20
|
+
this.mqClient = new MockMQClient();
|
|
21
|
+
this.registry = new MockRegistry();
|
|
22
|
+
this.storage = new MockStorage();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Test state
|
|
26
|
+
this.server = null;
|
|
27
|
+
this.port = options.port || 0; // 0 = random port
|
|
28
|
+
this.baseUrl = null;
|
|
29
|
+
this.isRunning = false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Start test harness
|
|
34
|
+
*/
|
|
35
|
+
async start() {
|
|
36
|
+
if (this.isRunning) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Start Express server if provided
|
|
41
|
+
if (this.service) {
|
|
42
|
+
await this.startServer();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Connect mocks
|
|
46
|
+
if (this.mockInfrastructure) {
|
|
47
|
+
await this.mqClient.connect();
|
|
48
|
+
await this.registry.register({
|
|
49
|
+
name: this.serviceName,
|
|
50
|
+
url: this.baseUrl,
|
|
51
|
+
openapi: this.openApiSpec
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.isRunning = true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Stop test harness
|
|
60
|
+
*/
|
|
61
|
+
async stop() {
|
|
62
|
+
if (!this.isRunning) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Stop server
|
|
67
|
+
if (this.server) {
|
|
68
|
+
await new Promise(resolve => {
|
|
69
|
+
this.server.close(resolve);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Disconnect mocks
|
|
74
|
+
if (this.mockInfrastructure) {
|
|
75
|
+
await this.mqClient.disconnect();
|
|
76
|
+
await this.registry.unregister(this.serviceName);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.isRunning = false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Start Express server
|
|
84
|
+
* @private
|
|
85
|
+
*/
|
|
86
|
+
async startServer() {
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
this.server = this.service.listen(this.port, () => {
|
|
89
|
+
const actualPort = this.server.address().port;
|
|
90
|
+
this.baseUrl = `http://localhost:${actualPort}`;
|
|
91
|
+
resolve();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Call API endpoint
|
|
98
|
+
*/
|
|
99
|
+
async callApi(method, path, data = null, headers = {}) {
|
|
100
|
+
if (!this.baseUrl) {
|
|
101
|
+
throw new Error('Service not started');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const config = {
|
|
105
|
+
method,
|
|
106
|
+
url: `${this.baseUrl}${path}`,
|
|
107
|
+
headers,
|
|
108
|
+
timeout: 5000
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (data) {
|
|
112
|
+
if (method === 'GET') {
|
|
113
|
+
config.params = data;
|
|
114
|
+
} else {
|
|
115
|
+
config.data = data;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const response = await axios(config);
|
|
120
|
+
return response.data;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Simulate workflow execution
|
|
125
|
+
*/
|
|
126
|
+
async simulateWorkflow(workflow) {
|
|
127
|
+
const results = [];
|
|
128
|
+
|
|
129
|
+
for (const step of workflow.steps) {
|
|
130
|
+
if (step.type === 'task' && step.service === this.serviceName) {
|
|
131
|
+
// Execute task step
|
|
132
|
+
const result = await this.executeStep(step);
|
|
133
|
+
results.push(result);
|
|
134
|
+
|
|
135
|
+
// Simulate publishing result to queue
|
|
136
|
+
if (this.mockInfrastructure) {
|
|
137
|
+
await this.mqClient.publish('workflow.completed', {
|
|
138
|
+
step: step.id,
|
|
139
|
+
result
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
workflow: workflow,
|
|
147
|
+
results,
|
|
148
|
+
completed: true
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Execute a single workflow step
|
|
154
|
+
* @private
|
|
155
|
+
*/
|
|
156
|
+
async executeStep(step) {
|
|
157
|
+
// Map step to API call based on operation
|
|
158
|
+
const operation = step.operation || step.id;
|
|
159
|
+
const endpoint = this.findEndpoint(operation);
|
|
160
|
+
|
|
161
|
+
if (!endpoint) {
|
|
162
|
+
throw new Error(`No endpoint found for operation: ${operation}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Call the API
|
|
166
|
+
const response = await this.callApi(
|
|
167
|
+
endpoint.method,
|
|
168
|
+
endpoint.path,
|
|
169
|
+
step.input
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
stepId: step.id,
|
|
174
|
+
operation,
|
|
175
|
+
response
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Find endpoint in OpenAPI spec
|
|
181
|
+
* @private
|
|
182
|
+
*/
|
|
183
|
+
findEndpoint(operationId) {
|
|
184
|
+
if (!this.openApiSpec.paths) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const [path, pathItem] of Object.entries(this.openApiSpec.paths)) {
|
|
189
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
190
|
+
if (operation.operationId === operationId) {
|
|
191
|
+
return { method: method.toUpperCase(), path };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get published messages from mock MQ
|
|
201
|
+
*/
|
|
202
|
+
getPublishedMessages(queue = null) {
|
|
203
|
+
if (!this.mockInfrastructure) {
|
|
204
|
+
throw new Error('Mock infrastructure not enabled');
|
|
205
|
+
}
|
|
206
|
+
return this.mqClient.getPublishedMessages(queue);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get service registration from mock registry
|
|
211
|
+
*/
|
|
212
|
+
getServiceRegistration() {
|
|
213
|
+
if (!this.mockInfrastructure) {
|
|
214
|
+
throw new Error('Mock infrastructure not enabled');
|
|
215
|
+
}
|
|
216
|
+
return this.registry.getService(this.serviceName);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get storage contents from mock storage
|
|
221
|
+
*/
|
|
222
|
+
async getStorageObject(bucket, key) {
|
|
223
|
+
if (!this.mockInfrastructure) {
|
|
224
|
+
throw new Error('Mock infrastructure not enabled');
|
|
225
|
+
}
|
|
226
|
+
return await this.storage.get(bucket, key);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Clear all test data
|
|
231
|
+
*/
|
|
232
|
+
clear() {
|
|
233
|
+
if (this.mockInfrastructure) {
|
|
234
|
+
this.mqClient.clear();
|
|
235
|
+
this.registry.clear();
|
|
236
|
+
this.storage.clear();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get test statistics
|
|
242
|
+
*/
|
|
243
|
+
getStats() {
|
|
244
|
+
if (!this.mockInfrastructure) {
|
|
245
|
+
return { mocks: 'disabled' };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
mq: this.mqClient.getStats(),
|
|
250
|
+
registry: this.registry.getStats(),
|
|
251
|
+
storage: this.storage.getStats()
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = ServiceTestHarness;
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const axios = require('axios');
|
|
4
|
+
const Ajv = require('ajv');
|
|
5
|
+
const addFormats = require('ajv-formats');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ServiceValidator - Validates service compliance with OpenAPI spec
|
|
9
|
+
*/
|
|
10
|
+
class ServiceValidator {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.ajv = new Ajv({ allErrors: true, strict: false });
|
|
13
|
+
addFormats(this.ajv);
|
|
14
|
+
this.timeout = options.timeout || 5000;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validate service against OpenAPI specification
|
|
19
|
+
*/
|
|
20
|
+
async validateService(serviceUrl, openApiSpec) {
|
|
21
|
+
const results = {
|
|
22
|
+
valid: true,
|
|
23
|
+
errors: [],
|
|
24
|
+
warnings: [],
|
|
25
|
+
testedEndpoints: [],
|
|
26
|
+
coverage: 0
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (!openApiSpec.paths) {
|
|
30
|
+
results.valid = false;
|
|
31
|
+
results.errors.push('No paths defined in OpenAPI spec');
|
|
32
|
+
return results;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const totalEndpoints = this.countEndpoints(openApiSpec.paths);
|
|
36
|
+
let testedCount = 0;
|
|
37
|
+
|
|
38
|
+
// Test each endpoint
|
|
39
|
+
for (const [path, pathItem] of Object.entries(openApiSpec.paths)) {
|
|
40
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
41
|
+
if (method === 'parameters' || method.startsWith('x-')) continue;
|
|
42
|
+
|
|
43
|
+
const endpointResult = await this.testEndpoint(
|
|
44
|
+
serviceUrl,
|
|
45
|
+
path,
|
|
46
|
+
method,
|
|
47
|
+
operation
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
results.testedEndpoints.push(endpointResult);
|
|
51
|
+
if (!endpointResult.valid) {
|
|
52
|
+
results.valid = false;
|
|
53
|
+
results.errors.push(...endpointResult.errors);
|
|
54
|
+
}
|
|
55
|
+
if (endpointResult.warnings) {
|
|
56
|
+
results.warnings.push(...endpointResult.warnings);
|
|
57
|
+
}
|
|
58
|
+
testedCount++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
results.coverage = (testedCount / totalEndpoints) * 100;
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Test individual endpoint
|
|
68
|
+
*/
|
|
69
|
+
async testEndpoint(serviceUrl, path, method, operation) {
|
|
70
|
+
const result = {
|
|
71
|
+
path,
|
|
72
|
+
method: method.toUpperCase(),
|
|
73
|
+
operationId: operation.operationId,
|
|
74
|
+
valid: true,
|
|
75
|
+
errors: [],
|
|
76
|
+
warnings: [],
|
|
77
|
+
response: null
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Build test request
|
|
82
|
+
const testRequest = this.buildTestRequest(path, method, operation);
|
|
83
|
+
const url = `${serviceUrl}${testRequest.path}`;
|
|
84
|
+
|
|
85
|
+
// Execute request
|
|
86
|
+
const response = await axios({
|
|
87
|
+
method: method.toUpperCase(),
|
|
88
|
+
url,
|
|
89
|
+
data: testRequest.body,
|
|
90
|
+
params: testRequest.query,
|
|
91
|
+
headers: testRequest.headers,
|
|
92
|
+
timeout: this.timeout,
|
|
93
|
+
validateStatus: () => true // Don't throw on any status
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
result.response = {
|
|
97
|
+
status: response.status,
|
|
98
|
+
headers: response.headers,
|
|
99
|
+
data: response.data
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Validate response
|
|
103
|
+
this.validateResponse(response, operation, result);
|
|
104
|
+
|
|
105
|
+
} catch (error) {
|
|
106
|
+
result.valid = false;
|
|
107
|
+
result.errors.push(`Failed to call endpoint: ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Build test request from OpenAPI operation
|
|
115
|
+
*/
|
|
116
|
+
buildTestRequest(path, method, operation) {
|
|
117
|
+
const request = {
|
|
118
|
+
path,
|
|
119
|
+
headers: {},
|
|
120
|
+
query: {},
|
|
121
|
+
body: null
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Process parameters
|
|
125
|
+
if (operation.parameters) {
|
|
126
|
+
operation.parameters.forEach(param => {
|
|
127
|
+
const testValue = this.generateTestValue(param.schema);
|
|
128
|
+
|
|
129
|
+
switch (param.in) {
|
|
130
|
+
case 'path':
|
|
131
|
+
request.path = request.path.replace(`{${param.name}}`, testValue);
|
|
132
|
+
break;
|
|
133
|
+
case 'query':
|
|
134
|
+
if (!param.required && Math.random() > 0.5) break;
|
|
135
|
+
request.query[param.name] = testValue;
|
|
136
|
+
break;
|
|
137
|
+
case 'header':
|
|
138
|
+
if (!param.required && Math.random() > 0.5) break;
|
|
139
|
+
request.headers[param.name] = testValue;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Process request body
|
|
146
|
+
if (operation.requestBody && operation.requestBody.content) {
|
|
147
|
+
const contentType = Object.keys(operation.requestBody.content)[0];
|
|
148
|
+
const schema = operation.requestBody.content[contentType].schema;
|
|
149
|
+
request.headers['Content-Type'] = contentType;
|
|
150
|
+
request.body = this.generateTestValue(schema);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return request;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Generate test value based on schema
|
|
158
|
+
*/
|
|
159
|
+
generateTestValue(schema) {
|
|
160
|
+
if (!schema) return null;
|
|
161
|
+
|
|
162
|
+
switch (schema.type) {
|
|
163
|
+
case 'string':
|
|
164
|
+
if (schema.enum) return schema.enum[0];
|
|
165
|
+
if (schema.format === 'email') return 'test@example.com';
|
|
166
|
+
if (schema.format === 'date') return '2024-01-01';
|
|
167
|
+
if (schema.format === 'date-time') return '2024-01-01T00:00:00Z';
|
|
168
|
+
if (schema.format === 'uuid') return '123e4567-e89b-12d3-a456-426614174000';
|
|
169
|
+
return schema.example || 'test';
|
|
170
|
+
|
|
171
|
+
case 'number':
|
|
172
|
+
case 'integer':
|
|
173
|
+
if (schema.minimum !== undefined) return schema.minimum;
|
|
174
|
+
if (schema.maximum !== undefined) return schema.maximum;
|
|
175
|
+
return schema.example || 1;
|
|
176
|
+
|
|
177
|
+
case 'boolean':
|
|
178
|
+
return schema.example !== undefined ? schema.example : true;
|
|
179
|
+
|
|
180
|
+
case 'array':
|
|
181
|
+
const itemValue = this.generateTestValue(schema.items);
|
|
182
|
+
return [itemValue];
|
|
183
|
+
|
|
184
|
+
case 'object':
|
|
185
|
+
const obj = {};
|
|
186
|
+
if (schema.properties) {
|
|
187
|
+
Object.entries(schema.properties).forEach(([key, prop]) => {
|
|
188
|
+
if (schema.required && schema.required.includes(key)) {
|
|
189
|
+
obj[key] = this.generateTestValue(prop);
|
|
190
|
+
} else if (Math.random() > 0.5) {
|
|
191
|
+
obj[key] = this.generateTestValue(prop);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return obj;
|
|
196
|
+
|
|
197
|
+
default:
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Validate response against OpenAPI schema
|
|
204
|
+
*/
|
|
205
|
+
validateResponse(response, operation, result) {
|
|
206
|
+
if (!operation.responses) {
|
|
207
|
+
result.warnings.push('No responses defined in OpenAPI spec');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const statusCode = response.status.toString();
|
|
212
|
+
const responseSpec = operation.responses[statusCode] || operation.responses.default;
|
|
213
|
+
|
|
214
|
+
if (!responseSpec) {
|
|
215
|
+
result.valid = false;
|
|
216
|
+
result.errors.push(`Unexpected status code: ${statusCode}`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Validate response body
|
|
221
|
+
if (responseSpec.content) {
|
|
222
|
+
const contentType = Object.keys(responseSpec.content)[0];
|
|
223
|
+
const schema = responseSpec.content[contentType].schema;
|
|
224
|
+
|
|
225
|
+
if (schema) {
|
|
226
|
+
const validate = this.ajv.compile(schema);
|
|
227
|
+
if (!validate(response.data)) {
|
|
228
|
+
result.valid = false;
|
|
229
|
+
result.errors.push(`Response validation failed: ${JSON.stringify(validate.errors)}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Validate headers
|
|
235
|
+
if (responseSpec.headers) {
|
|
236
|
+
Object.entries(responseSpec.headers).forEach(([headerName, headerSpec]) => {
|
|
237
|
+
const headerValue = response.headers[headerName.toLowerCase()];
|
|
238
|
+
if (headerSpec.required && !headerValue) {
|
|
239
|
+
result.valid = false;
|
|
240
|
+
result.errors.push(`Required header missing: ${headerName}`);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Count total endpoints in paths
|
|
248
|
+
*/
|
|
249
|
+
countEndpoints(paths) {
|
|
250
|
+
let count = 0;
|
|
251
|
+
Object.values(paths).forEach(pathItem => {
|
|
252
|
+
Object.keys(pathItem).forEach(method => {
|
|
253
|
+
if (method !== 'parameters' && !method.startsWith('x-')) {
|
|
254
|
+
count++;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
return count;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Validate cookbook structure and operations
|
|
263
|
+
*/
|
|
264
|
+
async validateCookbook(cookbook, serviceRegistry) {
|
|
265
|
+
const results = {
|
|
266
|
+
valid: true,
|
|
267
|
+
errors: [],
|
|
268
|
+
warnings: [],
|
|
269
|
+
validatedSteps: []
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
if (!cookbook.steps || !Array.isArray(cookbook.steps)) {
|
|
273
|
+
results.valid = false;
|
|
274
|
+
results.errors.push('Cookbook must have steps array');
|
|
275
|
+
return results;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Validate each step
|
|
279
|
+
for (const step of cookbook.steps) {
|
|
280
|
+
const stepResult = await this.validateStep(step, serviceRegistry);
|
|
281
|
+
results.validatedSteps.push(stepResult);
|
|
282
|
+
|
|
283
|
+
if (!stepResult.valid) {
|
|
284
|
+
results.valid = false;
|
|
285
|
+
results.errors.push(...stepResult.errors);
|
|
286
|
+
}
|
|
287
|
+
if (stepResult.warnings) {
|
|
288
|
+
results.warnings.push(...stepResult.warnings);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return results;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Validate individual cookbook step
|
|
297
|
+
*/
|
|
298
|
+
async validateStep(step, serviceRegistry) {
|
|
299
|
+
const result = {
|
|
300
|
+
stepId: step.id,
|
|
301
|
+
valid: true,
|
|
302
|
+
errors: [],
|
|
303
|
+
warnings: []
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Basic validation
|
|
307
|
+
if (!step.id) {
|
|
308
|
+
result.valid = false;
|
|
309
|
+
result.errors.push('Step must have an id');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!step.type) {
|
|
313
|
+
result.valid = false;
|
|
314
|
+
result.errors.push('Step must have a type');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Type-specific validation
|
|
318
|
+
switch (step.type) {
|
|
319
|
+
case 'task':
|
|
320
|
+
if (!step.service) {
|
|
321
|
+
result.valid = false;
|
|
322
|
+
result.errors.push('Task step must have a service');
|
|
323
|
+
} else {
|
|
324
|
+
// Check if service exists
|
|
325
|
+
const service = serviceRegistry.getService(step.service);
|
|
326
|
+
if (!service) {
|
|
327
|
+
result.valid = false;
|
|
328
|
+
result.errors.push(`Service not found: ${step.service}`);
|
|
329
|
+
} else if (step.operation) {
|
|
330
|
+
// Validate operation exists in service
|
|
331
|
+
const hasOperation = this.serviceHasOperation(
|
|
332
|
+
service.openapi,
|
|
333
|
+
step.operation
|
|
334
|
+
);
|
|
335
|
+
if (!hasOperation) {
|
|
336
|
+
result.valid = false;
|
|
337
|
+
result.errors.push(`Operation not found: ${step.operation}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
|
|
343
|
+
case 'foreach':
|
|
344
|
+
if (!step.items) {
|
|
345
|
+
result.valid = false;
|
|
346
|
+
result.errors.push('Foreach step must have items');
|
|
347
|
+
}
|
|
348
|
+
if (!step.body) {
|
|
349
|
+
result.valid = false;
|
|
350
|
+
result.errors.push('Foreach step must have body');
|
|
351
|
+
}
|
|
352
|
+
break;
|
|
353
|
+
|
|
354
|
+
case 'switch':
|
|
355
|
+
if (!step.condition) {
|
|
356
|
+
result.valid = false;
|
|
357
|
+
result.errors.push('Switch step must have condition');
|
|
358
|
+
}
|
|
359
|
+
if (!step.cases || !Array.isArray(step.cases)) {
|
|
360
|
+
result.valid = false;
|
|
361
|
+
result.errors.push('Switch step must have cases array');
|
|
362
|
+
}
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Check if service has operation
|
|
371
|
+
*/
|
|
372
|
+
serviceHasOperation(openApiSpec, operationId) {
|
|
373
|
+
if (!openApiSpec || !openApiSpec.paths) return false;
|
|
374
|
+
|
|
375
|
+
for (const pathItem of Object.values(openApiSpec.paths)) {
|
|
376
|
+
for (const operation of Object.values(pathItem)) {
|
|
377
|
+
if (operation.operationId === operationId) {
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
module.exports = ServiceValidator;
|