Skip to content

Commit

Permalink
add request headers matching
Browse files Browse the repository at this point in the history
  • Loading branch information
Matthieu Gicquel committed Dec 28, 2023
1 parent 7014089 commit 8b3b103
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 5 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ mockServer.get<BodyType>("item", {
response: { body }
});

// Match headers -- a request must contain *at least* the specified headers to match
mockServer.get<BodyType>("item", {
request: { headers: { Authoriztion: "Bearer some-token" }}
response: { body }
});

// Check that the correct body was sent
const mockHandler = mockServer.post("item", responseBody)

Expand Down Expand Up @@ -177,6 +183,7 @@ const mockHandler = mockServer.get<BodyType>("some-route/:id", {
request: {
pathParams: { id: "1" }
searchParams: { lang: "fr" }
headers: { Authorization: "Bearer some-token" }
},
response: {
status: 418
Expand Down
2 changes: 2 additions & 0 deletions src/configToDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type HandlerDefinition = {
request: {
pathParams: Record<string, string>;
searchParams: Record<string, string> | undefined;
headers: Record<string, string>;
};
};

Expand All @@ -44,6 +45,7 @@ export const configToDefinition = (params: {
request: {
pathParams: {},
searchParams: undefined,
headers: {},
...serverConfig.request,
},
response: {
Expand Down
32 changes: 32 additions & 0 deletions src/expectRequestsToMatchHandlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,38 @@ it("pathParam mismatch -> param missing in request", async () => {
`);
});

it("header mismatch -> present but wrong value", async () => {
mockServer.get("/test", {
request: { headers: { "x-test": "hello" } },
response: { body },
});

await fetch("https://test.com/test", { headers: { "x-test": "halo" } });

expect(getThrownMessage()).toMatchInlineSnapshot(`
"SHM: Received requests did not match defined handlers
UNHANDLED REQUEST: GET https://test.com/test
--> handler GET https://test.com/test -> header \\"x-test\\" -> \\"hello\\" !== \\"halo\\"
"
`);
});

it("header mismatch -> missing in request", async () => {
mockServer.get("/test", {
request: { headers: { "x-test": "hello" } },
response: { body },
});

await fetch("https://test.com/test");

expect(getThrownMessage()).toMatchInlineSnapshot(`
"SHM: Received requests did not match defined handlers
UNHANDLED REQUEST: GET https://test.com/test
--> handler GET https://test.com/test -> header \\"x-test\\" -> expected by handler but absent in request
"
`);
});

it("multiple close requests", async () => {
mockServer.get("/test", {
request: { searchParams: { id: "hello" } },
Expand Down
103 changes: 98 additions & 5 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import axios from "axios";
import { expect, it, describe, afterEach, vi, beforeAll } from "vitest";

import { createMockServer, installInterceptor, resetMockServers } from ".";
import {
createMockServer,
expectRequestsToMatchHandlers,
installInterceptor,
resetMockServers,
} from ".";

vi.useFakeTimers();

Expand Down Expand Up @@ -154,13 +159,13 @@ describe("url path params matching", () => {
describe("url search params matching", () => {
it("matches url search params when specified with the config", async () => {
mockServer.get("/test", {
request: { searchParams: { id: "1" } },
response: { body: expectedResponse },
request: { searchParams: { id: "2" } },
response: { body: unexpectedResponse },
});

mockServer.get("/test", {
request: { searchParams: { id: "2" } },
response: { body: unexpectedResponse },
request: { searchParams: { id: "1" } },
response: { body: expectedResponse },
});

const response = await fetch("https://test.com/test?id=1");
Expand Down Expand Up @@ -232,6 +237,94 @@ describe("url search params matching", () => {
});
});

describe("headers matching", () => {
it("matches a header", async () => {
mockServer.get("/test", {
request: { headers: { Authorization: "Bearer unexpected-token" } },
response: { body: unexpectedResponse },
});

mockServer.get("/test", {
request: { headers: { Authorization: "Bearer expected-token" } },
response: { body: expectedResponse },
});

const response = await fetch("https://test.com/test", {
headers: { Authorization: "Bearer expected-token" },
});

expect(await response.json()).toEqual(expectedResponse);
});

it("matches even when other headers are present in the request", async () => {
mockServer.get("/test", {
request: { headers: { Authorization: "Bearer expected-token" } },
response: { body: expectedResponse },
});

const response = await fetch("https://test.com/test", {
headers: {
Authorization: "Bearer expected-token",
"X-Other-Header": "hellothere",
},
});

expect(await response.json()).toEqual(expectedResponse);
});

it("ignores case in the header name", async () => {
mockServer.get("/test", {
request: { headers: { authorization: "Bearer expected-token" } },
response: { body: expectedResponse },
});

const response = await fetch("https://test.com/test", {
headers: { Authorization: "Bearer expected-token" },
});

expect(await response.json()).toEqual(expectedResponse);
});

it("matches even if the header is present multiple times", async () => {
mockServer.get("/test", {
request: { headers: { "Accept-Language": "fr-FR" } },
response: { body: expectedResponse },
});

const headers = new Headers();
headers.append("Accept-Language", "en-US");
headers.append("Accept-Language", "fr-FR");

const response = await fetch("https://test.com/test", {
headers,
});

expect(await response.json()).toEqual(expectedResponse);
});

it("matches multiple header values", async () => {
mockServer.get("/test", {
request: { headers: { Accept: "application/json, */*" } },
response: { body: unexpectedResponse },
});

mockServer.get("/test", {
request: { headers: { Accept: "application/json, text/plain" } },
response: { body: expectedResponse },
});

const headers = new Headers();
headers.append("Accept", "text/plain");
headers.append("Accept", "application/json");

const response = await fetch("https://test.com/test", {
headers,
});

expect(await response.json()).toEqual(expectedResponse);
});
});

describe("missing handlers", () => {
it("responds with an error when no handler matches", async () => {
const response = await fetch("https://test.com/test");
Expand Down
21 changes: 21 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,27 @@ const isHandlerMatching = (
}
}

for (const [key, handlerHeader] of Object.entries(handler.request.headers)) {
const requestHeader = request.headers.get(key);

if (!requestHeader) {
explain(`header "${key}" -> expected by handler but absent in request`);

return false;
}

const handlerHeaderValues = handlerHeader.split(",").map((v) => v.trim());
const requestHeaderValues = requestHeader.split(",").map((v) => v.trim());

for (const handlerHeaderValue of handlerHeaderValues) {
if (!requestHeaderValues.includes(handlerHeaderValue)) {
explain(`header "${key}" -> "${handlerHeaderValue}" !== "${requestHeader}"`);

return false;
}
}
}

return true;
};

Expand Down
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ export type FullHandlerConfig<TResponse> = {
*
*/
searchParams?: Record<string, string>;
/**
* Specify headers that must be present in a request to match
* Extra headers in the request will not cause a mismatch
* Extra values for a given header will not cause a mismatch (eg `Accept: application/json` will match `Accept: application/json, text/plain`)
*
* @example // match "Accept: application/json"
* mockServer.get("/test", { request: { headers: { Accept: "application/json" } } })
*/
headers?: Record<string, string>;
};
response?: {
/**
Expand Down

0 comments on commit 8b3b103

Please sign in to comment.