Honore les entetes du proxy, URI de retour testee, contraste du bouton de connexion
build / Garde-fou (pull_request) Successful in 9m29s
build / Images Harbor (catalog-sync, Dockerfile.catalog-sync) (pull_request) Skipped
build / Images Harbor (web, Dockerfile) (pull_request) Skipped
build / Images Harbor (catalog-sync, Dockerfile.catalog-sync) (push) Successful in 9m40s
build / Garde-fou (push) Successful in 9m29s
build / Images Harbor (web, Dockerfile) (push) Successful in 13m6s

This commit is contained in:
hcornet committed 2026-09-10 21:30:47 +02:00
1 parent 5766e24213
commit 5659c32b66
15 files changed
+289 -26

No files matched your search

+114
View File
@@ -0,0 +1,114 @@
/* Test de l'editeur dans un DOM headless.
*
* Deux defauts d'interface ont echappe aux tests Python : un lien casse vers le
* script, puis une propriete en lecture seule qui interrompait le rendu des
* modeles de service. Aucun des deux ne produisait d'erreur cote serveur.
*
* Ce script charge la page reelle, execute le vrai editeur, clique sur chaque
* entree du rail et verifie que la fenetre principale se remplit. Toute
* exception non rattrapee fait echouer le test.
*
* node tests/interface.mjs (la page est fournie sur l'entree standard)
*/
import { readFileSync } from 'node:fs';
import { JSDOM } from 'jsdom'; // NODE_PATH=tests/node_modules
const html = readFileSync(process.argv[2], 'utf8');
const script = readFileSync('enclume/static/editeur.js', 'utf8');
const erreurs = [];
const dom = new JSDOM(html, {
runScripts: 'outside-only',
url: 'https://enclume.test/',
beforeParse(fenetre) {
fenetre.addEventListener('error', (e) => erreurs.push(String(e.error || e.message)));
},
});
const { window } = dom;
// Reponses minimales mais de la bonne forme, comme celles du serveur.
const reponses = {
'/api/packs': [],
'/api/generer': { summary: {}, issues: [], files: { 'deploy.clapi': '' } },
'/api/projets': [],
};
window.fetch = (url) => Promise.resolve({
ok: true,
json: () => Promise.resolve(reponses[String(url).split('?')[0]] ?? {}),
});
window.alert = () => {};
window.confirm = () => true;
function executer(code, etape) {
try {
window.eval(code);
} catch (erreur) {
erreurs.push(`${etape} : ${erreur.message}`);
}
}
executer(script, 'chargement du script');
const document = window.document;
const rail = document.getElementById('rail');
const principal = document.getElementById('main');
if (!rail || !principal) {
erreurs.push('le rail ou la fenetre principale est absent du gabarit');
}
// Chaque type d'objet doit se creer, s'afficher, et remplir la fenetre.
const types = ['command', 'stpl', 'htpl', 'host'];
if (erreurs.length === 0) {
for (const type of types) {
const ajouter = [...rail.querySelectorAll('button')].filter((b) => b.textContent === '+');
const index = types.indexOf(type);
try {
ajouter[index].click();
} catch (erreur) {
erreurs.push(`ajout d'un objet de type ${type} : ${erreur.message}`);
continue;
}
if (principal.children.length === 0) {
erreurs.push(`type ${type} : la fenetre principale est restee vide`);
}
}
// Puis on reparcourt chaque entree du rail, comme le ferait un utilisateur.
for (const entree of rail.querySelectorAll('.rail-item')) {
principal.innerHTML = '';
try {
entree.click();
} catch (erreur) {
erreurs.push(`clic sur ${entree.textContent.trim()} : ${erreur.message}`);
continue;
}
if (principal.children.length === 0) {
erreurs.push(`entree ${entree.textContent.trim()} : aucun contenu affiche`);
}
}
// Et les boutons de la barre d'outils ne doivent pas lever d'exception.
for (const identifiant of ['btn-pack', 'btn-generer', 'btn-vider']) {
const bouton = document.getElementById(identifiant);
if (!bouton) {
erreurs.push(`bouton absent : ${identifiant}`);
continue;
}
try {
bouton.click();
} catch (erreur) {
erreurs.push(`${identifiant} : ${erreur.message}`);
}
}
}
if (erreurs.length) {
console.error('ECHEC de l\'interface :');
for (const erreur of erreurs) console.error(' - ' + erreur);
process.exit(1);
}
console.log('interface : les quatre types d\'objets s\'affichent, les boutons repondent');
+21
View File
@@ -0,0 +1,21 @@
#!/bin/sh
# Rend la page reelle, puis la fait vivre dans un DOM headless.
# Depuis la racine du depot : sh tests/interface.sh
set -eu
if [ ! -d tests/node_modules ]; then
echo "installation de jsdom"
(cd tests && npm install --silent --no-audit --no-fund)
fi
PAGE=$(mktemp /tmp/enclume-page-XXXXXX.html)
trap 'rm -f "$PAGE"' EXIT
ENCLUME_CATALOG=${ENCLUME_CATALOG:-catalog} python3 - "$PAGE" <<'PY'
import sys
from enclume.webapp import create_app
with open(sys.argv[1], "w", encoding="utf-8") as sortie:
sortie.write(create_app().test_client().get("/").data.decode())
PY
NODE_PATH=tests/node_modules node tests/interface.mjs "$PAGE"
+9
View File
@@ -0,0 +1,9 @@
{
"name": "enclume-tests-interface",
"private": true,
"description": "DOM headless pour verifier que l'editeur repond reellement",
"type": "module",
"dependencies": {
"jsdom": "^25.0.0"
}
}
+69
View File
@@ -319,3 +319,72 @@ def test_alembic_lit_la_configuration_de_l_application():
env = Path("migrations/env.py").read_text(encoding="utf-8")
assert "Config.SQLALCHEMY_DATABASE_URI" in env
assert 'os.environ.get("ENCLUME_DATABASE_URL"' not in env
@pytest.fixture()
def app_oidc(tmp_path, monkeypatch):
"""Application avec un fournisseur OIDC declare, sans reseau : Authlib ne
contacte le fournisseur qu'au moment de la redirection."""
monkeypatch.setenv("ENCLUME_DATABASE_URL", f"sqlite:///{tmp_path/'oidc.db'}")
monkeypatch.setenv("ENCLUME_CATALOG", "catalog")
monkeypatch.setenv("ENCLUME_SECRET_KEY", "cle-de-test")
monkeypatch.setenv("ENCLUME_OIDC_METADATA", "https://authentik.example/application/o/enclume/.well-known/openid-configuration")
monkeypatch.setenv("ENCLUME_OIDC_CLIENT_ID", "identifiant")
monkeypatch.setenv("ENCLUME_OIDC_CLIENT_SECRET", "secret")
import enclume.auth
import enclume.config
import enclume.db
import enclume.routes_donnees
import enclume.webapp
for module in (enclume.config, enclume.db, enclume.auth, enclume.routes_donnees, enclume.webapp):
importlib.reload(module)
application = enclume.webapp.create_app()
with application.app_context():
enclume.db.db.create_all()
return application
def test_uri_de_retour_respecte_l_url_publique(app_oidc, monkeypatch):
"""L'URI annoncee au fournisseur doit correspondre a celle declaree cote Authentik."""
from enclume import auth
from enclume.config import Config
monkeypatch.setattr(Config, "URL_PUBLIQUE", "https://enclume.example")
with app_oidc.test_request_context("/connexion"):
assert auth.url_de_retour() == "https://enclume.example/connexion/retour"
def test_uri_de_retour_sans_url_publique(app_oidc, monkeypatch):
"""Sans URL publique declaree, l'URI se deduit de la requete."""
from enclume import auth
from enclume.config import Config
monkeypatch.setattr(Config, "URL_PUBLIQUE", "")
with app_oidc.test_request_context("/connexion", base_url="https://enclume.example"):
assert auth.url_de_retour() == "https://enclume.example/connexion/retour"
def test_les_entetes_du_proxy_sont_honores(app_oidc):
"""Sans ProxyFix, l'application se croit en http sur localhost derriere Traefik,
et l'URI de redirection annoncee au fournisseur devient fausse."""
from werkzeug.middleware.proxy_fix import ProxyFix
assert isinstance(app_oidc.wsgi_app, ProxyFix)
vu = {}
@app_oidc.route("/essai-proxy")
def essai_proxy():
from flask import request
vu["url"] = request.url_root
return "ok"
app_oidc.test_client().get(
"/essai-proxy",
headers={"X-Forwarded-Proto": "https", "X-Forwarded-Host": "enclume.example"},
)
assert vu["url"] == "https://enclume.example/"