Files
Investidor-V2/tests/frontend/portal-api.test.mjs
T
2026-11-02 20:31:15 +02:00

242 lines
8.2 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
PortalApiError,
fieldErrors,
interestPayload,
newSubmissionKey,
money,
portalRequest,
searchUrl,
toFeaturedOpportunity,
toOpportunity,
} from '../../resources/js/lib/portal-api.ts';
await test('submission keys are distinct UUIDv4 values', () => {
const first = newSubmissionKey();
assert.match(
first,
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
);
assert.notEqual(first, newSubmissionKey());
});
await test('filters preserve one-based maturity and pagination and escape query text', () => {
const url = new URL(
searchUrl(
{
query: ' energia & solar ',
sector: 'energia',
province: 'Zambézia',
maturity: '4',
},
2,
),
'http://localhost',
);
assert.equal(url.searchParams.get('q'), 'energia & solar');
assert.equal(url.searchParams.get('maturity_level'), '4');
assert.equal(url.searchParams.get('page'), '2');
assert.equal(url.searchParams.get('province'), 'Zambézia');
});
await test('empty filters do not restrict results', () => {
const url = searchUrl(
{ query: '', sector: '', province: '', maturity: '' },
1,
);
assert.equal(url, '/api/v1/opportunities/search?page=1&per_page=12');
});
await test('request uses fullName and transmits consent without inventing a reference', () => {
const payload = interestPayload({
name: ' Teste fictício ',
email: 'teste@example.invalid',
phone: '',
investorType: 'empresa',
country: 'Moçambique',
sectorInterest: 'Energia',
opportunityReference: 'TEST-1',
investmentRange: '',
message: 'Mensagem fictícia para teste.',
privacyConsent: true,
});
assert.equal(payload.fullName, 'Teste fictício');
assert.equal(payload.privacyConsent, true);
assert.equal(payload.opportunityReference, 'TEST-1');
assert.equal('reference' in payload, false);
assert.equal('name' in payload, false);
});
await test('server validation maps fullName back to the name field', () => {
assert.deepEqual(
fieldErrors(
new PortalApiError(422, {
fullName: ['Nome obrigatório'],
email: ['Email inválido'],
unexpected: ['ignored'],
}),
),
{ name: 'Nome obrigatório', email: 'Email inválido' },
);
});
await test('monetary rendering preserves large decimal values and the API currency', () => {
const amount = money('123456789012345678.90', 'MZN');
assert.match(amount, /MZN|MTn?/);
assert.equal(amount.replace(/\D/g, ''), '12345678901234567890');
assert.notEqual(money('1000.00', 'EUR'), money('1000.00', 'USD'));
assert.equal(money(null, 'USD'), 'Por indicar');
assert.notEqual(money('0.00', 'USD'), 'Por indicar');
});
await test('database records map without changing maturity or amount precision', () => {
const item = toOpportunity({
slug: 'test',
reference: 'TEST',
title: 'Teste',
sector: null,
province: null,
maturity_label: 'Projecto estruturado',
estimated_investment_amount: '1000.25',
estimated_investment_currency: 'MZN',
financing_modality: null,
expected_impact: null,
summary: null,
description: 'Descrição completa',
is_featured: false,
});
assert.equal(item.sectorName, 'Sector não indicado');
assert.equal(item.maturity, 'Projecto estruturado');
assert.equal(item.amount, '1000.25');
assert.equal(item.description, 'Descrição completa');
});
await test('home cards retain the slug needed to fetch the full detail', () => {
const item = toFeaturedOpportunity({
reference: 'TEST',
title: 'Teste',
sector: 'Energia',
province: null,
maturity: null,
estimatedInvestment: { amount: null, currency: 'USD' },
url: '/oportunidades/projecto-teste',
});
assert.equal(item.slug, 'projecto-teste');
assert.equal(item.description, null);
});
await test('fetch accepts a real server confirmation', async (t) => {
t.mock.method(globalThis, 'fetch', async (_url, options) => {
assert.equal(options.headers.get('Accept'), 'application/json');
assert.equal(options.credentials, 'same-origin');
return new Response(
JSON.stringify({
success: true,
data: { reference: 'IR-SERVER-001' },
}),
{ status: 201 },
);
});
const response = await portalRequest('/api/v1/interest-requests', {
method: 'POST',
});
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(
globalThis,
'fetch',
async () =>
new Response(
JSON.stringify({
errors: { email: ['Inválido'] },
message: 'Internal diagnostic',
}),
{ status },
),
);
await assert.rejects(
portalRequest('/api/v1/interest-requests'),
(error) => {
assert.equal(error.status, status);
assert.doesNotMatch(error.message, /Internal diagnostic/);
if (status === 422)
assert.equal(error.errors.email[0], 'Inválido');
return true;
},
);
});
}
await test('invalid JSON and network errors remain failures', async (t) => {
t.mock.method(
globalThis,
'fetch',
async () => new Response('<html>Error</html>', { status: 200 }),
);
await assert.rejects(portalRequest('/api/v1/home'), PortalApiError);
t.mock.method(globalThis, 'fetch', async () => {
throw new TypeError('Failed to fetch');
});
await assert.rejects(portalRequest('/api/v1/home'), TypeError);
});
await test('malformed CSRF cookie prevents writes with a readable session error but permits reads', async (t) => {
const previous = Object.getOwnPropertyDescriptor(globalThis, 'document');
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: { cookie: 'XSRF-TOKEN=%broken' },
});
let calls = 0;
t.mock.method(globalThis, 'fetch', async () => {
calls++;
return new Response('{"success":true}');
});
try {
await assert.rejects(
portalRequest('/synthetic', { method: 'POST' }),
(error) =>
error instanceof PortalApiError && /sessão/.test(error.message),
);
assert.equal(calls, 0);
await portalRequest('/synthetic');
assert.equal(calls, 1);
} finally {
if (previous) Object.defineProperty(globalThis, 'document', previous);
else delete globalThis.document;
}
});
await test('nullable validation errors never crash field rendering', async (t) => {
t.mock.method(
globalThis,
'fetch',
async () => new Response('{"errors":null}', { status: 422 }),
);
await assert.rejects(portalRequest('/synthetic'), (error) => {
assert.deepEqual(fieldErrors(error), {});
return error instanceof PortalApiError;
});
});