Checkpoint peering identity and opt-in discovery

This commit is contained in:
Codex committed 2026-10-13 22:00:54 +02:00
1 parent eba9b50e30
commit 47de081721
44 files changed
+1557 -55

No files matched your search

+54 -1
View File
@@ -118,6 +118,49 @@ async function browserCheck(instance, cookie, peerId, shareId) {
} finally { await browser.close(); }
}
async function peerSettingsUiCheck(instance, cookie, options = {}) {
const { chromium } = require('playwright');
const browser = await chromium.launch({ channel: process.env.QA_BROWSER_CHANNEL || 'msedge', headless: true });
try {
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
const [name, ...value] = cookie.split('='); await context.addCookies([{ name, value: value.join('='), url: instance.url }]);
const page = await context.newPage(), errors = []; page.on('pageerror', error => errors.push(error.message));
await page.goto(instance.url);
await page.locator('.nav[data-peer-catalog-resolved="true"]').waitFor();
assert.equal(await page.locator('.nav a[href="peer-catalog.html"]').count(), options.hasPeer ? 1 : 0,
'Peer catalogs nav appears only after a peer is configured');
if (options.peerId) {
await page.goto(`${instance.url}/peers.html#admin/peering/${encodeURIComponent(options.peerId)}`);
await page.getByRole('heading', { name: 'Access between servers' }).waitFor();
const issue = page.getByRole('button', { name: options.outboundGrant ? 'Save grant permissions' : 'Issue grant', exact: true });
if (options.paired) {
await page.getByText('Peering confirmed on both servers', { exact: true }).waitFor();
if (!options.outboundGrant) await page.getByLabel('Browse approved catalog').check();
assert.equal(await issue.isDisabled(), false, 'A selected grant permission enables the grant action after pairing');
} else {
await page.getByText('Identity not verified yet', { exact: true }).waitFor();
await page.getByLabel('Browse approved catalog').check();
await page.getByLabel('Download chapters (includes catalog access)').check();
assert.equal(await issue.isDisabled(), true, 'Grant issue remains locked before identity verification');
await page.getByText(/Granting is locked until identity pairing is complete/).waitFor();
}
if (options.outboundGrant) await page.getByText('Grant active', { exact: true }).waitFor();
if (options.inboundGrant) await page.getByText('Active grant from peer', { exact: true }).waitFor();
if (options.paired) {
const stateCard = page.locator('.peer-state-ok').first();
const visual = await stateCard.evaluate(element => {
const style = getComputedStyle(element);
return { weight: getComputedStyle(element.querySelector('strong')).fontWeight,
border: style.borderLeftWidth, background: style.backgroundColor };
});
assert.ok(Number(visual.weight) >= 700 && visual.border === '4px' && visual.background !== 'rgba(0, 0, 0, 0)',
`Confirmed state must be visually prominent: ${JSON.stringify(visual)}`);
}
}
assert.equal(errors.length, 0, `Peer settings browser errors: ${errors.join('; ')}`);
} finally { await browser.close(); }
}
async function main() {
let started = [], succeeded = false;
try {
@@ -138,6 +181,8 @@ async function main() {
}), `${instance.label}: peer gateway health`);
}
const cookieA = await login(A), cookieB = await login(B);
await peerSettingsUiCheck(A, cookieA, { hasPeer: false });
await peerSettingsUiCheck(B, cookieB, { hasPeer: false });
const stateA = (await json(`${A.url}/api/admin/peering`, cookieA)).data;
const stateB = (await json(`${B.url}/api/admin/peering`, cookieB)).data;
const hostUrlA = `https://host.docker.internal:${gateA}/`, hostUrlB = `https://host.docker.internal:${gateB}/`;
@@ -149,6 +194,7 @@ async function main() {
const peerA = importedA.data.peers.find(peer => peer.fingerprint === inviteB.fingerprint);
const peerB = importedB.data.peers.find(peer => peer.fingerprint === inviteA.fingerprint);
assert.ok(peerA && peerB);
await peerSettingsUiCheck(B, cookieB, { hasPeer: true, peerId: peerB.id, paired: false });
const probeA = await json(`${A.url}/api/admin/peering`, cookieA, { command: 'probe', id: peerA.id });
const probeB = await json(`${B.url}/api/admin/peering`, cookieB, { command: 'probe', id: peerB.id });
if (probeA.response.status !== 200 || probeB.response.status !== 200) {
@@ -161,6 +207,7 @@ async function main() {
assert.equal(probeA.response.status, 200); assert.equal(probeB.response.status, 200);
assert.equal(probeA.data.peers.find(peer => peer.id === peerA.id).paired, true);
assert.equal(probeB.data.peers.find(peer => peer.id === peerB.id).paired, true);
await peerSettingsUiCheck(A, cookieA, { hasPeer: true, peerId: peerA.id, paired: true });
assert.ok(stateA.identity?.fingerprint && stateB.identity?.fingerprint);
let libraryA = [];
@@ -173,8 +220,11 @@ async function main() {
const grant = await json(`${A.url}/api/admin/peering`, cookieA, { command: 'grant', id: peerA.id, scopes: ['catalog'] });
assert.equal(grant.response.status, 200);
assert.equal((await json(`${B.url}/api/admin/peering`, cookieB, { command: 'accept-grant', id: peerB.id, token: grant.data.grant.token })).response.status, 200);
await peerSettingsUiCheck(A, cookieA, { hasPeer: true, peerId: peerA.id, paired: true, outboundGrant: true });
await peerSettingsUiCheck(B, cookieB, { hasPeer: true, peerId: peerB.id, paired: true, inboundGrant: true });
const catalog = await json(`${B.url}/api/peering/catalog?peer=${encodeURIComponent(peerB.id)}`, cookieB);
assert.equal(catalog.response.status, 200); assert.equal(catalog.data.items.length, 1);
assert.equal(catalog.data.items[0].workIdentity?.scheme, 'mangacircus-work-v1', 'Peer catalogs expose a stable, opaque work identity');
const shareId = catalog.data.items[0].id;
const cover = await fetch(`${B.url}/api/peering/cover?peer=${encodeURIComponent(peerB.id)}&manga=${shareId}`, { headers: { cookie: cookieB } });
assert.equal(cover.status, 200); assert.match(cover.headers.get('content-type'), /^image\/png/);
@@ -189,7 +239,10 @@ async function main() {
await browserCheck(B, cookieB, peerB.id, shareId);
const queue = (await json(`${B.url}/api/admin/scraper`, cookieB)).data;
assert.equal(queue.paused, true, 'Peer testing must leave the scraper queue paused');
assert.equal(queue.queue.filter(item => item.url === `peer://${peerB.id}/${shareId}`).length, 1, 'Exactly one peer queue item is added');
const peerQueueItems = queue.queue.filter(item => item.url === `peer://${peerB.id}/${shareId}`);
assert.equal(peerQueueItems.length, 1, 'Exactly one peer queue item is added');
assert.deepEqual(peerQueueItems[0].workIdentity, catalog.data.items[0].workIdentity,
'The resolved work identity is persisted on the queued peer transfer');
const volumes = await Promise.all(started.map(({ project, env }) => docker(project, env, 'ps', '--format', '{{.Service}}')));
for (const services of volumes) for (const name of ['reader', 'scraper', 'sentinel', 'peer-gateway']) assert.ok(services.includes(name), `${name} is running in A and B`);
console.log('PASS: isolated Compose A↔B pairing, catalog-only access, cover proxy, grant update, browser details/bulk queue, one paused queue writer, and admin-only API/UI boundary.');