Skip to content

Commit 6cb9e05

Browse files
committed
[ENGG-2064] - Merge branch 'master' on top of existing log-poc
2 parents 1623e2c + 63dd75a commit 6cb9e05

File tree

13 files changed

+671
-242
lines changed

13 files changed

+671
-242
lines changed

package-lock.json

Lines changed: 474 additions & 165 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
{
22
"name": "@requestly/mock-server",
3-
"version": "0.1.6",
3+
"version": "0.2.13",
44
"description": "- Methods: GET, POST, PUT, OPTIONS - Description - Endpoint (can be full path) (/api/v1/users) - Multiple Responses - Shuffle Response - Sequential Response - Rules in Response - Status (Any status code 2xx, 4xx) - Latency - Body - Templating - Faker js - Headers",
55
"main": "build/index.js",
66
"scripts": {
77
"test": "echo \"Error: no test specified\" && exit 1",
88
"start": "npm run build && node build/test/index.js",
99
"start:dev": "npx nodemon",
10-
"build": "rimraf ./build && tsc"
10+
"build": "rimraf ./build && tsc",
11+
"prepublish": "npm run build"
1112
},
1213
"author": "",
1314
"license": "ISC",
@@ -16,7 +17,7 @@
1617
"@types/express": "^4.17.14",
1718
"@types/node": "^18.11.11",
1819
"@types/path-to-regexp": "^1.7.0",
19-
"nodemon": "^2.0.20",
20+
"nodemon": "^3.1.0",
2021
"rimraf": "^3.0.2",
2122
"ts-node": "^10.9.1",
2223
"typescript": "^4.9.4"
@@ -25,6 +26,7 @@
2526
"@types/har-format": "^1.2.14",
2627
"cors": "^2.8.5",
2728
"express": "^4.18.2",
29+
"handlebars": "^4.7.8",
2830
"path-to-regexp": "^0.1.7"
2931
},
3032
"files": [

src/core/common/mockHandler.ts

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,18 @@ class MockServerHandler {
1212
const method = req.method as RequestMethod;
1313
const queryParams = req.query || {};
1414

15-
endpoint = MockServerHandler.cleanupEndpoint(endpoint);
16-
1715
const kwargs = {
1816
queryParams: queryParams
1917
}
2018

2119
const mockData = await MockSelector.selectMock(endpoint, method, kwargs);
2220

2321
if(mockData) {
24-
console.debug("[Debug] Mock Selected with data", mockData);
22+
// console.debug("[Debug] Mock Selected with data", mockData);
2523
const mockResponse: MockServerResponse = await MockProcessor.process(
2624
mockData,
27-
{
28-
endpoint,
29-
method,
30-
password: queryParams[RQ_PASSWORD] as string
31-
}
25+
req,
26+
queryParams[RQ_PASSWORD] as string,
3227
);
3328
return {
3429
...mockResponse,
@@ -39,18 +34,6 @@ class MockServerHandler {
3934
console.debug("[Debug] No Mock Selected");
4035
return getServerMockResponse(HttpStatusCode.NOT_FOUND);
4136
}
42-
43-
static cleanupEndpoint = (endpoint: string): string => {
44-
// Stripping front slash. Eg: /users/123/ -> users/123/
45-
endpoint = endpoint.slice(1);
46-
47-
// Stripping end slash. Eg: users/123/ -> users/123
48-
if(endpoint.slice(-1) === "/") {
49-
endpoint = endpoint.slice(0, -1);
50-
}
51-
52-
return endpoint
53-
}
5437
}
5538

5639
export default MockServerHandler;

src/core/common/mockProcessor.ts

Lines changed: 67 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,85 @@
1+
import { Request } from "express";
12
import { HttpStatusCode } from "../../enums/mockServerResponse";
23
import { MockServerResponse, RequestMethod } from "../../types";
4+
import { MockContextParams } from "../../types/internal";
35
import { Mock, MockMetadata, Response } from "../../types/mock";
46
import { validatePassword } from "../utils/mockProcessor";
57
import { getServerMockResponse } from "../utils/mockServerResponseHelper";
68
import pathMatcher from "../utils/pathMatcher";
9+
import { renderTemplate } from "../utils/templating";
710

811
class MockProcessor {
9-
static process = async (mockData: Mock, requestContextParams: Partial<MockMetadata>): Promise<MockServerResponse> => {
10-
const { endpoint, method, password } = requestContextParams;
12+
static process = async (
13+
mockData: Mock,
14+
request: Request,
15+
password?: string
16+
): Promise<MockServerResponse> => {
17+
if (!validatePassword(mockData.password, password)) {
18+
return getServerMockResponse(HttpStatusCode.UNAUTHORIZED);
19+
}
1120

12-
if(!validatePassword(mockData.password, password)) {
13-
return getServerMockResponse(HttpStatusCode.UNAUTHORIZED);
14-
}
21+
return this.renderMockServerResponse(mockData, request);
22+
};
1523

16-
const urlParams = pathMatcher(mockData.endpoint, endpoint!).params || {}
17-
return this.renderMockServerResponse(mockData);
18-
}
24+
static renderMockServerResponse = async (
25+
mockData: Mock,
26+
request: Request,
27+
): Promise<MockServerResponse> => {
28+
// TODO: Right now we select only first response.
29+
// In future this needs to be selected on the basis of rules
30+
const responseTemplate: Response = mockData.responses[0];
1931

20-
static renderMockServerResponse = async (mockData: Mock): Promise<MockServerResponse> => {
21-
// TODO: Right now we select only first response.
22-
// In future this needs to be selected on the basis of rules
23-
const responseTemplate: Response = mockData.responses[0]
24-
25-
const mockServerResponse: MockServerResponse = {
26-
statusCode: this.renderStatusCode(responseTemplate),
27-
headers: this.renderHeaders(responseTemplate),
28-
body: this.renderBody(responseTemplate),
29-
};
32+
const urlParams = pathMatcher(mockData.endpoint, request.path).params || {};
33+
const contextParams: MockContextParams = {
34+
method: request.method as RequestMethod,
35+
statusCode: responseTemplate.statusCode,
36+
urlParams,
37+
headers: request.headers as Record<string, string> || {},
38+
};
39+
40+
console.log({ contextParams });
3041

31-
await this.addDelay(responseTemplate.latency);
32-
return mockServerResponse;
33-
}
42+
const mockServerResponse: MockServerResponse = {
43+
statusCode: this.renderStatusCode(responseTemplate),
44+
headers: this.renderHeaders(responseTemplate),
45+
body: this.renderBody(responseTemplate, contextParams),
46+
};
3447

35-
static renderStatusCode = (responseTemplate: Response) => {
36-
return responseTemplate.statusCode;
37-
}
48+
await this.addDelay(responseTemplate.latency);
49+
return mockServerResponse;
50+
};
3851

39-
// TODO: Pass extra params here required for rendering
40-
// TODO: Do rendering of header here
41-
static renderHeaders = (responseTemplate: Response) => {
42-
const headers: any = {};
43-
Object.keys(responseTemplate.headers).map(key => {
44-
headers[key] = responseTemplate.headers[key];
45-
})
46-
return headers;
47-
}
48-
49-
// TODO: Pass extra params here required for rendering
50-
// TODO: Do template rendering here
51-
static renderBody = (responseTemplate: Response) => {
52-
let finalBody = null;
53-
finalBody = responseTemplate.body;
54-
return finalBody;
55-
}
52+
static renderStatusCode = (responseTemplate: Response) => {
53+
return responseTemplate.statusCode;
54+
};
5655

57-
// Time in ms
58-
// TODO: Write logic for delay here
59-
static addDelay = async (delay: number = 0) => {
60-
console.debug(`[Debug] Adding delay of ${delay}`);
61-
return new Promise(resolve => setTimeout(resolve, delay));
62-
}
56+
// TODO: Pass extra params here required for rendering
57+
// TODO: Do rendering of header here
58+
static renderHeaders = (responseTemplate: Response) => {
59+
const headers: any = {};
60+
Object.keys(responseTemplate.headers).map((key) => {
61+
headers[key] = responseTemplate.headers[key];
62+
});
63+
return headers;
64+
};
65+
66+
// TODO: Pass extra params here required for rendering
67+
static renderBody = (
68+
responseTemplate: Response,
69+
mockContextParams: MockContextParams
70+
) => {
71+
let finalBody = null;
72+
let bodyTemplate: string = responseTemplate.body;
73+
finalBody = renderTemplate(bodyTemplate, mockContextParams);
74+
return finalBody;
75+
};
76+
77+
// Time in ms
78+
// TODO: Write logic for delay here
79+
static addDelay = async (delay: number = 0) => {
80+
console.debug(`[Debug] Adding delay of ${delay}`);
81+
return new Promise((resolve) => setTimeout(resolve, delay));
82+
};
6383
}
6484

6585
export default MockProcessor;

src/core/server.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import storageService from "../services/storageService";
77
import { MockServerResponse } from "../types";
88
import ILogSink from "../interfaces/logSinkInterface";
99
import { HarMiddleware } from "../middlewares/har";
10+
import { cleanupPath } from "./utils";
1011

1112
interface MockServerConfig {
1213
port: number;
@@ -75,10 +76,10 @@ class MockServer {
7576
if(req.path.indexOf(this.config.pathPrefix) === 0) {
7677
console.log(`Stripping pathPrefix: ${this.config.pathPrefix}`);
7778
Object.defineProperty(req, 'path', {
78-
value: req.path.slice(this.config.pathPrefix.length),
79+
value: cleanupPath(req.path.slice(this.config.pathPrefix.length)),
7980
writable: true
8081
});
81-
console.log(`Path after stripping prefix: ${req.path}`);
82+
console.log(`Path after stripping prefix and cleanup: ${req.path}`);
8283
}
8384

8485
const mockResponse: MockServerResponse = await MockServerHandler.handleEndpoint(req);

src/core/utils/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const cleanupPath = (path: string): string => {
2+
// Stripping front slash. Eg: /users/123/ -> users/123/
3+
path = path.slice(1);
4+
5+
// Stripping end slash. Eg: users/123/ -> users/123
6+
if(path.slice(-1) === "/") {
7+
path = path.slice(0, -1);
8+
}
9+
10+
return path
11+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { MockContextParams } from "../../../../types/internal";
2+
3+
const requestHelpers = (params: MockContextParams) => {
4+
const helpers = {
5+
urlParam: (param: string) => params.urlParams[param],
6+
method: () => params.method,
7+
statusCode: () => params.statusCode,
8+
header: (param: string, defaultValue: string = '') => {
9+
// handlebars passes object when no value is passed
10+
// {
11+
// lookupProperty: [Function: lookupProperty],
12+
// name: 'header',
13+
// hash: {},
14+
// data: { root: [Object] },
15+
// loc: { start: [Object], end: [Object] }
16+
// }
17+
if(typeof defaultValue === 'object') {
18+
defaultValue = '';
19+
}
20+
21+
if(typeof param === 'object') {
22+
return defaultValue
23+
}
24+
25+
return params.headers[param?.toLowerCase()] || defaultValue;
26+
},
27+
};
28+
return helpers;
29+
};
30+
31+
export default requestHelpers;

src/core/utils/templating/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { compile } from "handlebars";
2+
3+
import requestHelpers from "./helpers/requestHelpers";
4+
import { MockContextParams } from "../../../types/internal";
5+
import { wrapUnexpectedTemplateCaptures } from "./utils";
6+
7+
8+
export const renderTemplate = (template: string, params: MockContextParams): string => {
9+
const allHelpers = {...requestHelpers(params)}
10+
const wrappedTemplate = wrapUnexpectedTemplateCaptures(template, allHelpers);
11+
const hbsTemplate = compile(wrappedTemplate);
12+
return hbsTemplate(params, {
13+
helpers: allHelpers
14+
});
15+
};

src/core/utils/templating/utils.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* if match is a string like {{unknownHelper arg1 arg2}}
3+
* it will add `\\` to its prefix, signaling handlebars should ignore it
4+
*
5+
* https://handlebarsjs.com/guide/expressions.html#escaping-handlebars-expressions
6+
*/
7+
function escapeMatchFromHandlebars(match: string) {
8+
return match.replace(/({{)/g, '\\$1');
9+
}
10+
11+
export function wrapUnexpectedTemplateCaptures(template: string, allHelpers: Record<string, unknown>) {
12+
const helperNames = Object.keys(allHelpers)
13+
return template.replace(/{{\s*([\s\S]*?)\s*}}/g, (completeMatch, firstMatchedGroup) => {
14+
const isMatchEmpty = firstMatchedGroup.trim() === ''; // {{}}
15+
const matchStartsWithKnownHelper = helperNames.some(helperName => firstMatchedGroup.includes(helperName));
16+
17+
if(isMatchEmpty || !matchStartsWithKnownHelper) return escapeMatchFromHandlebars(completeMatch);
18+
else return completeMatch;
19+
});
20+
}

src/test/dummy/mock1.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@ import { Mock } from "../../types/mock";
44
export const dummyMock1: Mock = {
55
id: "1",
66
desc: "Mock 1",
7-
method: RequestMethod.GET,
7+
method: RequestMethod.POST,
88
endpoint: "abcd/:userId/:name",
99
responses: [
1010
{
1111
id: "1",
1212
desc: "Mock 1 Response 1",
1313
latency: 1000,
14-
statusCode: 404,
14+
statusCode: 201,
1515
headers:{
1616
"foo": "bar",
1717
"content-type": "application/json"
1818
},
19-
body: "{\"Hello\":\"There\",\"mockId\":\"1\"}"
19+
body: "{\"Hello\":\"There\",\"mockId\":\"1\", \"statusCode\": {{ statusCode }}, \"method\": \"{{ method }}\", \"urlParams\": \"{{ urlParam 'userId' }}\", \"header\": \"{{ header 'userid' 'test' }}\" }}"
2020
}
2121
]
2222
}
@@ -62,6 +62,27 @@ export const dummyMock3: Mock = {
6262
]
6363
}
6464

65+
export const dummyMock4: Mock = {
66+
id: "4",
67+
desc: "Mock 4 : Password protected",
68+
method: RequestMethod.GET,
69+
endpoint: "users4/:id/:name",
70+
responses: [
71+
{
72+
id: "1",
73+
desc: "Mock 4 Response 1",
74+
latency: 0,
75+
statusCode: 200,
76+
headers: {
77+
"x-foo": "bar",
78+
"content-type": "text/plain",
79+
},
80+
body: `the id is {{urlParam 'id'}} . the url is {{url}} . not passing param to url param {{urlParam}}. Content type is {{header 'Content-Type'}}. giberish ahead: {{random values}} {{}} {{color: "something"}} {{url 'http://localhost:3000'}} {{urlParam 'id'}} {{ color: "red", display: flex}}`
81+
}
82+
]
83+
84+
}
85+
6586
export const getSelectorMap = (): any => {
6687
let selectorMap: any = {}
6788
selectorMap[dummyMock1.id] = {
@@ -79,5 +100,10 @@ export const getSelectorMap = (): any => {
79100
endpoint: dummyMock3.endpoint
80101
};
81102

103+
selectorMap[dummyMock4.id] = {
104+
method: dummyMock4.method,
105+
endpoint: dummyMock4.endpoint
106+
};
107+
82108
return selectorMap;
83109
}

0 commit comments

Comments
 (0)