@onlineapps/conn-orch-validator 2.0.32 → 2.0.34

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.
@@ -1,399 +0,0 @@
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
- // Validation context: ALWAYS add x-validation-request header to indicate this is a validation probe.
121
- // This allows services to return deterministic responses without accessing external resources.
122
- 'x-validation-request': 'true'
123
- },
124
- query: {},
125
- body: null
126
- };
127
-
128
- // Process parameters
129
- if (operation.parameters) {
130
- operation.parameters.forEach(param => {
131
- const testValue = this.generateTestValue(param.schema);
132
-
133
- switch (param.in) {
134
- case 'path':
135
- request.path = request.path.replace(`{${param.name}}`, testValue);
136
- break;
137
- case 'query':
138
- if (!param.required && Math.random() > 0.5) break;
139
- request.query[param.name] = testValue;
140
- break;
141
- case 'header':
142
- if (!param.required && Math.random() > 0.5) break;
143
- request.headers[param.name] = testValue;
144
- break;
145
- }
146
- });
147
- }
148
-
149
- // Process request body
150
- if (operation.requestBody && operation.requestBody.content) {
151
- const contentType = Object.keys(operation.requestBody.content)[0];
152
- const schema = operation.requestBody.content[contentType].schema;
153
- request.headers['Content-Type'] = contentType;
154
- request.body = this.generateTestValue(schema);
155
- }
156
-
157
- return request;
158
- }
159
-
160
- /**
161
- * Generate test value based on schema
162
- */
163
- generateTestValue(schema) {
164
- if (!schema) return null;
165
-
166
- switch (schema.type) {
167
- case 'string':
168
- if (schema.enum) return schema.enum[0];
169
- if (schema.format === 'email') return 'test@example.com';
170
- if (schema.format === 'date') return '2024-01-01';
171
- if (schema.format === 'date-time') return '2024-01-01T00:00:00Z';
172
- if (schema.format === 'uuid') return '123e4567-e89b-12d3-a456-426614174000';
173
- return schema.example || 'test';
174
-
175
- case 'number':
176
- case 'integer':
177
- if (schema.minimum !== undefined) return schema.minimum;
178
- if (schema.maximum !== undefined) return schema.maximum;
179
- return schema.example || 1;
180
-
181
- case 'boolean':
182
- return schema.example !== undefined ? schema.example : true;
183
-
184
- case 'array':
185
- const itemValue = this.generateTestValue(schema.items);
186
- return [itemValue];
187
-
188
- case 'object':
189
- const obj = {};
190
- if (schema.properties) {
191
- const entries = Object.entries(schema.properties);
192
- entries.forEach(([key, prop]) => {
193
- if (schema.required && schema.required.includes(key)) {
194
- obj[key] = this.generateTestValue(prop);
195
- } else if (Math.random() > 0.5) {
196
- obj[key] = this.generateTestValue(prop);
197
- }
198
- });
199
-
200
- // If schema has properties but nothing was picked (e.g., no required fields and randomness skipped),
201
- // pick the first property to keep requests deterministic and non-empty.
202
- if (Object.keys(obj).length === 0 && entries.length > 0) {
203
- const [firstKey, firstSchema] = entries[0];
204
- obj[firstKey] = this.generateTestValue(firstSchema);
205
- }
206
- }
207
- return obj;
208
-
209
- default:
210
- return null;
211
- }
212
- }
213
-
214
- /**
215
- * Validate response against OpenAPI schema
216
- */
217
- validateResponse(response, operation, result) {
218
- if (!operation.responses) {
219
- result.warnings.push('No responses defined in OpenAPI spec');
220
- return;
221
- }
222
-
223
- const statusCode = response.status.toString();
224
- const responseSpec = operation.responses[statusCode] || operation.responses.default;
225
-
226
- if (!responseSpec) {
227
- result.valid = false;
228
- result.errors.push(`Unexpected status code: ${statusCode}`);
229
- return;
230
- }
231
-
232
- // Validate response body
233
- if (responseSpec.content) {
234
- const contentType = Object.keys(responseSpec.content)[0];
235
- const schema = responseSpec.content[contentType].schema;
236
-
237
- if (schema) {
238
- const validate = this.ajv.compile(schema);
239
- if (!validate(response.data)) {
240
- result.valid = false;
241
- result.errors.push(`Response validation failed: ${JSON.stringify(validate.errors)}`);
242
- }
243
- }
244
- }
245
-
246
- // Validate headers
247
- if (responseSpec.headers) {
248
- Object.entries(responseSpec.headers).forEach(([headerName, headerSpec]) => {
249
- const headerValue = response.headers[headerName.toLowerCase()];
250
- if (headerSpec.required && !headerValue) {
251
- result.valid = false;
252
- result.errors.push(`Required header missing: ${headerName}`);
253
- }
254
- });
255
- }
256
- }
257
-
258
- /**
259
- * Count total endpoints in paths
260
- */
261
- countEndpoints(paths) {
262
- let count = 0;
263
- Object.values(paths).forEach(pathItem => {
264
- Object.keys(pathItem).forEach(method => {
265
- if (method !== 'parameters' && !method.startsWith('x-')) {
266
- count++;
267
- }
268
- });
269
- });
270
- return count;
271
- }
272
-
273
- /**
274
- * Validate cookbook structure and operations
275
- */
276
- async validateCookbook(cookbook, serviceRegistry) {
277
- const results = {
278
- valid: true,
279
- errors: [],
280
- warnings: [],
281
- validatedSteps: []
282
- };
283
-
284
- if (!cookbook.steps || !Array.isArray(cookbook.steps)) {
285
- results.valid = false;
286
- results.errors.push('Cookbook must have steps array');
287
- return results;
288
- }
289
-
290
- // Validate each step
291
- for (const step of cookbook.steps) {
292
- const stepResult = await this.validateStep(step, serviceRegistry);
293
- results.validatedSteps.push(stepResult);
294
-
295
- if (!stepResult.valid) {
296
- results.valid = false;
297
- results.errors.push(...stepResult.errors);
298
- }
299
- if (stepResult.warnings) {
300
- results.warnings.push(...stepResult.warnings);
301
- }
302
- }
303
-
304
- return results;
305
- }
306
-
307
- /**
308
- * Validate individual cookbook step
309
- */
310
- async validateStep(step, serviceRegistry) {
311
- const result = {
312
- stepId: step.id,
313
- valid: true,
314
- errors: [],
315
- warnings: []
316
- };
317
-
318
- // Basic validation
319
- if (!step.id) {
320
- result.valid = false;
321
- result.errors.push('Step must have an id');
322
- }
323
-
324
- if (!step.type) {
325
- result.valid = false;
326
- result.errors.push('Step must have a type');
327
- }
328
-
329
- // Type-specific validation
330
- switch (step.type) {
331
- case 'task':
332
- if (!step.service) {
333
- result.valid = false;
334
- result.errors.push('Task step must have a service');
335
- } else {
336
- // Check if service exists
337
- const service = serviceRegistry.getService(step.service);
338
- if (!service) {
339
- result.valid = false;
340
- result.errors.push(`Service not found: ${step.service}`);
341
- } else if (step.operation) {
342
- // Validate operation exists in service
343
- const hasOperation = this.serviceHasOperation(
344
- service.openapi,
345
- step.operation
346
- );
347
- if (!hasOperation) {
348
- result.valid = false;
349
- result.errors.push(`Operation not found: ${step.operation}`);
350
- }
351
- }
352
- }
353
- break;
354
-
355
- case 'foreach':
356
- if (!step.items) {
357
- result.valid = false;
358
- result.errors.push('Foreach step must have items');
359
- }
360
- if (!step.body) {
361
- result.valid = false;
362
- result.errors.push('Foreach step must have body');
363
- }
364
- break;
365
-
366
- case 'switch':
367
- if (!step.condition) {
368
- result.valid = false;
369
- result.errors.push('Switch step must have condition');
370
- }
371
- if (!step.cases || !Array.isArray(step.cases)) {
372
- result.valid = false;
373
- result.errors.push('Switch step must have cases array');
374
- }
375
- break;
376
- }
377
-
378
- return result;
379
- }
380
-
381
- /**
382
- * Check if service has operation
383
- */
384
- serviceHasOperation(openApiSpec, operationId) {
385
- if (!openApiSpec || !openApiSpec.paths) return false;
386
-
387
- for (const pathItem of Object.values(openApiSpec.paths)) {
388
- for (const operation of Object.values(pathItem)) {
389
- if (operation.operationId === operationId) {
390
- return true;
391
- }
392
- }
393
- }
394
-
395
- return false;
396
- }
397
- }
398
-
399
- module.exports = ServiceValidator;