From c6638cb0acca40a9c4a2f81f84615143d3584b21 Mon Sep 17 00:00:00 2001 From: Diego Aquino Date: Fri, 29 Mar 2024 11:38:24 -0300 Subject: [PATCH] docs: v0.3.0 (#24) (#117) ### Documentation - [#zimic] Improved the examples using headers and search params. - [#zimic] Added a link to the examples section to the headline. - [#zimic] Documented the new APIs `tracker.with(restriction)` and `tracker.clear()`. - [#zimic] Improved notes using [Markdown Alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts). Closes #24. --- README.md | 750 ++++++++++++------ .../zimic/src/http/headers/HttpHeaders.ts | 17 +- packages/zimic/src/http/headers/types.ts | 1 + .../src/http/searchParams/HttpSearchParams.ts | 17 +- packages/zimic/src/http/searchParams/types.ts | 1 + packages/zimic/src/http/types/requests.ts | 8 + packages/zimic/src/http/types/schema.ts | 24 + .../interceptor/http/interceptor/factory.ts | 5 +- .../http/interceptor/types/options.ts | 1 + .../http/interceptor/types/public.ts | 43 +- .../OtherHttpInterceptorWorkerRunningError.ts | 1 + .../http/interceptorWorker/factory.ts | 4 +- .../http/interceptorWorker/types/options.ts | 1 + .../http/requestTracker/types/public.ts | 42 +- .../http/requestTracker/types/requests.ts | 10 + packages/zimic/src/types/json.ts | 4 + 16 files changed, 664 insertions(+), 265 deletions(-) diff --git a/README.md b/README.md index c1d80138..aa8037bd 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@   •   Docs   •   + Examples +   •   Issues   •   Roadmap @@ -18,8 +20,12 @@ --- -> 🚧 This project is under active development! Check our -> [roadmap to v1](https://github.com/users/diego-aquino/projects/5/views/6). Contributors and ideas are welcome! +> [!NOTE] +> +> 🚧 This project is still experimental and under active development! +> +> Check our [roadmap to v1](https://github.com/users/diego-aquino/projects/5/views/6). Contributors and ideas are +> welcome! Zimic is a lightweight, TypeScript-first HTTP request mocking library, inspired by [Zod](https://github.com/colinhacks/zod)'s type inference and using [MSW](https://github.com/mswjs/msw) under the hood. @@ -57,7 +63,9 @@ Zimic provides a simple, flexible and type-safe way to mock HTTP requests. - [Testing](#testing) - [`zimic` API](#zimic-api) - [`HttpHeaders`](#httpheaders) + - [Comparing `HttpHeaders`](#comparing-httpheaders) - [`HttpSearchParams`](#httpsearchparams) + - [Comparing `HttpSearchParams`](#comparing-httpsearchparams) - [`zimic/interceptor` API](#zimicinterceptor-api) - [`HttpInterceptorWorker`](#httpinterceptorworker) - [`createHttpInterceptorWorker`](#createhttpinterceptorworker) @@ -67,7 +75,7 @@ Zimic provides a simple, flexible and type-safe way to mock HTTP requests. - [`worker.isRunning()`](#workerisrunning) - [`HttpInterceptor`](#httpinterceptor) - [`createHttpInterceptor`](#createhttpinterceptor) - - [`HttpInterceptor` schema](#httpinterceptor-schema) + - [Declaring service schemas](#declaring-service-schemas) - [Declaring paths](#declaring-paths) - [Declaring methods](#declaring-methods) - [Declaring requests](#declaring-requests) @@ -79,10 +87,14 @@ Zimic provides a simple, flexible and type-safe way to mock HTTP requests. - [`HttpRequestTracker`](#httprequesttracker) - [`tracker.method()`](#trackermethod) - [`tracker.path()`](#trackerpath) + - [`tracker.with(restriction)`](#trackerwithrestriction) + - [Static restrictions](#static-restrictions) + - [Computed restrictions](#computed-restrictions) - [`tracker.respond(declaration)`](#trackerresponddeclaration) - [Static responses](#static-responses) - [Computed responses](#computed-responses) - [`tracker.bypass()`](#trackerbypass) + - [`tracker.clear()`](#trackerclear) - [`tracker.requests()`](#trackerrequests) - [CLI](#cli) - [`zimic --version`](#zimic---version) @@ -151,116 +163,159 @@ Visit our [examples](./examples) to see how to use Zimic with popular frameworks ### Basic usage -To start using Zimic, create a [worker](#httpinterceptorworker) targeting your platform. +1. To start using Zimic, create a [worker](#httpinterceptorworker) targeting your platform. -```ts -import { createHttpInterceptorWorker } from 'zimic/interceptor'; + ```ts + import { createHttpInterceptorWorker } from 'zimic/interceptor'; -const worker = createHttpInterceptorWorker({ - platform: 'node', // or 'browser' -}); -``` + const worker = createHttpInterceptorWorker({ + platform: 'node', // Or 'browser' + }); + ``` -Then, create your first [interceptor](#httpinterceptor): +2. Then, create your first [interceptor](#httpinterceptor): -```ts -import { createHttpInterceptor } from 'zimic/interceptor'; + ```ts + import { JSONValue } from 'zimic'; + import { createHttpInterceptor } from 'zimic/interceptor'; -const interceptor = createHttpInterceptor<{ - '/users': { - GET: { - response: { - 200: { body: User[] }; - }; - }; - }; -}>({ - worker, - baseURL: 'http://localhost:3000', -}); -``` + type User = JSONValue<{ + username: string; + }>; -In this example, we're creating an interceptor for a service with a single path, `/users`, that supports a `GET` method. -The response for a successful request is an array of `User` objects. Learn more about declaring interceptor schemas at -[`HttpInterceptor` schema](#httpinterceptor-schema). + const interceptor = createHttpInterceptor<{ + '/users': { + GET: { + response: { + 200: { body: User[] }; + }; + }; + }; + }>({ + worker, + baseURL: 'http://localhost:3000', + }); + ``` -Finally, start the worker to intercept requests: + In this example, we're creating an interceptor for a service with a single path, `/users`, that supports a `GET` + method. The response for a successful request is an array of `User` objects, which is checked to be a valid JSON + using `JSONValue`. Learn more at [Declaring service schemas](#declaring-service-schemas). -```ts -await worker.start(); -``` +3. Finally, start the worker to intercept requests: -Now, you can start intercepting requests and returning mock responses! + ```ts + await worker.start(); + ``` -```ts -const listTracker = interceptor.get('/users').respond({ - status: 200, - body: [{ username: 'diego-aquino' }], -}); +4. Now, you can start intercepting requests and returning mock responses! -const response = await fetch('http://localhost:3000/users'); -const users = await response.json(); -console.log(users); // [{ username: 'diego-aquino' }] -``` + ```ts + const listTracker = interceptor.get('/users').respond({ + status: 200, + body: [{ username: 'diego-aquino' }], + }); + + const response = await fetch('http://localhost:3000/users'); + const users = await response.json(); + console.log(users); // [{ username: 'diego-aquino' }] + ``` -More examples are available at [`zimic/interceptor` API](#zimicinterceptor-api). +More usage examples and recommendations are available at [`zimic/interceptor` API](#zimicinterceptor-api) and +[Examples](#examples). ### Testing We recommend managing the lifecycle of your workers and interceptors using `beforeAll` and `afterAll` hooks in your test setup file. An example using Jest/Vitest structure: -```ts -// tests/setup.ts -import { createHttpInterceptorWorker, createHttpInterceptor } from 'zimic/interceptor'; +1. Create a worker: -// create a worker -const worker = createHttpInterceptorWorker({ - platform: 'node', -}); + `tests/interceptors/worker.ts` -const userInterceptor = createHttpInterceptor<{ - // declare your schema -}>({ - worker, - baseURL: 'http://localhost:3000', -}); + ```ts + import { createHttpInterceptorWorker } from 'zimic/interceptor'; -const logInterceptor = createHttpInterceptor<{ - // declare your schema -}>({ - worker, - baseURL: 'http://localhost:3001', -}); + const worker = createHttpInterceptorWorker({ + platform: 'node', // Or 'browser' + }); -beforeAll(async () => { - // start intercepting requests - await worker.start(); -}); + export default worker; + ``` -beforeEach(async () => { - // clear all interceptors to make sure no tests affect each other - userInterceptor.clear(); - logInterceptor.clear(); -}); +2. Create interceptors for your services: -afterAll(async () => { - // stop intercepting requests - await worker.stop(); -}); -``` + `tests/interceptors/userInterceptor.ts` + + ```ts + import { createHttpInterceptor } from 'zimic/interceptor'; + import worker from './worker'; + + const userInterceptor = createHttpInterceptor<{ + // User service schema + }>({ + worker, + baseURL: 'http://localhost:3000', + }); + + export default userInterceptor; + ``` + + `tests/interceptors/analyticsInterceptor.ts` + + ```ts + import { createHttpInterceptor } from 'zimic/interceptor'; + import worker from './worker'; + + const analyticsInterceptor = createHttpInterceptor<{ + // Analytics service schema + }>({ + worker, + baseURL: 'http://localhost:3001', + }); + + export default analyticsInterceptor; + ``` + +3. Create a setup file to manage lifecycle of the worker and the interceptors: + + `tests/setup.ts` + + ```ts + import userInterceptor from './interceptors/userInterceptor'; + import analyticsInterceptor from './interceptors/analyticsInterceptor'; + + beforeAll(async () => { + // Start intercepting requests + await worker.start(); + }); + + beforeEach(async () => { + // Clear all interceptors to make sure no tests affect each other + userInterceptor.clear(); + analyticsInterceptor.clear(); + }); + + afterAll(async () => { + // Stop intercepting requests + await worker.stop(); + }); + ``` --- ## `zimic` API -This module provides general utilities, such as HTTP classes. +This module provides general resources, such as HTTP classes and types. + +> [!TIP] +> +> All APIs are documented using [JSDoc](https://jsdoc.app), so you can view detailed descriptions directly in your IDE. ### `HttpHeaders` A superset of the built-in [`Headers`](https://developer.mozilla.org/docs/Web/API/Headers) class, with a strictly-typed -schema. `HttpHeaders` is fully compatible with `Headers` and is used by Zimic abstractions to provide type safety when -applying mocks. +schema. `HttpHeaders` is fully compatible with `Headers` and is used by Zimic to provide type safety when applying +mocks. ```ts import { HttpHeaders } from 'zimic'; @@ -277,11 +332,51 @@ const contentType = headers.get('content-type'); console.log(contentType); // 'application/json' ``` +#### Comparing `HttpHeaders` + +`HttpHeaders` also provide the utility methods `.equals` and `.contains`, useful in comparisons with other headers: + +```ts +import { HttpSchema, HttpHeaders } from 'zimic'; + +type HeaderSchema = HttpSchema.Headers<{ + accept?: string; + 'content-type'?: string; +}>; + +const headers1 = new HttpHeaders({ + accept: '*/*', + 'content-type': 'application/json', +}); + +const headers2 = new HttpHeaders({ + accept: '*/*', + 'content-type': 'application/json', +}); + +const headers3 = new HttpHeaders< + HeaderSchema & { + 'x-custom-header'?: string; + } +>({ + accept: '*/*', + 'content-type': 'application/json', + 'x-custom-header': 'value', +}); + +console.log(headers1.equals(headers2)); // true +console.log(headers1.equals(headers3)); // false + +console.log(headers1.contains(headers2)); // true +console.log(headers1.contains(headers3)); // false +console.log(headers3.contains(headers1)); // true +``` + ### `HttpSearchParams` A superset of the built-in [`URLSearchParams`](https://developer.mozilla.org/docs/Web/API/URLSearchParams) class, with a -strictly-typed schema. `HttpSearchParams` is fully compatible with `URLSearchParams` and is used by Zimic abstractions -to provide type safety when applying mocks. +strictly-typed schema. `HttpSearchParams` is fully compatible with `URLSearchParams` and is used by Zimic to provide +type safety when applying mocks. ```ts import { HttpSearchParams } from 'zimic'; @@ -301,12 +396,54 @@ const page = searchParams.get('page'); console.log(page); // '1' ``` +#### Comparing `HttpSearchParams` + +`HttpSearchParams` also provide the utility methods `.equals` and `.contains`, useful in comparisons with other search +params: + +```ts +import { HttpSchema, HttpSearchParams } from 'zimic'; + +type SearchParamsSchema = HttpSchema.SearchParams<{ + names?: string[]; + page?: `${number}`; +}>; + +const searchParams1 = new HttpSearchParams({ + names: ['user 1', 'user 2'], + page: '1', +}); + +const searchParams2 = new HttpSearchParams({ + names: ['user 1', 'user 2'], + page: '1', +}); + +const searchParams3 = new HttpSearchParams< + SearchParamsSchema & { + orderBy?: `${'name' | 'email'}.${'asc' | 'desc'}[]`; + } +>({ + names: ['user 1', 'user 2'], + page: '1', + orderBy: ['name.asc'], +}); + +console.log(searchParams1.equals(searchParams2)); // true +console.log(searchParams1.equals(searchParams3)); // false + +console.log(searchParams1.contains(searchParams2)); // true +console.log(searchParams1.contains(searchParams3)); // false +console.log(searchParams3.contains(searchParams1)); // true +``` + ## `zimic/interceptor` API -This module provides a set of utilities to create HTTP interceptors for both Node.js and browser environments. +This module provides a set of resources to create HTTP interceptors for both Node.js and browser environments. -All APIs are documented using [JSDoc](https://jsdoc.app) comments, so you can view detailed descriptions directly in -your IDE! +> [!TIP] +> +> All APIs are documented using [JSDoc](https://jsdoc.app), so you can view detailed descriptions directly in your IDE. ### `HttpInterceptorWorker` @@ -324,9 +461,7 @@ Creates an HTTP interceptor worker. A platform is required to specify the enviro ```ts import { createHttpInterceptorWorker } from 'zimic/interceptor'; - const worker = createHttpInterceptorWorker({ - platform: 'node', - }); + const worker = createHttpInterceptorWorker({ platform: 'node' }); ``` - Browser: @@ -334,9 +469,7 @@ Creates an HTTP interceptor worker. A platform is required to specify the enviro ```ts import { createHttpInterceptorWorker } from 'zimic/interceptor'; - const worker = createHttpInterceptorWorker({ - platform: 'browser', - }); + const worker = createHttpInterceptorWorker({ platform: 'browser' }); ``` #### `worker.platform()` @@ -385,22 +518,24 @@ Each interceptor represents a service and can be used to mock its paths and meth #### `createHttpInterceptor` -Creates an HTTP interceptor, the main interface to intercept HTTP requests and return responses. Learn more about -interceptor schemas at [`HttpInterceptor` schema](#httpinterceptor-schema). +Creates an HTTP interceptor, the main interface to intercept HTTP requests and return responses. Learn more at +[Declaring service schemas](#declaring-service-schemas). ```ts +import { JSONValue } from 'zimic'; import { createHttpInterceptorWorker, createHttpInterceptor } from 'zimic/interceptor'; -const worker = createHttpInterceptorWorker({ - platform: 'node', -}); +const worker = createHttpInterceptorWorker({ platform: 'node' }); + +type User = JSONValue<{ + username: string; +}>; const interceptor = createHttpInterceptor<{ '/users/:id': { GET: { response: { 200: { body: User }; - 404: { body: NotFoundError }; }; }; }; @@ -410,31 +545,55 @@ const interceptor = createHttpInterceptor<{ }); ``` -#### `HttpInterceptor` schema +#### Declaring service schemas -HTTP interceptor schemas define the structure of the real services being mocked. This includes paths, methods, request -and response bodies, and status codes. Based on the schema, interceptors will provide type validation when applying -mocks. +HTTP service schemas define the structure of the real services being used. This includes paths, methods, request and +response bodies, and status codes. Based on the schema, interceptors will provide type validation when applying mocks.
An example of a complete interceptor schema: ```ts +import { HttpSchema, JSONValue } from 'zimic'; import { createHttpInterceptor } from 'zimic/interceptor'; +// Declaring base types +type User = JSONValue<{ + username: string; +}>; + +type UserCreationBody = JSONValue<{ + username: string; +}>; + +type NotFoundError = JSONValue<{ + message: string; +}>; + +type UserListSearchParams = HttpSchema.SearchParams<{ + name?: string; + orderBy?: `${'name' | 'email'}.${'asc' | 'desc'}`[]; +}>; + +// Creating the interceptor const interceptor = createHttpInterceptor<{ '/users': { POST: { request: { - body: { - username: string; - }; + headers: { accept: string }; + body: UserCreationBody; }; response: { - 201: { body: User }; + 201: { + headers: { 'content-type': string }; + body: User; + }; }; }; GET: { + request: { + searchParams: UserListSearchParams; + }; response: { 200: { body: User[] }; 404: { body: NotFoundError }; @@ -450,14 +609,6 @@ const interceptor = createHttpInterceptor<{ }; }; }; - - '/posts': { - GET: { - response: { - 200: { body: Post[] }; - }; - }; - }; }>({ worker, baseURL: 'http://localhost:3000', @@ -470,54 +621,76 @@ const interceptor = createHttpInterceptor<{ Alternatively, you can compose the schema using utility types: ```ts -import { createHttpInterceptor, HttpInterceptorSchema } from 'zimic/interceptor'; +import { HttpSchema, JSONValue } from 'zimic'; +import { createHttpInterceptor } from 'zimic/interceptor'; -type UserPaths = HttpInterceptorSchema.Root<{ - '/users': { - POST: { - request: { - body: { - username: string; - }; - }; - response: { - 201: { body: User }; - }; - }; +// Declaring the base types +type User = JSONValue<{ + username: string; +}>; - GET: { - response: { - 200: { body: User[] }; - 404: { body: NotFoundError }; +type UserCreationBody = JSONValue<{ + username: string; +}>; + +type NotFoundError = JSONValue<{ + message: string; +}>; + +type UserListSearchParams = HttpSchema.SearchParams<{ + name?: string; + orderBy?: `${'name' | 'email'}.${'asc' | 'desc'}`[]; +}>; + +// Declaring user methods +type UserMethods = HttpSchema.Methods<{ + POST: { + request: { + headers: { accept: string }; + body: UserCreationBody; + }; + response: { + 201: { + headers: { 'content-type': string }; + body: User; }; }; }; -}>; -type UserByIdPaths = HttpInterceptorSchema.Root<{ - '/users/:id': { - GET: { - response: { - 200: { body: User }; - 404: { body: NotFoundError }; - }; + GET: { + request: { + searchParams: UserListSearchParams; + }; + response: { + 200: { body: User[] }; + 404: { body: NotFoundError }; }; }; }>; -type PostPaths = HttpInterceptorSchema.Root<{ - '/posts': { - GET: { - response: { - 200: { body: Post[] }; - }; +type UserGetByIdMethods = HttpSchema.Methods<{ + GET: { + response: { + 200: { body: User }; + 404: { body: NotFoundError }; }; }; }>; -type InterceptorSchema = HttpInterceptorSchema.Root; +// Declaring user paths +type UserPaths = HttpSchema.Paths<{ + '/users': UserMethods; +}>; + +type UserByIdPaths = HttpSchema.Paths<{ + '/users/:id': UserGetByIdMethods; +}>; + +// Declaring interceptor schema +type ServiceSchema = UserPaths & UserByIdPaths; -const interceptor = createHttpInterceptor({ +// Creating the interceptor +const interceptor = createHttpInterceptor({ worker, baseURL: 'http://localhost:3000', }); @@ -527,20 +700,20 @@ const interceptor = createHttpInterceptor({ ##### Declaring paths -At the root level, each key represents a path or route: +At the root level, each key represents a path or route of the service: ```ts import { createHttpInterceptor } from 'zimic/interceptor'; const interceptor = createHttpInterceptor<{ '/users': { - // path schema + // Path schema }; '/users/:id': { - // path schema + // Path schema }; '/posts': { - // path schema + // Path schema }; }>({ worker, @@ -550,24 +723,25 @@ const interceptor = createHttpInterceptor<{
- Alternatively, you can also compose root level paths using the utility type HttpInterceptorSchema.Root: + Alternatively, you can also compose paths using the utility type HttpSchema.Paths: ```ts -import { createHttpInterceptor, HttpInterceptorSchema } from 'zimic/interceptor'; +import { HttpSchema } from 'zimic'; +import { createHttpInterceptor } from 'zimic/interceptor'; -type UserPaths = HttpInterceptorSchema.Root<{ +type UserPaths = HttpSchema.Paths<{ '/users': { - // path schema + // Path schema }; '/users/:id': { - // path schema + // Path schema }; }>; -type PostPaths = HttpInterceptorSchema.Root<{ +type PostPaths = HttpSchema.Paths<{ '/posts': { - // path schema + // Path schema }; }>; @@ -590,13 +764,13 @@ import { createHttpInterceptor } from 'zimic/interceptor'; const interceptor = createHttpInterceptor<{ '/users': { GET: { - // method schema + // Method schema }; POST: { - // method schema + // Method schema }; }; - // other paths + // Other paths }>({ worker, baseURL: 'http://localhost:3000', @@ -606,18 +780,19 @@ const interceptor = createHttpInterceptor<{
Similarly to paths, you can also compose methods using the utility type - HttpInterceptorSchema.Method: + HttpSchema.Methods: ```ts -import { createHttpInterceptor, HttpInterceptorSchema } from 'zimic/interceptor'; +import { HttpSchema } from 'zimic'; +import { createHttpInterceptor } from 'zimic/interceptor'; -type UserMethods = HttpInterceptorSchema.Method<{ +type UserMethods = HttpSchema.Methods<{ GET: { - // method schema + // Method schema }; POST: { - // method schema + // Method schema }; }>; @@ -636,61 +811,69 @@ const interceptor = createHttpInterceptor<{ Each method can have a `request`, which defines the schema of the accepted requests. `headers`, `searchParams`, and `body` are supported to provide type safety when applying mocks. -> **Tip**: You only need to declare the properties you want to use in your mocks. For example, if you are referencing -> only the header `accept`, you need to declare only it. Similarly, if you are not using any headers, search params, or -> body, you can omit them. - ```ts +import { HttpSchema, JSONValue } from 'zimic'; import { createHttpInterceptor } from 'zimic/interceptor'; +type UserCreationBody = JSONValue<{ + username: string; +}>; + +type UserListSearchParams = HttpSchema.SearchParams<{ + username?: string; +}>; + const interceptor = createHttpInterceptor<{ '/users': { POST: { request: { - headers: { - accept: string; - }; - body: { - username: string; - }; + body: UserCreationBody; }; // ... }; GET: { request: { - searchParams: { - username?: string; - }; + searchParams: UserListSearchParams; }; // ... }; - - // other methods + // Other methods }; - // other paths + // Other paths }>({ worker, baseURL: 'http://localhost:3000', }); ``` +> [!TIP] +> +> You only need to include in the schema the properties you want to use in your mocks. Headers, search params, or body +> fields that are not used do not need to be declared, keeping your type definitions clean and concise. + +> [!IMPORTANT] +> +> Body types cannot be declared using the keyword `interface`, because interfaces do not have implicit index signatures +> as types do. Part of Zimic's JSON validation relies on index signatures. To workaround this, you can declare bodies +> using `type`. As an extra step to make sure the type is a valid JSON, you can use the utility type `JSONValue`. +
- You can also compose requests using the utility type HttpInterceptorSchema.Request, similarly to + You can also compose requests using the utility type HttpSchema.Request, similarly to methods: ```ts -import { createHttpInterceptor, HttpInterceptorSchema } from 'zimic/interceptor'; +import { HttpSchema, JSONValue } from 'zimic'; +import { createHttpInterceptor } from 'zimic/interceptor'; -type UserCreationRequest = HttpInterceptorSchema.Request<{ - headers: { - accept: '*/*'; - }; - body: { - username: string; - }; +type UserCreationBody = JSONValue<{ + username: string; +}>; + +type UserCreationRequest = HttpSchema.Request<{ + body: UserCreationBody; }>; const interceptor = createHttpInterceptor<{ @@ -712,66 +895,76 @@ const interceptor = createHttpInterceptor<{ Each method can also have a `response`, which defines the schema of the returned responses. The status codes are used as keys. `headers` and `body` are supported to provide type safety when applying mocks. -> **Tip**: Similarly to [Declaring requests](#declaring-requests), you only need to declare the properties you want to -> use in your mocks. For example, if you are referencing only the header `content-type`, you can declare only it. If you -> are not using any headers or body, you can omit them. - ```ts +import { JSONValue } from 'zimic'; import { createHttpInterceptor } from 'zimic/interceptor'; +type User = JSONValue<{ + username: string; +}>; + +type NotFoundError = JSONValue<{ + message: string; +}>; + const interceptor = createHttpInterceptor<{ '/users/:id': { GET: { // ... response: { - 200: { - headers: { - 'content-type': string; - }; - body: User; - }; - 404: { - headers: { - 'content-type': string; - }; - body: NotFoundError; - }; + 200: { body: User }; + 404: { body: NotFoundError }; }; }; - // other methods + // Other methods }; - // other paths + // Other paths }>({ worker, baseURL: 'http://localhost:3000', }); ``` +> [!TIP] +> +> Similarly to [Declaring requests](#declaring-requests), you only need to include in the schema the properties you want +> to use in your mocks. Headers, search params, or body fields that are not used do not need to be declared, keeping +> your type definitions clean and concise. + +> [!IMPORTANT] +> +> Also similarly to [Declaring requests](#declaring-requests), body types cannot be declared using the keyword +> `interface`, because interfaces do not have implicit index signatures as types do. Part of Zimic's JSON validation +> relies on index signatures. To workaround this, you can declare bodies using `type`. As an extra step to make sure the +> type is a valid JSON, you can use the utility type `JSONValue`. +
- You can also compose responses using the utility types HttpInterceptorSchema.ResponseByStatusCode and - HttpInterceptorSchema.Response, similarly to requests: + You can also compose responses using the utility types HttpSchema.ResponseByStatusCode and + HttpSchema.Response, similarly to requests: ```ts -import { createHttpInterceptor, HttpInterceptorSchema } from 'zimic/interceptor'; +import { HttpSchema, JSONValue } from 'zimic'; +import { createHttpInterceptor } from 'zimic/interceptor'; +type User = JSONValue<{ + username: string; +}>; -type SuccessUserGetResponse = HttpInterceptorSchema.Response<{ - headers: { - 'content-type': string; - }; +type NotFoundError = JSONValue<{ + message: string; +}>; + +type SuccessUserGetResponse = HttpSchema.Response<{ body: User; }>; -type NotFoundUserGetResponse = **HttpInterceptorSchema**.Response<{ - headers: { - 'content-type': string; - }; +type NotFoundUserGetResponse = HttpSchema.Response<{ body: NotFoundError; }>; -type UserGetResponses = HttpInterceptorSchema.ResponseByStatusCode<{ +type UserGetResponses = HttpSchema.ResponseByStatusCode<{ 200: SuccessUserGetResponse; 404: NotFoundUserGetResponse; }>; @@ -779,12 +972,9 @@ type UserGetResponses = HttpInterceptorSchema.ResponseByStatusCode<{ const interceptor = createHttpInterceptor<{ '/users/:id': { GET: { - // ... response: UserGetResponses; }; - // other methods }; - // other paths }>({ worker, baseURL: 'http://localhost:3000', @@ -824,7 +1014,7 @@ const interceptor = createHttpInterceptor<{ baseURL: 'http://localhost:3000', }); -// intercept any GET requests to http://localhost:3000/users and return this response +// Intercept any GET requests to http://localhost:3000/users and return this response const listTracker = interceptor.get('/users').respond({ status: 200 body: [{ username: 'diego-aquino' }], @@ -857,8 +1047,8 @@ const interceptor = createHttpInterceptor<{ baseURL: 'http://localhost:3000', }); -interceptor.get('/users/:id'); // matches any id -interceptor.get<'/users/:id'>(`/users/${1}`); // only matches id 1 (notice the original path as a type parameter) +interceptor.get('/users/:id'); // Matches any id +interceptor.get<'/users/:id'>(`/users/${1}`); // Only matches id 1 (notice the original path as a type parameter) ``` #### `interceptor.clear()` @@ -903,6 +1093,69 @@ const path = tracker.path(); console.log(path); // '/users' ``` +#### `tracker.with(restriction)` + +Declares a restriction to intercepted request matches. `headers`, `searchParams`, and `body` are supported to limit +which requests will match the tracker and receive the mock response. If multiple restrictions are declared, either in a +single object or with multiple calls to `.with()`, all of them must be met, essentially creating an AND condition. + +##### Static restrictions + +```ts +const creationTracker = interceptor + .post('/users') + .with({ + headers: { 'content-type': 'application/json' }, + body: creationPayload, + }) + .respond({ + status: 200, + body: [{ username: 'diego-aquino' }], + }); +``` + +By default, restrictions use `exact: false`, meaning that any request **containing** the declared restrictions will +match the tracker, regardless of having more properties or values. In the example above, requests with more headers than +`content-type: application/json` will still match the tracker. The same applies to search params and body restrictions. + +If you want to match only requests with the exact values declared, you can use `exact: true`: + +```ts +const creationTracker = interceptor + .post('/users') + .with({ + headers: { 'content-type': 'application/json' }, + body: creationPayload, + exact: true, // Only requests with these exact headers and body will match + }) + .respond({ + status: 200, + body: [{ username: 'diego-aquino' }], + }); +``` + +##### Computed restrictions + +A function is also supported to declare restrictions, in case they are dynamic. + +```ts +const creationTracker = interceptor + .post('/users') + .with((request) => { + const accept = request.headers.get('accept'); + return accept !== null && accept.startsWith('application'); + }) + .respond({ + status: 200, + body: [{ username: 'diego-aquino' }], + }); +``` + +The `request` parameter represents the intercepted request, containing useful properties such as `.body`, `.headers`, +and `.searchParams`, which are typed based on the interceptor schema. The function should return a boolean: `true` if +the request matches the tracker and should receive the mock response; `false` otherwise and the request should bypass +the tracker. + #### `tracker.respond(declaration)` Declares a response to return for matched intercepted requests. @@ -921,7 +1174,7 @@ const listTracker = interceptor.get('/users').respond({ ##### Computed responses -A function is also supported, in case the response is dynamic: +A function is also supported to declare a response, in case it is dynamic: ```ts const listTracker = interceptor.get('/users').respond((request) => { @@ -938,9 +1191,14 @@ and `.searchParams`, which are typed based on the interceptor schema. #### `tracker.bypass()` -Clears any declared response and intercepted requests, and makes the tracker stop matching intercepted requests. The -next tracker, created before this one, that matches the same method and path will be used if present. If not, the -requests to the method and path will not be intercepted. +Clears any response declared with [`.respond(declaration)`](#trackerresponddeclaration), making the tracker stop +matching requests. The next tracker, created before this one, that matches the same method and path will be used if +present. If not, the requests of the method and path will not be intercepted. + +To make the tracker match requests again, register a new response with `tracker.respond()`. + +This method is useful to skip a tracker. It is more gentle than [`tracker.clear()`](#trackerclear), as it only removed +the response, keeping restrictions and intercepted requests. ```ts const listTracker1 = interceptor.get('/users').respond({ @@ -954,8 +1212,38 @@ const listTracker2 = interceptor.get('/users').respond({ }); listTracker2.bypass(); +// Now, GET requests to /users will match listTracker1 and return an empty array + +listTracker2.requests(); // Still contains the intercepted requests up to the bypass +``` + +#### `tracker.clear()` + +Clears any response declared with [`.respond(declaration)`](#trackerresponddeclaration), restrictions declared with +[`.with(restriction)`](#trackerwithrestriction), and intercepted requests, making the tracker stop matching requests. +The next tracker, created before this one, that matches the same method and path will be used if present. If not, the +requests of the method and path will not be intercepted. + +To make the tracker match requests again, register a new response with `tracker.respond()`. + +This method is useful to reset trackers to a clean state between tests. It is more aggressive than +[`tracker.bypass()`](#trackerbypass), as it also clears restrictions and intercepted requests. + +```ts +const listTracker1 = interceptor.get('/users').respond({ + status: 200, + body: [], +}); + +const listTracker2 = interceptor.get('/users').respond({ + status: 200, + body: [{ username: 'diego-aquino' }], +}); + +listTracker2.clear(); +// Now, GET requests to /users will match listTracker1 and return an empty array -// GET requests to /users will match listTracker1 and return an empty array +listTracker2.requests(); // Now empty ``` #### `tracker.requests()` diff --git a/packages/zimic/src/http/headers/HttpHeaders.ts b/packages/zimic/src/http/headers/HttpHeaders.ts index 8cb1b6cb..cccc9b89 100644 --- a/packages/zimic/src/http/headers/HttpHeaders.ts +++ b/packages/zimic/src/http/headers/HttpHeaders.ts @@ -12,7 +12,7 @@ function pickPrimitiveProperties(schema: Schem } /** - * An HTTP headers object with a strictly-typed schema. Fully compatible with the built-in + * An extended HTTP headers object with a strictly-typed schema. Fully compatible with the built-in * {@link https://developer.mozilla.org/docs/Web/API/Headers Headers} class. */ class HttpHeaders extends Headers { @@ -73,6 +73,13 @@ class HttpHeaders extends >; } + /** + * Checks if this headers object is equal to another set of headers. Equality is defined as having the same keys and + * values, regardless of the order of keys. + * + * @param otherHeaders The other headers object to compare against. + * @returns `true` if the headers are equal, `false` otherwise. + */ equals(otherHeaders: HttpHeaders): boolean { for (const [key, value] of otherHeaders.entries()) { const otherValue = super.get.call(this, key); @@ -91,6 +98,14 @@ class HttpHeaders extends return true; } + /** + * Checks if this headers object contains another set of headers. This method is less strict than + * {@link HttpHeaders.equals} and only requires that all keys and values in the other headers are present in these + * headers. + * + * @param otherHeaders The other headers object to compare against. + * @returns `true` if these headers contain the other headers, `false` otherwise. + */ contains(otherHeaders: HttpHeaders): boolean { for (const [key, value] of otherHeaders.entries()) { const otherValue = super.get.call(this, key); diff --git a/packages/zimic/src/http/headers/types.ts b/packages/zimic/src/http/headers/types.ts index fd96252e..0dcddf5c 100644 --- a/packages/zimic/src/http/headers/types.ts +++ b/packages/zimic/src/http/headers/types.ts @@ -12,6 +12,7 @@ export type HttpHeadersSchemaTuple = { [Key in keyof Schema & string]: [Key, Defined]; }[keyof Schema & string]; +/** An initialization value for {@link HttpHeaders}. */ export type HttpHeadersInit = | Headers | Schema diff --git a/packages/zimic/src/http/searchParams/HttpSearchParams.ts b/packages/zimic/src/http/searchParams/HttpSearchParams.ts index 8e1a3912..8df06dbf 100644 --- a/packages/zimic/src/http/searchParams/HttpSearchParams.ts +++ b/packages/zimic/src/http/searchParams/HttpSearchParams.ts @@ -16,7 +16,7 @@ function pickPrimitiveProperties(schema: } /** - * An HTTP search params object with a strictly-typed schema. Fully compatible with the built-in + * An extended HTTP search params object with a strictly-typed schema. Fully compatible with the built-in * {@link https://developer.mozilla.org/docs/Web/API/URLSearchParams URLSearchParams} class. */ class HttpSearchParams extends URLSearchParams { @@ -98,10 +98,25 @@ class HttpSearchParams extends UR >; } + /** + * Checks if the current search parameters are equal to another set of search parameters. Equality is defined as + * having the same keys and values, regardless of the order of the keys. + * + * @param otherParams The other search parameters to compare against. + * @returns `true` if the search parameters are equal, `false` otherwise. + */ equals(otherParams: HttpSearchParams): boolean { return this.contains(otherParams) && this.size === otherParams.size; } + /** + * Checks if the current search parameters contain another set of search parameters. This method is less strict than + * {@link HttpSearchParams.equals} and only requires that all keys and values in the other search parameters are + * present in these search parameters. + * + * @param otherParams The other search parameters to check for containment. + * @returns `true` if these search parameters contain the other search parameters, `false` otherwise. + */ contains(otherParams: HttpSearchParams): boolean { for (const [key, value] of otherParams.entries()) { if (!super.has.call(this, key, value)) { diff --git a/packages/zimic/src/http/searchParams/types.ts b/packages/zimic/src/http/searchParams/types.ts index c51ac403..68792625 100644 --- a/packages/zimic/src/http/searchParams/types.ts +++ b/packages/zimic/src/http/searchParams/types.ts @@ -12,6 +12,7 @@ export type HttpSearchParamsSchemaTuple = [Key in keyof Schema & string]: [Key, ArrayItemIfArray>]; }[keyof Schema & string]; +/** An initialization value for {@link HttpSearchParams}. */ export type HttpSearchParamsInit = | string | URLSearchParams diff --git a/packages/zimic/src/http/types/requests.ts b/packages/zimic/src/http/types/requests.ts index 436c4eca..e5df5863 100644 --- a/packages/zimic/src/http/types/requests.ts +++ b/packages/zimic/src/http/types/requests.ts @@ -8,8 +8,16 @@ import { HttpSearchParamsSchema } from '../searchParams/types'; /** The default body type (JSON) for HTTP requests and responses. */ export type HttpBody = JSONValue; +/** + * An HTTP headers object with a strictly-typed schema. Fully compatible with the built-in + * {@link https://developer.mozilla.org/docs/Web/API/Headers Headers} class. + */ export type StrictHeaders = Pick, keyof Headers>; +/** + * An HTTP search params object with a strictly-typed schema. Fully compatible with the built-in + * {@link https://developer.mozilla.org/docs/Web/API/URLSearchParams URLSearchParams} class. + */ export type StrictURLSearchParams = Pick< HttpSearchParams, keyof URLSearchParams diff --git a/packages/zimic/src/http/types/schema.ts b/packages/zimic/src/http/types/schema.ts index 7822c86e..e6461866 100644 --- a/packages/zimic/src/http/types/schema.ts +++ b/packages/zimic/src/http/types/schema.ts @@ -7,60 +7,82 @@ import { HttpBody } from './requests'; export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] as const; export type HttpMethod = (typeof HTTP_METHODS)[number]; +/** A schema representing the structure of an HTTP request. */ export interface HttpServiceRequestSchema { headers?: HttpHeadersSchema; searchParams?: HttpSearchParamsSchema; body?: HttpBody; } +/** A schema representing the structure of an HTTP response. */ export interface HttpServiceResponseSchema { headers?: HttpHeadersSchema; body?: HttpBody; } +/** A schema representing the structures of HTTP responses by status code. */ export interface HttpServiceResponseSchemaByStatusCode { [statusCode: number]: HttpServiceResponseSchema; } +/** Extracts the status codes used in response schema by status code. */ export type HttpServiceResponseSchemaStatusCode< ResponseSchemaByStatusCode extends HttpServiceResponseSchemaByStatusCode, > = Extract; +/** A schema representing the structures of an HTTP request and response for a given method. */ export interface HttpServiceMethodSchema { request?: HttpServiceRequestSchema; response?: HttpServiceResponseSchemaByStatusCode; } +/** A schema representing the structures of HTTP request and response by method. */ export type HttpServiceMethodsSchema = { [Method in HttpMethod]?: HttpServiceMethodSchema; }; +/** A schema representing the structures of paths, methods, requests, and responses for an HTTP service. */ export interface HttpServiceSchema { [path: string]: HttpServiceMethodsSchema; } +/** A namespace containing utility types for validating HTTP type schemas. */ export namespace HttpSchema { + /** Validates that a type is a valid HTTP service schema. */ export type Paths = Schema; + /** Validates that a type is a valid HTTP service methods schema. */ export type Methods = Schema; + /** Validates that a type is a valid HTTP service method schema. */ export type Method = Schema; + /** Validates that a type is a valid HTTP service request schema. */ export type Request = Schema; + /** Validates that a type is a valid HTTP service response schema by status code. */ export type ResponseByStatusCode = Schema; + /** Validates that a type is a valid HTTP service response schema. */ export type Response = Schema; + /** Validates that a type is a valid HTTP headers schema. */ export type Headers = Schema; + /** Validates that a type is a valid HTTP search params schema. */ export type SearchParams = Schema; } +/** Extracts the methods from an HTTP service schema. */ export type HttpServiceSchemaMethod = IfAny< Schema, any, // eslint-disable-line @typescript-eslint/no-explicit-any Extract, HttpMethod> >; +/** + * Extracts the literal paths from an HTTP service schema containing certain methods. Only the methods defined in the + * schema are allowed. + */ export type LiteralHttpServiceSchemaPath< Schema extends HttpServiceSchema, Method extends HttpServiceSchemaMethod, > = LooseLiteralHttpServiceSchemaPath; +/** Extracts the literal paths from an HTTP service schema containing certain methods. Any method is allowed. */ export type LooseLiteralHttpServiceSchemaPath = { [Path in Extract]: Method extends keyof Schema[Path] ? Path : never; }[Extract]; @@ -72,11 +94,13 @@ export type AllowAnyStringInPathParameters = ? `${Prefix}${string}` : Path; +/** Extracts the non-literal paths from an HTTP service schema containing certain methods. */ export type NonLiteralHttpServiceSchemaPath< Schema extends HttpServiceSchema, Method extends HttpServiceSchemaMethod, > = AllowAnyStringInPathParameters>; +/** Extracts the paths from an HTTP service schema containing certain methods. */ export type HttpServiceSchemaPath> = | LiteralHttpServiceSchemaPath | NonLiteralHttpServiceSchemaPath; diff --git a/packages/zimic/src/interceptor/http/interceptor/factory.ts b/packages/zimic/src/interceptor/http/interceptor/factory.ts index eb1e0510..8d49aaa3 100644 --- a/packages/zimic/src/interceptor/http/interceptor/factory.ts +++ b/packages/zimic/src/interceptor/http/interceptor/factory.ts @@ -7,8 +7,9 @@ import { HttpInterceptor as PublicHttpInterceptor } from './types/public'; /** * Creates an HTTP interceptor. * - * @param {HttpInterceptorOptions} options The options for the interceptor. - * @returns {HttpInterceptor} The created HTTP interceptor. + * @param options The options for the interceptor. + * @returns The created HTTP interceptor. + * @throws {InvalidHttpInterceptorWorkerPlatform} When the worker platform is invalid. * @see {@link https://github.com/diego-aquino/zimic#createhttpinterceptor} */ export function createHttpInterceptor( diff --git a/packages/zimic/src/interceptor/http/interceptor/types/options.ts b/packages/zimic/src/interceptor/http/interceptor/types/options.ts index 6e1dd9da..51e515fa 100644 --- a/packages/zimic/src/interceptor/http/interceptor/types/options.ts +++ b/packages/zimic/src/interceptor/http/interceptor/types/options.ts @@ -1,5 +1,6 @@ import { HttpInterceptorWorker } from '../../interceptorWorker/types/public'; +/** Options to create an HTTP interceptor. */ export interface HttpInterceptorOptions { /** * The {@link https://github.com/diego-aquino/zimic#httpinterceptorworker HttpInterceptorWorker} instance for the diff --git a/packages/zimic/src/interceptor/http/interceptor/types/public.ts b/packages/zimic/src/interceptor/http/interceptor/types/public.ts index d7ca1c52..b88b9abc 100644 --- a/packages/zimic/src/interceptor/http/interceptor/types/public.ts +++ b/packages/zimic/src/interceptor/http/interceptor/types/public.ts @@ -20,72 +20,71 @@ export interface HttpInterceptor { baseURL: () => string; /** + * @param path The path to intercept. Paths with dynamic parameters, such as `/users/:id`, are supported, but you need + * to specify the original path as a type parameter to get type-inference and type-validation. * @returns A GET {@link https://github.com/diego-aquino/zimic#httprequesttracker HttpRequestTracker} for the provided * path. The path and method must be declared in the interceptor schema. - * - * Paths with dynamic parameters, such as `/users/:id`, are supported, but you need to specify the original - * - * Path as a type parameter to get type-inference and type-validation. + * @throws {NotStartedHttpInterceptorWorkerError} If the worker is not running. * @see {@link https://github.com/diego-aquino/zimic#interceptormethodpath} */ get: HttpInterceptorMethodHandler; /** + * @param path The path to intercept. Paths with dynamic parameters, such as `/users/:id`, are supported, but you need + * to specify the original path as a type parameter to get type-inference and type-validation. * @returns A POST {@link https://github.com/diego-aquino/zimic#httprequesttracker HttpRequestTracker} for the provided * path. The path and method must be declared in the interceptor schema. - * - * Paths with dynamic parameters, such as `/users/:id`, are supported, but you need to specify the original path as a - * type parameter to get type-inference and type-validation. + * @throws {NotStartedHttpInterceptorWorkerError} If the worker is not running. * @see {@link https://github.com/diego-aquino/zimic#interceptormethodpath} */ post: HttpInterceptorMethodHandler; /** + * @param path The path to intercept. Paths with dynamic parameters, such as `/users/:id`, are supported, but you need + * to specify the original path as a type parameter to get type-inference and type-validation. * @returns A PATCH {@link https://github.com/diego-aquino/zimic#httprequesttracker HttpRequestTracker} for the * provided path. The path and method must be declared in the interceptor schema. - * - * Paths with dynamic parameters, such as `/users/:id`, are supported, but you need to specify the original path as a - * type parameter to get type-inference and type-validation. + * @throws {NotStartedHttpInterceptorWorkerError} If the worker is not running. * @see {@link https://github.com/diego-aquino/zimic#interceptormethodpath} */ patch: HttpInterceptorMethodHandler; /** + * @param path The path to intercept. Paths with dynamic parameters, such as `/users/:id`, are supported, but you need + * to specify the original path as a type parameter to get type-inference and type-validation. * @returns A PUT {@link https://github.com/diego-aquino/zimic#httprequesttracker HttpRequestTracker} for the provided * path. The path and method must be declared in the interceptor schema. - * - * Paths with dynamic parameters, such as `/users/:id`, are supported, but you need to specify the original path as a - * type parameter to get type-inference and type-validation. + * @throws {NotStartedHttpInterceptorWorkerError} If the worker is not running. * @see {@link https://github.com/diego-aquino/zimic#interceptormethodpath} */ put: HttpInterceptorMethodHandler; /** + * @param path The path to intercept. Paths with dynamic parameters, such as `/users/:id`, are supported, but you need + * to specify the original path as a type parameter to get type-inference and type-validation. * @returns A DELETE {@link https://github.com/diego-aquino/zimic#httprequesttracker HttpRequestTracker} for the * provided path. The path and method must be declared in the interceptor schema. - * - * Paths with dynamic parameters, such as `/users/:id`, are supported, but you need to specify the original path as a - * type parameter to get type-inference and type-validation. + * @throws {NotStartedHttpInterceptorWorkerError} If the worker is not running. * @see {@link https://github.com/diego-aquino/zimic#interceptormethodpath} */ delete: HttpInterceptorMethodHandler; /** + * @param path The path to intercept. Paths with dynamic parameters, such as `/users/:id`, are supported, but you need + * to specify the original path as a type parameter to get type-inference and type-validation. * @returns A HEAD {@link https://github.com/diego-aquino/zimic#httprequesttracker HttpRequestTracker} for the provided * path. The path and method must be declared in the interceptor schema. - * - * Paths with dynamic parameters, such as `/users/:id`, are supported, but you need to specify the original path as a - * type parameter to get type-inference and type-validation. + * @throws {NotStartedHttpInterceptorWorkerError} If the worker is not running. * @see {@link https://github.com/diego-aquino/zimic#interceptormethodpath} */ head: HttpInterceptorMethodHandler; /** + * @param path The path to intercept. Paths with dynamic parameters, such as `/users/:id`, are supported, but you need + * to specify the original path as a type parameter to get type-inference and type-validation. * @returns An OPTIONS {@link https://github.com/diego-aquino/zimic#httprequesttracker HttpRequestTracker} for the * provided path. The path and method must be declared in the interceptor schema. - * - * Paths with dynamic parameters, such as `/users/:id`, are supported, but you need to specify the original path as a - * type parameter to get type-inference and type-validation. + * @throws {NotStartedHttpInterceptorWorkerError} If the worker is not running. * @see {@link https://github.com/diego-aquino/zimic#interceptormethodpath} */ options: HttpInterceptorMethodHandler; diff --git a/packages/zimic/src/interceptor/http/interceptorWorker/errors/OtherHttpInterceptorWorkerRunningError.ts b/packages/zimic/src/interceptor/http/interceptorWorker/errors/OtherHttpInterceptorWorkerRunningError.ts index ede62842..dd7d96b1 100644 --- a/packages/zimic/src/interceptor/http/interceptorWorker/errors/OtherHttpInterceptorWorkerRunningError.ts +++ b/packages/zimic/src/interceptor/http/interceptorWorker/errors/OtherHttpInterceptorWorkerRunningError.ts @@ -1,3 +1,4 @@ +/** Error thrown when trying to start a new HTTP interceptor worker while another one is already running. */ class OtherHttpInterceptorWorkerRunningError extends Error { constructor() { super( diff --git a/packages/zimic/src/interceptor/http/interceptorWorker/factory.ts b/packages/zimic/src/interceptor/http/interceptorWorker/factory.ts index 71991a1d..7200082a 100644 --- a/packages/zimic/src/interceptor/http/interceptorWorker/factory.ts +++ b/packages/zimic/src/interceptor/http/interceptorWorker/factory.ts @@ -5,8 +5,8 @@ import { HttpInterceptorWorker as PublicHttpInterceptorWorker } from './types/pu /** * Creates an HTTP interceptor worker. * - * @param {HttpInterceptorWorkerOptions} options The options for the worker. - * @returns {HttpInterceptorWorker} The created HTTP interceptor worker. + * @param options The options for the worker. + * @returns The created HTTP interceptor worker. * @see {@link https://github.com/diego-aquino/zimic#createhttpinterceptorworker} */ export function createHttpInterceptorWorker(options: HttpInterceptorWorkerOptions): PublicHttpInterceptorWorker { diff --git a/packages/zimic/src/interceptor/http/interceptorWorker/types/options.ts b/packages/zimic/src/interceptor/http/interceptorWorker/types/options.ts index 903f5a46..254418a7 100644 --- a/packages/zimic/src/interceptor/http/interceptorWorker/types/options.ts +++ b/packages/zimic/src/interceptor/http/interceptorWorker/types/options.ts @@ -9,6 +9,7 @@ type HttpInterceptorWorkerPlatformUnion = `${HttpInterceptorWorkerPlatformEnum}` export type HttpInterceptorWorkerPlatform = HttpInterceptorWorkerPlatformEnum | HttpInterceptorWorkerPlatformUnion; export const HttpInterceptorWorkerPlatform = HttpInterceptorWorkerPlatformEnum; // eslint-disable-line @typescript-eslint/no-redeclare +/** Options to create an HTTP interceptor worker. */ export interface HttpInterceptorWorkerOptions { /** * The platform used by the worker (`browser` or `node`). diff --git a/packages/zimic/src/interceptor/http/requestTracker/types/public.ts b/packages/zimic/src/interceptor/http/requestTracker/types/public.ts index 997fabcd..95af205f 100644 --- a/packages/zimic/src/interceptor/http/requestTracker/types/public.ts +++ b/packages/zimic/src/interceptor/http/requestTracker/types/public.ts @@ -94,6 +94,23 @@ export interface HttpRequestTracker< */ path: () => Path; + /** + * Declares a restriction to intercepted request matches. `headers`, `searchParams`, and `body` are supported to limit + * which requests will match the tracker and receive the mock response. If multiple restrictions are declared, either + * in a single object or with multiple calls to `.with()`, all of them must be met, essentially creating an AND + * condition. + * + * By default, restrictions use `exact: false`, meaning that any request **containing** the declared restrictions will + * match the tracker, regardless of having more properties or values. If you want to match only requests with the + * exact values declared, you can use `exact: true`. + * + * A function is also supported to declare restrictions, in case they are dynamic. + * + * @param restriction The restriction to match intercepted requests. + * @returns The same tracker, now considering the specified restriction. + * @see {@link https://github.com/diego-aquino/zimic#trackerwithrestriction} + */ + with: ( restriction: HttpRequestTrackerRestriction, ) => HttpRequestTracker; @@ -116,23 +133,36 @@ export interface HttpRequestTracker< ) => HttpRequestTracker; /** - * Clears any declared responses, making the tracker stop matching intercepted requests. The next tracker, created - * before this one, that matches the same method and path will be used if present. If not, the requests of the method - * and path will not be intercepted. + * Clears any response declared with + * [`.respond(declaration)`](https://github.com/diego-aquino/zimic#trackerresponddeclaration), making the tracker stop + * matching requests. The next tracker, created before this one, that matches the same method and path will be used if + * present. If not, the requests of the method and path will not be intercepted. * * To make the tracker match requests again, register a new response with `tracker.respond()`. * + * This method is useful to skip a tracker. It is more gentle than + * [`tracker.clear()`](https://github.com/diego-aquino/zimic#trackerclear), as it only removed the response, keeping + * restrictions and intercepted requests. + * + * @returns The same tracker, now without a declared responses. * @see {@link https://github.com/diego-aquino/zimic#trackerbypass} */ bypass: () => HttpRequestTracker; /** - * Clears any declared responses, restrictions, and intercepted requests, making the tracker stop matching intercepted - * requests. The next tracker, created before this one, that matches the same method and path will be used if present. - * If not, the requests of the method and path will not be intercepted. + * Clears any response declared with + * [`.respond(declaration)`](https://github.com/diego-aquino/zimic#trackerresponddeclaration), restrictions declared + * with [`.with(restriction)`](https://github.com/diego-aquino/zimic#trackerwithrestriction), and intercepted + * requests, making the tracker stop matching requests. The next tracker, created before this one, that matches the + * same method and path will be used if present. If not, the requests of the method and path will not be intercepted. * * To make the tracker match requests again, register a new response with `tracker.respond()`. * + * This method is useful to reset trackers to a clean state between tests. It is more aggressive than + * [`tracker.bypass()`](https://github.com/diego-aquino/zimic#trackerbypass), as it also clears restrictions and + * intercepted requests. + * + * @returns The same tracker, now cleared of any declared responses, restrictions, and intercepted requests. * @see {@link https://github.com/diego-aquino/zimic#trackerclear} */ clear: () => HttpRequestTracker; diff --git a/packages/zimic/src/interceptor/http/requestTracker/types/requests.ts b/packages/zimic/src/interceptor/http/requestTracker/types/requests.ts index 1164f460..0c982e7f 100644 --- a/packages/zimic/src/interceptor/http/requestTracker/types/requests.ts +++ b/packages/zimic/src/interceptor/http/requestTracker/types/requests.ts @@ -17,6 +17,7 @@ export type HttpRequestTrackerResponseHeadersAttribute> }; +/** A declaration of an HTTP response for an intercepted request. */ export type HttpRequestTrackerResponseDeclaration< MethodSchema extends HttpServiceMethodSchema, StatusCode extends HttpServiceResponseSchemaStatusCode>, @@ -25,6 +26,12 @@ export type HttpRequestTrackerResponseDeclaration< } & HttpRequestTrackerResponseBodyAttribute[StatusCode]> & HttpRequestTrackerResponseHeadersAttribute[StatusCode]>; +/** + * A factory function for creating {@link HttpRequestTrackerResponseDeclaration} objects. + * + * @param request The intercepted request. + * @returns The response declaration. + */ export type HttpRequestTrackerResponseDeclarationFactory< MethodSchema extends HttpServiceMethodSchema, StatusCode extends HttpServiceResponseSchemaStatusCode>, @@ -45,6 +52,7 @@ export type HttpRequestBodySchema null >; +/** A strict representation of an intercepted HTTP request. The body is already parsed by default. */ export interface HttpInterceptorRequest extends Omit { headers: HttpHeaders>; @@ -63,6 +71,7 @@ export type HttpResponseBodySchema< StatusCode extends HttpServiceResponseSchemaStatusCode>, > = Default[StatusCode]['body'], null>; +/** A strict representation of an intercepted HTTP response. The body is already parsed by default. */ export interface HttpInterceptorResponse< MethodSchema extends HttpServiceMethodSchema, StatusCode extends HttpServiceResponseSchemaStatusCode>, @@ -82,6 +91,7 @@ export const HTTP_INTERCEPTOR_RESPONSE_HIDDEN_BODY_PROPERTIES = Exclude> >; +/** A strict representation of a tracked, intercepted HTTP request, along with its response. */ export interface TrackedHttpInterceptorRequest< MethodSchema extends HttpServiceMethodSchema, StatusCode extends HttpServiceResponseSchemaStatusCode> = never, diff --git a/packages/zimic/src/types/json.ts b/packages/zimic/src/types/json.ts index 8db14d38..2c07cb63 100644 --- a/packages/zimic/src/types/json.ts +++ b/packages/zimic/src/types/json.ts @@ -3,6 +3,10 @@ type JSON = { [key: string]: JSON } | JSON[] | string | number | boolean | null /** Value that is compatible and can be represented in JSON. */ export type JSONValue = Type; +/** + * Recursively converts a type to its JSON-serialized version. Dates are converted to strings and non-JSON values are + * excluded. + */ export type JSONSerialized = Type extends string | number | boolean | null | undefined ? Type : Type extends Date