Reconciliar verificacoes com navegacao e relatorios aprovados

This commit is contained in:
2026-10-20 15:12:17 +02:00
parent 74b081e93e
commit 6e439def02
5 changed files with 42 additions and 12 deletions
+1 -1
View File
@@ -114,7 +114,7 @@ export async function portalRequest<T>(
const headers = new Headers(options.headers);
headers.set('Accept', 'application/json');
if (options.method && !['GET', 'HEAD'].includes(options.method.toUpperCase())) {
const token = document.cookie.split('; ').find(value => value.startsWith('XSRF-TOKEN='))?.slice(11);
const token = typeof document === 'undefined' ? undefined : document.cookie.split('; ').find(value => value.startsWith('XSRF-TOKEN='))?.slice(11);
if (token) headers.set('X-XSRF-TOKEN', decodeURIComponent(token));
}
const response = await fetch(url, {
+15
View File
@@ -144,6 +144,21 @@ await test('fetch accepts a real server confirmation', async (t) => {
assert.equal(response.data.reference, 'IR-SERVER-001');
});
await test('browser writes preserve the CSRF cookie header', async (t) => {
const previous = Object.getOwnPropertyDescriptor(globalThis, 'document');
Object.defineProperty(globalThis, 'document', { configurable: true, value: { cookie: 'other=x; XSRF-TOKEN=qa%2Ftoken' } });
try {
t.mock.method(globalThis, 'fetch', async (_url, options) => {
assert.equal(options.headers.get('X-XSRF-TOKEN'), 'qa/token');
return new Response('{"success":true}', { status: 201 });
});
await portalRequest('/api/v1/interest-requests', { method: 'POST' });
} finally {
if (previous) Object.defineProperty(globalThis, 'document', previous);
else delete globalThis.document;
}
});
for (const status of [422, 429, 500]) {
await test(`HTTP ${status} never becomes a successful confirmation`, async (t) => {
t.mock.method(
+14 -3
View File
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import {
portalNavigation,
legacyPortalNavigation,
currentNavigationGroup,
currentNavigationItem,
} from '../../resources/js/lib/portal-navigation.ts';
@@ -15,7 +16,7 @@ void test('public and authenticated navigation share groups without revealing ma
assert.deepEqual(publicGroups.slice(0, 3), investor.slice(0, 3));
assert.deepEqual(
publicGroups.map((g) => g.id),
['explore', 'prepare', 'support', 'partnerships', 'account'],
['discovery', 'realization', 'support', 'account'],
);
const hrefs = JSON.stringify(investor);
assert.equal(hrefs.includes('/management/'), false);
@@ -32,7 +33,7 @@ void test('management and optional modules follow permissions and feature flags'
education: true,
ai: true,
});
assert.equal(groups.at(-1).id, 'management');
assert.ok(groups.some((group) => group.id === 'management'));
assert.equal(JSON.stringify(groups).includes('/investidor/negocios'), true);
assert.equal(JSON.stringify(groups).includes('/management/ai'), true);
assert.equal(
@@ -48,7 +49,9 @@ void test('management and optional modules follow permissions and feature flags'
});
void test('detail URLs and filtered catalogue pages retain the current group', () => {
const groups = portalNavigation({ authenticated: true, manager: true });
// Preserve the retained catalogue's detail/filter matching independently
// of the reconciled shell's new grouping names.
const groups = legacyPortalNavigation({ authenticated: true, manager: true });
for (const url of [
'/carteira/project/fixture',
'/zonas?category=zee',
@@ -81,3 +84,11 @@ void test('detail URLs and filtered catalogue pages retain the current group', (
'zee',
);
});
void test('reconciled navigation selects discovery, realization and private support correctly', () => {
const groups = portalNavigation({ authenticated: false, manager: false });
assert.equal(currentNavigationGroup(groups, '/oportunidades/fixture'), 'discovery');
assert.equal(currentNavigationGroup(groups, '/simulador-fiscal'), 'realization');
assert.equal(currentNavigationGroup(groups, '/balcao-virtual/dossiers/fixture'), 'support');
assert.equal(currentNavigationGroup(groups, '/management/crm/leads'), null);
});
@@ -114,15 +114,19 @@ void test('business plan values in the dossier equal the existing simulator fore
const finance = report.sections.find((section) => section.id === 'finance');
const row = (field) =>
finance.rows.find((item) => item.field === field).value;
const numeric = (value) => {
assert.match(value, /MZN$/);
return value.replace(/\s|MZN/g, '').replace(',', '.');
};
assert.equal(
row('Facturação no horizonte'),
`${expected.totalRevenue} MZN`,
numeric(row('Facturação no horizonte')),
expected.totalRevenue,
);
assert.equal(
row('Resultado antes de impostos'),
`${expected.totalProfitBeforeTax} MZN`,
numeric(row('Resultado antes de impostos')),
expected.totalProfitBeforeTax,
);
assert.equal(row('Caixa final projectada'), `${expected.endingCash} MZN`);
assert.equal(numeric(row('Caixa final projectada')), expected.endingCash);
assert.equal(row('Reconciliação do financiamento'), expected.fundingStatus);
});
+3 -3
View File
@@ -26,8 +26,8 @@ void test('portal home hero exposes a direct public simulator link', () => {
void test('completed simulator result keeps public interest submission independent', () => {
assert.match(simulator, /href="\/manifestar-interesse"/);
assert.match(
simulator,
/Manifestar interesse sem partilhar\s+esta simulação/,
simulator.replace(/\s+/g, ' '),
/Manifestar interesse sem partilhar esta simulação/,
);
assert.match(simulator, /sem\s+partilhar os dados desta simulação/);
assert.match(simulator.replace(/\s+/g, ' '), /sem partilhar os dados desta simulação/);
});