Passe theme clair, libelles utilisateur, aides conteneurs, recherche de mise a jour et indication honnete des moteurs optionnels non embarques. Tests GUI unitaires: 126 passed.
211 lines
7.0 KiB
Python
211 lines
7.0 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, get_response=None, get_exc=None):
|
|
self._response = response
|
|
self._exc = exc
|
|
self._get_response = response if get_response is None else get_response
|
|
self._get_exc = exc if get_exc is None else get_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 get(self, url, timeout):
|
|
self.calls.append({"url": url, "timeout": timeout})
|
|
if self._get_exc is not None:
|
|
raise self._get_exc
|
|
return self._get_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_latest_version_reads_active_artifact(tmp_path):
|
|
payload = {
|
|
"version": "v11.0-beta",
|
|
"channel": "beta",
|
|
"filename": "Anonymisation-Setup.exe",
|
|
}
|
|
session = FakeSession(get_response=FakeResponse(200, payload))
|
|
client, _ = _client(tmp_path, session)
|
|
|
|
version = client.latest_version()
|
|
|
|
assert version == payload
|
|
assert session.calls[0]["url"] == "https://portail.example/api/v1/version"
|
|
|
|
|
|
def test_latest_version_unavailable_on_404_or_network_error(tmp_path):
|
|
client_404, _ = _client(tmp_path, FakeSession(get_response=FakeResponse(404, {"detail": "No active version"})))
|
|
assert client_404.latest_version() is None
|
|
|
|
client_down, _ = _client(tmp_path, FakeSession(get_exc=TimeoutError("timeout")))
|
|
assert client_down.latest_version() is None
|
|
|
|
|
|
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()
|