const {chromium}=require('playwright'); const assert=require('node:assert/strict'); const jsQR=require('jsqr'); const {totp}=require('./browser_mfa'); (async()=>{ const browser=await chromium.launch({executablePath:process.env.HCR_TEST_CHROME||'C:/Program Files/Google/Chrome/Application/chrome.exe',headless:true}); const page=await browser.newPage({viewport:{width:390,height:844}}),errors=[],requests=[];let checks=0; page.on('pageerror',e=>errors.push(e.message));page.on('request',r=>requests.push(r.url())); const pass=message=>{checks++;console.log('PASS:',message);}; const click=async button=>{await Promise.all([page.waitForNavigation(),button.click()]);}; async function scan(){ const image=page.locator('.mfa-setup-qr');await image.waitFor({state:'visible'}); const pixels=await image.evaluate(async img=>{ await img.decode();const canvas=document.createElement('canvas');canvas.width=canvas.height=320; const ctx=canvas.getContext('2d');ctx.drawImage(img,0,0,320,320); return Array.from(ctx.getImageData(0,0,320,320).data); }); const decoded=jsQR(new Uint8ClampedArray(pixels),320,320,{inversionAttempts:'dontInvert'}); assert(decoded,'QR image did not decode');return new URL(decoded.data); } try{ await page.goto(process.env.HCR_TEST_URL+'/main.php');await page.locator('#user_email').fill('admin@example.test');await page.locator('#user_password').fill('Admin test password 123!');await click(page.locator('#login button[type=submit]')); const image=page.locator('.mfa-setup-qr');assert.match(await image.getAttribute('src'),/^data:image\/svg\+xml;base64,/);pass('Setup renders a locally generated QR image under the security policy'); const uri=await scan(),secret=uri.searchParams.get('secret');assert.equal(uri.protocol,'otpauth:');assert.equal(uri.host,'totp');assert.equal(uri.searchParams.get('issuer'),'My Hunting Club Roster');assert(decodeURIComponent(uri.pathname).includes('admin@example.test'));pass('Independent decoder reads the correct authenticator type, issuer and account'); assert.equal(secret,(await page.locator('pre').textContent()).trim());assert.equal(uri.searchParams.get('period')||'30','30');assert.equal(uri.searchParams.get('digits')||'6','6');assert.equal((uri.searchParams.get('algorithm')||'SHA1').toUpperCase(),'SHA1');pass('QR and manual key configure the same secret and verification settings'); await page.reload();assert((await scan()).href===uri.href,'Refreshing setup changed its provisioning details');pass('Refreshing pending setup preserves its QR details and manual key'); const box=await image.boundingBox();assert(box.width<=390&&box.height===box.width);pass('QR image fits a phone-width browser without distortion'); await page.locator('[name=current_password]').fill('Admin test password 123!');await page.locator('[name=mfa_code]').fill(totp(secret));await click(page.getByRole('button',{name:'Verify',exact:true})); await page.getByRole('heading',{name:'Save your recovery codes'}).waitFor();const codes=(await page.locator('pre').textContent()).trim().split(/\s+/);assert.equal(codes.length,10);pass('A code generated from the scanned QR completes MFA enrollment'); assert.equal(await page.locator('.mfa-setup-qr').count(),0);await click(page.getByRole('link',{name:'I have saved my recovery codes'}));await page.goto(process.env.HCR_TEST_URL+'/main.php?Xaction=security');assert.equal(await page.locator('.mfa-setup-qr').count(),0);pass('Enrolled accounts and recovery-code pages do not display the setup QR'); await page.locator('[name=current_password]').fill('Admin test password 123!');await page.locator('[name=mfa_code]').fill(codes[0]);await click(page.getByRole('button',{name:'Replace lost authenticator',exact:true})); const replacement=await scan();assert.notEqual(replacement.searchParams.get('secret'),secret);assert.equal(replacement.searchParams.get('secret'),(await page.locator('pre').textContent()).trim());pass('Authenticator replacement gets a fresh QR matching the new manual key'); await page.locator('[name=current_password]').fill('Admin test password 123!');await page.locator('[name=mfa_code]').fill(totp(replacement.searchParams.get('secret')));await click(page.getByRole('button',{name:'Confirm replacement',exact:true}));await page.getByRole('heading',{name:'Save your recovery codes'}).waitFor();pass('Scanned replacement code confirms the atomic factor swap'); assert(requests.every(url=>url.startsWith(process.env.HCR_TEST_URL)||url.startsWith('data:')));assert(!requests.some(url=>url.includes(secret)));pass('Setup makes no external QR requests and puts no secret in request URLs'); assert.deepEqual(errors,[]);pass('QR enrollment and replacement produce no browser script errors'); console.log(checks,'MFA QR browser checks passed.'); }finally{await browser.close();} })().catch(e=>{console.error(e);process.exit(1);});