"""URLs publiques dracodev par configuration — chantier dev/prod (v7.82.0). Règle vérifiée : aucun nom public codé en dur dans le code vivant ; les défauts sont les noms OFFICIELS (jamais de ``-dev`` en défaut) et chaque environnement déclare ses noms en ``-dev`` par l'environnement (``DRACO_SSO_URL``, ``DRACO_AGENT_URL``, ``TIKTAK_URL``, ``KANBANTE_URL``, ``FLOWDECK_URL``/``OAUTH_REDIRECT_URI``…). """ import pytest from app.services import draco_urls VARS = ("DRACO_SSO_URL", "DRACO_AGENT_URL", "TIKTAK_URL", "KANBANTE_URL", "FLOWDECK_URL", "OAUTH_REDIRECT_URI", "AGENT_EVENEMENTS_URL") @pytest.fixture(autouse=True) def _env_propre(monkeypatch): for v in VARS: monkeypatch.delenv(v, raising=False) def test_defauts_officiels_sans_dev(): urls = draco_urls.toutes() assert urls["obsigate"] == "https://og.dracodev.net" assert urls["agent"] == "https://agent.dracodev.net" assert urls["tiktak"] == "https://tiktak.dracodev.net" assert urls["kanbante"] == "https://kanbante.dracodev.net" assert urls["flowdeck"] == "https://flowdeck.dracodev.net" assert all("-dev" not in u for u in urls.values()) def test_surcharge_dev_par_env(monkeypatch): monkeypatch.setenv("DRACO_SSO_URL", "https://og-dev.dracodev.net") monkeypatch.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net") monkeypatch.setenv("TIKTAK_URL", "https://tiktak-dev.dracodev.net") monkeypatch.setenv("KANBANTE_URL", "https://kanbante-dev.dracodev.net") monkeypatch.setenv("FLOWDECK_URL", "https://flowdeck-dev.dracodev.net") urls = draco_urls.toutes() assert urls["obsigate"] == "https://og-dev.dracodev.net" assert urls["agent"] == "https://agent-dev.dracodev.net" assert urls["tiktak"] == "https://tiktak-dev.dracodev.net" assert urls["kanbante"] == "https://kanbante-dev.dracodev.net" assert urls["flowdeck"] == "https://flowdeck-dev.dracodev.net" def test_flowdeck_url_derivée_oauth_redirect(monkeypatch): monkeypatch.setenv( "OAUTH_REDIRECT_URI", "https://flowdeck-dev.dracodev.net/auth/callback") assert draco_urls.flowdeck_url() == "https://flowdeck-dev.dracodev.net" def test_evenements_url_derivée_agent(monkeypatch): from app.services import agent_evenements monkeypatch.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net") assert agent_evenements._url() == \ "https://agent-dev.dracodev.net/api/v1/evenements" monkeypatch.setenv("AGENT_EVENEMENTS_URL", "http://127.0.0.1:9/api/v1/evenements") assert agent_evenements._url() == "http://127.0.0.1:9/api/v1/evenements" def test_csp_agent_depuis_config(client, monkeypatch): monkeypatch.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net") r = client.get("/") csp = r.headers.get("content-security-policy", "") assert "https://agent-dev.dracodev.net" in csp assert "connect-src 'self' https://agent-dev.dracodev.net" in csp assert "frame-src 'self' https://agent-dev.dracodev.net" in csp assert "https://agent.dracodev.net " not in csp.replace( "https://agent-dev.dracodev.net", "") def test_parse_deep_link_dev_et_officiel(monkeypatch): from app.services.external_refs import parse_deep_link monkeypatch.setenv("TIKTAK_URL", "https://tiktak-dev.dracodev.net") monkeypatch.setenv("DRACO_SSO_URL", "https://og-dev.dracodev.net") # Hôte configuré (dev)… assert parse_deep_link("https://tiktak-dev.dracodev.net/?task=7")[:3] == \ ("tiktak", "task", "7") assert parse_deep_link("https://og-dev.dracodev.net/#file=P:A.md")[:3] == \ ("obsigate", "note", "P:A.md") # …et hôte officiel toujours reconnu (références déjà stockées). assert parse_deep_link("https://tiktak.dracodev.net/?task=7")[:3] == \ ("tiktak", "task", "7") assert parse_deep_link("https://og.dracodev.net/#file=P:A.md")[:3] == \ ("obsigate", "note", "P:A.md") def test_url_affichage_reecrit_hote_sans_toucher_stockage(monkeypatch): from app.services.external_refs import url_affichage monkeypatch.setenv("TIKTAK_URL", "https://tiktak-dev.dracodev.net") stockee = "https://tiktak.dracodev.net/?task=42" assert url_affichage(stockee) == "https://tiktak-dev.dracodev.net/?task=42" # La fonction est pure : la valeur d'entrée (celle en base) ne bouge pas. assert stockee == "https://tiktak.dracodev.net/?task=42" assert url_affichage("https://example.com/x") == "https://example.com/x" def test_preset_tiktak_depuis_config(monkeypatch): from app.services import connectors assert connectors.PRESETS["tiktak"]["url"] == \ "https://tiktak.dracodev.net/mcp" monkeypatch.setenv("TIKTAK_URL", "https://tiktak-dev.dracodev.net") assert connectors.PRESETS["tiktak"]["url"] == \ "https://tiktak-dev.dracodev.net/mcp" def test_derivation_dev_depuis_agent_evenements_seul(monkeypatch): # Cas réel de la dev : seul AGENT_EVENEMENTS_URL est posé — # les quatre URLs sœurs servies suivent en ``-dev``, sans # déclarer quatre variables de plus. Gitea ne change pas. monkeypatch.setenv( "AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements") urls = draco_urls.toutes() assert urls["obsigate"] == "https://og-dev.dracodev.net" assert urls["flowdeck"] == "https://flowdeck-dev.dracodev.net" assert urls["tiktak"] == "https://tiktak-dev.dracodev.net" assert urls["kanbante"] == "https://kanbante-dev.dracodev.net" assert urls["gitea"] == "https://git.dracodev.net" # Les accesseurs individuels partagent la même résolution. assert draco_urls.obsigate_url() == "https://og-dev.dracodev.net" assert draco_urls.flowdeck_url() == "https://flowdeck-dev.dracodev.net" assert draco_urls.tiktak_url() == "https://tiktak-dev.dracodev.net" assert draco_urls.kanbante_url() == "https://kanbante-dev.dracodev.net" def test_derivation_explicite_gagne_sur_derivation(monkeypatch): monkeypatch.setenv( "AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements") # Explicites (contrat inchangé) : elles priment sur la dérivation, # y compris quand elles pointent le nom officiel. monkeypatch.setenv("DRACO_SSO_URL", "https://og.dracodev.net") monkeypatch.setenv("TIKTAK_URL", "https://tiktak.example.net") urls = draco_urls.toutes() assert urls["obsigate"] == "https://og.dracodev.net" assert urls["tiktak"] == "https://tiktak.example.net" # Non déclarées : dérivées. assert urls["flowdeck"] == "https://flowdeck-dev.dracodev.net" assert urls["kanbante"] == "https://kanbante-dev.dracodev.net" def test_derivation_hote_agent_hors_motif_defauts_officiels(monkeypatch): # Autre domaine, LAN, IP : aucune dérivation, jamais un lien deviné. for url_agent in ( "https://agent.example.com/api/v1/evenements", "https://agent-dev.example.net/api/v1/evenements", "https://monagent-dev.dracodev.net/api/v1/evenements", "https://agent-dev.dracodev.net.evil.com/api/v1/evenements", "http://192.168.30.101:8790/api/v1/evenements", "http://127.0.0.1:8790/api/v1/evenements", "http://agent-dev.local:8790/api/v1/evenements", ): monkeypatch.setenv("AGENT_EVENEMENTS_URL", url_agent) urls = draco_urls.toutes() assert urls["obsigate"] == "https://og.dracodev.net", url_agent assert urls["flowdeck"] == "https://flowdeck.dracodev.net", url_agent assert urls["tiktak"] == "https://tiktak.dracodev.net", url_agent assert urls["kanbante"] == "https://kanbante.dracodev.net", url_agent def test_derivation_agent_officiel_suffixe_vide(monkeypatch): # Hôte agent officiel : suffixe vide, les sœurs restent officielles. monkeypatch.setenv( "AGENT_EVENEMENTS_URL", "https://agent.dracodev.net/api/v1/evenements") urls = draco_urls.toutes() assert urls["obsigate"] == "https://og.dracodev.net" assert urls["flowdeck"] == "https://flowdeck.dracodev.net" assert urls["tiktak"] == "https://tiktak.dracodev.net" assert urls["kanbante"] == "https://kanbante.dracodev.net" def test_bouton_sso_login_depuis_config(monkeypatch): from app.routers.auth import _sso_login_url assert _sso_login_url() == ( "https://og.dracodev.net/sso/authorize?return_to=" "https%3A%2F%2Fflowdeck.dracodev.net%2Fauth%2Fdraco") monkeypatch.setenv("DRACO_SSO_URL", "https://og-dev.dracodev.net") monkeypatch.setenv( "OAUTH_REDIRECT_URI", "https://flowdeck-dev.dracodev.net/auth/callback") assert _sso_login_url() == ( "https://og-dev.dracodev.net/sso/authorize?return_to=" "https%3A%2F%2Fflowdeck-dev.dracodev.net%2Fauth%2Fdraco") # ── v7.82.4 — résolution par l'hôte de la requête ───────────────────── # Correctif du mode console mort sur flowdeck-dev : l'agent n'avait # aucune dérivation (v7.82.3) et rien ne lisait l'hôte de la requête. # Ordre : explicite > hôte public de la requête (motif fermé) > # dérivation par l'agent connu en configuration > officiel. @pytest.fixture def avec_hote(): """Pose l'hôte de requête comme le middleware CSP le fait.""" jetons = [] def poser(hote): jetons.append(draco_urls.HOTE_REQUETE.set(hote)) yield poser for jeton in reversed(jetons): draco_urls.HOTE_REQUETE.reset(jeton) def test_agent_url_derivee_depuis_evenements_seul(monkeypatch): # Trou n° 1 de la v7.82.3 : seul AGENT_EVENEMENTS_URL posé (cas # réel de la dev) → l'agent lui-même suit en ``-dev``, pas # seulement les sœurs. monkeypatch.setenv( "AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements") assert draco_urls.agent_url() == "https://agent-dev.dracodev.net" assert draco_urls.agent_evenements_url() == \ "https://agent-dev.dracodev.net/api/v1/evenements" def test_hote_requete_dev_toutes_urls_sans_env(avec_hote): # Aucune variable d'environnement (fixture _env_propre) : l'hôte # public suffit — sœurs ET agent en ``-dev``, Gitea inchangée. avec_hote("flowdeck-dev.dracodev.net") urls = draco_urls.toutes() assert urls["obsigate"] == "https://og-dev.dracodev.net" assert urls["agent"] == "https://agent-dev.dracodev.net" assert urls["tiktak"] == "https://tiktak-dev.dracodev.net" assert urls["kanbante"] == "https://kanbante-dev.dracodev.net" assert urls["flowdeck"] == "https://flowdeck-dev.dracodev.net" assert urls["gitea"] == "https://git.dracodev.net" def test_hote_requete_quelconque_des_cinq_slugs(avec_hote): # L'environnement est celui de l'installation, pas de l'app # demandée : un hôte ``agent-dev`` ou ``og-dev`` vaut ``-dev`` # pour toutes, port éventuel ignoré. for hote in ("agent-dev.dracodev.net", "og-dev.dracodev.net:443", "tiktak-dev.dracodev.net", "kanbante-dev.dracodev.net"): avec_hote(hote) assert draco_urls.agent_url() == "https://agent-dev.dracodev.net", hote assert draco_urls.obsigate_url() == "https://og-dev.dracodev.net", hote def test_hote_requete_prod_noms_officiels(avec_hote): avec_hote("flowdeck.dracodev.net") urls = draco_urls.toutes() assert urls["obsigate"] == "https://og.dracodev.net" assert urls["agent"] == "https://agent.dracodev.net" assert urls["flowdeck"] == "https://flowdeck.dracodev.net" assert all("-dev" not in u for u in urls.values()) def test_hote_requete_motif_ferme_aucune_derivation(avec_hote): # Jamais de suffixe inventé depuis un hôte arbitraire. for hote in ("evil-dev.dracodev.net", "flowdeck-dev.dracodev.net.evil.com", "monflowdeck-dev.dracodev.net", "flowdeckdev.dracodev.net", "flowdeck-dev.example.net"): avec_hote(hote) urls = draco_urls.toutes() assert urls["agent"] == "https://agent.dracodev.net", hote assert urls["obsigate"] == "https://og.dracodev.net", hote assert urls["flowdeck"] == "https://flowdeck.dracodev.net", hote def test_hote_requete_lan_repli_configuration(avec_hote, monkeypatch): # Hors motif (LAN, IP) : repli configuration/défauts, comme # hors requête — ici la dérivation par AGENT_EVENEMENTS_URL. avec_hote("192.168.30.101:8080") urls = draco_urls.toutes() assert urls["agent"] == "https://agent.dracodev.net" assert urls["obsigate"] == "https://og.dracodev.net" monkeypatch.setenv( "AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements") urls = draco_urls.toutes() assert urls["agent"] == "https://agent-dev.dracodev.net" assert urls["obsigate"] == "https://og-dev.dracodev.net" assert urls["tiktak"] == "https://tiktak-dev.dracodev.net" def test_hote_requete_explicite_gagne_sur_hote(avec_hote, monkeypatch): avec_hote("flowdeck-dev.dracodev.net") # Explicites prioritaires, y compris quand elles pointent le # nom officiel ou un domaine propre. monkeypatch.setenv("DRACO_AGENT_URL", "https://agent.dracodev.net") monkeypatch.setenv("DRACO_SSO_URL", "https://og.example.net") urls = draco_urls.toutes() assert urls["agent"] == "https://agent.dracodev.net" assert urls["obsigate"] == "https://og.example.net" # Non déclarées : l'hôte de la requête fait foi. assert urls["tiktak"] == "https://tiktak-dev.dracodev.net" assert urls["kanbante"] == "https://kanbante-dev.dracodev.net" assert urls["flowdeck"] == "https://flowdeck-dev.dracodev.net" def test_hote_requete_bouton_sso_tout_dev(avec_hote): from app.routers.auth import _sso_login_url avec_hote("flowdeck-dev.dracodev.net") # Émettrice via DRACO_SSO_URL résolu par l'hôte + callback = # propre origine résolue de l'instance. assert _sso_login_url() == ( "https://og-dev.dracodev.net/sso/authorize?return_to=" "https%3A%2F%2Fflowdeck-dev.dracodev.net%2Fauth%2Fdraco") def test_http_dev_sans_env_tout_en_dev(client): # Requête complète, aucune variable d'environnement : barre, # panneau (config de l'iframe console), CSP et SSO en ``-dev``. r = client.get("/workspaces", headers={"Host": "flowdeck-dev.dracodev.net"}) assert r.status_code == 200 html = r.text # Barre inter-apps (les quatre sœurs). for soeur in ("og-dev", "flowdeck-dev", "tiktak-dev", "kanbante-dev"): assert f'href="https://{soeur}.dracodev.net/"' in html # Panneau : la source de l'iframe console vient de cette config. assert 'agent_url: "https://agent-dev.dracodev.net"' in html # CSP de la MÊME réponse : même origine agent résolue. csp = r.headers.get("content-security-policy", "") assert "frame-src 'self' https://agent-dev.dracodev.net" in csp assert "connect-src 'self' https://agent-dev.dracodev.net" in csp # Aucun nom officiel nu ne subsiste dans la réponse. for officiel in ("https://og.dracodev.net", "https://agent.dracodev.net", "https://tiktak.dracodev.net", "https://kanbante.dracodev.net", "https://flowdeck.dracodev.net"): assert officiel not in html.replace("-dev.dracodev.net", ""), officiel assert officiel not in csp.replace("-dev.dracodev.net", ""), officiel # Bouton SSO de la page de connexion, même requête type. r = client.get("/auth/login?provider=local", headers={"Host": "flowdeck-dev.dracodev.net"}) assert ("https://og-dev.dracodev.net/sso/authorize?return_to=" "https%3A%2F%2Fflowdeck-dev.dracodev.net%2Fauth%2Fdraco") in r.text def test_http_prod_noms_officiels(client): r = client.get("/workspaces", headers={"Host": "flowdeck.dracodev.net"}) assert r.status_code == 200 assert 'agent_url: "https://agent.dracodev.net"' in r.text assert 'href="https://og.dracodev.net/"' in r.text csp = r.headers.get("content-security-policy", "") assert "frame-src 'self' https://agent.dracodev.net" in csp assert "-dev.dracodev.net" not in r.text assert "-dev.dracodev.net" not in csp def test_http_lan_repli_defauts(client): r = client.get("/workspaces", headers={"Host": "192.168.30.101:8080"}) assert r.status_code == 200 assert 'agent_url: "https://agent.dracodev.net"' in r.text csp = r.headers.get("content-security-policy", "") assert "frame-src 'self' https://agent.dracodev.net" in csp def test_http_explicite_gagne_sur_hote(client, monkeypatch): monkeypatch.setenv("DRACO_AGENT_URL", "https://agent.dracodev.net") r = client.get("/workspaces", headers={"Host": "flowdeck-dev.dracodev.net"}) assert 'agent_url: "https://agent.dracodev.net"' in r.text csp = r.headers.get("content-security-policy", "") assert "frame-src 'self' https://agent.dracodev.net;" in csp # Le reste suit l'hôte de la requête. assert 'href="https://og-dev.dracodev.net/"' in r.text