diff --git a/frontend-modern/browser-tests/reverse-proxy-help.cjs b/frontend-modern/browser-tests/reverse-proxy-help.cjs new file mode 100644 index 0000000000..cfd6292286 --- /dev/null +++ b/frontend-modern/browser-tests/reverse-proxy-help.cjs @@ -0,0 +1,120 @@ +// Real shipped Docs/router/CSS, with no backend, proxy or real credential. +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { createHash } = require('node:crypto'); +const { chromium, webkit } = require('playwright'); + +(async () => { + const workspace = process.cwd(); + const root = path.join(workspace, 'frontend-modern'); + const output = path.join(workspace, 'tmp', 'reverse-proxy-help'); + fs.mkdirSync(output, { recursive: true }); + const binding = JSON.parse(fs.readFileSync(path.join(output, 'binding.json'), 'utf8')); + assert.match(binding.sourceSha, /^[0-9a-f]{40}$/); + for (const [file, digest] of Object.entries(binding.contentSha256)) { + assert.equal( + createHash('sha256').update(fs.readFileSync(path.join(workspace, file))).digest('hex'), + digest, + file, + ); + } + process.chdir(root); + const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js')); + const server = await createServer({ + root, + configFile: path.join(root, 'vite.config.ts'), + cacheDir: path.join(root, 'node_modules', '.vite-reverse-proxy-help'), + server: { host: '127.0.0.1', port: 5268, strictPort: true }, + optimizeDeps: { noDiscovery: true, include: [] }, + }); + const observations = []; + let browser; + const inViewport = (id) => { + const heading = document.getElementById(id); + return heading && heading.getBoundingClientRect().top >= 0 && + heading.getBoundingClientRect().top < innerHeight; + }; + try { + await server.listen(); + for (const [engine, width, height, tone] of [ + ['chromium', 1280, 1000, 'light'], ['webkit', 390, 844, 'dark'], + ]) { + browser = await { chromium, webkit }[engine].launch(engine === 'chromium' + ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] } + : { headless: true }); + const page = await browser.newPage({ + viewport: { width, height }, + ...(engine === 'webkit' ? { isMobile: true, hasTouch: true } : {}), + }); + const errors = []; + page.on('pageerror', (error) => errors.push(error.message)); + page.on('console', (message) => { + if (message.type() === 'error') errors.push(message.text()); + }); + await page.goto('http://127.0.0.1:5268/browser-tests/docs-fragment-navigation.html'); + if (tone === 'dark') await page.evaluate(() => document.documentElement.classList.add('dark')); + await page.getByRole('link', { name: '← All documentation' }).click(); + const entry = page.locator('a[data-doc-link][href="/docs/REVERSE_PROXY"]').first(); + await entry.focus(); + await page.keyboard.press('Enter'); + const heading = page.locator('#before-configuring-the-proxy'); + await heading.waitFor(); + await heading.scrollIntoViewIfNeeded(); + let text = await page.locator('article').innerText(); + for (const phrase of [ + 'configured immediate peer', 'PULSE_TRUSTED_PROXY_CIDRS=127.0.0.1/32', + 'Wildcard ranges', 'rejected at startup', 'not an authentication bypass', + 'Adding the header alone is not sufficient', + ]) assert.ok(text.includes(phrase), phrase); + assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)); + await page.screenshot({ path: path.join(output, `${engine}-trust.png`) }); + const nginx = page.locator('pre').filter({ hasText: 'proxy_set_header X-Forwarded-For $remote_addr;' }); + await nginx.scrollIntoViewIfNeeded(); + const copied = await nginx.innerText(); + assert.ok(copied.includes('proxy_set_header X-Forwarded-Proto $scheme;')); + assert.ok(copied.includes('proxy_set_header Forwarded "";')); + assert.ok(!copied.includes('$proxy_add_x_forwarded_for')); + assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)); + await page.screenshot({ path: path.join(output, `${engine}-nginx.png`) }); + const configuration = page.getByRole('link', { name: 'configuration overrides', exact: true }); + await configuration.focus(); + await page.keyboard.press('Enter'); + await page.waitForFunction(inViewport, 'common-overrides-environment-variables'); + text = await page.locator('article').innerText(); + assert.ok(text.includes('forwarded client IP, scheme, host and port')); + const back = page.getByRole('link', { name: 'Reverse Proxy', exact: true }); + await back.focus(); + await page.keyboard.press('Enter'); + await page.waitForFunction(inViewport, 'before-configuring-the-proxy'); + assert.ok(page.url().endsWith('/docs/REVERSE_PROXY#before-configuring-the-proxy')); + const authentication = page.getByRole('link', { name: 'proxy authentication guide', exact: true }); + await authentication.focus(); + await page.keyboard.press('Enter'); + await page.waitForFunction(inViewport, '-header-trust-boundary'); + assert.ok(page.url().endsWith('/docs/PROXY_AUTH#-header-trust-boundary')); + assert.ok((await page.locator('article').innerText()).includes('never append to them')); + assert.deepEqual(errors, []); + observations.push({ + engine, width, height, tone, browserVersion: browser.version(), + trustWarningRendered: true, safeNginxBlockRendered: true, + keyboardConfigurationRoundTrip: true, keyboardAuthenticationBoundary: true, + noPageOverflow: true, errors, + }); + await browser.close(); + browser = undefined; + } + const result = { + result: 'passed', playwrightVersion: require('playwright/package.json').version, + sourceSha: binding.sourceSha, verifiedAt: new Date().toISOString(), + contentSha256: binding.contentSha256, dependencySnapshot: binding.dependencySnapshot, + scope: 'Production Docs rendering/navigation; not native proxy configuration, TLS or SSO acceptance', + observations, + }; + fs.writeFileSync(path.join(output, 'result.json'), JSON.stringify(result, null, 2) + '\n'); + console.log(JSON.stringify(result)); + } finally { + if (browser) await browser.close(); + await server.close(); + } +})().catch((error) => { console.error(error); process.exitCode = 1; });