Checkpoint peering identity and opt-in discovery
This commit is contained in:
1 parent
eba9b50e30
commit
47de081721
44 files changed
+1557
-55
No files matched your search
+54
-1
@@ -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.');
|
||||
|
||||
Reference in new issue
Block a user