'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'), os = require('node:os'), path = require('node:path'); const { PeeringState } = require('../peering/state'); module.exports = async function testPeerUI(url, cookie) { const { chromium } = require('playwright'); const browser = await chromium.launch({ channel: process.env.QA_BROWSER_CHANNEL || 'msedge', headless: true }); const context = await browser.newContext({ viewport: { width: 1280, height: 900 } }); const [name, ...value] = cookie.split('='); await context.addCookies([{ name, value: value.join('='), url }]); const page = await context.newPage(), errors = []; page.on('pageerror', error => errors.push(error.message)); try { await page.goto(url + '/peers.html#admin'); await page.getByRole('heading', { name: 'Known peers' }).waitFor(); assert.ok(await page.getByRole('link', { name: 'Peer catalogs' }).count() >= 1); const state = await page.evaluate(() => fetch('/api/admin/peering').then(response => response.json())); assert.equal(state.identity?.name, 'ComposeQA'); await page.getByRole('button', { name: 'Add a peer' }).click(); await page.getByRole('heading', { name: 'Add a peer' }).waitFor(); await page.getByLabel('This server’s reachable peer gateway URL').fill('http://localhost:8080'); await page.getByRole('button', { name: 'Create 48-hour invitation' }).click(); assert.match(await page.getByLabel('This server’s reachable peer gateway URL').evaluate(el => el.validationMessage), /HTTPS peer gateway origin/); await page.getByLabel('This server’s reachable peer gateway URL').fill('https://peer-test.example:8093'); await page.getByRole('button', { name: 'Create 48-hour invitation' }).click(); const outgoing = page.getByLabel('Invitation to send'); await page.waitForFunction(() => document.querySelector('textarea[aria-label="Invitation to send"]')?.value.includes('fingerprint')); const invitation = JSON.parse(await outgoing.inputValue()); assert.equal(invitation.url, 'https://peer-test.example:8093'); assert.match(invitation.name, /^[A-Za-z0-9]{1,25}$/); assert.ok(Date.parse(invitation.expiresAt) > Date.now()); await page.getByLabel('Invitation received').fill('{bad json'); await page.getByRole('button', { name: 'Import invitation' }).click(); await page.getByText('That is not valid invitation JSON.').waitFor(); const peerDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mc-peer-ui-')); let peerId; try { if (process.platform === 'win32' && !process.env.OPENSSL_BIN) process.env.OPENSSL_BIN = 'C:\\Program Files\\Git\\usr\\bin\\openssl.exe'; const peer = new PeeringState(peerDir); peer.generateIdentity('UIFixturePeer'); const fixtureInvitation = peer.createInvitation('https://fixture-peer.example:8093'); await page.getByLabel('Invitation received').fill(JSON.stringify(fixtureInvitation)); await page.getByRole('button', { name: 'Import invitation' }).click(); await page.getByRole('heading', { name: '', exact: true }).waitFor(); const apiState = await page.evaluate(() => fetch('/api/peering').then(response => response.json())); peerId = apiState.peers.find(item => item.alias === 'UIFixturePeer').id; await page.locator('.peer-page-tabs').getByRole('link', { name: 'Peer catalogs' }).click(); await page.getByRole('heading', { name: 'Peer catalogs' }).waitFor(); if (!await page.getByRole('button', { name: 'Select visible' }).count()) { console.error('Peer catalog page content:', await page.locator('main').innerText()); console.error('Peer catalog JS errors:', errors); } await page.getByLabel('Select peer').selectOption(peerId); assert.equal(await page.getByText('Setup is incomplete', { exact: false }).count(), 0, 'Sharing can be prepared before pairing'); await page.getByRole('button', { name: 'Select visible' }).click(); await page.getByRole('button', { name: 'Save approvals' }).click(); await page.getByText('Approved titles updated.').waitFor(); const saved = await page.evaluate(id => fetch('/api/admin/peering').then(r => r.json()).then(s => s.peers.find(p => p.id === id).shares.length), peerId); assert.equal(saved, 3, 'Bulk approval saves the selected local catalog'); assert.equal(await page.getByRole('button', { name: 'Browse peer catalog' }).count(), 1); await page.getByRole('button', { name: 'Browse peer catalog' }).click(); await page.getByText('Setup is incomplete', { exact: false }).waitFor(); await page.setViewportSize({ width: 390, height: 844 }); assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), 'Peer catalog should fit mobile width'); } finally { fs.rmSync(peerDir, { recursive: true, force: true }); } assert.equal(errors.length, 0, `Browser errors: ${errors.join('; ')}`); const created = await page.evaluate(async () => (await fetch('/api/admin/user', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: 'peer-reader-qa', password: 'disposable-reader-test', admin: false }) })).status); assert.equal(created, 200); await page.evaluate(() => fetch('/api/logout', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })); await page.evaluate(() => fetch('/api/login', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: 'peer-reader-qa', password: 'disposable-reader-test' }) })); await page.goto(url + '/peer-catalog.html'); await page.getByText('Peer catalogs are available to administrators only.').waitFor(); const denied = await page.evaluate(async () => Promise.all(['/api/admin/peering', '/api/peering', '/api/peering/catalog?peer=x', '/api/peering/cover?peer=x&manga=x'] .map(async endpoint => (await fetch(endpoint)).status))); assert.deepEqual(denied, [403, 403, 403, 403], 'Every peering API remains admin-only'); await page.goto(url + '/'); await page.getByRole('link', { name: 'Peer catalogs' }).waitFor({ state: 'detached' }); assert.equal(errors.length, 0, `Browser errors: ${errors.join('; ')}`); } finally { await browser.close(); } };