import fs from 'node:fs';
import {test,expect} from '@playwright/test';
const runtime=fs.readFileSync(new URL('../public/app-runtime.js',import.meta.url),'utf8');
const start=runtime.indexOf(' let developerMfaPending=null');
if(start<0)throw new Error('Developer MFA recovery is not integrated');
const implementation=runtime.slice(start,runtime.indexOf('\n async function renderOverview',start));
const pending={id:'abandoned',factor_type:'totp',friendly_name:'Caterium Developer',status:'unverified'};
async function fixture(page,options={}){
await page.setContent('
');
await page.evaluate(options=>{
window.testUid='developer';window.mfaServer={rows:options.rows||[],aal:'aal1',enrolls:0,deletes:[],lists:0,verifies:0,results:[],errors:[],...options};
const server=window.mfaServer;
window.testClient={auth:{refreshSession:async()=>({error:null}),mfa:{
getAuthenticatorAssuranceLevel:async()=>({data:{currentLevel:server.aal}}),
listFactors:async()=>{server.lists++;if(server.verifyOnList===server.lists)server.rows[0].status='verified';return {data:{all:structuredClone(server.rows),totp:structuredClone(server.rows.filter(f=>f.factor_type==='totp'&&f.status==='verified'))}};},
unenroll:async({factorId})=>{server.deletes.push(factorId);if(server.resetError)return {error:{message:'Network failure'}};const factor=server.rows.find(f=>f.id===factorId);if(factor?.status==='verified')throw new Error('Must never remove verified MFA');server.rows=server.rows.filter(f=>f.id!==factorId);return {error:null};},
enroll:async()=>{server.enrolls++;if(server.rows.some(f=>f.friendly_name==='Caterium Developer'))return {error:{message:'A factor with this friendly name already exists'}};const f={id:'new-'+server.enrolls,factor_type:'totp',friendly_name:'Caterium Developer',status:'unverified'};server.rows.push(f);return {data:{...f,totp:{qr_code:'',secret:'SYNTHETIC-TEST-KEY'}}};},
challengeAndVerify:async({factorId,code})=>{server.verifies++;if(code!=='123456')return {error:{message:'Неверный код'}};if(server.delayVerify)await new Promise(r=>window.finishVerification=r);server.rows.find(f=>f.id===factorId).status='verified';server.aal='aal2';return {error:null};}
}}};
},options);
await page.addScriptTag({content:`(()=>{
const client=()=>window.testClient,session=()=>({user:{id:window.testUid}}),toast=()=>{};
const esc=x=>String(x??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
function modal(title,html){document.getElementById('sunDevModalV22')?.remove();const m=document.createElement('div');m.id='sunDevModalV22';m.innerHTML='
'+esc(title)+'
'+html;document.body.append(m);const close=()=>m.remove();m.addEventListener('click',e=>{if(e.target===m||e.target.closest('[data-close]'))close();});return {m,close};}
${implementation}
window.openTestMfa=()=>ensureDeveloperMfa().then(v=>mfaServer.results.push(v),e=>mfaServer.errors.push(e.message));
})();`});
}
async function open(page){await page.evaluate(()=>{void openTestMfa();});await expect(page.locator('#sunDevMfaCode')).toBeVisible();}
const counts=page=>page.evaluate(()=>({enrolls:mfaServer.enrolls,deletes:mfaServer.deletes,verifies:mfaServer.verifies}));
test('abandoned enrollment in all is recovered without touching unrelated factors',async({page})=>{
await fixture(page,{rows:[pending,{...pending,id:'other-app',friendly_name:'Other app'},{...pending,id:'phone',factor_type:'phone',friendly_name:'My phone'}]});await open(page);
expect(await counts(page)).toEqual({enrolls:1,deletes:['abandoned'],verifies:0});
await expect(page.locator('.sun-dev-mfa-qr')).toHaveAttribute('src',/^data:image\/svg\+xml/);
});
test('closing and reopening keeps the QR and concurrent opens share one enrollment',async({page})=>{
await fixture(page);await open(page);const qr=await page.locator('.sun-dev-mfa-qr').getAttribute('src');
await page.locator('[data-close]').click();await expect(page.locator('#sunDevMfaCode')).toHaveCount(0);
await page.evaluate(()=>{for(let i=0;i<5;i++)void openTestMfa();});await expect(page.locator('#sunDevMfaCode')).toHaveCount(1);
await expect(page.locator('.sun-dev-mfa-qr')).toHaveAttribute('src',qr);expect((await counts(page)).enrolls).toBe(1);
await page.locator('#sunDevMfaCode').fill('123456');await page.locator('#sunDevMfaVerify').click();
await expect.poll(()=>page.evaluate(()=>mfaServer.results)).toEqual([false,true,true,true,true,true]);expect((await counts(page)).verifies).toBe(1);
});
test('verified MFA stays enrolled and access still requires a correct code and AAL2',async({page})=>{
await fixture(page,{rows:[{...pending,status:'verified'}]});await open(page);await expect(page.locator('.sun-dev-mfa-qr')).toHaveCount(0);
await page.locator('#sunDevMfaCode').fill('999999');await page.locator('#sunDevMfaVerify').click();await expect(page.locator('#sunDevMfaError')).toContainText('Неверный код');
expect(await page.evaluate(()=>mfaServer.results)).toEqual([]);
await page.locator('#sunDevMfaCode').fill('123456');await page.locator('#sunDevMfaVerify').click();await expect.poll(()=>page.evaluate(()=>mfaServer.results)).toEqual([true]);
expect(await counts(page)).toEqual({enrolls:0,deletes:[],verifies:2});
});
test('failed cleanup stops enrollment and never reports developer access',async({page})=>{
await fixture(page,{rows:[pending],resetError:true});await page.evaluate(()=>{void openTestMfa();});
await expect.poll(()=>page.evaluate(()=>mfaServer.errors.length)).toBe(1);
expect((await counts(page)).enrolls).toBe(0);expect(await page.evaluate(()=>mfaServer.results)).toEqual([]);await expect(page.locator('#sunDevMfaCode')).toHaveCount(0);
});
test('a factor verified during recovery is not removed',async({page})=>{
await fixture(page,{rows:[pending],verifyOnList:2});await open(page);
expect(await counts(page)).toEqual({enrolls:0,deletes:[],verifies:0});await expect(page.locator('.sun-dev-mfa-qr')).toHaveCount(0);
});
test('closing during verification cannot orphan the modal or issue another request',async({page})=>{
await fixture(page,{delayVerify:true});await open(page);await page.locator('#sunDevMfaCode').fill('123456');await page.locator('#sunDevMfaVerify').click();
await page.locator('[data-close]').click();await expect(page.locator('#sunDevMfaCode')).toBeVisible();await expect(page.locator('#sunDevMfaVerify')).toBeDisabled();
await page.evaluate(()=>finishVerification());await expect.poll(()=>page.evaluate(()=>mfaServer.results)).toEqual([true]);await expect(page.locator('#sunDevMfaCode')).toHaveCount(0);
});
test('account change closes the sensitive QR and cannot grant access to another account',async({page})=>{
await fixture(page);await open(page);await page.evaluate(()=>{testUid='another-user';window.dispatchEvent(new Event('sun:cloud-permissions-changed'));});
await expect.poll(()=>page.evaluate(()=>mfaServer.results)).toEqual([false]);await expect(page.locator('.sun-dev-mfa-qr')).toHaveCount(0);
expect(implementation).not.toMatch(/localStorage|sessionStorage/);
});