feat: URLs publiques de l'écosystème par configuration — dev/prod sans noms en dur #223
CI / lint (push) Successful in 3m11s
CI / security (push) Failing after 1m55s
CI / test (push) Successful in 5m23s
CI / build (push) Successful in 2m35s
CI / e2e (push) Successful in 18m24s

This commit is contained in:
Spark
2026-10-25 18:10:59 +00:00
parent 287afa1537
commit f29a22886a
21 changed files with 608 additions and 33 deletions
+20 -1
View File
@@ -100,9 +100,28 @@ def test_panneau_console_surimpression_222():
def test_csp_ouvre_le_service_d_agents_218():
# Le panneau parle au service d'agents : la CSP doit nommer son
# hôte en connect-src, sans ouvrir de générique supplémentaire.
# #223 : l'origine n'est plus un littéral dans main.py — elle est
# dérivée de la configuration (backend.draco_config.agent_origine,
# testée via la config dans tests/test_security_headers.py).
main = (Path(__file__).resolve().parent.parent / "backend" / "main.py").read_text(encoding="utf-8")
ligne = next(l for l in main.splitlines() if "connect-src" in l)
assert "https://agent.dracodev.net" in ligne
assert "agent_origine" in main
assert "{agent}" in ligne
def test_urls_ecosysteme_via_injection_hote_223():
# #223 : panneau Spark, liens TikTak et onglets des apps lisent
# window.DRACO_URLS (injecté par l'hôte/backend depuis sa config) ;
# les littéraux restants ne sont que les défauts officiels en repli.
panneau = (FRONTEND / "draco-panneau.js").read_text(encoding="utf-8")
assert "window.DRACO_URLS" in panneau
assert "DRACO_URLS.agent" in panneau
liens = (FRONTEND / "draco-liens.js").read_text(encoding="utf-8")
assert "window.DRACO_URLS" in liens
assert "baseTikTak()" in liens
html = _index()
assert "window.DRACO_URLS" in html
assert "DRACO_URLS[a.dataset.app]" in html
def test_rail_mobile_vers_le_vrai_hamburger():
+115
View File
@@ -0,0 +1,115 @@
"""URLs publiques de l'écosystème par configuration — #223 (dev/prod).
Règle : aucun nom public de l'écosystème n'est figé dans le code
vivant. Défauts = noms **officiels** (production future) ; le dev
déclare ses noms en ``-dev`` dans son seul ``.env``
(``DRACO_*_URL`` / ``AGENT_EVENEMENTS_URL``). On vérifie ici la
config centralisée, la dérivation de l'URL d'événements du journal,
la reconnaissance des liens profonds des deux environnements et la
reconstruction des URL d'affichage des références croisées.
"""
import pytest
from backend import draco_config
@pytest.fixture()
def env_propre(monkeypatch):
"""Aucune variable d'URL d'écosystème posée (défauts officiels)."""
for nom in (
"DRACO_AGENT_URL", "DRACO_OBSIGATE_URL", "DRACO_FLOWDECK_URL",
"DRACO_TIKTAK_URL", "DRACO_KANBANTE_URL", "DRACO_GITEA_URL",
"AGENT_EVENEMENTS_URL",
):
monkeypatch.delenv(nom, raising=False)
return monkeypatch
def test_defauts_officiels_sans_dev(env_propre):
assert draco_config.agent_url() == "https://agent.dracodev.net"
assert draco_config.app_url("tiktak") == "https://tiktak.dracodev.net"
assert draco_config.app_url("obsigate") == "https://og.dracodev.net"
assert draco_config.app_url("flowdeck") == "https://flowdeck.dracodev.net"
assert draco_config.app_url("kanbante") == "https://kanbante.dracodev.net"
assert draco_config.app_url("gitea") == "https://git.dracodev.net"
for url in draco_config.urls_publiques().values():
assert "-dev" not in url
def test_agent_derive_des_evenements(env_propre):
"""Sans DRACO_AGENT_URL, l'origine suit AGENT_EVENEMENTS_URL —
la configuration réelle du poste (dev inclus)."""
env_propre.setenv(
"AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements")
assert draco_config.agent_url() == "https://agent-dev.dracodev.net"
assert draco_config.agent_origine() == "https://agent-dev.dracodev.net"
def test_agent_explicite_prime(env_propre):
env_propre.setenv(
"AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements")
env_propre.setenv("DRACO_AGENT_URL", "https://agent.example.net/")
assert draco_config.agent_url() == "https://agent.example.net"
def test_journal_url_suit_la_config(env_propre):
from backend import agent_journal
assert agent_journal._url() == "https://agent.dracodev.net/api/v1/evenements"
env_propre.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net")
assert agent_journal._url() == "https://agent-dev.dracodev.net/api/v1/evenements"
env_propre.setenv(
"AGENT_EVENEMENTS_URL", "https://autre.example.net/api/v1/evenements")
assert agent_journal._url() == "https://autre.example.net/api/v1/evenements"
# ------------------------------------------------------------ liens profonds
def test_parse_hotes_configures(env_propre):
"""Un lien collé depuis l'environnement dev (hôtes déclarés dans
son .env) est reconnu, comme un lien officiel."""
from backend.external_refs import parse_deep_link
env_propre.setenv("DRACO_TIKTAK_URL", "https://tiktak-dev.dracodev.net")
env_propre.setenv("DRACO_OBSIGATE_URL", "https://og-dev.dracodev.net")
assert parse_deep_link("https://tiktak-dev.dracodev.net/?task=5") == (
"tiktak", "task", "5", "TikTak · tâche 5")
assert parse_deep_link("https://og-dev.dracodev.net/#file=v:Dossier/Note.md") == (
"obsigate", "note", "v:Dossier/Note.md", "ObsiGate · Note.md")
# L'officiel reste reconnu en parallèle (lien venu de la prod).
assert parse_deep_link("https://tiktak.dracodev.net/?task=5") is not None
def test_url_affichage_reconstruite_depuis_config(env_propre, tmp_path):
"""#223 : la référence stockée garde son URL absolue d'origine
(aucune migration), mais l'URL **servie** est reconstruite depuis
la config courante."""
from backend.external_refs import add_ref, build_deep_link, list_refs
env_propre.setenv("OBSIGATE_EXTERNAL_REFS_DIR", str(tmp_path / "refs"))
assert build_deep_link("tiktak", "task", "42") == \
"https://tiktak.dracodev.net/?task=42"
assert build_deep_link("kanbante", "card", "Projets/obsigate#7") == \
"https://git.dracodev.net/Projets/obsigate/issues/7"
assert build_deep_link("obsigate", "note", "v:Dossier/Note.md") == \
"https://og.dracodev.net/#file=v:Dossier%2FNote.md"
env_propre.setenv("DRACO_TIKTAK_URL", "https://tiktak-dev.dracodev.net")
ref = add_ref("alice", "Vault", "N.md", "tiktak", "task", "42",
"https://tiktak.dracodev.net/?task=42", "TikTak · tâche 42")
assert ref["target_url"] == "https://tiktak.dracodev.net/?task=42" # stocké tel quel
servies = list_refs("alice", "Vault", "N.md")
assert servies[0]["target_url"] == "https://tiktak-dev.dracodev.net/?task=42"
# ------------------------------------------------------------ injection hôte
def test_index_injecte_draco_urls(client, env_propre):
"""L'hôte sert index.html avec window.DRACO_URLS issu de sa config."""
env_propre.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net")
resp = client.get("/")
assert resp.status_code == 200
assert "window.DRACO_URLS" in resp.text
assert '"agent": "https://agent-dev.dracodev.net"' in resp.text
assert '"tiktak": "https://tiktak.dracodev.net"' in resp.text
+31 -2
View File
@@ -84,13 +84,19 @@ def _csp_directives(csp: str) -> dict[str, list[str]]:
return directives
def test_csp_frame_src_allows_agent_console(client):
def test_csp_frame_src_allows_agent_console(client, monkeypatch):
"""BUG-119 : le « mode console » du panneau Spark charge la console du
service d'agents (``https://agent.dracodev.net``) dans une iframe —
``frame-src`` doit autoriser cette origine, sinon le navigateur bloque
le chargement (« This content is blocked »). Les sources historiques
(``'self'`` et ``blob:``, visualiseur PDF) restent en place et aucune
autre origine n'est admise."""
autre origine n'est admise.
#223 : la configuration est neutralisée ici (défaut officiel) —
l'origine n'est plus figée dans le code, voir le test via config
ci-dessous."""
monkeypatch.delenv("DRACO_AGENT_URL", raising=False)
monkeypatch.delenv("AGENT_EVENEMENTS_URL", raising=False)
resp = client.get("/api/health")
csp = resp.headers.get("content-security-policy", "")
directives = _csp_directives(csp)
@@ -99,3 +105,26 @@ def test_csp_frame_src_allows_agent_console(client):
"blob:",
"https://agent.dracodev.net",
]
assert "https://agent.dracodev.net" in directives.get("connect-src", [])
def test_csp_agent_origine_via_config(client, monkeypatch):
"""#223 : ``frame-src``/``connect-src`` suivent l'origine de l'agent
**configurée** — dérivée d'``AGENT_EVENEMENTS_URL`` (config réelle
du poste, noms en ``-dev`` en dev), ou de ``DRACO_AGENT_URL`` qui
prime. Aucun nom d'environnement n'est codé en dur."""
monkeypatch.delenv("DRACO_AGENT_URL", raising=False)
monkeypatch.setenv(
"AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements")
resp = client.get("/api/health")
directives = _csp_directives(resp.headers.get("content-security-policy", ""))
assert directives.get("frame-src") == [
"'self'", "blob:", "https://agent-dev.dracodev.net"]
assert "https://agent-dev.dracodev.net" in directives.get("connect-src", [])
assert "https://agent.dracodev.net" not in directives.get("connect-src", [])
monkeypatch.setenv("DRACO_AGENT_URL", "https://agent.example.net")
resp = client.get("/api/health")
directives = _csp_directives(resp.headers.get("content-security-policy", ""))
assert directives.get("frame-src") == [
"'self'", "blob:", "https://agent.example.net"]