Skip to content

Commit

Permalink
Support OpenAPI 3.1 specs
Browse files Browse the repository at this point in the history
  • Loading branch information
myrotvorets-team committed Sep 29, 2023
1 parent 4e97d26 commit 0e2000a
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 51 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
dist/**
node_modules/**
*.cjs
*.mjs
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"sourceType": "module"
},
"extends": [
"@myrotvorets/myrotvorets-ts"
"@myrotvorets/myrotvorets-ts",
"plugin:mocha/recommended"
],
"env": {
"es2022": true,
Expand Down
3 changes: 1 addition & 2 deletions .mocharc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ module.exports = {
recursive: true,
spec: ['test/**/*.test.mts'],
'node-option': ['loader=ts-node/esm', 'no-warnings'],
// require: 'mocha.setup.mjs',
reporter: 'mocha-multi',
'reporter-option': [
'spec=-',
process.env.GITHUB_ACTIONS === 'true' ? 'mocha-reporter-gha=-' : null,
'mocha-reporter-sonarqube=test-report.xml'
process.env.SONARSCANNER === 'true' ? 'mocha-reporter-sonarqube=test-report.xml' : null,
].filter(Boolean),
}
20 changes: 20 additions & 0 deletions lib/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,32 @@ export async function installOpenApiValidator(
): Promise<void> {
const loader = new OpenApiSpecLoader({
apiDoc: specFile,
validateApiSpec: extraOptions.validateApiSpec ?? false,
$refParser: {
mode: 'dereference',
},
});

const spec = await loader.load();
if (spec.apiDoc.openapi.startsWith('3.1.') && extraOptions.validateApiSpec !== false) {
// express-openapi-validator does not support OpenAPI 3.1 yet - see https://github.com/cdimascio/express-openapi-validator/issues/755
// We make use of `links` in our OpenAPI spec, which is only supported in OpenAPI 3.1
spec.apiDoc.openapi = '3.0.3';

const { paths } = spec.apiDoc;
Object.keys(paths).forEach((name) => {
const path = paths[name];
delete (path.delete as Record<string, unknown> | undefined)?.links;
delete (path.get as Record<string, unknown> | undefined)?.links;
delete (path.head as Record<string, unknown> | undefined)?.links;
delete (path.options as Record<string, unknown> | undefined)?.links;
delete (path.patch as Record<string, unknown> | undefined)?.links;
delete (path.post as Record<string, unknown> | undefined)?.links;
delete (path.put as Record<string, unknown> | undefined)?.links;
delete (path.trace as Record<string, unknown> | undefined)?.links;
});
}

spec.apiDoc.servers = [{ url: '/' }];

const validator = middleware({
Expand Down
50 changes: 50 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"prepare": "npm run build",
"pretest": "npm run lint",
"test": "mocha",
"pretest:coverage": "npm run lint",
"test:coverage": "c8 mocha"
},
"files": [
Expand All @@ -29,6 +30,7 @@
"@types/supertest": "^2.0.10",
"c8": "^8.0.1",
"eslint-formatter-gha": "^1.2.0",
"eslint-plugin-mocha": "^10.2.0",
"express": "^4.17.1",
"express-openapi-validator": "^5.0.0",
"mocha": "^10.2.0",
Expand Down
116 changes: 68 additions & 48 deletions test/index.test.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, it } from 'mocha';
import express, { type Application, type NextFunction, type Request, type Response } from 'express';
import request from 'supertest';
import { installOpenApiValidator } from '../lib/index.mjs';
Expand Down Expand Up @@ -32,65 +31,86 @@ async function buildServer(install: boolean, env: string): Promise<Application>
return app;
}

describe('Without installOpenApiValidator', () => {
it('should return 200 for bad request', async (): Promise<unknown> => {
const server = await buildServer(false, '');
return request(server).get('/test').expect(200);
});
});

describe('With installOpenApiValidator', () => {
it('will not run security handlers', async (): Promise<unknown> => {
const server = await buildServer(true, 'test');
return request(server).get('/auth').expect(204);
describe('installOpenApiValidator', function () {
describe('Without installOpenApiValidator', function () {
it('should return 200 for bad request', async function (): Promise<unknown> {
const server = await buildServer(false, '');
return request(server).get('/test').expect(200);
});
});

describe('in test mode', () => {
// This one checks that `servers` section gets overwritten
// If `servers` is not overwritten, the URL won't match the base URL constraint
it('will validate all requests', async (): Promise<unknown> => {
describe('With installOpenApiValidator', function () {
it('will not run security handlers', async function (): Promise<unknown> {
const server = await buildServer(true, 'test');
return request(server)
.get('/test')
.expect(400)
.expect(/\/query\/s/u);
return request(server).get('/auth').expect(204);
});

it('will thoroughly validate requests', async (): Promise<unknown> => {
const server = await buildServer(true, 'test');
return request(server)
.get('/test?s=2012-13-31')
.expect(400)
.expect(/\/query\/s/u)
.expect(/must match format/u);
describe('in test mode', function () {
// This one checks that `servers` section gets overwritten
// If `servers` is not overwritten, the URL won't match the base URL constraint
it('will validate all requests', async function (): Promise<unknown> {
const server = await buildServer(true, 'test');
return request(server)
.get('/test')
.expect(400)
.expect(/\/query\/s/u);
});

it('will thoroughly validate requests', async function (): Promise<unknown> {
const server = await buildServer(true, 'test');
return request(server)
.get('/test?s=2012-13-31')
.expect(400)
.expect(/\/query\/s/u)
.expect(/must match format/u);
});

it('will validate responses', async function (): Promise<unknown> {
const server = await buildServer(true, 'test');
return request(server)
.get('/test?s=2012-12-31')
.expect(500)
.expect(/\/response\/debug/u);
});
});

it('will validate responses', async (): Promise<unknown> => {
const server = await buildServer(true, 'test');
return request(server)
.get('/test?s=2012-12-31')
.expect(500)
.expect(/\/response\/debug/u);
describe('in production mode', function () {
it('will validate all requests', async function (): Promise<unknown> {
const server = await buildServer(true, 'production');
return request(server)
.get('/test')
.expect(400)
.expect(/\/query\/s/u);
});

it('will not thoroughly validate requests', async function (): Promise<unknown> {
const server = await buildServer(true, 'production');
return request(server).get('/test?s=2012-13-31').expect(200);
});

it('will not validate responses', async function (): Promise<unknown> {
const server = await buildServer(true, 'production');
return request(server).get('/test?s=2012-12-31').expect(200);
});
});
});

describe('in production mode', () => {
it('will validate all requests', async (): Promise<unknown> => {
const server = await buildServer(true, 'production');
return request(server)
.get('/test')
.expect(400)
.expect(/\/query\/s/u);
});
describe('OpenAPI 3.1', function () {
async function buildApp(): Promise<Application> {
const app = express();
await installOpenApiValidator(
join(dirname(fileURLToPath(import.meta.url)), 'openapi-3.1.yaml'),
app,
'development',
);

it('will not thoroughly validate requests', async (): Promise<unknown> => {
const server = await buildServer(true, 'production');
return request(server).get('/test?s=2012-13-31').expect(200);
});
app.all('/test', (_req: Request, res: Response): unknown => res.status(204).end());
return app;
}

it('will not validate responses', async (): Promise<unknown> => {
const server = await buildServer(true, 'production');
return request(server).get('/test?s=2012-12-31').expect(200);
it('should load the spec', async function () {
const app = await buildApp();
return request(app).head('/test').expect(204);
});
});
});
Loading

0 comments on commit 0e2000a

Please sign in to comment.