Files
Magent/scripts/review_installation_ui.cjs
T
Assclaw fd6671cf7e
Magent CI/CD / verify (push) Successful in 5m20s
Magent CI/CD / deploy-beta (push) Successful in 1m41s
feat: add backup recovery, setup wizard and user-view guards
2026-09-18 17:23:03 +12:00

475 lines
29 KiB
JavaScript

// Fixture-only installation review. All API calls are intercepted; no backups are restored.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { chromium } = require(process.env.REVIEW_PLAYWRIGHT || 'playwright');
const base = process.env.REVIEW_BASE || 'http://127.0.0.1:3114';
const output = process.env.REVIEW_DIR;
async function reviewBackups(browser) {
const context = await browser.newContext({ acceptDownloads: true });
try {
await context.addCookies([{ name: 'magent_logged_in', value: '1', url: base }]);
const calls = [];
const errors = [];
let role = 'admin';
let failStatus = false;
let failRestore = false;
const backup = { created_at: '2026-09-18T01:00:00Z', build: 'fixture-build', include_cache: true };
const status = {
format_version: 1,
max_upload_bytes: 1024,
max_expanded_bytes: 134217728,
include_cache_default: false,
pending_restore: null,
last_restore: null,
};
await context.route('**/api/**', async (route) => {
const request = route.request();
const pathname = new URL(request.url()).pathname;
const method = request.method();
const json = request.headers()['content-type']?.includes('application/json') ? request.postDataJSON() : null;
calls.push({ pathname, method, json, body: request.postDataBuffer() });
const reply = (value) => route.fulfill({ json: value });
if (pathname === '/api/setup/status') return reply({ setup_required: false, needs_admin: false });
if (pathname === '/api/auth/me') {
return reply({ username: 'Fixture admin', role, features: {}, invite_management_enabled: true });
}
if (pathname.startsWith('/api/admin/backups')) {
if (role !== 'admin') {
return route.fulfill({ status: role === 'unauthorized' ? 401 : 403, json: { detail: 'Admin access required.' } });
}
if (pathname === '/api/admin/backups' && method === 'GET') {
if (failStatus) return route.fulfill({ status: 503, json: { detail: 'Backup service unavailable.' } });
return reply(status);
}
if (pathname === '/api/admin/backups/export' && method === 'POST') {
return route.fulfill({
contentType: 'application/octet-stream',
headers: { 'Content-Disposition': 'attachment; filename="magent-backup-fixture.magent-backup"' },
body: Buffer.from('fixture-only-backup-download'),
});
}
if (pathname === '/api/admin/backups/restore' && method === 'POST') {
if (failRestore) return route.fulfill({ status: 400, json: { detail: 'Invalid backup or passphrase.' } });
status.pending_restore = { ...backup, staged_at: '2026-09-18T02:00:00Z' };
return reply({ status: 'staged', restart_required: true, backup, message: 'Restart to apply.' });
}
if (pathname === '/api/admin/backups/restore' && method === 'DELETE') {
status.pending_restore = null;
return reply({ status: 'cancelled' });
}
throw new Error(`Unexpected backup API call: ${method} ${pathname}`);
}
if (pathname.includes('/events/stream')) {
return route.fulfill({ contentType: 'text/event-stream', body: ': fixture\n\n' });
}
if (pathname.includes('/branding/')) return route.fulfill({ status: 404 });
return reply({ items: [], total: 0, services: [], navigation: { showRequests: true } });
});
const page = await context.newPage();
page.on('pageerror', (error) => errors.push(error.message));
const exportPanel = page.getByRole('region', { name: 'Create a backup', exact: true });
const restorePanel = page.getByRole('region', { name: 'Restore a backup', exact: true });
const exportCalls = () => calls.filter((call) => call.pathname === '/api/admin/backups/export');
const restoreCalls = () => calls.filter((call) => call.pathname === '/api/admin/backups/restore' && call.method === 'POST');
const screenshot = async (name) => {
if (output) await page.screenshot({ path: path.join(output, name), fullPage: true });
};
for (const width of [1440, 980, 390, 320]) {
await page.setViewportSize({ width, height: 1000 });
await page.goto(`${base}/admin/backups`);
await exportPanel.getByRole('button', { name: 'Download encrypted backup', exact: true }).waitFor();
assert.equal(await exportPanel.getByLabel('Include artwork caches', { exact: true }).isChecked(), false);
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), `Backups overflow at ${width}px`);
await screenshot(`installation-backups-${width}.png`);
}
await page.setViewportSize({ width: 1440, height: 1000 });
await exportPanel.getByLabel('Backup passphrase', { exact: true }).fill('fixture-passphrase-alpha');
await exportPanel.getByLabel('Confirm backup passphrase', { exact: true }).fill('fixture-passphrase-other');
await exportPanel.getByRole('button', { name: 'Download encrypted backup', exact: true }).click();
await page.getByRole('alert').filter({ hasText: 'passphrases do not match' }).waitFor();
assert.equal(exportCalls().length, 0, 'Mismatched passphrases must not request a backup');
await exportPanel.getByLabel('Confirm backup passphrase', { exact: true }).fill('fixture-passphrase-alpha');
await exportPanel.getByLabel('Include artwork caches', { exact: true }).check();
const [download] = await Promise.all([
page.waitForEvent('download'),
exportPanel.getByRole('button', { name: 'Download encrypted backup', exact: true }).click(),
]);
assert.equal(download.suggestedFilename(), 'magent-backup-fixture.magent-backup');
await page.getByRole('status').filter({ hasText: 'encrypted backup is ready' }).waitFor();
assert.deepEqual(exportCalls().map((call) => call.json), [{ passphrase: 'fixture-passphrase-alpha', include_cache: true }]);
assert.equal(await exportPanel.getByLabel('Backup passphrase', { exact: true }).inputValue(), '');
assert.equal(await exportPanel.getByLabel('Confirm backup passphrase', { exact: true }).inputValue(), '');
const upload = restorePanel.getByLabel('Backup file', { exact: true });
const confirmation = restorePanel.getByLabel('Type RESTORE to confirm replacement', { exact: true });
const prepare = restorePanel.getByRole('button', { name: 'Prepare restore', exact: true });
await restorePanel.getByLabel('Backup passphrase', { exact: true }).fill('fixture-passphrase-alpha');
await confirmation.fill('RESTORE');
await upload.setInputFiles({ name: 'empty.magent-backup', mimeType: 'application/octet-stream', buffer: Buffer.alloc(0) });
await prepare.click();
await page.getByRole('alert').filter({ hasText: 'Choose a Magent backup file' }).waitFor();
assert.equal(restoreCalls().length, 0, 'Empty files must not be uploaded');
await upload.setInputFiles({ name: 'oversize.magent-backup', mimeType: 'application/octet-stream', buffer: Buffer.alloc(1025) });
await prepare.click();
await page.getByRole('alert').filter({ hasText: 'no larger than' }).waitFor();
assert.equal(restoreCalls().length, 0, 'Files exceeding the server limit must not be uploaded');
const fixtureFile = { name: 'fixture.magent-backup', mimeType: 'application/octet-stream', buffer: Buffer.from('fixture-backup-upload') };
await upload.setInputFiles(fixtureFile);
await confirmation.fill('restore');
await prepare.click();
assert.equal(await confirmation.evaluate((element) => element.validity.patternMismatch), true);
assert.equal(restoreCalls().length, 0, 'RESTORE must be typed exactly before uploading');
await confirmation.fill('RESTORE');
failRestore = true;
await prepare.click();
await page.getByRole('alert').filter({ hasText: 'Invalid backup or passphrase.' }).waitFor();
assert.equal(await page.getByRole('heading', { name: 'Restore ready — restart required', exact: true }).count(), 0);
assert.equal(await prepare.isEnabled(), true, 'A rejected backup must leave the form usable');
failRestore = false;
await prepare.click();
await page.getByRole('heading', { name: 'Restore ready — restart required', exact: true }).waitFor();
assert.equal(restoreCalls().length, 2);
const submitted = restoreCalls()[1].body.toString('utf8');
assert.match(submitted, /name="file"; filename="fixture\.magent-backup"/);
assert.match(submitted, /name="passphrase"\r\n\r\nfixture-passphrase-alpha/);
assert.match(submitted, /name="confirmation"\r\n\r\nRESTORE/);
assert.equal(await prepare.isDisabled(), true, 'A pending restore must block a second upload');
assert.equal(await restorePanel.getByLabel('Backup passphrase', { exact: true }).inputValue(), '');
assert.equal(await confirmation.inputValue(), '');
assert.equal(await upload.evaluate((element) => element.files.length), 0);
await screenshot('installation-backups-pending.png');
await page.reload();
await page.getByRole('heading', { name: 'Restore ready — restart required', exact: true }).waitFor();
assert.equal(await prepare.isDisabled(), true, 'Pending state must survive reloading the page');
await page.getByRole('button', { name: 'Cancel pending restore', exact: true }).click();
await page.getByRole('status').filter({ hasText: 'Pending restore cancelled.' }).waitFor();
assert.equal(await page.getByRole('heading', { name: 'Restore ready — restart required', exact: true }).count(), 0);
assert.equal(await prepare.isEnabled(), true);
assert.equal(calls.filter((call) => call.pathname === '/api/admin/backups/restore' && call.method === 'DELETE').length, 1);
failStatus = true;
await page.reload();
await page.getByRole('alert').filter({ hasText: 'Backup service unavailable.' }).waitFor();
assert.equal(await exportPanel.count(), 0, 'Backup controls must not render without authenticated status');
failStatus = false;
await page.getByRole('button', { name: 'Try again', exact: true }).click();
await exportPanel.getByRole('button', { name: 'Download encrypted backup', exact: true }).waitFor();
status.last_restore = {
status: 'rolled_back', restored_at: '2026-09-18T04:00:00Z', rollback_directory: 'fixture-rollback',
message: 'An interrupted or failed restore was rolled back automatically.',
};
await page.reload();
await page.getByText(/Last restore was rolled back/).waitFor();
assert.equal(await page.getByText(/Last restore completed/).count(), 0, 'A rollback must not be labelled a successful restore');
role = 'user';
await page.goto(`${base}/admin/backups`);
await page.getByRole('heading', { name: 'Administrator access required', exact: true }).waitFor();
assert.equal(await page.getByRole('heading', { name: 'Create a backup', exact: true }).count(), 0);
role = 'unauthorized';
await page.goto(`${base}/admin/backups`);
await page.getByRole('heading', { name: 'Administrator access required', exact: true }).waitFor();
assert.equal(await page.getByRole('link', { name: 'Sign in', exact: true }).getAttribute('href'), '/login');
assert.deepEqual(errors, []);
assert.equal(calls.some((call) => /restart/.test(call.pathname)), false, 'Preparing a restore must not restart the app');
console.log('Backup UI passed: responsive layouts, passphrase confirmation, encrypted download, file bounds, RESTORE confirmation, rejected backup retry, pending/cancel state, and admin access.');
} finally {
await context.close();
}
}
async function reviewSetup(browser) {
const context = await browser.newContext();
try {
const calls = [];
const errors = [];
const securityErrors = [];
let authenticated = false;
let role = 'admin';
let needsAdmin = true;
let failBootstrap = false;
let failComplete = false;
let expireNextSave = false;
let expectedLoginPassword = 'Fixture-administrator-passphrase';
const state = { completed: false, step: 'administrator', completed_at: null };
const settings = new Map(Object.entries({
site_login_show_local_login: true,
site_login_show_jellyfin_login: false,
site_login_show_signup_link: true,
magent_notify_email_use_tls: true,
magent_notify_email_use_ssl: false,
magent_notify_email_smtp_port: 587,
requests_poll_interval_seconds: 30,
requests_delta_sync_interval_minutes: 15,
requests_full_sync_time: '03:00',
requests_cleanup_days: 30,
}));
const secret = (key) => key.endsWith('_api_key') || key.endsWith('_password');
await context.route('**/api/**', async (route) => {
const request = route.request();
const pathname = new URL(request.url()).pathname;
const method = request.method();
const json = request.headers()['content-type']?.includes('application/json') ? request.postDataJSON() : null;
calls.push({ pathname, method, json, body: request.postData() });
const reply = (value) => route.fulfill({ json: value });
if (pathname === '/api/setup/status') return reply({ setup_required: !state.completed, needs_admin: needsAdmin });
if (pathname === '/api/setup/bootstrap') {
if (failBootstrap) return route.fulfill({ status: 403, json: { detail: 'Invalid setup token.' } });
assert.equal(needsAdmin, true, 'An existing administrator must never be recreated');
needsAdmin = false;
state.step = 'apps';
return route.fulfill({ status: 201, json: { status: 'created', username: json.username } });
}
if (pathname === '/api/auth/login') {
const credentials = new URLSearchParams(request.postData());
assert.equal(credentials.get('username'), 'fixture-admin');
assert.equal(credentials.get('password'), expectedLoginPassword);
authenticated = true;
return reply({ authenticated: true, user: { role } });
}
if (pathname === '/api/auth/logout') {
authenticated = false;
return reply({ status: 'ok' });
}
if (pathname === '/api/auth/me') {
return authenticated
? reply({ username: 'Fixture admin', role, features: {}, invite_management_enabled: true })
: route.fulfill({ status: 401, json: { detail: 'Not authenticated.' } });
}
if (pathname === '/api/admin/settings' && method === 'PUT' && expireNextSave) {
expireNextSave = false;
authenticated = false;
return route.fulfill({ status: 401, json: { detail: 'Session expired.' } });
}
if (pathname === '/api/setup/state' || pathname === '/api/setup/complete' || pathname.startsWith('/api/admin/')) {
assert.equal(authenticated, true, `Unauthenticated access attempted: ${pathname}`);
assert.equal(role, 'admin', `Non-admin access attempted: ${pathname}`);
}
if (pathname === '/api/setup/state') {
if (method === 'PUT') state.step = json.step;
return reply(state);
}
if (pathname === '/api/setup/complete') {
if (failComplete) return route.fulfill({ status: 503, json: { detail: 'Could not finish setup. Please retry.' } });
state.completed = true;
state.completed_at = '2026-09-18T03:00:00Z';
return reply(state);
}
if (pathname === '/api/admin/settings') {
if (method === 'PUT') for (const [key, value] of Object.entries(json)) settings.set(key, value);
return reply({ settings: Array.from(settings, ([key, value]) => ({
key, value: secret(key) ? '********' : value, sensitive: secret(key), isSet: value !== '' && value !== null,
})) });
}
if (/\/status\/services\/[^/]+\/test$/.test(pathname)) return reply({ status: 'up', message: 'Fixture connection succeeded.' });
if (/\/admin\/(sonarr|radarr)\/options$/.test(pathname)) {
return reply({ rootFolders: [{ path: '/library/tv' }], qualityProfiles: [{ id: 8, name: 'HD 1080p' }] });
}
if (pathname.includes('/events/stream')) return route.fulfill({ contentType: 'text/event-stream', body: ': fixture\n\n' });
if (pathname.includes('/branding/')) return route.fulfill({ status: 404 });
return reply({ items: [], total: 0, services: [], navigation: { showRequests: true } });
});
const page = await context.newPage();
page.on('pageerror', (error) => errors.push(error.message));
page.on('console', (entry) => {
if (/content security policy|blocked by cors policy/i.test(entry.text())) securityErrors.push(entry.text());
});
const bootstrapCalls = () => calls.filter((call) => call.pathname === '/api/setup/bootstrap');
const writes = () => calls.filter((call) => call.pathname === '/api/admin/settings' && call.method === 'PUT');
const screenshot = async (name) => {
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), `Setup overflow: ${name}`);
if (output) await page.screenshot({ path: path.join(output, name), fullPage: true });
};
const appPanel = (name) => page.locator('details').filter({ has: page.getByText(name, { exact: true }) });
const continueButton = page.getByRole('button', { name: 'Save & continue', exact: true });
await page.goto(`${base}/welcome`);
await page.waitForURL(`${base}/setup`);
await page.getByRole('heading', { name: 'Create your administrator', exact: true }).waitFor();
assert.equal(await page.locator('.header').count(), 0, 'Setup must not display account navigation before installation');
for (const width of [1440, 390, 320]) {
await page.setViewportSize({ width, height: 1000 });
await screenshot(`installation-administrator-${width}.png`);
}
await page.getByLabel('Setup token', { exact: true }).fill('fixture-operator-token-0123456789abcdef');
await page.getByLabel('Username', { exact: true }).fill('fixture-admin');
await page.getByLabel('Password', { exact: true }).fill(' Fixture-administrator-passphrase ');
await page.getByLabel('Confirm password', { exact: true }).fill('Different-administrator-passphrase');
await page.getByRole('button', { name: 'Create administrator', exact: true }).click();
await page.getByRole('alert').filter({ hasText: 'passwords do not match' }).waitFor();
assert.equal(bootstrapCalls().length, 0);
await page.getByLabel('Confirm password', { exact: true }).fill(' Fixture-administrator-passphrase ');
failBootstrap = true;
await page.getByRole('button', { name: 'Create administrator', exact: true }).click();
await page.getByRole('alert').filter({ hasText: 'Invalid setup token.' }).waitFor();
assert.equal(authenticated, false);
failBootstrap = false;
await page.getByRole('button', { name: 'Create administrator', exact: true }).click();
await page.getByRole('heading', { name: 'Connect your apps', exact: true }).waitFor();
assert.equal(bootstrapCalls().length, 2);
assert.equal(needsAdmin, false);
assert.equal((await context.cookies()).some((cookie) => cookie.name === 'magent_logged_in'), true);
assert.equal(await page.getByLabel('Setup token', { exact: true }).count(), 0);
assert.equal(await page.getByRole('link', { name: 'Restore it here', exact: true }).getAttribute('href'), '/admin/backups');
assert.equal(await page.locator('details').count(), 8, 'Every supported app must appear');
for (const width of [1440, 390, 320]) {
await page.setViewportSize({ width, height: 1000 });
await screenshot(`installation-apps-${width}.png`);
}
const jellyfin = appPanel('Jellyfin');
await jellyfin.locator('summary').click();
await jellyfin.getByLabel('Server URL', { exact: true }).fill('jellyfin:8096');
await jellyfin.getByRole('button', { name: 'Save & test Jellyfin', exact: true }).click();
await page.getByRole('alert').filter({ hasText: 'HTTP or HTTPS URL' }).waitFor();
assert.equal(writes().length, 0, 'Save and test must validate URLs without relying on native submit validation');
await page.getByRole('navigation', { name: 'Setup steps', exact: true }).getByRole('button', { name: 'Preferences' }).click();
await page.getByRole('alert').filter({ hasText: 'HTTP or HTTPS URL' }).waitFor();
assert.equal(writes().length, 0, 'Step navigation must validate URL drafts too');
await jellyfin.getByLabel('Server URL', { exact: true }).fill('http://jellyfin:8096');
await jellyfin.getByLabel('API key', { exact: true }).fill('fixture-jellyfin-secret');
await jellyfin.getByLabel('Public playback URL', { exact: true }).fill('https://watch.example.test');
await jellyfin.getByRole('button', { name: 'Save & test Jellyfin', exact: true }).click();
await page.getByRole('status').filter({ hasText: 'Jellyfin: Connected' }).waitFor();
assert.deepEqual(writes()[0].json, {
jellyfin_base_url: 'http://jellyfin:8096', jellyfin_api_key: 'fixture-jellyfin-secret', jellyfin_public_url: 'https://watch.example.test',
});
assert.equal(await page.locator('#setup-jellyfin_api_key').inputValue(), '', 'Saved API keys must not be echoed into the form');
assert.match(await page.locator('#setup-jellyfin_api_key').getAttribute('placeholder'), /Leave blank to keep/);
await jellyfin.getByLabel('Public playback URL', { exact: true }).fill('https://new-watch.example.test');
await jellyfin.getByRole('button', { name: 'Save & test Jellyfin', exact: true }).click();
await page.getByRole('status').filter({ hasText: 'Jellyfin: Connected' }).waitFor();
assert.deepEqual(writes()[1].json, { jellyfin_public_url: 'https://new-watch.example.test' });
assert.equal(settings.get('jellyfin_api_key'), 'fixture-jellyfin-secret', 'Blank secret fields must preserve credentials');
await screenshot('installation-jellyfin-320.png');
const sonarr = appPanel('Sonarr');
await sonarr.locator('summary').click();
await sonarr.getByLabel('Server URL', { exact: true }).fill('http://sonarr:8989');
await sonarr.getByLabel('API key', { exact: true }).fill('fixture-sonarr-secret');
await sonarr.getByRole('button', { name: 'Save & test Sonarr', exact: true }).click();
await sonarr.getByRole('combobox', { name: 'Quality profile ID', exact: true }).selectOption('8');
await sonarr.getByLabel('Root folder', { exact: true }).fill('/library/tv');
await continueButton.click();
await page.getByRole('heading', { name: 'Choose your preferences', exact: true }).waitFor();
assert.equal(settings.get('sonarr_quality_profile_id'), 8);
assert.equal(settings.get('sonarr_root_folder'), '/library/tv');
assert.equal(state.step, 'preferences');
await page.reload();
await page.getByRole('heading', { name: 'Choose your preferences', exact: true }).waitFor();
assert.equal(bootstrapCalls().length, 2, 'Reloading must resume without recreating an administrator');
assert.equal(await page.getByLabel('Show Magent account sign-in', { exact: true }).isChecked(), true);
for (const width of [1440, 390, 320]) {
await page.setViewportSize({ width, height: 1000 });
await screenshot(`installation-preferences-${width}.png`);
}
await page.getByLabel('Show Magent account sign-in', { exact: true }).uncheck();
const writesBeforeInvalidPreferences = writes().length;
await continueButton.click();
await page.getByRole('alert').filter({ hasText: 'at least one sign-in method' }).waitFor();
assert.equal(writes().length, writesBeforeInvalidPreferences);
await page.getByLabel('Show Magent account sign-in', { exact: true }).check();
await page.getByLabel('Use implicit TLS (usually port 465)', { exact: true }).check();
await continueButton.click();
await page.getByRole('alert').filter({ hasText: 'Choose STARTTLS or implicit TLS' }).waitFor();
assert.equal(writes().length, writesBeforeInvalidPreferences);
await page.getByLabel('Use implicit TLS (usually port 465)', { exact: true }).uncheck();
await page.getByLabel('Public Magent URL', { exact: true }).fill('https://magent.example.test');
await page.getByLabel('Login page message', { exact: true }).fill('Welcome to the fixture installation.');
await continueButton.click();
await page.getByRole('heading', { name: 'Ready to finish?', exact: true }).waitFor();
assert.equal(state.step, 'review');
assert.equal(settings.get('magent_application_url'), 'https://magent.example.test');
assert.equal(settings.get('site_login_message'), 'Welcome to the fixture installation.');
const finish = page.getByRole('button', { name: 'Finish setup', exact: true });
assert.equal(await finish.isDisabled(), true, 'Setup must not complete without explicit review confirmation');
for (const width of [1440, 390, 320]) {
await page.setViewportSize({ width, height: 1000 });
await screenshot(`installation-review-${width}.png`);
}
await page.getByLabel('I have reviewed the connections and want to finish setup.', { exact: true }).check();
failComplete = true;
await finish.click();
await page.getByRole('alert').filter({ hasText: 'Could not finish setup. Please retry.' }).waitFor();
assert.equal(state.completed, false);
failComplete = false;
await finish.click();
await page.waitForURL(`${base}/admin`);
await page.getByRole('heading', { name: 'Settings', exact: true }).waitFor();
assert.equal(state.completed, true);
assert.equal(calls.filter((call) => call.pathname === '/api/setup/complete').length, 2);
await page.goto(`${base}/setup`);
await page.getByText(/This installation is already set up/).waitFor();
await page.getByRole('heading', { name: 'Connect your apps', exact: true }).waitFor();
await appPanel('Jellyfin').locator('summary').click();
assert.equal(await page.locator('#setup-jellyfin_api_key').inputValue(), '');
assert.equal(bootstrapCalls().length, 2);
await appPanel('Jellyfin').getByLabel('Public playback URL', { exact: true }).fill('https://after-expiry.example.test');
expireNextSave = true;
await appPanel('Jellyfin').getByRole('button', { name: 'Save & test Jellyfin', exact: true }).click();
await page.getByRole('heading', { name: 'Sign in to continue', exact: true }).waitFor();
await page.getByRole('alert').filter({ hasText: 'Your session expired.' }).waitFor();
await page.getByLabel('Username', { exact: true }).fill('fixture-admin');
await page.getByLabel('Password', { exact: true }).fill(expectedLoginPassword);
await page.getByRole('button', { name: 'Sign in to continue', exact: true }).click();
await page.getByRole('heading', { name: 'Connect your apps', exact: true }).waitFor();
await appPanel('Jellyfin').locator('summary').click();
assert.equal(await appPanel('Jellyfin').getByLabel('Public playback URL', { exact: true }).inputValue(), 'https://after-expiry.example.test');
await appPanel('Jellyfin').getByRole('button', { name: 'Save & test Jellyfin', exact: true }).click();
await page.getByRole('status').filter({ hasText: 'Jellyfin: Connected' }).waitFor();
assert.equal(settings.get('jellyfin_public_url'), 'https://after-expiry.example.test');
await context.clearCookies();
authenticated = false;
await page.goto(`${base}/setup`);
await page.getByRole('heading', { name: 'Sign in to continue', exact: true }).waitFor();
assert.equal(await page.getByLabel('Setup token', { exact: true }).count(), 0);
await page.getByLabel('Username', { exact: true }).fill('fixture-admin');
await page.getByLabel('Password', { exact: true }).fill('Fixture-administrator-passphrase');
await page.getByRole('button', { name: 'Sign in to continue', exact: true }).click();
await page.getByRole('heading', { name: 'Connect your apps', exact: true }).waitFor();
const protectedReads = calls.filter((call) => ['/api/setup/state', '/api/admin/settings'].includes(call.pathname)).length;
role = 'user';
state.completed = false;
await page.goto(`${base}/setup`);
await page.getByRole('heading', { name: 'Administrator access required', exact: true }).waitFor();
assert.equal(await page.getByRole('navigation', { name: 'Setup steps', exact: true }).count(), 0);
assert.equal(calls.filter((call) => ['/api/setup/state', '/api/admin/settings'].includes(call.pathname)).length, protectedReads);
await page.getByRole('button', { name: 'Sign in with an administrator account', exact: true }).click();
await page.getByRole('heading', { name: 'Sign in to continue', exact: true }).waitFor();
assert.equal(page.url(), `${base}/setup`, 'Switching accounts on an unfinished install must avoid the login redirect loop');
role = 'admin';
expectedLoginPassword = ' Existing-administrator-passphrase ';
await page.getByLabel('Username', { exact: true }).fill('fixture-admin');
await page.getByLabel('Password', { exact: true }).fill(expectedLoginPassword);
await page.getByRole('button', { name: 'Sign in to continue', exact: true }).click();
await page.getByRole('heading', { name: 'Ready to finish?', exact: true }).waitFor();
assert.deepEqual(errors, []);
assert.deepEqual(securityErrors, [], 'Setup must hydrate without CSP or CORS errors');
console.log('Setup UI passed: fresh-install redirect, token/password checks, bootstrap/login, app save/test, masked credentials, collector choices, preferences validation, resume, finish/retry, existing installs, and admin-only access.');
} finally {
await context.close();
}
}
(async () => {
if (output) fs.mkdirSync(output, { recursive: true });
const browser = await chromium.launch({ headless: true, executablePath: process.env.REVIEW_CHROMIUM || undefined });
try {
await reviewBackups(browser);
await reviewSetup(browser);
} finally {
await browser.close();
}
})().catch((error) => { console.error(error); process.exitCode = 1; });