trails/e2e/public-content.test.ts
Ullrich Schäfer f94020a93a
Block unmocked external requests in E2E tests
Surface net for the class of bug we hit in #282 — a POI test silently
relied on a live Overpass server for months because its `page.route`
mock was targeting a URL the browser no longer hit directly. CI stayed
green on coincidence (real OSM data happened to satisfy the "any
marker renders" assertion) until upstream behaviour drifted.

Add a shared Playwright fixture (e2e/fixtures/test.ts) that installs a
catch-all `page.route("**", ...)` before each test. Requests to
localhost + known tile CDNs pass through via `route.continue()`;
anything else is aborted and accumulated. At test teardown, a non-empty
blocked list throws, failing the test with the offending URLs and a
pointer to the fixture.

All seven e2e *.test.ts files updated to import test/expect (+ types)
from ./fixtures/test.

Full suite passes with this fixture in place (50/50 at --workers=2;
catch-all never fires, meaning no test currently relies on an
unallowlisted external service).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:42:29 +02:00

179 lines
7.2 KiB
TypeScript

import { test, expect, type CDPSession, type Page } from "./fixtures/test";
// Reuses the virtual-authenticator + register helpers from auth.test.ts.
// Inlined rather than factored out to keep this file independently runnable.
async function setupVirtualAuthenticator(cdp: CDPSession) {
await cdp.send("WebAuthn.enable");
const { authenticatorId } = await cdp.send("WebAuthn.addVirtualAuthenticator", {
options: {
protocol: "ctap2",
transport: "internal",
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
},
});
return authenticatorId;
}
async function registerUser(page: Page, email: string, username: string) {
await page.goto("/auth/register");
await expect(page.getByRole("heading", { name: "Register" })).toBeVisible();
await page.getByLabel("Email").fill(email);
await page.getByLabel("Username").fill(username);
await page.getByRole("checkbox").check();
await page.getByRole("button", { name: /Register with Passkey/ }).click();
await expect(page).toHaveURL("/", { timeout: 10000 });
}
async function createRoute(page: Page, name: string): Promise<string> {
await page.goto("/routes/new");
await page.getByLabel("Name").fill(name);
await page.getByRole("button", { name: "Create Route" }).click();
await page.waitForURL(/\/routes\/[0-9a-f-]+$/, { timeout: 10000 });
const url = page.url();
const id = url.split("/").pop()!;
return id;
}
async function setRouteVisibility(
page: Page,
id: string,
visibility: "private" | "unlisted" | "public",
) {
await page.goto(`/routes/${id}/edit`);
await page.getByLabel("Visibility").selectOption(visibility);
await page.getByRole("button", { name: "Save Changes" }).click();
await page.waitForURL(new RegExp(`/routes/${id}$`), { timeout: 10000 });
}
// Registration + WebAuthn virtual authenticator can race under parallel
// Playwright workers on a shared local Postgres; run this file serially.
test.describe.configure({ mode: "serial" });
test.describe("Public content visibility", () => {
test("private route is 404 for a logged-out visitor", async ({ page, browser }) => {
const cdp = await page.context().newCDPSession(page);
await setupVirtualAuthenticator(cdp);
const email = `pcv-priv-${Date.now()}@example.com`;
const username = `pcvpriv${Date.now()}`;
await registerUser(page, email, username);
const id = await createRoute(page, "My private draft");
// Open a second browser context (no cookies) and try the same URL.
const anonCtx = await browser.newContext();
const anon = await anonCtx.newPage();
const resp = await anon.goto(`/routes/${id}`);
// Loader throws 404; the response status reflects that.
expect(resp?.status()).toBe(404);
await anonCtx.close();
});
test("public route is reachable by a logged-out visitor and emits OG tags", async ({ page, browser }) => {
const cdp = await page.context().newCDPSession(page);
await setupVirtualAuthenticator(cdp);
const email = `pcv-pub-${Date.now()}@example.com`;
const username = `pcvpub${Date.now()}`;
await registerUser(page, email, username);
const id = await createRoute(page, "Public ride");
await setRouteVisibility(page, id, "public");
const anonCtx = await browser.newContext();
const anon = await anonCtx.newPage();
const resp = await anon.goto(`/routes/${id}`);
expect(resp?.status()).toBe(200);
await expect(anon.getByRole("heading", { name: "Public ride" })).toBeVisible();
// OG tags are present in the document head on public pages.
const ogTitle = await anon.locator('meta[property="og:title"]').getAttribute("content");
expect(ogTitle).toContain("Public ride");
const ogType = await anon.locator('meta[property="og:type"]').getAttribute("content");
expect(ogType).toBe("article");
const ogSite = await anon.locator('meta[property="og:site_name"]').getAttribute("content");
expect(ogSite).toBe("trails.cool");
await anonCtx.close();
});
test("owner can still view their own private content", async ({ page }) => {
const cdp = await page.context().newCDPSession(page);
await setupVirtualAuthenticator(cdp);
const email = `pcv-owner-${Date.now()}@example.com`;
const username = `pcvowner${Date.now()}`;
await registerUser(page, email, username);
const id = await createRoute(page, "My only route");
// Same session reload: owner still sees it even though default is private.
await page.goto(`/routes/${id}`);
await expect(page.getByRole("heading", { name: "My only route" })).toBeVisible();
});
test("profile 404 when user has no public content", async ({ page, browser }) => {
const cdp = await page.context().newCDPSession(page);
await setupVirtualAuthenticator(cdp);
const email = `pcv-empty-${Date.now()}@example.com`;
const username = `pcvempty${Date.now()}`;
await registerUser(page, email, username);
await createRoute(page, "Private thoughts"); // left private
const anonCtx = await browser.newContext();
const anon = await anonCtx.newPage();
const resp = await anon.goto(`/users/${username}`);
expect(resp?.status()).toBe(404);
await anonCtx.close();
});
test("profile renders when user has at least one public route", async ({ page, browser }) => {
const cdp = await page.context().newCDPSession(page);
await setupVirtualAuthenticator(cdp);
const email = `pcv-prof-${Date.now()}@example.com`;
const username = `pcvprof${Date.now()}`;
await registerUser(page, email, username);
const id = await createRoute(page, "Bikepacking loop");
await setRouteVisibility(page, id, "public");
const anonCtx = await browser.newContext();
const anon = await anonCtx.newPage();
const resp = await anon.goto(`/users/${username}`);
expect(resp?.status()).toBe(200);
await expect(anon.getByRole("heading", { name: username })).toBeVisible();
await expect(anon.getByRole("link", { name: /Bikepacking loop/ })).toBeVisible();
await anonCtx.close();
});
test("unlisted route is reachable by direct URL but does not appear on profile", async ({ page, browser }) => {
const cdp = await page.context().newCDPSession(page);
await setupVirtualAuthenticator(cdp);
const email = `pcv-unl-${Date.now()}@example.com`;
const username = `pcvunl${Date.now()}`;
await registerUser(page, email, username);
// Two routes: one unlisted, one public — profile shows only the public one.
const publicId = await createRoute(page, "Public one");
await setRouteVisibility(page, publicId, "public");
const unlistedId = await createRoute(page, "Unlisted secret");
await setRouteVisibility(page, unlistedId, "unlisted");
const anonCtx = await browser.newContext();
const anon = await anonCtx.newPage();
// Direct URL to unlisted → 200
const direct = await anon.goto(`/routes/${unlistedId}`);
expect(direct?.status()).toBe(200);
await expect(anon.getByRole("heading", { name: "Unlisted secret" })).toBeVisible();
// Profile lists only the public route
await anon.goto(`/users/${username}`);
await expect(anon.getByRole("link", { name: /Public one/ })).toBeVisible();
await expect(anon.getByRole("link", { name: /Unlisted secret/ })).not.toBeVisible();
await anonCtx.close();
});
});