"""Tests du client licence : session HTTP injectée, aucun appel réseau réel. Le client accepte une session mockable. On vérifie : activation réussie + stockage local, refus serveur (4xx), serveur indisponible (exception réseau), réponse illisible, check, et lecture du statut local. """ from __future__ import annotations import pytest from gui_v6.license_client import LicenseClient, LicenseStatus from gui_v6.license_store import LicenseStore class FakeResponse: def __init__(self, status_code, payload=None, raise_on_json=False): self.status_code = status_code self._payload = payload self._raise_on_json = raise_on_json def json(self): if self._raise_on_json: raise ValueError("not json") return self._payload class FakeSession: """Session HTTP mockable : enregistre les appels, renvoie des réponses scriptées.""" def __init__(self, response=None, exc=None): self._response = response self._exc = exc self.calls = [] def post(self, url, json, timeout): self.calls.append({"url": url, "json": json, "timeout": timeout}) if self._exc is not None: raise self._exc return self._response def _client(tmp_path, session): store = LicenseStore(tmp_path / "license.json") return LicenseClient("https://portail.example/", session=session, store=store), store def test_activate_success_stores_license(tmp_path): payload = { "state": "active", "license": { "payload": { "license_ref": "LIC-9", "expires_at": "2027-06-01", "grace_days": 7, "machine_id": "MID-0001", }, "signature": "signed", "alg": "RSASSA-PSS-SHA256", }, } session = FakeSession(FakeResponse(200, payload)) client, store = _client(tmp_path, session) status = client.activate("TOKEN-ABC", "MID-0001") assert isinstance(status, LicenseStatus) assert status.valid is True assert status.status == "active" assert status.license_ref == "LIC-9" assert status.grace_days == 7 # La licence signée est stockée localement. assert store.load() == payload # L'URL et le payload envoyés sont corrects. assert session.calls[0]["url"] == "https://portail.example/api/v1/activate" assert session.calls[0]["json"] == {"token": "TOKEN-ABC", "machine_id": "MID-0001"} def test_activate_grace_is_valid(tmp_path): session = FakeSession( FakeResponse(200, {"state": "grace", "license": {"payload": {"grace_days": 3}}}) ) client, _ = _client(tmp_path, session) status = client.activate("T", "M") assert status.valid is True assert status.status == "grace" def test_activate_rejected_4xx_is_invalid_and_not_stored(tmp_path): session = FakeSession(FakeResponse(403, {"detail": "token revoked"})) client, store = _client(tmp_path, session) status = client.activate("BAD", "MID-1") assert status.valid is False assert status.status == "invalid" assert "revoked" in status.message # Rien n'est stocké en cas de refus. assert store.load() is None def test_activate_server_unavailable_does_not_crash(tmp_path): session = FakeSession(exc=ConnectionError("server down")) client, store = _client(tmp_path, session) status = client.activate("T", "M") assert status.valid is False assert status.status == "unavailable" assert store.load() is None def test_activate_unreadable_response_is_unavailable(tmp_path): session = FakeSession(FakeResponse(200, raise_on_json=True)) client, _ = _client(tmp_path, session) status = client.activate("T", "M") assert status.status == "unavailable" def test_check_active(tmp_path): payload = {"state": "active", "license": {"payload": {"license_ref": "LIC-1"}}} session = FakeSession(FakeResponse(200, payload)) client, store = _client(tmp_path, session) status = client.check("LIC-1", "MID-1") assert status.valid is True assert session.calls[0]["url"] == "https://portail.example/api/v1/check" assert session.calls[0]["json"] == {"license_ref": "LIC-1", "machine_id": "MID-1"} assert store.load() == payload def test_check_expired_is_not_valid(tmp_path): session = FakeSession(FakeResponse(200, {"state": "expired"})) client, _ = _client(tmp_path, session) status = client.check("LIC-1", "MID-1") assert status.valid is False assert status.status == "expired" def test_check_revoked_without_license_payload(tmp_path): session = FakeSession(FakeResponse(200, {"state": "revoked"})) client, store = _client(tmp_path, session) status = client.check("LIC-1", "MID-1") assert status.valid is False assert status.status == "revoked" assert store.load() is None def test_check_server_unavailable(tmp_path): session = FakeSession(exc=TimeoutError("timeout")) client, _ = _client(tmp_path, session) status = client.check("LIC-1", "MID-1") assert status.status == "unavailable" def test_local_status_none_when_empty(tmp_path): session = FakeSession(FakeResponse(200, {})) client, _ = _client(tmp_path, session) status = client.local_status() assert status.valid is False assert status.status == "none" def test_local_status_reads_store(tmp_path): store = LicenseStore(tmp_path / "license.json") store.save({"state": "active", "license": {"payload": {"license_ref": "LIC-7"}}}) client = LicenseClient("https://x", session=FakeSession(), store=store) status = client.local_status() assert status.valid is True assert status.license_ref == "LIC-7" def test_status_never_exposes_token(): # Le statut ne porte pas de token : la repr ne peut pas le fuiter. status = LicenseStatus.from_payload({"status": "active", "license_ref": "LIC-1"}) assert "token" not in repr(status).lower()