From 30170509f7f2ed39ef7f797b1b1f3d7b1f7a4f83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Thu, 30 Jul 2026 17:27:22 -0300 Subject: [PATCH] fix(e2e): WebAuthn on prod build, saved-filters navigation, Vercel scripts CI e2e serves a production build on localhost; Next inlines NODE_ENV so WebAuthn RP resolution now treats localhost requests as test mode. Saved filters avoids a racing bare goto. Analytics/SpeedInsights render only on Vercel. --- src/app/(frontend)/layout.tsx | 8 ++++++-- .../webauthn/campaignWebAuthnConfig.ts | 17 +++++++++++++++-- tests/e2e/campaignSavedFilters.e2e.spec.ts | 9 ++++++--- tests/unit/campaignWebAuthnConfig.unit.spec.ts | 9 +++++++++ 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/app/(frontend)/layout.tsx b/src/app/(frontend)/layout.tsx index aede5250..c16c2d32 100644 --- a/src/app/(frontend)/layout.tsx +++ b/src/app/(frontend)/layout.tsx @@ -77,8 +77,12 @@ export default async function RootLayout(props: { children: React.ReactNode }) { > - - + {process.env.VERCEL === '1' ? ( + <> + + + + ) : null} {children} diff --git a/src/utilities/webauthn/campaignWebAuthnConfig.ts b/src/utilities/webauthn/campaignWebAuthnConfig.ts index a5879f66..d1008573 100644 --- a/src/utilities/webauthn/campaignWebAuthnConfig.ts +++ b/src/utilities/webauthn/campaignWebAuthnConfig.ts @@ -17,6 +17,12 @@ export type CampaignWebAuthnRelyingParty = { const RP_NAME = 'Campanha Jorge Solla' +/** Matches `LOCAL_AUTHORITY_PATTERN` in `campaignInviteOrigin` — kept local on purpose. */ +const LOCAL_REQUEST_HOST = /^(localhost|127\.0\.0\.1|\[::1\])(?::[0-9]{1,5})?$/i + +const isLocalRequestHost = (host: string | undefined): boolean => + Boolean(host && LOCAL_REQUEST_HOST.test(host)) + /** * The authority the browser is actually talking to, which is what has to agree * with the relying party. Compared as `host` (name + port) and not as a full @@ -61,9 +67,17 @@ const requestHostOf = ({ export const getCampaignWebAuthnRelyingParty = ( input: CampaignInviteOriginInput = {}, ): CampaignWebAuthnRelyingParty | null => { + const requestHost = requestHostOf(input) + // `getCampaignInviteBaseURL` reads `process.env.NODE_ENV`, which Next inlines + // at build time as `"production"`. CI e2e serves that build on + // http://localhost:3000 (`pnpm start`), which would otherwise demand HTTPS + // DNS and hide biometrics. The request host is authoritative here. + const environment = + input.environment ?? (isLocalRequestHost(requestHost) ? ('test' as const) : undefined) + let baseURL: string try { - baseURL = getCampaignInviteBaseURL(input) + baseURL = getCampaignInviteBaseURL(environment ? { ...input, environment } : input) } catch { return null } @@ -74,7 +88,6 @@ export const getCampaignWebAuthnRelyingParty = ( // Absent host: a non-request context (a script, a test) that cannot be // checked. The ceremonies always pass the headers. - const requestHost = requestHostOf(input) if (requestHost && requestHost !== url.host.toLowerCase()) return null return { rpID, rpName: RP_NAME, origin: url.origin } diff --git a/tests/e2e/campaignSavedFilters.e2e.spec.ts b/tests/e2e/campaignSavedFilters.e2e.spec.ts index 5c34fd90..dfb42227 100644 --- a/tests/e2e/campaignSavedFilters.e2e.spec.ts +++ b/tests/e2e/campaignSavedFilters.e2e.spec.ts @@ -55,11 +55,14 @@ test.describe('Filtros salvos de Municípios', () => { await expect(shortcut).toHaveAttribute('aria-current', 'page') // A page is a position inside the recorte, not part of it. - await page.goto(`${SAVED_RECORTE}&page=2`) + await page.goto(`${SAVED_RECORTE}&page=2`, NAVIGATION) await expect(shortcut).toHaveAttribute('aria-current', 'page') - // Another recorte is not this one, and the shortcut brings it back. - await page.goto('/campanha/municipios') + // Another recorte is not this one, and the shortcut brings it back. Use the + // sidebar link — a bare `goto` here races an in-flight RSC navigation and + // flakes as net::ERR_ABORTED under prod load (measured in CI 2026-07-30). + await page.getByRole('link', { name: 'Municípios', exact: true }).click() + await expect(page).toHaveURL('/campanha/municipios', NAVIGATION) await expect(shortcut).not.toHaveAttribute('aria-current', 'page') await shortcut.click() await expect(page).toHaveURL(SAVED_RECORTE, NAVIGATION) diff --git a/tests/unit/campaignWebAuthnConfig.unit.spec.ts b/tests/unit/campaignWebAuthnConfig.unit.spec.ts index 0cb57115..d6b53b97 100644 --- a/tests/unit/campaignWebAuthnConfig.unit.spec.ts +++ b/tests/unit/campaignWebAuthnConfig.unit.spec.ts @@ -46,6 +46,15 @@ describe('getCampaignWebAuthnRelyingParty', () => { ).toMatchObject({ rpID: 'localhost', origin: 'http://localhost:3100' }) }) + it('follows localhost when a production build is served there (CI e2e)', () => { + expect( + getCampaignWebAuthnRelyingParty({ + configuredURL: 'https://pt.jorgesolla.com.br', + forwardedHost: 'localhost:3000', + }), + ).toMatchObject({ rpID: 'localhost', origin: 'http://localhost:3000' }) + }) + it('refuses production without a configured site URL', () => { expect( getCampaignWebAuthnRelyingParty({ environment: 'production', configuredURL: undefined }),