feat: URLs publiques de l'écosystème par configuration — dev/prod sans noms en dur #223
This commit is contained in:
@@ -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():
|
||||
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user