Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir, verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire. 94 preuves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
434 lines
23 KiB
Python
434 lines
23 KiB
Python
#!/usr/bin/env python3
|
|
"""Le GUI REND-IL ? Chaque vue est dessinee pour de vrai, dans un DOM simule.
|
|
|
|
CE QUE `verifier_gui.py` NE POUVAIT PAS VOIR (2026-09-08). Il passe le JS a
|
|
`node --check` : la syntaxe, donc, et rien d'autre. Le formulaire genere des bases
|
|
lisait `data.schema` — or il n'existe aucun `data` global dans cette page ; c'est une
|
|
const locale de `charger()`. Syntaxe parfaite, `ReferenceError` a l'ouverture. Pire dans
|
|
`sauvegarderBases`, ou `const data` est declare PLUS BAS dans la meme fonction : zone
|
|
morte temporelle, l'enregistrement jetait avant meme d'envoyer.
|
|
|
|
Le defaut a survecu a une verification sous node parce que le banc APPELAIT la fonction
|
|
en lui passant `data` — il reproduisait la fonction, pas sa PORTEE. Une garantie qu'on
|
|
n'a jamais vue echouer n'est pas une garantie.
|
|
|
|
Ce banc-ci charge le JS entier, tel que la page le recoit, dans un DOM simule, appelle
|
|
`charger()` sur la VRAIE reponse de l'API, puis dessine CHAQUE vue. Toute reference a un
|
|
identifiant qui n'existe pas leve, et le test echoue.
|
|
|
|
Ce qu'il ne fait PAS : juger l'apparence. Il dit que ca rend, pas que c'est beau.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
RACINE = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(RACINE / "scripts"))
|
|
|
|
import inventory_gui as g # noqa: E402
|
|
|
|
# Un DOM juste assez reel : tout element repond, retient son innerHTML, et n'invente
|
|
# rien. Un `getElementById` qui rendrait `null` masquerait les fautes qu'on cherche.
|
|
BANC = r"""
|
|
'use strict';
|
|
function Elem(id) {
|
|
this.id = id; this.innerHTML = ''; this.textContent = ''; this.value = '';
|
|
this.hidden = false; this.disabled = false; this.title = ''; this.className = '';
|
|
this.style = {}; this.dataset = {}; this.children = [];
|
|
this.classList = {add(){}, remove(){}, toggle(){}, contains(){ return false; }};
|
|
}
|
|
Elem.prototype.appendChild = function (e) { this.children.push(e); return e; };
|
|
Elem.prototype.removeChild = function (e) { return e; };
|
|
Elem.prototype.setAttribute = function () {};
|
|
Elem.prototype.getAttribute = function () { return ''; };
|
|
Elem.prototype.addEventListener = function () {};
|
|
Elem.prototype.querySelector = function () { return null; };
|
|
// `querySelectorAll('[data-champ]')` rend vraiment les controles du dernier rendu : le
|
|
// GUI s'en sert pour valider avant d'enregistrer, et un stub qui rend toujours [] ferait
|
|
// passer cette validation sur ZERO champ — le banc mesurerait alors moins que rien.
|
|
Elem.prototype.querySelectorAll = function (sel) {
|
|
if (!/data-champ/.test(String(sel))) { return []; }
|
|
var html = this.innerHTML || '';
|
|
var out = [], re = /<(input|select)([^>]*?)data-champ="([^"]*)"([^>]*)>/g, m;
|
|
while ((m = re.exec(html)) !== null) {
|
|
var attrs = m[2] + m[4];
|
|
var v = /\svalue="([^"]*)"/.exec(attrs);
|
|
out.push({__champ: m[3], value: v ? v[1] : '',
|
|
getAttribute: function (n) { return n === 'data-champ' ? this.__champ : null; },
|
|
classList: {add: function () {}, remove: function () {}, toggle: function () {}},
|
|
focus: function () {}});
|
|
}
|
|
return out;
|
|
};
|
|
Elem.prototype.focus = function () {};
|
|
Elem.prototype.remove = function () {};
|
|
Elem.prototype.closest = function () { return null; };
|
|
Elem.prototype.scrollIntoView = function () {};
|
|
|
|
var __elems = {};
|
|
var document = {
|
|
getElementById: function (id) { return __elems[id] || (__elems[id] = new Elem(id)); },
|
|
createElement: function () { return new Elem('cree'); },
|
|
querySelector: function () { return null; },
|
|
// `#detail [data-champ]` : le GUI interroge le DOCUMENT, pas l'element. Un stub qui
|
|
// rendait [] ici faisait croire que le formulaire n'etait pas marque — et la
|
|
// sauvegarde des serveurs refusait, silencieusement, dans le banc seul.
|
|
querySelectorAll: function (sel) {
|
|
var m = /^#([\w-]+)\s+(.+)$/.exec(String(sel));
|
|
if (m && __elems[m[1]]) { return __elems[m[1]].querySelectorAll(m[2]); }
|
|
return [];
|
|
},
|
|
addEventListener: function () {},
|
|
body: new Elem('body'),
|
|
documentElement: new Elem('html'),
|
|
};
|
|
var window = {addEventListener: function () {}, location: {href: '', reload: function () {}},
|
|
matchMedia: function () { return {matches: false, addEventListener: function () {}}; }};
|
|
var navigator = {clipboard: {writeText: function () { return Promise.resolve(); }}};
|
|
function MutationObserver(f) { this.observe = function () {}; this.disconnect = function () {}; }
|
|
function requestAnimationFrame(f) { return 0; }
|
|
function cancelAnimationFrame() {}
|
|
function EventSource() { this.addEventListener = function () {}; this.close = function () {}; }
|
|
function alert() {} function confirm() { return false; } function prompt() { return null; }
|
|
var localStorage = {getItem: function () { return null; }, setItem: function () {}, removeItem: function () {}};
|
|
var __PAYLOAD__ = JSON.parse(process.env.PAYLOAD);
|
|
// Les routes en lecture qu'un test sert pour de vrai (lot 2 : le contrat site <-> locataire).
|
|
var __ROUTES__ = JSON.parse(process.env.ROUTES || '{}');
|
|
var __postsCaptures = {};
|
|
// `fetch` rend la VRAIE reponse de /api/inventaire ; les autres routes rendent un objet
|
|
// vide plutot que de jeter, pour que les sondes de fond ne masquent pas l'erreur cherchee.
|
|
function fetch(url, opts) {
|
|
if (opts && opts.method === 'POST') { __postsCaptures[String(url)] = JSON.parse(opts.body); }
|
|
// Un POST rend la MEME charge que l'inventaire : le GUI recharge apres sauvegarde, et
|
|
// lui rendre {} le ferait repartir d'un plan vide — un faux vert, pas un test.
|
|
var corps = (String(url).indexOf('/api/inventaire') === 0 || (opts && opts.method === 'POST'))
|
|
? __PAYLOAD__ : (__ROUTES__[String(url)] || {});
|
|
return Promise.resolve({ok: true, status: 200, json: function () { return Promise.resolve(corps); },
|
|
text: function () { return Promise.resolve(''); }});
|
|
}
|
|
var __erreurs = [];
|
|
function __essayer(quoi, f) { try { f(); } catch (e) { __erreurs.push(quoi + ' : ' + e); } }
|
|
"""
|
|
|
|
APRES = r"""
|
|
// --- L'EPREUVE -----------------------------------------------------------------
|
|
charger().then(function () {
|
|
['serveurs', 'applications', 'bases', 'domaines', 'nomenclature',
|
|
'integrations', 'flux', 'couches', 'reseau'].forEach(function (v) {
|
|
__essayer('setVue(' + v + ')', function () { setVue(v); });
|
|
});
|
|
// Les vues a maitre-detail ne montrent leur formulaire QU'UNE FOIS une entree
|
|
// choisie : dessiner la grille seule ne prouve rien du formulaire genere.
|
|
__essayer('bases + selection', function () {
|
|
setVue('bases');
|
|
if (typeof bdApplis !== 'undefined' && bdApplis.length) { choisirBase('bd:0'); }
|
|
if (typeof bdServeurs !== 'undefined' && bdServeurs.length) { choisirBase('srv:0'); }
|
|
});
|
|
__essayer('nomenclature + selection', function () {
|
|
setVue('nomenclature');
|
|
var f = Object.keys(nomEdit.fonctions || {});
|
|
var c = Object.keys(nomEdit.categories || {});
|
|
if (f.length) { choisirNom('fon:' + f[0]); }
|
|
if (c.length) { choisirNom('cat:' + c[0]); }
|
|
});
|
|
__essayer('domaines + selection', function () { setVue('domaines'); if (domaines.length) choisirDomaine(0); });
|
|
__essayer('applications + selection', function () { setVue('applications'); if (applications.length) ouvrirApp(applications[0].id); });
|
|
console.log('__RESULTAT__' + JSON.stringify(__erreurs));
|
|
}).catch(function (e) {
|
|
console.log('__RESULTAT__' + JSON.stringify(['charger() : ' + (e && e.stack || e)]));
|
|
});
|
|
"""
|
|
|
|
|
|
# LE BANC D'UNE CONSOLE DE SITE. On ne rejoue pas les neuf vues : on regarde CE QUE
|
|
# la vue d'accueil dessine, parce que c'est la qu'un site ne montrait rien.
|
|
APRES_SITE = r"""
|
|
charger().then(function () {
|
|
var sortie = {erreurs: [], grille: '', detail: '', detailChoisi: '', hotes: 0};
|
|
try {
|
|
sortie.hotes = (typeof hotes !== 'undefined' && hotes) ? hotes.length : 0;
|
|
setVue('serveurs');
|
|
sortie.grille = document.getElementById('grilles').innerHTML;
|
|
sortie.detail = document.getElementById('detail').innerHTML;
|
|
if (sortie.hotes) { choisirMachine(0); sortie.detailChoisi = document.getElementById('detail').innerHTML; }
|
|
} catch (e) { sortie.erreurs.push(String((e && e.stack) || e)); }
|
|
console.log('__RESULTAT__' + JSON.stringify(sortie));
|
|
}).catch(function (e) {
|
|
console.log('__RESULTAT__' + JSON.stringify({erreurs: ['charger() : ' + ((e && e.stack) || e)]}));
|
|
});
|
|
"""
|
|
|
|
|
|
# LE BANC DU CONTRAT (lot 2). Les deux vues lisent leur route a l'ouverture : on attend
|
|
# leur promesse, sinon on lirait l'ecran « Lecture… » et on croirait la vue dessinee.
|
|
APRES_CONTRAT = r"""
|
|
charger().then(async function () {
|
|
var o = {erreurs: [], lien: '', lienDetail: '', locataires: '', locDetail: ''};
|
|
try {
|
|
vuePrincipale = 'lien'; lienCharge = null; await dessinerLienSite();
|
|
o.lien = document.getElementById('grilles').innerHTML;
|
|
o.lienDetail = document.getElementById('detail').innerHTML;
|
|
vuePrincipale = 'locataires'; locatairesCharge = null; await dessinerLocataires();
|
|
o.locataires = document.getElementById('grilles').innerHTML;
|
|
o.locDetail = document.getElementById('detail').innerHTML;
|
|
vuePrincipale = 'a-traiter'; aTraiterCharge = null; await dessinerATraiter();
|
|
o.veille = document.getElementById('grilles').innerHTML;
|
|
o.veilleDetail = document.getElementById('detail').innerHTML;
|
|
} catch (e) { o.erreurs.push(String((e && e.stack) || e)); }
|
|
console.log('__RESULTAT__' + JSON.stringify(o));
|
|
}).catch(function (e) {
|
|
console.log('__RESULTAT__' + JSON.stringify({erreurs: ['charger() : ' + ((e && e.stack) || e)]}));
|
|
});
|
|
"""
|
|
|
|
|
|
class TestFormeDesCharges(unittest.TestCase):
|
|
"""Les deux charges que la page consomme ont-elles la MEME FORME ?
|
|
|
|
`ConsoleLocataire` et `ConsoleSite` servent la meme page : ce qu'elle sait lire d'un
|
|
cote, elle doit pouvoir le lire de l'autre. Depuis le decoupage en classes, c'est le
|
|
TRONC qui assemble — une seule `charge()` pour les deux — donc cette garde ne devrait
|
|
plus jamais se declencher. On la garde parce qu'une branche peut surcharger `charge()`
|
|
un jour, et parce qu'un test qui ne se declenche plus est precisement celui qui
|
|
prouve que la refonte tient.
|
|
|
|
Le 2026-09-20, `serveurs` valait `[]` d'un cote et `{}` de l'autre. Les deux sont
|
|
« vides », et la page ne les lit pas pareil : `(data.serveurs || []).map(...)` trouve
|
|
une table — truthy, sans `.map` — et `charger()` LEVE. La console d'un site mourait
|
|
avant de dessiner, et l'erreur accusait une methode manquante plutot qu'une forme.
|
|
|
|
Un vide doit avoir la FORME de ce qu'il remplace. Ce test ne demande pas que les deux
|
|
charges portent les memes VALEURS — un site n'a pas de plan de locataire — seulement
|
|
qu'elles portent les memes TYPES. Ce test n'a pas besoin de node.
|
|
"""
|
|
|
|
# Ce que les deux charges ne partagent legitimement pas, et pourquoi.
|
|
ECARTS_ADMIS = {
|
|
"contexte": "ajoute par la route GET pour un locataire, deja present pour un site",
|
|
"intrants": "un site n'a pas de plan de locataire dont tirer des intrants",
|
|
}
|
|
|
|
def test_les_deux_charges_portent_les_memes_types(self):
|
|
if not g.FICHIER_UNDERLAY.exists():
|
|
self.skipTest("aucune fabric montee : pas de charge de site a comparer")
|
|
site = g.ConsoleSite(g._depot_hebergeur(), True).charge()
|
|
tenant = g.ConsoleLocataire(g.INSTANCE, g.INVENTAIRE_DEFAUT, True).charge()
|
|
|
|
ecarts = []
|
|
for clef in sorted(set(tenant) & set(site)):
|
|
if clef in self.ECARTS_ADMIS:
|
|
continue
|
|
a, b = type(tenant[clef]).__name__, type(site[clef]).__name__
|
|
if a != b:
|
|
ecarts.append(f"{clef} : locataire={a}, site={b}")
|
|
self.assertEqual(ecarts, [],
|
|
"des clefs changent de type selon la console :\n - "
|
|
+ "\n - ".join(ecarts))
|
|
|
|
def test_un_ecart_admis_doit_encore_exister(self):
|
|
"""Une exemption qui survit a son objet est un mensonge tranquille."""
|
|
tenant = g.inventaire_api(g.INVENTAIRE_DEFAUT)
|
|
for clef in self.ECARTS_ADMIS:
|
|
self.assertTrue(clef in tenant or clef == "contexte",
|
|
f"{clef} n'existe plus : son exemption n'a plus d'objet")
|
|
|
|
|
|
class TestRenduGui(unittest.TestCase):
|
|
"""Chaque vue du GUI se dessine sans lever."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
if not shutil.which("node"):
|
|
raise unittest.SkipTest("node absent")
|
|
html = g.HTML.replace("__JETON__", "TEST")
|
|
blocs = re.findall(r"<script>(.*?)</script>", html, re.S)
|
|
assert blocs, "aucun bloc <script>"
|
|
cls.blocs = blocs
|
|
cls.js = BANC + "\n".join(blocs) + APRES
|
|
|
|
def _jouer(self, js: str, payload: dict, routes: dict | None = None) -> list:
|
|
with tempfile.NamedTemporaryFile("w", suffix=".js", delete=False, encoding="utf-8") as f:
|
|
f.write(js)
|
|
chemin = f.name
|
|
try:
|
|
r = subprocess.run(["node", chemin], capture_output=True, text=True, timeout=120,
|
|
env={**__import__("os").environ, "PAYLOAD": json.dumps(payload),
|
|
"ROUTES": json.dumps(routes or {})})
|
|
finally:
|
|
Path(chemin).unlink(missing_ok=True)
|
|
marque = "__RESULTAT__"
|
|
for ligne in r.stdout.splitlines():
|
|
if ligne.startswith(marque):
|
|
return json.loads(ligne[len(marque):])
|
|
self.fail(f"le banc n'a rien rendu.\nstdout: {r.stdout[-2000:]}\nstderr: {r.stderr[-2000:]}")
|
|
|
|
def test_toutes_les_vues_rendent(self):
|
|
"""La vraie reponse de l'API, les neuf vues, zero exception."""
|
|
payload = g.inventaire_api(g.INVENTAIRE_DEFAUT)
|
|
erreurs = self._jouer(self.js, payload)
|
|
self.assertEqual(erreurs, [], "des vues ont leve :\n - " + "\n - ".join(erreurs))
|
|
|
|
def test_une_console_de_site_montre_ses_machines(self):
|
|
"""UN SITE A DOUZE MACHINES ET SA CONSOLE N'EN MONTRAIT AUCUNE (2026-09-20).
|
|
|
|
`inventaire_api_du_site` sert volontairement `serveurs: {}` — un site declare ses
|
|
machines dans SON plan, avec `ip` et `vmid` ecrits, la ou un locataire les derive.
|
|
Mais la vue d'accueil tirait de ce registre vide le message d'un locataire sans
|
|
serveurs (« + Serveur », `make serveurs-bootstrap`), et les machines servies dans
|
|
`hotes` n'etaient dessinees nulle part.
|
|
|
|
C'est le defaut du 2026-09-16 par une autre porte : la premiere fois l'inventaire
|
|
etait vide ; cette fois il est PLEIN et c'est la vue qui regarde au mauvais endroit.
|
|
"""
|
|
if not g.FICHIER_UNDERLAY.exists():
|
|
self.skipTest("aucune fabric montee : pas de console de site a simuler")
|
|
payload = g.ConsoleSite(g._depot_hebergeur(), True).charge()
|
|
if not payload["hotes"]:
|
|
self.skipTest("l'inventaire du site ne rend aucune machine ici")
|
|
|
|
js = BANC + "\n".join(self.blocs) + APRES_SITE
|
|
r = self._jouer(js, payload)
|
|
self.assertEqual(r["erreurs"], [], "la vue a leve :\n - " + "\n - ".join(r["erreurs"]))
|
|
self.assertEqual(r["hotes"], len(payload["hotes"]))
|
|
|
|
noms = [h["nom"] for h in payload["hotes"]]
|
|
manquants = [n for n in noms if n not in r["grille"]]
|
|
self.assertEqual(manquants, [],
|
|
f"des machines du site ne sont pas dessinees : {manquants}")
|
|
# Et surtout : plus le message d'un locataire vide, qui envoyait l'exploitant
|
|
# vers un amorcage de plan que ce site n'a pas.
|
|
self.assertNotIn("serveurs-bootstrap", r["grille"])
|
|
self.assertIn("Machines de", r["grille"])
|
|
# Le panneau droit dit ce que cette console PEUT, au lieu de rester muet.
|
|
self.assertIn("matérialise", r["detail"])
|
|
self.assertIn(noms[0], r["detailChoisi"])
|
|
|
|
def test_les_deux_bouts_du_contrat_se_dessinent(self):
|
|
"""LOT 2 (2026-10-11) : « Lien avec le site » et « Locataires heberges », servis
|
|
par leurs VRAIES routes sur les depots montes ici. Chaque vue doit montrer le
|
|
commit de la face qu'elle lit — c'est lui que l'autre bout lit."""
|
|
if not g.FICHIER_UNDERLAY.exists() or not g.INSTANCE.exists():
|
|
self.skipTest("il faut un locataire et un site montes")
|
|
lien = g.lien_du_locataire(g.INSTANCE.resolve())
|
|
locs = g.locataires_du_site(g._depot_hebergeur())
|
|
veille = g.veille_locataire(g.INSTANCE.resolve())
|
|
payload = g.inventaire_api(g.INVENTAIRE_DEFAUT)
|
|
js = BANC + "\n".join(self.blocs) + APRES_CONTRAT
|
|
r = self._jouer(js, payload, {"/api/lien-site": lien, "/api/locataires-heberges": locs,
|
|
"/api/a-traiter": {"elements": veille}})
|
|
self.assertEqual(r["erreurs"], [], "une vue a leve :\n - " + "\n - ".join(r["erreurs"]))
|
|
self.assertIn("Ma face réseau", r["lien"])
|
|
if lien["face"]["trace"]["commit"]:
|
|
self.assertIn(lien["face"]["trace"]["commit"], r["lien"])
|
|
self.assertIn("Qui fait quoi", r["lienDetail"])
|
|
for l in locs["locataires"]:
|
|
self.assertIn(l["nom"], r["locataires"])
|
|
if locs["locataires"]:
|
|
self.assertIn("Ce que je ne vois pas", r["locDetail"])
|
|
# LOT 3a : chaque element de la veille est dessine, « ok » compris (replie).
|
|
self.assertIn("À traiter", r["veille"])
|
|
for e in veille:
|
|
self.assertIn(e["titre"].replace("'", "'"), r["veille"].replace("'", "'"))
|
|
self.assertIn(veille[0]["source"], r["veilleDetail"] + r["veille"])
|
|
|
|
def test_une_console_de_site_sert_son_propre_plan(self):
|
|
"""UN SITE A UN PLAN, ET ON LE LUI SERVAIT VIDE (2026-09-20).
|
|
|
|
Cinq registres partaient vides « pour ne pas fabriquer un faux plan de tenant ».
|
|
Le site n'a pas de faux plan : il a LE SIEN, au meme format, que les lecteurs du
|
|
moteur lisent sans broncher. Mesure du jour : 9 serveurs, 21 applications.
|
|
"""
|
|
if not g.FICHIER_UNDERLAY.exists():
|
|
self.skipTest("aucune fabric montee")
|
|
depot = g._depot_hebergeur()
|
|
charge = g.ConsoleSite(depot, True).charge()
|
|
for registre, fichier in (("applications", "applications.yml"),
|
|
("bases", "bases-donnees.yml"),
|
|
("domaines", "domaines.yml")):
|
|
if not (depot / "plan" / fichier).is_file():
|
|
continue
|
|
interne = charge[registre].get(registre) or charge[registre]
|
|
self.assertTrue(interne,
|
|
f"{registre} est servi vide alors que {fichier} en declare")
|
|
if (depot / "plan/serveurs.yml").is_file():
|
|
self.assertTrue(charge["serveurs"],
|
|
"le site declare des serveurs et la charge n'en porte aucun")
|
|
# Et leur FORME est celle que la page sait lire, pas la table du plan.
|
|
self.assertIsInstance(charge["serveurs"], list)
|
|
premier = charge["serveurs"][0]
|
|
for clef in ("nom", "etat", "adresse_ip", "vmid", "statut"):
|
|
self.assertIn(clef, premier)
|
|
|
|
def test_controle_negatif_reference_absente(self):
|
|
"""LE CONTROLE. Une lecture d'identifiant inexistant DOIT faire echouer le banc.
|
|
|
|
Sans lui, ce test passerait aussi bien sur un banc qui n'execute rien. On rejoue
|
|
exactement la faute du 2026-09-08 : le formulaire des bases lit un global qui
|
|
n'existe pas.
|
|
"""
|
|
casse = self.js.replace("const reg = schemaPlan[nomRegistre] || null;",
|
|
"const reg = (donneesQuiNexistentPas.schema || {})[nomRegistre] || null;")
|
|
self.assertNotEqual(casse, self.js, "le motif du controle negatif n'existe plus")
|
|
payload = g.inventaire_api(g.INVENTAIRE_DEFAUT)
|
|
erreurs = self._jouer(casse, payload)
|
|
self.assertTrue(any("donneesQuiNexistentPas" in e for e in erreurs),
|
|
f"le banc n'a pas vu la reference absente : {erreurs}")
|
|
|
|
def test_charger_puis_sauvegarder_ne_change_pas_le_plan(self):
|
|
"""L'EPREUVE DECISIVE : ouvrir une vue et enregistrer sans rien toucher.
|
|
|
|
Les six formulaires sont GENERES, et leur sauvegarde l'est aussi : les champs
|
|
ecrits sont ceux que le schema declare. C'est ce qui evite le pire des deux
|
|
mondes — un champ visible a l'ecran et perdu en silence a l'enregistrement.
|
|
|
|
Encore faut-il le mesurer. On charge la vraie reponse de l'API, on ouvre chaque
|
|
vue, on appelle sa sauvegarde, on intercepte le POST, et on compare la charge au
|
|
plan REEL lu par le moteur. Aucune entite perdue, aucune en trop, aucun champ
|
|
different : sinon un simple aller-retour appauvrirait le plan.
|
|
"""
|
|
js = self.js.replace(APRES, r"""
|
|
charger()
|
|
.then(function () { setVue('serveurs'); choisirServeur(0); return sauvegarderServeurs(); })
|
|
.then(function () { setVue('applications'); if (applications.length) ouvrirApp(applications[0].id); return sauvegarderApplications(); })
|
|
.then(function () { setVue('domaines'); if (domaines.length) choisirDomaine(0); return sauvegarderDomaines(); })
|
|
.then(function () { setVue('bases'); return sauvegarderBases(); })
|
|
.then(function () { console.log('__RESULTAT__' + JSON.stringify(__postsCaptures)); })
|
|
.catch(function (e) { console.log('__RESULTAT__' + JSON.stringify({erreur: String(e && e.stack || e)})); });
|
|
""")
|
|
payload = g.inventaire_api(g.INVENTAIRE_DEFAUT)
|
|
posts = self._jouer(js, payload)
|
|
self.assertNotIn("erreur", posts, posts.get("erreur", ""))
|
|
|
|
attendu = {
|
|
"/api/serveurs": (g.charger_serveurs(g.FICHIER_SERVEURS), "serveurs"),
|
|
"/api/applications": (g.charger_applications(g.FICHIER_APPLICATIONS), "applications"),
|
|
"/api/domaines": (g.charger_domaines(g.FICHIER_DOMAINES), "domaines_publics"),
|
|
"/api/bases": (g.charger_bases_donnees(g.FICHIER_BASES), "bases_donnees"),
|
|
}
|
|
for url, (plan, racine) in attendu.items():
|
|
with self.subTest(route=url):
|
|
envoye = (posts.get(url) or {}).get(racine)
|
|
self.assertIsNotNone(envoye, f"{url} n'a pas ete appele — la sauvegarde a-t-elle refuse ?")
|
|
reel = plan.get(racine) or {}
|
|
self.assertEqual(sorted(envoye), sorted(reel), "entites perdues ou ajoutees")
|
|
for cle in sorted(reel):
|
|
self.assertEqual(envoye[cle], reel[cle],
|
|
f"{racine}.{cle} : l'aller-retour a change l'entite")
|
|
|
|
def test_le_schema_est_bien_servi(self):
|
|
"""Le formulaire ne peut rien generer si l'API n'envoie pas le schema."""
|
|
payload = g.inventaire_api(g.INVENTAIRE_DEFAUT)
|
|
self.assertIn("schema", payload, "l'API ne sert pas le schema du plan")
|
|
self.assertIn("nomenclature", payload["schema"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|