diff --git a/test/e2e/app-dir/app/app/bootstrap/[[...*]]/ClientComponent.js b/test/e2e/app-dir/app/app/bootstrap/[[...*]]/ClientComponent.js new file mode 100644 index 0000000000000..6cbcf95b770f1 --- /dev/null +++ b/test/e2e/app-dir/app/app/bootstrap/[[...*]]/ClientComponent.js @@ -0,0 +1,11 @@ +'use client' + +import { useState, useEffect } from 'react' + +export function ClientComponent() { + const [val, setVal] = useState('initial') + useEffect(() => { + setVal('[[updated]]') + }, []) + return {val} +} diff --git a/test/e2e/app-dir/app/app/bootstrap/[[...*]]/page.js b/test/e2e/app-dir/app/app/bootstrap/[[...*]]/page.js new file mode 100644 index 0000000000000..4e53effb26f73 --- /dev/null +++ b/test/e2e/app-dir/app/app/bootstrap/[[...*]]/page.js @@ -0,0 +1,15 @@ +import { ClientComponent } from './ClientComponent' + +export default async function Page() { + return ( + <> +
+ This fixture is to assert where the bootstrap scripts and other required + scripts emit during SSR +
+
+ +
+ + ) +} diff --git a/test/e2e/app-dir/app/app/bootstrap/page.js b/test/e2e/app-dir/app/app/bootstrap/page.js deleted file mode 100644 index d83b335ed8698..0000000000000 --- a/test/e2e/app-dir/app/app/bootstrap/page.js +++ /dev/null @@ -1,8 +0,0 @@ -export default async function Page() { - return ( -
- This fixture is to assert where the bootstrap scripts and other required - scripts emit during SSR -
- ) -} diff --git a/test/e2e/app-dir/app/index.test.ts b/test/e2e/app-dir/app/index.test.ts index c0b21cbe03ab9..0c5bae1ab70bb 100644 --- a/test/e2e/app-dir/app/index.test.ts +++ b/test/e2e/app-dir/app/index.test.ts @@ -1373,8 +1373,9 @@ createNextDescribe( // Find all the script tags without src attributes. const elements = $('script[src]') - // Expect there to be at least 1 script tag without a src attribute. - expect(elements.length).toBeGreaterThan(0) + // Expect there to be at least 2 script tag with a src attribute. + // The main chunk and the webpack runtime. + expect(elements.length).toBeGreaterThan(1) // Expect all inline scripts to have the nonce value. elements.each((i, el) => { @@ -1899,6 +1900,39 @@ createNextDescribe( expect($('script[async]').length).toBeGreaterThan(1) expect($('body').find('script[async]').length).toBe(1) }) + + if (!isDev) { + it('should successfully bootstrap even when using CSP', async () => { + // This path has a nonce applied in middleware + const browser = await next.browser('/bootstrap/with-nonce') + const response = await next.fetch('/bootstrap/with-nonce') + // We expect this page to response with CSP headers requiring a nonce for scripts + expect(response.headers.get('content-security-policy')).toContain( + "script-src 'nonce" + ) + // We expect to find the updated text which demonstrates our app + // was able to bootstrap successfully (scripts run) + expect( + await browser.eval('document.getElementById("val").textContent') + ).toBe('[[updated]]') + }) + } else { + it('should fail to bootstrap when using CSP in Dev due to eval', async () => { + // This test is here to ensure that we don't accidentally turn CSP off + // for the prod version. + const browser = await next.browser('/bootstrap/with-nonce') + const response = await next.fetch('/bootstrap/with-nonce') + // We expect this page to response with CSP headers requiring a nonce for scripts + expect(response.headers.get('content-security-policy')).toContain( + "script-src 'nonce" + ) + // We expect our app to fail to bootstrap due to invalid eval use in Dev. + // We assert the html is in it's SSR'd state. + expect( + await browser.eval('document.getElementById("val").textContent') + ).toBe('initial') + }) + } }) } ) diff --git a/test/e2e/app-dir/app/middleware.js b/test/e2e/app-dir/app/middleware.js index efd1bf33146e5..2f7830a92a708 100644 --- a/test/e2e/app-dir/app/middleware.js +++ b/test/e2e/app-dir/app/middleware.js @@ -3,9 +3,9 @@ import { NextResponse } from 'next/server' /** * @param {import('next/server').NextRequest} request - * @returns {NextResponse | undefined} + * @returns {Promise} */ -export function middleware(request) { +export async function middleware(request) { if (request.nextUrl.pathname === '/searchparams-normalization-bug') { const headers = new Headers(request.headers) headers.set('test', request.nextUrl.searchParams.get('val') || '') @@ -25,6 +25,17 @@ export function middleware(request) { return NextResponse.rewrite(new URL('/dashboard', request.url)) } + // In dev this route will fail to bootstrap because webpack uses eval which is dissallowed by + // this policy. In production this route will work + if (request.nextUrl.pathname === '/bootstrap/with-nonce') { + const nonce = crypto.randomUUID() + return NextResponse.next({ + headers: { + 'Content-Security-Policy': `script-src 'nonce-${nonce}' 'strict-dynamic';`, + }, + }) + } + if (request.nextUrl.pathname.startsWith('/internal/test')) { const method = request.nextUrl.pathname.endsWith('rewrite') ? 'rewrite'