503 lines
20 KiB
JavaScript
503 lines
20 KiB
JavaScript
const {
|
|
start,
|
|
BASE,
|
|
DIR,
|
|
assert,
|
|
fs,
|
|
path,
|
|
sanitized,
|
|
} = require('./portal-visual-qa-lib.cjs');
|
|
const crypto = require('node:crypto');
|
|
const mode = process.argv[2];
|
|
void (async () => {
|
|
const q = await start('next-' + mode + '-chromium-20261010');
|
|
let page, ctx;
|
|
async function open(route) {
|
|
const r = await q.open(page, route);
|
|
const html = await r.text();
|
|
const script = html.match(
|
|
/<script[^>]*type="application\/json"[^>]*>([\s\S]*?)<\/script>/,
|
|
);
|
|
const attr = html.match(/data-page="([^"]+)"/);
|
|
const raw =
|
|
attr && attr[1].startsWith('{')
|
|
? attr[1]
|
|
.replace(/"/g, '"')
|
|
.replace(/&/g, '&')
|
|
.replace(/'/g, "'")
|
|
: script?.[1];
|
|
assert(raw, 'Inertia props');
|
|
return JSON.parse(raw).props;
|
|
}
|
|
async function actor(role) {
|
|
if (ctx) await ctx.close();
|
|
({ ctx, page } = await q.context(role));
|
|
await q.login(page, role);
|
|
}
|
|
async function mutation(url, data, method = 'POST') {
|
|
return page.evaluate(
|
|
async ({ url, data, method }) => {
|
|
const c = document.cookie
|
|
.split('; ')
|
|
.find((c) => c.startsWith('XSRF-TOKEN='));
|
|
const r = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'Content-Type': 'application/json',
|
|
'X-XSRF-TOKEN': c
|
|
? decodeURIComponent(c.slice(11))
|
|
: '',
|
|
},
|
|
...(method === 'GET' || method === 'HEAD'
|
|
? {}
|
|
: { body: JSON.stringify(data) }),
|
|
});
|
|
return { status: r.status, data: await r.json() };
|
|
},
|
|
{ url, data, method },
|
|
);
|
|
}
|
|
async function save(button, url, method) {
|
|
const wait = page.waitForResponse(
|
|
(r) =>
|
|
new URL(r.url()).pathname === url &&
|
|
r.request().method() === method,
|
|
);
|
|
await button.click();
|
|
const r = await wait;
|
|
assert([200, 201, 302, 303].includes(r.status()));
|
|
await page.waitForLoadState('networkidle');
|
|
return r.status();
|
|
}
|
|
try {
|
|
if (mode === 'organization') {
|
|
q.proof.fixture = q.prior.fixture || {};
|
|
const f = q.proof.fixture;
|
|
await actor('promoter');
|
|
let p = await open('/investidor/profile');
|
|
assert(p.organizationCountryAvailable);
|
|
const org = p.organizations.find(
|
|
(o) => o.id === '99964753-2632-4658-a687-8ebc6a178296',
|
|
);
|
|
assert(org?.canEdit);
|
|
f.organization = org.id;
|
|
const name = 'QA reconciliação país mestre — cópia explícita';
|
|
let company = p.companies.find((c) => c.payload.name === name);
|
|
if (!company) {
|
|
const box = page.locator('section').filter({
|
|
has: page.getByRole('heading', {
|
|
name: 'Acrescentar empresa',
|
|
exact: true,
|
|
}),
|
|
});
|
|
await box
|
|
.getByLabel('Nome da empresa', { exact: true })
|
|
.fill(name);
|
|
await box
|
|
.locator('select[id^="incorporation_country_code"]')
|
|
.selectOption('MZ');
|
|
await box
|
|
.getByLabel('Função ou relação com a empresa', {
|
|
exact: true,
|
|
})
|
|
.fill('Declaração sintética QA, sem mandato real');
|
|
await save(
|
|
box.getByRole('button', {
|
|
name: 'Guardar declaração da empresa',
|
|
exact: true,
|
|
}),
|
|
'/investidor/companies',
|
|
'POST',
|
|
);
|
|
p = await open('/investidor/profile');
|
|
company = p.companies.find((c) => c.payload.name === name);
|
|
}
|
|
f.company = company.id;
|
|
q.save();
|
|
if (!f.old_submission) {
|
|
const section = page.locator('section').filter({
|
|
has: page.getByRole('heading', { name, exact: true }),
|
|
});
|
|
await save(
|
|
section.getByRole('button', {
|
|
name: 'Preparar manifestação desta empresa',
|
|
exact: true,
|
|
}),
|
|
'/investidor/manifestacoes',
|
|
'POST',
|
|
);
|
|
await page.waitForURL((u) =>
|
|
/^\/investidor\/manifestacoes\/.+/.test(u.pathname),
|
|
);
|
|
f.old_submission = new URL(page.url()).pathname
|
|
.split('/')
|
|
.pop();
|
|
q.save();
|
|
}
|
|
let old = await open(
|
|
'/investidor/manifestacoes/' + f.old_submission,
|
|
);
|
|
assert.equal(
|
|
old.submission.payload.incorporation_country_code,
|
|
'MZ',
|
|
);
|
|
f.old_version = old.submission.version;
|
|
q.save();
|
|
p = await open('/investidor/profile');
|
|
const section = page.getByRole('region', {
|
|
name: 'Cadastro mestre: Synthetic QA organization',
|
|
exact: true,
|
|
});
|
|
if (!f.master_saved) {
|
|
q.phase(
|
|
'C97: master country with explicit source, validation error and keyboard focus',
|
|
);
|
|
await section
|
|
.getByRole('combobox', {
|
|
name: 'País de constituição da organização',
|
|
exact: true,
|
|
})
|
|
.selectOption('');
|
|
await section
|
|
.getByLabel('Fonte ou referência deste país', {
|
|
exact: true,
|
|
})
|
|
.fill('');
|
|
await save(
|
|
section.getByRole('button', {
|
|
name: 'Guardar país no cadastro mestre',
|
|
exact: true,
|
|
}),
|
|
'/investidor/organizations/' + org.id + '/country',
|
|
'PUT',
|
|
);
|
|
const invalid = section.getByRole('combobox', {
|
|
name: 'País de constituição da organização',
|
|
exact: true,
|
|
});
|
|
await page.waitForFunction(
|
|
() =>
|
|
document.activeElement?.getAttribute('aria-invalid') ===
|
|
'true',
|
|
);
|
|
assert.equal(
|
|
await invalid.getAttribute('aria-invalid'),
|
|
'true',
|
|
);
|
|
assert(
|
|
await invalid.evaluate((n) => document.activeElement === n),
|
|
);
|
|
assert(await invalid.getAttribute('aria-describedby'));
|
|
q.proof.checks.master_errors_named_and_focused = true;
|
|
await q.shot(page, 'master-error-focused', false);
|
|
await invalid.selectOption('FR');
|
|
await section
|
|
.getByLabel('Fonte ou referência deste país', {
|
|
exact: true,
|
|
})
|
|
.fill(
|
|
'Declaração sintética QA do representante, 10/10/2026; sem registo ou decisão real.',
|
|
);
|
|
await save(
|
|
section.getByRole('button', {
|
|
name: 'Guardar país no cadastro mestre',
|
|
exact: true,
|
|
}),
|
|
'/investidor/organizations/' + org.id + '/country',
|
|
'PUT',
|
|
);
|
|
p = await open('/investidor/profile');
|
|
assert.equal(
|
|
p.organizations.find((o) => o.id === org.id).country,
|
|
'FR',
|
|
);
|
|
f.master_saved = true;
|
|
f.master_version = p.organizations.find(
|
|
(o) => o.id === org.id,
|
|
).version;
|
|
q.save();
|
|
}
|
|
if (!f.reused) {
|
|
const section = page.locator('section').filter({
|
|
has: page.getByRole('heading', { name, exact: true }),
|
|
});
|
|
await save(
|
|
section.getByRole('button', {
|
|
name: 'Reutilizar país de Synthetic QA organization',
|
|
exact: true,
|
|
}),
|
|
'/investidor/companies/' +
|
|
f.company +
|
|
'/organization-country',
|
|
'POST',
|
|
);
|
|
p = await open('/investidor/profile');
|
|
company = p.companies.find((c) => c.id === f.company);
|
|
assert.equal(company.payload.incorporation_country_code, 'FR');
|
|
assert.equal(company.countrySource.organization_id, org.id);
|
|
f.reused = true;
|
|
q.save();
|
|
}
|
|
if (!f.new_submission) {
|
|
const section = page.locator('section').filter({
|
|
has: page.getByRole('heading', { name, exact: true }),
|
|
});
|
|
await save(
|
|
section.getByRole('button', {
|
|
name: 'Preparar manifestação desta empresa',
|
|
exact: true,
|
|
}),
|
|
'/investidor/manifestacoes',
|
|
'POST',
|
|
);
|
|
await page.waitForURL((u) =>
|
|
/^\/investidor\/manifestacoes\/.+/.test(u.pathname),
|
|
);
|
|
f.new_submission = new URL(page.url()).pathname
|
|
.split('/')
|
|
.pop();
|
|
q.save();
|
|
}
|
|
const newSubmission = await open(
|
|
'/investidor/manifestacoes/' + f.new_submission,
|
|
);
|
|
assert.equal(
|
|
newSubmission.submission.payload.incorporation_country_code,
|
|
'FR',
|
|
);
|
|
assert.equal(
|
|
newSubmission.submission.payload.organization_country_snapshot
|
|
.organization_version,
|
|
f.master_version,
|
|
);
|
|
if (!f.master_changed) {
|
|
p = await open('/investidor/profile');
|
|
const box = page.getByRole('region', {
|
|
name: 'Cadastro mestre: Synthetic QA organization',
|
|
exact: true,
|
|
});
|
|
await box
|
|
.getByRole('combobox', {
|
|
name: 'País de constituição da organização',
|
|
exact: true,
|
|
})
|
|
.selectOption('ZA');
|
|
await box
|
|
.getByLabel('Fonte ou referência deste país', {
|
|
exact: true,
|
|
})
|
|
.fill(
|
|
'Segunda declaração sintética QA: ensaio de cópias históricas preservadas.',
|
|
);
|
|
await save(
|
|
box.getByRole('button', {
|
|
name: 'Guardar país no cadastro mestre',
|
|
exact: true,
|
|
}),
|
|
'/investidor/organizations/' + org.id + '/country',
|
|
'PUT',
|
|
);
|
|
f.master_changed = true;
|
|
q.save();
|
|
}
|
|
q.phase(
|
|
'Fresh session: current master ZA, prior copies MZ/FR and provenance preserved',
|
|
);
|
|
await actor('promoter');
|
|
p = await open('/investidor/profile');
|
|
assert.equal(
|
|
p.organizations.find((o) => o.id === org.id).country,
|
|
'ZA',
|
|
);
|
|
assert.equal(
|
|
p.companies.find((c) => c.id === f.company).payload
|
|
.incorporation_country_code,
|
|
'FR',
|
|
);
|
|
await q.shot(page, 'master-versus-explicit-copy');
|
|
old = await open('/investidor/manifestacoes/' + f.old_submission);
|
|
assert.equal(
|
|
old.submission.payload.incorporation_country_code,
|
|
'MZ',
|
|
);
|
|
assert.equal(old.submission.version, f.old_version);
|
|
const later = await open(
|
|
'/investidor/manifestacoes/' + f.new_submission,
|
|
);
|
|
assert.equal(
|
|
later.submission.payload.incorporation_country_code,
|
|
'FR',
|
|
);
|
|
assert.equal(
|
|
later.submission.payload.organization_country_snapshot
|
|
.organization_version,
|
|
f.master_version,
|
|
);
|
|
q.proof.checks.new_session_no_historical_rewrite = true;
|
|
await actor('outsider');
|
|
p = await open('/investidor/profile');
|
|
assert(!p.organizations.some((o) => o.id === org.id));
|
|
const denied = await mutation(
|
|
'/investidor/organizations/' + org.id + '/country',
|
|
{
|
|
version: f.master_version + 1,
|
|
country: 'MZ',
|
|
source_reference:
|
|
'Tentativa sintética de outra organização',
|
|
},
|
|
'PUT',
|
|
);
|
|
assert.equal(denied.status, 403);
|
|
q.proof.checks.other_organization_denied = true;
|
|
} else if (mode === 'sharing') {
|
|
const old = JSON.parse(
|
|
fs.readFileSync(
|
|
path.join(DIR, 'independent-documents-http-qa.json'),
|
|
'utf8',
|
|
),
|
|
);
|
|
const room = old.receipts.room_id;
|
|
assert.equal(room, 1);
|
|
q.proof.documents = [];
|
|
await actor('promoter');
|
|
let p = await open('/parcerias/salas/' + room);
|
|
assert.equal(p.room.id, room);
|
|
const owner = { page, ctx };
|
|
({ page, ctx } = await q.context('colleague'));
|
|
await q.login(page, 'colleague');
|
|
let candidate = { page, ctx };
|
|
const base = '/parcerias/salas/' + room;
|
|
for (const d of old.room) {
|
|
q.phase('Visual explicit sharing and revocation ' + d.type);
|
|
page = owner.page;
|
|
ctx = owner.ctx;
|
|
p = await open(base);
|
|
const record = p.documents.find(
|
|
(x) => x.id === d.attachment_id,
|
|
);
|
|
assert(record && !record.shared);
|
|
const article = page.locator('article').filter({
|
|
has: page.getByText(record.file_name, { exact: false }),
|
|
});
|
|
await save(
|
|
article.getByRole('button', {
|
|
name: 'Autorizar partilha com a contraparte',
|
|
exact: true,
|
|
}),
|
|
base + '/documentos/' + d.attachment_id + '/partilha',
|
|
'POST',
|
|
);
|
|
page = candidate.page;
|
|
ctx = candidate.ctx;
|
|
p = await open(base);
|
|
assert(p.documents.some((x) => x.id === d.attachment_id));
|
|
const response = await page.request.get(
|
|
BASE + base + '/documentos/' + d.attachment_id,
|
|
);
|
|
assert.equal(response.status(), 200);
|
|
assert.equal(
|
|
crypto
|
|
.createHash('sha256')
|
|
.update(await response.body())
|
|
.digest('hex'),
|
|
d.sha256,
|
|
);
|
|
await q.shot(page, 'shared-' + d.type, false);
|
|
page = owner.page;
|
|
ctx = owner.ctx;
|
|
await open(base);
|
|
const card = page.locator('article').filter({
|
|
has: page.getByText(record.file_name, { exact: false }),
|
|
});
|
|
await save(
|
|
card.getByRole('button', {
|
|
name: 'Retirar autorização de partilha',
|
|
exact: true,
|
|
}),
|
|
base + '/documentos/' + d.attachment_id + '/partilha',
|
|
'POST',
|
|
);
|
|
page = candidate.page;
|
|
ctx = candidate.ctx;
|
|
const denied = await page.request.get(
|
|
BASE + base + '/documentos/' + d.attachment_id,
|
|
);
|
|
assert.equal(denied.status(), 404);
|
|
p = await open(base);
|
|
assert(!p.documents.some((x) => x.id === d.attachment_id));
|
|
await q.shot(page, 'revoked-' + d.type, false);
|
|
q.proof.documents.push({
|
|
type: d.type,
|
|
id: d.attachment_id,
|
|
sha256: d.sha256,
|
|
previously_allowed: 200,
|
|
same_session_after_revocation: 404,
|
|
listing_removed: true,
|
|
});
|
|
q.save();
|
|
}
|
|
await candidate.ctx.close();
|
|
({ page, ctx } = await q.context('colleague'));
|
|
await q.login(page, 'colleague');
|
|
p = await open(base);
|
|
for (const d of q.proof.documents) {
|
|
const denied = await page.request.get(
|
|
BASE + base + '/documentos/' + d.id,
|
|
);
|
|
assert.equal(denied.status(), 404);
|
|
assert(!p.documents.some((x) => x.id === d.id));
|
|
d.fresh_session_after_revocation = 404;
|
|
}
|
|
await ctx.close();
|
|
page = owner.page;
|
|
ctx = owner.ctx;
|
|
for (const d of q.proof.documents) {
|
|
const r = await page.request.get(
|
|
BASE + base + '/documentos/' + d.id,
|
|
);
|
|
assert.equal(r.status(), 200);
|
|
assert.equal(
|
|
crypto
|
|
.createHash('sha256')
|
|
.update(await r.body())
|
|
.digest('hex'),
|
|
d.sha256,
|
|
);
|
|
}
|
|
q.proof.checks.visual_revocation_previously_granted_urls = true;
|
|
q.proof.checks.owner_originals_preserved = true;
|
|
}
|
|
q.proof.result = 'passed';
|
|
} catch (e) {
|
|
q.proof.result = 'failed';
|
|
q.proof.error = sanitized(e);
|
|
if (
|
|
page &&
|
|
!['/login', '/two-factor-challenge'].includes(
|
|
new URL(page.url()).pathname,
|
|
)
|
|
) {
|
|
q.proof.failure = {
|
|
route: new URL(page.url()).pathname,
|
|
alerts: await page.getByRole('alert').allTextContents(),
|
|
buttons: await page.getByRole('button').allTextContents(),
|
|
};
|
|
await q.shot(page, 'failure', false).catch(() => {});
|
|
}
|
|
} finally {
|
|
q.proof.completed_at = new Date().toISOString();
|
|
q.save();
|
|
await q.browser.close();
|
|
}
|
|
console.log(
|
|
JSON.stringify({
|
|
result: q.proof.result,
|
|
phase: q.proof.phase,
|
|
checks: q.proof.checks,
|
|
error: q.proof.error,
|
|
}),
|
|
);
|
|
if (q.proof.result === 'failed') process.exitCode = 1;
|
|
})();
|