// ============================================================ // ADMIN — SEARCH SOURCES // ============================================================ // The screen that decides whether a generated resource may search the web or // PubMed, and holds the keys for both. // // This exists because that card could not previously be tested at all: there // was no admin account to log in as, so it was checked by reading its markup // and confirming the element ids matched the handlers. That verifies the wiring // and nothing about whether an administrator can actually reach the screen, // whether an ordinary user is kept off it, or whether a saved key survives a // round trip. // // Two things are asserted that are easy to get wrong and expensive to get // wrong: the routes are admin-only, and a key is never sent back to the browser // in readable form. // ============================================================ const { test, expect, E2E_BASE, TEST_EMAIL, ADMIN_EMAIL, loginAs, getAuthToken, getAdminToken } = require('../fixtures'); test.describe('Search Sources', () => { let adminToken, userToken; test.beforeAll(async ({ request }) => { adminToken = await getAdminToken(request); userToken = await getAuthToken(request); }); const auth = t => ({ Authorization: 'Bearer ' + t, 'Content-Type': 'application/json' }); test('an ordinary account cannot read or change search settings', async ({ request }) => { const read = await request.get(E2E_BASE + '/api/admin/websearch', { headers: auth(userToken) }); expect(read.status(), 'a non-admin must not read the settings').toBeGreaterThanOrEqual(400); const write = await request.put(E2E_BASE + '/api/admin/websearch', { headers: auth(userToken), data: { enabled: 'true', provider: 'tavily' }, }); expect(write.status(), 'nor change them').toBeGreaterThanOrEqual(400); }); test('an administrator reads the settings, and no key comes back readable', async ({ request }) => { const r = await request.get(E2E_BASE + '/api/admin/websearch', { headers: auth(adminToken) }); expect(r.ok(), await r.text()).toBeTruthy(); const config = (await r.json()).config; expect(config, 'settings come back under config').toBeTruthy(); // Both sources are represented, so the screen has something to render. // Bracketed, not toHaveProperty: these key names contain dots, and a dotted // string is read as a path into the object rather than as one key. for (const key of ['websearch.enabled', 'websearch.provider', 'pubmed.enabled']) { expect(Object.keys(config), key + ' is missing').toContain(key); } // A key is either absent or masked. Anything else means a secret is being // handed to the browser, which is the one failure here worth catching. for (const key of ['websearch.api_key', 'pubmed.api_key']) { const value = config[key]; if (value) expect(value, key + ' must be masked').toMatch(/^•+/); } }); test('the Test button reports each source separately', async ({ request }) => { const r = await request.post(E2E_BASE + '/api/admin/websearch/test', { headers: auth(adminToken), data: { query: 'bronchiolitis high flow' }, }); expect(r.ok(), await r.text()).toBeTruthy(); const body = await r.json(); // One press has to say which of the two works, so each reports either a // count or a reason — never nothing at all. for (const source of ['web', 'pubmed']) { expect(body[source], source + ' must be reported').toBeTruthy(); const reported = typeof body[source].count === 'number' || Boolean(body[source].reason); expect(reported, source + ' reported neither a count nor a reason').toBeTruthy(); } }); // Admin is not a tab on the rail; it is an item in the account-card menu that // is only created when the signed-in user has the admin role. So opening it // and finding the menu item missing are the same assertion from both sides. async function openAdmin(page) { await page.locator('.account-card-btn').first().click(); await page.locator('[data-account-tab="admin"]').first().click(); await page.waitForFunction(() => { const el = document.getElementById('admin-tab'); return el && el.classList.contains('active') && el.innerHTML.trim().length > 100; }, { timeout: 20000 }); } // AccountBoundary allows one verified owner per document and freezes the page // rather than letting a second account in, so each account is checked in its // own browser context. Swapping the cookie inside one context is not a // shortcut here — it is the thing the app deliberately refuses. async function pageFor(browser, email) { const context = await browser.newContext(); await loginAs(context, context.request, email); const page = await context.newPage(); await page.goto(E2E_BASE + '/'); await page.waitForSelector('button.tab-btn', { timeout: 20000 }); // At phone width the rail — and the account card with it — is behind the // menu toggle, the same way the other specs open it. const vp = page.viewportSize(); if (vp && vp.width <= 768) await page.click('#btn-menu-toggle').catch(() => {}); return { page, context }; } test('the account menu offers Admin to an administrator only', async ({ browser }) => { const mine = await pageFor(browser, TEST_EMAIL); await mine.page.locator('.account-card-btn').first().click(); await expect(mine.page.locator('[data-account-tab="admin"]'), 'an ordinary account is never offered Admin').toHaveCount(0); await mine.context.close(); const theirs = await pageFor(browser, ADMIN_EMAIL); await theirs.page.locator('.account-card-btn').first().click(); await expect(theirs.page.locator('[data-account-tab="admin"]').first()).toBeVisible(); await theirs.context.close(); }); test('the card renders for an administrator, with both sources', async ({ browser }) => { const { page: adminPage, context } = await pageFor(browser, ADMIN_EMAIL); await openAdmin(adminPage); await adminPage.waitForSelector('#ws-provider', { timeout: 20000 }); // Every control the save handler reads must exist, which is the failure the // static id linter catches and this confirms in a real render. for (const id of ['ws-enabled', 'ws-provider', 'ws-api-key', 'ws-base-url', 'pm-enabled', 'pm-api-key', 'pm-email', 'ws-status']) { await expect(adminPage.locator('#' + id), '#' + id + ' is missing').toHaveCount(1); } // Anything typed into a key field must not be a readable input. for (const id of ['ws-api-key', 'pm-api-key']) { await expect(adminPage.locator('#' + id)).toHaveAttribute('type', 'password'); } await context.close(); }); });