Skip to content

Commit e5338b6

Browse files
authored
Add unit tests for handling middleware (opennextjs#576)
1 parent 8979fde commit e5338b6

File tree

2 files changed

+271
-3
lines changed

2 files changed

+271
-3
lines changed

packages/open-next/src/core/routing/middleware.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ type MiddlewareOutputEvent = InternalEvent & {
2828
externalRewrite?: boolean;
2929
};
3030

31+
type Middleware = (request: Request) => Response | Promise<Response>;
32+
type MiddlewareLoader = () => Promise<{ default: Middleware }>;
33+
34+
function defaultMiddlewareLoader() {
35+
// @ts-expect-error - This is bundled
36+
return import("./middleware.mjs");
37+
}
38+
3139
// NOTE: As of Nextjs 13.4.13+, the middleware is handled outside the next-server.
3240
// OpenNext will run the middleware in a sandbox and set the appropriate req headers
3341
// and res.body prior to processing the next-server.
@@ -36,6 +44,7 @@ type MiddlewareOutputEvent = InternalEvent & {
3644
// if res.end() is return, the parent needs to return and not process next server
3745
export async function handleMiddleware(
3846
internalEvent: InternalEvent,
47+
middlewareLoader: MiddlewareLoader = defaultMiddlewareLoader,
3948
): Promise<MiddlewareOutputEvent | InternalResult> {
4049
const { query } = internalEvent;
4150
const normalizedPath = localizePath(internalEvent);
@@ -53,8 +62,7 @@ export async function handleMiddleware(
5362
const url = initialUrl.toString();
5463
// console.log("url", url, normalizedPath);
5564

56-
// @ts-expect-error - This is bundled
57-
const middleware = await import("./middleware.mjs");
65+
const middleware = await middlewareLoader();
5866

5967
const result: Response = await middleware.default({
6068
geo: {
@@ -73,7 +81,7 @@ export async function handleMiddleware(
7381
},
7482
url,
7583
body: convertBodyToReadableStream(internalEvent.method, internalEvent.body),
76-
});
84+
} as unknown as Request);
7785
const statusCode = result.status;
7886

7987
/* Apply override headers from middleware
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import { handleMiddleware } from "@opennextjs/aws/core/routing/middleware.js";
2+
import { convertFromQueryString } from "@opennextjs/aws/core/routing/util.js";
3+
import { InternalEvent } from "@opennextjs/aws/types/open-next.js";
4+
import { toReadableStream } from "@opennextjs/aws/utils/stream.js";
5+
import { vi } from "vitest";
6+
7+
vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({
8+
NextConfig: {},
9+
MiddlewareManifest: {
10+
sortedMiddleware: ["/"],
11+
middleware: {
12+
"/": {
13+
files: [
14+
"prerender-manifest.js",
15+
"server/edge-runtime-webpack.js",
16+
"server/middleware.js",
17+
],
18+
name: "middleware",
19+
page: "/",
20+
matchers: [
21+
{
22+
regexp:
23+
"^(?:\\/(_next\\/data\\/[^/]{1,}))?(?:\\/((?!_next|favicon.ico|match|static|fonts|api\\/auth|og).*))(.json)?[\\/#\\?]?$",
24+
originalSource:
25+
"/((?!_next|favicon.ico|match|static|fonts|api/auth|og).*)",
26+
},
27+
],
28+
wasm: [],
29+
assets: [],
30+
},
31+
},
32+
functions: {},
33+
version: 2,
34+
},
35+
}));
36+
37+
vi.mock("@opennextjs/aws/core/routing/i18n/index.js", () => ({
38+
localizePath: (event: InternalEvent) => event.rawPath,
39+
}));
40+
41+
const middleware = vi.fn();
42+
const middlewareLoader = vi.fn().mockResolvedValue({
43+
default: middleware,
44+
});
45+
46+
type PartialEvent = Partial<
47+
Omit<InternalEvent, "body" | "rawPath" | "query">
48+
> & { body?: string };
49+
50+
function createEvent(event: PartialEvent): InternalEvent {
51+
const [rawPath, qs] = (event.url ?? "/").split("?", 2);
52+
return {
53+
type: "core",
54+
method: event.method ?? "GET",
55+
rawPath,
56+
url: event.url ?? "/",
57+
body: Buffer.from(event.body ?? ""),
58+
headers: event.headers ?? {},
59+
query: convertFromQueryString(qs ?? ""),
60+
cookies: event.cookies ?? {},
61+
remoteAddress: event.remoteAddress ?? "::1",
62+
};
63+
}
64+
65+
beforeEach(() => {
66+
vi.clearAllMocks();
67+
});
68+
69+
/**
70+
* Ideally these tests would be broken up and tests smaller parts of the middleware rather than the entire function.
71+
*/
72+
describe("handleMiddleware", () => {
73+
it("should bypass middlware for internal requests", async () => {
74+
const event = createEvent({
75+
headers: {
76+
"x-isr": "1",
77+
},
78+
});
79+
const result = await handleMiddleware(event, middlewareLoader);
80+
81+
expect(middlewareLoader).not.toBeCalled();
82+
expect(result).toEqual(event);
83+
});
84+
85+
it("should invoke middlware with redirect", async () => {
86+
const event = createEvent({});
87+
middleware.mockResolvedValue({
88+
status: 302,
89+
headers: new Headers({
90+
location: "/redirect",
91+
}),
92+
});
93+
const result = await handleMiddleware(event, middlewareLoader);
94+
95+
expect(middlewareLoader).toBeCalled();
96+
expect(result.statusCode).toEqual(302);
97+
expect(result.headers.location).toEqual("/redirect");
98+
});
99+
100+
it("should invoke middlware with external redirect", async () => {
101+
const event = createEvent({});
102+
middleware.mockResolvedValue({
103+
status: 302,
104+
headers: new Headers({
105+
location: "http://external/redirect",
106+
}),
107+
});
108+
const result = await handleMiddleware(event, middlewareLoader);
109+
110+
expect(middlewareLoader).toBeCalled();
111+
expect(result.statusCode).toEqual(302);
112+
expect(result.headers.location).toEqual("http://external/redirect");
113+
});
114+
115+
it("should invoke middlware with rewrite", async () => {
116+
const event = createEvent({
117+
headers: {
118+
host: "localhost",
119+
},
120+
});
121+
middleware.mockResolvedValue({
122+
headers: new Headers({
123+
"x-middleware-rewrite": "http://localhost/rewrite",
124+
}),
125+
});
126+
const result = await handleMiddleware(event, middlewareLoader);
127+
128+
expect(middlewareLoader).toBeCalled();
129+
expect(result).toEqual({
130+
...event,
131+
rawPath: "/rewrite",
132+
url: "/rewrite",
133+
responseHeaders: {
134+
"x-middleware-rewrite": "http://localhost/rewrite",
135+
},
136+
externalRewrite: false,
137+
});
138+
});
139+
140+
it("should invoke middlware with rewrite with __nextDataReq", async () => {
141+
const event = createEvent({
142+
url: "/rewrite?__nextDataReq=1&key=value",
143+
headers: {
144+
host: "localhost",
145+
},
146+
});
147+
middleware.mockResolvedValue({
148+
headers: new Headers({
149+
"x-middleware-rewrite": "http://localhost/rewrite?newKey=value",
150+
}),
151+
});
152+
const result = await handleMiddleware(event, middlewareLoader);
153+
154+
expect(middlewareLoader).toBeCalled();
155+
expect(result).toEqual({
156+
...event,
157+
rawPath: "/rewrite",
158+
url: "/rewrite",
159+
responseHeaders: {
160+
"x-middleware-rewrite": "http://localhost/rewrite?newKey=value",
161+
},
162+
query: {
163+
__nextDataReq: "1",
164+
newKey: "value",
165+
},
166+
externalRewrite: false,
167+
});
168+
});
169+
170+
it("should invoke middlware with external rewrite", async () => {
171+
const event = createEvent({
172+
headers: {
173+
host: "localhost",
174+
},
175+
});
176+
middleware.mockResolvedValue({
177+
headers: new Headers({
178+
"x-middleware-rewrite": "http://external/rewrite",
179+
}),
180+
});
181+
const result = await handleMiddleware(event, middlewareLoader);
182+
183+
expect(middlewareLoader).toBeCalled();
184+
expect(result).toEqual({
185+
...event,
186+
rawPath: "http://external/rewrite",
187+
url: "http://external/rewrite",
188+
responseHeaders: {
189+
"x-middleware-rewrite": "http://external/rewrite",
190+
},
191+
externalRewrite: true,
192+
});
193+
});
194+
195+
it("should map x-middleware-request- headers as request headers", async () => {
196+
const event = createEvent({});
197+
middleware.mockResolvedValue({
198+
headers: new Headers({
199+
"x-middleware-request-custom-header": "value",
200+
}),
201+
});
202+
const result = await handleMiddleware(event, middlewareLoader);
203+
204+
expect(middlewareLoader).toBeCalled();
205+
expect(result).toEqual({
206+
...event,
207+
headers: {
208+
"custom-header": "value",
209+
},
210+
responseHeaders: {},
211+
externalRewrite: false,
212+
});
213+
});
214+
215+
it("should return a response from middleware", async () => {
216+
const event = createEvent({});
217+
const body = toReadableStream("Hello, world!");
218+
219+
middleware.mockResolvedValue({
220+
status: 200,
221+
headers: new Headers(),
222+
body,
223+
});
224+
const result = await handleMiddleware(event, middlewareLoader);
225+
226+
expect(middlewareLoader).toBeCalled();
227+
expect(result).toEqual({
228+
type: "core",
229+
statusCode: 200,
230+
headers: {},
231+
body,
232+
isBase64Encoded: false,
233+
});
234+
});
235+
236+
it("should return a response from middleware with set-cookie header", async () => {
237+
const event = createEvent({});
238+
const body = toReadableStream("Hello, world!");
239+
240+
middleware.mockResolvedValue({
241+
status: 200,
242+
headers: new Headers({
243+
"set-cookie": "cookie=value",
244+
}),
245+
body,
246+
});
247+
const result = await handleMiddleware(event, middlewareLoader);
248+
249+
expect(middlewareLoader).toBeCalled();
250+
expect(result).toEqual({
251+
type: "core",
252+
statusCode: 200,
253+
headers: {
254+
"set-cookie": ["cookie=value"],
255+
},
256+
body,
257+
isBase64Encoded: false,
258+
});
259+
});
260+
});

0 commit comments

Comments
 (0)