From ff097750917734bda06a195962c766482f86779c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sat, 25 Apr 2026 23:46:56 +0200 Subject: [PATCH] Fix e2e fallout from default-private profile_visibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three places assumed the old default-public model: 1. e2e/public-content.test.ts: "profile 404 when user has no public content" → updated to assert the new locked-account stub renders 200 with "This profile is private" and the private route doesn't appear. The two follow-on tests (public-route profile, unlisted route) now flip the owner to public via a setProfileVisibilityPublic helper before the anon-visit assertions, since new users default to private. 2. e2e/demo-bot.test.ts: bruno is seeded with profile_visibility = 'public' on insert (timestamped seed) and the existing-bruno path gets a follow-up UPDATE so the test's anon-visitor assertion (profile renders, public route is listed) holds regardless of which default he was first created under. 3. apps/journal/app/lib/demo-bot.server.ts ensureDemoUser: also pins profile_visibility = 'public' on insert. The demo persona is discoverable by design — that's the whole point. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/journal/app/lib/demo-bot.server.ts | 5 +++++ e2e/demo-bot.test.ts | 16 +++++++++++---- e2e/public-content.test.ts | 27 ++++++++++++++++++++++--- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/apps/journal/app/lib/demo-bot.server.ts b/apps/journal/app/lib/demo-bot.server.ts index f99f952..92e6a67 100644 --- a/apps/journal/app/lib/demo-bot.server.ts +++ b/apps/journal/app/lib/demo-bot.server.ts @@ -279,6 +279,11 @@ export async function ensureDemoUser( displayName: persona.displayName, bio: persona.bio, domain, + // The demo persona is meant to be discoverable to anyone — its + // entire purpose is to populate empty instances with public + // content. Force `public` even though new accounts default to + // `private` (locked-account model). + profileVisibility: "public", termsAcceptedAt: new Date(), termsVersion: TERMS_VERSION, }) diff --git a/e2e/demo-bot.test.ts b/e2e/demo-bot.test.ts index b93ba09..0e9c755 100644 --- a/e2e/demo-bot.test.ts +++ b/e2e/demo-bot.test.ts @@ -19,9 +19,12 @@ async function seedBrunoWithSyntheticRoute(): Promise<{ username: string; routeN const userId = randomUUID(); const routeId = randomUUID(); try { + // Demo bot needs profile_visibility='public' so anon visitors see + // his profile in full (the locked-account default would render a + // stub instead). await sql` - INSERT INTO journal.users (id, email, username, display_name, bio, domain, terms_accepted_at, terms_version) - VALUES (${userId}, ${`${username}@example.com`}, ${username}, 'Bruno', 'Professional park inspector.', 'localhost', now(), '2026-04-19') + INSERT INTO journal.users (id, email, username, display_name, bio, domain, profile_visibility, terms_accepted_at, terms_version) + VALUES (${userId}, ${`${username}@example.com`}, ${username}, 'Bruno', 'Professional park inspector.', 'localhost', 'public', now(), '2026-04-19') `; await sql` INSERT INTO journal.routes (id, owner_id, name, description, visibility, synthetic, distance, created_at, updated_at) @@ -79,13 +82,18 @@ test.describe("Demo-bot UX", () => { // Use ON CONFLICT so a pre-existing prod `bruno` user doesn't block // the test; we don't clean up a row we didn't create. const created = await sql` - INSERT INTO journal.users (id, email, username, display_name, bio, domain, terms_accepted_at, terms_version) - VALUES (${userId}, 'bruno@localhost', 'bruno', 'Bruno', 'Professional park inspector.', 'localhost', now(), '2026-04-19') + INSERT INTO journal.users (id, email, username, display_name, bio, domain, profile_visibility, terms_accepted_at, terms_version) + VALUES (${userId}, 'bruno@localhost', 'bruno', 'Bruno', 'Professional park inspector.', 'localhost', 'public', now(), '2026-04-19') ON CONFLICT (username) DO NOTHING RETURNING id `; const weCreatedIt = created.length > 0; const effectiveUserId = weCreatedIt ? userId : (await sql`SELECT id FROM journal.users WHERE username='bruno'`)[0]!.id as string; + // Make sure bruno is public regardless of who created him — the + // demo profile must render in full for anon visitors. If he was + // pre-existing with `private` (e.g. on a fresh DB after the + // default flipped), force him public for this test's purposes. + await sql`UPDATE journal.users SET profile_visibility = 'public' WHERE id = ${effectiveUserId}`; await sql` INSERT INTO journal.routes (id, owner_id, name, description, visibility, synthetic, distance, created_at, updated_at) diff --git a/e2e/public-content.test.ts b/e2e/public-content.test.ts index 338b940..50288b7 100644 --- a/e2e/public-content.test.ts +++ b/e2e/public-content.test.ts @@ -47,6 +47,16 @@ async function setRouteVisibility( await page.waitForURL(new RegExp(`/routes/${id}$`), { timeout: 10000 }); } +// New users default to profile_visibility='private' (locked-account +// model). Tests that need an anon visitor to see the profile in full +// have to flip the owner to public first. +async function setProfileVisibilityPublic(page: Page) { + await page.goto("/settings"); + await page.getByLabel("Public").check(); + await page.getByRole("button", { name: /^Save$/ }).first().click(); + await page.waitForLoadState("networkidle"); +} + // Registration + WebAuthn virtual authenticator can race under parallel // Playwright workers on a shared local Postgres; run this file serially. test.describe.configure({ mode: "serial" }); @@ -111,19 +121,28 @@ test.describe("Public content visibility", () => { await expect(page.getByRole("heading", { name: "My only route" })).toBeVisible(); }); - test("profile 404 when user has no public content", async ({ page, browser }) => { + test("profile renders private stub when user has no public content (locked model)", 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 + // New users default to profile_visibility='private', so an anonymous + // visitor sees the locked-account stub (200, not 404). Their private + // route never appears. + const id = 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); + expect(resp?.status()).toBe(200); + await expect(anon.getByText(/This profile is private/i)).toBeVisible(); + await expect(anon.getByText("Private thoughts")).not.toBeVisible(); + // Direct route URL still 404s for visitors because the route itself + // is private-visibility. + const direct = await anon.goto(`/routes/${id}`); + expect(direct?.status()).toBe(404); await anonCtx.close(); }); @@ -134,6 +153,7 @@ test.describe("Public content visibility", () => { const email = `pcv-prof-${Date.now()}@example.com`; const username = `pcvprof${Date.now()}`; await registerUser(page, email, username); + await setProfileVisibilityPublic(page); const id = await createRoute(page, "Bikepacking loop"); await setRouteVisibility(page, id, "public"); @@ -154,6 +174,7 @@ test.describe("Public content visibility", () => { const email = `pcv-unl-${Date.now()}@example.com`; const username = `pcvunl${Date.now()}`; await registerUser(page, email, username); + await setProfileVisibilityPublic(page); // Two routes: one unlisted, one public — profile shows only the public one. const publicId = await createRoute(page, "Public one");