Files
compras/scripts/menu-links-check.mjs
2026-10-30 20:14:24 +02:00

267 lines
9.0 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import {
procurementNavigation,
activeProcurementEntry,
} from '../resources/js/lib/procurement-navigation.ts';
const base = 'http://127.0.0.1:8005';
const cookies = new Map();
let version = '';
async function request(path, { method = 'GET', body, inertia = false } = {}) {
const response = await fetch(base + path, {
method,
redirect: 'manual',
headers: {
Accept: 'application/json',
Cookie: [...cookies].map(([k, v]) => `${k}=${v}`).join('; '),
...(body
? { 'Content-Type': 'application/x-www-form-urlencoded' }
: {}),
...(inertia
? { 'X-Inertia': 'true', 'X-Inertia-Version': version }
: {}),
},
...(body ? { body: new URLSearchParams(body) } : {}),
});
for (const cookie of response.headers.getSetCookie()) {
const pair = cookie.split(';', 1)[0],
index = pair.indexOf('=');
cookies.set(pair.slice(0, index), pair.slice(index + 1));
}
const text = await response.text();
let data;
try {
data = JSON.parse(text);
} catch {}
if (!data) {
const page = text.match(
/<script data-page="app" type="application\/json">([\s\S]*?)<\/script>/,
);
if (page) data = JSON.parse(page[1]);
}
if (data?.version) version = data.version;
return { status: response.status, text, data };
}
async function enter(role) {
const landing = await request('/avaliar');
assert.equal(landing.status, 200);
const csrf = landing.text.match(/name="_token" value="([^"]+)"/)[1];
assert.equal(
(
await request('/avaliar/entrar', {
method: 'POST',
body: { role, _token: csrf },
})
).status,
302,
);
}
await enter('admin');
await request('/dashboard');
const baseline = (await request('/api/supplier-payables')).data;
const results = [];
for (const role of [
'admin',
'procurement',
'finance',
'department_head',
'warehouse',
'requester',
]) {
await enter(role);
const dashboard = await request('/dashboard', { inertia: true });
assert.equal(dashboard.status, 200);
assert.equal(dashboard.data.props.auth.user.purchasing_role, role);
const groups = procurementNavigation(
role,
dashboard.data.props.payablesAvailable === true,
);
const entries = groups.flatMap((group) => group.entries);
for (const entry of entries) {
const response = await request(entry.path, {
inertia: !entry.fullPage,
});
assert.equal(response.status, 200, role + ' ' + entry.path);
if (!entry.fullPage)
assert(response.data?.component, role + ' Inertia ' + entry.path);
if (entry.id === 'new_request')
assert.equal(
typeof response.data.props.defaultTaxRate,
'string',
'Configured VAT must reach new request lines',
);
if (entry.id === 'organization_parameters')
assert.equal(
typeof response.data.props.companyRules.default_tax_rate,
'string',
'Administration must expose the VAT setting',
);
if (entry.id === 'organization_parameters')
assert.equal(
Object.values(
response.data.props.comparisonPolicy.weights,
).reduce((sum, value) => sum + value, 0),
100,
'Comparison policy weights must reach administration',
);
if (entry.id === 'people_hiring')
assert.equal(
response.data.props.ready,
true,
'People hiring must be installed in the evaluation environment',
);
if (entry.id === 'people_hiring') {
assert.equal(
typeof response.data.props.workdesk.counts.tasks,
'number',
);
assert.equal(
typeof response.data.props.workdesk.counts.approvals,
'number',
);
assert(
response.data.props.processes.data.every(
(p) =>
!('rules' in p) &&
!('selection' in p) &&
!('contract' in p),
),
'People index must not expose private process payloads',
);
}
assert.equal(
activeProcurementEntry(groups, entry.path),
entry.id,
role + ' active ' + entry.path,
);
if (entry.id === 'my_drafts') {
assert.equal(response.data.props.filters.view, 'mine');
assert.equal(response.data.props.filters.status, 'draft');
}
}
for (const path of ['/settings/profile', '/settings/appearance'])
assert.equal(
(await request(path, { inertia: true })).status,
200,
role + ' ' + path,
);
// Hidden administration and finance pages remain enforced on the server.
if (role !== 'admin')
assert.equal(
(await request('/empresa')).status,
403,
role + ' company isolation',
);
if (!['admin', 'finance'].includes(role))
assert.equal(
(await request('/facturas')).status,
403,
role + ' finance isolation',
);
results.push({
role,
checked_entries: entries.length,
paths: entries.map((entry) => entry.path),
});
}
await enter('admin');
const all = await request('/requisicoes?view=all', { inertia: true });
const requestId = all.data.props.requisitions.data[0]?.id;
const processLinks = [];
if (requestId) {
const comparison = await request(
'/requisicoes/' + requestId + '/cotacoes',
{ inertia: true },
);
assert.equal(comparison.status, 200);
assert(Array.isArray(comparison.data.props.topIds));
assert(comparison.data.props.comparisonPolicy.minimum_proposals >= 1);
assert(comparison.data.props.comparisonPolicy.minimum_evaluators >= 1);
assert(
comparison.data.props.quotations.every((q) =>
Array.isArray(q.ineligibility_reasons),
),
);
assert.equal(
(await request('/requisicoes/' + requestId, { inertia: true })).status,
200,
);
const process = await request('/requisicoes/' + requestId + '/processo', {
inertia: true,
});
assert.equal(process.status, 200);
function collectLinks(value) {
if (!value || typeof value !== 'object') return;
if (
typeof value.href === 'string' &&
value.href.startsWith('/') &&
!value.href.startsWith('//')
)
processLinks.push(value.href);
for (const child of Object.values(value))
if (typeof child === 'object') collectLinks(child);
}
collectLinks(process.data.props);
for (const href of new Set(processLinks))
assert.equal((await request(href)).status, 200, 'Process link ' + href);
}
const invoices = await request('/facturas', { inertia: true });
for (const invoice of invoices.data.props.invoices.data) {
const page = await request(
'/obrigacoes-fornecedores?invoice=' + invoice.id,
);
assert.equal(page.status, 200);
assert(page.text.includes('href="/facturas"'));
}
assert.deepEqual(
(await request('/api/supplier-payables')).data,
baseline,
'Business history must remain unchanged',
);
const groups = procurementNavigation('admin', true);
assert.equal(
activeProcurementEntry(groups, '/requisicoes?status=draft&view=mine'),
'my_drafts',
);
assert.equal(activeProcurementEntry(groups, '/ordens/1/recepcoes'), 'receipts');
assert.equal(
activeProcurementEntry(groups, '/requisicoes/1/cotacoes'),
'quotations',
);
assert.equal(
activeProcurementEntry(groups, '/requisicoes/1/relatorio'),
'quotations',
);
assert.equal(
activeProcurementEntry(groups, '/requisicoes/1/adjudicacao'),
'orders',
);
assert.equal(
activeProcurementEntry(groups, '/stock/operacoes'),
'stock_operations',
);
await fs.writeFile(
'docs/proofs/menu-links.json',
JSON.stringify(
{
checked_at: new Date().toISOString(),
results,
request_detail_checked: !!requestId,
invoice_count: invoices.data.props.invoices.data.length,
process_links_checked: [...new Set(processLinks)],
canonical_query_order: true,
active_subroutes: true,
vat_setting_and_default_checked: true,
comparison_ties_and_reasons_checked: !!requestId,
business_history_unchanged: true,
business_commands: 0,
},
null,
2,
) + '\n',
);
console.log(
'All visible menu links passed for six roles, with active entries, query filters, detail links and unchanged business history.',
);