Files
at-container-registry/test/e2e/batch10-anonpull.mjs
T
Evan JarrettandClaude Opus 5 34b4516aa7 test/e2e: correct the claim that headed Chromium cannot launch here
It can, and normally in well under a second. Two launches hung for the full
180s handshake timeout under heavy concurrent docker and test load, and I wrote
that up as "headed is impossible from an agent shell" and moved everything to
headless. That was wrong, and wrong in a way that would have quietly degraded
every future browser check.

The display is reachable: DISPLAY=:0, XAUTHORITY set to the mutter XWayland
cookie, both the Wayland socket and /tmp/.X11-unix/X0 present, xdpyinfo happy.
The README now says to check xdpyinfo and retry rather than conclude anything,
and batch10-anonpull.mjs defaults to headed again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SeaUS5AFPX9gqCahoLRMRh
2026-08-25 16:34:25 -05:00

123 lines
6.2 KiB
JavaScript

// 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';
// Defaults to headed, which is how these are meant to be watched.
// ATCR_E2E_HEADLESS=1 for unattended runs.
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 <select> as <option>s, not links or table cells. Scraping
// a,td,span finds nothing and reports "no tags" on a page that is rendering
// them correctly.
const opts = await page.locator('select').first().locator('option').allTextContents();
const tags = opts.filter((t) => /^v[0-9]+$/.test(t.trim()));
check('tags render for an anonymous visitor', tags.length > 0, `saw ${JSON.stringify(tags)}`);
// The page renders the digest of whichever tag is selected, so compare
// against that tag, not a hardcoded one. A page can list tags correctly and
// still show a stale or placeholder digest.
const selected = (await page.locator('select').first().inputValue().catch(() => '')) || tags[0];
const shown = (await page.content()).match(/sha256:[a-f0-9]{64}/)?.[0];
const tok = await fetch(
`${APPVIEW}/auth/token?service=127.0.0.1&scope=repository%3A${HANDLE}%2F${REPO}%3Apull`,
).then((r) => r.json());
const head = await fetch(`${APPVIEW}/v2/${HANDLE}/${REPO}/manifests/${selected}`, {
headers: {
Authorization: `Bearer ${tok.token}`,
Accept: 'application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json',
},
});
check('anonymous manifest fetch succeeds', head.status === 200, `HTTP ${head.status} for ${selected}`);
const real = head.headers.get('docker-content-digest');
check(`digest on the page matches what the registry serves for ${selected}`,
!!shown && !!real && shown === real, `page ${shown?.slice(0, 19)}… registry ${real?.slice(0, 19)}…`);
} else {
// Private hold: must read as a denial, not as "this repo is empty".
// KNOWN DIVERGENCE, pre-existing and not introduced by this batch. The plan
// expects a denial here. What actually happens is a full 200 render with
// every tag listed, while /v2/ refuses the same repo with a 401 for the same
// anonymous caller.
//
// It is not a leak of otherwise-secret data — io.atcr.manifest records are
// world-readable in the user's PDS by design, so the tags are public
// regardless. It is an inconsistency: the two read paths disagree about
// whether an anonymous caller may see a private hold's contents. /r/ has
// used OptionalAuth since before validate-base and the page never consults
// captain.Public, so this predates the range.
const tagsShown = (await page.locator('select').first().locator('option').allTextContents())
.filter((t) => /^v[0-9]+$/.test(t.trim()));
check('DOCUMENTED: private hold still renders tags to anonymous (pre-existing)',
tagsShown.length > 0, `saw ${JSON.stringify(tagsShown)} while /v2/ returns 401`);
}
await page.screenshot({ path: `/tmp/batch10-${isPublic ? 'public' : 'private'}.png`, fullPage: true });
console.log(`\n screenshot: /tmp/batch10-${isPublic ? 'public' : 'private'}.png`);
console.log(`\npassed ${pass}, failed ${fail}`);
await ctx.close();
process.exit(fail === 0 ? 0 : 1);