Skip to content

Commit

Permalink
Merge pull request #67 from aautio/master
Browse files Browse the repository at this point in the history
match() throws an error if no matching validator is found. Fixes #64.
  • Loading branch information
Hilzu committed Feb 22, 2021
2 parents 71b4af0 + dffa3ce commit 2793c89
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 20 deletions.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,18 +228,18 @@ Object][openapi-path-item-object]:
`RequestHandler` is an express middleware function with the signature
`(req: Request, res: Response, next: NextFunction): any;`.

#### `match(): RequestHandler`
#### `match(options: MatchOptions = { allowNoMatch: false }): RequestHandler`

Returns an express middleware function which calls `validate()` based on the
request method and path. Using this function removes the need to specify
`validate()` middleware for each express endpoint individually.

Note that behaviour is different to `validate()` for routes where no schema is
specified: `validate()` will throw an exception if no matching route
specification is found in the OpenAPI schema. `match()` will not throw an
exception in this case; the request is simply not validated. Be careful to
ensure you OpenAPI schema contains validators for each endpoint if using
`match()`.
`match()` throws an error if matching route specification is not found. This
ensures all requests are validated.

Use `match({ allowNoMatch: true})` if you want to skip validation for routes
that are not mentioned in the OpenAPI schema. Use with caution: It allows
requests to be handled without any validation.

The following examples achieve the same result:

Expand Down
16 changes: 14 additions & 2 deletions src/OpenApiValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export interface PathRegexpObject {
regex: RegExp;
}

export interface MatchOptions {
allowNoMatch?: boolean;
}

export default class OpenApiValidator {
private _ajv: Ajv.Ajv;

Expand Down Expand Up @@ -151,7 +155,9 @@ export default class OpenApiValidator {
return validate;
}

public match(): RequestHandler {
public match(
options: MatchOptions = { allowNoMatch: false },
): RequestHandler {
const paths: PathRegexpObject[] = _.keys(this._document.paths).map(
path => ({
path,
Expand All @@ -160,10 +166,16 @@ export default class OpenApiValidator {
);
const matchAndValidate: RequestHandler = (req, res, next) => {
const match = paths.find(({ regex }) => regex.test(req.path));
const method = req.method.toLowerCase() as Operation;
if (match) {
const method = req.method.toLowerCase() as Operation;
this.validate(method, match.path)(req, res, next);
} else if (!options.allowNoMatch) {
const err = new Error(
`Path=${req.path} with method=${method} not found from OpenAPI document`,
);
next(err);
} else {
// match not required
next();
}
};
Expand Down
29 changes: 26 additions & 3 deletions test/OpenApiValidator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,7 @@ describe("OpenApiValidator", () => {
expect(validateHandler).toBeCalled();
});

test("match() - does not call validate() if request does not match", async () => {
test("match() - does not call validate() if request does not match and yields error", async () => {
const validator = new OpenApiValidator(openApiDocument);
const match = validator.match();

Expand All @@ -626,11 +626,34 @@ describe("OpenApiValidator", () => {

const req = {
...baseReq,
method: "POST",
path: "/no-match",
};

// eslint-disable-next-line @typescript-eslint/no-empty-function
match(req, {} as Response, () => {});
const nextMock = jest.fn();

match(req, {} as Response, nextMock);
expect(validateMock).not.toHaveBeenCalled();
expect(nextMock).toHaveBeenCalledWith(expect.any(Error));
});

test("match({ allowNoMatch: true }) - does not call validate() if request does not match and does not yield error", async () => {
const validator = new OpenApiValidator(openApiDocument);
const match = validator.match({ allowNoMatch: true });

const validateMock = jest.fn();
validator.validate = validateMock;

const req = {
...baseReq,
method: "POST",
path: "/no-match",
};

const nextMock = jest.fn();

match(req, {} as Response, nextMock);
expect(validateMock).not.toHaveBeenCalled();
expect(nextMock).toHaveBeenCalledWith();
});
});
29 changes: 29 additions & 0 deletions test/integration/__snapshots__/integration.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,32 @@ Object {
},
}
`;

exports[`Integration tests with real app requests against /match are validated correctly 2`] = `
Object {
"error": Object {
"data": Array [
Object {
"dataPath": ".body",
"keyword": "required",
"message": "should have required property 'input'",
"params": Object {
"missingProperty": "input",
},
"schemaPath": "#/properties/body/required",
},
],
"message": "Error while validating request: request.body should have required property 'input'",
"name": "ValidationError",
},
}
`;

exports[`Integration tests with real app requests against /no-match cause an error 1`] = `
Object {
"error": Object {
"message": "Path=/no-match with method=post not found from OpenAPI document",
"name": "Error",
},
}
`;
8 changes: 3 additions & 5 deletions test/integration/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import cookieParser from "cookie-parser";
import express from "express";
import { OpenApiValidator } from "../../dist"; // eslint-disable-line
import { OpenApiValidator, ValidationError } from "../../dist"; // eslint-disable-line
import openApiDocument from "../open-api-document";

const app: express.Express = express();
Expand All @@ -34,9 +34,7 @@ app.post("/match/:optional?", validator.match(), (req, res, _next) => {
res.json({ output: req.params.optional || req.body.input });
});

app.post("/no-match", validator.match(), (req, res, _next) => {
res.json({ extra: req.body.anything });
});
app.post("/no-match", validator.match());

app.get(
"/parameters",
Expand Down Expand Up @@ -74,7 +72,7 @@ app.get(
);

const errorHandler: express.ErrorRequestHandler = (err, req, res, _next) => {
res.status(err.statusCode).json({
res.status(err instanceof ValidationError ? err.statusCode : 500).json({
error: {
name: err.name,
message: err.message,
Expand Down
15 changes: 12 additions & 3 deletions test/integration/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,23 @@ describe("Integration tests with real app", () => {
expect(res.status).toBe(200);
expect(res.body).toEqual({ output: "works-with-url-param" });
expect(validate(res)).toBeUndefined();

res = await request(app)
.post("/match/works-with-url-param")
.send({});
expect(validate(res)).toBeUndefined();
expect(res.status).toBe(400);
expect(res.body).toHaveProperty("error");
expect(res.body).toMatchSnapshot();
});

test("requests against /no-match are not validated", async () => {
test("requests against /no-match cause an error", async () => {
const res = await request(app)
.post("/no-match")
.send({ anything: "anything" });
expect(res.status).toBe(200);
expect(res.body).toEqual({ extra: "anything" });
expect(res.status).toBe(500);
expect(res.body).toHaveProperty("error");
expect(res.body).toMatchSnapshot();
});

test("path parameters are validated", async () => {
Expand Down
34 changes: 34 additions & 0 deletions test/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,40 @@ paths:
- output
default:
$ref: "#/components/responses/Error"
/match/{optional}:
post:
description: Echo url-param back
requestBody:
content:
application/json:
schema:
type: object
properties:
input:
type: string
required:
- input
parameters:
- name: optional
in: path
required: true
schema:
type: string
responses:
"200":
description: Response
content:
application/json:
schema:
type: object
properties:
output:
type: string
required:
- output
default:
$ref: "#/components/responses/Error"

/internal-ref:
post:
responses:
Expand Down

0 comments on commit 2793c89

Please sign in to comment.