Files
kanbante/tests/test_draco_barre.py
T
Spark d419fe3086
CI / test (push) Successful in 2m21s
fix(0.32.2): résolution par requête des URL dracodev, base agent dérivée et CSP du panneau
La dérivation 0.32.1 ne couvrait que les URL sœurs depuis
AGENT_EVENEMENTS_URL, jamais agent_url : sur la dev, le panneau
visait le défaut prod (agent injoignable, prod non installée),
console morte. Désormais, pour tout rendu à une requête (barre,
iframe du panneau, CSP frame-src) : explicite > hôte public au
motif fermé dracodev > dérivation élargie (événements ou base du
service) > défaut officiel. Contextes sans requête inchangés
sinon ; aucun SSO ajouté.
2026-10-26 02:07:46 +00:00

423 lines
16 KiB
Python

"""Fusion ACC-1 — barre d'apps commune (carte kanbante#28).
Le snippet figé de dracodev-commons est vendangé (CSS/JS statiques +
partiel _draco_bar.html) et inclus par le cadre partagé _header.html :
toutes les pages applicatives l'affichent avec Kanbante comme app
courante ; l'écran de connexion, autonome, ne l'affiche pas.
"""
def test_barre_presente_sur_le_dashboard(client):
r = client.get("/")
assert r.status_code == 200
assert 'class="draco-bar"' in r.text
assert 'data-current="kanbante"' in r.text
assert "draco-barre.css" in r.text
assert "draco-barre.js" in r.text
assert 'window.DRACO_BAR_CONFIG' in r.text
# Les quatre applications de la barre, aux URL publiques par
# défaut (noms officiels/prod, issus de la configuration).
for url in ("https://og.dracodev.net/", "https://flowdeck.dracodev.net/",
"https://tiktak.dracodev.net/", "https://kanbante.dracodev.net/"):
assert url in r.text
# L'adresse du service d'agents est injectée au panneau.
assert 'window.DRACO_PANNEAU_CONFIG = { api: "https://agent.dracodev.net" }' in r.text
def test_barre_et_panneau_urls_par_configuration(monkeypatch, fake_gitea):
# Dev/prod (0.32.0) : la dev déclare ses noms en -dev par
# l'environnement ; aucun nom public n'est codé en dur.
from fastapi.testclient import TestClient
import app.main as main
monkeypatch.setenv("DRACO_SSO_URL", "https://og-dev.dracodev.net")
monkeypatch.setenv("DRACO_FLOWDECK_URL", "https://flowdeck-dev.dracodev.net")
monkeypatch.setenv("DRACO_TIKTAK_URL", "https://tiktak-dev.dracodev.net")
monkeypatch.setenv("KANBANTE_PUBLIC_URL", "https://kanbante-dev.dracodev.net")
monkeypatch.setenv("AGENT_URL", "https://agent-dev.dracodev.net")
with TestClient(main.app) as client:
r = client.get("/")
assert r.status_code == 200
for url in ("https://og-dev.dracodev.net/",
"https://flowdeck-dev.dracodev.net/",
"https://tiktak-dev.dracodev.net/",
"https://kanbante-dev.dracodev.net/"):
assert url in r.text
assert 'api: "https://agent-dev.dracodev.net"' in r.text
# Le JS statique du panneau ne contient aucun nom en dur :
# il lit l'adresse injectée par l'hôte.
js = client.get("/static/js/draco-panneau.js").text
assert "DRACO_PANNEAU_CONFIG" in js
assert "dracodev.net" not in js
def test_config_agent_evenements_derive_de_agent_url(monkeypatch):
from app.config import _env_settings
monkeypatch.setenv("AGENT_URL", "https://agent-dev.dracodev.net")
monkeypatch.delenv("AGENT_EVENEMENTS_URL", raising=False)
s = _env_settings()
assert s.agent_url == "https://agent-dev.dracodev.net"
assert s.agent_evenements_url == (
"https://agent-dev.dracodev.net/api/v1/evenements"
)
# L'URL d'événements explicite garde la priorité (override compose).
monkeypatch.setenv(
"AGENT_EVENEMENTS_URL", "https://exemple.invalid/journal"
)
assert _env_settings().agent_evenements_url == (
"https://exemple.invalid/journal"
)
# ── Dérivation d'environnement des URL sœurs (0.32.1) ──
# Sur la dev, seul AGENT_EVENEMENTS_URL est posé : les URL sœurs
# doivent suivre son suffixe (-dev) sans quatre variables de plus.
_ENV_URLS_SOEURS = (
"DRACO_SSO_URL", # ObsiGate (émettrice SSO)
"DRACO_FLOWDECK_URL",
"DRACO_TIKTAK_URL",
"KANBANTE_PUBLIC_URL", # Kanbante elle-même
"AGENT_URL",
)
_DEFAUTS_OFFICIELS = (
"https://og.dracodev.net",
"https://flowdeck.dracodev.net",
"https://tiktak.dracodev.net",
"https://kanbante.dracodev.net",
)
_URLS_DEV = (
"https://og-dev.dracodev.net",
"https://flowdeck-dev.dracodev.net",
"https://tiktak-dev.dracodev.net",
"https://kanbante-dev.dracodev.net",
)
def _nettoyer_env_soeurs(monkeypatch):
for nom in _ENV_URLS_SOEURS:
monkeypatch.delenv(nom, raising=False)
def _urls_soeurs(settings):
return (
settings.sso_url,
settings.flowdeck_url,
settings.tiktak_url,
settings.public_url,
)
def test_config_urls_soeurs_derivees_du_seul_agent_evenements(monkeypatch):
from app.config import _env_settings
_nettoyer_env_soeurs(monkeypatch)
monkeypatch.setenv(
"AGENT_EVENEMENTS_URL",
"https://agent-dev.dracodev.net/api/v1/evenements",
)
assert _urls_soeurs(_env_settings()) == _URLS_DEV
def test_barre_urls_soeurs_derivees_du_seul_agent_evenements(
monkeypatch, fake_gitea
):
from fastapi.testclient import TestClient
import app.main as main
_nettoyer_env_soeurs(monkeypatch)
monkeypatch.setenv(
"AGENT_EVENEMENTS_URL",
"https://agent-dev.dracodev.net/api/v1/evenements",
)
with TestClient(main.app) as client:
r = client.get("/")
assert r.status_code == 200
for url in _URLS_DEV:
assert f"{url}/" in r.text
def test_config_url_soeur_explicite_gagne_sur_la_derivation(monkeypatch):
from app.config import _env_settings
_nettoyer_env_soeurs(monkeypatch)
monkeypatch.setenv(
"AGENT_EVENEMENTS_URL",
"https://agent-dev.dracodev.net/api/v1/evenements",
)
monkeypatch.setenv(
"DRACO_FLOWDECK_URL", "https://flowdeck.example.test"
)
s = _env_settings()
assert s.flowdeck_url == "https://flowdeck.example.test"
# Les autres sœurs restent dérivées de l'environnement.
assert s.sso_url == "https://og-dev.dracodev.net"
assert s.tiktak_url == "https://tiktak-dev.dracodev.net"
assert s.public_url == "https://kanbante-dev.dracodev.net"
def test_config_hote_agent_hors_motif_reste_aux_defauts(monkeypatch):
from app.config import _env_settings
_nettoyer_env_soeurs(monkeypatch)
for url_agent in (
"https://agent.example.com/api/v1/evenements",
"https://agent-dev.autre-domaine.net/api/v1/evenements",
"http://192.168.30.101:8790/api/v1/evenements",
"http://127.0.0.1:8790/api/v1/evenements",
):
monkeypatch.setenv("AGENT_EVENEMENTS_URL", url_agent)
assert _urls_soeurs(_env_settings()) == _DEFAUTS_OFFICIELS
def test_config_aucun_env_reste_aux_defauts_officiels(monkeypatch):
from app.config import _env_settings
_nettoyer_env_soeurs(monkeypatch)
monkeypatch.delenv("AGENT_EVENEMENTS_URL", raising=False)
assert _urls_soeurs(_env_settings()) == _DEFAUTS_OFFICIELS
def test_statiques_de_la_barre_servis(client):
assert client.get("/static/css/draco-barre.css").status_code == 200
assert client.get("/static/js/draco-barre.js").status_code == 200
def test_menu_des_applications_par_la_marque(client):
# v0.1.3 : la marque ✦ porte data-act="apps" et le JS commun
# construit le menu (.draco-apps-menu) ; le logo passe à 24 px.
accueil = client.get("/")
assert 'data-act="apps"' in accueil.text
# 0.23.3 : assets versionnés, sinon le cache garde l'ancien JS.
assert "draco-barre.css?v=" in accueil.text
assert "draco-barre.js?v=" in accueil.text
js = client.get("/static/js/draco-barre.js").text
assert "draco-apps-menu" in js
css = client.get("/static/css/draco-barre.css").text
assert "font-size: 24px" in css
def test_barre_pleine_largeur_et_palier_mobile(client):
# 0.23.1 : la barre est fixe pleine largeur (le padding du cadre
# ne la repousse plus) et le palier mobile commons v0.1.2 ne
# garde que l'onglet de l'app courante.
style = client.get("/static/css/style.css").text
assert "body.has-draco-bar .draco-bar" in style
assert "position: fixed" in style
barre = client.get("/static/css/draco-barre.css").text
assert '.draco-bar[data-current="kanbante"] .draco-apptab[data-app="kanbante"]' in barre
def test_pas_de_barre_sur_le_login(monkeypatch, fake_gitea):
from fastapi.testclient import TestClient
import app.main as main
monkeypatch.setenv("KANBANTE_AUTH_USER", "bruno")
monkeypatch.setenv("KANBANTE_AUTH_PASSWORD", "motdepasse")
with TestClient(main.app) as client:
r = client.get("/login")
assert r.status_code == 200
assert "draco-bar" not in r.text
# ── Résolution par requête et base de l'agent dérivée (0.32.2) ──
# Couche fautive corrigée : la dérivation 0.32.1 ne couvrait que les
# URL sœurs depuis AGENT_EVENEMENTS_URL — jamais agent_url, donc le
# panneau visait le défaut prod (agent injoignable : la prod n'est
# pas installée) et la console était une page morte sur la dev.
# Aucune résolution ne tenait compte de l'hôte de la requête, et
# aucune CSP n'énonçait l'origine de l'agent.
_ENV_RESOLUTION = _ENV_URLS_SOEURS + ("AGENT_EVENEMENTS_URL",)
def _nettoyer_env_resolution(monkeypatch):
for nom in _ENV_RESOLUTION:
monkeypatch.delenv(nom, raising=False)
def test_config_base_agent_derivee_du_seul_agent_evenements(monkeypatch):
# Le cas réel de la dev : seul le journal est déclaré en -dev.
# La base du service (panneau/console) doit suivre, sinon
# l'iframe de la console charge l'agent prod, injoignable.
from app.config import _env_settings
_nettoyer_env_resolution(monkeypatch)
monkeypatch.setenv(
"AGENT_EVENEMENTS_URL",
"https://agent-dev.dracodev.net/api/v1/evenements",
)
s = _env_settings()
assert s.agent_url == "https://agent-dev.dracodev.net"
assert _urls_soeurs(s) == _URLS_DEV
def test_config_derivation_elargie_a_la_base_du_service(monkeypatch):
# Élargie : si l'URL d'événements est hors motif mais que la
# base du service suit le motif, les sœurs dérivent de la base.
from app.config import _env_settings
_nettoyer_env_resolution(monkeypatch)
monkeypatch.setenv("AGENT_URL", "https://agent-dev.dracodev.net")
monkeypatch.setenv(
"AGENT_EVENEMENTS_URL", "https://journal.example.test/evenements"
)
s = _env_settings()
assert s.agent_url == "https://agent-dev.dracodev.net"
assert s.agent_evenements_url == (
"https://journal.example.test/evenements"
)
assert _urls_soeurs(s) == _URLS_DEV
def test_suffixe_environnement_requete_motif_ferme():
from app.config import suffixe_environnement_requete
assert suffixe_environnement_requete("kanbante-dev.dracodev.net") == "-dev"
assert suffixe_environnement_requete("kanbante.dracodev.net") == ""
assert suffixe_environnement_requete("og-dev.dracodev.net") == "-dev"
assert suffixe_environnement_requete("agent-dev.dracodev.net:8790") == "-dev"
assert suffixe_environnement_requete("Kanbante-Dev.Dracodev.Net") == "-dev"
# Hors motif : jamais de suffixe inventé depuis un hôte arbitraire.
assert suffixe_environnement_requete("192.168.30.101:8082") is None
assert suffixe_environnement_requete("127.0.0.1") is None
assert suffixe_environnement_requete("kanbante-dev.autre-domaine.net") is None
assert suffixe_environnement_requete("dev.dracodev.net") is None
assert suffixe_environnement_requete("testserver") is None
assert suffixe_environnement_requete("") is None
assert suffixe_environnement_requete(None) is None
def test_requete_hote_dev_sans_env_tout_en_dev(monkeypatch, fake_gitea):
# Aucune variable d'environnement : barre, panneau (source de
# l'iframe de la console) et CSP entièrement en -dev, cohérents.
from fastapi.testclient import TestClient
import app.main as main
_nettoyer_env_resolution(monkeypatch)
with TestClient(main.app) as client:
r = client.get("/", headers={"Host": "kanbante-dev.dracodev.net"})
assert r.status_code == 200
for url in _URLS_DEV:
assert f"{url}/" in r.text
# Source de l'iframe du panneau : l'origine injectée au JS.
assert 'api: "https://agent-dev.dracodev.net"' in r.text
# La CSP de la même réponse énonce la même origine résolue.
assert r.headers["content-security-policy"] == (
"frame-src 'self' https://agent-dev.dracodev.net"
)
# Aucun nom prod ne subsiste dans le rendu.
for url in _DEFAUTS_OFFICIELS:
assert f"{url}/" not in r.text
assert 'api: "https://agent.dracodev.net"' not in r.text
def test_requete_hote_prod_sans_env_noms_officiels(monkeypatch, fake_gitea):
from fastapi.testclient import TestClient
import app.main as main
_nettoyer_env_resolution(monkeypatch)
with TestClient(main.app) as client:
r = client.get("/", headers={"Host": "kanbante.dracodev.net"})
assert r.status_code == 200
for url in _DEFAUTS_OFFICIELS:
assert f"{url}/" in r.text
assert 'api: "https://agent.dracodev.net"' in r.text
assert r.headers["content-security-policy"] == (
"frame-src 'self' https://agent.dracodev.net"
)
def test_requete_hote_lan_repli_configuration(monkeypatch, fake_gitea):
# Hôte hors motif (LAN/IP) : repli sur la configuration, ici
# sans variable — donc les défauts officiels, jamais devinés.
from fastapi.testclient import TestClient
import app.main as main
_nettoyer_env_resolution(monkeypatch)
with TestClient(main.app) as client:
r = client.get("/", headers={"Host": "192.168.30.101:8082"})
assert r.status_code == 200
for url in _DEFAUTS_OFFICIELS:
assert f"{url}/" in r.text
assert 'api: "https://agent.dracodev.net"' in r.text
assert r.headers["content-security-policy"] == (
"frame-src 'self' https://agent.dracodev.net"
)
def test_requete_hote_lan_repli_derivation_configuration(
monkeypatch, fake_gitea
):
# Hôte hors motif mais configuration en -dev (journal seul) :
# le repli est la dérivation de configuration, panneau inclus.
from fastapi.testclient import TestClient
import app.main as main
_nettoyer_env_resolution(monkeypatch)
monkeypatch.setenv(
"AGENT_EVENEMENTS_URL",
"https://agent-dev.dracodev.net/api/v1/evenements",
)
with TestClient(main.app) as client:
r = client.get("/", headers={"Host": "192.168.30.101:8082"})
assert r.status_code == 200
for url in _URLS_DEV:
assert f"{url}/" in r.text
assert 'api: "https://agent-dev.dracodev.net"' in r.text
assert r.headers["content-security-policy"] == (
"frame-src 'self' https://agent-dev.dracodev.net"
)
def test_requete_explicite_gagne_sur_hote(monkeypatch, fake_gitea):
# L'explicite reste prioritaire, URL par URL : Flowdeck et
# l'agent explicites gagnent sur l'hôte dev ; les autres
# sœurs suivent l'hôte.
from fastapi.testclient import TestClient
import app.main as main
_nettoyer_env_resolution(monkeypatch)
monkeypatch.setenv("DRACO_FLOWDECK_URL", "https://flowdeck.example.test")
monkeypatch.setenv("AGENT_URL", "https://agent.example.test")
with TestClient(main.app) as client:
r = client.get("/", headers={"Host": "kanbante-dev.dracodev.net"})
assert r.status_code == 200
assert "https://flowdeck.example.test/" in r.text
assert "https://flowdeck-dev.dracodev.net/" not in r.text
assert 'api: "https://agent.example.test"' in r.text
# La CSP consomme l'origine explicite, comme l'iframe.
assert r.headers["content-security-policy"] == (
"frame-src 'self' https://agent.example.test"
)
for url in (
"https://og-dev.dracodev.net/",
"https://tiktak-dev.dracodev.net/",
"https://kanbante-dev.dracodev.net/",
):
assert url in r.text
def test_resolution_sans_requete_inchangee(monkeypatch):
# Contextes sans requête : la résolution par configuration
# seule, comme avant (défauts officiels sans environnement).
from app.config import _env_settings, resoudre_urls_draco
_nettoyer_env_resolution(monkeypatch)
urls = resoudre_urls_draco()
assert urls.sso_url == "https://og.dracodev.net"
assert urls.flowdeck_url == "https://flowdeck.dracodev.net"
assert urls.tiktak_url == "https://tiktak.dracodev.net"
assert urls.public_url == "https://kanbante.dracodev.net"
assert urls.agent_url == "https://agent.dracodev.net"
assert _env_settings().agent_url == "https://agent.dracodev.net"