Files
portail-santinova/test/portal-contract.test.js
Manus abfd216924
All checks were successful
Validation applicative / Tests de contrat (push) Successful in 9s
feat: séparer les portails production par entité
2026-10-24 09:49:04 +00:00

137 lines
5.5 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.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');