返回 CodeWhale
middleware.test.ts
根目录 / web / lib / middleware.test.ts
1 import { describe, expect, it } from "vitest";
2 import { NextRequest } from "next/server";
3 import { middleware } from "../middleware";
4
5 /**
6 * `www.codewhale.net` and `codewhale.net` are both bound to this worker as
7 * custom domains, so without a canonical-host redirect the whole site is
8 * reachable — and indexable — twice.
9 */
10 function request(url: string, host: string, headers: Record<string, string> = {}, method = "GET") {
11 return new NextRequest(new URL(url), {
12 method,
13 headers: new Headers({ host, ...headers }),
14 });
15 }
16
17 describe("canonical host", () => {
18 it("permanently redirects www to the apex, preserving path and query", () => {
19 const res = middleware(
20 request("https://www.codewhale.net/en/docs/hooks?x=1", "www.codewhale.net"),
21 );
22 expect(res.status).toBe(308);
23 expect(res.headers.get("location")).toBe("https://codewhale.net/en/docs/hooks?x=1");
24 });
25
26 it("redirects assets and API routes too, so a moved document stops pulling from www", () => {
27 for (const path of ["/_next/static/chunk.js", "/api/curated", "/opengraph-image"]) {
28 const res = middleware(request(`https://www.codewhale.net${path}`, "www.codewhale.net"));
29 expect(res.status, path).toBe(308);
30 expect(res.headers.get("location"), path).toBe(`https://codewhale.net${path}`);
31 }
32 });
33
34 it("keeps an admin form POST a POST across the host move", () => {
35 // A 301 lets clients replay a POST as a body-less GET (#6561 W01-07).
36 for (const path of ["/api/admin/login", "/api/admin/post"]) {
37 const res = middleware(request(`https://www.codewhale.net${path}`, "www.codewhale.net", {}, "POST"));
38 expect(res.status, path).toBe(308);
39 expect(res.headers.get("location"), path).toBe(`https://codewhale.net${path}`);
40 }
41 });
42
43 it("leaves the apex host alone", () => {
44 const res = middleware(request("https://codewhale.net/en", "codewhale.net"));
45 expect(res.status).not.toBe(308);
46 });
47
48 it("leaves localhost and preview hosts alone", () => {
49 for (const host of ["localhost:3000", "codewhale-web.pages.dev"]) {
50 const res = middleware(request("https://example.test/en", host));
51 expect(res.status, host).not.toBe(308);
52 }
53 });
54
55 it("still applies security headers to the redirect", () => {
56 const res = middleware(request("https://www.codewhale.net/en", "www.codewhale.net"));
57 expect(res.headers.get("X-Content-Type-Options")).toBe("nosniff");
58 expect(res.headers.get("Strict-Transport-Security")).toContain("max-age=");
59 });
60 });
61
62 describe("dotted well-known paths", () => {
63 it("does not locale-prefix /llms.txt", () => {
64 const res = middleware(request("https://codewhale.net/llms.txt", "codewhale.net"));
65 expect(res.status).not.toBe(307);
66 expect(res.status).not.toBe(308);
67 expect(res.headers.get("location")).toBeNull();
68 });
69 });
70
71 describe("public auth aliases (#5767)", () => {
72 it("does not locale-prefix /auth/callback into a 404", () => {
73 const res = middleware(
74 request("https://codewhale.net/auth/callback?code=abc&state=1", "codewhale.net"),
75 );
76 expect(res.status).toBe(307);
77 expect(res.headers.get("location")).toBe(
78 "https://app.codewhale.net/auth/callback?code=abc&state=1",
79 );
80 });
81
82 it("hops an already-localized callback to the CWC app", () => {
83 const res = middleware(
84 request("https://codewhale.net/en/auth/callback?code=abc", "codewhale.net"),
85 );
86 expect(res.status).toBe(307);
87 expect(res.headers.get("location")).toBe(
88 "https://app.codewhale.net/auth/callback?code=abc",
89 );
90 });
91
92 it("folds /login onto /signin before locale prefixing", () => {
93 const res = middleware(request("https://codewhale.net/login", "codewhale.net"));
94 expect(res.status).toBe(308);
95 expect(res.headers.get("location")).toBe("https://codewhale.net/signin");
96 });
97
98 it("folds a localized /register onto /signup", () => {
99 const res = middleware(request("https://codewhale.net/zh/register", "codewhale.net"));
100 expect(res.status).toBe(308);
101 expect(res.headers.get("location")).toBe("https://codewhale.net/zh/signup");
102 });
103
104 it("locale-prefixes /signin and /signup onto real public pages", () => {
105 const signin = middleware(
106 request("https://codewhale.net/signin", "codewhale.net", {
107 "accept-language": "ja,en;q=0.8",
108 }),
109 );
110 expect(signin.status).toBe(307);
111 expect(signin.headers.get("location")).toBe("https://codewhale.net/ja/signin");
112
113 const signup = middleware(request("https://codewhale.net/signup", "codewhale.net"));
114 expect(signup.status).toBe(307);
115 expect(signup.headers.get("location")).toBe("https://codewhale.net/en/signup");
116 });
117
118 it("leaves an already-localized sign-in page unprefixed", () => {
119 const res = middleware(request("https://codewhale.net/en/signin", "codewhale.net"));
120 expect(res.status).not.toBe(307);
121 expect(res.headers.get("location")).toBeNull();
122 });
123 });
124
125 describe("locale prefix", () => {
126 it("prefixes a bare path with the detected locale", () => {
127 const res = middleware(
128 request("https://codewhale.net/install", "codewhale.net", {
129 "accept-language": "ja,en;q=0.8",
130 }),
131 );
132 expect(res.status).toBe(307);
133 expect(res.headers.get("location")).toBe("https://codewhale.net/ja/install");
134 });
135
136 it("leaves an already-localized path, including pt-BR, unprefixed", () => {
137 for (const path of ["/zh/install", "/pt-BR/docs/guide", "/de"]) {
138 const res = middleware(request(`https://codewhale.net${path}`, "codewhale.net"));
139 expect(res.status, path).not.toBe(307);
140 expect(res.headers.get("location"), path).toBeNull();
141 }
142 });
143
144 it("folds a miscased locale prefix onto its canonical spelling", () => {
145 // Without this the segment reads as bare and the request lands on
146 // `/en/pt-br/install`, which is a 404 rather than the Portuguese page.
147 const res = middleware(
148 request("https://codewhale.net/pt-br/install?x=1", "codewhale.net"),
149 );
150 expect(res.status).toBe(308);
151 expect(res.headers.get("location")).toBe("https://codewhale.net/pt-BR/install?x=1");
152 });
153
154 it("settles after one canonicalizing redirect", () => {
155 const res = middleware(request("https://codewhale.net/pt-BR/install", "codewhale.net"));
156 expect(res.status).not.toBe(308);
157 expect(res.headers.get("location")).toBeNull();
158 });
159 });
160
161 describe("install aliases (M2, UX-13)", () => {
162 it("sends bare /download, /desktop and /pricing to install in one hop", () => {
163 for (const path of ["/download", "/desktop", "/pricing"]) {
164 const res = middleware(
165 request(`https://codewhale.net${path}?ref=x`, "codewhale.net", {
166 "accept-language": "ja,en;q=0.8",
167 }),
168 );
169 expect(res.status, path).toBe(307);
170 expect(res.headers.get("location"), path).toBe("https://codewhale.net/ja/install?ref=x");
171 }
172 });
173
174 it("keeps an existing locale, folding miscased prefixes in the same hop", () => {
175 for (const [path, want] of [
176 ["/zh/download", "/zh/install"],
177 ["/en/pricing", "/en/install"],
178 ["/pt-br/desktop", "/pt-BR/install"],
179 ]) {
180 const res = middleware(request(`https://codewhale.net${path}`, "codewhale.net"));
181 expect(res.status, path).toBe(307);
182 expect(res.headers.get("location"), path).toBe(`https://codewhale.net${want}`);
183 }
184 });
185
186 it("leaves deeper paths that merely start with an alias alone", () => {
187 const res = middleware(request("https://codewhale.net/en/docs/pricing", "codewhale.net"));
188 expect(res.headers.get("location")).toBeNull();
189 });
190 });
191
191 lines TYPESCRIPT