Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
15 changes: 13 additions & 2 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 @@ -22,6 +22,7 @@
"typescript": "^4.9.4"
},
"dependencies": {
"@types/har-format": "^1.2.14",
"cors": "^2.8.5",
"express": "^4.18.2",
"path-to-regexp": "^0.1.7"
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 @@ -30,7 +30,10 @@ class MockServerHandler {
password: queryParams[RQ_PASSWORD] as string
}
);
return mockResponse;
return {
...mockResponse,
metadata: { mockId: mockData.id },
}
}

console.debug("[Debug] No Mock Selected");
Expand Down
17 changes: 15 additions & 2 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";

interface MockServerConfig {
port: number;
Expand All @@ -14,14 +16,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 = "") {
this.config = {
port,
pathPrefix
};
this.configFetcher = configFetcher;
this.logSink = logSink;

this.app = this.setup();
}
Expand All @@ -36,6 +40,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 }));

app.use(HarMiddleware);

app.use((_, res, next) => {
res.set({
Expand Down Expand Up @@ -73,14 +83,17 @@ 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);

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

return app;
}

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

Expand Down
104 changes: 104 additions & 0 deletions src/core/utils/harFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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'] => {
// req.query is any, which isn't ideal; we need to ensure it's an object with string values
const queryObject: Request['query'] = req.query;

// Convert the object into an array of name-value pairs
const queryString: HarRequest['queryString'] = [];

for (const [name, value] of Object.entries(queryObject)) {
if (Array.isArray(value)) {
// If the value is an array, add an entry for each value
value.forEach(val => queryString.push({ name, value: val as string }));
} else {
// Otherwise, just add the name-value pair directly
queryString.push({ name, value: value as string });
}
}

return queryString;
}

export const buildHarRequest = (req: Request): HarRequest => {
return {
method: req.method,
url: req.url,
httpVersion: req.httpVersion,
cookies: [],
headers: getHarHeaders(req.headers),
queryString: getHarRequestQueryString(req),
postData: getPostData(req),
headersSize: -1,
bodySize: -1,
}
};

export const buildHarResponse = (res: Response, metadata?: any): HarResponse => {
const { body } = metadata;
return {
status: res.statusCode,
statusText: res.statusMessage,
httpVersion: res.req.httpVersion,
cookies: [],
headers: getHarHeaders(res.getHeaders()),
content: {
size: Buffer.byteLength(JSON.stringify(body)),
mimeType: res.get('Content-Type') || 'application/json',
text: JSON.stringify(body),
},
redirectURL: '',
headersSize: -1,
bodySize: -1,
}
};
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
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";

export {
MockServer,
IConfigFetcher,
IlogSink,
MockSchema,
MockMetadataSchema,
MockResponseSchema,
Expand Down
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> => {
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, })
});

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;
3 changes: 2 additions & 1 deletion 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(3000, firebaseConfigFetcher, "/mocksv2");
const server = new MockServer(3000, firebaseConfigFetcher, fileLogSink, "/mocksv2");
console.log(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>;
}