@onlineapps/conn-orch-validator 2.0.33 → 3.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.
@@ -1,256 +0,0 @@
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://127.0.0.1:${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;
@@ -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;