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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
node_modules/
build/
build/
.DS_Store
*.log
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand All @@ -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
Expand All @@ -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;
```


Expand All @@ -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
11 changes: 11 additions & 0 deletions package-lock.json

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

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

console.debug("[Debug] No Mock Selected");
Expand Down
39 changes: 24 additions & 15 deletions src/core/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ 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 { HarMiddleware } from "../middlewares/har";
import { cleanupPath } from "./utils";

interface MockServerConfig {
Expand All @@ -13,30 +14,36 @@ interface MockServerConfig {
}

class MockServer {
config: MockServerConfig;
configFetcher: IConfigFetcher;
mockConfig: MockServerConfig;
config: IConfig;
Copy link
Member

Choose a reason for hiding this comment

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

These both can be merged into 1 as both are same.

mockServerConfig: MockServerConfig

Also IConfig is incorrect name as prefix I is used for marking abstract classes

Copy link
Member

Choose a reason for hiding this comment

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

This can be converted to this
constructor (config: MockServerConfig)

app: Express

constructor (port: number = 3000, configFetcher: IConfigFetcher, pathPrefix: string = "") {
this.config = {
constructor (config: IConfig, port: number = 3000, pathPrefix: string = "") {
this.mockConfig = {
port,
pathPrefix
};
this.configFetcher = configFetcher;
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}`);
})
}

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

const app = express();

// Use middleware to parse `application/json` and `application/x-www-form-urlencoded` body data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use(HarMiddleware);

app.use((_, res, next) => {
res.set({
Expand All @@ -56,32 +63,34 @@ 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}`);
}

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

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

return app;
}

initStorageService = () => {
storageService.setConfigFetcher(this.configFetcher);
storageService.setConfig(this.config);
}
}

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

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

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

return harHeaders;
};

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

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

return postData;
}
return undefined;
}

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

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

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

return queryString;
}

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

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

import {Log as MockLog} from "./types";
export {
MockServer,
IConfigFetcher,
IConfig, ISink, ISource,
MockSchema,
MockMetadataSchema,
MockResponseSchema,
MockLog,
};
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import { Mock } from "../types/mock";

class IConfigFetcher {

import { Log } from "../types";

export class ISink {
/**
* specify how and where to store logs from mock execution
*/
sendLog = (log: Log): Promise<void> => {
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}


/**
Expand All @@ -25,9 +36,11 @@ class IConfigFetcher {
* }
* }
*/
getMockSelectorMap = (kwargs?: any): any => {
return {}
}
getMockSelectorMap = (kwargs?: any): any => {return {}}
}

export default IConfigFetcher;

export interface IConfig {
src: ISource;
sink?: ISink;
}
Loading