Skip to content

Commit 3d996dd

Browse files
committed
test: move middleware-i18n-exluded-path to test variants
1 parent 6069a74 commit 3d996dd

File tree

8 files changed

+211
-135
lines changed

8 files changed

+211
-135
lines changed

tests/e2e/edge-middleware.test.ts

Lines changed: 137 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type ExtendedFixtures = {
1212
edgeOrNodeMiddleware: Fixture
1313
edgeOrNodeMiddlewarePages: Fixture
1414
edgeOrNodeMiddlewareI18n: Fixture
15+
edgeOrNodeMiddlewareI18nExcludedPaths: Fixture
1516
}
1617

1718
for (const { expectedRuntime, label, testWithSwitchableMiddlewareRuntime } of [
@@ -43,6 +44,14 @@ for (const { expectedRuntime, label, testWithSwitchableMiddlewareRuntime } of [
4344
scope: 'worker',
4445
},
4546
],
47+
edgeOrNodeMiddlewareI18nExcludedPaths: [
48+
async ({ middlewareI18nExcludedPaths }, use) => {
49+
await use(middlewareI18nExcludedPaths)
50+
},
51+
{
52+
scope: 'worker',
53+
},
54+
],
4655
}),
4756
},
4857
hasNodeMiddlewareSupport()
@@ -74,6 +83,14 @@ for (const { expectedRuntime, label, testWithSwitchableMiddlewareRuntime } of [
7483
scope: 'worker',
7584
},
7685
],
86+
edgeOrNodeMiddlewareI18nExcludedPaths: [
87+
async ({ middlewareI18nExcludedPathsNode }, use) => {
88+
await use(middlewareI18nExcludedPathsNode)
89+
},
90+
{
91+
scope: 'worker',
92+
},
93+
],
7794
}),
7895
}
7996
: undefined,
@@ -297,154 +314,161 @@ for (const { expectedRuntime, label, testWithSwitchableMiddlewareRuntime } of [
297314
})
298315
})
299316

300-
if (expectedRuntime !== 'node') {
301-
// those tests use `fetch` instead of `page.goto` intentionally to avoid potential client rendering
302-
// hiding any potential edge/server issues
303-
test.describe('Middleware with i18n and excluded paths', () => {
304-
const DEFAULT_LOCALE = 'en'
305-
306-
/** helper function to extract JSON data from page rendering data with `<pre>{JSON.stringify(data)}</pre>` */
307-
function extractDataFromHtml(html: string): Record<string, any> {
308-
const match = html.match(/<pre>(?<rawInput>[^<]+)<\/pre>/)
309-
if (!match || !match.groups?.rawInput) {
310-
console.error('<pre> not found in html input', {
311-
html,
312-
})
313-
throw new Error('Failed to extract data from HTML')
314-
}
315-
316-
const { rawInput } = match.groups
317-
const unescapedInput = rawInput.replaceAll('&quot;', '"')
318-
try {
319-
return JSON.parse(unescapedInput)
320-
} catch (originalError) {
321-
console.error('Failed to parse JSON', {
322-
originalError,
323-
rawInput,
324-
unescapedInput,
325-
})
326-
}
317+
// those tests use `fetch` instead of `page.goto` intentionally to avoid potential client rendering
318+
// hiding any potential edge/server issues
319+
test.describe('Middleware with i18n and excluded paths', () => {
320+
const DEFAULT_LOCALE = 'en'
321+
322+
/** helper function to extract JSON data from page rendering data with `<pre>{JSON.stringify(data)}</pre>` */
323+
function extractDataFromHtml(html: string): Record<string, any> {
324+
const match = html.match(/<pre>(?<rawInput>[^<]+)<\/pre>/)
325+
if (!match || !match.groups?.rawInput) {
326+
console.error('<pre> not found in html input', {
327+
html,
328+
})
327329
throw new Error('Failed to extract data from HTML')
328330
}
329331

330-
// those tests hit paths ending with `/json` which has special handling in middleware
331-
// to return JSON response from middleware itself
332-
test.describe('Middleware response path', () => {
333-
test('should match on non-localized not excluded page path', async ({
334-
middlewareI18nExcludedPaths,
335-
}) => {
336-
const response = await fetch(`${middlewareI18nExcludedPaths.url}/json`)
332+
const { rawInput } = match.groups
333+
const unescapedInput = rawInput.replaceAll('&quot;', '"')
334+
try {
335+
return JSON.parse(unescapedInput)
336+
} catch (originalError) {
337+
console.error('Failed to parse JSON', {
338+
originalError,
339+
rawInput,
340+
unescapedInput,
341+
})
342+
}
343+
throw new Error('Failed to extract data from HTML')
344+
}
337345

338-
expect(response.headers.get('x-test-used-middleware')).toBe('true')
339-
expect(response.status).toBe(200)
346+
// those tests hit paths ending with `/json` which has special handling in middleware
347+
// to return JSON response from middleware itself
348+
test.describe('Middleware response path', () => {
349+
test('should match on non-localized not excluded page path', async ({
350+
edgeOrNodeMiddlewareI18nExcludedPaths,
351+
}) => {
352+
const response = await fetch(`${edgeOrNodeMiddlewareI18nExcludedPaths.url}/json`)
340353

341-
const { nextUrlPathname, nextUrlLocale } = await response.json()
354+
expect(response.headers.get('x-test-used-middleware')).toBe('true')
355+
expect(response.headers.get('x-runtime')).toEqual(expectedRuntime)
356+
expect(response.status).toBe(200)
342357

343-
expect(nextUrlPathname).toBe('/json')
344-
expect(nextUrlLocale).toBe(DEFAULT_LOCALE)
345-
})
358+
const { nextUrlPathname, nextUrlLocale } = await response.json()
359+
360+
expect(nextUrlPathname).toBe('/json')
361+
expect(nextUrlLocale).toBe(DEFAULT_LOCALE)
362+
})
346363

347-
test('should match on localized not excluded page path', async ({
348-
middlewareI18nExcludedPaths,
349-
}) => {
350-
const response = await fetch(`${middlewareI18nExcludedPaths.url}/fr/json`)
364+
test('should match on localized not excluded page path', async ({
365+
edgeOrNodeMiddlewareI18nExcludedPaths,
366+
}) => {
367+
const response = await fetch(`${edgeOrNodeMiddlewareI18nExcludedPaths.url}/fr/json`)
351368

352-
expect(response.headers.get('x-test-used-middleware')).toBe('true')
353-
expect(response.status).toBe(200)
369+
expect(response.headers.get('x-test-used-middleware')).toBe('true')
370+
expect(response.headers.get('x-runtime')).toEqual(expectedRuntime)
371+
expect(response.status).toBe(200)
354372

355-
const { nextUrlPathname, nextUrlLocale } = await response.json()
373+
const { nextUrlPathname, nextUrlLocale } = await response.json()
356374

357-
expect(nextUrlPathname).toBe('/json')
358-
expect(nextUrlLocale).toBe('fr')
359-
})
375+
expect(nextUrlPathname).toBe('/json')
376+
expect(nextUrlLocale).toBe('fr')
360377
})
378+
})
361379

362-
// those tests hit paths that don't end with `/json` while still satisfying middleware matcher
363-
// so middleware should pass them through to origin
364-
test.describe('Middleware passthrough', () => {
365-
test('should match on non-localized not excluded page path', async ({
366-
middlewareI18nExcludedPaths,
367-
}) => {
368-
const response = await fetch(`${middlewareI18nExcludedPaths.url}/html`)
380+
// those tests hit paths that don't end with `/json` while still satisfying middleware matcher
381+
// so middleware should pass them through to origin
382+
test.describe('Middleware passthrough', () => {
383+
test('should match on non-localized not excluded page path', async ({
384+
edgeOrNodeMiddlewareI18nExcludedPaths,
385+
}) => {
386+
const response = await fetch(`${edgeOrNodeMiddlewareI18nExcludedPaths.url}/html`)
369387

370-
expect(response.headers.get('x-test-used-middleware')).toBe('true')
371-
expect(response.status).toBe(200)
372-
expect(response.headers.get('content-type')).toMatch(/text\/html/)
388+
expect(response.headers.get('x-test-used-middleware')).toBe('true')
389+
expect(response.headers.get('x-runtime')).toEqual(expectedRuntime)
390+
expect(response.status).toBe(200)
391+
expect(response.headers.get('content-type')).toMatch(/text\/html/)
373392

374-
const html = await response.text()
375-
const { locale, params } = extractDataFromHtml(html)
393+
const html = await response.text()
394+
const { locale, params } = extractDataFromHtml(html)
376395

377-
expect(params).toMatchObject({ catchall: ['html'] })
378-
expect(locale).toBe(DEFAULT_LOCALE)
379-
})
396+
expect(params).toMatchObject({ catchall: ['html'] })
397+
expect(locale).toBe(DEFAULT_LOCALE)
398+
})
380399

381-
test('should match on localized not excluded page path', async ({
382-
middlewareI18nExcludedPaths,
383-
}) => {
384-
const response = await fetch(`${middlewareI18nExcludedPaths.url}/fr/html`)
400+
test('should match on localized not excluded page path', async ({
401+
edgeOrNodeMiddlewareI18nExcludedPaths,
402+
}) => {
403+
const response = await fetch(`${edgeOrNodeMiddlewareI18nExcludedPaths.url}/fr/html`)
385404

386-
expect(response.headers.get('x-test-used-middleware')).toBe('true')
387-
expect(response.status).toBe(200)
388-
expect(response.headers.get('content-type')).toMatch(/text\/html/)
405+
expect(response.headers.get('x-test-used-middleware')).toBe('true')
406+
expect(response.headers.get('x-runtime')).toEqual(expectedRuntime)
407+
expect(response.status).toBe(200)
408+
expect(response.headers.get('content-type')).toMatch(/text\/html/)
389409

390-
const html = await response.text()
391-
const { locale, params } = extractDataFromHtml(html)
410+
const html = await response.text()
411+
const { locale, params } = extractDataFromHtml(html)
392412

393-
expect(params).toMatchObject({ catchall: ['html'] })
394-
expect(locale).toBe('fr')
395-
})
413+
expect(params).toMatchObject({ catchall: ['html'] })
414+
expect(locale).toBe('fr')
396415
})
416+
})
397417

398-
// those tests hit paths that don't satisfy middleware matcher, so should go directly to origin
399-
// without going through middleware
400-
test.describe('Middleware skipping (paths not satisfying middleware matcher)', () => {
401-
test('should NOT match on non-localized excluded API path', async ({
402-
middlewareI18nExcludedPaths,
403-
}) => {
404-
const response = await fetch(`${middlewareI18nExcludedPaths.url}/api/html`)
418+
// those tests hit paths that don't satisfy middleware matcher, so should go directly to origin
419+
// without going through middleware
420+
test.describe('Middleware skipping (paths not satisfying middleware matcher)', () => {
421+
test('should NOT match on non-localized excluded API path', async ({
422+
edgeOrNodeMiddlewareI18nExcludedPaths,
423+
}) => {
424+
const response = await fetch(`${edgeOrNodeMiddlewareI18nExcludedPaths.url}/api/html`)
405425

406-
expect(response.headers.get('x-test-used-middleware')).not.toBe('true')
407-
expect(response.status).toBe(200)
426+
expect(response.headers.get('x-test-used-middleware')).not.toBe('true')
427+
expect(response.headers.get('x-runtime')).toEqual(expectedRuntime)
428+
expect(response.status).toBe(200)
408429

409-
const { params } = await response.json()
430+
const { params } = await response.json()
410431

411-
expect(params).toMatchObject({ catchall: ['html'] })
412-
})
432+
expect(params).toMatchObject({ catchall: ['html'] })
433+
})
413434

414-
test('should NOT match on non-localized excluded page path', async ({
415-
middlewareI18nExcludedPaths,
416-
}) => {
417-
const response = await fetch(`${middlewareI18nExcludedPaths.url}/excluded`)
435+
test('should NOT match on non-localized excluded page path', async ({
436+
edgeOrNodeMiddlewareI18nExcludedPaths,
437+
}) => {
438+
const response = await fetch(`${edgeOrNodeMiddlewareI18nExcludedPaths.url}/excluded`)
418439

419-
expect(response.headers.get('x-test-used-middleware')).not.toBe('true')
420-
expect(response.status).toBe(200)
421-
expect(response.headers.get('content-type')).toMatch(/text\/html/)
440+
expect(response.headers.get('x-test-used-middleware')).not.toBe('true')
441+
expect(response.headers.get('x-runtime')).toEqual(expectedRuntime)
442+
expect(response.status).toBe(200)
443+
expect(response.headers.get('content-type')).toMatch(/text\/html/)
422444

423-
const html = await response.text()
424-
const { locale, params } = extractDataFromHtml(html)
445+
const html = await response.text()
446+
const { locale, params } = extractDataFromHtml(html)
425447

426-
expect(params).toMatchObject({ catchall: ['excluded'] })
427-
expect(locale).toBe(DEFAULT_LOCALE)
428-
})
448+
expect(params).toMatchObject({ catchall: ['excluded'] })
449+
expect(locale).toBe(DEFAULT_LOCALE)
450+
})
429451

430-
test('should NOT match on localized excluded page path', async ({
431-
middlewareI18nExcludedPaths,
432-
}) => {
433-
const response = await fetch(`${middlewareI18nExcludedPaths.url}/fr/excluded`)
452+
test('should NOT match on localized excluded page path', async ({
453+
edgeOrNodeMiddlewareI18nExcludedPaths,
454+
}) => {
455+
const response = await fetch(`${edgeOrNodeMiddlewareI18nExcludedPaths.url}/fr/excluded`)
434456

435-
expect(response.headers.get('x-test-used-middleware')).not.toBe('true')
436-
expect(response.status).toBe(200)
437-
expect(response.headers.get('content-type')).toMatch(/text\/html/)
457+
expect(response.headers.get('x-test-used-middleware')).not.toBe('true')
458+
expect(response.headers.get('x-runtime')).toEqual(expectedRuntime)
459+
expect(response.status).toBe(200)
460+
expect(response.headers.get('content-type')).toMatch(/text\/html/)
438461

439-
const html = await response.text()
440-
const { locale, params } = extractDataFromHtml(html)
462+
const html = await response.text()
463+
const { locale, params } = extractDataFromHtml(html)
441464

442-
expect(params).toMatchObject({ catchall: ['excluded'] })
443-
expect(locale).toBe('fr')
444-
})
465+
expect(params).toMatchObject({ catchall: ['excluded'] })
466+
expect(locale).toBe('fr')
445467
})
446468
})
469+
})
447470

471+
if (expectedRuntime !== 'node') {
448472
test("requests with x-middleware-subrequest don't skip middleware (GHSA-f82v-jwr5-mffw)", async ({
449473
middlewareSubrequestVuln,
450474
}) => {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export { middleware } from './middleware-shared'
2+
3+
// matcher copied from example in https://nextjs.org/docs/pages/building-your-application/routing/middleware#matcher
4+
// with `excluded` segment added to exclusion
5+
export const config = {
6+
matcher: [
7+
/*
8+
* Match all request paths except for the ones starting with:
9+
* - api (API routes)
10+
* - excluded (for testing localized routes and not just API routes)
11+
* - _next/static (static files)
12+
* - _next/image (image optimization files)
13+
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
14+
*/
15+
'/((?!api|excluded|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
16+
],
17+
runtime: 'nodejs',
18+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
// report Next.js Middleware Runtime (not the execution runtime, but target runtime)
19+
// @ts-expect-error EdgeRuntime global not declared
20+
response.headers.append('x-runtime', typeof EdgeRuntime !== 'undefined' ? EdgeRuntime : 'node')
21+
22+
return response
23+
}

tests/fixtures/middleware-i18n-excluded-paths/middleware.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,4 @@
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-
}
1+
export { middleware } from './middleware-shared'
212

223
// matcher copied from example in https://nextjs.org/docs/pages/building-your-application/routing/middleware#matcher
234
// with `excluded` segment added to exclusion
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
module.exports = {
22
output: 'standalone',
3+
distDir: process.env.NEXT_DIST_DIR ?? '.next',
34
eslint: {
45
ignoreDuringBuilds: true,
56
},
67
i18n: {
78
locales: ['en', 'fr'],
89
defaultLocale: 'en',
910
},
11+
experimental: {
12+
nodeMiddleware: true,
13+
},
1014
outputFileTracingRoot: __dirname,
1115
}

0 commit comments

Comments
 (0)