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

1"""Tests du client principal : authentification, refresh token et gestion des erreurs HTTP.""" 

2 

3from __future__ import annotations 

4 

5import json 

6from unittest.mock import MagicMock, AsyncMock 

7 

8import pytest 

9 

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 

21 

22 

23# ── Authentification synchrone ──────────────────────────────────────────────── 

24 

25 

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 

39 

40 token = client.authenticate() 

41 

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 

45 

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}) 

50 

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 

59 

60 client.request("GET", "/v1/companies/1") 

61 

62 mock_http.post.assert_called_once() 

63 

64 

65# ── Refresh token synchrone ─────────────────────────────────────────────────── 

66 

67 

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) 

73 

74 sync_client._do_refresh() # pylint: disable=W0212 

75 

76 assert sync_client._access_token == "new_access_token" # pylint: disable=W0212 

77 

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 ] 

86 

87 sync_client._do_refresh() # pylint: disable=W0212 

88 

89 assert mock_http.post.call_count == 2 

90 

91 

92# ── Gestion des erreurs HTTP ────────────────────────────────────────────────── 

93 

94 

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

120 

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) 

130 

131 with pytest.raises(HenrriAuthError): 

132 sync_client.request("GET", "/v1/companies/1") 

133 

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 

141 

142 with pytest.raises(HenrriValidationError) as exc_info: 

143 sync_client.request("GET", "/v1/companies/1") 

144 

145 assert "Bad Request" in str(exc_info.value) 

146 

147 

148# ── Authentification asynchrone ─────────────────────────────────────────────── 

149 

150 

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) 

156 

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 

165 

166 token = await client.authenticate() 

167 

168 assert token.access_token == "new_access_token" 

169 assert client._access_token == "new_access_token" # pylint: disable=W0212