Skip to content

Commit

Permalink
Merge a3d07cd into c647e29
Browse files Browse the repository at this point in the history
  • Loading branch information
codeclown authored Jan 21, 2020
2 parents c647e29 + a3d07cd commit 80bb636
Show file tree
Hide file tree
Showing 10 changed files with 269 additions and 805 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"ajv": "^6.5.4",
"debug": "^4.1.0",
"lodash": "^4.17.11",
"path-to-regexp": "^2.2.1",
"semver": "^5.6.0"
},
"devDependencies": {
Expand All @@ -53,6 +54,7 @@
"@types/jest": "^23.3.7",
"@types/js-yaml": "^3.11.2",
"@types/lodash": "^4.14.117",
"@types/path-to-regexp": "^1.7.0",
"@types/semver": "^5.5.0",
"@types/supertest": "^2.0.6",
"cookie-parser": "^1.4.3",
Expand Down
25 changes: 24 additions & 1 deletion src/OpenApiValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import draft04Schema from "ajv/lib/refs/json-schema-draft-04.json";
// eslint-disable-next-line
import { RequestHandler } from "express";
import * as _ from "lodash";
import pathToRegexp from "path-to-regexp";
import * as semver from "semver";

import debug from "./debug";
Expand All @@ -29,7 +30,7 @@ import OpenApiDocument, {
SchemaObject,
} from "./OpenApiDocument";
import * as parameters from "./parameters";
import { mapOasSchemaToJsonSchema, resolveReference } from "./schema-utils";
import { mapOasSchemaToJsonSchema, oasPathToExpressPath, resolveReference } from "./schema-utils";
import ValidationError from "./ValidationError";
// tslint:disable-next-line ordered-imports
import Ajv = require("ajv");
Expand All @@ -54,6 +55,11 @@ export interface ValidatorConfig {
ajvOptions?: Ajv.Options;
}

export interface PathRegexpObject {
path: string;
regex: RegExp;
}

export default class OpenApiValidator {
private _ajv: Ajv.Ajv;
private _document: OpenApiDocument;
Expand Down Expand Up @@ -140,6 +146,23 @@ export default class OpenApiValidator {
return validate;
}

public match(): RequestHandler {
const paths: PathRegexpObject[] = _.keys(this._document.paths).map(path => ({
path,
regex: pathToRegexp(oasPathToExpressPath(path))
}));
const matchAndValidate: RequestHandler = (req, res, next) => {
const match = paths.find(({ regex }) => regex.test(req.path));
if (match) {
const method = req.method.toLowerCase() as Operation;
this.validate(method, match.path)(req, res, next);
} else {
next();
}
};
return matchAndValidate;
}

public validateResponse(method: Operation, path: string): (res: any) => void {
const operation = this._getOperationObject(method, path);
const validateResponse = (userResponse: any) => {
Expand Down
2 changes: 2 additions & 0 deletions src/schema-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,5 @@ export const mapOasSchemaToJsonSchema = (
(jsonSchema as any).$schema = "http://json-schema.org/draft-04/schema#";
return jsonSchema;
};

export const oasPathToExpressPath = (path: string): string => path.replace(/\{([^}]+)\}/g, ':$1');
37 changes: 37 additions & 0 deletions test/OpenApiValidator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
limitations under the License.
*/

import { Response } from "express";
import OpenApiDocument, { Operation } from "../src/OpenApiDocument";
import OpenApiValidator, { ValidatorConfig } from "../src/OpenApiValidator";
import * as parameters from "../src/parameters";
Expand Down Expand Up @@ -591,4 +592,40 @@ describe("OpenApiValidator", () => {
validateResponse({ ...baseRes, headers: { "x-hullo": "aa" } })
).toBeUndefined();
});

test("match() - finds and calls validate() based on request URL", async () => {
const validator = new OpenApiValidator(openApiDocument);
const match = validator.match();

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

const req = {
...baseReq,
method: "POST",
path: "/match",
body: { input: "Hello!" }
};

match(req, {} as Response, () => {});
expect(validateMock).toBeCalledWith("post", "/match");
expect(validateHandler).toBeCalled();
});

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

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

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

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

exports[`Integration tests with real app requests against /match are validated correctly 1`] = `
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",
},
}
`;
8 changes: 8 additions & 0 deletions test/integration/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ app.post("/echo", validator.validate("post", "/echo"), (req, res, _next) => {
res.json({ output: req.body.input });
});

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.get(
"/parameters",
validator.validate("get", "/parameters"),
Expand Down
34 changes: 34 additions & 0 deletions test/integration/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,40 @@ describe("Integration tests with real app", () => {
expect(validate(res)).toBeUndefined();
});

test("requests against /match are validated correctly", async () => {
const validate = validator.validateResponse("post", "/match");

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

res = await request(app)
.post("/match")
.send({ input: "Hello!" });
expect(res.status).toBe(200);
expect(res.body).toEqual({ output: "Hello!" });
expect(validate(res)).toBeUndefined();

res = await request(app)
.post("/match/works-with-url-param")
.send({ input: "Hello!" });
expect(res.status).toBe(200);
expect(res.body).toEqual({ output: "works-with-url-param" });
expect(validate(res)).toBeUndefined();
});

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

test("path parameters are validated", async () => {
let res = await request(app).get("/parameters/id/lol");
expect(res.status).toBe(400);
Expand Down
27 changes: 27 additions & 0 deletions test/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,33 @@ paths:
- output
default:
$ref: "#/components/responses/Error"
/match:
post:
description: Echo input back
requestBody:
content:
application/json:
schema:
type: object
properties:
input:
type: string
required:
- input
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
7 changes: 7 additions & 0 deletions test/schema-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
mapOasSchemaToJsonSchema,
resolveReference,
walkSchema,
oasPathToExpressPath,
} from "../src/schema-utils";
import openApiDocument from "./open-api-document";

Expand Down Expand Up @@ -169,4 +170,10 @@ describe("schema utils module", () => {
additionalProperties: false,
});
});

test("oasPathToExpressPath formats URL parameters for path-to-regexp", () => {
expect(oasPathToExpressPath('/foo')).toEqual('/foo');
expect(oasPathToExpressPath('/foo/{param}')).toEqual('/foo/:param');
expect(oasPathToExpressPath('/foo/{param}/bar')).toEqual('/foo/:param/bar');
});
});
Loading

0 comments on commit 80bb636

Please sign in to comment.