Skip to content

Commit e15c756

Browse files
nsrCodesCodHeK
andauthored
feat: add storeLog in Config (#21)
Co-authored-by: Gagan Ganapathy Ajjikuttira <[email protected]>
1 parent 63dd75a commit e15c756

File tree

16 files changed

+285
-59
lines changed

16 files changed

+285
-59
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
node_modules/
2-
build/
2+
build/
3+
.DS_Store
4+
*.log

README.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ npm i @requestly/mock-server
2424
``` javascript
2525
import * as functions from 'firebase-functions';
2626
import { MockServer } from '@requestly/mock-server';
27-
import firebaseConfigFetcher from '../firebaseConfigFetcher';
27+
import firebaseConfig from '../firebaseConfig';
2828

2929
const startMockServer = () => {
30-
const expressApp = new MockServer(3000, firebaseConfigFetcher, '/api/mockv2').app;
30+
const expressApp = new MockServer(3000, firebaseConfig, '/api/mockv2').app;
3131

3232
return functions.runWith({ minInstances: isProdEnv() ? 1 : 0 }).https.onRequest(expressApp);
3333
};
@@ -36,7 +36,7 @@ export const handleMockRequest = startMockServer();
3636
```
3737

3838
``` javascript
39-
class FirebaseConfigFetcher implements IConfigFetcher {
39+
class FirebaseConfig implements IConfig {
4040
getMockSelectorMap = (kwargs?: any) => {
4141
/**
4242
* Fetch and return mockSelectorMap from firestore
@@ -54,10 +54,16 @@ class FirebaseConfigFetcher implements IConfigFetcher {
5454
* Fetch mock details from firestore
5555
*/
5656
}
57+
58+
storeLog? = (log: Log) => {
59+
/**
60+
* Store log in cloud storages
61+
*/
62+
}
5763
}
5864

59-
const firebaseConfigFetcher = new FirebaseConfigFetcher();
60-
export default firebaseConfigFetcher;
65+
const firebaseConfig = new FirebaseConfig();
66+
export default firebaseConfig;
6167
```
6268
6369
@@ -69,9 +75,9 @@ export default firebaseConfigFetcher;
6975
1. Request coming from GET `https://username.requestly.dev/users`
7076
2. Firebase Function passes the request to @requestly/mock-server
7177
3. @requestly/mock-server - MockSelector
72-
a. Fetches all the available mocks using `IConfigFetcher.getMockSelectorMap()` (Firestore in case of Requestly)
78+
a. Fetches all the available mocks using `IConfig.getMockSelectorMap()` (Firestore in case of Requestly)
7379
b. Select mock if any endpoint+method matches the incoming request (GET /users)
74-
c. Fetch Mock using `IConfigFetcher.getMock(mockId)` and pass it to MockProcessor
80+
c. Fetch Mock using `IConfig.getMock(mockId)` and pass it to MockProcessor
7581
4. @requestly/mock-server - MockProcessor
7682
a. Process Mock - Response Rendering
7783
b. Return Response

package-lock.json

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"typescript": "^4.9.4"
2424
},
2525
"dependencies": {
26+
"@types/har-format": "^1.2.14",
2627
"cors": "^2.8.5",
2728
"express": "^4.18.2",
2829
"handlebars": "^4.7.8",

src/core/common/mockHandler.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ class MockServerHandler {
2525
req,
2626
queryParams[RQ_PASSWORD] as string,
2727
);
28-
return mockResponse;
28+
return {
29+
...mockResponse,
30+
metadata: { mockId: mockData.id },
31+
}
2932
}
3033

3134
console.debug("[Debug] No Mock Selected");

src/core/server.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,52 @@ import express, { Request, Response, Express } from "express";
22
import cors from "cors";
33

44
import MockServerHandler from "./common/mockHandler";
5-
import IConfigFetcher from "../interfaces/configFetcherInterface";
5+
import { Config } from "../interfaces/config";
66
import storageService from "../services/storageService";
77
import { MockServerResponse } from "../types";
8+
import { HarMiddleware } from "../middlewares/har";
89
import { cleanupPath } from "./utils";
910

10-
interface MockServerConfig {
11+
interface MockServerOptions {
1112
port: number;
1213
pathPrefix: string;
14+
storageConfig: Config;
1315
}
1416

17+
/* To make the constructor options optional except for storageConfig */
18+
type MockServerConstructorOptions = Pick<MockServerOptions, 'storageConfig'> & Partial<MockServerOptions>;
19+
1520
class MockServer {
16-
config: MockServerConfig;
17-
configFetcher: IConfigFetcher;
21+
serverOptions: MockServerOptions;
1822
app: Express
1923

20-
constructor (port: number = 3000, configFetcher: IConfigFetcher, pathPrefix: string = "") {
21-
this.config = {
22-
port,
23-
pathPrefix
24+
constructor (options: MockServerConstructorOptions) {
25+
this.serverOptions = {
26+
storageConfig: options.storageConfig,
27+
port: options.port ?? 3000,
28+
pathPrefix: options.pathPrefix ?? "",
2429
};
25-
this.configFetcher = configFetcher;
30+
2631

2732
this.app = this.setup();
2833
}
2934

3035
start = () => {
31-
this.app.listen(this.config.port, () => {
32-
console.log(`Mock Server Listening on port ${this.config.port}`);
36+
this.app.listen(this.serverOptions.port, () => {
37+
console.log(`Mock Server Listening on port ${this.serverOptions.port}`);
3338
})
3439
}
3540

3641
setup = (): Express => {
3742
this.initStorageService();
3843

3944
const app = express();
45+
46+
// Use middleware to parse `application/json` and `application/x-www-form-urlencoded` body data
47+
app.use(express.json());
48+
app.use(express.urlencoded({ extended: true }));
49+
50+
app.use(HarMiddleware);
4051

4152
app.use((_, res, next) => {
4253
res.set({
@@ -56,32 +67,34 @@ class MockServer {
5667
}));
5768

5869
// pathPrefix to handle /mockv2 prefix in cloud functions
59-
const regex = new RegExp(`${this.config.pathPrefix}\/(.+)`);
70+
const regex = new RegExp(`${this.serverOptions.pathPrefix}\/(.+)`);
6071
app.all(regex, async (req: Request, res: Response) => {
6172
console.log(`Initial Request`);
6273
console.log(`Path: ${req.path}`);
6374
console.log(`Query Params: ${JSON.stringify(req.query)}`);
6475

6576
// Stripping URL prefix
66-
if(req.path.indexOf(this.config.pathPrefix) === 0) {
67-
console.log(`Stripping pathPrefix: ${this.config.pathPrefix}`);
77+
if(req.path.indexOf(this.serverOptions.pathPrefix) === 0) {
78+
console.log(`Stripping pathPrefix: ${this.serverOptions.pathPrefix}`);
6879
Object.defineProperty(req, 'path', {
69-
value: cleanupPath(req.path.slice(this.config.pathPrefix.length)),
80+
value: cleanupPath(req.path.slice(this.serverOptions.pathPrefix.length)),
7081
writable: true
7182
});
7283
console.log(`Path after stripping prefix and cleanup: ${req.path}`);
7384
}
7485

7586
const mockResponse: MockServerResponse = await MockServerHandler.handleEndpoint(req);
76-
// console.debug("[Debug] Final Mock Response", mockResponse);
77-
return res.status(mockResponse.statusCode).set(mockResponse.headers).end(mockResponse.body);
87+
console.debug("[Debug] Final Mock Response", mockResponse);
88+
89+
res.locals.rq_metadata = mockResponse.metadata;
90+
return res.status(mockResponse.statusCode).set(mockResponse.headers).send(mockResponse.body);
7891
});
7992

8093
return app;
8194
}
8295

8396
initStorageService = () => {
84-
storageService.setConfigFetcher(this.configFetcher);
97+
storageService.setConfig(this.serverOptions.storageConfig);
8598
}
8699
}
87100

src/core/utils/harFormatter.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import type {
2+
Request as HarRequest,
3+
Response as HarResponse,
4+
Header as HarHeader,
5+
} from "har-format";
6+
import { IncomingHttpHeaders, OutgoingHttpHeaders } from "http";
7+
import { Request, Response } from "express";
8+
import { RequestMethod } from "../../types";
9+
10+
export const getHarHeaders = (headers: IncomingHttpHeaders | OutgoingHttpHeaders): HarHeader[] => {
11+
const harHeaders: HarHeader[] = [];
12+
13+
for (const headerName in headers) {
14+
const headerValue = headers[headerName];
15+
// Header values can be string | string[] according to Node.js typings,
16+
// but HAR format requires a string, so we need to handle this.
17+
if (headerValue) {
18+
const value = Array.isArray(headerValue) ? headerValue.join('; ') : headerValue;
19+
harHeaders.push({ name: headerName, value: value.toString() });
20+
}
21+
}
22+
23+
return harHeaders;
24+
};
25+
26+
export const getPostData = (req: Request): HarRequest['postData'] => {
27+
if ([RequestMethod.POST, RequestMethod.PUT, RequestMethod.PATCH].includes(req.method as RequestMethod)) {
28+
const postData: any = {
29+
mimeType: req.get('Content-Type') || 'application/json',
30+
text: '',
31+
params: [],
32+
};
33+
34+
// When the body is URL-encoded, the body should be converted into params
35+
if (postData.mimeType === 'application/x-www-form-urlencoded' && typeof req.body === 'object') {
36+
postData.params = Object.keys(req.body).map(key => ({
37+
name: key,
38+
value: req.body[key],
39+
}));
40+
} else if (req.body) {
41+
try {
42+
postData.text = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);
43+
} catch (error) {
44+
postData.text = "";
45+
}
46+
}
47+
48+
return postData;
49+
}
50+
return undefined;
51+
}
52+
53+
export const getHarRequestQueryString = (req: Request): HarRequest['queryString'] => {
54+
const queryObject: Request['query'] = req.query;
55+
56+
const queryString: HarRequest['queryString'] = [];
57+
58+
for (const [name, value] of Object.entries(queryObject)) {
59+
if (Array.isArray(value)) {
60+
value.forEach(val => queryString.push({ name, value: val as string }));
61+
} else {
62+
queryString.push({ name, value: value as string });
63+
}
64+
}
65+
66+
return queryString;
67+
}
68+
69+
export const buildHarRequest = (req: Request): HarRequest => {
70+
const requestData = getPostData(req)
71+
return {
72+
method: req.method,
73+
url: req.url,
74+
httpVersion: req.httpVersion,
75+
cookies: [],
76+
headers: getHarHeaders(req.headers),
77+
queryString: getHarRequestQueryString(req),
78+
postData: requestData,
79+
headersSize: -1, // not calculating for now
80+
bodySize: requestData ? Buffer.byteLength(requestData.text!) : -1,
81+
}
82+
};
83+
84+
export const buildHarResponse = (res: Response, metadata?: any): HarResponse => {
85+
const { body } = metadata;
86+
const bodySize = body ? Buffer.byteLength(JSON.stringify(body || {})) : -1;
87+
return {
88+
status: res.statusCode,
89+
statusText: res.statusMessage,
90+
httpVersion: res.req.httpVersion,
91+
cookies: [],
92+
headers: getHarHeaders(res.getHeaders()),
93+
content: {
94+
size: bodySize, // same as bodySize since serving uncompressed
95+
mimeType: res.get('Content-Type') || 'application/json',
96+
text: JSON.stringify(body),
97+
},
98+
redirectURL: '', // todo: implement when we integrate rules to mocks
99+
headersSize: -1, // not calculating for now
100+
bodySize,
101+
}
102+
};

src/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import IConfigFetcher from "./interfaces/configFetcherInterface";
1+
import {Config, ISink, ISource} from "./interfaces/config";
22
import MockServer from "./core/server";
33
import { Mock as MockSchema, MockMetadata as MockMetadataSchema, Response as MockResponseSchema } from "./types/mock";
4-
4+
import {Log as MockLog} from "./types";
55
export {
66
MockServer,
7-
IConfigFetcher,
7+
Config, ISink, ISource,
88
MockSchema,
99
MockMetadataSchema,
1010
MockResponseSchema,
11+
MockLog,
1112
};
Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
1-
import { Mock } from "../types/mock";
21

3-
class IConfigFetcher {
42

3+
import { Log } from "../types";
4+
5+
export class ISink {
6+
/**
7+
* specify how and where to store logs from mock execution
8+
*/
9+
sendLog = (log: Log): Promise<void> => {
10+
return Promise.resolve();
11+
}
12+
}
13+
14+
15+
import { Mock } from "../types/mock"
16+
17+
export class ISource {
518
/**
619
*
720
* @param id Mock Id
821
* @param kwargs Contains extra val required for storage fetching. Eg. uid in case of firebaseStorageService
922
* @returns Return the Mock definition
1023
*/
11-
getMock = (id: string, kwargs?: any): Mock | null => {
12-
return null
13-
}
24+
getMock = (id: string, kwargs?: any): Mock | null => {return null}
1425

1526

1627
/**
@@ -25,9 +36,11 @@ class IConfigFetcher {
2536
* }
2637
* }
2738
*/
28-
getMockSelectorMap = (kwargs?: any): any => {
29-
return {}
30-
}
39+
getMockSelectorMap = (kwargs?: any): any => {return {}}
3140
}
3241

33-
export default IConfigFetcher;
42+
43+
export interface Config {
44+
src: ISource;
45+
sink?: ISink;
46+
}

0 commit comments

Comments
 (0)