Files
anonymisation/tests/unit/test_gui_v6_license_client.py
Domi31tls a6ee68a8a3 feat(gui): add GUI V6 G1 foundation (license client/store, shell, About tab)
Socle de la refonte GUI V6 (couche présentation uniquement, aucune logique de
détection) :
- license_store: stockage licence hors dépôt (%LOCALAPPDATA%/Aivanov | XDG),
  read/write atomique/delete, ne journalise aucun token
- license_client: LicenseStatus + activate/check/local_status, session HTTP
  injectable, serveur indisponible géré sans crash, aucune clé privée
- theme: 4 thèmes + couleurs de statut licence
- app + tab_about: shell customtkinter minimal (header, bandeau licence,
  3 onglets), onglet À propos étoffé
- Pseudonymisation_Gui_V6.py: point d'entrée + --self-test (exit 0 sans fenêtre)
- requirements.txt: customtkinter==5.2.2

Tests: 20 nouveaux (store sur vrais fichiers, client sur session injectée).
Suite tests/unit: 167 passed, 0 régression. V5/moteur/managers/specs intacts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 18:50:23 +02:00

180 lines
5.8 KiB
Python

"""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()