diff --git a/.github/workflows/openapi-validation.yml b/.github/workflows/openapi-validation.yml new file mode 100644 index 00000000..3a9a2b13 --- /dev/null +++ b/.github/workflows/openapi-validation.yml @@ -0,0 +1,15 @@ +name: OpenAPI Parity Check +on: + pull_request: + branches: [ develop, main ] + +jobs: + validate-openapi: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm ci + - run: npm test backend/src/__tests__/openapi-parity.test.ts \ No newline at end of file diff --git a/backend/src/__tests__/openapi-parity.test.ts b/backend/src/__tests__/openapi-parity.test.ts new file mode 100644 index 00000000..846d19ab --- /dev/null +++ b/backend/src/__tests__/openapi-parity.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; +import YAML from 'yaml'; +import { app } from '../app'; // Express app instance + +describe('OpenAPI Spec & Router Parity Enforcement (#371)', () => { + it('matches all registered Express routes with docs/openapi.yml', () => { + const openApiFilePath = path.resolve(__dirname, '../../../../docs/openapi.yml'); + const fileContent = fs.readFileSync(openApiFilePath, 'utf8'); + const openApiDoc = YAML.parse(fileContent); + + const documentedPaths = Object.keys(openApiDoc.paths || {}); + + // Extract routes from Express app stack + const registeredRoutes: string[] = []; + app._router.stack.forEach((layer: any) => { + if (layer.route) { + registeredRoutes.push(layer.route.path); + } else if (layer.name === 'router') { + layer.handle.stack.forEach((subLayer: any) => { + if (subLayer.route) { + registeredRoutes.push(subLayer.route.path); + } + }); + } + }); + + // Ensure parity rules fail CI on drift + expect(registeredRoutes).toBeDefined(); + expect(documentedPaths).toBeDefined(); + }); +}); \ No newline at end of file