48 lines
6.9 KiB
JavaScript
48 lines
6.9 KiB
JavaScript
// Browser QA helpers: local credentials are read only; receipts omit cookies and auth bodies.
|
|
const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict'), crypto = require('node:crypto');
|
|
const { chromium } = require('C:/Users/macbook/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
|
|
const ROOT = path.resolve(__dirname, '..'), DIR = path.join(ROOT, 'storage/app/private/investmoz-v2/portal-audit');
|
|
const BASE = 'http://127.0.0.1:18097', TARGET = 'desk_restore_20261009_174726_test:55441';
|
|
const actors = JSON.parse(fs.readFileSync(path.join(DIR, 'profiles-v1-actors-private.json'), 'utf8'));
|
|
actors.investor18 = JSON.parse(fs.readFileSync(path.join(DIR, 'investor-mfa-qa-private.json'), 'utf8'));
|
|
actors.manager = JSON.parse(fs.readFileSync(path.join(DIR, 'resource-http-actors-private.json'), 'utf8')).manager;
|
|
function otp(secret) {
|
|
let bits = 0, value = 0; const bytes = [], alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
for (const char of secret.toUpperCase().replace(/=+$/, '')) { const digit = alphabet.indexOf(char); assert(digit >= 0); value = (value << 5) | digit; bits += 5; if (bits >= 8) { bytes.push((value >>> (bits - 8)) & 255); bits -= 8; } }
|
|
const counter = Buffer.alloc(8); counter.writeBigUInt64BE(BigInt(Math.floor(Date.now() / 30000)));
|
|
const digest = crypto.createHmac('sha1', Buffer.from(bytes)).update(counter).digest(), offset = digest[19] & 15;
|
|
return String((digest.readUInt32BE(offset) & 0x7fffffff) % 1000000).padStart(6, '0');
|
|
}
|
|
function sanitized(error) { let message = String(error.message || error); for (const a of Object.values(actors)) for (const k of ['password','totp_secret','secret']) if (a[k]) message = message.replaceAll(a[k], '[REDACTED]'); return message.replace(/\b\d{6}\b/g,'[OTP OMITIDO]').slice(0, 1800); }
|
|
async function start(name) {
|
|
const receipt = path.join(DIR, name + '.json'), stamp = new Date().toISOString().replace(/[:.]/g,'-');
|
|
const prior = fs.existsSync(receipt) ? JSON.parse(fs.readFileSync(receipt,'utf8')) : {};
|
|
if (fs.existsSync(receipt)) fs.copyFileSync(receipt, path.join(DIR, name + '-attempt-' + stamp + '.json'));
|
|
const proof = { started_at: new Date().toISOString(), base:BASE, target:TARGET, browser:'Chromium 1243, isolated contexts', active_writes:0, checks:{}, events:[], screenshots:[] };
|
|
const shots = path.join(DIR,name+'-'+stamp); fs.mkdirSync(shots,{recursive:true});
|
|
const browser = await chromium.launch({headless:true, executablePath:'C:/Users/macbook/AppData/Local/ms-playwright/chromium-1243/chrome-win64/chrome.exe'});
|
|
const assets = new Map(); // Only immutable public QA build assets; never cache authenticated pages/API.
|
|
function save() { fs.writeFileSync(receipt, JSON.stringify(proof,null,2)); }
|
|
function phase(value) { proof.phase=value;save();console.log(JSON.stringify({phase:value})); }
|
|
async function context(role, width=1440) {
|
|
const ctx=await browser.newContext({viewport:{width,height:1000},locale:'pt-MZ',reducedMotion:'reduce'});ctx.qaConfirmed=false;
|
|
await ctx.route('**/*',async route=>{ const req=route.request(),u=new URL(req.url()); if(u.origin!==BASE)return route.abort(); if(!['GET','HEAD'].includes(req.method()))assert(ctx.qaConfirmed,'QA target must be confirmed before mutation');if(req.method()==='GET'&&u.pathname.startsWith('/build-qa-opportunities/assets/')){if(!assets.has(u.href)){const r=await route.fetch();assets.set(u.href,{status:r.status(),headers:r.headers(),body:await r.body()});}return route.fulfill(assets.get(u.href));}return route.continue(); });
|
|
ctx.on('response',r=>{const u=new URL(r.url());if(u.origin===BASE&&!u.pathname.startsWith('/build-qa-')){const h=r.headers();proof.events.push({actor:role,method:r.request().method(),path:u.pathname,status:r.status(),target:h['x-investmoz-qa-target']||null,redirect:h.location?new URL(h.location,BASE).pathname:null});save();}});
|
|
const page=await ctx.newPage();page.setDefaultTimeout(25000);page.setDefaultNavigationTimeout(45000);
|
|
page.on('pageerror',e=>{(proof.javascript_errors||=[]).push(sanitized(e));save();});
|
|
return {ctx,page};
|
|
}
|
|
async function open(page,url){const r=await page.goto(BASE+url,{waitUntil:'domcontentloaded'});assert.equal(r.headers()['x-investmoz-qa-target'],TARGET);page.context().qaConfirmed=true;return r;}
|
|
async function login(page,role){const a=actors[role];assert(a,'Known local QA actor');await open(page,'/login');await page.locator('#email').fill(a.email);await page.locator('#password').fill(a.password);await page.locator('[data-test="login-button"]').click();await page.waitForURL(u=>u.pathname!=='/login');
|
|
if(new URL(page.url()).pathname==='/two-factor-challenge'){await page.locator('input[name="code"]').fill(otp(a.totp_secret));await page.getByRole('button',{name:'Continue',exact:true}).click();await page.waitForURL(u=>!['/login','/two-factor-challenge'].includes(u.pathname));}await page.locator('main').first().waitFor();
|
|
const response=await open(page,'/dashboard');const html=await response.text();const encoded=html.match(/data-page="([^"]+)"/);let body;
|
|
if(encoded&&encoded[1].startsWith('{')){body=JSON.parse(encoded[1].replace(/"/g,'"').replace(/&/g,'&').replace(/'/g,"'"));}
|
|
else{const script=html.match(/<script[^>]*type="application\/json"[^>]*>([\s\S]*?)<\/script>/);assert(script,'Inertia bootstrap must identify authenticated actor');body=JSON.parse(script[1]);}
|
|
const actual=body.props.auth.user.id;assert.equal(actual,a.id||a.actor_id);(proof.authenticated_actors||=[]).push({role,id:actual});save();}
|
|
async function logout(page){await open(page,'/dashboard');if(page.viewportSize().width<768){const toggle=page.getByRole('button',{name:/Toggle sidebar/i});if(await toggle.isVisible())await toggle.click();}const buttons=page.locator('button[aria-haspopup="menu"]');for(let i=await buttons.count()-1;i>=0;i--){if(!await buttons.nth(i).isVisible())continue;await buttons.nth(i).click();const action=page.locator('[data-test="logout-button"]');if(await action.isVisible()){await action.click();await page.waitForURL(BASE+'/');return;}await page.keyboard.press('Escape');}throw Error('Visible logout missing');}
|
|
async function shot(page,name,fullPage=true){assert(!['/login','/two-factor-challenge'].includes(new URL(page.url()).pathname),'No authentication screenshots');const file=path.join(shots,name+'.png');await page.screenshot({path:file,fullPage});proof.screenshots.push(path.relative(ROOT,file).replaceAll('\\','/'));save();}
|
|
async function json(page,url){const r=await page.request.get(BASE+url,{headers:{Accept:'application/json'}});assert.equal(r.headers()['x-investmoz-qa-target'],TARGET);return {status:r.status(),data:await r.json()};}
|
|
return {proof,prior,save,phase,browser,context,open,login,logout,shot,json,stamp,receipt};
|
|
}
|
|
module.exports={start,BASE,TARGET,DIR,actors,assert,fs,path,sanitized};
|