Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
node_modules/
build/
build/
.DS_Store
*.log
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"typescript": "^4.9.4"
},
"dependencies": {
"@types/har-format": "^1.2.14",
"cors": "^2.8.5",
"express": "^4.18.2",
"handlebars": "^4.7.8",
Expand Down
5 changes: 4 additions & 1 deletion src/core/common/mockHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ class MockServerHandler {
req,
queryParams[RQ_PASSWORD] as string,
);
return mockResponse;
return {
...mockResponse,
metadata: { mockId: mockData.id },
}
}

console.debug("[Debug] No Mock Selected");
Expand Down
19 changes: 16 additions & 3 deletions src/core/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import MockServerHandler from "./common/mockHandler";
import IConfigFetcher from "../interfaces/configFetcherInterface";
import storageService from "../services/storageService";
import { MockServerResponse } from "../types";
import ILogSink from "../interfaces/logSinkInterface";
import { HarMiddleware } from "../middlewares/har";
import { cleanupPath } from "./utils";

interface MockServerConfig {
Expand All @@ -15,14 +17,16 @@ interface MockServerConfig {
class MockServer {
config: MockServerConfig;
configFetcher: IConfigFetcher;
logSink: ILogSink;
app: Express

constructor (port: number = 3000, configFetcher: IConfigFetcher, pathPrefix: string = "") {
constructor (port: number = 3000, configFetcher: IConfigFetcher, logSink: ILogSink, pathPrefix: string = "") {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logSink should be optional. If not given, should default to empty Sink

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The api of the constructor has been updated in the other PR

this.config = {
port,
pathPrefix
};
this.configFetcher = configFetcher;
this.logSink = logSink;

this.app = this.setup();
}
Expand All @@ -37,6 +41,12 @@ class MockServer {
this.initStorageService();

const app = express();

// Use middleware to parse `application/json` and `application/x-www-form-urlencoded` body data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
Comment thread
wrongsahil marked this conversation as resolved.

app.use(HarMiddleware);

app.use((_, res, next) => {
res.set({
Expand Down Expand Up @@ -73,15 +83,18 @@ class MockServer {
}

const mockResponse: MockServerResponse = await MockServerHandler.handleEndpoint(req);
// console.debug("[Debug] Final Mock Response", mockResponse);
return res.status(mockResponse.statusCode).set(mockResponse.headers).end(mockResponse.body);
console.debug("[Debug] Final Mock Response", mockResponse);

res.locals.metadata = mockResponse.metadata;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets namespace metadata -> rq_metadata for scoping Requestly variables

return res.status(mockResponse.statusCode).set(mockResponse.headers).send(mockResponse.body);
});

return app;
}

initStorageService = () => {
storageService.setConfigFetcher(this.configFetcher);
storageService.setLogSink(this.logSink);
}
}

Expand Down
102 changes: 102 additions & 0 deletions src/core/utils/harFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type {
Request as HarRequest,
Response as HarResponse,
Header as HarHeader,
} from "har-format";
import { IncomingHttpHeaders, OutgoingHttpHeaders } from "http";
import { Request, Response } from "express";
import { RequestMethod } from "../../types";

export const getHarHeaders = (headers: IncomingHttpHeaders | OutgoingHttpHeaders): HarHeader[] => {
const harHeaders: HarHeader[] = [];

for (const headerName in headers) {
const headerValue = headers[headerName];
// Header values can be string | string[] according to Node.js typings,
// but HAR format requires a string, so we need to handle this.
if (headerValue) {
const value = Array.isArray(headerValue) ? headerValue.join('; ') : headerValue;
harHeaders.push({ name: headerName, value: value.toString() });
}
}

return harHeaders;
};

export const getPostData = (req: Request): HarRequest['postData'] => {
if ([RequestMethod.POST, RequestMethod.PUT, RequestMethod.PATCH].includes(req.method as RequestMethod)) {
const postData: any = {
mimeType: req.get('Content-Type') || 'application/json',
text: '',
params: [],
};

// When the body is URL-encoded, the body should be converted into params
if (postData.mimeType === 'application/x-www-form-urlencoded' && typeof req.body === 'object') {
postData.params = Object.keys(req.body).map(key => ({
name: key,
value: req.body[key],
}));
} else if (req.body) {
try {
postData.text = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);
} catch (error) {
postData.text = "";
}
}

return postData;
}
return undefined;
}

export const getHarRequestQueryString = (req: Request): HarRequest['queryString'] => {
const queryObject: Request['query'] = req.query;

const queryString: HarRequest['queryString'] = [];

for (const [name, value] of Object.entries(queryObject)) {
if (Array.isArray(value)) {
value.forEach(val => queryString.push({ name, value: val as string }));
} else {
queryString.push({ name, value: value as string });
}
}

return queryString;
}

export const buildHarRequest = (req: Request): HarRequest => {
const requestData = getPostData(req)
return {
method: req.method,
url: req.url,
httpVersion: req.httpVersion,
cookies: [],
headers: getHarHeaders(req.headers),
queryString: getHarRequestQueryString(req),
postData: requestData,
headersSize: -1, // not calculating for now
bodySize: requestData ? Buffer.byteLength(requestData.text!) : -1,
}
};

export const buildHarResponse = (res: Response, metadata?: any): HarResponse => {
const { body } = metadata;
const bodySize = body ? Buffer.byteLength(JSON.stringify(body || {})) : -1;
return {
status: res.statusCode,
statusText: res.statusMessage,
httpVersion: res.req.httpVersion,
cookies: [],
headers: getHarHeaders(res.getHeaders()),
content: {
size: bodySize, // same as bodySize since serving uncompressed
mimeType: res.get('Content-Type') || 'application/json',
text: JSON.stringify(body),
},
redirectURL: '', // todo: implement when we integrate rules to mocks
headersSize: -1, // not calculating for now
bodySize,
}
};
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import IConfigFetcher from "./interfaces/configFetcherInterface";
import IlogSink from "./interfaces/logSinkInterface";
import MockServer from "./core/server";
import { Mock as MockSchema, MockMetadata as MockMetadataSchema, Response as MockResponseSchema } from "./types/mock";

import {Log as MockLog} from "./types";
export {
MockServer,
IConfigFetcher,
IlogSink,
MockSchema,
MockMetadataSchema,
MockResponseSchema,
MockLog,
};
9 changes: 9 additions & 0 deletions src/interfaces/logSinkInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Log } from "../types";

class ILogSink {
store = async (log: Log): Promise<void> => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let rename this to sendLog

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be better to address this one's these changes are merged here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed it in backend implementation here

return;
}
}

export default ILogSink;
32 changes: 32 additions & 0 deletions src/middlewares/har.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Entry } from "har-format";
import { NextFunction, Request, Response } from "express";
import storageService from "../services/storageService";
import { buildHarRequest, buildHarResponse } from "../core/utils/harFormatter";


export const HarMiddleware = (req: Request, res: Response, next: NextFunction) => {
const originalSend = res.send;

const requestStartTime = new Date();
const requestStartTimeStamp: string = requestStartTime.toISOString();

let responseBody: string;

res.send = function (body) {
responseBody = body;
return originalSend.call(this, body);
};

res.once('finish', () => {
const HarEntry: Partial<Entry> = {
time: Date.now() - requestStartTime.getTime(),
startedDateTime: requestStartTimeStamp,
request: buildHarRequest(req),
response: buildHarResponse(res, { body: responseBody }),
}

storageService.storeLog({ mockId: res.locals.metadata.mockId, HarEntry, })
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Faced a crash here while running locally

/Users/sahilgupta/Documents/dev/requestly/requestly-mock-server/src/middlewares/har.ts:28
        storageService.storeLog({ mockId: res.locals.metadata.mockId, HarEntry, })
                                                              ^
TypeError: Cannot read properties of undefined (reading 'mockId')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I faced the same error once. Probably because of the order in which we are applying the metadata in core/server.ts (but still the error shouldn't occur).

It suprisingly went away when I restarted the server and tested with the backend

});

next();
};
14 changes: 13 additions & 1 deletion src/services/storageService.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
import IConfigFetcher from "../interfaces/configFetcherInterface";
import ILogSink from "../interfaces/logSinkInterface";
import { Log } from "../types";

class StorageService {
configFetcher ?: IConfigFetcher|null = null;
logSink ?: ILogSink|null = null;

constructor(configFetcher ?: IConfigFetcher ) {
constructor(configFetcher ?: IConfigFetcher, logSink ?: ILogSink) {
this.configFetcher = configFetcher;
this.logSink = logSink;
}

// TODO: This should be set when starting the mock server
setConfigFetcher = (configFetcher: IConfigFetcher) => {
this.configFetcher = configFetcher;
}

setLogSink(logSink: ILogSink) {
this.logSink = logSink;
}

getMockSelectorMap = async (kwargs ?: any): Promise<any> => {
return this.configFetcher?.getMockSelectorMap(kwargs);
};

getMock = async (id: string, kwargs?: any): Promise<any> => {
return this.configFetcher?.getMock(id, kwargs);
}

storeLog = async (log: Log): Promise<void> => {
await this.logSink?.store(log);
}
}

const storageService = new StorageService();
Expand Down
21 changes: 21 additions & 0 deletions src/test/FileLogSink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import fs from 'fs';

import ILogSink from "../interfaces/logSinkInterface";
import { Log } from "../types";


class FileLogSink implements ILogSink {
store = async (log: Log): Promise<void> => {
const logLine = `${JSON.stringify(log.HarEntry)}\n`;
fs.writeFile(`${log.mockId}.log`, logLine, { flag: 'a+' }, (err) => {
if(err) {
console.log("Error dumping log to file.");
throw err;
}
});
Promise.resolve();
}
}

const fileLogSink = new FileLogSink();
export default fileLogSink;
5 changes: 3 additions & 2 deletions src/test/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import MockServer from "../core/server";
import firebaseConfigFetcher from "./firebaseConfigFetcher";
import fileLogSink from "./FileLogSink";

const server = new MockServer(3001, firebaseConfigFetcher, "/mocksv2");
console.log(server.app);
const server = new MockServer(3001, firebaseConfigFetcher, fileLogSink, "/mocksv2");
console.debug(server.app);
server.start();
7 changes: 7 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Entry } from "har-format";
import { HttpStatusCode } from "../enums/mockServerResponse";

export enum RequestMethod {
Expand All @@ -18,4 +19,10 @@ export interface MockServerResponse {
body: string,
statusCode: HttpStatusCode,
headers: { [key: string]: string }
metadata?: { mockId: string }
}

export interface Log {
mockId: string;
HarEntry: Partial<Entry>;
}