Coverage for tests/test_connect.py: 100%
76 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-05 11:17 +0200
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-05 11:17 +0200
1"""Tests du client principal : authentification, refresh token et gestion des erreurs HTTP."""
3from __future__ import annotations
5import json
6from unittest.mock import MagicMock, AsyncMock
8import pytest
10from henrri_connect.connect import (
11 AsyncHenrriClient, SyncHenrriClient, # type: ignore[import]
12)
13from henrri_connect.exc import (
14 HenrriAuthError,
15 HenrriForbiddenError,
16 HenrriNotFoundError,
17 HenrriServerError,
18 HenrriValidationError,
19)
20from tests.conftest import TOKEN_JSON, make_response
23# ── Authentification synchrone ────────────────────────────────────────────────
26class TestSyncAuthenticate:
27 """Tests de l'authentification synchrone."""
28 def test_stocke_access_token(self, mock_http: MagicMock) -> None:
29 """Le token d'accès est stocké après authentification réussie."""
30 mock_http.post.return_value = make_response(TOKEN_JSON)
31 client = SyncHenrriClient.__new__(SyncHenrriClient)
32 client._client_id = "id" # pylint: disable=W0212
33 client._client_secret = "secret" # pylint: disable=W0212
34 client._base_url = "https://api-sandbox.henrri.io" # pylint: disable=W0212
35 client._access_token = None # pylint: disable=W0212
36 client._refresh_token_str = None # pylint: disable=W0212
37 client._http = mock_http # pylint: disable=W0212
38 client._init_subclients() # pylint: disable=W0212
40 token = client.authenticate()
42 assert token.access_token == "new_access_token"
43 assert client._access_token == "new_access_token" # pylint: disable=W0212
44 assert client._refresh_token_str == "new_refresh_token" # pylint: disable=W0212
46 def test_auto_authenticate_avant_requete(self, mock_http: MagicMock) -> None:
47 """La première requête déclenche automatiquement l'authentification."""
48 mock_http.post.return_value = make_response(TOKEN_JSON)
49 mock_http.request.return_value = make_response({"id": 1})
51 client = SyncHenrriClient.__new__(SyncHenrriClient)
52 client._client_id = "id" # pylint: disable=W0212
53 client._client_secret = "secret" # pylint: disable=W0212
54 client._base_url = "https://api-sandbox.henrri.io" # pylint: disable=W0212
55 client._access_token = None # pylint: disable=W0212
56 client._refresh_token_str = None # pylint: disable=W0212
57 client._http = mock_http # pylint: disable=W0212
58 client._init_subclients() # pylint: disable=W0212
60 client.request("GET", "/v1/companies/1")
62 mock_http.post.assert_called_once()
65# ── Refresh token synchrone ───────────────────────────────────────────────────
68class TestSyncRefreshToken:
69 """Tests du refresh token synchrone."""
70 def test_refresh_succes(self, sync_client: SyncHenrriClient, mock_http: MagicMock) -> None:
71 """Le refresh token met à jour l'access token."""
72 mock_http.post.return_value = make_response(TOKEN_JSON)
74 sync_client._do_refresh() # pylint: disable=W0212
76 assert sync_client._access_token == "new_access_token" # pylint: disable=W0212
78 def test_refresh_echec_reauthentifie(
79 self, sync_client: SyncHenrriClient, mock_http: MagicMock
80 ) -> None:
81 """En cas d'échec du refresh, le client se ré-authentifie."""
82 mock_http.post.side_effect = [
83 make_response({}, status_code=401), # refresh échoue
84 make_response(TOKEN_JSON), # ré-authentification réussit
85 ]
87 sync_client._do_refresh() # pylint: disable=W0212
89 assert mock_http.post.call_count == 2
92# ── Gestion des erreurs HTTP ──────────────────────────────────────────────────
95class TestRaiseForStatus:
96 """Tests de la gestion des erreurs HTTP."""
97 @pytest.mark.parametrize(
98 "status_code, exc_class",
99 [
100 (400, HenrriValidationError),
101 (403, HenrriForbiddenError),
102 (404, HenrriNotFoundError),
103 (500, HenrriServerError),
104 (503, HenrriServerError),
105 ],
106 )
107 def test_leve_exception_selon_code_http(
108 self,
109 sync_client: SyncHenrriClient,
110 mock_http: MagicMock,
111 status_code: int,
112 exc_class: type,
113 ) -> None:
114 """Chaque code HTTP d'erreur lève l'exception appropriée."""
115 mock_http.request.return_value = make_response(
116 {"detail": "erreur"}, status_code=status_code
117 )
118 with pytest.raises(exc_class):
119 sync_client.request("GET", "/v1/companies/1")
121 def test_leve_auth_error_apres_refresh_echec(
122 self,
123 sync_client: SyncHenrriClient,
124 mock_http: MagicMock,
125 ) -> None:
126 """Un 401 persistant après refresh lève HenrriAuthError."""
127 # Refresh réussit (retourne un token valide), mais la requête répond toujours 401
128 mock_http.post.return_value = make_response(TOKEN_JSON)
129 mock_http.request.return_value = make_response({"detail": "erreur"}, status_code=401)
131 with pytest.raises(HenrriAuthError):
132 sync_client.request("GET", "/v1/companies/1")
134 def test_erreur_json_invalide_utilise_texte(
135 self, sync_client: SyncHenrriClient, mock_http: MagicMock
136 ) -> None:
137 """Si le corps de la réponse n'est pas du JSON valide, le texte brut est utilisé."""
138 resp = make_response({}, status_code=400, text="Bad Request")
139 resp.json.side_effect = json.JSONDecodeError("Expecting value", "", 0)
140 mock_http.request.return_value = resp
142 with pytest.raises(HenrriValidationError) as exc_info:
143 sync_client.request("GET", "/v1/companies/1")
145 assert "Bad Request" in str(exc_info.value)
148# ── Authentification asynchrone ───────────────────────────────────────────────
151class TestAsyncAuthenticate:
152 """Tests de l'authentification asynchrone."""
153 async def test_stocke_access_token(self, mock_async_http: AsyncMock) -> None:
154 """Le token d'accès est stocké après authentification asynchrone réussie."""
155 mock_async_http.post.return_value = make_response(TOKEN_JSON)
157 client = AsyncHenrriClient.__new__(AsyncHenrriClient)
158 client._client_id = "id" # pylint: disable=W0212
159 client._client_secret = "secret" # pylint: disable=W0212
160 client._base_url = "https://api-sandbox.henrri.io" # pylint: disable=W0212
161 client._access_token = None # pylint: disable=W0212
162 client._refresh_token_str = None # pylint: disable=W0212
163 client._http = mock_async_http # pylint: disable=W0212
164 client._init_subclients() # type: ignore[union-attr] # pylint: disable=W0212
166 token = await client.authenticate()
168 assert token.access_token == "new_access_token"
169 assert client._access_token == "new_access_token" # pylint: disable=W0212