Files
portail-santinova/test/portal-contract.test.js
manus-admin a64634d003
Some checks failed
Validation applicative / Tests de contrat (push) Failing after 6s
fix: supprimer les portails historiques et leur tuile
2026-10-24 13:44:32 +00:00

161 lines
8.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];
const historicalRecetteHost = ['portail', 'recette', 'santinova-soft', 'org'].join('.');
const historicalProductionHost = ['portail', 'santinova-soft', 'org'].join('.');
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.equal(html.includes(historicalRecetteHost), false, 'le portail historique recette ne doit plus être livré');
assert.equal(html.includes(historicalProductionHost), false, 'le portail historique production ne doit plus être livré');
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.equal(compose.includes(['portail', 'legacy'].join('-')), false, 'aucune route historique ne doit rester exposée');
assert.equal(compose.includes(historicalRecetteHost), false, 'l’ancienne URL recette ne doit plus être routée');
assert.equal(compose.includes(historicalProductionHost), false, 'l’ancienne URL production ne doit plus être routée');
assert.equal((compose.match(/tls\.certresolver=letsencrypt/g) || []).length, 4, 'seuls les quatre portails par entité doivent 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: 'portail-santinova', category: 'INFRA', urls: { recette: 'https://portail-santinova.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`);
}
for (const historicalHost of [historicalRecetteHost, historicalProductionHost]) {
const historicalPortal = loadPortal(historicalHost);
assert.equal(historicalPortal.sandbox.PORTAL, null, `${historicalHost} ne doit plus être reconnu comme portail`);
assert.deepEqual(historicalPortal.redirects, [], `${historicalHost} ne doit plus rediriger vers un portail`);
}
console.log('OK portal-contract');