From 7139dcf487a1462f174cb4bb36e0c1e07d40a8ba Mon Sep 17 00:00:00 2001 From: Gagan Ganapathy Ajjikuttira Date: Tue, 31 Oct 2023 23:55:58 -0700 Subject: [PATCH 01/15] Add support to dump logs --- .gitignore | 3 ++- 2.log | 11 +++++++++++ src/core/common/mockHandler.ts | 2 ++ src/core/server.ts | 6 +++++- src/index.ts | 2 ++ src/interfaces/logSinkInterface.ts | 9 +++++++++ src/services/storageService.ts | 14 +++++++++++++- src/test/FileLogSink.ts | 22 ++++++++++++++++++++++ src/test/index.ts | 3 ++- src/types/index.ts | 6 ++++++ 10 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 2.log create mode 100644 src/interfaces/logSinkInterface.ts create mode 100644 src/test/FileLogSink.ts diff --git a/.gitignore b/.gitignore index b51ea71..5e37476 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ -build/ \ No newline at end of file +build/ +.DS_Store \ No newline at end of file diff --git a/2.log b/2.log new file mode 100644 index 0000000..7d9e7d0 --- /dev/null +++ b/2.log @@ -0,0 +1,11 @@ +createdTs=[1698821698481] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} +createdTs=[1698821701020] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} +createdTs=[1698821701862] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} +createdTs=[1698821702203] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} +createdTs=[1698821702416] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} +createdTs=[1698821702622] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} +createdTs=[1698821702810] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} +createdTs=[1698821703027] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} +createdTs=[1698821703241] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} +createdTs=[1698821703460] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} +createdTs=[1698821703716] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} diff --git a/src/core/common/mockHandler.ts b/src/core/common/mockHandler.ts index 4130222..80fa269 100644 --- a/src/core/common/mockHandler.ts +++ b/src/core/common/mockHandler.ts @@ -5,6 +5,7 @@ import MockSelector from "./mockSelector"; import { getServerMockResponse } from "../utils/mockServerResponseHelper"; import { HttpStatusCode } from "../../enums/mockServerResponse"; import { RQ_PASSWORD } from "../../constants/queryParams"; +import storageService from "../../services/storageService"; class MockServerHandler { static handleEndpoint = async (req: Request): Promise => { @@ -30,6 +31,7 @@ class MockServerHandler { password: queryParams[RQ_PASSWORD] as string } ); + storageService.storeLogs({ mockId: mockData.id, createdTs: Date.now(), Har: mockResponse }); return mockResponse; } diff --git a/src/core/server.ts b/src/core/server.ts index ee825fa..2172914 100644 --- a/src/core/server.ts +++ b/src/core/server.ts @@ -5,6 +5,7 @@ import MockServerHandler from "./common/mockHandler"; import IConfigFetcher from "../interfaces/configFetcherInterface"; import storageService from "../services/storageService"; import { MockServerResponse } from "../types"; +import ILogSink from "../interfaces/logSinkInterface"; interface MockServerConfig { port: number; @@ -14,14 +15,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(); } @@ -81,6 +84,7 @@ class MockServer { initStorageService = () => { storageService.setConfigFetcher(this.configFetcher); + storageService.setLogSink(this.logSink); } } diff --git a/src/index.ts b/src/index.ts index 31400d5..656d50f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, diff --git a/src/interfaces/logSinkInterface.ts b/src/interfaces/logSinkInterface.ts new file mode 100644 index 0000000..cc410f9 --- /dev/null +++ b/src/interfaces/logSinkInterface.ts @@ -0,0 +1,9 @@ +import { Log } from "../types"; + +class ILogSink { + store = async (log: Log): Promise => { + return; + } +} + +export default ILogSink; \ No newline at end of file diff --git a/src/services/storageService.ts b/src/services/storageService.ts index 9625eb7..a049122 100644 --- a/src/services/storageService.ts +++ b/src/services/storageService.ts @@ -1,10 +1,14 @@ 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 @@ -12,6 +16,10 @@ class StorageService { this.configFetcher = configFetcher; } + setLogSink(logSink: ILogSink) { + this.logSink = logSink; + } + getMockSelectorMap = async (kwargs ?: any): Promise => { return this.configFetcher?.getMockSelectorMap(kwargs); }; @@ -19,6 +27,10 @@ class StorageService { getMock = async (id: string, kwargs?: any): Promise => { return this.configFetcher?.getMock(id, kwargs); } + + storeLogs = async (log: Log): Promise => { + await this.logSink?.store(log); + } } const storageService = new StorageService(); diff --git a/src/test/FileLogSink.ts b/src/test/FileLogSink.ts new file mode 100644 index 0000000..9f6aa89 --- /dev/null +++ b/src/test/FileLogSink.ts @@ -0,0 +1,22 @@ +import fs from 'fs'; +import path from 'path'; + +import ILogSink from "../interfaces/logSinkInterface"; +import { Log } from "../types"; + + +class FileLogSink implements ILogSink { + store = async (log: Log): Promise => { + const logLine = `createdTs=[${log.createdTs}] Har=${JSON.stringify(log.Har)}\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; diff --git a/src/test/index.ts b/src/test/index.ts index 48d4c3f..6a4c98f 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -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(); diff --git a/src/types/index.ts b/src/types/index.ts index a0d3c45..c16133d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -19,3 +19,9 @@ export interface MockServerResponse { statusCode: HttpStatusCode, headers: { [key: string]: string } } + +export interface Log { + mockId: string; + createdTs: number; + Har: any; // checkout nodejs middleware for request to HAR (https://www.npmjs.com/package/@types/har-format) +} \ No newline at end of file From c6d9f438cf25118a39a53840c2f9c9a879d6b5ac Mon Sep 17 00:00:00 2001 From: Gagan Ganapathy Ajjikuttira Date: Tue, 31 Oct 2023 23:56:53 -0700 Subject: [PATCH 02/15] Remove .log files --- .gitignore | 3 ++- 2.log | 11 ----------- 2 files changed, 2 insertions(+), 12 deletions(-) delete mode 100644 2.log diff --git a/.gitignore b/.gitignore index 5e37476..0da1096 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ build/ -.DS_Store \ No newline at end of file +.DS_Store +.log \ No newline at end of file diff --git a/2.log b/2.log deleted file mode 100644 index 7d9e7d0..0000000 --- a/2.log +++ /dev/null @@ -1,11 +0,0 @@ -createdTs=[1698821698481] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} -createdTs=[1698821701020] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} -createdTs=[1698821701862] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} -createdTs=[1698821702203] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} -createdTs=[1698821702416] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} -createdTs=[1698821702622] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} -createdTs=[1698821702810] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} -createdTs=[1698821703027] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} -createdTs=[1698821703241] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} -createdTs=[1698821703460] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} -createdTs=[1698821703716] Har={"statusCode":200,"headers":{"foo":"bar","content-type":"application/json"},"body":"{\"Hello\":\"There\",\"mockId\":\"2\"}"} From 55287d4492d07359359845fde1c92b85e6345cf0 Mon Sep 17 00:00:00 2001 From: Gagan Ganapathy Ajjikuttira Date: Sun, 5 Nov 2023 19:40:06 -0800 Subject: [PATCH 03/15] Add Har middleware --- .gitignore | 2 +- package-lock.json | 15 +++- package.json | 1 + src/core/common/mockHandler.ts | 7 +- src/core/server.ts | 11 ++- src/middlewares/har.ts | 143 +++++++++++++++++++++++++++++++++ src/services/storageService.ts | 2 +- src/test/FileLogSink.ts | 2 +- src/types/index.ts | 6 +- src/types/utils.ts | 4 + 10 files changed, 182 insertions(+), 11 deletions(-) create mode 100644 src/middlewares/har.ts create mode 100644 src/types/utils.ts diff --git a/.gitignore b/.gitignore index 0da1096..d088a01 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ node_modules/ build/ .DS_Store -.log \ No newline at end of file +*.log \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a610bf3..dfa9326 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "@requestly/mock-server", - "version": "0.1.4", + "version": "0.1.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@requestly/mock-server", - "version": "0.1.4", + "version": "0.1.6", "license": "ISC", "dependencies": { + "@types/har-format": "^1.2.14", "cors": "^2.8.5", "express": "^4.18.2", "path-to-regexp": "^0.1.7" @@ -136,6 +137,11 @@ "@types/range-parser": "*" } }, + "node_modules/@types/har-format": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.14.tgz", + "integrity": "sha512-pEmBAoccWvO6XbSI8A7KvIDGEoKtlLWtdqVCKoVBcCDSFvR4Ijd7zGLu7MWGEqk2r8D54uWlMRt+VZuSrfFMzQ==" + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -1476,6 +1482,11 @@ "@types/range-parser": "*" } }, + "@types/har-format": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.14.tgz", + "integrity": "sha512-pEmBAoccWvO6XbSI8A7KvIDGEoKtlLWtdqVCKoVBcCDSFvR4Ijd7zGLu7MWGEqk2r8D54uWlMRt+VZuSrfFMzQ==" + }, "@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", diff --git a/package.json b/package.json index 2673b2a..9ffd0bb 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/core/common/mockHandler.ts b/src/core/common/mockHandler.ts index 80fa269..4ac88d1 100644 --- a/src/core/common/mockHandler.ts +++ b/src/core/common/mockHandler.ts @@ -5,7 +5,6 @@ import MockSelector from "./mockSelector"; import { getServerMockResponse } from "../utils/mockServerResponseHelper"; import { HttpStatusCode } from "../../enums/mockServerResponse"; import { RQ_PASSWORD } from "../../constants/queryParams"; -import storageService from "../../services/storageService"; class MockServerHandler { static handleEndpoint = async (req: Request): Promise => { @@ -31,8 +30,10 @@ class MockServerHandler { password: queryParams[RQ_PASSWORD] as string } ); - storageService.storeLogs({ mockId: mockData.id, createdTs: Date.now(), Har: mockResponse }); - return mockResponse; + return { + ...mockResponse, + metadata: { mockId: mockData.id }, + } } console.debug("[Debug] No Mock Selected"); diff --git a/src/core/server.ts b/src/core/server.ts index 2172914..3ea46c1 100644 --- a/src/core/server.ts +++ b/src/core/server.ts @@ -6,6 +6,7 @@ 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; @@ -39,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({ @@ -76,7 +83,9 @@ 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; diff --git a/src/middlewares/har.ts b/src/middlewares/har.ts new file mode 100644 index 0000000..5859167 --- /dev/null +++ b/src/middlewares/har.ts @@ -0,0 +1,143 @@ +import type { + Har, + Request as HarRequest, + Response as HarResponse, + Header as HarHeader, + Param, +} from "har-format"; +import { IncomingHttpHeaders, OutgoingHttpHeaders } from "http"; +import { NextFunction, Request, Response } from "express"; +import storageService from "../services/storageService"; +import { DeepPartial } from "../types/utils"; +import { RequestMethod } from "../types"; + + +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; +}; + +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; +} + +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; +} + +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, + } +}; + +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, + } +}; + + +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 Har: DeepPartial = { + log: { + entries: [ + { + time: Date.now() - requestStartTime.getTime(), + startedDateTime: requestStartTimeStamp, + request: buildHarRequest(req), + response: buildHarResponse(res, { body: responseBody }), + }, + ] + } + }; + + storageService.storeLog({ mockId: res.locals.metadata.mockId, Har, }) + }); + + next(); +}; \ No newline at end of file diff --git a/src/services/storageService.ts b/src/services/storageService.ts index a049122..457460e 100644 --- a/src/services/storageService.ts +++ b/src/services/storageService.ts @@ -28,7 +28,7 @@ class StorageService { return this.configFetcher?.getMock(id, kwargs); } - storeLogs = async (log: Log): Promise => { + storeLog = async (log: Log): Promise => { await this.logSink?.store(log); } } diff --git a/src/test/FileLogSink.ts b/src/test/FileLogSink.ts index 9f6aa89..f1796d3 100644 --- a/src/test/FileLogSink.ts +++ b/src/test/FileLogSink.ts @@ -7,7 +7,7 @@ import { Log } from "../types"; class FileLogSink implements ILogSink { store = async (log: Log): Promise => { - const logLine = `createdTs=[${log.createdTs}] Har=${JSON.stringify(log.Har)}\n`; + const logLine = `${JSON.stringify(log.Har)}\n`; fs.writeFile(`${log.mockId}.log`, logLine, { flag: 'a+' }, (err) => { if(err) { console.log("Error dumping log to file."); diff --git a/src/types/index.ts b/src/types/index.ts index c16133d..bb2dda1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,6 @@ +import { Har } from "har-format"; import { HttpStatusCode } from "../enums/mockServerResponse"; +import { DeepPartial } from "./utils"; export enum RequestMethod { GET = "GET", @@ -18,10 +20,10 @@ export interface MockServerResponse { body: string, statusCode: HttpStatusCode, headers: { [key: string]: string } + metadata?: { mockId: string } } export interface Log { mockId: string; - createdTs: number; - Har: any; // checkout nodejs middleware for request to HAR (https://www.npmjs.com/package/@types/har-format) + Har: DeepPartial; } \ No newline at end of file diff --git a/src/types/utils.ts b/src/types/utils.ts new file mode 100644 index 0000000..51bbaac --- /dev/null +++ b/src/types/utils.ts @@ -0,0 +1,4 @@ +// ref: https://stackoverflow.com/a/61132308/10473181 +export type DeepPartial = T extends object ? { + [P in keyof T]?: DeepPartial; +} : T; \ No newline at end of file From c9a9c5e5d3adc355e3411353ee7447690fa674d5 Mon Sep 17 00:00:00 2001 From: Gagan Ganapathy Ajjikuttira Date: Sun, 5 Nov 2023 19:48:27 -0800 Subject: [PATCH 04/15] Move formatting methods to utils --- src/core/utils/harFormatter.ts | 104 +++++++++++++++++++++++++++++++ src/middlewares/har.ts | 108 +-------------------------------- 2 files changed, 106 insertions(+), 106 deletions(-) create mode 100644 src/core/utils/harFormatter.ts diff --git a/src/core/utils/harFormatter.ts b/src/core/utils/harFormatter.ts new file mode 100644 index 0000000..d299fd4 --- /dev/null +++ b/src/core/utils/harFormatter.ts @@ -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, + } +}; \ No newline at end of file diff --git a/src/middlewares/har.ts b/src/middlewares/har.ts index 5859167..0993a37 100644 --- a/src/middlewares/har.ts +++ b/src/middlewares/har.ts @@ -1,112 +1,8 @@ -import type { - Har, - Request as HarRequest, - Response as HarResponse, - Header as HarHeader, - Param, -} from "har-format"; -import { IncomingHttpHeaders, OutgoingHttpHeaders } from "http"; +import type { Har } from "har-format"; import { NextFunction, Request, Response } from "express"; import storageService from "../services/storageService"; import { DeepPartial } from "../types/utils"; -import { RequestMethod } from "../types"; - - -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; -}; - -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; -} - -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; -} - -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, - } -}; - -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, - } -}; +import { buildHarRequest, buildHarResponse } from "../core/utils/harFormatter"; export const HarMiddleware = (req: Request, res: Response, next: NextFunction) => { From 1623e2c08dd8d186c83427a79472955318f7aaaa Mon Sep 17 00:00:00 2001 From: Gagan Ganapathy Ajjikuttira Date: Wed, 8 Nov 2023 09:05:20 -0800 Subject: [PATCH 05/15] Return HarEntry instead of Har --- src/middlewares/har.ts | 23 ++++++++--------------- src/test/FileLogSink.ts | 3 +-- src/types/index.ts | 5 ++--- src/types/utils.ts | 4 ---- 4 files changed, 11 insertions(+), 24 deletions(-) delete mode 100644 src/types/utils.ts diff --git a/src/middlewares/har.ts b/src/middlewares/har.ts index 0993a37..acab6f8 100644 --- a/src/middlewares/har.ts +++ b/src/middlewares/har.ts @@ -1,7 +1,6 @@ -import type { Har } from "har-format"; +import type { Entry } from "har-format"; import { NextFunction, Request, Response } from "express"; import storageService from "../services/storageService"; -import { DeepPartial } from "../types/utils"; import { buildHarRequest, buildHarResponse } from "../core/utils/harFormatter"; @@ -19,20 +18,14 @@ export const HarMiddleware = (req: Request, res: Response, next: NextFunction) = }; res.once('finish', () => { - const Har: DeepPartial = { - log: { - entries: [ - { - time: Date.now() - requestStartTime.getTime(), - startedDateTime: requestStartTimeStamp, - request: buildHarRequest(req), - response: buildHarResponse(res, { body: responseBody }), - }, - ] - } - }; + const HarEntry: Partial = { + time: Date.now() - requestStartTime.getTime(), + startedDateTime: requestStartTimeStamp, + request: buildHarRequest(req), + response: buildHarResponse(res, { body: responseBody }), + } - storageService.storeLog({ mockId: res.locals.metadata.mockId, Har, }) + storageService.storeLog({ mockId: res.locals.metadata.mockId, HarEntry, }) }); next(); diff --git a/src/test/FileLogSink.ts b/src/test/FileLogSink.ts index f1796d3..ccb5bc0 100644 --- a/src/test/FileLogSink.ts +++ b/src/test/FileLogSink.ts @@ -1,5 +1,4 @@ import fs from 'fs'; -import path from 'path'; import ILogSink from "../interfaces/logSinkInterface"; import { Log } from "../types"; @@ -7,7 +6,7 @@ import { Log } from "../types"; class FileLogSink implements ILogSink { store = async (log: Log): Promise => { - const logLine = `${JSON.stringify(log.Har)}\n`; + 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."); diff --git a/src/types/index.ts b/src/types/index.ts index bb2dda1..563025e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,5 @@ -import { Har } from "har-format"; +import type { Entry } from "har-format"; import { HttpStatusCode } from "../enums/mockServerResponse"; -import { DeepPartial } from "./utils"; export enum RequestMethod { GET = "GET", @@ -25,5 +24,5 @@ export interface MockServerResponse { export interface Log { mockId: string; - Har: DeepPartial; + HarEntry: Partial; } \ No newline at end of file diff --git a/src/types/utils.ts b/src/types/utils.ts deleted file mode 100644 index 51bbaac..0000000 --- a/src/types/utils.ts +++ /dev/null @@ -1,4 +0,0 @@ -// ref: https://stackoverflow.com/a/61132308/10473181 -export type DeepPartial = T extends object ? { - [P in keyof T]?: DeepPartial; -} : T; \ No newline at end of file From 9fb968224318c73e3058387e546b85b584b343f1 Mon Sep 17 00:00:00 2001 From: nsrCodes Date: Thu, 8 Aug 2024 07:41:39 +0530 Subject: [PATCH 06/15] chore: add bodySize to har --- src/core/utils/harFormatter.ts | 22 ++++++++++------------ src/test/index.ts | 2 +- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/core/utils/harFormatter.ts b/src/core/utils/harFormatter.ts index d299fd4..82ee6a0 100644 --- a/src/core/utils/harFormatter.ts +++ b/src/core/utils/harFormatter.ts @@ -51,18 +51,14 @@ export const getPostData = (req: Request): HarRequest['postData'] => { } 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 }); } } @@ -71,6 +67,7 @@ export const getHarRequestQueryString = (req: Request): HarRequest['queryString' } export const buildHarRequest = (req: Request): HarRequest => { + const requestData = getPostData(req) return { method: req.method, url: req.url, @@ -78,14 +75,15 @@ export const buildHarRequest = (req: Request): HarRequest => { cookies: [], headers: getHarHeaders(req.headers), queryString: getHarRequestQueryString(req), - postData: getPostData(req), - headersSize: -1, - bodySize: -1, + 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, @@ -93,12 +91,12 @@ export const buildHarResponse = (res: Response, metadata?: any): HarResponse => cookies: [], headers: getHarHeaders(res.getHeaders()), content: { - size: Buffer.byteLength(JSON.stringify(body)), + size: bodySize, // same as bodySize since serving uncompressed mimeType: res.get('Content-Type') || 'application/json', text: JSON.stringify(body), }, - redirectURL: '', - headersSize: -1, - bodySize: -1, + redirectURL: '', // todo: implement when we integrate rules to mocks + headersSize: -1, // not calculating for now + bodySize, } }; \ No newline at end of file diff --git a/src/test/index.ts b/src/test/index.ts index efbc2d1..1c5248e 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -3,5 +3,5 @@ import firebaseConfigFetcher from "./firebaseConfigFetcher"; import fileLogSink from "./FileLogSink"; const server = new MockServer(3001, firebaseConfigFetcher, fileLogSink, "/mocksv2"); -console.log(server.app); +console.debug(server.app); server.start(); From fbf76e90c4b7a0b7767f2185967a4abcf6b946ec Mon Sep 17 00:00:00 2001 From: nsrCodes Date: Thu, 8 Aug 2024 07:58:10 +0530 Subject: [PATCH 07/15] refactor: - move logSink inside the configFetcher - rename configFetcher to config - rename the internal config in class MockServer to mockConfig --- README.md | 20 ++++++++----- src/core/server.ts | 30 ++++++++----------- src/index.ts | 6 ++-- .../{configFetcherInterface.ts => config.ts} | 14 +++++++-- src/services/storageService.ts | 24 ++++++--------- src/test/index.ts | 5 ++-- ...firebaseConfigFetcher.ts => testConfig.ts} | 23 +++++++++++--- 7 files changed, 70 insertions(+), 52 deletions(-) rename src/interfaces/{configFetcherInterface.ts => config.ts} (76%) rename src/test/{firebaseConfigFetcher.ts => testConfig.ts} (50%) diff --git a/README.md b/README.md index 78e9655..654b089 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,10 @@ npm i @requestly/mock-server ``` javascript import * as functions from 'firebase-functions'; import { MockServer } from '@requestly/mock-server'; -import firebaseConfigFetcher from '../firebaseConfigFetcher'; +import firebaseConfig from '../firebaseConfig'; const startMockServer = () => { - const expressApp = new MockServer(3000, firebaseConfigFetcher, '/api/mockv2').app; + const expressApp = new MockServer(3000, firebaseConfig, '/api/mockv2').app; return functions.runWith({ minInstances: isProdEnv() ? 1 : 0 }).https.onRequest(expressApp); }; @@ -36,7 +36,7 @@ export const handleMockRequest = startMockServer(); ``` ``` javascript -class FirebaseConfigFetcher implements IConfigFetcher { +class FirebaseConfig implements IConfig { getMockSelectorMap = (kwargs?: any) => { /** * Fetch and return mockSelectorMap from firestore @@ -54,10 +54,16 @@ class FirebaseConfigFetcher implements IConfigFetcher { * Fetch mock details from firestore */ } + + storeLog? = (log: Log) => { + /** + * Store log in cloud storages + */ + } } -const firebaseConfigFetcher = new FirebaseConfigFetcher(); -export default firebaseConfigFetcher; +const firebaseConfig = new FirebaseConfig(); +export default firebaseConfig; ``` @@ -69,9 +75,9 @@ export default firebaseConfigFetcher; 1. Request coming from GET `https://username.requestly.dev/users` 2. Firebase Function passes the request to @requestly/mock-server 3. @requestly/mock-server - MockSelector - a. Fetches all the available mocks using `IConfigFetcher.getMockSelectorMap()` (Firestore in case of Requestly) + a. Fetches all the available mocks using `IConfig.getMockSelectorMap()` (Firestore in case of Requestly) b. Select mock if any endpoint+method matches the incoming request (GET /users) - c. Fetch Mock using `IConfigFetcher.getMock(mockId)` and pass it to MockProcessor + c. Fetch Mock using `IConfig.getMock(mockId)` and pass it to MockProcessor 4. @requestly/mock-server - MockProcessor a. Process Mock - Response Rendering b. Return Response diff --git a/src/core/server.ts b/src/core/server.ts index fdd7475..7cc8335 100644 --- a/src/core/server.ts +++ b/src/core/server.ts @@ -2,10 +2,9 @@ import express, { Request, Response, Express } from "express"; import cors from "cors"; import MockServerHandler from "./common/mockHandler"; -import IConfigFetcher from "../interfaces/configFetcherInterface"; +import IConfig from "../interfaces/config"; import storageService from "../services/storageService"; import { MockServerResponse } from "../types"; -import ILogSink from "../interfaces/logSinkInterface"; import { HarMiddleware } from "../middlewares/har"; import { cleanupPath } from "./utils"; @@ -15,25 +14,23 @@ interface MockServerConfig { } class MockServer { - config: MockServerConfig; - configFetcher: IConfigFetcher; - logSink: ILogSink; + mockConfig: MockServerConfig; + config: IConfig; app: Express - constructor (port: number = 3000, configFetcher: IConfigFetcher, logSink: ILogSink, pathPrefix: string = "") { - this.config = { + constructor (port: number = 3000, config: IConfig, pathPrefix: string = "") { + this.mockConfig = { port, pathPrefix }; - this.configFetcher = configFetcher; - this.logSink = logSink; + this.config = config; this.app = this.setup(); } start = () => { - this.app.listen(this.config.port, () => { - console.log(`Mock Server Listening on port ${this.config.port}`); + this.app.listen(this.mockConfig.port, () => { + console.log(`Mock Server Listening on port ${this.mockConfig.port}`); }) } @@ -66,17 +63,17 @@ class MockServer { })); // pathPrefix to handle /mockv2 prefix in cloud functions - const regex = new RegExp(`${this.config.pathPrefix}\/(.+)`); + const regex = new RegExp(`${this.mockConfig.pathPrefix}\/(.+)`); app.all(regex, async (req: Request, res: Response) => { console.log(`Initial Request`); console.log(`Path: ${req.path}`); console.log(`Query Params: ${JSON.stringify(req.query)}`); // Stripping URL prefix - if(req.path.indexOf(this.config.pathPrefix) === 0) { - console.log(`Stripping pathPrefix: ${this.config.pathPrefix}`); + if(req.path.indexOf(this.mockConfig.pathPrefix) === 0) { + console.log(`Stripping pathPrefix: ${this.mockConfig.pathPrefix}`); Object.defineProperty(req, 'path', { - value: cleanupPath(req.path.slice(this.config.pathPrefix.length)), + value: cleanupPath(req.path.slice(this.mockConfig.pathPrefix.length)), writable: true }); console.log(`Path after stripping prefix and cleanup: ${req.path}`); @@ -93,8 +90,7 @@ class MockServer { } initStorageService = () => { - storageService.setConfigFetcher(this.configFetcher); - storageService.setLogSink(this.logSink); + storageService.setConfig(this.config); } } diff --git a/src/index.ts b/src/index.ts index 656d50f..54f5915 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,10 @@ -import IConfigFetcher from "./interfaces/configFetcherInterface"; -import IlogSink from "./interfaces/logSinkInterface"; +import IConfig from "./interfaces/config"; import MockServer from "./core/server"; import { Mock as MockSchema, MockMetadata as MockMetadataSchema, Response as MockResponseSchema } from "./types/mock"; export { MockServer, - IConfigFetcher, - IlogSink, + IConfig, MockSchema, MockMetadataSchema, MockResponseSchema, diff --git a/src/interfaces/configFetcherInterface.ts b/src/interfaces/config.ts similarity index 76% rename from src/interfaces/configFetcherInterface.ts rename to src/interfaces/config.ts index c6aa046..b7e9281 100644 --- a/src/interfaces/configFetcherInterface.ts +++ b/src/interfaces/config.ts @@ -1,6 +1,7 @@ +import { Log } from "../types"; import { Mock } from "../types/mock"; -class IConfigFetcher { +class IConfig { /** * @@ -28,6 +29,15 @@ class IConfigFetcher { getMockSelectorMap = (kwargs?: any): any => { return {} } + + /** + * specify how and where to store logs from mock execution + */ + + storeLog? = async (log: Log): Promise => { + return; + } } -export default IConfigFetcher; \ No newline at end of file + +export default IConfig; \ No newline at end of file diff --git a/src/services/storageService.ts b/src/services/storageService.ts index 457460e..0ac9470 100644 --- a/src/services/storageService.ts +++ b/src/services/storageService.ts @@ -1,35 +1,29 @@ -import IConfigFetcher from "../interfaces/configFetcherInterface"; +import IConfig from "../interfaces/config"; import ILogSink from "../interfaces/logSinkInterface"; import { Log } from "../types"; class StorageService { - configFetcher ?: IConfigFetcher|null = null; - logSink ?: ILogSink|null = null; + config ?: IConfig|null = null; - constructor(configFetcher ?: IConfigFetcher, logSink ?: ILogSink) { - this.configFetcher = configFetcher; - this.logSink = logSink; + constructor(config ?: IConfig, logSink ?: ILogSink) { + this.config = config; } // TODO: This should be set when starting the mock server - setConfigFetcher = (configFetcher: IConfigFetcher) => { - this.configFetcher = configFetcher; - } - - setLogSink(logSink: ILogSink) { - this.logSink = logSink; + setConfig = (config: IConfig) => { + this.config = config; } getMockSelectorMap = async (kwargs ?: any): Promise => { - return this.configFetcher?.getMockSelectorMap(kwargs); + return this.config?.getMockSelectorMap(kwargs); }; getMock = async (id: string, kwargs?: any): Promise => { - return this.configFetcher?.getMock(id, kwargs); + return this.config?.getMock(id, kwargs); } storeLog = async (log: Log): Promise => { - await this.logSink?.store(log); + await this.config?.storeLog?.(log); } } diff --git a/src/test/index.ts b/src/test/index.ts index 1c5248e..00629f9 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -1,7 +1,6 @@ import MockServer from "../core/server"; -import firebaseConfigFetcher from "./firebaseConfigFetcher"; -import fileLogSink from "./FileLogSink"; +import TestConfig from "./testConfig"; -const server = new MockServer(3001, firebaseConfigFetcher, fileLogSink, "/mocksv2"); +const server = new MockServer(3001, TestConfig, "/mocksv2"); console.debug(server.app); server.start(); diff --git a/src/test/firebaseConfigFetcher.ts b/src/test/testConfig.ts similarity index 50% rename from src/test/firebaseConfigFetcher.ts rename to src/test/testConfig.ts index ff4a125..2f9618f 100644 --- a/src/test/firebaseConfigFetcher.ts +++ b/src/test/testConfig.ts @@ -1,9 +1,12 @@ import { dummyMock1, dummyMock2, dummyMock3, dummyMock4, getSelectorMap } from "./dummy/mock1"; -import IConfigFetcher from "../interfaces/configFetcherInterface"; +import IConfig from "../interfaces/config"; +import { Log } from "../types"; +import fs from 'fs'; + // TODO: Fetch from Firestore and return -class FirebaseConfigFetcher implements IConfigFetcher { +class TestConfig implements IConfig { getMockSelectorMap = (kwargs?: any) => { return getSelectorMap(); }; @@ -24,7 +27,19 @@ class FirebaseConfigFetcher implements IConfigFetcher { return null; } + + // file log store + storeLog = async (log: Log): Promise => { + 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 firebaseConfigFetcher = new FirebaseConfigFetcher(); -export default firebaseConfigFetcher; +const testConfig = new TestConfig(); +export default testConfig; From 52cf014335505937b83e0a669c62060c78d9728b Mon Sep 17 00:00:00 2001 From: nsrCodes Date: Thu, 8 Aug 2024 08:00:09 +0530 Subject: [PATCH 08/15] refactor: - make config the first argument for the MockServer class - followed by arguments that have default value (port then prefix) --- src/core/server.ts | 2 +- src/test/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/server.ts b/src/core/server.ts index 7cc8335..439c800 100644 --- a/src/core/server.ts +++ b/src/core/server.ts @@ -18,7 +18,7 @@ class MockServer { config: IConfig; app: Express - constructor (port: number = 3000, config: IConfig, pathPrefix: string = "") { + constructor (config: IConfig, port: number = 3000, pathPrefix: string = "") { this.mockConfig = { port, pathPrefix diff --git a/src/test/index.ts b/src/test/index.ts index 00629f9..daf5eac 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -1,6 +1,6 @@ import MockServer from "../core/server"; import TestConfig from "./testConfig"; -const server = new MockServer(3001, TestConfig, "/mocksv2"); +const server = new MockServer(TestConfig, 3001, "/mocksv2"); console.debug(server.app); server.start(); From 9564d0a76400f003f91fc6ce9d7a66606581b0ec Mon Sep 17 00:00:00 2001 From: nsrCodes Date: Fri, 9 Aug 2024 12:34:48 +0530 Subject: [PATCH 09/15] refactor: as discussed, added and to config --- src/core/server.ts | 2 +- src/index.ts | 4 +-- src/interfaces/config.ts | 37 +++++++++++---------- src/interfaces/logSinkInterface.ts | 9 ------ src/services/storageService.ts | 13 ++++---- src/test/{ => dummy}/FileLogSink.ts | 12 +++---- src/test/dummy/dummySource.ts | 25 +++++++++++++++ src/test/testConfig.ts | 50 +++++------------------------ 8 files changed, 66 insertions(+), 86 deletions(-) delete mode 100644 src/interfaces/logSinkInterface.ts rename src/test/{ => dummy}/FileLogSink.ts (52%) create mode 100644 src/test/dummy/dummySource.ts diff --git a/src/core/server.ts b/src/core/server.ts index 439c800..dfcaa1a 100644 --- a/src/core/server.ts +++ b/src/core/server.ts @@ -2,7 +2,7 @@ import express, { Request, Response, Express } from "express"; import cors from "cors"; import MockServerHandler from "./common/mockHandler"; -import IConfig from "../interfaces/config"; +import { IConfig } from "../interfaces/config"; import storageService from "../services/storageService"; import { MockServerResponse } from "../types"; import { HarMiddleware } from "../middlewares/har"; diff --git a/src/index.ts b/src/index.ts index 54f5915..86262a5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,10 @@ -import IConfig from "./interfaces/config"; +import {IConfig, ISink, ISource} from "./interfaces/config"; import MockServer from "./core/server"; import { Mock as MockSchema, MockMetadata as MockMetadataSchema, Response as MockResponseSchema } from "./types/mock"; export { MockServer, - IConfig, + IConfig, ISink, ISource, MockSchema, MockMetadataSchema, MockResponseSchema, diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index b7e9281..9f68f36 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -1,17 +1,27 @@ + + import { Log } from "../types"; -import { Mock } from "../types/mock"; -class IConfig { +export class ISink { + /** + * specify how and where to store logs from mock execution + */ + storeLog = (log: Log): Promise => { + return Promise.resolve(); + } +} + + +import { Mock } from "../types/mock" +export class ISource { /** * * @param id Mock Id * @param kwargs Contains extra val required for storage fetching. Eg. uid in case of firebaseStorageService * @returns Return the Mock definition */ - getMock = (id: string, kwargs?: any): Mock | null => { - return null - } + getMock = (id: string, kwargs?: any): Mock | null => {return null} /** @@ -26,18 +36,11 @@ class IConfig { * } * } */ - getMockSelectorMap = (kwargs?: any): any => { - return {} - } - - /** - * specify how and where to store logs from mock execution - */ - - storeLog? = async (log: Log): Promise => { - return; - } + getMockSelectorMap = (kwargs?: any): any => {return {}} } -export default IConfig; \ No newline at end of file +export interface IConfig { + src: ISource; + sink: ISink; +} \ No newline at end of file diff --git a/src/interfaces/logSinkInterface.ts b/src/interfaces/logSinkInterface.ts deleted file mode 100644 index cc410f9..0000000 --- a/src/interfaces/logSinkInterface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Log } from "../types"; - -class ILogSink { - store = async (log: Log): Promise => { - return; - } -} - -export default ILogSink; \ No newline at end of file diff --git a/src/services/storageService.ts b/src/services/storageService.ts index 0ac9470..05e0718 100644 --- a/src/services/storageService.ts +++ b/src/services/storageService.ts @@ -1,11 +1,10 @@ -import IConfig from "../interfaces/config"; -import ILogSink from "../interfaces/logSinkInterface"; +import {IConfig} from "../interfaces/config"; import { Log } from "../types"; class StorageService { - config ?: IConfig|null = null; + config?: IConfig | null = null; - constructor(config ?: IConfig, logSink ?: ILogSink) { + constructor(config?: IConfig) { this.config = config; } @@ -15,15 +14,15 @@ class StorageService { } getMockSelectorMap = async (kwargs ?: any): Promise => { - return this.config?.getMockSelectorMap(kwargs); + return this.config?.src.getMockSelectorMap(kwargs); }; getMock = async (id: string, kwargs?: any): Promise => { - return this.config?.getMock(id, kwargs); + return this.config?.src.getMock(id, kwargs); } storeLog = async (log: Log): Promise => { - await this.config?.storeLog?.(log); + await this.config?.sink?.storeLog(log); } } diff --git a/src/test/FileLogSink.ts b/src/test/dummy/FileLogSink.ts similarity index 52% rename from src/test/FileLogSink.ts rename to src/test/dummy/FileLogSink.ts index ccb5bc0..3c9b17d 100644 --- a/src/test/FileLogSink.ts +++ b/src/test/dummy/FileLogSink.ts @@ -1,11 +1,11 @@ import fs from 'fs'; -import ILogSink from "../interfaces/logSinkInterface"; -import { Log } from "../types"; +import { ISink } from "../../interfaces/config"; +import { Log } from "../../types"; -class FileLogSink implements ILogSink { - store = async (log: Log): Promise => { +export class FileLogSink implements ISink { + storeLog = async (log: Log): Promise => { const logLine = `${JSON.stringify(log.HarEntry)}\n`; fs.writeFile(`${log.mockId}.log`, logLine, { flag: 'a+' }, (err) => { if(err) { @@ -13,9 +13,5 @@ class FileLogSink implements ILogSink { throw err; } }); - Promise.resolve(); } } - -const fileLogSink = new FileLogSink(); -export default fileLogSink; diff --git a/src/test/dummy/dummySource.ts b/src/test/dummy/dummySource.ts new file mode 100644 index 0000000..d9d3712 --- /dev/null +++ b/src/test/dummy/dummySource.ts @@ -0,0 +1,25 @@ +import { dummyMock1, dummyMock2, dummyMock3, dummyMock4, getSelectorMap } from "./mock1"; +import { ISource } from "../../interfaces/config"; + +export class DummySource implements ISource { + getMockSelectorMap = (kwargs?: any) => { + return getSelectorMap(); + }; + + getMock = (id: string, kwargs?: any) => { + if(id === "1") { + return dummyMock1; + } + else if(id === "2") { + return dummyMock2; + } + else if(id === "3") { + return dummyMock3; + } + else if(id === "4") { + return dummyMock4; + } + + return null; + } +} \ No newline at end of file diff --git a/src/test/testConfig.ts b/src/test/testConfig.ts index 2f9618f..e593ce1 100644 --- a/src/test/testConfig.ts +++ b/src/test/testConfig.ts @@ -1,45 +1,11 @@ -import { dummyMock1, dummyMock2, dummyMock3, dummyMock4, getSelectorMap } from "./dummy/mock1"; -import IConfig from "../interfaces/config"; -import { Log } from "../types"; -import fs from 'fs'; +import { IConfig } from "../interfaces/config"; +import { DummySource } from "./dummy/dummySource"; +import { FileLogSink } from "./dummy/FileLogSink"; - -// TODO: Fetch from Firestore and return -class TestConfig implements IConfig { - getMockSelectorMap = (kwargs?: any) => { - return getSelectorMap(); - }; - - getMock = (id: string, kwargs?: any) => { - if(id === "1") { - return dummyMock1; - } - else if(id === "2") { - return dummyMock2; - } - else if(id === "3") { - return dummyMock3; - } - else if(id === "4") { - return dummyMock4; - } - - return null; - } - - // file log store - storeLog = async (log: Log): Promise => { - 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 testConfig = new TestConfig(); +const testConfig: IConfig = { + src: new DummySource(), + sink: new FileLogSink() +}; export default testConfig; + From e1537a5e78ecd08302bb98f014de89cbbe56ae95 Mon Sep 17 00:00:00 2001 From: nsrCodes Date: Fri, 9 Aug 2024 12:57:25 +0530 Subject: [PATCH 10/15] refactor: config is much more consumable as a class --- src/interfaces/config.ts | 9 +++++++-- src/test/testConfig.ts | 5 +---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 9f68f36..6d54ea0 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -40,7 +40,12 @@ export class ISource { } -export interface IConfig { +export class IConfig { src: ISource; - sink: ISink; + sink?: ISink; + + constructor(src: ISource, sink?: ISink) { + this.src = src; + this.sink = sink; + } } \ No newline at end of file diff --git a/src/test/testConfig.ts b/src/test/testConfig.ts index e593ce1..e86df1e 100644 --- a/src/test/testConfig.ts +++ b/src/test/testConfig.ts @@ -3,9 +3,6 @@ import { IConfig } from "../interfaces/config"; import { DummySource } from "./dummy/dummySource"; import { FileLogSink } from "./dummy/FileLogSink"; -const testConfig: IConfig = { - src: new DummySource(), - sink: new FileLogSink() -}; +const testConfig = new IConfig(new DummySource(), new FileLogSink()); export default testConfig; From 8237aadea11269526687b9d54b5ff90e20691cc4 Mon Sep 17 00:00:00 2001 From: nsrCodes Date: Tue, 13 Aug 2024 09:58:25 +0530 Subject: [PATCH 11/15] feat: expose log type from package --- src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 656d50f..bfc4c70 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ 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, @@ -10,4 +10,5 @@ export { MockSchema, MockMetadataSchema, MockResponseSchema, + MockLog, }; From af35675ef891e4adcf14394ebbb37b6bb9b07a2e Mon Sep 17 00:00:00 2001 From: nsrCodes Date: Wed, 21 Aug 2024 17:44:38 +0530 Subject: [PATCH 12/15] chore: address review commits: - convert IConfig from class to interface - save src and sink in storageService instead of complete IConfig --- src/interfaces/config.ts | 7 +------ src/services/storageService.ts | 17 ++++++++++------- src/test/testConfig.ts | 5 ++++- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 6d54ea0..74871eb 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -40,12 +40,7 @@ export class ISource { } -export class IConfig { +export interface IConfig { src: ISource; sink?: ISink; - - constructor(src: ISource, sink?: ISink) { - this.src = src; - this.sink = sink; - } } \ No newline at end of file diff --git a/src/services/storageService.ts b/src/services/storageService.ts index 05e0718..4b29fd6 100644 --- a/src/services/storageService.ts +++ b/src/services/storageService.ts @@ -1,28 +1,31 @@ -import {IConfig} from "../interfaces/config"; +import {IConfig, ISink, ISource} from "../interfaces/config"; import { Log } from "../types"; class StorageService { - config?: IConfig | null = null; + src: ISource | null = null; + sink: ISink | null = null; constructor(config?: IConfig) { - this.config = config; + this.src = config?.src || null; + this.sink = config?.sink || null; } // TODO: This should be set when starting the mock server setConfig = (config: IConfig) => { - this.config = config; + this.src = config.src || null; + this.sink = config.sink || null; } getMockSelectorMap = async (kwargs ?: any): Promise => { - return this.config?.src.getMockSelectorMap(kwargs); + return this.src?.getMockSelectorMap(kwargs); }; getMock = async (id: string, kwargs?: any): Promise => { - return this.config?.src.getMock(id, kwargs); + return this.src?.getMock(id, kwargs); } storeLog = async (log: Log): Promise => { - await this.config?.sink?.storeLog(log); + await this.sink?.storeLog(log); } } diff --git a/src/test/testConfig.ts b/src/test/testConfig.ts index e86df1e..4b93e95 100644 --- a/src/test/testConfig.ts +++ b/src/test/testConfig.ts @@ -3,6 +3,9 @@ import { IConfig } from "../interfaces/config"; import { DummySource } from "./dummy/dummySource"; import { FileLogSink } from "./dummy/FileLogSink"; -const testConfig = new IConfig(new DummySource(), new FileLogSink()); +const testConfig: IConfig = { + src: new DummySource(), + sink: new FileLogSink(), +} export default testConfig; From fed0cafe270dae9af55912f943ef48cf3163270d Mon Sep 17 00:00:00 2001 From: nsrCodes Date: Wed, 21 Aug 2024 18:51:55 +0530 Subject: [PATCH 13/15] chore: rename locals.metadata to locals.rq_metadata --- src/core/server.ts | 2 +- src/middlewares/har.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/server.ts b/src/core/server.ts index fdd7475..3d73e76 100644 --- a/src/core/server.ts +++ b/src/core/server.ts @@ -85,7 +85,7 @@ class MockServer { const mockResponse: MockServerResponse = await MockServerHandler.handleEndpoint(req); console.debug("[Debug] Final Mock Response", mockResponse); - res.locals.metadata = mockResponse.metadata; + res.locals.rq_metadata = mockResponse.metadata; return res.status(mockResponse.statusCode).set(mockResponse.headers).send(mockResponse.body); }); diff --git a/src/middlewares/har.ts b/src/middlewares/har.ts index acab6f8..e4dd002 100644 --- a/src/middlewares/har.ts +++ b/src/middlewares/har.ts @@ -25,7 +25,7 @@ export const HarMiddleware = (req: Request, res: Response, next: NextFunction) = response: buildHarResponse(res, { body: responseBody }), } - storageService.storeLog({ mockId: res.locals.metadata.mockId, HarEntry, }) + storageService.storeLog({ mockId: res.locals.rq_metadata.mockId, HarEntry, }) }); next(); From a611ae416d8bf1fea06113ccfca0f9a7b4f62a8e Mon Sep 17 00:00:00 2001 From: nsrCodes Date: Thu, 22 Aug 2024 15:52:55 +0530 Subject: [PATCH 14/15] chore: merged refactor PR for ease of review --- src/interfaces/config.ts | 2 +- src/services/storageService.ts | 2 +- src/test/dummy/FileLogSink.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 74871eb..e1d55a4 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -6,7 +6,7 @@ export class ISink { /** * specify how and where to store logs from mock execution */ - storeLog = (log: Log): Promise => { + sendLog = (log: Log): Promise => { return Promise.resolve(); } } diff --git a/src/services/storageService.ts b/src/services/storageService.ts index 4b29fd6..17cddf6 100644 --- a/src/services/storageService.ts +++ b/src/services/storageService.ts @@ -25,7 +25,7 @@ class StorageService { } storeLog = async (log: Log): Promise => { - await this.sink?.storeLog(log); + await this.sink?.sendLog(log); } } diff --git a/src/test/dummy/FileLogSink.ts b/src/test/dummy/FileLogSink.ts index 3c9b17d..d3cde9b 100644 --- a/src/test/dummy/FileLogSink.ts +++ b/src/test/dummy/FileLogSink.ts @@ -5,7 +5,7 @@ import { Log } from "../../types"; export class FileLogSink implements ISink { - storeLog = async (log: Log): Promise => { + sendLog = async (log: Log): Promise => { const logLine = `${JSON.stringify(log.HarEntry)}\n`; fs.writeFile(`${log.mockId}.log`, logLine, { flag: 'a+' }, (err) => { if(err) { From 243356fb97a81fd3879ed35b02d39e4bd87553d2 Mon Sep 17 00:00:00 2001 From: nsrCodes Date: Thu, 22 Aug 2024 18:38:17 +0530 Subject: [PATCH 15/15] refactor: address review comments - combine config and mockServerConfig into serverOptions of MockServerClass (handle types accordingly) - remove the I prefix from storage config --- src/core/server.ts | 36 +++++++++++++++++++--------------- src/index.ts | 4 ++-- src/interfaces/config.ts | 2 +- src/services/storageService.ts | 16 +++++++-------- src/test/index.ts | 6 +++++- src/test/testConfig.ts | 4 ++-- 6 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/core/server.ts b/src/core/server.ts index 7fcf444..a8cceb8 100644 --- a/src/core/server.ts +++ b/src/core/server.ts @@ -2,35 +2,39 @@ import express, { Request, Response, Express } from "express"; import cors from "cors"; import MockServerHandler from "./common/mockHandler"; -import { IConfig } from "../interfaces/config"; +import { Config } from "../interfaces/config"; import storageService from "../services/storageService"; import { MockServerResponse } from "../types"; import { HarMiddleware } from "../middlewares/har"; import { cleanupPath } from "./utils"; -interface MockServerConfig { +interface MockServerOptions { port: number; pathPrefix: string; + storageConfig: Config; } +/* To make the constructor options optional except for storageConfig */ +type MockServerConstructorOptions = Pick & Partial; + class MockServer { - mockConfig: MockServerConfig; - config: IConfig; + serverOptions: MockServerOptions; app: Express - constructor (config: IConfig, port: number = 3000, pathPrefix: string = "") { - this.mockConfig = { - port, - pathPrefix + constructor (options: MockServerConstructorOptions) { + this.serverOptions = { + storageConfig: options.storageConfig, + port: options.port ?? 3000, + pathPrefix: options.pathPrefix ?? "", }; - this.config = config; + this.app = this.setup(); } start = () => { - this.app.listen(this.mockConfig.port, () => { - console.log(`Mock Server Listening on port ${this.mockConfig.port}`); + this.app.listen(this.serverOptions.port, () => { + console.log(`Mock Server Listening on port ${this.serverOptions.port}`); }) } @@ -63,17 +67,17 @@ class MockServer { })); // pathPrefix to handle /mockv2 prefix in cloud functions - const regex = new RegExp(`${this.mockConfig.pathPrefix}\/(.+)`); + const regex = new RegExp(`${this.serverOptions.pathPrefix}\/(.+)`); app.all(regex, async (req: Request, res: Response) => { console.log(`Initial Request`); console.log(`Path: ${req.path}`); console.log(`Query Params: ${JSON.stringify(req.query)}`); // Stripping URL prefix - if(req.path.indexOf(this.mockConfig.pathPrefix) === 0) { - console.log(`Stripping pathPrefix: ${this.mockConfig.pathPrefix}`); + if(req.path.indexOf(this.serverOptions.pathPrefix) === 0) { + console.log(`Stripping pathPrefix: ${this.serverOptions.pathPrefix}`); Object.defineProperty(req, 'path', { - value: cleanupPath(req.path.slice(this.mockConfig.pathPrefix.length)), + value: cleanupPath(req.path.slice(this.serverOptions.pathPrefix.length)), writable: true }); console.log(`Path after stripping prefix and cleanup: ${req.path}`); @@ -90,7 +94,7 @@ class MockServer { } initStorageService = () => { - storageService.setConfig(this.config); + storageService.setConfig(this.serverOptions.storageConfig); } } diff --git a/src/index.ts b/src/index.ts index 72fa3b4..4cb107e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,10 @@ -import {IConfig, ISink, ISource} from "./interfaces/config"; +import {Config, ISink, ISource} from "./interfaces/config"; 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, - IConfig, ISink, ISource, + Config, ISink, ISource, MockSchema, MockMetadataSchema, MockResponseSchema, diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index e1d55a4..e4953b0 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -40,7 +40,7 @@ export class ISource { } -export interface IConfig { +export interface Config { src: ISource; sink?: ISink; } \ No newline at end of file diff --git a/src/services/storageService.ts b/src/services/storageService.ts index 17cddf6..0568028 100644 --- a/src/services/storageService.ts +++ b/src/services/storageService.ts @@ -1,27 +1,27 @@ -import {IConfig, ISink, ISource} from "../interfaces/config"; +import {Config, ISink, ISource} from "../interfaces/config"; import { Log } from "../types"; class StorageService { - src: ISource | null = null; + source: ISource | null = null; sink: ISink | null = null; - constructor(config?: IConfig) { - this.src = config?.src || null; + constructor(config?: Config) { + this.source = config?.src || null; this.sink = config?.sink || null; } // TODO: This should be set when starting the mock server - setConfig = (config: IConfig) => { - this.src = config.src || null; + setConfig = (config: Config) => { + this.source = config.src || null; this.sink = config.sink || null; } getMockSelectorMap = async (kwargs ?: any): Promise => { - return this.src?.getMockSelectorMap(kwargs); + return this.source?.getMockSelectorMap(kwargs); }; getMock = async (id: string, kwargs?: any): Promise => { - return this.src?.getMock(id, kwargs); + return this.source?.getMock(id, kwargs); } storeLog = async (log: Log): Promise => { diff --git a/src/test/index.ts b/src/test/index.ts index daf5eac..0651f7c 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -1,6 +1,10 @@ import MockServer from "../core/server"; import TestConfig from "./testConfig"; -const server = new MockServer(TestConfig, 3001, "/mocksv2"); +const server = new MockServer({ + port: 3001, + pathPrefix: "/mocksv2", + storageConfig: TestConfig +}); console.debug(server.app); server.start(); diff --git a/src/test/testConfig.ts b/src/test/testConfig.ts index 4b93e95..b2f3592 100644 --- a/src/test/testConfig.ts +++ b/src/test/testConfig.ts @@ -1,9 +1,9 @@ -import { IConfig } from "../interfaces/config"; +import { Config } from "../interfaces/config"; import { DummySource } from "./dummy/dummySource"; import { FileLogSink } from "./dummy/FileLogSink"; -const testConfig: IConfig = { +const testConfig: Config = { src: new DummySource(), sink: new FileLogSink(), }