mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
fix(web): render public pages under nonce CSP
This commit is contained in:
1 parent
aaa51fb494
commit
1682c3b54d
3 files changed
+52
-42
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { expect, test } from "./setup/test";
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* v1.4.27 MB6 — public surfaces.
|
||||
@@ -8,10 +9,9 @@ import { expect, test } from "./setup/test";
|
||||
* 1. `/about` — added to `PUBLIC_PATHS` in `src/proxy.ts` for the
|
||||
* GeoLite2 CC BY-SA 4.0 attribution. A logged-out visitor must
|
||||
* load the page (HTTP 200) and see the attribution copy.
|
||||
* 2. `/this-route-does-not-exist` — Next.js renders
|
||||
* `src/app/not-found.tsx`, a branded 404 page with the logo, a
|
||||
* headline, and a "Back to dashboard" link. The proxy must NOT
|
||||
* bounce the missing route to `/auth/login`.
|
||||
* 2. `/this-route-does-not-exist` — the proxy keeps an anonymous visitor
|
||||
* behind the login boundary, while an authenticated visitor reaches
|
||||
* Next.js's branded `src/app/not-found.tsx`.
|
||||
* 3. `/privacy` — the page mounts a collapsible `<details>` TOC
|
||||
* (`data-slot="privacy-toc"`); clicking the summary opens it
|
||||
* and the anchor links inside navigate to the right section.
|
||||
@@ -24,11 +24,7 @@ test.describe("v1.4.27 — public pages", () => {
|
||||
// Force no storage state so we run as an anonymous visitor.
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
// v1.4.28 backlog: selectors below describe the intent (about page
|
||||
// heading, GeoLite2 attribution copy, MaxMind reference, CC BY-SA
|
||||
// text) but do not yet match the actual rendered DOM. Re-implement
|
||||
// against the live page once the surface is verified.
|
||||
test.fixme("/about returns 200 and renders the GeoLite2 attribution", async ({
|
||||
test("/about returns 200 and renders the GeoLite2 attribution", async ({
|
||||
page,
|
||||
}) => {
|
||||
const response = await page.goto("/about", {
|
||||
@@ -43,42 +39,26 @@ test.describe("v1.4.27 — public pages", () => {
|
||||
).toBeVisible();
|
||||
|
||||
// GeoLite2 attribution is the load-bearing reason this page exists.
|
||||
await expect(page.getByText(/GeoLite2/)).toBeVisible();
|
||||
await expect(page.getByText(/MaxMind/)).toBeVisible();
|
||||
await expect(page.getByText(/GeoLite2/).first()).toBeVisible();
|
||||
await expect(page.getByText(/MaxMind/).first()).toBeVisible();
|
||||
await expect(page.getByText(/Attribution-ShareAlike 4\.0/i)).toBeVisible();
|
||||
});
|
||||
|
||||
// v1.4.28 backlog: Next.js's `not-found.tsx` may return 200 or 404
|
||||
// depending on rendering pipeline; the branded body selectors below
|
||||
// need a sanity pass against the actual page output. Re-enable once
|
||||
// the contract is verified.
|
||||
test.fixme("/this-route-does-not-exist renders the branded 404 page", async ({
|
||||
test("/this-route-does-not-exist keeps an anonymous visitor behind login", async ({
|
||||
page,
|
||||
}) => {
|
||||
const response = await page.goto("/this-route-does-not-exist", {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
// Next.js returns 404 for `not-found.tsx` — the page must render
|
||||
// its own body, not redirect to `/auth/login`.
|
||||
expect(response?.status()).toBe(404);
|
||||
expect(page.url()).not.toMatch(/\/auth\/login/);
|
||||
|
||||
// Branded splash: small "404" eyebrow + headline + back link.
|
||||
await expect(page.getByText(/^404$/i)).toBeVisible();
|
||||
// `page.goto` follows the proxy's 307 and returns the final login response.
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page).toHaveURL(/\/auth\/login(?:\?|$)/);
|
||||
await expect(
|
||||
page.getByRole("heading", { name: /page not found/i }),
|
||||
page.getByRole("button", { name: /sign in with passkey/i }),
|
||||
).toBeVisible();
|
||||
|
||||
const back = page.getByRole("link", { name: /back to dashboard/i });
|
||||
await expect(back).toBeVisible();
|
||||
await expect(back).toHaveAttribute("href", "/");
|
||||
});
|
||||
|
||||
// v1.4.28 backlog: the privacy TOC `<details>` slot selector
|
||||
// does not yet match what MB6 actually shipped. Re-implement once
|
||||
// the data-slot attribute and anchor targets are verified against
|
||||
// the live page.
|
||||
test.fixme("/privacy renders the TOC and the anchor links jump to sections", async ({
|
||||
test("/privacy renders the TOC and the anchor links jump to sections", async ({
|
||||
page,
|
||||
}) => {
|
||||
const response = await page.goto("/privacy", {
|
||||
@@ -121,3 +101,30 @@ test.describe("v1.4.27 — public pages", () => {
|
||||
expect(introTop).toBeLessThan(800);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("v1.4.27 — authenticated not-found page", () => {
|
||||
test.use({ storageState: STORAGE_STATE_PATH });
|
||||
|
||||
test("/this-route-does-not-exist renders the branded 404 page", async ({
|
||||
page,
|
||||
}) => {
|
||||
const response = await page.goto("/this-route-does-not-exist", {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
expect(response?.status()).toBe(404);
|
||||
expect(page.url()).not.toMatch(/\/auth\/login/);
|
||||
|
||||
await expect(page.getByText(/^404$/i)).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", {
|
||||
name: /this page doesn't exist|diese seite existiert nicht/i,
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
const back = page.getByRole("link", {
|
||||
name: /back to dashboard|zurück zum dashboard/i,
|
||||
});
|
||||
await expect(back).toBeVisible();
|
||||
await expect(back).toHaveAttribute("href", "/");
|
||||
});
|
||||
});
|
||||
@@ -28,8 +28,12 @@ import Link from "next/link";
|
||||
* `#credits` still clear the sticky header on iPhones with a notch.
|
||||
*/
|
||||
|
||||
export const dynamic = "force-static";
|
||||
export const revalidate = false;
|
||||
// The edge CSP carries a fresh nonce on every response. Rendering this route
|
||||
// statically would freeze Next's inline streaming scripts without that
|
||||
// request nonce, so the browser would reject them and leave only the root
|
||||
// Suspense fallback visible. Keep the public page request-rendered instead of
|
||||
// weakening the CSP with `unsafe-inline`.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const LAST_UPDATED = "2026-05-15";
|
||||
|
||||
|
||||
@@ -27,15 +27,14 @@ import Link from "next/link";
|
||||
* `<AuthShell>` PUBLIC_PATHS so an unauthenticated visitor (or App-Store
|
||||
* reviewer) lands directly on the content without an auth redirect.
|
||||
*
|
||||
* Static-rendered (`force-static`, no `revalidate`). The bilingual body
|
||||
* is inlined verbatim instead of routed through the i18n provider so the
|
||||
* page works without a client-side context — a legal document should
|
||||
* never depend on JavaScript to display its non-default-locale text to a
|
||||
* reviewer.
|
||||
* The bilingual body is inlined verbatim instead of routed through the i18n
|
||||
* provider, so the legal text itself never depends on client-side locale
|
||||
* state. The route is request-rendered because the edge CSP carries a fresh
|
||||
* nonce: a static document would freeze Next's inline streaming scripts
|
||||
* without that request nonce and strand visitors on the Suspense fallback.
|
||||
*/
|
||||
|
||||
export const dynamic = "force-static";
|
||||
export const revalidate = false;
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const POLICY_VERSION = "1.4.40";
|
||||
const LAST_UPDATED = "2026-05-18";
|
||||
|
||||
Reference in new issue
Block a user