Files
Investidor-V2/tools/portal-workflow-upgrade-documents-qa.cjs
nala 287e991486
tests / ci (push) Waiting to run
oportunidades 7
2026-10-13 11:40:29 +02:00

52 lines
14 KiB
JavaScript

const {start,assert,DIR,path,fs,sanitized,actors,TARGET}=require('./portal-workflow-upgrade-browser-lib.cjs');
const cp=require('node:child_process'),crypto=require('node:crypto');
const PHP='C:/laragon/bin/php/php-8.5.11-Win32-vs17-x64/php.exe';
void(async()=>{const q=await start('workflow-upgrade-document-pipeline-chromium-20261011');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){const out=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,target:r.headers.get('X-Investmoz-QA-Target'),data:await r.json()};},{url,data});assert.equal(out.target,TARGET);return out;}
async function binary(page,url){const r=await page.evaluate(async url=>{const r=await fetch(url,{headers:{Accept:'application/json'}}),b=await r.arrayBuffer(),h=await crypto.subtle.digest('SHA-256',b);return {status:r.status,target:r.headers.get('X-Investmoz-QA-Target'),hash:[...new Uint8Array(h)].map(v=>v.toString(16).padStart(2,'0')).join(''),cache:r.headers.get('cache-control')};},url);assert.equal(r.target,TARGET);return r;}
function inspect(id){return JSON.parse(cp.execFileSync(PHP,[path.join(__dirname,'portal-workflow-upgrade-inspect.php'),String(id)],{timeout:60000,encoding:'utf8'}));}
const fixture=JSON.parse(fs.readFileSync(path.join(DIR,'workflow-upgrade-fixtures-private.json'),'utf8'));
const first=await actor('super_first'),second=await actor('super_second'),promoter=await actor('promoter');
q.phase('Documented synthetic Promotor request, independent approval and explicit scoped review/publication grants');
let request=q.prior.profile_request_id;
if(!request){const r=await post(promoter,'/perfil/promotor',{creation_key:crypto.randomUUID(),representation_type:'individual',organization_id:null,opportunity_types:['real_estate'],document_ids:[fixture.profile_document_id],reason:'SIMULAÇÃO QA: pedido de Promotor individual para testar anexos no clone do destino, sem identidade ou representação real.'});assert.equal(r.status,201);request=r.data.id;q.proof.profile_request_id=request;q.save();
const approved=await post(first,'/perfis/pedidos/'+request+'/decisao',{version:1,decision:'approved',verified_identity:true,verified_representation:true,verified_documents:true,approved_types:['real_estate'],reason:'Revisão independente de comprovativo explicitamente fictício neste clone QA; não corresponde a decisão sobre pessoa real.'});assert.equal(approved.status,200);}else q.proof.profile_request_id=request;
for(const role of ['validator','publisher']){if(!(q.prior.grants||{})[role]){const r=await post(first,'/perfis/atribuicoes',{user_id:actors[role].id,role,organization_id:null,opportunity_types:['real_estate'],responsibilities:[],reason:'SIMULAÇÃO QA: âmbito funcional explícito para verificar recuperação documental sem alterar permissões reais.'});assert.equal(r.status,201);(q.proof.grants||={})[role]=r.data.id;q.save();}else(q.proof.grants||={})[role]=q.prior.grants[role];}
q.phase('Create private synthetic draft, upload through the same browser/server and verify durable queue/quarantine');
let id=q.prior.opportunity_id,etag;
if(!id){const r=await post(promoter,'/minhas-oportunidades/drafts',{creation_key:crypto.randomUUID(),opportunity_category:'real_estate',title:'SIMULAÇÃO QA — processamento documental do clone do destino',reason:'Rascunho incompleto sintético para ensaio documental; sem publicação ou efeito jurídico.'});assert.equal(r.status,201);id=r.data.id;etag=r.data.etag;q.proof.opportunity_id=id;q.save();}else{id=q.prior.opportunity_id;q.proof.opportunity_id=id;}
async function props(){const r=await q.open(promoter,`/minhas-oportunidades/${id}/resources`);assert.equal(r.status(),200);const t=await r.text(),enc=t.match(/data-page="([^"]+)"/);return enc&&enc[1].startsWith('{')?JSON.parse(enc[1].replace(/&quot;/g,'"').replace(/&amp;/g,'&').replace(/&#039;/g,"'")).props:JSON.parse(t.match(/<script[^>]*type="application\/json"[^>]*>([\s\S]*?)<\/script>/)[1]).props;}
async function upload(kind,name,type,bytes,payload){const p=await props();const r=await promoter.evaluate(async({url,etag,name,type,base64,payload})=>{const f=new FormData();f.set('etag',etag);f.set('creation_key',crypto.randomUUID());f.set('reason','SIMULAÇÃO QA: preservar original privado e ensaiar fila real, antimalware e recuperação autorizada.');f.set('payload',JSON.stringify(payload));f.set('file',new File([Uint8Array.from(atob(base64),c=>c.charCodeAt(0))],name,{type}));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','X-XSRF-TOKEN':token},body:f});return {status:r.status,data:await r.json()};},{url:`/minhas-oportunidades/${id}/resources/${kind}`,etag:p.etag,name,type,base64:bytes.toString('base64'),payload});assert.equal(r.status,201);return r.data.id;}
let doc=q.prior.document_resource_id,img=q.prior.image_resource_id;
if(!doc){doc=await upload('documents','SIMULACAO-QA-documento.pdf','application/pdf',Buffer.from(fixture.document_bytes_base64,'base64'),{title:'Comprovativo fictício QA',classification:'restricted',visibility:'internal',source_reference:'Teste sintético; nenhum ato jurídico real.'});q.proof.document_resource_id=doc;q.save();}else q.proof.document_resource_id=doc;
if(!img){img=await upload('images','SIMULACAO-QA-imagem.png','image/png',fs.readFileSync(fixture.image_path),{title:'Imagem sintética QA',alt_text:'Imagem de ensaio QA com texto que identifica a simulação.',rights_status:'owned',visibility:'internal',role:'evidence'});q.proof.image_resource_id=img;q.save();}else q.proof.image_resource_id=img;
const url=`/minhas-oportunidades/${id}/resources/documents/${doc}/download`,imgurl=`/minhas-oportunidades/${id}/resources/images/${img}/download`;
if(!q.prior.worker_fault_injected){const state=inspect(id);assert(state.scanner.available&&state.scanner_port===13310&&state.GD_WebP);assert.equal(state.queue_pending,2);assert(state.rows.every(r=>r.malware_scan_status==='pending'));assert.equal((await binary(promoter,url)).status,423);q.proof.before_worker=state;
try{cp.execFileSync(PHP,[path.join(__dirname,'portal-workflow-upgrade-worker.php'),'--fault'],{timeout:60000,stdio:['ignore','pipe','pipe']});assert.fail('Fault worker must terminate nonzero');}catch(e){assert.equal(e.status,70);}
const fault=inspect(id);assert.equal(fault.queue_pending,2);assert.equal(fault.queue_reserved,1);assert.equal((await binary(promoter,url)).status,423);q.proof.worker_fault_injected=true;q.proof.after_worker_fault=fault;q.proof.checks.worker_failure_keeps_queue_and_quarantine=true;q.save();
const out=fs.openSync(path.join(DIR,'workflow-upgrade-worker-recovery.log'),'a');const worker=cp.spawn(PHP,[path.join(__dirname,'portal-workflow-upgrade-worker.php'),'--continuous'],{detached:true,windowsHide:true,stdio:['ignore',out,out]});worker.unref();q.proof.recovery_worker_pid=worker.pid;q.save();}
q.phase('Recover the same reserved job after the real 210-second lease; no resubmission or duplicate original');
let state=inspect(id);const end=Date.now()+270000;while(!state.rows.every(r=>r.malware_scan_status==='clean')&&Date.now()<end){await promoter.waitForTimeout(12000);state=inspect(id);}assert(state.rows.every(r=>r.malware_scan_status==='clean'));assert.equal(state.rows.length,2);assert.equal(state.queue_pending,0);assert(state.rows.every(r=>r.plaintext_hash_matches));q.proof.after_recovery=state;for(const r of state.rows)assert.equal(r.original_cipher_sha256,q.proof.before_worker.rows.find(v=>v.document_id===r.document_id).original_cipher_sha256);q.proof.checks.original_cipher_preserved_during_failure_and_scan=true;
assert.equal((await binary(promoter,url)).hash,fixture.profile_document_sha256);assert.equal((await binary(promoter,imgurl)).hash,fixture.image_sha256);q.proof.checks.upload_queue_real_scan_recovery_original_bytes=true;
q.phase('Generate private derivatives, scan each stream independently, retry generation without duplicates and display');
let p=await props(),image=p.resources.images.find(r=>r.id===img);assert(image);
let r=await post(promoter,`/minhas-oportunidades/${id}/resources/images/${img}/prepare`,{etag:p.etag,version:image.version,reason:'SIMULAÇÃO QA: derivadas do mesmo original, preservação de EXIF/original e scan independente.'});assert.equal(r.status,200);
state=inspect(id);const initial=state.rows.find(r=>r.kind==='images');assert.equal(initial.derivatives.length,3);q.proof.derivative_ids=initial.derivatives.map(d=>d.document_id);q.save();
const scanEnd=Date.now()+90000;while(!state.rows.find(r=>r.kind==='images').derivatives.every(d=>d.malware_scan_status==='clean')&&Date.now()<scanEnd){await promoter.waitForTimeout(2000);state=inspect(id);}assert(state.rows.find(r=>r.kind==='images').derivatives.every(d=>d.malware_scan_status==='clean'));
p=await props();image=p.resources.images.find(r=>r.id===img);const retry=await post(promoter,`/minhas-oportunidades/${id}/resources/images/${img}/prepare`,{etag:p.etag,version:image.version,reason:'Repetição QA: recuperar as mesmas derivadas, sem novos documentos, decisões ou versões.'});assert.equal(retry.status,200);const repeated=inspect(id);assert.deepEqual(repeated.rows.find(r=>r.kind==='images'),state.rows.find(r=>r.kind==='images'));assert.equal(repeated.queue_pending,0);q.proof.checks.derivatives_idempotent_and_original_cipher_preserved=true;
await props();await promoter.getByRole('button',{name:'Imagens',exact:true}).click();const preview=promoter.locator('img[src*="/resources/images/"]').first();await preview.waitFor();const signed=await preview.getAttribute('src');assert.equal((await binary(promoter,signed)).status,200);q.proof.signed_preview_path=new URL(signed).pathname;q.proof.signed_query_omitted=true;await q.shot(promoter,'clone-scanned-private-derivatives-visible',false);q.proof.checks.visible_original_scan_and_derivative_preview=true;
for(const role of ['validator','publisher']){const a=await actor(role);assert.equal((await binary(a,url)).hash,fixture.profile_document_sha256);assert.equal((await binary(a,signed)).status,200);}q.proof.checks.scoped_review_and_publication_profiles_recover=true;
const outsider=await actor('outsider');assert.equal((await binary(outsider,url)).status,403);q.proof.checks.outside_profile_denied=true;
q.phase('Superuser administration alone must not expose annexes; scope and independent temporary elevation required');
const generic=`/management/documents/${state.rows.find(r=>r.kind==='documents').document_id}/download`;
assert.equal((await binary(first,url)).status,403);assert.equal((await binary(first,generic)).status,403);assert.equal((await q.open(first,`/minhas-oportunidades/${id}/resources`)).status(),403);
assert.equal((await post(first,'/perfis/elevacoes',{action:'view',record_id:id,reason:'SIMULAÇÃO QA: pedido sem papel deve ser recusado antes da aprovação independente.'})).status,403);q.proof.checks.superuser_without_scope_denied=true;
const grant=await post(second,'/perfis/atribuicoes',{user_id:actors.super_first.id,role:'validator',organization_id:null,opportunity_types:['real_estate'],responsibilities:[],reason:'SIMULAÇÃO QA: conceder âmbito funcional ao titular sintético; não autoriza consulta sem elevação.'});assert.equal(grant.status,201);q.proof.superuser_read_grant_id=grant.data.id;q.save();assert.equal((await binary(first,generic)).status,403);
const elev=await post(first,'/perfis/elevacoes',{action:'view',record_id:id,reason:'SIMULAÇÃO QA: consulta temporária apenas deste dossier e anexos; aprovação por outro titular.'});assert.equal(elev.status,201);q.proof.elevation_id=elev.data.id;q.save();assert.equal((await post(first,'/perfis/seguranca/'+elev.data.id+'/aprovar',{})).status,403);assert.equal((await binary(first,url)).status,403);
assert.equal((await post(second,'/perfis/seguranca/'+elev.data.id+'/aprovar',{})).status,200);assert.equal((await binary(first,url)).hash,fixture.profile_document_sha256);assert.equal((await binary(first,generic)).hash,fixture.profile_document_sha256);assert.equal((await binary(first,signed)).status,200);q.proof.checks.independent_scoped_temporary_read_succeeds=true;
const other=await post(promoter,'/minhas-oportunidades/drafts',{creation_key:crypto.randomUUID(),opportunity_category:'real_estate',title:'SIMULAÇÃO QA — controlo negativo de âmbito da elevação',reason:'Segundo rascunho apenas para verificar que a elevação do primeiro não concede consulta universal.'});assert.equal(other.status,201);q.proof.other_opportunity_id=other.data.id;assert.equal((await q.open(first,`/minhas-oportunidades/${other.data.id}/resources`)).status(),403);q.proof.checks.elevation_does_not_authorize_other_dossier=true;
assert.equal((await post(second,'/perfis/atribuicoes/'+grant.data.id+'/revogar',{reason:'SIMULAÇÃO QA: revogar âmbito após prova, invalidar sessões e verificar que o URL antigo deixa de funcionar.'})).status,200);assert.notEqual((await binary(first,generic)).hash,fixture.profile_document_sha256);const fresh=await actor('super_first');assert.equal((await binary(fresh,generic)).status,403);q.proof.checks.scope_revocation_invalidates_old_and_new_session=true;
q.proof.final_document_state=inspect(id);q.proof.result='passed';q.proof.AB_C37_not_repeated=true;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 ctx of contexts)await ctx.close();await q.browser.close();}})();