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
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:
1 parent
5766e24213
commit
5659c32b66
15 files changed
+289
-26
No files matched your search
@@ -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');
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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/"
|
||||
Reference in new issue
Block a user