Files
portail-santinova/test/portal-contract.test.js
manus-admin e2ce31b555
All checks were successful
Validation applicative / Tests de contrat (push) Successful in 14s
fix: isoler Santelis hors inventaire dashboard
2026-10-24 09:27:58 +00:00

160 lines
7.0 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 portails applicatifs par entité.
* Un unique code sert les quatre domaines de chaque environnement, mais le
* domaine courant choisit strictement son dashboard, ses URLs et son groupe.
*/
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: '',
addEventListener() {},
getElementById(id) {
if (!elements.has(id)) {
elements.set(id, {
innerHTML: '',
textContent: '',
className: '',
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 = {
itinovaRecette: 'portail-itinova.recette.santinova-soft.org',
soinsRecette: 'portail-soins-et-sante.recette.santinova-soft.org',
santinovaRecette: 'portail-santinova.recette.santinova-soft.org',
santelisRecette: 'portail-santelis.recette.santinova-soft.org',
itinovaProd: 'portail-itinova.santinova-soft.org',
soinsProd: 'portail-soins-et-sante.santinova-soft.org',
santinovaProd: 'portail-santinova.santinova-soft.org',
santelisProd: 'portail-santelis.santinova-soft.org'
};
const expected = [
[hosts.itinovaRecette, 'recette', 'ITINOVA', 'https://dashboard.recette.santinova-soft.org/api/public/apps'],
[hosts.soinsRecette, 'recette', 'SOINS_ET_SANTE', 'https://dashboard.recette.santinova-soft.org/api/public/apps'],
[hosts.santinovaRecette, 'recette', 'SANTINOVA', 'https://dashboard.recette.santinova-soft.org/api/public/apps'],
[hosts.santelisRecette, 'recette', 'SANTELIS', 'https://dashboard.recette.santinova-soft.org/api/public/apps'],
[hosts.itinovaProd, 'prod', 'ITINOVA', 'https://dashboard.santinova-soft.org/api/public/apps'],
[hosts.soinsProd, 'prod', 'SOINS_ET_SANTE', 'https://dashboard.santinova-soft.org/api/public/apps'],
[hosts.santinovaProd, 'prod', 'SANTINOVA', 'https://dashboard.santinova-soft.org/api/public/apps'],
[hosts.santelisProd, 'prod', 'SANTELIS', 'https://dashboard.santinova-soft.org/api/public/apps']
];
for (const [host, env, group, appsUrl] of expected) {
const { sandbox } = loadPortal(host);
assert.equal(sandbox.PORTAL.env, env, `${host} doit utiliser l’environnement ${env}`);
assert.equal(sandbox.PORTAL.group, group, `${host} doit afficher le groupe ${group}`);
assert.equal(sandbox.PORTAL.dashboardAppsUrl, appsUrl, `${host} doit appeler uniquement le bon dashboard`);
assert.equal(sandbox.PORTAL.dashboardStatusUrl, appsUrl.replace('/apps', '/status'), `${host} doit appeler le bon endpoint statut`);
}
for (const host of [hosts.itinovaRecette, hosts.soinsRecette, hosts.santinovaRecette, hosts.santelisRecette]) {
assert.match(
compose,
new RegExp(`traefik\\.http\\.routers\\.portail-[a-z-]+\\.rule=Host\\(\\\`${host.replace(/\./g, '\\.') }\\\`\\)`),
`${host} doit avoir un routeur Traefik dédié`
);
}
assert.match(
compose,
/traefik\.http\.routers\.portail-legacy-recette\.rule=Host\(`portail\.recette\.santinova-soft\.org`\)/,
'l’ancienne URL recette doit garder un routeur de redirection'
);
assert.equal((compose.match(/tls\.certresolver=letsencrypt/g) || []).length, 5, 'chaque routeur recette doit demander un certificat TLS');
const recetteApps = [
{ id: 'itinova-contacts', category: 'ITINOVA', urls: { recette: 'https://contacts.recette.santinova-soft.org' } },
{ id: 'gestion-astreintes-had', category: 'SOINS_ET_SANTE', urls: { recette: 'https://astreintes-had.recette.santinova-soft.org' } },
{ id: 'demat-facturation-dsi', category: 'SANTINOVA', urls: { recette: 'https://demat-facturation.recette.santinova-soft.org' } },
{ id: 'referentiel-ia', category: 'INFRA', urls: { recette: 'https://referentiel-ia.recette.santinova-soft.org' } },
{ id: 'santelis-gestion', category: 'SANTINOVA', urls: { recette: 'https://gestion-santelis.recette.santinova-soft.org' } }
];
const expectedVisible = {
[hosts.itinovaRecette]: ['itinova-contacts'],
[hosts.soinsRecette]: ['gestion-astreintes-had'],
[hosts.santinovaRecette]: ['demat-facturation-dsi', 'referentiel-ia'],
[hosts.santelisRecette]: []
};
for (const [host, ids] of Object.entries(expectedVisible)) {
const { sandbox } = loadPortal(host);
const visible = recetteApps.filter(sandbox.isAppInCurrentPortal).map(app => app.id);
assert.deepEqual(visible, ids, `${host} ne doit afficher que les tuiles de son portail`);
}
const santelisProductionPortal = loadPortal(hosts.santelisProd).sandbox;
assert.deepEqual(
Array.from(santelisProductionPortal.getPortalOnlyApps(), app => app.id),
['santelis-gestion'],
'SANTELIS Gestion doit être fournie par le portail production sans être publiée par le 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', 'gestion-astreintes-had.jpg', 'gestion-indus-had.jpg', 'santelis-gestion.jpg', 'santino-chat.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.recette.santinova-soft.org').redirects,
['https://portail-santinova.recette.santinova-soft.org/'],
'l’ancienne URL recette doit rediriger vers le portail Santinova'
);
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');