242 lines
8.2 KiB
JavaScript
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;
|
|
});
|
|
});
|