Skip to content

Commit 766806b

Browse files
authored
Fixes for 881 - multiple specs w/validateRequests fail (#903)
1 parent 708f2f5 commit 766806b

File tree

10 files changed

+154
-13
lines changed

10 files changed

+154
-13
lines changed

examples/2-standard-multiple-api-specs/api.v2.yaml

+3-3
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,9 @@ components:
117117
schemas:
118118
Pet:
119119
required:
120-
- id
121-
- name
122-
- type
120+
- pet_id
121+
- pet_name
122+
- pet_type
123123
properties:
124124
pet_id:
125125
readOnly: true

src/framework/openapi.context.ts

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export class OpenApiContext {
1212
public readonly routes: RouteMetadata[] = [];
1313
public readonly ignoreUndocumented: boolean;
1414
public readonly useRequestUrl: boolean;
15+
public readonly serial: number;
1516
private readonly basePaths: string[];
1617
private readonly ignorePaths: RegExp | Function;
1718

@@ -28,6 +29,7 @@ export class OpenApiContext {
2829
this.ignoreUndocumented = ignoreUndocumented;
2930
this.buildRouteMaps(spec.routes);
3031
this.useRequestUrl = useRequestUrl;
32+
this.serial = spec.serial;
3133
}
3234

3335
public isManagedRoute(path: string): boolean {

src/framework/openapi.spec.loader.ts

+7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface Spec {
99
apiDoc: OpenAPIV3.Document;
1010
basePaths: string[];
1111
routes: RouteMetadata[];
12+
serial: number;
1213
}
1314

1415
export interface RouteMetadata {
@@ -23,6 +24,7 @@ interface DiscoveredRoutes {
2324
apiDoc: OpenAPIV3.Document;
2425
basePaths: string[];
2526
routes: RouteMetadata[];
27+
serial: number;
2628
}
2729
// Sort routes by most specific to least specific i.e. static routes before dynamic
2830
// e.g. /users/my_route before /users/{id}
@@ -33,6 +35,9 @@ export const sortRoutes = (r1, r2) => {
3335
return e1 > e2 ? 1 : -1;
3436
};
3537

38+
// Uniquely identify the Spec that is emitted
39+
let serial = 0;
40+
3641
export class OpenApiSpecLoader {
3742
private readonly framework: OpenAPIFramework;
3843
constructor(opts: OpenAPIFrameworkArgs) {
@@ -91,10 +96,12 @@ export class OpenApiSpecLoader {
9196

9297
routes.sort(sortRoutes);
9398

99+
serial = serial + 1;
94100
return {
95101
apiDoc,
96102
basePaths,
97103
routes,
104+
serial
98105
};
99106
}
100107

src/framework/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,7 @@ export interface OpenApiRequestMetadata {
497497
openApiRoute: string;
498498
pathParams: { [index: string]: string };
499499
schema: OpenAPIV3.OperationObject;
500+
serial: number;
500501
}
501502

502503
export interface OpenApiRequest extends Request {

src/middlewares/openapi.metadata.ts

+2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export function applyOpenApiMetadata(
4848
openApiRoute: openApiRoute,
4949
pathParams: pathParams,
5050
schema: schema,
51+
serial: openApiContext.serial,
5152
};
5253
req.params = pathParams;
5354
if (responseApiDoc) {
@@ -101,6 +102,7 @@ export function applyOpenApiMetadata(
101102
expressRoute,
102103
openApiRoute,
103104
pathParams,
105+
serial: -1,
104106
};
105107
(<any>r)._responseSchema = _schema;
106108
return r;

src/middlewares/openapi.response.validator.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,18 @@ export class ResponseValidator {
3232
[key: string]: { [key: string]: ValidateFunction };
3333
} = {};
3434
private eovOptions: ValidateResponseOpts;
35+
private serial: number;
3536

3637
constructor(
3738
openApiSpec: OpenAPIV3.Document,
3839
options: Options = {},
3940
eovOptions: ValidateResponseOpts = {},
41+
serial: number = -1,
4042
) {
4143
this.spec = openApiSpec;
4244
this.ajvBody = createResponseAjv(openApiSpec, options);
4345
this.eovOptions = eovOptions;
46+
this.serial = serial;
4447

4548
// This is a pseudo-middleware function. It doesn't get registered with
4649
// express via `use`
@@ -51,7 +54,7 @@ export class ResponseValidator {
5154

5255
public validate(): RequestHandler {
5356
return mung.json((body, req, res) => {
54-
if (req.openapi) {
57+
if (req.openapi && this.serial == req.openapi.serial) {
5558
const openapi = <OpenApiRequestMetadata>req.openapi;
5659
// instead of openapi.schema, use openapi._responseSchema to get the response copy
5760
const responses: OpenAPIV3.ResponsesObject = (<any>openapi)

src/openapi.validator.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ export {
3535
Forbidden,
3636
} from './framework/types';
3737

38+
interface MiddlewareContext {
39+
context: OpenApiContext,
40+
responseApiDoc: OpenAPIV3.Document,
41+
error: any,
42+
}
43+
3844
export class OpenApiValidator {
3945
readonly options: NormalizedOpenApiValidatorOpts;
4046
readonly ajvOpts: AjvOptions;
@@ -92,7 +98,7 @@ export class OpenApiValidator {
9298

9399
installMiddleware(spec: Promise<Spec>): OpenApiRequestHandler[] {
94100
const middlewares: OpenApiRequestHandler[] = [];
95-
const pContext = spec
101+
const pContext: Promise<MiddlewareContext> = spec
96102
.then((spec) => {
97103
const apiDoc = spec.apiDoc;
98104
const ajvOpts = this.ajvOpts.preprocessor;
@@ -183,7 +189,7 @@ export class OpenApiValidator {
183189
});
184190
}
185191

186-
// request middlweare
192+
// request middleware
187193
if (this.options.validateRequests) {
188194
let reqmw;
189195
middlewares.push(function requestMiddleware(req, res, next) {
@@ -201,8 +207,8 @@ export class OpenApiValidator {
201207
let resmw;
202208
middlewares.push(function responseMiddleware(req, res, next) {
203209
return pContext
204-
.then(({ responseApiDoc }) => {
205-
resmw = resmw || self.responseValidationMiddleware(responseApiDoc);
210+
.then(({ responseApiDoc, context: { serial } }) => {
211+
resmw = resmw || self.responseValidationMiddleware(responseApiDoc, serial);
206212
return resmw(req, res, next);
207213
})
208214
.catch(next);
@@ -288,12 +294,13 @@ export class OpenApiValidator {
288294
return (req, res, next) => requestValidator.validate(req, res, next);
289295
}
290296

291-
private responseValidationMiddleware(apiDoc: OpenAPIV3.Document) {
297+
private responseValidationMiddleware(apiDoc: OpenAPIV3.Document, serial: number) {
292298
return new middlewares.ResponseValidator(
293299
apiDoc,
294300
this.ajvOpts.response,
295301
// This has already been converted from boolean if required
296302
this.options.validateResponses as ValidateResponseOpts,
303+
serial
297304
).validate();
298305
}
299306

test/356.campaign.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as path from 'path';
1+
import * as path from 'path';
22
import * as express from 'express';
33
import * as request from 'supertest';
44
import { createApp } from './common/app';

test/881.spec.ts

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { expect } from 'chai';
2+
import * as request from 'supertest';
3+
4+
describe('multi-spec', () => {
5+
let app = null;
6+
7+
before(async () => {
8+
// Set up the express app
9+
app = createServer();
10+
});
11+
12+
after(() => {
13+
app.server.close();
14+
});
15+
16+
it('create campaign should return 200', async () =>
17+
request(app)
18+
.get(`/v1/pets`)
19+
.expect(400)
20+
.then((r) => {
21+
expect(r.body.message).include('limit');
22+
expect(r.body.message).include(`'type'`);
23+
}));
24+
25+
it('create campaign should return 200', async () =>
26+
request(app)
27+
.get(`/v2/pets`)
28+
.expect(400)
29+
.then((r) => {
30+
expect(r.body.message).include(`'pet_type'`);
31+
}));
32+
});
33+
34+
function createServer() {
35+
const express = require('express');
36+
const path = require('path');
37+
const bodyParser = require('body-parser');
38+
const http = require('http');
39+
const OpenApiValidator = require('../src');
40+
41+
const app = express();
42+
app.use(bodyParser.urlencoded({ extended: false }));
43+
app.use(bodyParser.text());
44+
app.use(bodyParser.json());
45+
46+
const versions = [1, 2];
47+
48+
for (const v of versions) {
49+
const apiSpec = path.join(__dirname, `api.v${v}.yaml`);
50+
app.use(
51+
OpenApiValidator.middleware({
52+
apiSpec,
53+
validateResponses: true,
54+
}),
55+
);
56+
57+
routes(app, v);
58+
}
59+
60+
const server = http.createServer(app);
61+
server.listen(3000);
62+
console.log('Listening on port 3000');
63+
64+
function routes(app, v) {
65+
if (v === 1) routesV1(app);
66+
if (v === 2) routesV2(app);
67+
}
68+
69+
function routesV1(app) {
70+
const v = '/v1';
71+
app.post(`${v}/pets`, (req, res, next) => {
72+
res.json({ ...req.body });
73+
});
74+
app.get(`${v}/pets`, (req, res, next) => {
75+
res.json([
76+
{
77+
id: 1,
78+
name: 'happy',
79+
type: 'cat',
80+
},
81+
]);
82+
});
83+
84+
app.use((err, req, res, next) => {
85+
// format error
86+
res.status(err.status || 500).json({
87+
message: err.message,
88+
errors: err.errors,
89+
});
90+
});
91+
}
92+
93+
function routesV2(app) {
94+
const v = '/v2';
95+
app.get(`${v}/pets`, (req, res, next) => {
96+
res.json([
97+
{
98+
pet_id: 1,
99+
pet_name: 'happy',
100+
pet_type: 'kitty',
101+
},
102+
]);
103+
});
104+
app.post(`${v}/pets`, (req, res, next) => {
105+
res.json({ ...req.body });
106+
});
107+
108+
app.use((err, req, res, next) => {
109+
// format error
110+
res.status(err.status || 500).json({
111+
message: err.message,
112+
errors: err.errors,
113+
});
114+
});
115+
}
116+
117+
app.server = server;
118+
return app;
119+
}

test/api.v2.yaml

+3-3
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,9 @@ components:
117117
schemas:
118118
Pet:
119119
required:
120-
- id
121-
- name
122-
- type
120+
- pet_id
121+
- pet_name
122+
- pet_type
123123
properties:
124124
pet_id:
125125
readOnly: true

0 commit comments

Comments
 (0)