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>
124 lines
5.6 KiB
TypeScript
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();
|
|
}
|
|
}
|
|
});
|
|
});
|