"""Porte centrale unique (0.46.6) — réglage, écran, refus, secours. Branche de la phase 2 du programme « compte partout » (basée sur la 0.46.5, fusion #145/#144). Couvert ici, sur instance fraîche par test (base temporaire : seul le compte d'amorçage « proprio », admin, créé depuis l'environnement — même patron que tests/test_amorcage_pont.py) : - mode INACTIF (défaut à la livraison) : comportement actuel — formulaire local visible et fonctionnel ; - activation par le PANNEAU d'administration (le réglage ``porte_centrale`` se pose et se retire : ``true`` puis ``false`` — la conversion booléenne explicite de l'enregistrement est exercée au passage) ; - mode ACTIF : l'écran ne présente que l'entrée centrale (lien SSO, aucun champ local) ; le formulaire local est refusé même avec des identifiants locaux valides ; une session déjà établie reste valide (aucun impact) ; - porte de secours : chemin discret ``/login/secours``, jamais proposé sur l'écran principal ; identifiants posés par les variables d'environnement ``KANBANTE_PORTE_SECOURS_USER`` / ``KANBANTE_PORTE_SECOURS_PASSWORD`` ; seul le compte administrateur désigné passe (un membre désigné est refusé) ; sans les variables, le chemin répond 404. """ import json import pytest from fastapi.testclient import TestClient import app.main as main from app import accounts, models from app.config import get_settings MDP_AMORCE = "mdp-amorce" MDP_MEMBRE = "mdp-membre" MDP_SECOURS = "mdp-secours-test" @pytest.fixture() def instance(monkeypatch, tmp_path, fake_gitea): monkeypatch.setenv("KANBANTE_DB_PATH", str(tmp_path / "porte.db")) monkeypatch.setenv("KANBANTE_AUTH_USER", "proprio") monkeypatch.setenv("KANBANTE_AUTH_PASSWORD", MDP_AMORCE) with TestClient(main.app) as client: users = models.list_users() assert len(users) == 1 and users[0]["username"] == "proprio" membre_id = models.create_user( "membre1", accounts.hash_password(MDP_MEMBRE), role="member", display_name="Membre Un", ) yield client, users[0], membre_id def _csrf(client, chemin="/login"): client.get(chemin) return client.cookies.get("kanbante_csrf") or "" def _connecter(client, username, password, chemin="/login"): token = _csrf(client, chemin) return client.post( chemin, data={"username": username, "password": password, "next": "/", "csrf_token": token}, follow_redirects=False, ) def _activer_porte_par_le_panneau(client): """Connexion admin native, puis POST /admin/settings (true).""" r = _connecter(client, "proprio", MDP_AMORCE) assert r.status_code == 303 r = client.get("/admin/settings") assert r.status_code == 200 assert "porte_centrale" in r.text # le réglage est au panneau token = client.cookies.get("kanbante_csrf") or "" r = client.post( "/admin/settings", data={"setting_porte_centrale": "true", "csrf_token": token}, follow_redirects=False, ) assert r.status_code == 303 assert get_settings().porte_centrale is True # ── Mode inactif : comportement actuel ─────────────────────── def test_mode_inactif_par_defaut_comportement_actuel(instance): client, _amorce, _membre = instance assert get_settings().porte_centrale is False r = client.get("/login") assert r.status_code == 200 assert 'name="password"' in r.text # formulaire local présent assert "Se connecter avec ObsiGate (SSO)" in r.text # Le formulaire local fonctionne pour un compte ordinaire. r = _connecter(client, "membre1", MDP_MEMBRE) assert r.status_code == 303 assert "kanbante_session" in r.headers.get("set-cookie", "") assert client.get("/").status_code == 200 def test_panneau_active_puis_desactive(instance): client, _amorce, _membre = instance _activer_porte_par_le_panneau(client) # Désactivation : saisir « false » doit RÉELLEMENT désactiver # (conversion booléenne explicite, pas bool("false") == True). token = client.cookies.get("kanbante_csrf") or "" r = client.post( "/admin/settings", data={"setting_porte_centrale": "false", "csrf_token": token}, follow_redirects=False, ) assert r.status_code == 303 assert get_settings().porte_centrale is False # Et le formulaire refonctionne (session admin refermée). client.cookies.delete("kanbante_session") r = _connecter(client, "membre1", MDP_MEMBRE) assert r.status_code == 303 # ── Mode actif : écran central, formulaire refusé ──────────── def test_mode_actif_ecran_et_formulaire_refuse(instance): client, _amorce, _membre = instance _activer_porte_par_le_panneau(client) # La session DÉJÀ ÉTABLIE (celle de l'admin qui vient # d'activer) n'est pas touchée par l'activation. assert client.get("/").status_code == 200 assert client.get("/admin/settings").status_code == 200 # Écran de connexion, visiteur déconnecté : entrée centrale seule. client.cookies.delete("kanbante_session") r = client.get("/login") assert r.status_code == 200 assert "Se connecter avec ObsiGate (SSO)" in r.text assert 'name="password"' not in r.text assert 'action="/login"' not in r.text # Le chemin de secours n'est JAMAIS proposé sur l'écran principal. assert "/login/secours" not in r.text # Formulaire local refusé, même avec les identifiants valides # d'un compte ordinaire… r = _connecter(client, "membre1", MDP_MEMBRE) assert r.status_code == 403 assert "porte centrale est active" in r.text assert "kanbante_session" not in r.headers.get("set-cookie", "") # …et même avec ceux de l'administrateur d'amorçage. r = _connecter(client, "proprio", MDP_AMORCE) assert r.status_code == 403 assert "kanbante_session" not in r.headers.get("set-cookie", "") # ── Porte de secours ───────────────────────────────────────── def test_porte_secours_chemin_discret(instance, monkeypatch): client, _amorce, _membre = instance monkeypatch.setenv("KANBANTE_PORTE_SECOURS_USER", "proprio") monkeypatch.setenv("KANBANTE_PORTE_SECOURS_PASSWORD", MDP_SECOURS) _activer_porte_par_le_panneau(client) client.cookies.delete("kanbante_session") # L'écran principal ne propose toujours pas le chemin. assert "/login/secours" not in client.get("/login").text # Le chemin discret, lui, présente son formulaire… r = client.get("/login/secours") assert r.status_code == 200 assert 'action="/login/secours"' in r.text # …refuse un mauvais mot de passe… r = _connecter(client, "proprio", "mauvais", chemin="/login/secours") assert r.status_code == 401 assert "kanbante_session" not in r.headers.get("set-cookie", "") # …refuse le compte membre (pas le compte désigné)… r = _connecter(client, "membre1", MDP_SECOURS, chemin="/login/secours") assert r.status_code == 401 # …et ouvre la session de l'administrateur désigné, en plein # mode porte centrale. r = _connecter(client, "proprio", MDP_SECOURS, chemin="/login/secours") assert r.status_code == 303 assert "kanbante_session" in r.headers.get("set-cookie", "") assert client.get("/admin").status_code == 200 def test_porte_secours_membre_designe_refuse(instance, monkeypatch): """Le compte désigné doit être administrateur : un membre ne passe pas.""" client, _amorce, _membre = instance monkeypatch.setenv("KANBANTE_PORTE_SECOURS_USER", "membre1") monkeypatch.setenv("KANBANTE_PORTE_SECOURS_PASSWORD", MDP_SECOURS) r = _connecter(client, "membre1", MDP_SECOURS, chemin="/login/secours") assert r.status_code == 401 assert "kanbante_session" not in r.headers.get("set-cookie", "") def test_porte_secours_non_configuree_404(instance): client, _amorce, _membre = instance # Aucune variable de secours dans l'environnement de test. assert client.get("/login/secours").status_code == 404 r = _connecter(client, "proprio", MDP_AMORCE, chemin="/login/secours") assert r.status_code == 404 assert "kanbante_session" not in r.headers.get("set-cookie", "") def test_activation_par_variable_environnement(monkeypatch, tmp_path, fake_gitea): """L'environnement peut amorcer le réglage (défaut officiel : inactif).""" monkeypatch.setenv("KANBANTE_DB_PATH", str(tmp_path / "porte-env.db")) monkeypatch.setenv("KANBANTE_AUTH_USER", "proprio") monkeypatch.setenv("KANBANTE_AUTH_PASSWORD", MDP_AMORCE) monkeypatch.setenv("KANBANTE_PORTE_CENTRALE", "1") with TestClient(main.app) as client: assert get_settings().porte_centrale is True r = client.get("/login") assert 'name="password"' not in r.text # La surcharge du panneau reste possible par-dessus : # (kv settings_overrides, même écriture que le panneau). models.set_kv( "settings_overrides", json.dumps({"porte_centrale": False}) ) assert get_settings().porte_centrale is False