diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 87560bd..372cb95 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -86,19 +86,25 @@ jobs: ## 🚀 swagger-coverage-cli v${{ env.NEW_VERSION }} ### ✨ New Features - - **🌐 Multi-Protocol Support**: Native support for REST (OpenAPI/Swagger), gRPC (Protocol Buffers), and GraphQL schemas - - **🔄 Mixed API Analysis**: Process multiple API specifications with different protocols in a single run - - **🎯 Protocol-Aware Matching**: Intelligent request matching tailored to each API protocol's characteristics - - **📊 Unified Reporting**: Generate consolidated HTML reports with protocol-specific insights and color coding - - **⚡ Universal CLI**: Single interface works across all supported protocols with consistent syntax + - **🛡️ Flexible Spec Validation**: New `--disable-spec-validation` flag to process specs with validation or reference issues ### 🎨 Enhanced Features - - **Smart Endpoint Mapping**: Intelligent endpoint matching with status code prioritization enabled by default - - **Enhanced Path Matching**: Improved handling of path parameters with different naming conventions - - **Confidence Scoring**: Match quality assessment with 0.0-1.0 confidence scores - - **Status Code Intelligence**: Prioritizes successful (2xx) codes over error codes for better coverage - - **Multi-API Support**: Process multiple API specifications in a single run - - **Enhanced HTML Reports**: Interactive reports with protocol identification and color coding + - **Legacy API Support**: Work with incomplete or invalid specs using `--disable-spec-validation` + + ### 🛡️ Spec Validation Control + + The new `--disable-spec-validation` flag allows you to analyze coverage even when specs have validation issues: + + ```bash + # Skip validation for specs with broken references or validation errors + swagger-coverage-cli api.yaml collection.json --disable-spec-validation + ``` + + **Use cases:** + - Legacy APIs with incomplete specifications + - Specs with external references that can't be resolved + - APIs in development where specs aren't fully complete + - Quick coverage checks without fixing all spec issues first ### 🎯 Protocol Support @@ -147,11 +153,12 @@ jobs: ``` ### 🧪 Quality Assurance - - **147 Tests**: Comprehensive test suite covering all protocols and scenarios - - **19 Test Suites**: Dedicated test coverage for each protocol and integration scenarios - - **Edge Case Coverage**: Robust handling of malformed URLs, missing data, and complex scenarios + - **183 Tests**: Comprehensive test suite covering all protocols and scenarios including spec validation control + - **22 Test Suites**: Dedicated test coverage for each protocol, integration scenarios, and validation features + - **Edge Case Coverage**: Robust handling of malformed URLs, missing data, broken references, and complex scenarios - **Performance Tested**: Validated with large datasets and mixed protocol specifications - **Protocol Isolation**: Each protocol's parsing and matching logic is independently tested + - **Validation Testing**: 16 new tests for `--disable-spec-validation` flag covering unit and CLI integration --- @@ -184,15 +191,5 @@ jobs: echo "- **GitHub Release:** [v${{ env.NEW_VERSION }}](https://github.com/${{ github.repository }}/releases/tag/v${{ env.NEW_VERSION }})" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### 🎯 Key Features" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Multi-protocol support (REST, gRPC, GraphQL)" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Protocol-aware matching logic" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Mixed API analysis in single run" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Smart endpoint mapping (enabled by default)" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Status code prioritization" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Enhanced path matching" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Confidence scoring" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Multi-API support" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Newman report support" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Enhanced HTML reports with protocol identification" >> $GITHUB_STEP_SUMMARY - echo "- ✅ YAML, JSON, CSV, .proto, .graphql support" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Flexible spec validation with --disable-spec-validation flag" >> $GITHUB_STEP_SUMMARY echo "- ✅ Backwards compatibility" >> $GITHUB_STEP_SUMMARY diff --git a/auto-detect-newman.html b/auto-detect-newman.html index d4d7d0e..7f9738b 100644 --- a/auto-detect-newman.html +++ b/auto-detect-newman.html @@ -384,7 +384,7 @@

Swagger Coverage Report

🔆
-

Timestamp: 9/18/2025, 2:32:58 PM

+

Timestamp: 10/9/2025, 8:30:30 AM

API Spec: Test API

Postman Collection: Test Newman Collection

diff --git a/cli.js b/cli.js index f877d4a..213d056 100755 --- a/cli.js +++ b/cli.js @@ -27,11 +27,12 @@ program .option("-v, --verbose", "Show verbose debug info") .option("--strict-query", "Enable strict validation of query parameters") .option("--strict-body", "Enable strict validation of requestBody (JSON)") + .option("--disable-spec-validation", "Disable OpenAPI/Swagger spec validation (useful for specs with validation or reference issues)") .option("--output ", "HTML report output file", "coverage-report.html") .option("--newman", "Treat input file as Newman run report instead of Postman collection") .action(async (apiFiles, postmanFile, options) => { try { - const { verbose, strictQuery, strictBody, output, newman } = options; + const { verbose, strictQuery, strictBody, output, newman, disableSpecValidation } = options; // Parse comma-separated API files const files = apiFiles.includes(',') ? @@ -80,7 +81,7 @@ program } } else { // Original OpenAPI/Swagger flow - const spec = await loadAndParseSpec(apiFile); + const spec = await loadAndParseSpec(apiFile, { disableValidation: disableSpecValidation }); specName = spec.info.title; protocol = 'rest'; if (verbose) { diff --git a/lib/swagger.js b/lib/swagger.js index 80e3b1f..396913f 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -10,7 +10,7 @@ const SwaggerParser = require('@apidevtools/swagger-parser'); /** * Загрузка и парсинг Swagger/OpenAPI (v2/v3) файла. */ -async function loadAndParseSpec(filePath) { +async function loadAndParseSpec(filePath, options = {}) { if (!fs.existsSync(filePath)) { throw new Error(`Spec file not found: ${filePath}`); } @@ -24,6 +24,11 @@ async function loadAndParseSpec(filePath) { doc = JSON.parse(raw); } + // If validation is disabled, just return the parsed document without validation + if (options.disableValidation) { + return doc; + } + // Валидируем и нормализуем через SwaggerParser const parsed = await SwaggerParser.validate(doc); return parsed; diff --git a/readme.md b/readme.md index ceea06a..fbc2d5a 100644 --- a/readme.md +++ b/readme.md @@ -113,6 +113,7 @@ swagger-coverage-cli "api.yaml,service.proto" collection.json --verbose --strict - **🏗️ Enterprise Ready**: Perfect for microservices architectures using diverse API protocols - **🎨 Smart Endpoint Mapping**: Intelligent endpoint matching with status code prioritization and enhanced path matching - **🔒 Strict Matching (Optional)**: Enforce strict checks for query parameters, request bodies, and more +- **🛡️ Flexible Validation**: Skip spec validation with `--disable-spec-validation` for legacy APIs or specs with reference issues - **📈 Enhanced HTML Reports**: Generates interactive `coverage-report.html` with protocol identification - **🧩 Extensible**: Modular code structure allows customization of matching logic and protocol support - **📋 CSV Support**: Flexible API documentation format for teams preferring spreadsheet-based docs @@ -260,6 +261,7 @@ npm swagger-coverage-cli "users-api.yaml,products-api.yaml" newman-report.json - - `--newman`: Treat input file as Newman run report instead of Postman collection. - `--strict-query`: Enforce strict checks on query parameters (e.g., required params, `enum`, `pattern`, etc.). - `--strict-body`: Verify that `application/json` request bodies in the spec match raw JSON bodies in Postman requests. +- `--disable-spec-validation`: Disable OpenAPI/Swagger spec validation (useful for specs with validation or reference issues). - `--output `: Customize the name of the HTML report file (default is `coverage-report.html`). ### Run via NPM Script @@ -539,6 +541,60 @@ Beyond basic percentage, consider these quality indicators: --- +## Handling Specs with Validation Issues + +Sometimes API specifications may have validation errors or broken references, especially in legacy systems or during development. The `--disable-spec-validation` flag allows you to analyze coverage even when specs have issues. + +### When to Use + +Use `--disable-spec-validation` when: +- Working with legacy APIs that have incomplete specifications +- Specs contain broken `$ref` references that can't be resolved +- External references aren't available in your CI/CD environment +- You need quick coverage analysis without fixing all spec issues first +- API specifications are still in development + +### Usage + +```bash +# Standard usage (validation enabled - will fail on invalid specs) +swagger-coverage-cli api.yaml collection.json + +# Disable validation for specs with issues +swagger-coverage-cli api.yaml collection.json --disable-spec-validation + +# Works with all other flags +swagger-coverage-cli api.yaml collection.json --disable-spec-validation --verbose --strict-body +``` + +### Example + +**Without the flag (validation enabled):** +```bash +$ swagger-coverage-cli broken-spec.yaml collection.json +Error: Token "NonExistentSchema" does not exist. +``` + +**With the flag (validation disabled):** +```bash +$ swagger-coverage-cli broken-spec.yaml collection.json --disable-spec-validation +=== Swagger Coverage Report === +Total operations in spec(s): 12 +Matched operations in Postman/Newman: 9 +Coverage: 75.00% + +HTML report saved to: coverage-report.html +``` + +### Important Notes + +- When validation is disabled, the tool parses the spec without validating references +- Coverage can still be calculated for the operations that are defined +- The tool won't catch structural issues in the spec +- Default behavior (with validation enabled) ensures spec quality + +--- + ## Detailed Matching Logic **swagger-coverage-cli** tries to match each **operation** from the spec with a **request** in Postman. An operation is considered **covered** if: diff --git a/test/disable-spec-validation-cli.test.js b/test/disable-spec-validation-cli.test.js new file mode 100644 index 0000000..7b6ba48 --- /dev/null +++ b/test/disable-spec-validation-cli.test.js @@ -0,0 +1,621 @@ +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +describe('CLI --disable-spec-validation Integration Tests', () => { + const cliPath = path.resolve(__dirname, '../cli.js'); + const fixturesDir = path.resolve(__dirname, 'fixtures'); + const invalidSpecPath = path.resolve(fixturesDir, 'cli-invalid-spec.yaml'); + const invalidJsonSpecPath = path.resolve(fixturesDir, 'cli-invalid-spec.json'); + const collectionPath = path.resolve(fixturesDir, 'simple-collection.json'); + const newmanReportPath = path.resolve(fixturesDir, 'simple-newman-report.json'); + const outputPath = path.resolve(__dirname, 'fixtures/test-disable-validation-output.html'); + const multiApiSpec1Path = path.resolve(fixturesDir, 'cli-invalid-spec-1.yaml'); + const multiApiSpec2Path = path.resolve(fixturesDir, 'cli-invalid-spec-2.yaml'); + + beforeAll(() => { + // Create an invalid YAML spec with broken references + const invalidSpec = `openapi: 3.0.0 +info: + title: Invalid Refs API + version: 1.0.0 +paths: + /users: + get: + summary: Get users + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/NonExistentSchema' + post: + summary: Create user + responses: + '201': + description: Created + /products: + get: + summary: Get products + responses: + '200': + description: Success +`; + fs.writeFileSync(invalidSpecPath, invalidSpec); + + // Create an invalid JSON spec with broken references + const invalidJsonSpec = { + openapi: '3.0.0', + info: { + title: 'Invalid JSON API', + version: '1.0.0' + }, + paths: { + '/orders': { + get: { + summary: 'Get orders', + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/MissingOrder' + } + } + } + } + } + } + } + } + }; + fs.writeFileSync(invalidJsonSpecPath, JSON.stringify(invalidJsonSpec, null, 2)); + + // Create a simple Postman collection + const collection = { + info: { name: 'Test Collection' }, + item: [ + { + name: 'Get Users', + request: { + method: 'GET', + url: { raw: 'https://api.example.com/users' } + } + }, + { + name: 'Get Products', + request: { + method: 'GET', + url: { raw: 'https://api.example.com/products' } + } + }, + { + name: 'Get Orders', + request: { + method: 'GET', + url: { raw: 'https://api.example.com/orders' } + } + } + ] + }; + fs.writeFileSync(collectionPath, JSON.stringify(collection, null, 2)); + + // Create a Newman report + const newmanReport = { + collection: { info: { name: 'Test Newman Collection' } }, + run: { + executions: [ + { + item: { name: 'Get Users' }, + request: { + method: 'GET', + url: { raw: 'https://api.example.com/users' } + }, + response: { + code: 200, + status: 'OK', + responseTime: 100 + } + }, + { + item: { name: 'Get Products' }, + request: { + method: 'GET', + url: { raw: 'https://api.example.com/products' } + }, + response: { + code: 200, + status: 'OK', + responseTime: 120 + } + } + ] + } + }; + fs.writeFileSync(newmanReportPath, JSON.stringify(newmanReport, null, 2)); + + // Create multi-API specs for testing + const multiSpec1 = `openapi: 3.0.0 +info: + title: Users API + version: 1.0.0 +paths: + /users: + get: + summary: Get users + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/MissingUsers' +`; + fs.writeFileSync(multiApiSpec1Path, multiSpec1); + + const multiSpec2 = `openapi: 3.0.0 +info: + title: Products API + version: 1.0.0 +paths: + /products: + get: + summary: Get products + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/MissingProducts' +`; + fs.writeFileSync(multiApiSpec2Path, multiSpec2); + }); + + afterAll(() => { + // Clean up test files + const testFiles = [ + invalidSpecPath, + invalidJsonSpecPath, + collectionPath, + newmanReportPath, + multiApiSpec1Path, + multiApiSpec2Path, + outputPath + ]; + testFiles.forEach(file => { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } + }); + }); + + describe('Basic validation behavior', () => { + test('CLI should fail with invalid spec when validation is enabled (default)', (done) => { + const child = spawn('node', [ + cliPath, + invalidSpecPath, + collectionPath, + '--output', outputPath + ]); + + let stderr = ''; + + child.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + child.on('close', (code) => { + try { + expect(code).toBe(1); // Should exit with error + expect(stderr).toContain('Error:'); + + done(); + } catch (error) { + done(error); + } + }); + + child.on('error', (error) => { + done(error); + }); + }, 15000); + + test('CLI should succeed with invalid spec when --disable-spec-validation is used', (done) => { + const child = spawn('node', [ + cliPath, + invalidSpecPath, + collectionPath, + '--disable-spec-validation', + '--output', outputPath, + '--verbose' + ]); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + child.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + child.on('close', (code) => { + try { + expect(code).toBe(0); // Should succeed + expect(stderr).toBe(''); + + // Should have coverage information + expect(stdout).toContain('Coverage:'); + expect(stdout).toContain('operations in spec'); + + // Check that HTML report was generated + expect(fs.existsSync(outputPath)).toBe(true); + const htmlContent = fs.readFileSync(outputPath, 'utf8'); + expect(htmlContent).toContain('Swagger Coverage Report'); + + done(); + } catch (error) { + done(error); + } + }); + + child.on('error', (error) => { + done(error); + }); + }, 15000); + + test('CLI should show coverage for matched operations even with invalid spec', (done) => { + const child = spawn('node', [ + cliPath, + invalidSpecPath, + collectionPath, + '--disable-spec-validation', + '--output', outputPath + ]); + + let stdout = ''; + + child.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + child.on('close', (code) => { + try { + expect(code).toBe(0); + + // Extract coverage information + const coverageMatch = stdout.match(/Coverage: ([\d.]+)%/); + expect(coverageMatch).toBeTruthy(); + + const coverage = parseFloat(coverageMatch[1]); + expect(coverage).toBeGreaterThanOrEqual(0); + expect(coverage).toBeLessThanOrEqual(100); + + // Should show matched operations + expect(stdout).toContain('Matched operations'); + + done(); + } catch (error) { + done(error); + } + }); + + child.on('error', (error) => { + done(error); + }); + }, 15000); + }); + + describe('JSON spec format', () => { + test('CLI should handle invalid JSON specs with --disable-spec-validation', (done) => { + const child = spawn('node', [ + cliPath, + invalidJsonSpecPath, + collectionPath, + '--disable-spec-validation', + '--output', outputPath + ]); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + child.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + child.on('close', (code) => { + try { + expect(code).toBe(0); + expect(stderr).toBe(''); + expect(stdout).toContain('Coverage:'); + + done(); + } catch (error) { + done(error); + } + }); + + child.on('error', (error) => { + done(error); + }); + }, 15000); + }); + + describe('Newman report support', () => { + test('CLI should work with Newman reports and --disable-spec-validation', (done) => { + const child = spawn('node', [ + cliPath, + invalidSpecPath, + newmanReportPath, + '--newman', + '--disable-spec-validation', + '--output', outputPath + ]); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + child.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + child.on('close', (code) => { + try { + expect(code).toBe(0); + expect(stderr).toBe(''); + expect(stdout).toContain('Coverage:'); + + // Verify Newman-specific output + const coverageMatch = stdout.match(/Coverage: ([\d.]+)%/); + expect(coverageMatch).toBeTruthy(); + + done(); + } catch (error) { + done(error); + } + }); + + child.on('error', (error) => { + done(error); + }); + }, 15000); + }); + + describe('Multi-API support', () => { + test('CLI should handle multiple invalid API specs with --disable-spec-validation', (done) => { + const child = spawn('node', [ + cliPath, + `${multiApiSpec1Path},${multiApiSpec2Path}`, + collectionPath, + '--disable-spec-validation', + '--output', outputPath + ]); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + child.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + child.on('close', (code) => { + try { + expect(code).toBe(0); + expect(stderr).toBe(''); + expect(stdout).toContain('Coverage:'); + expect(stdout).toContain('APIs analyzed:'); + + done(); + } catch (error) { + done(error); + } + }); + + child.on('error', (error) => { + done(error); + }); + }, 15000); + }); + + describe('Interaction with other flags', () => { + test('CLI should work with --disable-spec-validation and --strict-query', (done) => { + const child = spawn('node', [ + cliPath, + invalidSpecPath, + collectionPath, + '--disable-spec-validation', + '--strict-query', + '--output', outputPath + ]); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + child.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + child.on('close', (code) => { + try { + expect(code).toBe(0); + expect(stderr).toBe(''); + expect(stdout).toContain('Coverage:'); + + done(); + } catch (error) { + done(error); + } + }); + + child.on('error', (error) => { + done(error); + }); + }, 15000); + + test('CLI should work with --disable-spec-validation and --strict-body', (done) => { + const child = spawn('node', [ + cliPath, + invalidSpecPath, + collectionPath, + '--disable-spec-validation', + '--strict-body', + '--output', outputPath + ]); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + child.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + child.on('close', (code) => { + try { + expect(code).toBe(0); + expect(stderr).toBe(''); + expect(stdout).toContain('Coverage:'); + + done(); + } catch (error) { + done(error); + } + }); + + child.on('error', (error) => { + done(error); + }); + }, 15000); + + test('CLI should work with all flags combined', (done) => { + const child = spawn('node', [ + cliPath, + invalidSpecPath, + collectionPath, + '--disable-spec-validation', + '--strict-query', + '--strict-body', + '--verbose', + '--output', outputPath + ]); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + child.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + child.on('close', (code) => { + try { + expect(code).toBe(0); + expect(stderr).toBe(''); + expect(stdout).toContain('Coverage:'); + expect(stdout).toContain('OpenAPI specification loaded successfully'); + + done(); + } catch (error) { + done(error); + } + }); + + child.on('error', (error) => { + done(error); + }); + }, 15000); + }); + + describe('Output verification', () => { + test('HTML report should contain correct spec name for invalid spec', (done) => { + const child = spawn('node', [ + cliPath, + invalidSpecPath, + collectionPath, + '--disable-spec-validation', + '--output', outputPath + ]); + + child.on('close', (code) => { + try { + expect(code).toBe(0); + + const htmlContent = fs.readFileSync(outputPath, 'utf8'); + expect(htmlContent).toContain('Invalid Refs API'); + expect(htmlContent).toContain('Swagger Coverage Report'); + + done(); + } catch (error) { + done(error); + } + }); + + child.on('error', (error) => { + done(error); + }); + }, 15000); + + test('Console output should show correct metrics with invalid spec', (done) => { + const child = spawn('node', [ + cliPath, + invalidSpecPath, + collectionPath, + '--disable-spec-validation', + '--output', outputPath + ]); + + let stdout = ''; + + child.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + child.on('close', (code) => { + try { + expect(code).toBe(0); + + // Should show summary + expect(stdout).toContain('=== Swagger Coverage Report ==='); + expect(stdout).toContain('Total operations in spec'); + expect(stdout).toContain('Matched operations'); + expect(stdout).toContain('Coverage:'); + + done(); + } catch (error) { + done(error); + } + }); + + child.on('error', (error) => { + done(error); + }); + }, 15000); + }); +}); diff --git a/test/disable-spec-validation.test.js b/test/disable-spec-validation.test.js new file mode 100644 index 0000000..2628d98 --- /dev/null +++ b/test/disable-spec-validation.test.js @@ -0,0 +1,299 @@ +const { loadAndParseSpec, extractOperationsFromSpec } = require('../lib/swagger'); +const fs = require('fs'); +const path = require('path'); + +describe('Disable Spec Validation Feature', () => { + const fixturesDir = path.resolve(__dirname, 'fixtures'); + let invalidSpecPath; + let invalidJsonSpecPath; + let circularRefSpecPath; + let missingInfoSpecPath; + + beforeAll(() => { + // Ensure fixtures directory exists + if (!fs.existsSync(fixturesDir)) { + fs.mkdirSync(fixturesDir); + } + + // Create an invalid spec with broken references + invalidSpecPath = path.resolve(fixturesDir, 'invalid-refs-spec.yaml'); + const invalidSpec = `openapi: 3.0.0 +info: + title: Invalid Refs API + version: 1.0.0 +paths: + /users: + get: + summary: Get users + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/NonExistentSchema' + post: + summary: Create user + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AnotherMissingSchema' + responses: + '201': + description: Created + /products: + get: + summary: Get products + responses: + '200': + description: Success +components: + schemas: + User: + type: object + properties: + id: + type: integer +`; + fs.writeFileSync(invalidSpecPath, invalidSpec); + + // Create an invalid JSON spec with broken references + invalidJsonSpecPath = path.resolve(fixturesDir, 'invalid-refs-spec.json'); + const invalidJsonSpec = { + openapi: '3.0.0', + info: { + title: 'Invalid JSON Refs API', + version: '1.0.0' + }, + paths: { + '/orders': { + get: { + summary: 'Get orders', + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/NonExistentOrder' + } + } + } + } + } + } + } + } + }; + fs.writeFileSync(invalidJsonSpecPath, JSON.stringify(invalidJsonSpec, null, 2)); + + // Create a spec with circular references + circularRefSpecPath = path.resolve(fixturesDir, 'circular-ref-spec.yaml'); + const circularRefSpec = `openapi: 3.0.0 +info: + title: Circular Refs API + version: 1.0.0 +paths: + /nodes: + get: + summary: Get nodes + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/Node' +components: + schemas: + Node: + type: object + properties: + id: + type: string + children: + type: array + items: + $ref: '#/components/schemas/Node' +`; + fs.writeFileSync(circularRefSpecPath, circularRefSpec); + + // Create a spec missing required info + missingInfoSpecPath = path.resolve(fixturesDir, 'missing-info-spec.yaml'); + const missingInfoSpec = `openapi: 3.0.0 +paths: + /test: + get: + summary: Test + responses: + '200': + description: OK +`; + fs.writeFileSync(missingInfoSpecPath, missingInfoSpec); + }); + + afterAll(() => { + // Clean up test files + const testFiles = [invalidSpecPath, invalidJsonSpecPath, circularRefSpecPath, missingInfoSpecPath]; + testFiles.forEach(file => { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } + }); + }); + + describe('Basic functionality', () => { + test('should throw error when loading invalid spec with validation enabled (default)', async () => { + await expect(loadAndParseSpec(invalidSpecPath)) + .rejects + .toThrow(); + }); + + test('should successfully load invalid spec when validation is disabled', async () => { + const spec = await loadAndParseSpec(invalidSpecPath, { disableValidation: true }); + + // Verify that the spec was loaded + expect(spec).toBeDefined(); + expect(spec.info).toBeDefined(); + expect(spec.info.title).toBe('Invalid Refs API'); + expect(spec.paths).toBeDefined(); + expect(spec.paths['/users']).toBeDefined(); + expect(spec.paths['/products']).toBeDefined(); + }); + + test('should still work correctly with valid specs when validation is disabled', async () => { + const validSpecPath = path.resolve(fixturesDir, 'valid-spec-test.yaml'); + const validSpec = `openapi: 3.0.0 +info: + title: Valid API + version: 1.0.0 +paths: + /test: + get: + summary: Test endpoint + responses: + '200': + description: Success +`; + fs.writeFileSync(validSpecPath, validSpec); + + const spec = await loadAndParseSpec(validSpecPath, { disableValidation: true }); + + expect(spec).toBeDefined(); + expect(spec.info.title).toBe('Valid API'); + expect(spec.paths['/test']).toBeDefined(); + + // Clean up + fs.unlinkSync(validSpecPath); + }); + }); + + describe('JSON spec format', () => { + test('should handle invalid JSON specs when validation is disabled', async () => { + const spec = await loadAndParseSpec(invalidJsonSpecPath, { disableValidation: true }); + + expect(spec).toBeDefined(); + expect(spec.info.title).toBe('Invalid JSON Refs API'); + expect(spec.paths['/orders']).toBeDefined(); + }); + + test('should throw error for invalid JSON specs with validation enabled', async () => { + await expect(loadAndParseSpec(invalidJsonSpecPath)) + .rejects + .toThrow(); + }); + }); + + describe('Edge cases', () => { + test('should handle circular references when validation is disabled', async () => { + const spec = await loadAndParseSpec(circularRefSpecPath, { disableValidation: true }); + + expect(spec).toBeDefined(); + expect(spec.info.title).toBe('Circular Refs API'); + expect(spec.paths['/nodes']).toBeDefined(); + }); + + test('should handle specs with missing required fields when validation is disabled', async () => { + const spec = await loadAndParseSpec(missingInfoSpecPath, { disableValidation: true }); + + expect(spec).toBeDefined(); + expect(spec.paths['/test']).toBeDefined(); + }); + + test('should allow extractOperationsFromSpec to work with disabled validation', async () => { + const spec = await loadAndParseSpec(invalidSpecPath, { disableValidation: true }); + const operations = extractOperationsFromSpec(spec, false); + + expect(operations).toBeDefined(); + expect(operations.length).toBeGreaterThan(0); + expect(operations.some(op => op.path === '/users')).toBe(true); + expect(operations.some(op => op.path === '/products')).toBe(true); + }); + }); + + describe('Options parameter handling', () => { + test('should handle empty options object', async () => { + const validSpecPath = path.resolve(fixturesDir, 'temp-valid-spec.yaml'); + const validSpec = `openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /test: + get: + responses: + '200': + description: OK +`; + fs.writeFileSync(validSpecPath, validSpec); + + const spec = await loadAndParseSpec(validSpecPath, {}); + expect(spec).toBeDefined(); + + fs.unlinkSync(validSpecPath); + }); + + test('should handle undefined options parameter', async () => { + const validSpecPath = path.resolve(fixturesDir, 'temp-valid-spec2.yaml'); + const validSpec = `openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /test: + get: + responses: + '200': + description: OK +`; + fs.writeFileSync(validSpecPath, validSpec); + + const spec = await loadAndParseSpec(validSpecPath, undefined); + expect(spec).toBeDefined(); + + fs.unlinkSync(validSpecPath); + }); + + test('should handle null disableValidation value as false', async () => { + const validSpecPath = path.resolve(fixturesDir, 'temp-valid-spec3.yaml'); + const validSpec = `openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /test: + get: + responses: + '200': + description: OK +`; + fs.writeFileSync(validSpecPath, validSpec); + + const spec = await loadAndParseSpec(validSpecPath, { disableValidation: null }); + expect(spec).toBeDefined(); + + fs.unlinkSync(validSpecPath); + }); + }); +});