38 lines
7.0 KiB
JavaScript
38 lines
7.0 KiB
JavaScript
const {start,assert,DIR,path,fs,sanitized,TARGET,actors}=require('./portal-service-followup-browser-lib.cjs');
|
|
const cp=require('node:child_process'),crypto=require('node:crypto'),PHP='C:/laragon/bin/php/php-8.5.11-Win32-vs17-x64/php.exe';
|
|
void(async()=>{const q=await start('service-followup-bootstrap-cross-confirm-qa');const contexts=[];try{
|
|
async function actor(role){const a=await q.context(role);contexts.push(a.ctx);await q.login(a.page,role);return a.page;}
|
|
async function post(page,url,data){return await page.evaluate(async({url,data})=>{const token=decodeURIComponent(document.cookie.split('; ').find(v=>v.startsWith('XSRF-TOKEN='))?.slice(11)||'');const r=await fetch(url,{method:'POST',headers:{Accept:'application/json','Content-Type':'application/json','X-XSRF-TOKEN':token},body:JSON.stringify(data)});return {status:r.status,data:await r.json()};},{url,data});}
|
|
function inspect(){return JSON.parse(cp.execFileSync(PHP,[path.join(__dirname,'portal-service-followup-bootstrap-inspect.php')],{encoding:'utf8',timeout:30000}));}
|
|
function fixture(id,action){cp.execFileSync(PHP,[path.join(__dirname,'portal-service-followup-bootstrap-fixture.php'),id,action],{encoding:'utf8',timeout:30000});}
|
|
const first=await actor('super_first'),second=await actor('super_second'),outsider=await actor('outsider');
|
|
assert.equal(inspect().pair,null);q.phase('First designated holder requests, second receives only purpose-specific confirmation access');
|
|
const plan=await q.json(first,'/management/profile-install');assert.equal(plan.status,200);assert.equal(plan.data.installed,true);assert.equal(plan.data.consistent,true);
|
|
const denied=await q.json(second,'/management/profile-install');assert.equal(denied.status,403);q.proof.checks.second_needs_no_general_management_privilege=true;
|
|
await q.open(first,'/management/profile-install');await first.getByLabel('Fundamento',{exact:true}).fill('SIMULAÇÃO QA: confirmação cruzada de dois titulares sintéticos, sem designações reais ou alterações ativas.');await first.getByLabel('ID da primeira conta explicitamente designada').fill(String(actors.super_first.id));await first.getByLabel('ID da segunda conta explicitamente designada').fill(String(actors.super_second.id));await first.getByLabel('Confirmo exclusivamente a ação indicada nesta base').check();
|
|
const event=first.waitForResponse(r=>r.request().method()==='POST'&&new URL(r.url()).pathname==='/management/profile-install/bootstrap');await first.getByRole('button',{name:'Pedir confirmação ao segundo titular',exact:true}).click();const request=await event;assert.equal(request.status(),200);const result=await request.json(),id=result.approval_id,url=result.confirmation_url;
|
|
q.proof.approval_id=id;q.proof.request_result=result;
|
|
const requestBody=JSON.parse(request.request().postData()); // Only synthetic operation metadata, no auth fields.
|
|
assert.equal(result.initialized,false);assert.equal(result.privileges_granted,false);assert.equal(inspect().pair,null);assert.equal(inspect().approvals.length,1);assert.equal((await post(first,'/management/profile-install/bootstrap',requestBody)).data.approval_id,id);assert.equal(inspect().approvals.length,1);q.proof.checks.request_does_not_grant_and_retry_no_duplicate=true;
|
|
const changed=await post(first,'/management/profile-install/bootstrap',{...requestBody,reason:requestBody.reason+' alterado'});assert.equal(changed.status,409);
|
|
await q.shot(first,'bootstrap-request-pending-visible',false);
|
|
assert.equal((await q.json(outsider,url)).status,403);
|
|
const firstView=await q.json(first,url);assert.equal(firstView.status,200);assert.equal(firstView.data.can_confirm,false);
|
|
const data={hash:firstView.data.hash,confirm:true,idempotency_key:crypto.randomUUID(),reason:'SIMULAÇÃO QA: confirmar explicitamente a dupla sintética por outra conta e operação, com MFA normal.'};
|
|
assert.equal((await post(first,url+'/confirm',data)).status,403);assert.equal((await post(outsider,url+'/confirm',data)).status,403);assert.equal(inspect().pair,null);q.proof.checks.self_approval_and_outside_holder_denied=true;
|
|
await q.open(first,url);assert(await first.getByRole('button',{name:'Confirmar designação cruzada',exact:true}).isDisabled());
|
|
const secondView=await q.json(second,url);assert.equal(secondView.status,200);assert.equal(secondView.data.can_confirm,true);
|
|
assert.equal((await post(second,url+'/confirm',{...data,hash:'0'.repeat(64)})).status,409);
|
|
for(const operation of ['expire','code-mismatch']){fixture(id,operation);try{const v=await q.json(second,url);assert.equal(v.status,200);assert.equal(v.data.can_confirm,false);assert.equal((await post(second,url+'/confirm',{...data,hash:v.data.hash,idempotency_key:crypto.randomUUID()})).status,409);assert.equal(inspect().pair,null);}finally{fixture(id,'restore');}}
|
|
q.proof.checks.changed_hash_expiry_and_code_mismatch_do_not_initialize=true;
|
|
q.phase('Second holder confirms in separate authenticated/MFA context; atomic pair and separate audit');
|
|
await q.open(second,url);await second.getByLabel('Fundamento da confirmação',{exact:true}).fill(data.reason);await second.getByLabel('Confirmo apenas estas duas contas e esta base').check();await q.shot(second,'bootstrap-second-holder-confirmation',false);
|
|
const confirmed=second.waitForResponse(r=>r.request().method()==='POST'&&new URL(r.url()).pathname===url+'/confirm');await second.getByRole('button',{name:'Confirmar designação cruzada',exact:true}).click();const response=await confirmed;assert.equal(response.status(),200);const final=await response.json();assert.equal(final.initialized,true);const confirmBody=JSON.parse(response.request().postData());
|
|
const state=inspect();assert.equal(state.pair.first_user_id,actors.super_first.id);assert.equal(state.pair.second_user_id,actors.super_second.id);assert.equal(state.pair.approved_by,actors.super_second.id);assert.equal(state.approvals[0].requested_by,actors.super_first.id);assert.equal(state.approvals[0].approved_by,actors.super_second.id);assert(state.approvals[0].consumed_at);assert.equal(state.table_count,295);
|
|
assert.deepEqual((await post(second,url+'/confirm',confirmBody)).data,final);assert.equal(inspect().audits.length,state.audits.length);
|
|
assert.equal((await post(second,url+'/confirm',{...confirmBody,idempotency_key:crypto.randomUUID()})).status,409);q.proof.final_state=inspect();q.proof.checks.separate_mfa_confirmation_atomic_pair_audit_and_replay=true;
|
|
await q.shot(second,'bootstrap-confirmed-visible',false);
|
|
const fresh=await actor('super_second');assert.equal((await q.json(fresh,'/management/profile-install')).status,200);assert.equal((await q.json(fresh,url)).data.consumed,true);q.proof.checks.pair_and_receipt_recovered_new_session=true;
|
|
q.proof.result='passed';q.proof.active_writes=0;q.save();console.log(JSON.stringify({result:'passed',checks:q.proof.checks,target:TARGET,active_writes:0}));
|
|
}catch(e){q.proof.result='failed';q.proof.failure=sanitized(e);q.save();console.log(JSON.stringify({result:'failed',phase:q.proof.phase,failure:q.proof.failure}));process.exitCode=1;}finally{for(const c of contexts)await c.close();await q.browser.close();}})();
|