Files
portail-santinova/test/portal-contract.test.js
Manus 0d5229472e
All checks were successful
Validation applicative / Tests de contrat (push) Successful in 7s
fix: isoler la navigation des portails par entité
2026-10-24 10:39:00 +00:00

140 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Contrat de non-régression des quatre portails production.
* Un seul service statique répond sur quatre domaines, mais le domaine courant
* décide strictement du groupe d'applications et du dashboard de production.
*/
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const root = path.join(__dirname, '..');
const html = fs.readFileSync(path.join(root, 'index.html'), 'utf8');
const compose = fs.readFileSync(path.join(root, 'docker-compose.yml'), 'utf8');
const script = html.match(/<script>([\s\S]*?)<\/script>/)?.[1];
assert.ok(script, 'le script de configuration des portails doit être présent');
assert.equal(/^(<<<<<<<|=======|>>>>>>>)/m.test(html), false, 'aucun marqueur de conflit Git ne doit être livré');
assert.match(html, /PORTAL_CONFIGS/, 'les domaines des portails doivent être configurés explicitement');
assert.doesNotMatch(html, /class="entity-nav"/, 'un portail ne doit proposer aucun accès aux autres portails');
assert.doesNotMatch(html, /PORTAL_DEFINITIONS/, 'la liste de navigation croisée ne doit pas être livrée');
assert.doesNotMatch(html, /function renderNavigation\(/, 'aucune fonction ne doit construire des liens vers les autres portails');
assert.match(html, /STATUS_REFRESH_MS = 30000/, 'le délai de grisage doit être explicite');
assert.match(html, /setInterval\(fetchAndApplyStatuses, STATUS_REFRESH_MS\)/, 'les statuts doivent être réactualisés périodiquement');
function loadPortal(hostname) {
const redirects = [];
const elements = new Map();
const document = {
title: '',
documentElement: { style: { setProperty() {} } },
addEventListener() {},
getElementById(id) {
if (!elements.has(id)) {
elements.set(id, {
innerHTML: '',
textContent: '',
className: '',
firstChild: { nodeValue: '' },
classList: { toggle() {} },
setAttribute() {}
});
}
return elements.get(id);
},
querySelectorAll() { return []; }
};
const window = {
location: {
hostname,
replace(target) { redirects.push(target); }
}
};
const sandbox = {
AbortController,
Map,
Set,
URL,
console,
document,
window,
setInterval() {},
setTimeout() { return 0; },
clearTimeout() {}
};
vm.createContext(sandbox);
vm.runInContext(script, sandbox);
return { sandbox, redirects };
}
const hosts = {
itinova: 'portail-itinova.santinova-soft.org',
soins: 'portail-soins-et-sante.santinova-soft.org',
santinova: 'portail-santinova.santinova-soft.org',
santelis: 'portail-santelis.santinova-soft.org'
};
const apiApps = 'https://dashboard.santinova-soft.org/api/public/apps';
for (const [group, host] of Object.entries(hosts)) {
const { sandbox } = loadPortal(host);
assert.equal(sandbox.PORTAL.env, 'prod', `${host} doit utiliser la production`);
assert.equal(sandbox.PORTAL.dashboardAppsUrl, apiApps, `${host} doit appeler exclusivement le dashboard production`);
assert.equal(sandbox.PORTAL.dashboardStatusUrl, apiApps.replace('/apps', '/status'), `${host} doit appeler le statut production`);
assert.equal(sandbox.PORTAL.group, group === 'soins' ? 'SOINS_ET_SANTE' : group.toUpperCase(), `${host} doit sélectionner le bon groupe`);
assert.match(
compose,
new RegExp(`traefik\\.http\\.routers\\.portail-[a-z-]+-production\\.rule=Host\\(\\\`${host.replace(/\./g, '\\.')}\\\`\\)`),
`${host} doit avoir un routeur Traefik dédié`
);
}
assert.match(
compose,
/traefik\.http\.routers\.portail-legacy-production\.rule=Host\(`portail\.santinova-soft\.org`\)/,
'l’ancienne URL production doit garder un routeur de redirection'
);
assert.equal((compose.match(/tls\.certresolver=letsencrypt/g) || []).length, 5, 'chaque routeur production doit demander un certificat TLS');
const deployedApps = [
{ id: 'itinova-contacts', category: 'ITINOVA', urls: { prod: 'https://contacts.santinova-soft.org' } },
{ id: 'itinova-budget-si', category: 'SANTINOVA', urls: { prod: 'https://budget-si.santinova-soft.org' } },
{ id: 'manus-dashboard', category: 'INFRA', urls: { prod: 'https://dashboard.santinova-soft.org' } },
{ id: 'santelis-gestion', category: 'SANTINOVA', urls: { prod: 'https://gestion-santelis.santinova-soft.org' } }
];
const expectedVisible = {
[hosts.itinova]: ['itinova-contacts'],
[hosts.soins]: [],
[hosts.santinova]: ['itinova-budget-si', 'manus-dashboard'],
[hosts.santelis]: []
};
for (const [host, ids] of Object.entries(expectedVisible)) {
const { sandbox } = loadPortal(host);
const visible = deployedApps.filter(sandbox.isAppInCurrentPortal).map(app => app.id);
assert.deepEqual(visible, ids, `${host} ne doit afficher que les applications de son portail`);
}
const santelisProductionPortal = loadPortal(hosts.santelis).sandbox;
assert.deepEqual(
Array.from(santelisProductionPortal.getPortalOnlyApps(), app => app.id),
['santelis-gestion'],
'SANTELIS Gestion doit être fournie par le portail production sans être ajoutée au dashboard'
);
assert.equal(
santelisProductionPortal.getPortalOnlyApps()[0].urls.prod,
'https://gestion-santelis.santinova-soft.org',
'la tuile SANTELIS doit cibler son URL de production validée'
);
for (const imageName of ['budget-si.jpg', 'dashboard.png', 'portail.png', 'santelis-gestion.jpg', 'studio-portee.png']) {
const imagePath = path.join(root, 'images', imageName);
assert.equal(fs.existsSync(imagePath), true, `l’asset ${imageName} doit être publié`);
assert.ok(fs.statSync(imagePath).size > 2048, `l’asset ${imageName} ne doit pas être vide`);
}
assert.deepEqual(
loadPortal('portail.santinova-soft.org').redirects,
['https://portail-santinova.santinova-soft.org/'],
'l’ancienne URL production doit rediriger vers le portail Santinova'
);
console.log('OK portal-contract');