Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Mock response status and headers #7

Merged
merged 6 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
## 📖 Usage

```ts
import { mock, clearMocks } from "bun-bagel";
import { mock } from "bun-bagel";

// Register the mock for the example URL.
mock("https://example.com/api/users/*", { data: { name: "Foo" } });
Expand Down Expand Up @@ -69,7 +69,58 @@ describe("Unit Test", () => {

```

### Mock by headers and method
```ts
import { mock } from "bun-bagel";
import type { MockOptions } from "bun-bagel";

const options: MockOptions = {
method: "POST",
headers: { "x-foo-bar": "baz" },
response: {
data: { name: "Foo" },
}
};

// Register the mock for the example URL.
mock("https://example.com/api/users/*", options);

// Make a fetch request to the mocked URL
const response = await fetch("https://example.com/api/users/123", { headers: { "x-foo-bar": "baz" } });

// Requests without the headers will not be matched.
const response2 = await fetch("https://example.com/api/users/123");

// Check the response body.
console.log(await response.json()); // => { name: "Foo" }
```

### Mock response status and headers
```ts
import { mock } from "bun-bagel";
import type { MockOptions } from "bun-bagel";

const options: MockOptions = {
response: {
data: { name: "Foo" },
status: 404,
headers: { "x-foo-bar": "baz" },
}
};

// Register the mock for the example URL.
mock("https://example.com/api/users/*", options);

// Make a fetch request to the mocked URL
const response = await fetch("https://example.com/api/users/123");

// Check the status and headers.
console.log(response.status); // => 404
console.log(response.headers); // => { "x-foo-bar": "baz" }
```

## 📝 License
This project is licensed under the terms of the MIT license. See the LICENSE file for details.

#### 📢 Thanks to all contributors for making this library better!
#### 🤖 Thanks to Gemini for generating most of this code and readme.
9 changes: 8 additions & 1 deletion sandbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@ const data = { foo: "bar" };
// Mock fetch

if (MOCK_FETCH)
mock(url, { method, headers, data });
mock(url, {
method, headers, data, response: {
status: 418,
headers: new Headers({ "x-baz-qux": "quux" }),
}
});

// Call fetch method

const response = await fetch(url, { method, headers });
console.log("Response =>", response);
console.log("Status =>", response.status);
console.log("Headers =>", response.headers);

const body = await response.json();
console.log("Body =>", body);
26 changes: 5 additions & 21 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,9 @@ export const DEFAULT_MOCK_OPTIONS: MockOptions = {
method: 'GET',
data: null,
headers: new Headers(),
};

/**
* Map of status codes to status text.
*/
export const STATUS_TEXT_MAP = {
200: "OK",
201: "Created",
204: "No Content",
304: "Not Modified",
400: "Bad Request",
401: "Unauthorized",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
409: "Conflict",
418: "I'm a teapot",
422: "Unprocessable Entity",
429: "Too Many Requests",
500: "Internal Server Error",
503: "Service Unavailable",
response: {
data: null,
headers: new Headers(),
status: 200,
}
};
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './mock';
export * from './types';
export type * from './types';
44 changes: 27 additions & 17 deletions src/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,24 @@ export const mock = (request: Request | RegExp | string, options: MockOptions =
// Check if request is already mocked.
const isRequestMocked = [...MOCKED_REQUESTS.entries()].find(findRequest([regexInput.toString(), options]));

if (!isRequestMocked) {
if(process.env.VERBOSE) {
if (!isRequestMocked)
console.debug("\x1b[1mRegistered mocked request\x1b[0m");
else
console.debug("\x1b[1mRequest already mocked\x1b[0m \x1b[2mupdated\x1b[0m");

console.debug("\x1b[2mURL\x1b[0m", input);
console.debug("\x1b[2mPath Pattern\x1b[0m", regexInput);
console.debug("\x1b[2mStatus\x1b[0m", options.response?.status || 200);
console.debug("\x1b[2mMethod\x1b[0m", options.method || "GET");
console.debug("\n");
}

if(!isRequestMocked)
// Use regex as key.
MOCKED_REQUESTS.set(regexInput, options);

if(process.env.VERBOSE) {
console.debug("\x1b[1mRegistered mocked request\x1b[0m");
console.debug("\x1b[2mPath Pattern\x1b[0m", regexInput);
console.debug("\x1b[2mMethod\x1b[0m", options.method);
console.debug("\n");
}
} else {
if(process.env.VERBOSE)
console.debug("\x1b[1mRequest already mocked\x1b[0m", regexInput);
else
return;
}

if (!ORIGINAL_FETCH) {
// Cache the original fetch method before mocking it. Might be useful in the future to clean the mock.
Expand All @@ -44,17 +47,22 @@ export const mock = (request: Request | RegExp | string, options: MockOptions =
// @ts-ignore
globalThis.fetch = MOCKED_FETCH;
}
return true;
}

/**
* @description Clear the fetch mock.
*/
export const clearMocks = () => {
MOCKED_REQUESTS.clear();
// @ts-ignore
globalThis.fetch = ORIGINAL_FETCH;
// @ts-ignore
ORIGINAL_FETCH = undefined;

// Restore the original fetch method, if it was mocked.
if(!!ORIGINAL_FETCH) {
// @ts-ignore
globalThis.fetch = ORIGINAL_FETCH.bind({});
// @ts-ignore
ORIGINAL_FETCH = undefined;
}
}

/**
Expand All @@ -72,6 +80,8 @@ const MOCKED_FETCH = async (_request: Request | RegExp | string, init?: RequestI
if(process.env.VERBOSE)
console.debug("\x1b[2mMocked fetch called\x1b[0m", _path);

return makeResponse(200, _path, mockedRequest[1]);
const mockedStatus = mockedRequest[1].response?.status || 200;

return makeResponse(mockedStatus, _path, mockedRequest[1]);
};

12 changes: 12 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,19 @@
* Partial implementation of RequestInit with the addition of "data" property which value will be returned from the mock.
*/
export type MockOptions = {
/** @deprecated use response.data */
Copy link
Owner Author

Choose a reason for hiding this comment

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

I don't feel confidend deprecating the root data property and using the response.data instead.
It's logically correct to have the data inside the response object, as it de facto mocks the response body, but it also gets rid of the simplicity of mock('..', { data: {...} }).
On the other hand, keeping both seems unnecessary.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think having two ways to do such a same thing is a good idea, I find it unnecessary and confusing. I would suggest deprecating it.

data?: any;
headers?: RequestInit['headers'];
method?: RequestInit['method'];
response?: MockResponse;
};

/**
* @description The response for a mocked request.
* Partial implementation of Response with the addition of "data" property which value will be returned from the mock.
*/
export interface MockResponse {
data?: any;
status?: number;
headers?: RequestInit['headers'];
};
17 changes: 9 additions & 8 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DEFAULT_MOCK_OPTIONS, STATUS_TEXT_MAP } from "./constants";
import { DEFAULT_MOCK_OPTIONS } from "./constants";
import { MockOptions } from "./types";

/**
Expand Down Expand Up @@ -63,20 +63,21 @@ export const findRequest = (original: [string, RequestInit?]) => (mocked: [RegEx
* @param options - The options for the mocked request.
* @returns An object similar to Response class.
*/
export const makeResponse = (status: keyof typeof STATUS_TEXT_MAP, url: string, options: MockOptions = DEFAULT_MOCK_OPTIONS) => {
const { headers, data } = options;
export const makeResponse = (status: number, url: string, options: MockOptions = DEFAULT_MOCK_OPTIONS) => {
const { headers, data, response } = options;

const ok = status >= 200 && status < 300;
const body = response?.data ?? data;

return {
ok,
status,
statusText: STATUS_TEXT_MAP[status],
statusText: status,
url,
headers,
text: () => Promise.resolve(data),
json: () => Promise.resolve(data),
headers: response?.headers ?? headers,
text: () => Promise.resolve(body),
json: () => Promise.resolve(body),
redirected: false,
bodyUsed: !!data
bodyUsed: !!body
};
}
Loading
Loading