98 lines
6.2 KiB
JavaScript
98 lines
6.2 KiB
JavaScript
// Fixture-only review: every API request is intercepted; no live identities are changed.
|
|
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:3116'
|
|
|
|
;(async () => {
|
|
const browser = await chromium.launch({ headless: true })
|
|
try {
|
|
const context = await browser.newContext()
|
|
await context.addCookies([{ name: 'magent_logged_in', value: '1', url: base }])
|
|
let role = 'admin'
|
|
let mode = 'ready'
|
|
let scans = 0
|
|
const saves = []
|
|
const row = (id, username, state) => ({
|
|
user: { id, username, role: 'user', auth_provider: 'jellyfin', jellyseerr_user_id: null },
|
|
jellyfin: { id: 'a'.repeat(32), name: username }, candidate_jellyfin_id: 'a'.repeat(32), stored_jellyfin_id: null,
|
|
seerr: [{ id: 20 + id, name: username, jellyfin_id: 'a'.repeat(32) }],
|
|
jellystat: { state: state === 'unavailable' ? 'unavailable' : 'matched', id: 'a'.repeat(32), name: username },
|
|
basis: 'suggested_username', issues: state === 'conflict' ? ['Multiple Magent accounts resolve to this Jellyfin ID.'] : [],
|
|
state, can_confirm: state === 'ready', confirmed_at: state === 'confirmed' ? '2026-09-08T00:00:00Z' : null,
|
|
})
|
|
const fixture = {
|
|
revision: 'b'.repeat(64), checked_at: '2026-09-08T01:00:00Z', server_id: 'c'.repeat(32),
|
|
services: { jellyfin: 'available', seerr: 'available', jellystat: 'available' },
|
|
counts: { magent: 5, ready: 2, confirmed: 1, conflict: 1, unlinked: 0, unavailable: 1 },
|
|
rows: [row(51, 'Georgia', 'ready'), row(52, 'Another viewer', 'ready'), row(53, 'Duplicate account', 'conflict'), row(54, 'Unavailable viewer', 'unavailable'), row(55, 'Confirmed viewer', 'confirmed')],
|
|
upstream: [{ platform: 'Seerr', id: '99', name: 'Former viewer', jellyfin_id: 'd'.repeat(32), detail: 'No current Jellyfin account has this ID.' }],
|
|
}
|
|
await context.route('**/api/**', async (route) => {
|
|
const request = route.request()
|
|
const url = new URL(request.url())
|
|
if (url.pathname === '/api/auth/me') return route.fulfill({ json: { username: 'Fixture admin', role } })
|
|
if (url.pathname === '/api/admin/identities') {
|
|
scans++
|
|
if (mode === 'forbidden') return route.fulfill({ status: 403, json: { detail: 'Forbidden' } })
|
|
return route.fulfill({ json: fixture })
|
|
}
|
|
if (url.pathname === '/api/admin/identities/confirm') {
|
|
saves.push(request.postDataJSON())
|
|
if (mode === 'stale') return route.fulfill({ status: 409, json: { detail: 'The identity check has changed. Run it again before confirming accounts.' } })
|
|
return route.fulfill({ json: { confirmed: request.postDataJSON().user_ids.length, confirmed_at: '2026-09-08T02:00:00Z' } })
|
|
}
|
|
if (url.pathname.includes('/events/stream')) return route.fulfill({ contentType: 'text/event-stream', body: ': fixture\n\n' })
|
|
return route.fulfill({ json: {} })
|
|
})
|
|
const page = await context.newPage()
|
|
const errors = []
|
|
page.on('pageerror', (error) => errors.push(error.message))
|
|
for (const width of [1440, 980, 390, 320]) {
|
|
await page.setViewportSize({ width, height: 1000 })
|
|
await page.goto(`${base}/admin/identities`)
|
|
await page.getByRole('button', { name: 'Check all user IDs', exact: true }).waitFor()
|
|
const before = scans
|
|
assert.equal(await page.locator('.identity-account').count(), 0)
|
|
await page.getByRole('button', { name: 'Check all user IDs', exact: true }).click()
|
|
await page.getByRole('heading', { name: 'Georgia', exact: true }).waitFor()
|
|
assert.equal(scans, before + 1)
|
|
assert.equal(await page.getByRole('checkbox').count(), 2)
|
|
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), `Overflow at ${width}px`)
|
|
if (process.env.REVIEW_DIR) {
|
|
fs.mkdirSync(process.env.REVIEW_DIR, { recursive: true })
|
|
await page.screenshot({ path: path.join(process.env.REVIEW_DIR, `identities-${width}.png`), fullPage: true })
|
|
}
|
|
}
|
|
await page.getByLabel('Find an account').fill('Georgia')
|
|
await page.getByRole('button', { name: 'Select ready accounts shown' }).click()
|
|
assert.equal(saves.length, 0)
|
|
await page.getByRole('button', { name: 'Review selected links (1)', exact: true }).click()
|
|
await page.getByRole('region', { name: 'Review links before saving' }).waitFor()
|
|
assert.equal(await page.locator('.identity-confirm-panel li').count(), 1)
|
|
assert(await page.locator('.identity-confirm-panel').evaluate((element) => document.activeElement === element))
|
|
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth))
|
|
await page.getByRole('button', { name: 'Confirm and save links', exact: true }).click()
|
|
await page.getByText('1 account link confirmed and saved.', { exact: false }).waitFor()
|
|
assert.deepEqual(saves[0], { revision: fixture.revision, user_ids: [51] })
|
|
assert.equal(await page.locator('.identity-account').count(), 0)
|
|
mode = 'stale'
|
|
await page.getByRole('button', { name: 'Check all user IDs', exact: true }).click()
|
|
await page.getByRole('heading', { name: 'Georgia', exact: true }).waitFor()
|
|
await page.getByRole('button', { name: 'Select ready accounts shown' }).click()
|
|
await page.getByRole('button', { name: 'Review selected links (1)', exact: true }).click()
|
|
await page.getByRole('button', { name: 'Confirm and save links', exact: true }).click()
|
|
await page.getByRole('alert').filter({ hasText: 'The identity check has changed' }).waitFor()
|
|
assert.equal(await page.getByRole('button', { name: 'Confirm and save links', exact: true }).count(), 0)
|
|
mode = 'forbidden'
|
|
await page.getByRole('button', { name: 'Check all user IDs', exact: true }).click()
|
|
await page.waitForURL(base + '/')
|
|
role = 'user'
|
|
await page.goto(`${base}/admin/identities`)
|
|
await page.waitForURL(base + '/')
|
|
assert.deepEqual(errors, [])
|
|
console.log('Identity UI: desktop/mobile, filtering, conflict exclusions, review, save, stale checks and access control passed.')
|
|
} finally { await browser.close() }
|
|
})().catch((error) => { console.error(error); process.exit(1) })
|