Skip to content

Commit 6348f63

Browse files
committed
test: add test cases to i18n middleware with exclusions
1 parent 28217d4 commit 6348f63

File tree

10 files changed

+267
-0
lines changed

10 files changed

+267
-0
lines changed

tests/e2e/edge-middleware.test.ts

+143
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,146 @@ test('json data rewrite works', async ({ middlewarePages }) => {
6969

7070
expect(data.pageProps.message).toBeDefined()
7171
})
72+
73+
// those tests use `fetch` instead of `page.goto` intentionally to avoid potential client rendering
74+
// hiding any potential edge/server issues
75+
test.describe('Middleware with i18n and excluded paths', () => {
76+
const DEFAULT_LOCALE = 'en'
77+
78+
// those tests hit paths ending with `/json` which has special handling in middleware
79+
// to return JSON response from middleware itself
80+
test.describe('Middleware response path', () => {
81+
test('should match on non-localized not excluded page path', async ({
82+
middlewareI18nExcludedPaths,
83+
}) => {
84+
const response = await fetch(`${middlewareI18nExcludedPaths.url}/json`)
85+
86+
expect(response.headers.get('x-test-used-middleware')).toBe('true')
87+
expect(response.status).toBe(200)
88+
89+
const { nextUrlPathname, nextUrlLocale } = await response.json()
90+
91+
expect(nextUrlPathname).toBe('/json')
92+
expect(nextUrlLocale).toBe(DEFAULT_LOCALE)
93+
})
94+
95+
test('should match on localized not excluded page path', async ({
96+
middlewareI18nExcludedPaths,
97+
}) => {
98+
const response = await fetch(`${middlewareI18nExcludedPaths.url}/fr/json`)
99+
100+
expect(response.headers.get('x-test-used-middleware')).toBe('true')
101+
expect(response.status).toBe(200)
102+
103+
const { nextUrlPathname, nextUrlLocale } = await response.json()
104+
105+
expect(nextUrlPathname).toBe('/json')
106+
expect(nextUrlLocale).toBe('fr')
107+
})
108+
})
109+
110+
// those tests hit paths that don't end with `/json` so they should be passed through to origin
111+
// if they match middleware matcher OR skip middleware and go to origin directly if they don't
112+
// match middleware matcher
113+
test.describe('Middleware passthrough', () => {
114+
function extractDataFromHtml(html: string): Record<string, any> {
115+
const match = html.match(/<pre>(?<rawInput>[^<]+)<\/pre>/)
116+
if (!match || !match.groups?.rawInput) {
117+
console.error('<pre> not found in html input', {
118+
html,
119+
})
120+
throw new Error('Failed to extract data from HTML')
121+
}
122+
123+
const { rawInput } = match.groups
124+
const unescapedInput = rawInput.replaceAll('&quot;', '"')
125+
try {
126+
return JSON.parse(unescapedInput)
127+
} catch (originalError) {
128+
console.error('Failed to parse JSON', {
129+
originalError,
130+
rawInput,
131+
unescapedInput,
132+
})
133+
}
134+
throw new Error('Failed to extract data from HTML')
135+
}
136+
137+
test('should match on non-localized not excluded page path', async ({
138+
middlewareI18nExcludedPaths,
139+
}) => {
140+
const response = await fetch(`${middlewareI18nExcludedPaths.url}/html`)
141+
142+
expect(response.headers.get('x-test-used-middleware')).toBe('true')
143+
expect(response.status).toBe(200)
144+
expect(response.headers.get('content-type')).toMatch(/text\/html/)
145+
146+
const html = await response.text()
147+
const { locale, params } = extractDataFromHtml(html)
148+
149+
expect(params).toMatchObject({ catchall: ['html'] })
150+
expect(locale).toBe(DEFAULT_LOCALE)
151+
})
152+
153+
test('should match on localized not excluded page path', async ({
154+
middlewareI18nExcludedPaths,
155+
}) => {
156+
const response = await fetch(`${middlewareI18nExcludedPaths.url}/fr/html`)
157+
158+
expect(response.headers.get('x-test-used-middleware')).toBe('true')
159+
expect(response.status).toBe(200)
160+
expect(response.headers.get('content-type')).toMatch(/text\/html/)
161+
162+
const html = await response.text()
163+
const { locale, params } = extractDataFromHtml(html)
164+
165+
expect(params).toMatchObject({ catchall: ['html'] })
166+
expect(locale).toBe('fr')
167+
})
168+
169+
test('should NOT match on non-localized excluded API path', async ({
170+
middlewareI18nExcludedPaths,
171+
}) => {
172+
const response = await fetch(`${middlewareI18nExcludedPaths.url}/api/html`)
173+
174+
expect(response.headers.get('x-test-used-middleware')).not.toBe('true')
175+
expect(response.status).toBe(200)
176+
177+
const { params } = await response.json()
178+
179+
expect(params).toMatchObject({ catchall: ['html'] })
180+
})
181+
182+
test('should NOT match on non-localized excluded page path', async ({
183+
middlewareI18nExcludedPaths,
184+
}) => {
185+
const response = await fetch(`${middlewareI18nExcludedPaths.url}/excluded`)
186+
187+
expect(response.headers.get('x-test-used-middleware')).not.toBe('true')
188+
expect(response.status).toBe(200)
189+
expect(response.headers.get('content-type')).toMatch(/text\/html/)
190+
191+
const html = await response.text()
192+
const { locale, params } = extractDataFromHtml(html)
193+
194+
expect(params).toMatchObject({ catchall: ['excluded'] })
195+
expect(locale).toBe(DEFAULT_LOCALE)
196+
})
197+
198+
test('should NOT match on localized excluded page path', async ({
199+
middlewareI18nExcludedPaths,
200+
}) => {
201+
const response = await fetch(`${middlewareI18nExcludedPaths.url}/fr/excluded`)
202+
203+
expect(response.headers.get('x-test-used-middleware')).not.toBe('true')
204+
expect(response.status).toBe(200)
205+
expect(response.headers.get('content-type')).toMatch(/text\/html/)
206+
207+
const html = await response.text()
208+
const { locale, params } = extractDataFromHtml(html)
209+
210+
expect(params).toMatchObject({ catchall: ['excluded'] })
211+
expect(locale).toBe('fr')
212+
})
213+
})
214+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { NextResponse } from 'next/server'
2+
import type { NextRequest } from 'next/server'
3+
4+
export async function middleware(request: NextRequest) {
5+
const url = request.nextUrl
6+
7+
// if path ends with /json we create response in middleware, otherwise we pass it through
8+
// to next server to get page or api response from it
9+
const response = url.pathname.includes('/json')
10+
? NextResponse.json({
11+
requestUrlPathname: new URL(request.url).pathname,
12+
nextUrlPathname: request.nextUrl.pathname,
13+
nextUrlLocale: request.nextUrl.locale,
14+
})
15+
: NextResponse.next()
16+
17+
response.headers.set('x-test-used-middleware', 'true')
18+
19+
return response
20+
}
21+
22+
// matcher copied from example in https://nextjs.org/docs/pages/building-your-application/routing/middleware#matcher
23+
// with `excluded` segment added to exclusion
24+
export const config = {
25+
matcher: [
26+
/*
27+
* Match all request paths except for the ones starting with:
28+
* - api (API routes)
29+
* - excluded (for testing localized routes and not just API routes)
30+
* - _next/static (static files)
31+
* - _next/image (image optimization files)
32+
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
33+
*/
34+
'/((?!api|excluded|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
35+
],
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/// <reference types="next" />
2+
/// <reference types="next/image-types/global" />
3+
4+
// NOTE: This file should not be edited
5+
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = {
2+
output: 'standalone',
3+
eslint: {
4+
ignoreDuringBuilds: true,
5+
},
6+
i18n: {
7+
locales: ['en', 'fr'],
8+
defaultLocale: 'en',
9+
},
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "middleware-i18n-excluded-paths",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"postinstall": "next build",
7+
"dev": "next dev",
8+
"build": "next build"
9+
},
10+
"dependencies": {
11+
"next": "latest",
12+
"react": "18.2.0",
13+
"react-dom": "18.2.0"
14+
},
15+
"devDependencies": {
16+
"@types/node": "^17.0.12",
17+
"@types/react": "18.2.47",
18+
"typescript": "^5.2.2"
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { GetStaticPaths, GetStaticProps } from 'next'
2+
3+
export default function CatchAll({ params, locale }) {
4+
return <pre>{JSON.stringify({ params, locale }, null, 2)}</pre>
5+
}
6+
7+
export const getStaticPaths: GetStaticPaths = () => {
8+
return {
9+
paths: [],
10+
fallback: 'blocking',
11+
}
12+
}
13+
14+
export const getStaticProps: GetStaticProps = ({ params, locale }) => {
15+
return {
16+
props: {
17+
params,
18+
locale,
19+
},
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { NextApiRequest, NextApiResponse } from 'next'
2+
3+
type ResponseData = {
4+
params: {
5+
catchall?: string[]
6+
}
7+
}
8+
9+
export default function handler(req: NextApiRequest, res: NextApiResponse<ResponseData>) {
10+
res.status(200).json({ params: req.query })
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2017",
4+
"lib": ["dom", "dom.iterable", "esnext"],
5+
"allowJs": true,
6+
"skipLibCheck": true,
7+
"strict": false,
8+
"noEmit": true,
9+
"incremental": true,
10+
"module": "esnext",
11+
"esModuleInterop": true,
12+
"moduleResolution": "node",
13+
"resolveJsonModule": true,
14+
"isolatedModules": true,
15+
"jsx": "preserve"
16+
},
17+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
18+
"exclude": ["node_modules"]
19+
}

tests/prepare.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const e2eOnlyFixtures = new Set([
2323
'after',
2424
'cli-before-regional-blobs-support',
2525
'dist-dir',
26+
'middleware-i18n-excluded-paths',
2627
// There is also a bug on Windows on Node.js 18.20.6, that cause build failures on this fixture
2728
// see https://github.com/opennextjs/opennextjs-netlify/actions/runs/13268839161/job/37043172448?pr=2749#step:12:78
2829
'middleware-og',

tests/utils/create-e2e-fixture.ts

+1
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ export const fixtureFactories = {
333333
pnpm: () => createE2EFixture('pnpm', { packageManger: 'pnpm' }),
334334
bun: () => createE2EFixture('simple', { packageManger: 'bun' }),
335335
middleware: () => createE2EFixture('middleware'),
336+
middlewareI18nExcludedPaths: () => createE2EFixture('middleware-i18n-excluded-paths'),
336337
middlewareOg: () => createE2EFixture('middleware-og'),
337338
middlewarePages: () => createE2EFixture('middleware-pages'),
338339
pageRouter: () => createE2EFixture('page-router'),

0 commit comments

Comments
 (0)