Skip to content

Commit ad92bfd

Browse files
authored
add more e2e for route handlers in app-router (#780)
* add methods to app-router * add additional routes * add tests for additional routes * lint * changeset * Delete .changeset/moody-monkeys-explode.md * changeset patch * fix cookies * remove next gh issue * literal keys * toBe instead of toContain * lint * rm changeset * toContain in cookies test
1 parent dd3a610 commit ad92bfd

File tree

9 files changed

+341
-0
lines changed

9 files changed

+341
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export async function GET(
2+
request: Request,
3+
{ params }: { params: Promise<{ slug: string }> },
4+
) {
5+
const { slug } = await params;
6+
return Response.json({ slug });
7+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { NextRequest } from "next/server";
2+
3+
export function GET(request: NextRequest) {
4+
const searchParams = request.nextUrl.searchParams;
5+
const query = searchParams.get("query");
6+
if (query === "OpenNext is awesome!") {
7+
return Response.json({ query });
8+
}
9+
return new Response("Internal Server Error", { status: 500 });
10+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { redirect } from "next/navigation";
2+
3+
export async function GET(request: Request) {
4+
redirect("https://nextjs.org/");
5+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const revalidate = 5;
2+
3+
async function getTime() {
4+
return new Date().toISOString();
5+
}
6+
7+
export async function GET() {
8+
const time = await getTime();
9+
return Response.json({ time });
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const dynamic = "force-static";
2+
3+
async function getTime() {
4+
return new Date().toISOString();
5+
}
6+
7+
export async function GET() {
8+
const time = await getTime();
9+
return Response.json({ time });
10+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { cookies } from "next/headers";
2+
3+
export async function POST(request: Request) {
4+
const formData = await request.formData();
5+
const username = formData.get("username");
6+
const password = formData.get("password");
7+
if (username === "hakuna" && password === "matata") {
8+
(await cookies()).set("auth_session", "SUPER_SECRET_SESSION_ID_1234");
9+
return Response.json(
10+
{
11+
message: "ok",
12+
},
13+
{
14+
status: 202,
15+
},
16+
);
17+
}
18+
return Response.json({ message: "you must login" }, { status: 401 });
19+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export async function POST(request: Request) {
2+
const formData = await request.formData();
3+
const name = formData.get("name");
4+
const email = formData.get("email");
5+
if (name === "OpenNext [] () %&#!%$#" && email === "opennext@opennext.com") {
6+
return Response.json(
7+
{
8+
message: "ok",
9+
},
10+
{
11+
status: 202,
12+
},
13+
);
14+
}
15+
return Response.json({ message: "forbidden" }, { status: 403 });
16+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import type { NextRequest } from "next/server";
2+
3+
export async function GET() {
4+
return Response.json({
5+
message: "OpenNext is awesome! :) :] :> :D",
6+
});
7+
}
8+
9+
export async function POST(request: Request) {
10+
const text = await request.text();
11+
if (text === "OpenNext is awesome! :] :) :> :D") {
12+
return Response.json(
13+
{
14+
message: "ok",
15+
},
16+
{
17+
status: 202,
18+
},
19+
);
20+
}
21+
return Response.json({ message: "forbidden" }, { status: 403 });
22+
}
23+
24+
export async function PUT(request: Request) {
25+
const res = (await request.json()) as {
26+
message: string;
27+
};
28+
if (res.message === "OpenNext PUT") {
29+
return Response.json({ message: "ok" }, { status: 201 });
30+
}
31+
return Response.json({ message: "error" }, { status: 500 });
32+
}
33+
34+
export async function PATCH(request: Request) {
35+
const res = (await request.json()) as {
36+
message: string;
37+
};
38+
if (res.message === "OpenNext PATCH") {
39+
return Response.json(
40+
{ message: "ok", modified: true, timestamp: new Date().toISOString() },
41+
{ status: 202 },
42+
);
43+
}
44+
return Response.json({ message: "error" }, { status: 500 });
45+
}
46+
47+
export async function DELETE(request: NextRequest) {
48+
const searchParams = request.nextUrl.searchParams;
49+
const command = searchParams.get("command");
50+
if (command === "rm -rf / --no-preserve-root") {
51+
return new Response(null, { status: 204 });
52+
}
53+
return Response.json({ message: "error" }, { status: 500 });
54+
}
55+
56+
export async function HEAD() {
57+
return new Response("hello", {
58+
status: 200,
59+
headers: {
60+
"content-type": "text/html; charset=utf-8",
61+
// Once deployed to AWS this will always be 0
62+
// "content-length": "1234567",
63+
"special-header": "OpenNext is the best :) :] :> :D",
64+
},
65+
});
66+
}
67+
68+
export async function OPTIONS() {
69+
return new Response(null, {
70+
status: 204,
71+
headers: {
72+
Allow: "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, LOVE",
73+
Special: "OpenNext is the best :) :] :> :D",
74+
},
75+
});
76+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
test.describe("all supported methods should work in route handlers", () => {
4+
test("GET", async ({ request }) => {
5+
const getRes = await request.get("/methods");
6+
const getData = await getRes.json();
7+
expect(getRes.status()).toEqual(200);
8+
expect(getData.message).toEqual("OpenNext is awesome! :) :] :> :D");
9+
});
10+
11+
test("POST", async ({ request }) => {
12+
const postRes = await request.post("/methods", {
13+
headers: {
14+
"Content-Type": "text/plain",
15+
},
16+
data: "OpenNext is awesome! :] :) :> :D",
17+
});
18+
expect(postRes.status()).toBe(202);
19+
const postData = await postRes.json();
20+
expect(postData.message).toBe("ok");
21+
const errorPostRes = await request.post("/methods", {
22+
headers: {
23+
"Content-Type": "text/plain",
24+
},
25+
data: "OpenNext is not awesome! :C",
26+
});
27+
expect(errorPostRes.status()).toBe(403);
28+
const errorData = await errorPostRes.json();
29+
expect(errorData.message).toBe("forbidden");
30+
});
31+
32+
test("PUT", async ({ request }) => {
33+
const putRes = await request.put("/methods", {
34+
data: {
35+
message: "OpenNext PUT",
36+
},
37+
});
38+
expect(putRes.status()).toEqual(201);
39+
const putData = await putRes.json();
40+
expect(putData.message).toEqual("ok");
41+
});
42+
43+
test("PATCH", async ({ request }) => {
44+
const timestampBefore = new Date();
45+
const patchRes = await request.patch("/methods", {
46+
data: { message: "OpenNext PATCH" },
47+
});
48+
expect(patchRes.status()).toEqual(202);
49+
const patchData = await patchRes.json();
50+
expect(patchData.message).toEqual("ok");
51+
expect(patchData.modified).toEqual(true);
52+
expect(Date.parse(patchData.timestamp)).toBeGreaterThan(
53+
timestampBefore.getTime(),
54+
);
55+
});
56+
57+
test("DELETE", async ({ request }) => {
58+
const deleteRes = await request.delete("/methods", {
59+
params: {
60+
command: "rm -rf / --no-preserve-root",
61+
},
62+
});
63+
expect(deleteRes.status()).toEqual(204);
64+
});
65+
66+
test("HEAD", async ({ request }) => {
67+
const headRes = await request.head("/methods");
68+
expect(headRes.status()).toEqual(200);
69+
const headers = headRes.headers();
70+
expect(headers["content-type"]).toEqual("text/html; charset=utf-8");
71+
// expect(headers["content-length"]).toEqual("1234567");
72+
expect(headers["special-header"]).toEqual(
73+
"OpenNext is the best :) :] :> :D",
74+
);
75+
});
76+
77+
test("OPTIONS", async ({ request }) => {
78+
const optionsRes = await request.fetch("/methods", {
79+
method: "OPTIONS",
80+
});
81+
expect(optionsRes.status()).toEqual(204);
82+
const headers = optionsRes.headers();
83+
expect(headers.allow).toBe(
84+
"GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, LOVE",
85+
);
86+
expect(headers.special).toBe("OpenNext is the best :) :] :> :D");
87+
});
88+
});
89+
90+
test("formData should work in POST route handler", async ({ request }) => {
91+
const formData = new FormData();
92+
formData.append("name", "OpenNext [] () %&#!%$#");
93+
formData.append("email", "opennext@opennext.com");
94+
const postRes = await request.post("/methods/post/formdata", {
95+
form: formData,
96+
});
97+
expect(postRes.status()).toBe(202);
98+
const postData = await postRes.json();
99+
expect(postData.message).toBe("ok");
100+
});
101+
102+
test("revalidate should work in GET route handler", async ({
103+
request,
104+
page,
105+
}) => {
106+
let time = Date.parse(
107+
(await request.get("/methods/get/revalidate").then((res) => res.json()))
108+
.time,
109+
);
110+
let newTime: number;
111+
let tempTime = time;
112+
do {
113+
await page.waitForTimeout(1000);
114+
time = tempTime;
115+
const newTimeRes = await request.get("/methods/get/revalidate");
116+
newTime = Date.parse((await newTimeRes.json()).time);
117+
tempTime = newTime;
118+
} while (time !== newTime);
119+
const midTime = Date.parse(
120+
(await request.get("/methods/get/revalidate").then((res) => res.json()))
121+
.time,
122+
);
123+
124+
await page.waitForTimeout(1000);
125+
// Expect that the time is still stale
126+
expect(midTime).toEqual(newTime);
127+
128+
// Wait 5 + 1 seconds for ISR to regenerate time
129+
await page.waitForTimeout(6000);
130+
let finalTime = newTime;
131+
do {
132+
await page.waitForTimeout(2000);
133+
finalTime = Date.parse(
134+
(await request.get("/methods/get/revalidate").then((res) => res.json()))
135+
.time,
136+
);
137+
} while (newTime === finalTime);
138+
139+
expect(newTime).not.toEqual(finalTime);
140+
});
141+
142+
test("should cache a static GET route", async ({ request }) => {
143+
const res = await request.get("/methods/get/static");
144+
expect(res.headers()["cache-control"]).toBe("s-maxage=31536000,");
145+
});
146+
147+
test("should be able to set cookies in route handler", async ({ request }) => {
148+
const postRes = await request.post("/methods/post/cookies", {
149+
form: {
150+
username: "hakuna",
151+
password: "matata",
152+
},
153+
});
154+
expect(postRes.status()).toBe(202);
155+
const postData = await postRes.json();
156+
expect(postData.message).toBe("ok");
157+
const cookies = postRes.headers()["set-cookie"];
158+
expect(cookies).toContain("auth_session=SUPER_SECRET_SESSION_ID_1234");
159+
});
160+
161+
test("should be able to redirect in route handler", async ({ request }) => {
162+
const redirectRes = await request.get("/methods/get/redirect", {
163+
// Disable auto-redirect to check initial response
164+
maxRedirects: 0,
165+
});
166+
expect(redirectRes.status()).toBe(307);
167+
expect(redirectRes.headers().location).toBe("https://nextjs.org/");
168+
169+
// Check if the redirect works
170+
const followedRes = await request.get("/methods/get/redirect");
171+
expect(followedRes.url()).toBe("https://nextjs.org/");
172+
});
173+
174+
test("dynamic segments should work in route handlers", async ({ request }) => {
175+
const res = await request.get("/methods/get/dynamic-segments/this-is-a-slug");
176+
const data = await res.json();
177+
expect(data.slug).toBe("this-is-a-slug");
178+
});
179+
180+
test("query parameters should work in route handlers", async ({ request }) => {
181+
const res = await request.get("/methods/get/query", {
182+
params: {
183+
query: "OpenNext is awesome!",
184+
},
185+
});
186+
const data = await res.json();
187+
expect(data.query).toBe("OpenNext is awesome!");
188+
});

0 commit comments

Comments
 (0)