From 7139dcf487a1462f174cb4bb36e0c1e07d40a8ba Mon Sep 17 00:00:00 2001 From: Gagan Ganapathy Ajjikuttira Date: Tue, 31 Oct 2023 23:55:58 -0700 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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