@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.
Files changed (35) hide show
  1. package/README.md +127 -0
  2. package/coverage/clover.xml +468 -0
  3. package/coverage/coverage-final.json +8 -0
  4. package/coverage/lcov-report/base.css +224 -0
  5. package/coverage/lcov-report/block-navigation.js +87 -0
  6. package/coverage/lcov-report/favicon.png +0 -0
  7. package/coverage/lcov-report/index.html +146 -0
  8. package/coverage/lcov-report/prettify.css +1 -0
  9. package/coverage/lcov-report/prettify.js +2 -0
  10. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  11. package/coverage/lcov-report/sorter.js +210 -0
  12. package/coverage/lcov-report/src/index.html +116 -0
  13. package/coverage/lcov-report/src/index.js.html +643 -0
  14. package/coverage/lcov-report/src/security/certificateManager.js.html +799 -0
  15. package/coverage/lcov-report/src/security/index.html +131 -0
  16. package/coverage/lcov-report/src/security/tokenManager.js.html +622 -0
  17. package/coverage/lcov-report/src/validators/connectorValidator.js.html +787 -0
  18. package/coverage/lcov-report/src/validators/endpointValidator.js.html +577 -0
  19. package/coverage/lcov-report/src/validators/healthValidator.js.html +655 -0
  20. package/coverage/lcov-report/src/validators/index.html +161 -0
  21. package/coverage/lcov-report/src/validators/openApiValidator.js.html +517 -0
  22. package/coverage/lcov.info +982 -0
  23. package/jest.config.js +21 -0
  24. package/package.json +31 -0
  25. package/src/index.js +212 -0
  26. package/src/security/ValidationProofVerifier.js +178 -0
  27. package/src/security/certificateManager.js +239 -0
  28. package/src/security/tokenManager.js +194 -0
  29. package/src/validators/connectorValidator.js +235 -0
  30. package/src/validators/endpointValidator.js +165 -0
  31. package/src/validators/healthValidator.js +191 -0
  32. package/src/validators/openApiValidator.js +145 -0
  33. package/test/component/validation-flow.test.js +353 -0
  34. package/test/integration/real-validation.test.js +548 -0
  35. 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
+ });