"""TDD — DETTE-022 MAJ silencieuse v2 : logique PURE serveur de décision d'update. Périmètre testé ICI = parties PURES, testables sans démarrer le serveur (DETTE-013 : on N'IMPORTE PAS `api_stream` — on charge le module `update_check.py` par chemin, comme test_agent_v1_log_shipper). Couvre : - R3 `parse_version()` : tuple d'entiers, "1.0.2" < "1.0.10", égalité, "v1.2.3"/espaces tolérés, format invalide → fallback sans crash. - R2 logique de décision PURE `decide_update()` : compare version courante vs dernière dispo, choisit `update_type` (code-only/full), construit la réponse `{update_available, latest_version, update_type, url}`. Le NOYAU dangereux (swap fichiers / Lea.bat / restart) est HORS périmètre. """ import importlib.util from pathlib import Path import pytest _MOD_PATH = ( Path(__file__).resolve().parents[2] / "agent_v0" / "server_v1" / "update_check.py" ) def _load_module(): spec = importlib.util.spec_from_file_location("rpa_update_check", _MOD_PATH) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod @pytest.fixture def mod(): return _load_module() # --------------------------------------------------------------------------- # R3 — parse_version : tuple d'entiers (semver), pas comparaison lexicale # --------------------------------------------------------------------------- class TestParseVersion: def test_parse_basique(self, mod): assert mod.parse_version("1.0.2") == (1, 0, 2) assert mod.parse_version("1.0.10") == (1, 0, 10) def test_ordre_semver_pas_lexical(self, mod): # Le bug classique : "1.0.2" < "1.0.10" est FAUX en lexicographique. assert mod.parse_version("1.0.2") < mod.parse_version("1.0.10") assert mod.parse_version("1.0.10") > mod.parse_version("1.0.2") assert mod.parse_version("2.0.0") > mod.parse_version("1.99.99") def test_egalite(self, mod): assert mod.parse_version("1.0.1") == mod.parse_version("1.0.1") def test_prefixe_v_et_espaces_toleres(self, mod): assert mod.parse_version("v1.2.3") == mod.parse_version("1.2.3") assert mod.parse_version(" 1.2.3 ") == (1, 2, 3) assert mod.parse_version("V1.2.3") == (1, 2, 3) def test_format_invalide_fallback_sans_crash(self, mod): # Ne doit jamais lever — fallback (0,) (= la plus basse). assert mod.parse_version("") == (0,) assert mod.parse_version("abc") == (0,) assert mod.parse_version(None) == (0,) assert mod.parse_version("1.x.3") == (0,) # Une version valide reste toujours > au fallback invalide. assert mod.parse_version("0.0.1") > mod.parse_version("garbage") def test_is_newer_helper(self, mod): assert mod.is_newer("1.0.2", "1.0.1") is True assert mod.is_newer("1.0.10", "1.0.2") is True assert mod.is_newer("1.0.1", "1.0.1") is False assert mod.is_newer("1.0.0", "1.0.1") is False # --------------------------------------------------------------------------- # R2 — decide_update : logique PURE de décision serveur # --------------------------------------------------------------------------- class TestDecideUpdate: def test_pas_de_maj_si_a_jour(self, mod): resp = mod.decide_update(current_version="1.0.1", latest_version="1.0.1") assert resp["update_available"] is False assert resp["latest_version"] == "1.0.1" assert resp["update_type"] is None assert resp["url"] is None def test_pas_de_maj_si_client_plus_recent(self, mod): # Client en avance (dev local) → jamais de downgrade. resp = mod.decide_update(current_version="1.0.5", latest_version="1.0.2") assert resp["update_available"] is False def test_maj_disponible_code_only_par_defaut(self, mod): resp = mod.decide_update(current_version="1.0.1", latest_version="1.0.2") assert resp["update_available"] is True assert resp["latest_version"] == "1.0.2" # R2 : code-only = défaut (99% des cas, ~500 Ko). assert resp["update_type"] == "code-only" assert "1.0.2" in resp["url"] assert "code-only" in resp["url"] def test_maj_full_si_demande(self, mod): resp = mod.decide_update( current_version="1.0.1", latest_version="1.1.0", update_type="full" ) assert resp["update_available"] is True assert resp["update_type"] == "full" assert "full" in resp["url"] def test_update_type_invalide_retombe_sur_code_only(self, mod): resp = mod.decide_update( current_version="1.0.1", latest_version="1.0.2", update_type="banana" ) assert resp["update_type"] == "code-only" def test_ordre_semver_dans_decision(self, mod): # 1.0.2 < 1.0.10 → MAJ dispo (pas de faux négatif lexical). resp = mod.decide_update(current_version="1.0.2", latest_version="1.0.10") assert resp["update_available"] is True def test_url_inclut_machine_id_si_fourni(self, mod): resp = mod.decide_update( current_version="1.0.1", latest_version="1.0.2", machine_id="pc-7" ) assert "pc-7" in resp["url"] def test_versions_invalides_pas_de_crash_pas_de_maj(self, mod): # latest illisible → on ne propose RIEN (prudence : pas de MAJ douteuse). resp = mod.decide_update(current_version="1.0.1", latest_version="garbage") assert resp["update_available"] is False resp2 = mod.decide_update(current_version="", latest_version="") assert resp2["update_available"] is False