@onlineapps/service-validator-core 1.0.2
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 +127 -0
- package/coverage/clover.xml +468 -0
- package/coverage/coverage-final.json +8 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +146 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov-report/src/index.js.html +643 -0
- package/coverage/lcov-report/src/security/certificateManager.js.html +799 -0
- package/coverage/lcov-report/src/security/index.html +131 -0
- package/coverage/lcov-report/src/security/tokenManager.js.html +622 -0
- package/coverage/lcov-report/src/validators/connectorValidator.js.html +787 -0
- package/coverage/lcov-report/src/validators/endpointValidator.js.html +577 -0
- package/coverage/lcov-report/src/validators/healthValidator.js.html +655 -0
- package/coverage/lcov-report/src/validators/index.html +161 -0
- package/coverage/lcov-report/src/validators/openApiValidator.js.html +517 -0
- package/coverage/lcov.info +982 -0
- package/jest.config.js +21 -0
- package/package.json +31 -0
- package/src/index.js +212 -0
- package/src/security/ValidationProofVerifier.js +178 -0
- package/src/security/certificateManager.js +239 -0
- package/src/security/tokenManager.js +194 -0
- package/src/validators/connectorValidator.js +235 -0
- package/src/validators/endpointValidator.js +165 -0
- package/src/validators/healthValidator.js +191 -0
- package/src/validators/openApiValidator.js +145 -0
- package/test/component/validation-flow.test.js +353 -0
- package/test/integration/real-validation.test.js +548 -0
- package/test/unit/ValidationCore.test.js +320 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
class HealthValidator {
|
|
2
|
+
/**
|
|
3
|
+
* Validates health check endpoint configuration
|
|
4
|
+
* @param {string|object} health - Health endpoint path or configuration
|
|
5
|
+
* @returns {Promise<object>} Validation result
|
|
6
|
+
*/
|
|
7
|
+
async validate(health) {
|
|
8
|
+
const result = {
|
|
9
|
+
valid: true,
|
|
10
|
+
errors: [],
|
|
11
|
+
warnings: [],
|
|
12
|
+
info: {}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
if (!health) {
|
|
17
|
+
result.valid = false;
|
|
18
|
+
result.errors.push('Health check endpoint is required');
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Handle different health formats
|
|
23
|
+
if (typeof health === 'string') {
|
|
24
|
+
// Simple path string
|
|
25
|
+
if (!health.startsWith('/')) {
|
|
26
|
+
result.errors.push(`Health endpoint path "${health}" must start with /`);
|
|
27
|
+
result.valid = false;
|
|
28
|
+
}
|
|
29
|
+
result.info.endpoint = health;
|
|
30
|
+
result.info.type = 'simple';
|
|
31
|
+
} else if (typeof health === 'object') {
|
|
32
|
+
// Complex health configuration
|
|
33
|
+
if (!health.endpoint && !health.path) {
|
|
34
|
+
result.errors.push('Health configuration must have endpoint or path');
|
|
35
|
+
result.valid = false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const endpoint = health.endpoint || health.path;
|
|
39
|
+
if (endpoint && !endpoint.startsWith('/')) {
|
|
40
|
+
result.errors.push(`Health endpoint path "${endpoint}" must start with /`);
|
|
41
|
+
result.valid = false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
result.info.endpoint = endpoint;
|
|
45
|
+
result.info.type = 'configured';
|
|
46
|
+
|
|
47
|
+
// Validate interval if present
|
|
48
|
+
if (health.interval !== undefined) {
|
|
49
|
+
if (typeof health.interval !== 'number' || health.interval <= 0) {
|
|
50
|
+
result.warnings.push('Health check interval should be a positive number');
|
|
51
|
+
} else {
|
|
52
|
+
result.info.interval = health.interval;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Validate timeout if present
|
|
57
|
+
if (health.timeout !== undefined) {
|
|
58
|
+
if (typeof health.timeout !== 'number' || health.timeout <= 0) {
|
|
59
|
+
result.warnings.push('Health check timeout should be a positive number');
|
|
60
|
+
} else {
|
|
61
|
+
result.info.timeout = health.timeout;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Validate retries if present
|
|
66
|
+
if (health.retries !== undefined) {
|
|
67
|
+
if (typeof health.retries !== 'number' || health.retries < 0) {
|
|
68
|
+
result.warnings.push('Health check retries should be a non-negative number');
|
|
69
|
+
} else {
|
|
70
|
+
result.info.retries = health.retries;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check for custom health checks
|
|
75
|
+
if (health.checks && Array.isArray(health.checks)) {
|
|
76
|
+
result.info.customChecks = health.checks.length;
|
|
77
|
+
|
|
78
|
+
health.checks.forEach((check, index) => {
|
|
79
|
+
if (!check.name) {
|
|
80
|
+
result.warnings.push(`Health check[${index}] should have a name`);
|
|
81
|
+
}
|
|
82
|
+
if (!check.type) {
|
|
83
|
+
result.warnings.push(`Health check[${index}] should have a type`);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Validate expected response
|
|
89
|
+
if (health.expectedResponse) {
|
|
90
|
+
if (health.expectedResponse.status && typeof health.expectedResponse.status !== 'number') {
|
|
91
|
+
result.warnings.push('Expected response status should be a number');
|
|
92
|
+
}
|
|
93
|
+
result.info.expectedResponse = health.expectedResponse;
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
result.valid = false;
|
|
97
|
+
result.errors.push('Health configuration must be a string or object');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Additional recommendations
|
|
101
|
+
if (result.valid && !result.info.interval) {
|
|
102
|
+
result.warnings.push('Consider setting a health check interval');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (result.valid && !result.info.timeout) {
|
|
106
|
+
result.warnings.push('Consider setting a health check timeout');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
} catch (error) {
|
|
110
|
+
result.valid = false;
|
|
111
|
+
result.errors.push(`Health validation error: ${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generate health check configuration
|
|
119
|
+
* @param {string} endpoint - Health endpoint path
|
|
120
|
+
* @param {object} options - Additional options
|
|
121
|
+
* @returns {object} Health configuration
|
|
122
|
+
*/
|
|
123
|
+
generateHealthConfig(endpoint, options = {}) {
|
|
124
|
+
return {
|
|
125
|
+
endpoint: endpoint || '/health',
|
|
126
|
+
interval: options.interval || 30000, // 30 seconds
|
|
127
|
+
timeout: options.timeout || 5000, // 5 seconds
|
|
128
|
+
retries: options.retries || 3,
|
|
129
|
+
expectedResponse: {
|
|
130
|
+
status: 200,
|
|
131
|
+
body: options.expectedBody || { status: 'ok' }
|
|
132
|
+
},
|
|
133
|
+
checks: options.checks || [
|
|
134
|
+
{
|
|
135
|
+
name: 'database',
|
|
136
|
+
type: 'connectivity',
|
|
137
|
+
critical: true
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'memory',
|
|
141
|
+
type: 'resource',
|
|
142
|
+
critical: false
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Validate health response
|
|
150
|
+
* @param {object} response - Health check response
|
|
151
|
+
* @param {object} expected - Expected response configuration
|
|
152
|
+
* @returns {object} Validation result
|
|
153
|
+
*/
|
|
154
|
+
validateResponse(response, expected = {}) {
|
|
155
|
+
const result = {
|
|
156
|
+
valid: true,
|
|
157
|
+
errors: [],
|
|
158
|
+
warnings: []
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Check status code
|
|
162
|
+
if (expected.status) {
|
|
163
|
+
if (response.status !== expected.status) {
|
|
164
|
+
result.valid = false;
|
|
165
|
+
result.errors.push(`Expected status ${expected.status}, got ${response.status}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check response body
|
|
170
|
+
if (expected.body) {
|
|
171
|
+
if (!response.body) {
|
|
172
|
+
result.valid = false;
|
|
173
|
+
result.errors.push('Missing response body');
|
|
174
|
+
} else if (expected.body.status && response.body.status !== expected.body.status) {
|
|
175
|
+
result.valid = false;
|
|
176
|
+
result.errors.push(`Expected status "${expected.body.status}", got "${response.body.status}"`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check response time
|
|
181
|
+
if (response.responseTime && expected.maxResponseTime) {
|
|
182
|
+
if (response.responseTime > expected.maxResponseTime) {
|
|
183
|
+
result.warnings.push(`Response time ${response.responseTime}ms exceeds recommended ${expected.maxResponseTime}ms`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = HealthValidator;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
class OpenApiValidator {
|
|
2
|
+
/**
|
|
3
|
+
* Validates OpenAPI specification
|
|
4
|
+
* @param {object} spec - OpenAPI specification
|
|
5
|
+
* @returns {Promise<object>} Validation result
|
|
6
|
+
*/
|
|
7
|
+
async validate(spec) {
|
|
8
|
+
const result = {
|
|
9
|
+
valid: true,
|
|
10
|
+
errors: [],
|
|
11
|
+
warnings: [],
|
|
12
|
+
info: {}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Check for OpenAPI or Swagger version
|
|
17
|
+
if (!spec.openapi && !spec.swagger) {
|
|
18
|
+
result.valid = false;
|
|
19
|
+
result.errors.push('Invalid OpenAPI format: missing openapi or swagger version field');
|
|
20
|
+
} else {
|
|
21
|
+
result.info.version = spec.openapi || spec.swagger;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Validate info section
|
|
25
|
+
if (!spec.info) {
|
|
26
|
+
result.valid = false;
|
|
27
|
+
result.errors.push('Missing required "info" section in OpenAPI spec');
|
|
28
|
+
} else {
|
|
29
|
+
if (!spec.info.title) {
|
|
30
|
+
result.errors.push('Missing required "info.title" in OpenAPI spec');
|
|
31
|
+
}
|
|
32
|
+
if (!spec.info.version) {
|
|
33
|
+
result.errors.push('Missing required "info.version" in OpenAPI spec');
|
|
34
|
+
}
|
|
35
|
+
result.info.apiTitle = spec.info.title;
|
|
36
|
+
result.info.apiVersion = spec.info.version;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Validate paths
|
|
40
|
+
if (!spec.paths || Object.keys(spec.paths).length === 0) {
|
|
41
|
+
result.valid = false;
|
|
42
|
+
result.errors.push('OpenAPI spec must define at least one path');
|
|
43
|
+
} else {
|
|
44
|
+
result.info.pathCount = Object.keys(spec.paths).length;
|
|
45
|
+
const pathValidation = this.validatePaths(spec.paths);
|
|
46
|
+
result.errors.push(...pathValidation.errors);
|
|
47
|
+
result.warnings.push(...pathValidation.warnings);
|
|
48
|
+
result.info.operations = pathValidation.operations;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check for servers/host
|
|
52
|
+
if (spec.openapi && !spec.servers) {
|
|
53
|
+
result.warnings.push('OpenAPI 3.0 spec should define servers');
|
|
54
|
+
} else if (spec.swagger && !spec.host) {
|
|
55
|
+
result.warnings.push('Swagger 2.0 spec should define host');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (result.errors.length > 0) {
|
|
59
|
+
result.valid = false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
} catch (error) {
|
|
63
|
+
result.valid = false;
|
|
64
|
+
result.errors.push(`OpenAPI validation error: ${error.message}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validate paths section
|
|
72
|
+
* @param {object} paths - Paths object from OpenAPI spec
|
|
73
|
+
* @returns {object} Validation result
|
|
74
|
+
*/
|
|
75
|
+
validatePaths(paths) {
|
|
76
|
+
const result = {
|
|
77
|
+
errors: [],
|
|
78
|
+
warnings: [],
|
|
79
|
+
operations: 0
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const allowedMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
|
|
83
|
+
|
|
84
|
+
for (const [pathName, pathDef] of Object.entries(paths)) {
|
|
85
|
+
// Path must start with /
|
|
86
|
+
if (!pathName.startsWith('/')) {
|
|
87
|
+
result.errors.push(`Path "${pathName}" must start with /`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check for valid HTTP methods
|
|
91
|
+
const methods = Object.keys(pathDef).filter(m =>
|
|
92
|
+
allowedMethods.includes(m.toLowerCase())
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (methods.length === 0) {
|
|
96
|
+
result.errors.push(`Path "${pathName}" has no valid HTTP methods`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Validate each operation
|
|
100
|
+
for (const method of methods) {
|
|
101
|
+
result.operations++;
|
|
102
|
+
const operation = pathDef[method];
|
|
103
|
+
|
|
104
|
+
// Check for responses
|
|
105
|
+
if (!operation.responses) {
|
|
106
|
+
result.errors.push(`Operation ${method.toUpperCase()} ${pathName} missing responses`);
|
|
107
|
+
} else if (Object.keys(operation.responses).length === 0) {
|
|
108
|
+
result.errors.push(`Operation ${method.toUpperCase()} ${pathName} has empty responses`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check for summary or description
|
|
112
|
+
if (!operation.summary && !operation.description) {
|
|
113
|
+
result.warnings.push(`Operation ${method.toUpperCase()} ${pathName} should have summary or description`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check for operationId
|
|
117
|
+
if (!operation.operationId) {
|
|
118
|
+
result.warnings.push(`Operation ${method.toUpperCase()} ${pathName} should have operationId`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if spec is compatible with OpenAPI 3.0
|
|
128
|
+
* @param {object} spec - OpenAPI specification
|
|
129
|
+
* @returns {boolean} Compatibility check result
|
|
130
|
+
*/
|
|
131
|
+
isOpenApi3(spec) {
|
|
132
|
+
return spec.openapi && spec.openapi.startsWith('3.');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check if spec is compatible with Swagger 2.0
|
|
137
|
+
* @param {object} spec - OpenAPI specification
|
|
138
|
+
* @returns {boolean} Compatibility check result
|
|
139
|
+
*/
|
|
140
|
+
isSwagger2(spec) {
|
|
141
|
+
return spec.swagger && spec.swagger === '2.0';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = OpenApiValidator;
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
const ValidationCore = require('../../src/index');
|
|
2
|
+
|
|
3
|
+
describe('Validation Flow Component Tests', () => {
|
|
4
|
+
let validationCore;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
validationCore = new ValidationCore();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('Complete validation workflow', () => {
|
|
11
|
+
it('should perform full validation and generate token', async () => {
|
|
12
|
+
const serviceData = {
|
|
13
|
+
openApiSpec: {
|
|
14
|
+
openapi: '3.0.0',
|
|
15
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
16
|
+
paths: {
|
|
17
|
+
'/health': {
|
|
18
|
+
get: {
|
|
19
|
+
summary: 'Health check',
|
|
20
|
+
responses: { '200': { description: 'OK' } }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
endpoints: [
|
|
26
|
+
{ path: '/health', method: 'GET', handler: 'healthHandler' }
|
|
27
|
+
],
|
|
28
|
+
metadata: {
|
|
29
|
+
serviceName: 'test-service',
|
|
30
|
+
version: '1.0.0',
|
|
31
|
+
connectors: [
|
|
32
|
+
'connector-logger',
|
|
33
|
+
'connector-storage',
|
|
34
|
+
'connector-registry-client',
|
|
35
|
+
'connector-mq-client',
|
|
36
|
+
'connector-cookbook'
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
health: {
|
|
40
|
+
status: 'healthy',
|
|
41
|
+
uptime: 12345,
|
|
42
|
+
checks: {
|
|
43
|
+
database: 'ok',
|
|
44
|
+
redis: 'ok'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Mock validators to return success
|
|
50
|
+
jest.spyOn(validationCore.validators.openApi, 'validate').mockResolvedValue({
|
|
51
|
+
valid: true,
|
|
52
|
+
errors: []
|
|
53
|
+
});
|
|
54
|
+
jest.spyOn(validationCore.validators.endpoints, 'validate').mockResolvedValue({
|
|
55
|
+
valid: true,
|
|
56
|
+
errors: []
|
|
57
|
+
});
|
|
58
|
+
jest.spyOn(validationCore.validators.connectors, 'validate').mockResolvedValue({
|
|
59
|
+
valid: true,
|
|
60
|
+
warnings: []
|
|
61
|
+
});
|
|
62
|
+
jest.spyOn(validationCore.validators.health, 'validate').mockResolvedValue({
|
|
63
|
+
valid: true,
|
|
64
|
+
warnings: []
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Mock token generation
|
|
68
|
+
jest.spyOn(validationCore.tokenManager, 'generateToken').mockResolvedValue(
|
|
69
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test'
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Perform validation
|
|
73
|
+
const validationResults = await validationCore.validate(serviceData);
|
|
74
|
+
|
|
75
|
+
expect(validationResults.success).toBe(true);
|
|
76
|
+
expect(validationResults.validated).toBe(true);
|
|
77
|
+
expect(validationResults.checks).toHaveProperty('openApi');
|
|
78
|
+
expect(validationResults.checks).toHaveProperty('endpoints');
|
|
79
|
+
expect(validationResults.checks).toHaveProperty('connectors');
|
|
80
|
+
expect(validationResults.checks).toHaveProperty('health');
|
|
81
|
+
|
|
82
|
+
// Generate pre-validation token
|
|
83
|
+
const token = await validationCore.generatePreValidationToken(
|
|
84
|
+
validationResults,
|
|
85
|
+
serviceData.metadata.serviceName
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(token).toBeDefined();
|
|
89
|
+
expect(typeof token).toBe('string');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should handle partial validation with warnings', async () => {
|
|
93
|
+
const serviceData = {
|
|
94
|
+
metadata: {
|
|
95
|
+
serviceName: 'partial-service',
|
|
96
|
+
version: '1.0.0',
|
|
97
|
+
connectors: ['connector-logger'] // Missing some connectors
|
|
98
|
+
},
|
|
99
|
+
health: {
|
|
100
|
+
status: 'degraded',
|
|
101
|
+
uptime: 100
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Mock validators
|
|
106
|
+
jest.spyOn(validationCore.validators.connectors, 'validate').mockResolvedValue({
|
|
107
|
+
valid: false,
|
|
108
|
+
warnings: ['Missing required connector: connector-storage']
|
|
109
|
+
});
|
|
110
|
+
jest.spyOn(validationCore.validators.health, 'validate').mockResolvedValue({
|
|
111
|
+
valid: true,
|
|
112
|
+
warnings: ['Service status is degraded']
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const validationResults = await validationCore.validate(serviceData);
|
|
116
|
+
|
|
117
|
+
expect(validationResults.success).toBe(true); // Warnings don't fail validation in non-strict mode
|
|
118
|
+
expect(validationResults.warnings).toHaveLength(1);
|
|
119
|
+
expect(validationResults.warnings).toContain('Missing required connector: connector-storage');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should fail validation with errors in strict mode', async () => {
|
|
123
|
+
const strictCore = new ValidationCore({ strictMode: true });
|
|
124
|
+
|
|
125
|
+
const serviceData = {
|
|
126
|
+
metadata: {
|
|
127
|
+
serviceName: 'strict-service',
|
|
128
|
+
connectors: [] // No connectors
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
jest.spyOn(strictCore.validators.connectors, 'validate').mockResolvedValue({
|
|
133
|
+
valid: false,
|
|
134
|
+
warnings: ['No connectors found']
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const validationResults = await strictCore.validate(serviceData);
|
|
138
|
+
|
|
139
|
+
expect(validationResults.success).toBe(false);
|
|
140
|
+
expect(validationResults.errors).toContain('No connectors found');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('Certificate generation workflow', () => {
|
|
145
|
+
it('should validate and generate certificate', async () => {
|
|
146
|
+
const serviceData = {
|
|
147
|
+
metadata: {
|
|
148
|
+
serviceName: 'certified-service',
|
|
149
|
+
version: '2.0.0',
|
|
150
|
+
connectors: [
|
|
151
|
+
'connector-logger',
|
|
152
|
+
'connector-storage',
|
|
153
|
+
'connector-registry-client',
|
|
154
|
+
'connector-mq-client',
|
|
155
|
+
'connector-cookbook'
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Mock successful validation
|
|
161
|
+
jest.spyOn(validationCore.validators.connectors, 'validate').mockResolvedValue({
|
|
162
|
+
valid: true,
|
|
163
|
+
warnings: []
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Mock certificate generation
|
|
167
|
+
const mockCertificate = {
|
|
168
|
+
certificate: {
|
|
169
|
+
serviceName: 'certified-service',
|
|
170
|
+
version: '2.0.0',
|
|
171
|
+
issuedAt: new Date().toISOString(),
|
|
172
|
+
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
|
|
173
|
+
},
|
|
174
|
+
signature: 'mock-signature-xyz'
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
jest.spyOn(validationCore.certificateManager, 'generateCertificate')
|
|
178
|
+
.mockResolvedValue(mockCertificate);
|
|
179
|
+
|
|
180
|
+
// Validate
|
|
181
|
+
const validationResults = await validationCore.validate(serviceData);
|
|
182
|
+
expect(validationResults.success).toBe(true);
|
|
183
|
+
|
|
184
|
+
// Generate certificate
|
|
185
|
+
const certificate = await validationCore.generateCertificate(
|
|
186
|
+
validationResults,
|
|
187
|
+
serviceData.metadata.serviceName,
|
|
188
|
+
serviceData.metadata.version
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(certificate).toEqual(mockCertificate);
|
|
192
|
+
expect(certificate.certificate.serviceName).toBe('certified-service');
|
|
193
|
+
expect(certificate.certificate.version).toBe('2.0.0');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should verify certificate after generation', async () => {
|
|
197
|
+
const mockCertificate = {
|
|
198
|
+
certificate: {
|
|
199
|
+
serviceName: 'test-service',
|
|
200
|
+
version: '1.0.0',
|
|
201
|
+
issuedAt: new Date().toISOString()
|
|
202
|
+
},
|
|
203
|
+
signature: 'valid-signature'
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
jest.spyOn(validationCore.certificateManager, 'verifyCertificate')
|
|
207
|
+
.mockResolvedValue(true);
|
|
208
|
+
|
|
209
|
+
const isValid = await validationCore.verifyCertificate(mockCertificate);
|
|
210
|
+
expect(isValid).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('Token verification workflow', () => {
|
|
215
|
+
it('should verify and decode pre-validation token', async () => {
|
|
216
|
+
const mockTokenPayload = {
|
|
217
|
+
serviceName: 'test-service',
|
|
218
|
+
type: 'pre-validation',
|
|
219
|
+
validationResults: {
|
|
220
|
+
success: true,
|
|
221
|
+
validated: true
|
|
222
|
+
},
|
|
223
|
+
iat: Math.floor(Date.now() / 1000),
|
|
224
|
+
exp: Math.floor(Date.now() / 1000) + 86400 // 24 hours
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
jest.spyOn(validationCore.tokenManager, 'verifyToken')
|
|
228
|
+
.mockResolvedValue(mockTokenPayload);
|
|
229
|
+
|
|
230
|
+
const payload = await validationCore.verifyPreValidationToken('mock-token');
|
|
231
|
+
|
|
232
|
+
expect(payload).toEqual(mockTokenPayload);
|
|
233
|
+
expect(payload.serviceName).toBe('test-service');
|
|
234
|
+
expect(payload.type).toBe('pre-validation');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should handle invalid token', async () => {
|
|
238
|
+
jest.spyOn(validationCore.tokenManager, 'verifyToken')
|
|
239
|
+
.mockRejectedValue(new Error('Invalid token'));
|
|
240
|
+
|
|
241
|
+
await expect(
|
|
242
|
+
validationCore.verifyPreValidationToken('invalid-token')
|
|
243
|
+
).rejects.toThrow('Invalid token');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('Test generation from OpenAPI', () => {
|
|
248
|
+
it('should generate comprehensive test suite', async () => {
|
|
249
|
+
const openApiSpec = {
|
|
250
|
+
openapi: '3.0.0',
|
|
251
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
252
|
+
paths: {
|
|
253
|
+
'/users': {
|
|
254
|
+
get: {
|
|
255
|
+
summary: 'List users',
|
|
256
|
+
parameters: [
|
|
257
|
+
{ name: 'page', in: 'query', required: false },
|
|
258
|
+
{ name: 'limit', in: 'query', required: false }
|
|
259
|
+
],
|
|
260
|
+
responses: {
|
|
261
|
+
'200': { description: 'User list' },
|
|
262
|
+
'401': { description: 'Unauthorized' }
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
post: {
|
|
266
|
+
summary: 'Create user',
|
|
267
|
+
requestBody: {
|
|
268
|
+
required: true,
|
|
269
|
+
content: {
|
|
270
|
+
'application/json': {
|
|
271
|
+
schema: {
|
|
272
|
+
type: 'object',
|
|
273
|
+
required: ['name', 'email'],
|
|
274
|
+
properties: {
|
|
275
|
+
name: { type: 'string' },
|
|
276
|
+
email: { type: 'string', format: 'email' }
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
responses: {
|
|
283
|
+
'201': { description: 'User created' },
|
|
284
|
+
'400': { description: 'Bad request' }
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
'/users/{id}': {
|
|
289
|
+
get: {
|
|
290
|
+
summary: 'Get user',
|
|
291
|
+
parameters: [
|
|
292
|
+
{ name: 'id', in: 'path', required: true }
|
|
293
|
+
],
|
|
294
|
+
responses: {
|
|
295
|
+
'200': { description: 'User details' },
|
|
296
|
+
'404': { description: 'Not found' }
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
put: {
|
|
300
|
+
summary: 'Update user',
|
|
301
|
+
parameters: [
|
|
302
|
+
{ name: 'id', in: 'path', required: true }
|
|
303
|
+
],
|
|
304
|
+
requestBody: {
|
|
305
|
+
required: true,
|
|
306
|
+
content: {
|
|
307
|
+
'application/json': {
|
|
308
|
+
schema: { type: 'object' }
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
responses: {
|
|
313
|
+
'200': { description: 'Updated' }
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
delete: {
|
|
317
|
+
summary: 'Delete user',
|
|
318
|
+
parameters: [
|
|
319
|
+
{ name: 'id', in: 'path', required: true }
|
|
320
|
+
],
|
|
321
|
+
responses: {
|
|
322
|
+
'204': { description: 'Deleted' }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const tests = await validationCore.generateFunctionalTests(openApiSpec);
|
|
330
|
+
|
|
331
|
+
expect(tests).toHaveLength(5);
|
|
332
|
+
|
|
333
|
+
// Check GET /users test
|
|
334
|
+
const listUsersTest = tests.find(t => t.path === '/users' && t.method === 'GET');
|
|
335
|
+
expect(listUsersTest).toBeDefined();
|
|
336
|
+
expect(listUsersTest.parameters).toHaveLength(2);
|
|
337
|
+
expect(listUsersTest.expectedResponses).toContain('200');
|
|
338
|
+
expect(listUsersTest.expectedResponses).toContain('401');
|
|
339
|
+
|
|
340
|
+
// Check POST /users test
|
|
341
|
+
const createUserTest = tests.find(t => t.path === '/users' && t.method === 'POST');
|
|
342
|
+
expect(createUserTest).toBeDefined();
|
|
343
|
+
expect(createUserTest.requestBody).toBeDefined();
|
|
344
|
+
expect(createUserTest.expectedSuccessResponse).toBe('201');
|
|
345
|
+
|
|
346
|
+
// Check parameterized endpoints
|
|
347
|
+
const getUserTest = tests.find(t => t.path === '/users/{id}' && t.method === 'GET');
|
|
348
|
+
expect(getUserTest).toBeDefined();
|
|
349
|
+
expect(getUserTest.parameters).toHaveLength(1);
|
|
350
|
+
expect(getUserTest.parameters[0].name).toBe('id');
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
});
|