Files
portail-santinova/test/portal-contract.test.js
Santinova Deployment 7faf57cbb8
All checks were successful
Validation applicative / Tests de contrat (push) Successful in 15s
feat: normaliser les URL recette par entité
2026-10-24 17:06:48 +00:00

178 lines
9.1 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.match(html, /id="portal-hub-nav"/, 'le portail Santinova doit posséder une navigation dédiée vers les autres entités');
assert.match(html, /PORTAL_HUB_LINKS/, 'les liens du hub Santinova doivent être définis explicitement');
assert.match(html, /function renderPortalHubNavigation\(/, 'la navigation doit être rendue uniquement selon le portail courant');
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, elements };
}
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-santinova.recette.santinova-soft.org/api/public/apps'],
[hosts.soinsRecette, 'recette', 'SOINS_ET_SANTE', 'https://dashboard-santinova.recette.santinova-soft.org/api/public/apps'],
[hosts.santinovaRecette, 'recette', 'SANTINOVA', 'https://dashboard-santinova.recette.santinova-soft.org/api/public/apps'],
[hosts.santelisRecette, 'recette', 'SANTELIS', 'https://dashboard-santinova.recette.santinova-soft.org/api/public/apps'],
[hosts.itinovaProd, 'prod', 'ITINOVA', 'https://dashboard-santinova.santinova-soft.org/api/public/apps'],
[hosts.soinsProd, 'prod', 'SOINS_ET_SANTE', 'https://dashboard-santinova.santinova-soft.org/api/public/apps'],
[hosts.santinovaProd, 'prod', 'SANTINOVA', 'https://dashboard-santinova.santinova-soft.org/api/public/apps'],
[hosts.santelisProd, 'prod', 'SANTELIS', 'https://dashboard-santinova.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, env, group] of expected) {
const { sandbox, elements } = loadPortal(host);
sandbox.renderPortalHubNavigation();
const navigation = elements.get('portal-hub-nav');
const expectedVisible = group === 'SANTINOVA';
assert.equal(navigation.hidden, !expectedVisible, `${host} doit ${expectedVisible ? '' : 'ne pas '}afficher la navigation centrale`);
if (expectedVisible) {
for (const portal of ['portail-itinova', 'portail-soins-et-sante', 'portail-santelis']) {
const suffix = env === 'recette' ? '.recette' : '';
assert.match(navigation.innerHTML, new RegExp(`https://${portal}${suffix}\\.santinova-soft\\.org/`), `${host} doit accéder à ${portal} dans le même environnement`);
}
assert.doesNotMatch(navigation.innerHTML, /portail-santinova/, `${host} ne doit pas contenir un lien vers lui-même`);
} else {
assert.equal(navigation.innerHTML, '', `${host} ne doit rendre aucun lien vers les autres portails`);
}
}
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-itinova.recette.santinova-soft.org' } },
{ id: 'gestion-astreintes-had', category: 'SOINS_ET_SANTE', urls: { recette: 'https://astreintes-had-soins-et-sante.recette.santinova-soft.org' } },
{ id: 'demat-facturation-dsi', category: 'SANTINOVA', urls: { recette: 'https://demat-facturation-santinova.recette.santinova-soft.org' } },
{ id: 'referentiel-ia', category: 'INFRA', urls: { recette: 'https://referentiel-ia-santinova.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');