Skip to content

Commit

Permalink
Merge dev to main (#208)
Browse files Browse the repository at this point in the history
* fix: validating parameterized paths on the openapi is now supported
* fix: lazy loading the validation modules #203 (#206)
* feat: state helper (#207)
  • Loading branch information
shubhendumadhukar authored Nov 14, 2022
1 parent b2aa306 commit c57810f
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 19 deletions.
48 changes: 48 additions & 0 deletions docs/handlebars.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,54 @@ Content-Type: application/json

Enable injection if you understand the potential risks.

## state

Type: Custom Helper

Usage: State helper gets the mocked state value using a key send within the cookie header.
If no value is found it will use the default context within the block.

For example:
```json
{
"has_pending_order": {{#state key='has-pending-order'}}false{{/state}},
"cart": {{#state key='cart'}}[
{"id": 999, "name": "default prod"}
]{{/state}}
}
```

To set a value just send cookie with a specific prefix.

```js
const prefix = "mocked-state";
const key = "has-pending-order";
setCookie(`${prefix}-has-pending-order`, 'true');
setCookie(`${prefix}-cart`, '[{id: 1, name: "prod1"}, {id: 2, name: "prod2"}]');
```

!!! caution
the limit of cookie values in most browsers is 4KB

### Usage in Cypress

If you use Camouglage with [Cypress](https://www.cypress.io/) you could add the following custom command to make life easier.

```js
/**
* Custom cypress command to set a mocked state
*/
Cypress.Commands.add('setState', (key: string, value: unknown) => {
cy.setCookie(`mocked-state-${key}`, typeof value === 'string' ? value : JSON.stringify(value));
});
```

Then in your tests

```js
cy.setState('has_pending_order', true);
cy.setState('cart', [{id: 1, name: "prod1"}, {id: 2, name: "prod2"}]);
```

## Inbuilt Helpers

Expand Down
6 changes: 4 additions & 2 deletions mocks/hello-world/GET.mock
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ Response-Delay: {{num_between lower=500 upper=600}}
"greeting": "Hello {{capture from='query' key='name'}}",
"phone": {{randomValue length=10 type='NUMERIC'}},
"dateOfBirth": "{{now format='MM/DD/YYYY'}}",
"test": "{{randomValue}}"
"test": "{{randomValue}}",
"registered": {{#state key='registered'}}false{{/state}}
}
{{else}}
HTTP/1.1 200 OK
Expand All @@ -19,7 +20,8 @@ Content-Type: application/json
"greeting": "Hello World",
"phone": {{randomValue length=10 type='NUMERIC'}},
"dateOfBirth": "{{now format='MM/DD/YYYY'}}",
"test": "{{randomValue}}"
"test": "{{randomValue}}",
"registered": {{#state key='registered'}}true{{/state}}
}
{{/if}}

4 changes: 4 additions & 0 deletions mocks/pets/__/GET.mock
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
HTTP/1.1 200 OK
Content-Type: application/json

{ "id": 1, "name": "Rabbit" }
41 changes: 41 additions & 0 deletions src/handlebar/StateHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Defines and registers custom handlebar helper - state
*/
export class StateHelper {
private Handlebars: any;
constructor(Handlebars: any) {
this.Handlebars = Handlebars;
}
/**
* Registers code helper
*
* This helper gets the mocked state value using a key send within the cookie header.
* If no value is found it will use the default context within the block.
*
* For example:
* ```json
* {
* "has_pending_order": {{#state key='has-pending-order'}}false{{/state}}
* }
* ```
*
* To set a value just send cookie with a specific prefix .
*
* ```js
* const prefix = "mocked-state";
* const key = "has-pending-order";
* setCookie(`${prefix}-${key}`, "true");
* ```
*
* WARNING: the limit of cookie values in most browsers is 4KB
* @returns {void}
*/
register = () => {
this.Handlebars.registerHelper("state", (context: any) => {
const cookie = context.data.root.request.headers.cookie;
const key = context.hash.key;
const value = new RegExp(`mocked-state-${key}=([^;]+)`).exec(cookie);
return value ? value[1] : context.fn(this);
});
};
}
2 changes: 2 additions & 0 deletions src/handlebar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import * as Q from 'q';
import Handlebars from 'handlebars';
import { AssignHelper } from "./AssignHelper";
import { ConcatHelper } from "./ConcatHelper";
import { StateHelper } from "./StateHelper";
const HandlebarsPromised = promisedHandlebars(Handlebars, { Promise: Q.Promise })
/**
* Creates a instance of HandleBarHelper and register each custom helper
Expand Down Expand Up @@ -52,6 +53,7 @@ export const registerHandlebars = () => {
logger.warn("Code Injection is disabled. Helpers such as code, inject, pg, csv and functionalities such as external helpers, will not work.")
}
new ProxyHelper(HandlebarsPromised).register();
new StateHelper(HandlebarsPromised).register();
logger.info("Handlebar helpers registration completed");
};

Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ const start = (
configLoader.validateAndLoad();
setLoaderInstance(configLoader);
const config: CamouflageConfig = getLoaderInstance().getConfig();
if (config.validation && config.validation.enable) Validation.create(config.validation);
// Set log level to the configured level from config.yaml
setLogLevel(config.loglevel);
logger.debug(JSON.stringify(config, null, 2));
Expand Down
72 changes: 56 additions & 16 deletions src/validation/OpenApiAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,74 @@ import logger from "../logger";
import { ValidationAdapter, ValidationResult } from "./ValidationAdapter";
import type { HttpParserResponse } from "../parser/HttpParser";

const expressifyPath = (path: string) => {
/* eslint-disable */
const params = path.match(/(\{(?:\{.*\}|[^\{])*\})/g);
/* eslint-enable */
if (params?.length > 0) {
for (let x = 0; x < params.length; x++) {
const param = params[x];
path = path.replace(param, param.replace("{", ":").replace("}", ""));
}
}
return path;
};

export default class OpenApiAdapter extends ValidationAdapter {
private document: OpenAPI.Document;

private findRoute(req: Request) {
const { paths } = this.document;
const route = Object.entries(paths).find(([p]) => {
const expressPath = expressifyPath(p);
const route = Object.entries(paths).find(([p, v]) => {
const expressPath = this.expressifyPath(p);
v.url = p; // assign the url
const url = req.url.split("?")[0];
return (
expressPath === req.route.path ||
expressPath === url ||
expressPath.slice(0, -1) === url ||
expressPath === url.slice(0, -1)
expressPath === url.slice(0, -1) ||
this.urlMatchesParsedRoute(expressPath, url)
);
});
return route && route[1];
}

private urlMatchesParsedRoute(route: string, url: string) {
const routeParts = this.getParts(route);
const urlParts = this.getParts(url);
const parsedRoute = routeParts.map((part: string, x: number) => {
if (part.startsWith(":")) {
return urlParts[x];
}
return part;
});
const parsedUrl = `/${parsedRoute.join("/")}`;
if (parsedUrl === url) {
return true;
}
return false;
}

private extractPathParamsFromRequest(req: Request) {
const currentRoute = this.findRoute(req);
const routeParts = this.getParts(this.expressifyPath(currentRoute.url));
const urlParts = this.getParts(req.url.split("?")[0]);
const pathParams: { [k: string]: string } = {};
routeParts.forEach((part: string, x: number) => {
if (part.startsWith(":")) {
pathParams[part.slice(1)] = urlParts[x];
}
});
return pathParams;
}

private expressifyPath(path: string) {
/* eslint-disable */
const params = path.match(/(\{(?:\{.*\}|[^\{])*\})/g);
/* eslint-enable */
if (params?.length > 0) {
for (let x = 0; x < params.length; x++) {
const param = params[x];
path = path.replace(param, param.replace("{", ":").replace("}", ""));
}
}
return path;
}

private getParts(url: string) {
const parts = url.split("/");
parts.shift();
return parts;
}

async load(): Promise<void> {
const parser = new SwaggerParser();
try {
Expand Down Expand Up @@ -67,6 +104,9 @@ export default class OpenApiAdapter extends ValidationAdapter {
const requestValidator = new OpenAPIRequestValidator(
currentRoute[method]
);

req.params = this.extractPathParamsFromRequest(req);

const result = requestValidator.validateRequest(req);

if (result?.errors.length > 0) {
Expand Down
6 changes: 6 additions & 0 deletions src/validation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import {
ValidationConfig,
ValidationSchemaType,
ValidationSchema,
CamouflageConfig,
} from "../ConfigLoader/LoaderInterface";
import { HttpParserResponse } from "../parser/HttpParser";
import logger from "../logger";
import OpenApiAdapter from "./OpenApiAdapter";
import { ValidationResult, ValidationAdapter } from "./ValidationAdapter";
import { getLoaderInstance } from "../ConfigLoader";

export class Validation {
private static instance: Validation;
Expand All @@ -21,6 +23,10 @@ export class Validation {
}

public static getInstance(): Validation {
if (!Validation.instance) {
const config: CamouflageConfig = getLoaderInstance().getConfig();
return Validation.create(config.validation);
}
return Validation.instance;
}

Expand Down

0 comments on commit c57810f

Please sign in to comment.