Mentoss

repository·main·Indexed 41 days ago

https://github.com/humanwhocodes/mentoss

A utility for mocking fetch() requests and responses in browser and Node.js environments, including support for undici. Version 0.13.0 features MockServer for route definition, FetchMocker for intercepting fetch calls, and MockAgent as an undici Dispatcher. It supports advanced request matching, CORS simulation, and cookie management via CookieCredentials.

Tokens
19.6K
Snippets
71
Records
76
Agent score
51%

What's inside Mentoss

  1. What is a MockAgent and when to use it

    main

    A MockAgent is an undici Dispatcher that intercepts HTTP requests made by undici and returns responses defined in your MockServer instances.

    Use MockAgent when:

    • You are testing Node.js applications using undici (e.g., request(), stream(), pipeline()).
    • You are testing Node.js native fetch() (available in Node.js 18+).

    Use FetchMocker instead when:

    • You are testing in a browser environment.

    Note: MockAgent is designed for Node.js 18+ only.

  2. Understand the Mentoss website project structure

    main

    The Mentoss website follows an Astro + Starlight project structure. Key directories and files include:

    • src/content/docs/: Contains .md or .mdx files. Starlight uses these files to generate routes based on their filenames.
    • src/assets/: Place images here to embed them in Markdown using relative links.
    • public/: Stores static assets like favicons.
    • astro.config.mjs: The main Astro configuration file.
    • package.json: Defines dependencies and scripts.
    • content.config.ts: Configuration for content collections.
    .
    ├── public/
    ├── src/
    │   ├── assets/
    │   ├── content/
    │   │   ├── docs/
    │   └── content.config.ts
    ├── astro.config.mjs
    ├── package.json
    └── tsconfig.json
  3. Manage route matching order

    main

    The MockServer checks routes in the exact order they were registered. The server stops at the first match it finds. If multiple routes could potentially match a single request, the one added earliest to the server will be the one that responds.

    Best Practice: If you know the exact URL that will be requested, use a hardcoded path instead of a dynamic pattern (e.g., use /api/users/123 instead of /api/users/:id) to ensure specific behavior takes precedence.

    import { MockServer } from "mentoss";
    
    const server = new MockServer("https://api.example.com");
    
    // This route will catch the request first
    server.get("/api/users/:id", 200);
    
    // This route will NEVER be reached for /api/users/123 because the one above matches first
    server.get("/api/users/123", 204);
  4. Cookie matching rules in Mentoss

    main

    When using CookieCredentials with FetchMocker, cookies are automatically included in requests if they meet these criteria:

    1. Domain: The request domain must match or be a subdomain of the cookie's domain.
    2. Path: The request path must match or be under the cookie's path.
    3. Security: For cookies with secure: true, the request must use https.
    4. SameSite: The request must match the cookie's sameSite policy (if set).
    const credentials = new CookieCredentials("https://example.com");
    
    credentials.setCookie({
        name: "adminSession",
        value: "xyz789",
        path: "/admin",
        secure: true
    });
    
    // MATCHES: domain, path, and protocol match
    await fetch("https://example.com/admin/users");
    
    // FAILS: wrong protocol (http instead of https)
    await fetch("http://example.com/admin/users");
    
    // FAILS: wrong path
    await fetch("https://example.com/user/profile");
    
    // MATCHES: subdomain is allowed
    await fetch("https://sub.example.com/admin/users");
  5. How routes are matched in Mentoss

    main

    Mentoss matches incoming requests against registered routes by comparing request details in a specific order. When a match is found, the server returns a Response object; otherwise, it returns undefined.

    Matching Priority Order:

    1. Path: The URL path (e.g., /api/users).
    2. Method: The HTTP method (e.g., GET, POST).
    3. URL Parameters: Dynamic segments in the path (e.g., :id in /api/users/:id).
    4. Query Parameters: The query string (e.g., ?id=123).
    5. Headers: Request headers.
    6. Body: The request body content.

    Note on Query Parameters: If you do not explicitly provide a query object in your route definition, the server ignores all query parameters in the request. For example, a route for /api/users will match both /api/users and /api/users?sort=asc unless a query object is specified.

    import { MockServer } from "mentoss";
    
    const server = new MockServer("https://api.example.com");
    
    // Matches any GET to /api/users/:id, ignoring query params
    server.get("/api/users/:id", 200);
    
    // Matches only GET to /api/users/:id if query param profile=full is present
    server.get(
    	{
    		url: "/api/users/:id",
    		query: {
    			profile: "full",
    		},
    	},
    	200,
    );
  6. Use extended request patterns in MockServer

    main

    When defining routes in a MockServer instance (using methods like .get(), .post(), etc.), the first argument can be an object instead of a simple string. This object allows you to define complex matching criteria using several keys.

    Matching Logic: For query, headers, and body (when using objects or FormData), the pattern matches if the incoming request contains all the specified keys/values. Extra keys or headers present in the actual request that are not in the pattern will not prevent a match.

    import { MockServer } from "mentoss";
    
    const server = new MockServer("https://api.example.com");
    
    // Using an object pattern instead of a string
    server.get(
      {
        url: "/users",
        query: { userId: "123" }
      },
      200
    );
  7. Configure extended response patterns in MockServer

    main

    When defining routes in a MockServer instance, the second argument can be a response pattern. This pattern defines how the mock server should respond to matching requests.

    It can be:

    1. A number: Represents the HTTP status code.
    2. An object: Specifies detailed response data (status, headers, body, delay).
    3. A function: Allows for dynamic response generation based on the request context.
    import { MockServer } from "mentoss";
    
    const server = new MockServer("https://api.example.com");
    
    // Using a number
    server.get("/status", 200);
    
    // Using an object
    server.get("/data", {
      status: 200,
      body: { key: "value" }
    });
    
    // Using a function
    server.get("/dynamic", (request) => {
      return { status: 200, body: { id: request.id } };
    });
  8. Mock credentialed requests with `CookieCredentials`

    main

    To test authenticated requests (like those using cookies), use the credentials option in the FetchMocker constructor. This option accepts an array of credential objects, such as instances of CookieCredentials.

    Requirement: The credentials option must be used in conjunction with the baseUrl option in FetchMocker. Mentoss requires a base URL to determine which credentials apply to a request.

    import { MockServer, FetchMocker, CookieCredentials } from "mentoss";
    
    const BASE_URL = "https://example.com";
    const server = new MockServer(BASE_URL);
    const credentials = new CookieCredentials(BASE_URL);
    
    const mocker = new FetchMocker({
        servers: [server],
        credentials: [credentials],
        baseUrl: BASE_URL,
    });
  9. How MockServer, FetchMocker, and MockAgent work together

    main

    Mentoss uses three primary abstractions to handle mocking:

    1. MockServer: The core configuration object where you define your mocked routes, request matching criteria (URL, headers, body), and responses (status, body, headers).
    2. FetchMocker: A utility used in browser or Node.js environments to create a new fetch() function that routes requests to one or more MockServer instances. It can also overwrite the global fetch via .mockGlobal().
    3. MockAgent: An undici Dispatcher designed for Node.js. It intercepts undici requests and routes them to MockServer instances.

    To use Mentoss, you typically define a MockServer first, then pass it to either a FetchMocker or a MockAgent depending on your runtime and HTTP client.

    import { MockServer, FetchMocker } from "mentoss";
    
    const server = new MockServer("https://api.example.com");
    server.get("/foo/bar", 200);
    
    const mocker = new FetchMocker({ servers: [server] });
    const { fetch } = mocker;
  10. Mock Preflighted CORS Requests

    main

    For preflighted requests (e.g., requests with custom headers like Content-Type: application/json), Mentoss automatically sends an OPTIONS request before the actual request.

    To mock this successfully, you must:

    1. Define an OPTIONS route on your MockServer that returns the appropriate Access-Control-Allow-Origin and Access-Control-Allow-Headers.
    2. Define the actual method route (e.g., GET, POST) with the same CORS headers.

    If the OPTIONS response lacks the required headers, the original request will fail.

    import { MockServer, FetchMocker } from "mentoss";
    
    const BASE_URL = "https://api.example.com";
    
    describe("My API", () => {
    	const server = new MockServer("https://www.example.com");
    	const mocker = new FetchMocker({
    		servers: [server],
    		baseUrl: BASE_URL,
    	});
    
    	const myFetch = mocker.fetch;
    
    	it("should return a 200 status code", async () => {
    		// Preflight request setup
    		server.options("/ping", {
    			status: 200,
    			headers: {
    				"Access-Control-Allow-Origin": BASE_URL,
    				"Access-Control-Allow-Headers": "Content-Type",
    			},
    		});
    
    		// Actual request setup
    		server.get("/ping", {
    			status: 200,
    			headers: {
    				"Access-Control-Allow-Origin": BASE_URL,
    			},
    			body: { message: "pong" },
    		});
    
    		const response = await myFetch("https://www.example.com/ping", {
    			headers: {
    				"Content-Type": "application/json",
    			},
    		});
    
    		expect(response.status).to.equal(200);
    	});
    });
  11. Mock requests using `fetch()` (Browser and Node.js)

    main

    To mock fetch() calls, create a MockServer with a base URL, define your routes, and then use FetchMocker to generate a mocked fetch function or to overwrite the global one.

    import { MockServer, FetchMocker } from "mentoss";
    
    // 1. Create a server with a base URL
    const server = new MockServer("https://api.example.com");
    
    // 2. Define routes
    // Simple route
    server.get("/foo/bar", 200);
    
    // Route with specific response body and headers
    server.post("/foo/baz", {
    	status: 200,
    	body: { message: "Success" },
    	headers: {
    		"Content-Type": "application/json",
    	},
    });
    
    // Route with advanced matching (URL, headers, and body)
    server.post(
    	{
    		url: "/foo/boom",
    		headers: {
    			"Content-type": "application/json",
    		},
    		body: {
    			test: true,
    		},
    	},
    	404,
    );
    
    // 3. Create the mocker
    const mocker = new FetchMocker({
    	servers: [server],
    });
    
    // 4. Use the mocked fetch
    const { fetch } = mocker;
    // OR: mocker.mockGlobal();
    
    const response = await fetch("https://api.example.com/foo/bar");
    
    // 5. Assertions and cleanup
    assert(mocker.called("https://api.example.com/foo/bar"));
    assert(mocker.allRoutesCalled());
    
    server.clear(); // Clears routes on the server
    mocker.clearAll(); // Clears everything in the mocker
  12. Match a request with a Blob body

    main

    The body key in a request pattern cannot be a Blob instance. To match a request containing a Blob, you must manually set the Content-Type header and provide the body as the underlying data (e.g., an object or string) that the Blob represents.

    Tip: Runtimes like Bun may modify MIME types (e.g., changing text/plain to text/plain;charset=utf-8). To avoid matching failures, use the type property directly from a Blob instance when defining your pattern.

    import { MockServer } from "mentoss";
    const server = new MockServer("https://api.example.com");
    
    // The blob we expect to receive
    const blob = new Blob([JSON.stringify({ name: "John Doe" })], {
    	type: "application/json",
    });
    
    server.post(
    	{
    		url: "/users",
    		headers: {
    			"Content-Type": blob.type,
    		},
    		body: {
    			name: "John Doe"
    		}
    	},
    	200,
    );