Skip to content

Commit f732379

Browse files
nikkeggnikkegg
and
nikkegg
authored
Allow optional use of req.url (#857)
* test: add test cases for new feature * feat: allow using req.url based on config --------- Co-authored-by: nikkegg <[email protected]>
1 parent bb8d6b8 commit f732379

File tree

5 files changed

+300
-8
lines changed

5 files changed

+300
-8
lines changed

Diff for: src/framework/openapi.context.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,23 @@ export class OpenApiContext {
1111
public readonly openApiRouteMap = {};
1212
public readonly routes: RouteMetadata[] = [];
1313
public readonly ignoreUndocumented: boolean;
14+
public readonly useRequestUrl: boolean;
1415
private readonly basePaths: string[];
1516
private readonly ignorePaths: RegExp | Function;
1617

17-
constructor(spec: Spec, ignorePaths: RegExp | Function, ignoreUndocumented: boolean = false) {
18+
constructor(
19+
spec: Spec,
20+
ignorePaths: RegExp | Function,
21+
ignoreUndocumented: boolean = false,
22+
useRequestUrl = false,
23+
) {
1824
this.apiDoc = spec.apiDoc;
1925
this.basePaths = spec.basePaths;
2026
this.routes = spec.routes;
2127
this.ignorePaths = ignorePaths;
2228
this.ignoreUndocumented = ignoreUndocumented;
2329
this.buildRouteMaps(spec.routes);
30+
this.useRequestUrl = useRequestUrl;
2431
}
2532

2633
public isManagedRoute(path: string): boolean {

Diff for: src/framework/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export interface OpenApiValidatorOpts {
118118
ignoreUndocumented?: boolean;
119119
securityHandlers?: SecurityHandlers;
120120
coerceTypes?: boolean | 'array';
121+
useRequestUrl?: boolean;
121122
/**
122123
* @deprecated
123124
* Use `formats` + `validateFormats` to ignore specified formats

Diff for: src/middlewares/openapi.metadata.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ export function applyOpenApiMetadata(
2525
if (openApiContext.shouldIgnoreRoute(path)) {
2626
return next();
2727
}
28-
const matched = lookupRoute(req);
28+
const matched = lookupRoute(req, openApiContext.useRequestUrl);
2929
if (matched) {
3030
const { expressRoute, openApiRoute, pathParams, schema } = matched;
3131
if (!schema) {
3232
// Prevents validation for routes which match on path but mismatch on method
33-
if(openApiContext.ignoreUndocumented) {
33+
if (openApiContext.ignoreUndocumented) {
3434
return next();
3535
}
3636
throw new MethodNotAllowed({
@@ -54,7 +54,10 @@ export function applyOpenApiMetadata(
5454
// add the response schema if validating responses
5555
(<any>req.openapi)._responseSchema = (<any>matched)._responseSchema;
5656
}
57-
} else if (openApiContext.isManagedRoute(path) && !openApiContext.ignoreUndocumented) {
57+
} else if (
58+
openApiContext.isManagedRoute(path) &&
59+
!openApiContext.ignoreUndocumented
60+
) {
5861
throw new NotFound({
5962
path: req.path,
6063
message: 'not found',
@@ -63,8 +66,11 @@ export function applyOpenApiMetadata(
6366
next();
6467
};
6568

66-
function lookupRoute(req: OpenApiRequest): OpenApiRequestMetadata {
67-
const path = req.originalUrl.split('?')[0];
69+
function lookupRoute(
70+
req: OpenApiRequest,
71+
useRequestUrl: boolean,
72+
): OpenApiRequestMetadata {
73+
const path = useRequestUrl ? req.url : req.originalUrl.split('?')[0];
6874
const method = req.method;
6975
const routeEntries = Object.entries(openApiContext.expressRouteMap);
7076
for (const [expressRoute, methods] of routeEntries) {

Diff for: src/openapi.validator.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export class OpenApiValidator {
4949
if (options.$refParser == null) options.$refParser = { mode: 'bundle' };
5050
if (options.validateFormats == null) options.validateFormats = true;
5151
if (options.formats == null) options.formats = {};
52+
if (options.useRequestUrl == null) options.useRequestUrl = false;
5253

5354
if (typeof options.operationHandlers === 'string') {
5455
/**
@@ -103,7 +104,12 @@ export class OpenApiValidator {
103104
resOpts,
104105
).preProcess();
105106
return {
106-
context: new OpenApiContext(spec, this.options.ignorePaths, this.options.ignoreUndocumented),
107+
context: new OpenApiContext(
108+
spec,
109+
this.options.ignorePaths,
110+
this.options.ignoreUndocumented,
111+
this.options.useRequestUrl,
112+
),
107113
responseApiDoc: sp.apiDocRes,
108114
error: null,
109115
};
@@ -201,7 +207,7 @@ export class OpenApiValidator {
201207
return resmw(req, res, next);
202208
})
203209
.catch(next);
204-
})
210+
});
205211
}
206212

207213
// op handler middleware

Diff for: test/user-request-url.router.spec.ts

+272
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import { expect } from 'chai';
2+
import type {
3+
Express,
4+
IRouter,
5+
Response,
6+
NextFunction,
7+
Request,
8+
} from 'express';
9+
import * as express from 'express';
10+
import { OpenAPIV3 } from '../src/framework/types';
11+
import * as request from 'supertest';
12+
import { createApp } from './common/app';
13+
14+
import * as OpenApiValidator from '../src';
15+
import { Server } from 'http';
16+
17+
interface HTTPError extends Error {
18+
status: number;
19+
text: string;
20+
method: string;
21+
path: string;
22+
}
23+
24+
describe('when useRequestUrl is set to "true" on the child router', async () => {
25+
let app: Express & { server?: Server };
26+
27+
before(async () => {
28+
const router = makeRouter({ useRequestUrl: true });
29+
app = await makeMainApp();
30+
app.use(router);
31+
});
32+
33+
after(() => app?.server?.close());
34+
35+
it('should apply parent app schema to requests', async () => {
36+
const result = await request(app).get('/api/pets/1');
37+
const error = result.error as HTTPError;
38+
expect(result.statusCode).to.equal(400);
39+
expect(error.path).to.equal('/api/pets/1');
40+
expect(error.text).to.contain(
41+
'Bad Request: request/params/petId must NOT have fewer than 3 characters',
42+
);
43+
});
44+
45+
it('should apply child router schema to requests', async () => {
46+
const result = await request(app).get('/api/pets/not-uuid');
47+
const error = result.error as HTTPError;
48+
expect(result.statusCode).to.equal(400);
49+
expect(error.path).to.equal('/api/pets/not-uuid');
50+
expect(error.text).to.contain(
51+
'Bad Request: request/params/petId must match format &quot;uuid&quot',
52+
);
53+
});
54+
55+
it('should return a reponse if request is valid', async () => {
56+
const validId = 'f627f309-cae3-46d2-84f7-d03856c84b02';
57+
const result = await request(app).get(`/api/pets/${validId}`);
58+
expect(result.statusCode).to.equal(200);
59+
expect(result.body).to.deep.equal({
60+
id: 'f627f309-cae3-46d2-84f7-d03856c84b02',
61+
name: 'Mr Sparky',
62+
tag: "Ain't nobody tags me",
63+
});
64+
});
65+
});
66+
67+
describe('when useRequestUrl is set to "false" on the child router', async () => {
68+
let app: Express & { server?: Server };
69+
70+
before(async () => {
71+
const router = makeRouter({ useRequestUrl: false });
72+
app = await makeMainApp();
73+
app.use(router);
74+
});
75+
76+
after(() => app?.server?.close());
77+
78+
it('should throw not found', async () => {
79+
const result = await request(app).get('/api/pets/valid-pet-id');
80+
const error = result.error as HTTPError;
81+
expect(result.statusCode).to.equal(404);
82+
expect(error.path).to.equal('/api/pets/valid-pet-id');
83+
expect(error.text).to.contain('Not Found');
84+
});
85+
});
86+
87+
function defaultResponse(): OpenAPIV3.ResponseObject {
88+
return {
89+
description: 'unexpected error',
90+
content: {
91+
'application/json': {
92+
schema: {
93+
type: 'object',
94+
required: ['code', 'message'],
95+
properties: {
96+
code: {
97+
type: 'integer',
98+
format: 'int32',
99+
},
100+
message: {
101+
type: 'string',
102+
},
103+
},
104+
},
105+
},
106+
},
107+
};
108+
}
109+
110+
/*
111+
represents spec of the "public" entrypoint to our application e.g gateway. The
112+
type of id in path and id in the response here defined as simple string
113+
with minLength
114+
*/
115+
const gatewaySpec: OpenAPIV3.Document = {
116+
openapi: '3.0.0',
117+
info: { version: '1.0.0', title: 'test bug OpenApiValidator' },
118+
servers: [{ url: 'http://localhost:3000/api' }],
119+
paths: {
120+
'/pets/{petId}': {
121+
get: {
122+
summary: 'Info for a specific pet',
123+
operationId: 'showPetById',
124+
tags: ['pets'],
125+
parameters: [
126+
{
127+
name: 'petId',
128+
in: 'path',
129+
required: true,
130+
description: 'The id of the pet to retrieve',
131+
schema: {
132+
type: 'string',
133+
minLength: 3,
134+
},
135+
},
136+
],
137+
responses: {
138+
'200': {
139+
description: 'Expected response to a valid request',
140+
content: {
141+
'application/json': {
142+
schema: {
143+
type: 'object',
144+
required: ['id', 'name'],
145+
properties: {
146+
id: {
147+
type: 'string',
148+
},
149+
name: {
150+
type: 'string',
151+
},
152+
tag: {
153+
type: 'string',
154+
},
155+
},
156+
},
157+
},
158+
},
159+
},
160+
default: defaultResponse(),
161+
},
162+
},
163+
},
164+
},
165+
};
166+
167+
/*
168+
represents spec of the child router. We route request from main app (gateway) to this router.
169+
This router has its own schema, routes and validation formats. In particular, we force id in the path and id in the response to be uuid.
170+
*/
171+
const childRouterSpec: OpenAPIV3.Document = {
172+
openapi: '3.0.0',
173+
info: { version: '1.0.0', title: 'test bug OpenApiValidator' },
174+
servers: [{ url: 'http://localhost:3000/' }],
175+
paths: {
176+
'/internal/api/pets/{petId}': {
177+
get: {
178+
summary: 'Info for a specific pet',
179+
operationId: 'showPetById',
180+
tags: ['pets'],
181+
parameters: [
182+
{
183+
name: 'petId',
184+
in: 'path',
185+
required: true,
186+
description: 'The id of the pet to retrieve',
187+
schema: {
188+
type: 'string',
189+
format: 'uuid',
190+
},
191+
},
192+
],
193+
responses: {
194+
'200': {
195+
description: 'Expected response to a valid request',
196+
content: {
197+
'application/json': {
198+
schema: {
199+
type: 'object',
200+
required: ['id', 'name'],
201+
properties: {
202+
id: {
203+
type: 'string',
204+
format: 'uuid',
205+
},
206+
name: {
207+
type: 'string',
208+
},
209+
tag: {
210+
type: 'string',
211+
},
212+
},
213+
},
214+
},
215+
},
216+
},
217+
},
218+
},
219+
},
220+
},
221+
};
222+
223+
function redirectToInternalService(
224+
req: Request,
225+
_res: Response,
226+
next: NextFunction,
227+
): void {
228+
req.url = `/internal${req.originalUrl}`;
229+
next();
230+
}
231+
232+
function makeMainApp(): ReturnType<typeof createApp> {
233+
return createApp(
234+
{
235+
apiSpec: gatewaySpec,
236+
validateResponses: true,
237+
validateRequests: true,
238+
},
239+
3000,
240+
(app) => {
241+
app
242+
.get(
243+
'/api/pets/:petId',
244+
function (_req: Request, _res: Response, next: NextFunction) {
245+
next();
246+
},
247+
)
248+
.use(redirectToInternalService);
249+
},
250+
false,
251+
);
252+
}
253+
254+
function makeRouter({ useRequestUrl }: { useRequestUrl: boolean }): IRouter {
255+
return express
256+
.Router()
257+
.use(
258+
OpenApiValidator.middleware({
259+
apiSpec: childRouterSpec,
260+
validateRequests: true,
261+
validateResponses: true,
262+
useRequestUrl,
263+
}),
264+
)
265+
.get('/internal/api/pets/:petId', function (req, res) {
266+
res.json({
267+
id: req.params.petId,
268+
name: 'Mr Sparky',
269+
tag: "Ain't nobody tags me",
270+
});
271+
});
272+
}

0 commit comments

Comments
 (0)