diff --git a/test/e2e/batch10-anonpull.mjs b/test/e2e/batch10-anonpull.mjs new file mode 100644 index 0000000..42570c0 --- /dev/null +++ b/test/e2e/batch10-anonpull.mjs @@ -0,0 +1,120 @@ +// batch10-anonpull.mjs — the drive half of val/10-anonpull. +// +// Go tests cover NarrowToPullOnly and the auth matrix covers the API. What +// neither can see is the class of bug this batch is most likely to produce in +// the UI: a logged-out repo page that 500s, renders as a blank panel, or +// renders as a cheerful empty state that looks like "no tags" rather than +// "you may not see this". +// +// Logged-out checks run in an EPHEMERAL context, not the persistent profile in +// lib.mjs — that profile is signed in, and clearing its cookies would cost an +// interactive re-login for everything else. +// +// HOLD public=true → node test/e2e/batch10-anonpull.mjs +// HOLD public=false → node test/e2e/batch10-anonpull.mjs (runs the denied half) +import { chromium } from '@playwright/test'; +import { APPVIEW } from './lib.mjs'; + +const HOLD = process.env.ATCR_HOLD_URL ?? 'http://127.0.0.1:8080'; +const HANDLE = process.env.ATCR_E2E_HANDLE ?? 'evan.jarrett.net'; +const REPO = process.env.ATCR_E2E_REPO ?? 'valtest'; +const HEADLESS = process.env.ATCR_E2E_HEADLESS === '1'; + +const captain = await fetch( + `${HOLD}/xrpc/com.atproto.repo.listRecords?repo=did%3Aweb%3Alocalhost%253A8080&collection=io.atcr.hold.captain`, +).then((r) => r.json()); +const isPublic = captain.records[0].value.public === true; +console.log(`hold captain.public = ${isPublic}\n`); + +// A persistent context on a THROWAWAY profile dir: same launch path the other +// e2e scripts use (a plain chromium.launch() does not come up here), but a +// clean cookie jar, so this is genuinely logged out without disturbing the +// signed-in profile lib.mjs uses. +const ANON_PROFILE = process.env.ATCR_E2E_ANON_PROFILE ?? '/tmp/atcr-e2e-anon-profile'; +const ctx = await chromium.launchPersistentContext(ANON_PROFILE, { + headless: HEADLESS, + viewport: null, + args: ['--window-size=1400,1000'], +}); +const page = ctx.pages()[0] ?? (await ctx.newPage()); + +let pass = 0, fail = 0; +const check = (label, ok, detail = '') => { + console.log(` ${ok ? 'ok ' : 'FAIL'} ${label}${detail ? ' — ' + detail : ''}`); + ok ? pass++ : fail++; +}; + +// Anything 5xx is a failure regardless of what the page then renders: a 500 +// dressed as an empty state is exactly what this script exists to catch. +const statuses = []; +page.on('response', (r) => { + const u = new URL(r.url()); + if (u.origin === new URL(APPVIEW).origin) statuses.push([r.status(), u.pathname]); +}); + +// The repo page is /r/{handle}/* — NOT /{handle}/{repo}, which is a 404 +// ("Lost at Sea") and will happily look like a denial if you don't check. +const url = `${APPVIEW}/r/${HANDLE}/${REPO}`; +console.log(`==> logged out, GET ${url}`); +const resp = await page.goto(url, { waitUntil: 'networkidle' }); +const body = (await page.locator('body').innerText()).replace(/\s+/g, ' ').trim(); + +check('no 5xx on any subrequest', !statuses.some(([s]) => s >= 500), + statuses.filter(([s]) => s >= 500).map(([s, p]) => `${s} ${p}`).join(', ') || 'none'); +check('page is not blank', body.length > 40, `${body.length} chars of text`); +console.log(` top status ${resp.status()}`); +console.log(` text: ${body.slice(0, 180)}`); + +if (isPublic) { + // Tags live in a