From f2ed6d616d8b9f770e05090c61b4ac9cffea2546 Mon Sep 17 00:00:00 2001 From: "mike.eling97" Date: Mon, 5 Aug 2024 13:52:14 +0000 Subject: [PATCH 1/4] :sparkles: Add option to mock response status --- sandbox/index.ts | 5 +- src/constants.ts | 21 --- src/index.ts | 1 + src/mock.ts | 9 +- src/types.ts | 382 +++++++++++++++++++++++++++++++++++++++++++++++ src/utils.ts | 8 +- 6 files changed, 395 insertions(+), 31 deletions(-) diff --git a/sandbox/index.ts b/sandbox/index.ts index f90b38b..3673c0f 100644 --- a/sandbox/index.ts +++ b/sandbox/index.ts @@ -1,4 +1,4 @@ -import { mock } from '../dist/index'; +import { mock, HttpStatusCode } from '../dist/index'; const MOCK_FETCH = true; @@ -13,12 +13,13 @@ const data = { foo: "bar" }; // Mock fetch if (MOCK_FETCH) - mock(url, { method, headers, data }); + mock(url, { method, headers, data, status: HttpStatusCode.I_AM_A_TEAPOT }); // Call fetch method const response = await fetch(url, { method, headers }); console.log("Response =>", response); +console.log("Status =>", response.status); const body = await response.json(); console.log("Body =>", body); \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index a145282..d4fa5a8 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,25 +7,4 @@ 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", }; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 80eb450..a39d418 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export * from './mock'; +export * from './types'; export type * from './types'; \ No newline at end of file diff --git a/src/mock.ts b/src/mock.ts index 6ba5a61..0058d48 100644 --- a/src/mock.ts +++ b/src/mock.ts @@ -1,5 +1,5 @@ import { DEFAULT_MOCK_OPTIONS } from "./constants"; -import { MockOptions } from "./types"; +import { MockOptions, HttpStatusCode } from "./types"; import { findRequest, makeResponse, wildcardToRegex } from "./utils"; let ORIGINAL_FETCH: (request: Request, init?: RequestInit | undefined) => Promise; @@ -28,6 +28,7 @@ export const mock = (request: Request | RegExp | string, options: MockOptions = if(process.env.VERBOSE) { console.debug("\x1b[1mRegistered mocked request\x1b[0m"); console.debug("\x1b[2mPath Pattern\x1b[0m", regexInput); + console.debug("\x1b[2mStatus\x1b[0m", options.status); console.debug("\x1b[2mMethod\x1b[0m", options.method); console.debug("\n"); } @@ -42,7 +43,7 @@ export const mock = (request: Request | RegExp | string, options: MockOptions = ORIGINAL_FETCH = globalThis.fetch; // @ts-ignore - globalThis.fetch = MOCKED_FETCH; + globalThis.fetch = MOCKED_FETCH(options); } } @@ -60,7 +61,7 @@ export const clearMocks = () => { /** * @description A mocked fetch method. */ -const MOCKED_FETCH = async (_request: Request | RegExp | string, init?: RequestInit) => { +const MOCKED_FETCH = (options: MockOptions) => async (_request: Request | RegExp | string, init?: RequestInit) => { const _path = _request instanceof Request ? _request.url : _request.toString(); // When the request it fired, check if it matches a mocked request. @@ -72,6 +73,6 @@ 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]); + return makeResponse(options.status ?? HttpStatusCode.OK, _path, mockedRequest[1]); }; diff --git a/src/types.ts b/src/types.ts index 8d27c28..42d6855 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,4 +6,386 @@ export type MockOptions = { data?: any; headers?: RequestInit['headers']; method?: RequestInit['method']; + status?: HttpStatusCode; }; + +/** + * Hypertext Transfer Protocol (HTTP) response status codes. + * @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes} + * @see credits {@link https://gist.github.com/scokmen/f813c904ef79022e84ab2409574d1b45} + */ +export enum HttpStatusCode { + + /** + * The server has received the request headers and the client should proceed to send the request body + * (in the case of a request for which a body needs to be sent; for example, a POST request). + * Sending a large request body to a server after a request has been rejected for inappropriate headers would be inefficient. + * To have a server check the request's headers, a client must send Expect: 100-continue as a header in its initial request + * and receive a 100 Continue status code in response before sending the body. The response 417 Expectation Failed indicates the request should not be continued. + */ + CONTINUE = 100, + + /** + * The requester has asked the server to switch protocols and the server has agreed to do so. + */ + SWITCHING_PROTOCOLS = 101, + + /** + * A WebDAV request may contain many sub-requests involving file operations, requiring a long time to complete the request. + * This code indicates that the server has received and is processing the request, but no response is available yet. + * This prevents the client from timing out and assuming the request was lost. + */ + PROCESSING = 102, + + /** + * Standard response for successful HTTP requests. + * The actual response will depend on the request method used. + * In a GET request, the response will contain an entity corresponding to the requested resource. + * In a POST request, the response will contain an entity describing or containing the result of the action. + */ + OK = 200, + + /** + * The request has been fulfilled, resulting in the creation of a new resource. + */ + CREATED = 201, + + /** + * The request has been accepted for processing, but the processing has not been completed. + * The request might or might not be eventually acted upon, and may be disallowed when processing occurs. + */ + ACCEPTED = 202, + + /** + * SINCE HTTP/1.1 + * The server is a transforming proxy that received a 200 OK from its origin, + * but is returning a modified version of the origin's response. + */ + NON_AUTHORITATIVE_INFORMATION = 203, + + /** + * The server successfully processed the request and is not returning any content. + */ + NO_CONTENT = 204, + + /** + * The server successfully processed the request, but is not returning any content. + * Unlike a 204 response, this response requires that the requester reset the document view. + */ + RESET_CONTENT = 205, + + /** + * The server is delivering only part of the resource (byte serving) due to a range header sent by the client. + * The range header is used by HTTP clients to enable resuming of interrupted downloads, + * or split a download into multiple simultaneous streams. + */ + PARTIAL_CONTENT = 206, + + /** + * The message body that follows is an XML message and can contain a number of separate response codes, + * depending on how many sub-requests were made. + */ + MULTI_STATUS = 207, + + /** + * The members of a DAV binding have already been enumerated in a preceding part of the (multistatus) response, + * and are not being included again. + */ + ALREADY_REPORTED = 208, + + /** + * The server has fulfilled a request for the resource, + * and the response is a representation of the result of one or more instance-manipulations applied to the current instance. + */ + IM_USED = 226, + + /** + * Indicates multiple options for the resource from which the client may choose (via agent-driven content negotiation). + * For example, this code could be used to present multiple video format options, + * to list files with different filename extensions, or to suggest word-sense disambiguation. + */ + MULTIPLE_CHOICES = 300, + + /** + * This and all future requests should be directed to the given URI. + */ + MOVED_PERMANENTLY = 301, + + /** + * This is an example of industry practice contradicting the standard. + * The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect + * (the original describing phrase was "Moved Temporarily"), but popular browsers implemented 302 + * with the functionality of a 303 See Other. Therefore, HTTP/1.1 added status codes 303 and 307 + * to distinguish between the two behaviours. However, some Web applications and frameworks + * use the 302 status code as if it were the 303. + */ + FOUND = 302, + + /** + * SINCE HTTP/1.1 + * The response to the request can be found under another URI using a GET method. + * When received in response to a POST (or PUT/DELETE), the client should presume that + * the server has received the data and should issue a redirect with a separate GET message. + */ + SEE_OTHER = 303, + + /** + * Indicates that the resource has not been modified since the version specified by the request headers If-Modified-Since or If-None-Match. + * In such case, there is no need to retransmit the resource since the client still has a previously-downloaded copy. + */ + NOT_MODIFIED = 304, + + /** + * SINCE HTTP/1.1 + * The requested resource is available only through a proxy, the address for which is provided in the response. + * Many HTTP clients (such as Mozilla and Internet Explorer) do not correctly handle responses with this status code, primarily for security reasons. + */ + USE_PROXY = 305, + + /** + * No longer used. Originally meant "Subsequent requests should use the specified proxy." + */ + SWITCH_PROXY = 306, + + /** + * SINCE HTTP/1.1 + * In this case, the request should be repeated with another URI; however, future requests should still use the original URI. + * In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the original request. + * For example, a POST request should be repeated using another POST request. + */ + TEMPORARY_REDIRECT = 307, + + /** + * The request and all future requests should be repeated using another URI. + * 307 and 308 parallel the behaviors of 302 and 301, but do not allow the HTTP method to change. + * So, for example, submitting a form to a permanently redirected resource may continue smoothly. + */ + PERMANENT_REDIRECT = 308, + + /** + * The server cannot or will not process the request due to an apparent client error + * (e.g., malformed request syntax, too large size, invalid request message framing, or deceptive request routing). + */ + BAD_REQUEST = 400, + + /** + * Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet + * been provided. The response must include a WWW-Authenticate header field containing a challenge applicable to the + * requested resource. See Basic access authentication and Digest access authentication. 401 semantically means + * "unauthenticated",i.e. the user does not have the necessary credentials. + */ + UNAUTHORIZED = 401, + + /** + * Reserved for future use. The original intention was that this code might be used as part of some form of digital + * cash or micro payment scheme, but that has not happened, and this code is not usually used. + * Google Developers API uses this status if a particular developer has exceeded the daily limit on requests. + */ + PAYMENT_REQUIRED = 402, + + /** + * The request was valid, but the server is refusing action. + * The user might not have the necessary permissions for a resource. + */ + FORBIDDEN = 403, + + /** + * The requested resource could not be found but may be available in the future. + * Subsequent requests by the client are permissible. + */ + NOT_FOUND = 404, + + /** + * A request method is not supported for the requested resource; + * for example, a GET request on a form that requires data to be presented via POST, or a PUT request on a read-only resource. + */ + METHOD_NOT_ALLOWED = 405, + + /** + * The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request. + */ + NOT_ACCEPTABLE = 406, + + /** + * The client must first authenticate itself with the proxy. + */ + PROXY_AUTHENTICATION_REQUIRED = 407, + + /** + * The server timed out waiting for the request. + * According to HTTP specifications: + * "The client did not produce a request within the time that the server was prepared to wait. The client MAY repeat the request without modifications at any later time." + */ + REQUEST_TIMEOUT = 408, + + /** + * Indicates that the request could not be processed because of conflict in the request, + * such as an edit conflict between multiple simultaneous updates. + */ + CONFLICT = 409, + + /** + * Indicates that the resource requested is no longer available and will not be available again. + * This should be used when a resource has been intentionally removed and the resource should be purged. + * Upon receiving a 410 status code, the client should not request the resource in the future. + * Clients such as search engines should remove the resource from their indices. + * Most use cases do not require clients and search engines to purge the resource, and a "404 Not Found" may be used instead. + */ + GONE = 410, + + /** + * The request did not specify the length of its content, which is required by the requested resource. + */ + LENGTH_REQUIRED = 411, + + /** + * The server does not meet one of the preconditions that the requester put on the request. + */ + PRECONDITION_FAILED = 412, + + /** + * The request is larger than the server is willing or able to process. Previously called "Request Entity Too Large". + */ + PAYLOAD_TOO_LARGE = 413, + + /** + * The URI provided was too long for the server to process. Often the result of too much data being encoded as a query-string of a GET request, + * in which case it should be converted to a POST request. + * Called "Request-URI Too Long" previously. + */ + URI_TOO_LONG = 414, + + /** + * The request entity has a media type which the server or resource does not support. + * For example, the client uploads an image as image/svg+xml, but the server requires that images use a different format. + */ + UNSUPPORTED_MEDIA_TYPE = 415, + + /** + * The client has asked for a portion of the file (byte serving), but the server cannot supply that portion. + * For example, if the client asked for a part of the file that lies beyond the end of the file. + * Called "Requested Range Not Satisfiable" previously. + */ + RANGE_NOT_SATISFIABLE = 416, + + /** + * The server cannot meet the requirements of the Expect request-header field. + */ + EXPECTATION_FAILED = 417, + + /** + * This code was defined in 1998 as one of the traditional IETF April Fools' jokes, in RFC 2324, Hyper Text Coffee Pot Control Protocol, + * and is not expected to be implemented by actual HTTP servers. The RFC specifies this code should be returned by + * teapots requested to brew coffee. This HTTP status is used as an Easter egg in some websites, including Google.com. + */ + I_AM_A_TEAPOT = 418, + + /** + * The request was directed at a server that is not able to produce a response (for example because a connection reuse). + */ + MISDIRECTED_REQUEST = 421, + + /** + * The request was well-formed but was unable to be followed due to semantic errors. + */ + UNPROCESSABLE_ENTITY = 422, + + /** + * The resource that is being accessed is locked. + */ + LOCKED = 423, + + /** + * The request failed due to failure of a previous request (e.g., a PROPPATCH). + */ + FAILED_DEPENDENCY = 424, + + /** + * The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field. + */ + UPGRADE_REQUIRED = 426, + + /** + * The origin server requires the request to be conditional. + * Intended to prevent "the 'lost update' problem, where a client + * GETs a resource's state, modifies it, and PUTs it back to the server, + * when meanwhile a third party has modified the state on the server, leading to a conflict." + */ + PRECONDITION_REQUIRED = 428, + + /** + * The user has sent too many requests in a given amount of time. Intended for use with rate-limiting schemes. + */ + TOO_MANY_REQUESTS = 429, + + /** + * The server is unwilling to process the request because either an individual header field, + * or all the header fields collectively, are too large. + */ + REQUEST_HEADER_FIELDS_TOO_LARGE = 431, + + /** + * A server operator has received a legal demand to deny access to a resource or to a set of resources + * that includes the requested resource. The code 451 was chosen as a reference to the novel Fahrenheit 451. + */ + UNAVAILABLE_FOR_LEGAL_REASONS = 451, + + /** + * A generic error message, given when an unexpected condition was encountered and no more specific message is suitable. + */ + INTERNAL_SERVER_ERROR = 500, + + /** + * The server either does not recognize the request method, or it lacks the ability to fulfill the request. + * Usually this implies future availability (e.g., a new feature of a web-service API). + */ + NOT_IMPLEMENTED = 501, + + /** + * The server was acting as a gateway or proxy and received an invalid response from the upstream server. + */ + BAD_GATEWAY = 502, + + /** + * The server is currently unavailable (because it is overloaded or down for maintenance). + * Generally, this is a temporary state. + */ + SERVICE_UNAVAILABLE = 503, + + /** + * The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. + */ + GATEWAY_TIMEOUT = 504, + + /** + * The server does not support the HTTP protocol version used in the request + */ + HTTP_VERSION_NOT_SUPPORTED = 505, + + /** + * Transparent content negotiation for the request results in a circular reference. + */ + VARIANT_ALSO_NEGOTIATES = 506, + + /** + * The server is unable to store the representation needed to complete the request. + */ + INSUFFICIENT_STORAGE = 507, + + /** + * The server detected an infinite loop while processing the request. + */ + LOOP_DETECTED = 508, + + /** + * Further extensions to the request are required for the server to fulfill it. + */ + NOT_EXTENDED = 510, + + /** + * The client needs to authenticate to gain network access. + * Intended for use by intercepting proxies used to control access to the network (e.g., "captive portals" used + * to require agreement to Terms of Service before granting full Internet access via a Wi-Fi hotspot). + */ + NETWORK_AUTHENTICATION_REQUIRED = 511 +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 6146b4f..fad98a7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ -import { DEFAULT_MOCK_OPTIONS, STATUS_TEXT_MAP } from "./constants"; -import { MockOptions } from "./types"; +import { DEFAULT_MOCK_OPTIONS } from "./constants"; +import { MockOptions, HttpStatusCode } from "./types"; /** * @description Convert a wildcard string to a regular expression. @@ -63,7 +63,7 @@ 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) => { +export const makeResponse = (status: HttpStatusCode, url: string, options: MockOptions = DEFAULT_MOCK_OPTIONS) => { const { headers, data } = options; const ok = status >= 200 && status < 300; @@ -71,7 +71,7 @@ export const makeResponse = (status: keyof typeof STATUS_TEXT_MAP, url: string, return { ok, status, - statusText: STATUS_TEXT_MAP[status], + statusText: HttpStatusCode[status], url, headers, text: () => Promise.resolve(data), From e90f566993e0f8401048111c9e76d2cdc655dd98 Mon Sep 17 00:00:00 2001 From: "mike.eling97" Date: Mon, 5 Aug 2024 15:26:35 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=85=20=20Fix=20unit=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants.ts | 1 + src/mock.ts | 26 ++++++++++++++++---------- tests/mock.test.ts | 26 ++++++++++++++++++++++---- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index d4fa5a8..278d53c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,4 +7,5 @@ export const DEFAULT_MOCK_OPTIONS: MockOptions = { method: 'GET', data: null, headers: new Headers(), + status: 200, }; \ No newline at end of file diff --git a/src/mock.ts b/src/mock.ts index 0058d48..ec6cfd9 100644 --- a/src/mock.ts +++ b/src/mock.ts @@ -28,8 +28,8 @@ export const mock = (request: Request | RegExp | string, options: MockOptions = if(process.env.VERBOSE) { console.debug("\x1b[1mRegistered mocked request\x1b[0m"); console.debug("\x1b[2mPath Pattern\x1b[0m", regexInput); - console.debug("\x1b[2mStatus\x1b[0m", options.status); - console.debug("\x1b[2mMethod\x1b[0m", options.method); + console.debug("\x1b[2mStatus\x1b[0m", options.status || 200); + console.debug("\x1b[2mMethod\x1b[0m", options.method || "GET"); console.debug("\n"); } } else { @@ -43,7 +43,7 @@ export const mock = (request: Request | RegExp | string, options: MockOptions = ORIGINAL_FETCH = globalThis.fetch; // @ts-ignore - globalThis.fetch = MOCKED_FETCH(options); + globalThis.fetch = MOCKED_FETCH; } } @@ -52,27 +52,33 @@ export const mock = (request: Request | RegExp | string, options: MockOptions = */ 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; + } } /** * @description A mocked fetch method. */ -const MOCKED_FETCH = (options: MockOptions) => async (_request: Request | RegExp | string, init?: RequestInit) => { +const MOCKED_FETCH = async (_request: Request | RegExp | string, init?: RequestInit) => { const _path = _request instanceof Request ? _request.url : _request.toString(); // When the request it fired, check if it matches a mocked request. const mockedRequest = [...MOCKED_REQUESTS.entries()].find(findRequest([_path, init])); if (!mockedRequest) - return Promise.reject(makeResponse(404, _path)); + return Promise.reject(makeResponse(HttpStatusCode.NOT_FOUND, _path)); if(process.env.VERBOSE) console.debug("\x1b[2mMocked fetch called\x1b[0m", _path); - return makeResponse(options.status ?? HttpStatusCode.OK, _path, mockedRequest[1]); + const mockedStatus = mockedRequest[1].status || HttpStatusCode.OK; + + return makeResponse(mockedStatus, _path, mockedRequest[1]); }; diff --git a/tests/mock.test.ts b/tests/mock.test.ts index 7253b32..f3c8f0b 100644 --- a/tests/mock.test.ts +++ b/tests/mock.test.ts @@ -1,11 +1,14 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, test, afterEach } from "bun:test"; import { clearMocks, mock } from "../src/mock"; +import { HttpStatusCode } from "../src/types"; const API_URL = `https://bun-bagel.sweet/api/v1`; -let randomId = 123456789; - describe("Mock", () => { + afterEach(() => { + clearMocks(); + }); + test("mock: should mock a request", async () => { const request = new Request(`${API_URL}/users`); const options = { @@ -17,6 +20,7 @@ describe("Mock", () => { mock(request, options); const response = await fetch(`${API_URL}/users`); const data = await response.json(); + expect(response.status).toEqual(HttpStatusCode.OK); expect(data).toEqual(options.data); }); @@ -139,6 +143,20 @@ describe("Mock", () => { expect(act).toThrow(); }); + test("mock: should mock a request with response status", async () => { + const request = new Request(`${API_URL}/users`); + const options = { + data: { + id: 1, + name: "John Doe", + }, + status: HttpStatusCode.I_AM_A_TEAPOT, + }; + mock(request, options); + const response = await fetch(`${API_URL}/users`); + expect(response.status).toEqual(HttpStatusCode.I_AM_A_TEAPOT); + }); + test("mock: should not mock a request if it is not registered", async () => { const act = async () => { await fetch(`${API_URL}/posts`); @@ -157,7 +175,7 @@ describe("Mock", () => { const mockedFetch = globalThis.fetch; clearMocks(); mock(`${API_URL}/cats`, { data: { purr: "purr" } }); - expect(globalThis.fetch).toBe(mockedFetch); + expect(globalThis.fetch.name).toBe(mockedFetch.name); }); test("mock: should restore the original fetch method after the test", () => { From 283902480a2167cbef799b0bcbe23a33434efd90 Mon Sep 17 00:00:00 2001 From: "mike.eling97" Date: Mon, 5 Aug 2024 20:50:12 +0000 Subject: [PATCH 3/4] :sparkles: Add better response mocking --- README.md | 53 +++++++++++++++- sandbox/index.ts | 8 ++- src/constants.ts | 6 +- src/mock.ts | 31 ++++----- src/types.ts | 11 ++++ src/utils.ts | 11 ++-- tests/mock.test.ts | 153 ++++++++++++++++++++++++++++----------------- 7 files changed, 192 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 71959a4..5aff3c8 100644 --- a/README.md +++ b/README.md @@ -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" } }); @@ -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. \ No newline at end of file diff --git a/sandbox/index.ts b/sandbox/index.ts index 3673c0f..0122fae 100644 --- a/sandbox/index.ts +++ b/sandbox/index.ts @@ -13,13 +13,19 @@ const data = { foo: "bar" }; // Mock fetch if (MOCK_FETCH) - mock(url, { method, headers, data, status: HttpStatusCode.I_AM_A_TEAPOT }); + mock(url, { + method, headers, data, response: { + status: HttpStatusCode.I_AM_A_TEAPOT, + 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); \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index 278d53c..7aa6fbf 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,5 +7,9 @@ export const DEFAULT_MOCK_OPTIONS: MockOptions = { method: 'GET', data: null, headers: new Headers(), - status: 200, + response: { + data: null, + headers: new Headers(), + status: 200, + } }; \ No newline at end of file diff --git a/src/mock.ts b/src/mock.ts index ec6cfd9..c393a0f 100644 --- a/src/mock.ts +++ b/src/mock.ts @@ -21,22 +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[2mStatus\x1b[0m", options.status || 200); - console.debug("\x1b[2mMethod\x1b[0m", options.method || "GET"); - 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. @@ -45,6 +47,7 @@ export const mock = (request: Request | RegExp | string, options: MockOptions = // @ts-ignore globalThis.fetch = MOCKED_FETCH; } + return true; } /** @@ -77,7 +80,7 @@ const MOCKED_FETCH = async (_request: Request | RegExp | string, init?: RequestI if(process.env.VERBOSE) console.debug("\x1b[2mMocked fetch called\x1b[0m", _path); - const mockedStatus = mockedRequest[1].status || HttpStatusCode.OK; + const mockedStatus = mockedRequest[1].response?.status || HttpStatusCode.OK; return makeResponse(mockedStatus, _path, mockedRequest[1]); }; diff --git a/src/types.ts b/src/types.ts index 42d6855..8b4a7e1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,10 +3,21 @@ * 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 */ 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?: HttpStatusCode; + headers?: RequestInit['headers']; }; /** diff --git a/src/utils.ts b/src/utils.ts index fad98a7..8915e5b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -64,19 +64,20 @@ export const findRequest = (original: [string, RequestInit?]) => (mocked: [RegEx * @returns An object similar to Response class. */ export const makeResponse = (status: HttpStatusCode, url: string, options: MockOptions = DEFAULT_MOCK_OPTIONS) => { - const { headers, data } = options; + const { headers, data, response } = options; const ok = status >= 200 && status < 300; + const body = response?.data ?? data; return { ok, status, statusText: HttpStatusCode[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 }; } \ No newline at end of file diff --git a/tests/mock.test.ts b/tests/mock.test.ts index f3c8f0b..59d9233 100644 --- a/tests/mock.test.ts +++ b/tests/mock.test.ts @@ -1,105 +1,123 @@ -import { describe, expect, test, afterEach } from "bun:test"; +import { describe, expect, test, beforeEach } from "bun:test"; import { clearMocks, mock } from "../src/mock"; -import { HttpStatusCode } from "../src/types"; +import { HttpStatusCode, MockOptions } from "../src/types"; const API_URL = `https://bun-bagel.sweet/api/v1`; describe("Mock", () => { - afterEach(() => { + beforeEach(() => { clearMocks(); }); test("mock: should mock a request", async () => { const request = new Request(`${API_URL}/users`); - const options = { - data: { - id: 1, - name: "John Doe", + const options: MockOptions = { + response: { + data: { + id: 1, + name: "John Doe", + }, }, }; - mock(request, options); + const mocked = mock(request, options); + const response = await fetch(`${API_URL}/users`); const data = await response.json(); + expect(response.status).toEqual(HttpStatusCode.OK); - expect(data).toEqual(options.data); + expect(data).toEqual(options.response?.data); + expect(mocked).toEqual(true); }); test("mock: should not mock a request twice", async () => { const request = new Request(`${API_URL}/users`); - const options = { - data: { - id: 1, - name: "John Doe", + const options: MockOptions = { + response: { + data: { + id: 1, + name: "John Doe", + }, }, }; + expect(mock(request, options)).toBe(true); expect(mock(request, options)).toBe(undefined); await fetch(`${API_URL}/users`); }); test("mock: should mock a request with string path", async () => { const request = `${API_URL}/users`; - const options = { - data: { - id: 1, - name: "John Doe", + const options: MockOptions = { + response: { + data: { + id: 1, + name: "John Doe", + }, }, }; mock(request, options); const response = await fetch(`${API_URL}/users`); const data = await response.json(); - expect(data).toEqual(options.data); + expect(data).toEqual(options.response?.data); }); test("mock: should mock a request with wildcard", async () => { const request = `${API_URL}/users/*`; - const options = { - data: { - id: 1, - name: "John Doe", + const options: MockOptions = { + response: { + data: { + id: 1, + name: "John Doe", + }, }, }; mock(request, options); const response = await fetch(`${API_URL}/users/123`); const data = await response.json(); - expect(data).toEqual(options.data); + expect(data).toEqual(options.response?.data); }); test("mock: should mock a request using a regular expression", async () => { new Request(`${API_URL}/users/1`); - const options = { - data: { - id: 1, - name: "John Doe", + const options: MockOptions = { + response: { + data: { + id: 1, + name: "John Doe", + }, }, }; mock(/\/api\/v1\/users\/\d+/, options); const response = await fetch(`${API_URL}/users/1`); const data = await response.json(); - expect(data).toEqual(options.data); + expect(data).toEqual(options.response?.data); }); test("mock: should mock a request with method", async () => { const request = new Request(`${API_URL}/users`); - const options = { - data: { - id: 1, - name: "John Doe", + const options: MockOptions = { + response: { + data: { + id: 1, + name: "John Doe", + }, }, method: "POST" }; mock(request, options); const response = await fetch(`${API_URL}/users`, { method: "POST" }); const data = await response.json(); - expect(data).toEqual(options.data); + expect(data).toEqual(options.response?.data); }); - + test("mock: should not mock a request with method", async () => { const request = new Request(`${API_URL}/dogs`); - const options = { - data: { - id: 1, - name: "John Doe", + const options: MockOptions = { + response: { + data: { + id: 1, + name: "John Doe", + }, }, method: "POST" }; @@ -113,25 +131,29 @@ describe("Mock", () => { test("mock: should mock a request with headers", async () => { const request = new Request(`${API_URL}/users`); - const options = { - data: { - id: 1, - name: "John Doe", + const options: MockOptions = { + response: { + data: { + id: 1, + name: "John Doe", + }, }, headers: { "x-foo-bar": "Foo" } }; mock(request, options); const response = await fetch(`${API_URL}/users`, { headers: { "x-foo-bar": "Foo" } }); const data = await response.json(); - expect(data).toEqual(options.data); + expect(data).toEqual(options.response?.data); }); test("mock: should not mock a request with headers", async () => { const request = new Request(`${API_URL}/cats`); - const options = { - data: { - id: 1, - name: "John Doe", + const options: MockOptions = { + response: { + data: { + id: 1, + name: "John Doe", + }, }, headers: { "x-foo-bar": "Foo" } }; @@ -145,23 +167,36 @@ describe("Mock", () => { test("mock: should mock a request with response status", async () => { const request = new Request(`${API_URL}/users`); - const options = { - data: { - id: 1, - name: "John Doe", - }, - status: HttpStatusCode.I_AM_A_TEAPOT, + const options: MockOptions = { + response: { + data: { name: "John Doe" }, + status: HttpStatusCode.I_AM_A_TEAPOT, + } }; mock(request, options); const response = await fetch(`${API_URL}/users`); expect(response.status).toEqual(HttpStatusCode.I_AM_A_TEAPOT); }); + test("mock: should mock a request with response headers", async () => { + const request = new Request(`${API_URL}/users`); + const options: MockOptions = { + headers: { "x-foo-bar": "baz" }, + response: { + data: { name: "John Doe" }, + headers: { "x-baz-qux": "quux" }, + } + }; + mock(request, options); + const response = await fetch(`${API_URL}/users`, { headers: { "x-foo-bar": "baz" } }); + expect(response.headers).toEqual({ "x-baz-qux": "quux" }); + }); + test("mock: should not mock a request if it is not registered", async () => { const act = async () => { await fetch(`${API_URL}/posts`); } - expect(act).toThrow(); + expect(act).toThrow(); }); test("mock: should be async", () => { @@ -171,11 +206,11 @@ describe("Mock", () => { }); test("mock: should restore the original fetch method after the test", () => { - mock(`${API_URL}/cats`, { data: { meow: "meow" } }); - const mockedFetch = globalThis.fetch; - clearMocks(); - mock(`${API_URL}/cats`, { data: { purr: "purr" } }); - expect(globalThis.fetch.name).toBe(mockedFetch.name); + mock(`${API_URL}/cats`, { data: { meow: "meow" } }); + const mockedFetch = globalThis.fetch; + clearMocks(); + mock(`${API_URL}/cats`, { data: { purr: "purr" } }); + expect(globalThis.fetch.name).toBe(mockedFetch.name); }); test("mock: should restore the original fetch method after the test", () => { From 90e13a96fc7d48ffe5d5933cc4d8481c252c3f9e Mon Sep 17 00:00:00 2001 From: "mike.eling97" Date: Mon, 5 Aug 2024 21:24:44 +0000 Subject: [PATCH 4/4] :fire: Remove heavy enum --- sandbox/index.ts | 4 +- src/mock.ts | 6 +- src/types.ts | 385 +-------------------------------------------- src/utils.ts | 6 +- tests/mock.test.ts | 8 +- 5 files changed, 14 insertions(+), 395 deletions(-) diff --git a/sandbox/index.ts b/sandbox/index.ts index 0122fae..534a13b 100644 --- a/sandbox/index.ts +++ b/sandbox/index.ts @@ -1,4 +1,4 @@ -import { mock, HttpStatusCode } from '../dist/index'; +import { mock } from '../dist/index'; const MOCK_FETCH = true; @@ -15,7 +15,7 @@ const data = { foo: "bar" }; if (MOCK_FETCH) mock(url, { method, headers, data, response: { - status: HttpStatusCode.I_AM_A_TEAPOT, + status: 418, headers: new Headers({ "x-baz-qux": "quux" }), } }); diff --git a/src/mock.ts b/src/mock.ts index c393a0f..1193ec9 100644 --- a/src/mock.ts +++ b/src/mock.ts @@ -1,5 +1,5 @@ import { DEFAULT_MOCK_OPTIONS } from "./constants"; -import { MockOptions, HttpStatusCode } from "./types"; +import { MockOptions } from "./types"; import { findRequest, makeResponse, wildcardToRegex } from "./utils"; let ORIGINAL_FETCH: (request: Request, init?: RequestInit | undefined) => Promise; @@ -75,12 +75,12 @@ const MOCKED_FETCH = async (_request: Request | RegExp | string, init?: RequestI const mockedRequest = [...MOCKED_REQUESTS.entries()].find(findRequest([_path, init])); if (!mockedRequest) - return Promise.reject(makeResponse(HttpStatusCode.NOT_FOUND, _path)); + return Promise.reject(makeResponse(404, _path)); if(process.env.VERBOSE) console.debug("\x1b[2mMocked fetch called\x1b[0m", _path); - const mockedStatus = mockedRequest[1].response?.status || HttpStatusCode.OK; + const mockedStatus = mockedRequest[1].response?.status || 200; return makeResponse(mockedStatus, _path, mockedRequest[1]); }; diff --git a/src/types.ts b/src/types.ts index 8b4a7e1..43d5b4a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,387 +16,6 @@ export type MockOptions = { */ export interface MockResponse { data?: any; - status?: HttpStatusCode; + status?: number; headers?: RequestInit['headers']; -}; - -/** - * Hypertext Transfer Protocol (HTTP) response status codes. - * @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes} - * @see credits {@link https://gist.github.com/scokmen/f813c904ef79022e84ab2409574d1b45} - */ -export enum HttpStatusCode { - - /** - * The server has received the request headers and the client should proceed to send the request body - * (in the case of a request for which a body needs to be sent; for example, a POST request). - * Sending a large request body to a server after a request has been rejected for inappropriate headers would be inefficient. - * To have a server check the request's headers, a client must send Expect: 100-continue as a header in its initial request - * and receive a 100 Continue status code in response before sending the body. The response 417 Expectation Failed indicates the request should not be continued. - */ - CONTINUE = 100, - - /** - * The requester has asked the server to switch protocols and the server has agreed to do so. - */ - SWITCHING_PROTOCOLS = 101, - - /** - * A WebDAV request may contain many sub-requests involving file operations, requiring a long time to complete the request. - * This code indicates that the server has received and is processing the request, but no response is available yet. - * This prevents the client from timing out and assuming the request was lost. - */ - PROCESSING = 102, - - /** - * Standard response for successful HTTP requests. - * The actual response will depend on the request method used. - * In a GET request, the response will contain an entity corresponding to the requested resource. - * In a POST request, the response will contain an entity describing or containing the result of the action. - */ - OK = 200, - - /** - * The request has been fulfilled, resulting in the creation of a new resource. - */ - CREATED = 201, - - /** - * The request has been accepted for processing, but the processing has not been completed. - * The request might or might not be eventually acted upon, and may be disallowed when processing occurs. - */ - ACCEPTED = 202, - - /** - * SINCE HTTP/1.1 - * The server is a transforming proxy that received a 200 OK from its origin, - * but is returning a modified version of the origin's response. - */ - NON_AUTHORITATIVE_INFORMATION = 203, - - /** - * The server successfully processed the request and is not returning any content. - */ - NO_CONTENT = 204, - - /** - * The server successfully processed the request, but is not returning any content. - * Unlike a 204 response, this response requires that the requester reset the document view. - */ - RESET_CONTENT = 205, - - /** - * The server is delivering only part of the resource (byte serving) due to a range header sent by the client. - * The range header is used by HTTP clients to enable resuming of interrupted downloads, - * or split a download into multiple simultaneous streams. - */ - PARTIAL_CONTENT = 206, - - /** - * The message body that follows is an XML message and can contain a number of separate response codes, - * depending on how many sub-requests were made. - */ - MULTI_STATUS = 207, - - /** - * The members of a DAV binding have already been enumerated in a preceding part of the (multistatus) response, - * and are not being included again. - */ - ALREADY_REPORTED = 208, - - /** - * The server has fulfilled a request for the resource, - * and the response is a representation of the result of one or more instance-manipulations applied to the current instance. - */ - IM_USED = 226, - - /** - * Indicates multiple options for the resource from which the client may choose (via agent-driven content negotiation). - * For example, this code could be used to present multiple video format options, - * to list files with different filename extensions, or to suggest word-sense disambiguation. - */ - MULTIPLE_CHOICES = 300, - - /** - * This and all future requests should be directed to the given URI. - */ - MOVED_PERMANENTLY = 301, - - /** - * This is an example of industry practice contradicting the standard. - * The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect - * (the original describing phrase was "Moved Temporarily"), but popular browsers implemented 302 - * with the functionality of a 303 See Other. Therefore, HTTP/1.1 added status codes 303 and 307 - * to distinguish between the two behaviours. However, some Web applications and frameworks - * use the 302 status code as if it were the 303. - */ - FOUND = 302, - - /** - * SINCE HTTP/1.1 - * The response to the request can be found under another URI using a GET method. - * When received in response to a POST (or PUT/DELETE), the client should presume that - * the server has received the data and should issue a redirect with a separate GET message. - */ - SEE_OTHER = 303, - - /** - * Indicates that the resource has not been modified since the version specified by the request headers If-Modified-Since or If-None-Match. - * In such case, there is no need to retransmit the resource since the client still has a previously-downloaded copy. - */ - NOT_MODIFIED = 304, - - /** - * SINCE HTTP/1.1 - * The requested resource is available only through a proxy, the address for which is provided in the response. - * Many HTTP clients (such as Mozilla and Internet Explorer) do not correctly handle responses with this status code, primarily for security reasons. - */ - USE_PROXY = 305, - - /** - * No longer used. Originally meant "Subsequent requests should use the specified proxy." - */ - SWITCH_PROXY = 306, - - /** - * SINCE HTTP/1.1 - * In this case, the request should be repeated with another URI; however, future requests should still use the original URI. - * In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the original request. - * For example, a POST request should be repeated using another POST request. - */ - TEMPORARY_REDIRECT = 307, - - /** - * The request and all future requests should be repeated using another URI. - * 307 and 308 parallel the behaviors of 302 and 301, but do not allow the HTTP method to change. - * So, for example, submitting a form to a permanently redirected resource may continue smoothly. - */ - PERMANENT_REDIRECT = 308, - - /** - * The server cannot or will not process the request due to an apparent client error - * (e.g., malformed request syntax, too large size, invalid request message framing, or deceptive request routing). - */ - BAD_REQUEST = 400, - - /** - * Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet - * been provided. The response must include a WWW-Authenticate header field containing a challenge applicable to the - * requested resource. See Basic access authentication and Digest access authentication. 401 semantically means - * "unauthenticated",i.e. the user does not have the necessary credentials. - */ - UNAUTHORIZED = 401, - - /** - * Reserved for future use. The original intention was that this code might be used as part of some form of digital - * cash or micro payment scheme, but that has not happened, and this code is not usually used. - * Google Developers API uses this status if a particular developer has exceeded the daily limit on requests. - */ - PAYMENT_REQUIRED = 402, - - /** - * The request was valid, but the server is refusing action. - * The user might not have the necessary permissions for a resource. - */ - FORBIDDEN = 403, - - /** - * The requested resource could not be found but may be available in the future. - * Subsequent requests by the client are permissible. - */ - NOT_FOUND = 404, - - /** - * A request method is not supported for the requested resource; - * for example, a GET request on a form that requires data to be presented via POST, or a PUT request on a read-only resource. - */ - METHOD_NOT_ALLOWED = 405, - - /** - * The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request. - */ - NOT_ACCEPTABLE = 406, - - /** - * The client must first authenticate itself with the proxy. - */ - PROXY_AUTHENTICATION_REQUIRED = 407, - - /** - * The server timed out waiting for the request. - * According to HTTP specifications: - * "The client did not produce a request within the time that the server was prepared to wait. The client MAY repeat the request without modifications at any later time." - */ - REQUEST_TIMEOUT = 408, - - /** - * Indicates that the request could not be processed because of conflict in the request, - * such as an edit conflict between multiple simultaneous updates. - */ - CONFLICT = 409, - - /** - * Indicates that the resource requested is no longer available and will not be available again. - * This should be used when a resource has been intentionally removed and the resource should be purged. - * Upon receiving a 410 status code, the client should not request the resource in the future. - * Clients such as search engines should remove the resource from their indices. - * Most use cases do not require clients and search engines to purge the resource, and a "404 Not Found" may be used instead. - */ - GONE = 410, - - /** - * The request did not specify the length of its content, which is required by the requested resource. - */ - LENGTH_REQUIRED = 411, - - /** - * The server does not meet one of the preconditions that the requester put on the request. - */ - PRECONDITION_FAILED = 412, - - /** - * The request is larger than the server is willing or able to process. Previously called "Request Entity Too Large". - */ - PAYLOAD_TOO_LARGE = 413, - - /** - * The URI provided was too long for the server to process. Often the result of too much data being encoded as a query-string of a GET request, - * in which case it should be converted to a POST request. - * Called "Request-URI Too Long" previously. - */ - URI_TOO_LONG = 414, - - /** - * The request entity has a media type which the server or resource does not support. - * For example, the client uploads an image as image/svg+xml, but the server requires that images use a different format. - */ - UNSUPPORTED_MEDIA_TYPE = 415, - - /** - * The client has asked for a portion of the file (byte serving), but the server cannot supply that portion. - * For example, if the client asked for a part of the file that lies beyond the end of the file. - * Called "Requested Range Not Satisfiable" previously. - */ - RANGE_NOT_SATISFIABLE = 416, - - /** - * The server cannot meet the requirements of the Expect request-header field. - */ - EXPECTATION_FAILED = 417, - - /** - * This code was defined in 1998 as one of the traditional IETF April Fools' jokes, in RFC 2324, Hyper Text Coffee Pot Control Protocol, - * and is not expected to be implemented by actual HTTP servers. The RFC specifies this code should be returned by - * teapots requested to brew coffee. This HTTP status is used as an Easter egg in some websites, including Google.com. - */ - I_AM_A_TEAPOT = 418, - - /** - * The request was directed at a server that is not able to produce a response (for example because a connection reuse). - */ - MISDIRECTED_REQUEST = 421, - - /** - * The request was well-formed but was unable to be followed due to semantic errors. - */ - UNPROCESSABLE_ENTITY = 422, - - /** - * The resource that is being accessed is locked. - */ - LOCKED = 423, - - /** - * The request failed due to failure of a previous request (e.g., a PROPPATCH). - */ - FAILED_DEPENDENCY = 424, - - /** - * The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field. - */ - UPGRADE_REQUIRED = 426, - - /** - * The origin server requires the request to be conditional. - * Intended to prevent "the 'lost update' problem, where a client - * GETs a resource's state, modifies it, and PUTs it back to the server, - * when meanwhile a third party has modified the state on the server, leading to a conflict." - */ - PRECONDITION_REQUIRED = 428, - - /** - * The user has sent too many requests in a given amount of time. Intended for use with rate-limiting schemes. - */ - TOO_MANY_REQUESTS = 429, - - /** - * The server is unwilling to process the request because either an individual header field, - * or all the header fields collectively, are too large. - */ - REQUEST_HEADER_FIELDS_TOO_LARGE = 431, - - /** - * A server operator has received a legal demand to deny access to a resource or to a set of resources - * that includes the requested resource. The code 451 was chosen as a reference to the novel Fahrenheit 451. - */ - UNAVAILABLE_FOR_LEGAL_REASONS = 451, - - /** - * A generic error message, given when an unexpected condition was encountered and no more specific message is suitable. - */ - INTERNAL_SERVER_ERROR = 500, - - /** - * The server either does not recognize the request method, or it lacks the ability to fulfill the request. - * Usually this implies future availability (e.g., a new feature of a web-service API). - */ - NOT_IMPLEMENTED = 501, - - /** - * The server was acting as a gateway or proxy and received an invalid response from the upstream server. - */ - BAD_GATEWAY = 502, - - /** - * The server is currently unavailable (because it is overloaded or down for maintenance). - * Generally, this is a temporary state. - */ - SERVICE_UNAVAILABLE = 503, - - /** - * The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. - */ - GATEWAY_TIMEOUT = 504, - - /** - * The server does not support the HTTP protocol version used in the request - */ - HTTP_VERSION_NOT_SUPPORTED = 505, - - /** - * Transparent content negotiation for the request results in a circular reference. - */ - VARIANT_ALSO_NEGOTIATES = 506, - - /** - * The server is unable to store the representation needed to complete the request. - */ - INSUFFICIENT_STORAGE = 507, - - /** - * The server detected an infinite loop while processing the request. - */ - LOOP_DETECTED = 508, - - /** - * Further extensions to the request are required for the server to fulfill it. - */ - NOT_EXTENDED = 510, - - /** - * The client needs to authenticate to gain network access. - * Intended for use by intercepting proxies used to control access to the network (e.g., "captive portals" used - * to require agreement to Terms of Service before granting full Internet access via a Wi-Fi hotspot). - */ - NETWORK_AUTHENTICATION_REQUIRED = 511 -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 8915e5b..22257c3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ import { DEFAULT_MOCK_OPTIONS } from "./constants"; -import { MockOptions, HttpStatusCode } from "./types"; +import { MockOptions } from "./types"; /** * @description Convert a wildcard string to a regular expression. @@ -63,7 +63,7 @@ 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: HttpStatusCode, url: string, options: MockOptions = DEFAULT_MOCK_OPTIONS) => { +export const makeResponse = (status: number, url: string, options: MockOptions = DEFAULT_MOCK_OPTIONS) => { const { headers, data, response } = options; const ok = status >= 200 && status < 300; @@ -72,7 +72,7 @@ export const makeResponse = (status: HttpStatusCode, url: string, options: MockO return { ok, status, - statusText: HttpStatusCode[status], + statusText: status, url, headers: response?.headers ?? headers, text: () => Promise.resolve(body), diff --git a/tests/mock.test.ts b/tests/mock.test.ts index 59d9233..3adc3de 100644 --- a/tests/mock.test.ts +++ b/tests/mock.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test, beforeEach } from "bun:test"; import { clearMocks, mock } from "../src/mock"; -import { HttpStatusCode, MockOptions } from "../src/types"; +import { MockOptions } from "../src/types"; const API_URL = `https://bun-bagel.sweet/api/v1`; @@ -24,7 +24,7 @@ describe("Mock", () => { const response = await fetch(`${API_URL}/users`); const data = await response.json(); - expect(response.status).toEqual(HttpStatusCode.OK); + expect(response.status).toEqual(200); expect(data).toEqual(options.response?.data); expect(mocked).toEqual(true); }); @@ -170,12 +170,12 @@ describe("Mock", () => { const options: MockOptions = { response: { data: { name: "John Doe" }, - status: HttpStatusCode.I_AM_A_TEAPOT, + status: 418, } }; mock(request, options); const response = await fetch(`${API_URL}/users`); - expect(response.status).toEqual(HttpStatusCode.I_AM_A_TEAPOT); + expect(response.status).toEqual(418); }); test("mock: should mock a request with response headers", async () => {