trails/e2e/demo-bot.test.ts
Ullrich Schäfer ff09775091 Fix e2e fallout from default-private profile_visibility
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) <noreply@anthropic.com>
2026-04-25 23:46:56 +02:00

124 lines
5.6 KiB
TypeScript

import { test, expect } from "./fixtures/test";
import postgres from "postgres";
import { randomUUID } from "node:crypto";
// Demo-bot UX: a browser-side verification that a pre-seeded `bruno`
// profile renders the demo badge and surfaces his public content to an
// anonymous visitor. We seed directly via SQL rather than running the
// pg-boss handler, because the handler depends on BRouter and on the
// demo flag being live in the server process. The point of this test
// is the *public surface*, not the generation path (that's covered in
// app/lib/demo-bot.integration.test.ts).
const CONN = process.env.DATABASE_URL ?? "postgres://trails:trails@localhost:5432/trails";
async function seedBrunoWithSyntheticRoute(): Promise<{ username: string; routeName: string }> {
const sql = postgres(CONN, { max: 1 });
const username = `bruno-e2e-${Date.now()}`;
const routeName = `Bruno's ${Date.now()} walk`;
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, 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)
VALUES (${routeId}, ${userId}, ${routeName}, 'Sniffed three bushes.', 'public', true, 3200, now(), now())
`;
} finally {
await sql.end();
}
return { username, routeName };
}
async function cleanupBruno(username: string) {
const sql = postgres(CONN, { max: 1 });
try {
const [row] = await sql<{ id: string }[]>`SELECT id FROM journal.users WHERE username = ${username}`;
if (!row) return;
await sql`DELETE FROM journal.activities WHERE owner_id = ${row.id}`;
await sql`DELETE FROM journal.routes WHERE owner_id = ${row.id}`;
await sql`DELETE FROM journal.users WHERE id = ${row.id}`;
} finally {
await sql.end();
}
}
test.describe.configure({ mode: "serial" });
test.describe("Demo-bot UX", () => {
test("seeded bruno profile shows demo badge + public route to anon visitor", async ({ browser }) => {
const { username, routeName } = await seedBrunoWithSyntheticRoute();
try {
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: /Bruno/ })).toBeVisible();
// The demo badge is only rendered when username === 'bruno' — our
// seeded username includes a timestamp suffix, so the badge
// shouldn't appear. This is an intentional guard: only the *real*
// bruno gets the badge. The rest of the page still works.
await expect(anon.getByText("🐕 Demo account")).toHaveCount(0);
// Public synthetic route is listed on the profile.
await expect(anon.getByRole("link", { name: new RegExp(routeName) })).toBeVisible();
await anonCtx.close();
} finally {
await cleanupBruno(username);
}
});
test("profile rendered at /users/bruno shows the demo badge when username is exactly 'bruno'", async ({ browser }) => {
const sql = postgres(CONN, { max: 1 });
const userId = randomUUID();
const routeId = randomUUID();
// 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, 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)
VALUES (${routeId}, ${effectiveUserId}, 'Grunewald patrol', 'Sniffed three bushes.', 'public', true, 3200, now(), now())
`;
await sql.end();
try {
const anonCtx = await browser.newContext();
const anon = await anonCtx.newPage();
const resp = await anon.goto(`/users/bruno`);
expect(resp?.status()).toBe(200);
await expect(anon.getByText(/Demo account|Demo-Konto/)).toBeVisible();
await expect(anon.getByRole("link", { name: /Grunewald patrol/ })).toBeVisible();
await anonCtx.close();
} finally {
const cleanup = postgres(CONN, { max: 1 });
try {
await cleanup`DELETE FROM journal.routes WHERE id = ${routeId}`;
if (weCreatedIt) {
await cleanup`DELETE FROM journal.users WHERE id = ${userId}`;
}
} finally {
await cleanup.end();
}
}
});
});