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

476 lines
17 KiB
JavaScript

// Actual desktop Chromium UI on guarded QA. No QA18 credentials, cookies, or MFA values in evidence.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const 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, '..'),
PRIVATE = 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(PRIVATE, 'profiles-v1-actors-private.json'),
'utf8',
),
);
assert.equal(actors.promoter.id, 21);
const receipt = path.join(PRIVATE, 'legacy-sectors-browser-qa.json');
const old = fs.existsSync(receipt)
? JSON.parse(fs.readFileSync(receipt, 'utf8'))
: {};
if (old.started_at)
fs.copyFileSync(
receipt,
path.join(
PRIVATE,
'legacy-sectors-browser-attempt-' +
old.started_at.replaceAll(':', '-') +
'.json',
),
);
const stamp = new Date().toISOString().replace(/[:.]/g, '-'),
shots = path.join(PRIVATE, 'legacy-sectors-browser-' + stamp);
fs.mkdirSync(shots, { recursive: true });
const proof = {
started_at: new Date().toISOString(),
base: BASE,
target: TARGET,
browser: 'Chromium 1243, isolated headless desktop contexts',
interface_observed: true,
actor_id: 21,
active_writes: 0,
dossiers: old.dossiers || {},
checks: {},
screenshots: [],
events: [],
phase: 'initialization',
};
let browser;
function save() {
fs.writeFileSync(receipt, JSON.stringify(proof, null, 2));
}
function phase(value) {
proof.phase = value;
save();
console.log(JSON.stringify({ phase: value }));
}
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');
}
async function context() {
const ctx = await browser.newContext({
viewport: { width: 1440, height: 1000 },
locale: 'pt-MZ',
reducedMotion: 'reduce',
});
ctx.qaConfirmed = false;
await ctx.route('**/*', async (route) => {
const request = route.request(),
url = new URL(request.url());
if (url.origin !== BASE) return route.abort();
if (!['GET', 'HEAD'].includes(request.method()))
assert(ctx.qaConfirmed, 'Actual QA guard precedes writes');
await route.continue();
});
ctx.on('response', (response) => {
const url = new URL(response.url());
if (url.origin === BASE && !url.pathname.startsWith('/build-qa-')) {
const headers = response.headers();
proof.events.push({
method: response.request().method(),
path: url.pathname,
status: response.status(),
target: headers['x-investmoz-qa-target'] || null,
redirect_path: headers.location
? new URL(headers.location, BASE).pathname
: null,
});
save();
}
});
ctx.on('requestfailed', (request) => {
const url = new URL(request.url());
proof.events.push({
request_failed: url.pathname,
reason: request.failure()?.errorText,
});
save();
});
return ctx;
}
async function open(page, url) {
const response = await page.goto(BASE + url, {
waitUntil: 'domcontentloaded',
timeout: 45000,
});
assert.equal(response.headers()['x-investmoz-qa-target'], TARGET);
page.context().qaConfirmed = true;
return response;
}
async function shot(page, name, fullPage = true) {
const file = path.join(shots, name + '.png');
await page.screenshot({ path: file, fullPage });
proof.screenshots.push(path.relative(ROOT, file).replaceAll('\\', '/'));
}
async function login(page, role) {
const account = actors[role];
if (new URL(page.url()).pathname !== '/login') await open(page, '/login');
await page.locator('#email').fill(account.email);
await page.locator('#password').fill(account.password);
await page.locator('[data-test="login-button"]').click();
await page.waitForURL(BASE + '/two-factor-challenge');
await page.locator('input[name="code"]').fill(otp(account.totp_secret));
await page.getByRole('button', { name: 'Continue', exact: true }).click();
await page.waitForURL(
(url) => !['/login', '/two-factor-challenge'].includes(url.pathname),
);
await page.waitForLoadState('networkidle');
}
async function logout(page) {
await open(page, '/dashboard');
const triggers = page.locator('button[aria-haspopup="menu"]');
for (let i = (await triggers.count()) - 1; i >= 0; i--) {
await triggers.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 new Error('Visible logout missing');
}
async function dossier(page, entry) {
await page.waitForURL(BASE + '/investimentos/' + entry.id);
await page
.getByRole('heading', { name: entry.title, exact: true })
.waitFor();
assert.equal(
await page.getByRole('heading', { name: /^Fase \d+ ·/ }).count(),
5,
);
assert.equal(await page.locator('article[id^="S"]').count(), 19);
const refs = page.getByRole('region', { name: 'Fontes da ideia' });
assert.equal(await refs.locator('li').count(), 1);
assert.equal(
new URL(await refs.locator('a').first().getAttribute('href'), BASE)
.pathname,
entry.href,
);
await page
.getByRole('heading', { name: 'Próxima acção', exact: true })
.waitFor();
}
(async () => {
browser = await chromium.launch({
headless: true,
executablePath:
'C:/Users/macbook/AppData/Local/ms-playwright/chromium-1243/chrome-win64/chrome.exe',
});
phase('public sector pages, images and search');
const ctx = await context(),
page = await ctx.newPage();
page.setDefaultTimeout(15000);
for (const [group, expected] of [
['energia-mineracao', 3],
['turismo', 12],
['infraestruturas', 1],
]) {
assert.equal((await open(page, '/explorar/' + group)).status(), 200);
assert.equal(
await page
.getByRole('link', { name: 'Ler o artigo', exact: true })
.count(),
expected,
);
const image = page.locator('main img');
await image.waitFor();
await image.evaluate((img) => img.decode());
assert(
await image.evaluate((img) => img.complete && img.naturalWidth > 0),
);
assert(
await page.evaluate(
() => document.documentElement.scrollWidth <= innerWidth,
),
);
await shot(page, 'public-' + group);
const articleLinks = await page
.getByRole('link', { name: 'Ler o artigo', exact: true })
.evaluateAll((links) => links.map((a) => a.getAttribute('href')));
for (const href of articleLinks) {
await open(page, href);
const body = page.getByRole('region', { name: 'Artigo educativo' });
await body.waitFor();
assert((await body.innerText()).trim().length > 0);
await page.locator('main img').evaluate((img) => img.decode());
assert(
await page
.locator('main img')
.evaluate((img) => img.complete && img.naturalWidth > 0),
);
}
await open(page, '/explorar/' + group);
await page.getByRole('searchbox').fill('zzzz-no-content-qa');
await page
.getByRole('button', { name: 'Pesquisar', exact: true })
.click();
await page
.getByText('Nenhum conteúdo corresponde à pesquisa.', {
exact: true,
})
.waitFor();
assert.equal(
await page
.getByRole('link', { name: 'Ler o artigo', exact: true })
.count(),
0,
);
}
await open(page, '/explorar/energia-mineracao');
await page.getByRole('searchbox').fill('Energia');
await page.getByRole('button', { name: 'Pesquisar', exact: true }).click();
await page.locator('h3 a[href="/energia"]').waitFor();
await open(page, '/energia-mineracao');
const body = page.getByRole('region', { name: 'Artigo educativo' });
await body.locator('a[href="/energia"]').click();
await page.waitForURL(BASE + '/energia');
await open(page, '/energia-mineracao');
await body.locator('a[href="/mineracao"]').click();
await page.waitForURL(BASE + '/mineracao');
await open(page, '/infraestruturas');
await page
.getByRole('region', { name: 'Artigo educativo' })
.getByRole('heading', {
name: '2. Sectores Prioritários de Investimento',
exact: true,
})
.waitFor();
await shot(page, 'infra-original-heading');
assert.equal((await open(page, '/mercado-financeiro')).status(), 404);
proof.checks.public_read_search_images_distinct_articles = true;
save();
phase('public CTA, QA21 normal MFA, UI creation or continuation');
for (const href of [
'/energia',
'/mineracao',
'/turismo',
'/infraestruturas',
]) {
await open(page, href);
await page
.getByRole('link', {
name: 'Iniciar ou continuar uma ideia',
exact: true,
})
.click();
await page.waitForURL((url) =>
['/login', '/investimentos/a-partir-de' + href].includes(
url.pathname,
),
);
if (new URL(page.url()).pathname === '/login')
await login(page, 'promoter');
await page.waitForURL(BASE + '/investimentos/a-partir-de' + href);
const entry = (proof.dossiers[href] ||= {
title: 'QA visual — ideia ' + href.slice(1) + ' ' + stamp,
href,
});
if (!entry.id) {
await page.getByLabel('Título', { exact: true }).fill(entry.title);
await page
.getByLabel(
'Autorizo reutilizar o meu perfil neste dossier privado.',
)
.check();
await page
.getByRole('button', {
name: 'Criar e guardar dossier',
exact: true,
})
.click();
await page.waitForURL(/\/investimentos\/[0-9a-f-]{36}$/);
entry.id = new URL(page.url()).pathname.split('/').pop();
entry.created_via_interface_at = new Date().toISOString();
save();
} else {
const card = page.getByRole('listitem').filter({
has: page.getByRole('link', {
name: entry.title,
exact: true,
}),
});
await card
.getByRole('button', {
name: 'Continuar neste dossier com o artigo',
exact: true,
})
.click();
}
await dossier(page, entry);
await shot(page, 'dossier-' + href.slice(1), false);
await page
.getByRole('region', { name: 'Fontes da ideia' })
.locator('a')
.first()
.click();
await page.waitForURL(
(url) =>
url.pathname === href &&
url.searchParams.get('context') === entry.id,
);
await page
.getByRole('link', {
name: 'Continuar: ' + entry.title,
exact: true,
})
.click();
await dossier(page, entry);
entry.source_return_to_same_dossier = true;
save();
}
phase('visible logout, new MFA browser session, recovery and refresh');
await logout(page);
await ctx.close();
const freshCtx = await context(),
fresh = await freshCtx.newPage();
const first = Object.values(proof.dossiers)[0];
await open(fresh, '/investimentos/' + first.id);
await fresh.waitForURL(BASE + '/login');
await login(fresh, 'promoter');
for (const entry of Object.values(proof.dossiers)) {
await open(fresh, '/investimentos/' + entry.id);
await dossier(fresh, entry);
await fresh.reload({ waitUntil: 'networkidle' });
await dossier(fresh, entry);
await open(fresh, '/investimentos/a-partir-de' + entry.href);
const card = fresh.getByRole('listitem').filter({
has: fresh.getByRole('link', {
name: entry.title,
exact: true,
}),
});
await card
.getByRole('button', {
name: 'Continuar neste dossier com o artigo',
exact: true,
})
.click();
await dossier(fresh, entry);
entry.new_session_recovered_and_refreshed = true;
entry.single_reference_after_repetition = true;
save();
}
phase('other profile denies dossiers, public context remains private');
const otherCtx = await context(),
other = await otherCtx.newPage();
await open(other, '/login');
await login(other, 'outsider');
const publicCtx = await context(),
publicPage = await publicCtx.newPage();
for (const entry of Object.values(proof.dossiers)) {
assert.equal(
(await open(other, '/investimentos/' + entry.id)).status(),
403,
);
for (const reader of [other, publicPage]) {
await open(reader, entry.href + '?context=' + entry.id);
await reader
.getByRole('region', { name: 'Artigo educativo' })
.waitFor();
assert.equal(
await reader.getByText(entry.title, { exact: false }).count(),
0,
);
}
entry.other_actor_id = actors.outsider.id;
entry.denied_status = 403;
entry.private_context_hidden = true;
}
await logout(fresh);
await freshCtx.close();
await otherCtx.close();
await publicCtx.close();
proof.result = 'passed';
proof.completed_at = new Date().toISOString();
save();
console.log(
JSON.stringify({
result: proof.result,
dossiers: Object.keys(proof.dossiers).length,
screenshots: proof.screenshots.length,
interface_observed: true,
active_writes: 0,
}),
);
})()
.catch(async (error) => {
proof.result = 'failed';
proof.error_type = error.name;
let message = error.message;
for (const actor of Object.values(actors))
for (const key of ['password', 'totp_secret', 'secret'])
if (actor[key])
message = message.replaceAll(actor[key], '[REDACTED]');
proof.error = message
.replace(/\b\d{6}\b/g, '[MFA OMITIDO]')
.slice(0, 1600);
proof.page_states = browser
? await Promise.all(
browser
.contexts()
.flatMap((ctx) => ctx.pages())
.map(async (page) => ({
path: new URL(page.url()).pathname,
headings: await page
.locator('h1')
.allTextContents()
.catch(() => []),
alerts: await page
.getByRole('alert')
.count()
.catch(() => 0),
})),
)
: [];
save();
console.error(
JSON.stringify({
result: 'failed',
phase: proof.phase,
type: error.name,
}),
);
process.exitCode = 1;
})
.finally(async () => {
if (browser) await browser.close();
});