Skip to content

Commit

Permalink
Add JSDocs
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuajaco committed Jul 30, 2023
1 parent 5540f9b commit a403e03
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 8 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ test("custom assertion", async () => {

### `constructor(options): MockServer`

Creates a new [`MockServer`](#mockserver) instance.
Create a new [`MockServer`](#mockserver) instance.

| Param | Type | Default |
| ------- | --------------------- | ------- |
Expand All @@ -264,7 +264,7 @@ const mockServer = new MockServer({ port: 3000 });

### `start(): Promise<void>`

Starts the mock server.
Start the mock server.

#### Example

Expand All @@ -276,7 +276,7 @@ await mockServer.start();

### `stop(): Promise<void>`

Stops the mock server.
Stop the mock server.

#### Example

Expand Down
214 changes: 209 additions & 5 deletions src/MockServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,53 @@ import bodyParser from "body-parser";
import { matchRequest } from "./matchRequest";
import type { Matcher, MatcherObj } from "./matchRequest";

/**
* @property {number} status - status code to respond with
* @property {Record<string, string>} headers - headers to respond with
* @property {string | object} body - body to respond with
*/
export type ResponseObj = {
status?: number;
body?: string | object;
headers?: Record<string, string>;
};

/**
* @param {express.Request} req - request to match against
* @returns {ResponseObj} response to respond with
*/
export type ResponseFn = (req: express.Request) => ResponseObj;

/**
* response to respond with
*/
export type Response = ResponseObj | ResponseFn;

/**
* @property {boolean} overwrite - when set to `true`, previous mocks matching the same request will be overwritten
*/
export type MockOptions = { overwrite?: boolean };

/**
* @property {Matcher} matcher - matcher to match against the request
* @property {Response} response - response the server will respond with when matched
* @property {MockOptions} options
*/
export type Mock = {
matcher: Matcher;
response: Response;
options: MockOptions;
};

/**
* @property {express.Request} request - request the server was called with
* @property {Matcher} matcher - matcher thr request matched against
*/
export type Call = { request: express.Request; matcher: Matcher };

/**
* @property {number} port - port to run the mock server on
*/
export type Options = { port: number };

export class MockServer {
Expand All @@ -32,6 +59,13 @@ export class MockServer {
#server: http.Server | null = null;
readonly #app = express();

/**
* Create a new mock server instance.
* @param {Options} options
* @return {MockServer}
* @example
* const mockServer = new MockServer({ port: 3000 });
*/
constructor(private readonly options: Options) {
this.#app.use(bodyParser.raw({ type: "*/*" }));

Expand Down Expand Up @@ -81,6 +115,13 @@ export class MockServer {
});
}

/**
* Start the mock server.
* @async
* @return {Promise<void>}
* @example
* await mockServer.start();
*/
public start(): Promise<void> {
if (this.#server) {
console.warn("Server is already running");
Expand All @@ -92,17 +133,43 @@ export class MockServer {
});
}

/**
* Stop the mock server.
* @async
* @return {Promise<void>}
* @example
* await mockServer.stop();
*/
public stop(): Promise<void> {
const server = this.#server;
if (server) return new Promise((resolve) => server.close(() => resolve()));
console.warn("No server is running");
return Promise.resolve();
}

/**
* Get the port the mock server is running on.
* @return {number} port
* @example
* const port = mockServer.port();
*/
public port() {
return this.options.port;
}

/**
* Register a mock.
* @param {Matcher} matcher
* @param {string | number | Response} response - If response is a `string`, it will be used as the response body - If response is a `number`, it will be used as the response status code
* @param {MockOptions} [options={}]
* @return {MockServer} this
* @example
* mockServer.mock({ path:"/test" }, { status: 204 });
*
* const response = await fetch("http://localhost:3000/test");
*
* console.log(response.status); // 204
*/
public mock(
matcher: Matcher,
response: string | number | Response,
Expand All @@ -122,43 +189,180 @@ export class MockServer {
return this;
}

/**
* Register a mock that only responds to requests using the http `GET` method.
* @param {string | RegExp | Omit<MatcherObj, "method">} matcher - If matcher is a `string` or `RegExp`, it will be used to match the request path
* @param {string | number | Response} response - If response is a `string`, it will be used as the response body - If response is a `number`, it will be used as the response status code
* @param {MockOptions} [options={}]
* @return {MockServer} this
* @example
* mockServer.get("/test", {
* status: 200,
* body: { message: "Hello World" },
* });
*
* const response = await fetch("http://localhost:3000/test");
*
* console.log(response.status); // 200
* console.log(await response.json()); // { message: "Hello World" }
*/
public get = this.#createMockFn("GET");

/**
* Register a mock that only responds to requests using the http `POST` method.
* @param {string | RegExp | Omit<MatcherObj, "method">} matcher - If matcher is a `string` or `RegExp`, it will be used to match the request path
* @param {string | number | Response} response - If response is a `string`, it will be used as the response body - If response is a `number`, it will be used as the response status code
* @param {MockOptions} [options={}]
* @return {MockServer} this
* @example
* mockServer.post("/test", {
* status: 201,
* body: { message: "Hello World" },
* });
*
* const response = await fetch("http://localhost:3000/test", {
* method: "POST",
* body: JSON.stringify({ message: "Hello World" }),
* });
*
* console.log(response.status); // 201
* console.log(await response.json()); // { message: "Hello World" }
*/
public post = this.#createMockFn("POST");

/**
* Register a mock that only responds to requests using the http `PATCH` method.
* @param {string | RegExp | Omit<MatcherObj, "method">} matcher - If matcher is a `string` or `RegExp`, it will be used to match the request path
* @param {string | number | Response} response - If response is a `string`, it will be used as the response body - If response is a `number`, it will be used as the response status code
* @param {MockOptions} [options={}]
* @return {MockServer} this
* @example
* mockServer.patch("/test", {
* status: 200,
* body: { message: "Hello World" },
* });
*
* const response = await fetch("http://localhost:3000/test", {
* method: "PATCH",
* body: JSON.stringify({ message: "Hello World" }),
* });
*
* console.log(response.status); // 200
* console.log(await response.json()); // { message: "Hello World" }
*/
public patch = this.#createMockFn("PATCH");

/**
* Register a mock that only responds to requests using the http `DELETE` method.
* @param {string | RegExp | Omit<MatcherObj, "method">} matcher - If matcher is a `string` or `RegExp`, it will be used to match the request path
* @param {string | number | Response} response - If response is a `string`, it will be used as the response body - If response is a `number`, it will be used as the response status code
* @param {MockOptions} [options={}]
* @return {MockServer} this
* @example
* mockServer.delete("/test", { status: 204 });
*
* const response = await fetch("http://localhost:3000/test", {
* method: "DELETE",
* });
*
* console.log(response.status); // 204
*/
public delete = this.#createMockFn("DELETE");

/**
* Get all registered mocks.
* @return {readonly Mock[]} mocks
* @example
* mockServer.mock({ path: "/test" }, { status: 204 });
*
* const mocks = mockServer.mocks();
* console.log(mocks); // [{ matcher: "/test", response: { status: 204 } }]
*/
public mocks(): readonly Mock[] {
return this.#mocks.slice();
}

/**
* Get all registered calls.
* @return {readonly Call[]} calls
* @example
* await fetch("http://localhost:3000/test");
*
* const calls = mockServer.calls();
* console.log(calls);
* // [{ matcher: { path: "/test", request: <express.Request> } }]
*/
public calls(): readonly Call[] {
return this.#calls.slice();
}

/**
* Check if the route has been called with the given `matcher`.
* @param {Matcher} matcher
* @return {boolean} `true` if the route has been called with the given `matcher`, `false` otherwise
* @example
* await fetch("http://localhost:3000/test");
*
* console.log(mockServer.hasBeenCalledWith({ path: "/test" })); // true
*/
public hasBeenCalledWith(matcher: Matcher) {
return this.#calls.some(({ request }) => matchRequest(matcher, request));
}

/**
* Check if the route has been called a certain number of times with the given `matcher`.
* @param {number} times
* @param {Matcher} matcher
* @return {boolean} `true` if the route has been called `times` times with the given `matcher`, `false` otherwise
* @example
* await fetch("http://localhost:3000/test");
*
* console.log(mockServer.hasBeenCalledTimes(0, { path: "/test" })); // false
* console.log(mockServer.hasBeenCalledTimes(1, { path: "/test" })); // true
*/
public hasBeenCalledTimes(times: number, matcher: Matcher) {
return (
this.#calls.filter(({ request }) => matchRequest(matcher, request))
.length === times
);
}

/**
* Reset all mocks and calls.
* @return {void}
* @example
* mockServer.reset();
*
* console.log(mockServer.mocks()); // []
* console.log(mockServer.calls()); // []
*/
public reset() {
this.resetMocks();
this.resetCalls();
}

/**
* Reset all mocks.
* @return {void}
* @example
* mockServer.resetMocks();
* console.log(mockServer.mocks()); // []
*/
public resetMocks() {
this.#mocks = [];
}

/**
* Reset all calls.
* @return {void}
* @example
* mockServer.resetCalls();
* console.log(mockServer.calls()); // []
*/
public resetCalls() {
this.#calls = [];
}

public reset() {
this.resetMocks();
this.resetCalls();
}

#createMockFn(method: string) {
return (
matcher: string | RegExp | Omit<MatcherObj, "method">,
Expand Down
14 changes: 14 additions & 0 deletions src/matchRequest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import type express from "express";
import deepEqual from "deep-equal";

/**
* @property {string} method - http method to match against
* @property {string | RegExp} path - path to match against
* @property {express.Request["query"]} query - query parameters to match against
* @property {Record<string, string | undefined>} headers - headers to match against
* @property {string | object} body - to match against
*/
export type MatcherObj = {
method?: string;
path?: string | RegExp;
Expand All @@ -9,8 +16,15 @@ export type MatcherObj = {
body?: string | object;
};

/**
* @param {express.Request} req - request to match against
* @returns {boolean} whether the request should match
*/
export type MatcherFn = (req: express.Request) => boolean;

/**
* matcher to match against the request
*/
export type Matcher = MatcherObj | MatcherFn;

export function matchRequest(matcher: Matcher, req: express.Request): boolean {
Expand Down

0 comments on commit a403e03

Please sign in to comment.