Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ApiRequester, Authentifier: gestion de ConnectionError. Close #168 #174

Merged
merged 4 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

* Création/consultation des clefs depuis la ligne de commande #96
* doc/docs/comme-executable.md : Ajout de la documentation pour la suppression, les annexe, les fichiers statics, les fichiers de métadonnées et les clefs
* ApiRequester, Authentifier: gestion de l'erreur ConnectionError #168
* ProcessingExecutionAction: Prise en compte des behaviors pour les exécutions mettant à jour une donnée #166

### [Changed]
Expand Down
5 changes: 4 additions & 1 deletion sdk_entrepot_gpf/_conf/default.ini
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ totp_key=
# En cas d'échec lors de l'authentification : max nb_attempts tentatives, sec_between_attempt secondes entre chacune d'entre elles
nb_attempts=5
sec_between_attempt=1

# url pour vérifier le bon fonctionnement de la GPF
check_status_url=https://status.uptrends.com/aa35b49e519e4f90866dc6bfc0a797a9

[store_api]
############################### Paramètres de l'API Entrepôt ###############################
Expand All @@ -35,6 +36,8 @@ nb_limit=10
# Regex de parsing du Content-Range des réponses
regex_content_range=(?P<i_min>[0-9]+)-(?P<i_max>[0-9]+)/(?P<len>[0-9]+)
regex_entity_id=(?P<id>[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})
# url pour vérifier le bon fonctionnement de la GPF
check_status_url=https://status.uptrends.com/aa35b49e519e4f90866dc6bfc0a797a9


[routing]
Expand Down
25 changes: 17 additions & 8 deletions sdk_entrepot_gpf/auth/Authentifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,23 +105,26 @@ def __request_new_token(self, nb_attempts: int) -> None:
# On tente de récupérer le message
try:
s_message = o_response.json()["error_description"]
if "Account is not fully set up" in s_message:
raise AuthentificationError(
"Problème lors de l'authentification, veuillez vous connecter via l'interface en ligne KeyCloak pour vérifier son compte."
+ f" Votre mot de passe est sûrement expiré. ({s_message})"
)
except Exception:
s_message = "pas de raison indiquée"
if "Account is not fully set up" in s_message:
raise AuthentificationError(
"Problème lors de l'authentification, veuillez vous connecter via l'interface en ligne KeyCloak pour vérifier son compte."
+ f" Votre mot de passe est sûrement expiré. ({s_message})"
)
raise requests.exceptions.HTTPError(f"Code retour authentification KeyCloak = {o_response.status_code} ({s_message})", response=o_response, request=o_response.request)
except AuthentificationError as e_auth:
Config().om.error(e_auth.message)
# Affiche la pile d'exécution
Config().om.debug(traceback.format_exc())
# On propage l'erreur
raise e_auth
except Exception as e_error:
if isinstance(e_error, requests.exceptions.HTTPError):
Config().om.warning(e_error.args[0])
elif isinstance(e_error, requests.exceptions.ConnectionError):
Config().om.warning(
f"Le serveur d'authentification ({self.__token_url}) n'est pas joignable. Cela peut être dû à un problème de configuration si elle a changée récemment."
+ " Sinon, c'est un problème sur le service d’authentification : consultez l'état du service pour en savoir plus "
+ f": {Config().get_str('store_authentification', 'check_status_url')}."
)
else:
Config().om.warning("La récupération du jeton d'authentification a échoué...")
# Une erreur s'est produite : attend un peu et relance une nouvelle fois la fonction
Expand Down Expand Up @@ -150,6 +153,12 @@ def get_access_token_string(self) -> str:
while (self.__last_token is None) or (self.__last_token.is_valid() is False):
self.__request_new_token(self.__nb_attempts)
return self.__last_token.get_access_string()
except AuthentificationError as e_auth:
# erreur déjà traité
Config().om.error(e_auth.message)
# Affiche la pile d'exécution
Config().om.debug(traceback.format_exc())
raise e_auth
except Exception as e_error:
s_error_message = f"La récupération du jeton d'authentification a échoué après {self.__nb_attempts} tentatives"
Config().om.error(s_error_message)
Expand Down
15 changes: 15 additions & 0 deletions sdk_entrepot_gpf/io/ApiRequester.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,21 @@ def url_request(
except (ConflictError, NotFoundError, requests.Timeout) as e_error:
# S'il y a un conflit, un 404 ou un timeout, on ne retente pas, on ne fait rien. On propage l'erreur.
raise e_error
except requests.exceptions.ConnectionError as e_connexion:
s_message = (
f"Le serveur de l'API Entrepôt ({url}) n'est pas joignable. Cela peut être dû à un problème de configuration si elle a changée récemment."
+ " Sinon, c'est un problème sur l'API Entrepôt : consultez l'état du service pour en savoir plus "
+ f": {Config().get_str('store_api', 'check_status_url')}."
)
Config().om.warning(s_message)
# Affiche la pile d'exécution
Config().om.debug(traceback.format_exc())
# Une erreur s'est produite : attend un peu et relance une nouvelle fois la fonction
if i_nb_attempts < self.__nb_attempts:
time.sleep(self.__sec_between_attempt)
# Le nombre de tentatives est atteint : comme dirait Jim, this is the end...
else:
raise GpfSdkError(s_message) from e_connexion

except (ApiError, requests.RequestException) as e_error:
# Pour les autres erreurs, on retente selon les paramètres indiqués.
Expand Down
2 changes: 1 addition & 1 deletion sdk_entrepot_gpf/workflow/action/UploadAction.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def run(self, datastore: Optional[str], check_before_close: bool = False) -> Upl
# Envoie des fichiers md5 (pas de vérification sur les problèmes de livraison si check_before_close)
self.__push_md5_files(not check_before_close)
if check_before_close:
Config().om.info(f"Livraison {self.upload}: vérification de l'arborescent avant livraison ...")
Config().om.info(f"Livraison {self.upload}: vérification de l'arborescence avant livraison ...")
# vérification de la livraison des fichiers de données + ficher md5
l_error = self.__check_file_uploaded(list(self.__dataset.data_files.items()) + [(p_file, "") for p_file in self.__dataset.md5_files])
if l_error:
Expand Down
25 changes: 21 additions & 4 deletions tests/auth/AuthentifierTestCase.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from unittest.mock import patch
from http import HTTPStatus
import requests
import requests_mock

from sdk_entrepot_gpf.io.Config import Config
Expand Down Expand Up @@ -92,10 +93,10 @@ def test_get_access_token_string_too_much_attempts(self) -> None:
o_mock.post(
AuthentifierTestCase.url,
[
{"status_code": HTTPStatus.INTERNAL_SERVER_ERROR},
{"status_code": HTTPStatus.INTERNAL_SERVER_ERROR},
{"status_code": HTTPStatus.INTERNAL_SERVER_ERROR},
{"status_code": HTTPStatus.INTERNAL_SERVER_ERROR},
{"exc": Exception()},
{"status_code": 1, "json": {"error_description": "..."}},
{"status_code": 1, "json": {}},
{"exc": requests.exceptions.ConnectionError()},
],
)
# On s'attend à une exception
Expand All @@ -107,6 +108,22 @@ def test_get_access_token_string_too_much_attempts(self) -> None:
# On a dû faire 4 requêtes
self.assertEqual(o_mock.call_count, 4, "o_mock.call_count == 4")

def test_get_access_token_string_ko(self) -> None:
"""Vérifie les sorties en erreur de get_access_token_string"""
# code sortie non spécifique et mdp expirer
with requests_mock.Mocker() as o_mock:
s_message = "blabla. Account is not fully set up ... suite"
o_mock.post(AuthentifierTestCase.url, json={"error_description": s_message}, status_code=1)
with self.assertRaises(AuthentificationError) as o_arc:
# On tente de récupérer un token...
Authentifier().get_access_token_string()
print(o_arc.exception.args)
self.assertEqual(
o_arc.exception.message,
f"Problème lors de l'authentification, veuillez vous connecter via l'interface en ligne KeyCloak pour vérifier son compte. Votre mot de passe est sûrement expiré. ({s_message})",
)
# erreur de connexion

def test_get_http_header(self) -> None:
"""Vérifie le bon fonctionnement de test_get_http_header."""
# On mock get_access_token_string qui est déjà testée
Expand Down
53 changes: 37 additions & 16 deletions tests/io/ApiRequesterTestCase.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ def tearDownClass(cls) -> None:
super().tearDownClass()
# On ne mock plus la classe d'authentification
cls.o_mock_authentifier.stop()
# On détruit le Singleton Config
Config._instance = None

def test_route_request_ok_datastore_config(self) -> None:
"""Test de route_request quand la route existe en utilisant le datastore de base."""
Expand Down Expand Up @@ -247,14 +249,7 @@ def test_url_request_internal_server_error(self) -> None:
# On mock...
with requests_mock.Mocker() as o_mock:
# Une requête non réussie
o_mock.post(
self.url,
[
{"status_code": HTTPStatus.INTERNAL_SERVER_ERROR},
{"status_code": HTTPStatus.INTERNAL_SERVER_ERROR},
{"status_code": HTTPStatus.INTERNAL_SERVER_ERROR},
],
)
o_mock.post(self.url, status_code=HTTPStatus.INTERNAL_SERVER_ERROR)
# On s'attend à une exception
with self.assertRaises(GpfSdkError) as o_arc:
# On effectue une requête
Expand Down Expand Up @@ -300,14 +295,7 @@ def test_url_request_not_found(self) -> None:
# On mock...
with requests_mock.Mocker() as o_mock:
# Une requête non réussie
o_mock.post(
self.url,
[
{"status_code": HTTPStatus.NOT_FOUND},
{"status_code": HTTPStatus.NOT_FOUND},
{"status_code": HTTPStatus.NOT_FOUND},
],
)
o_mock.post(self.url, status_code=HTTPStatus.NOT_FOUND)
# On s'attend à une exception
with self.assertRaises(NotFoundError):
# On effectue une requête
Expand Down Expand Up @@ -336,6 +324,25 @@ def test_url_request_not_authorized(self) -> None:
# On a dû faire 2 appels à revoke_token
self.assertEqual(o_mock_revoke_token.call_count, 2, "o_mock_revoke_token.call_count == 2")

def test_url_request_connection_error(self) -> None:
"""Test de url_request dans le cadre d'une erreur ConnectionError."""
# On mock...
with requests_mock.Mocker() as o_mock:
o_mock.get(self.url, exc=requests.exceptions.ConnectionError)
# On s'attend à une exception
with self.assertRaises(GpfSdkError) as o_arc:
# Lancement de la requête
ApiRequester().url_request(self.url, ApiRequester.GET, params=self.param, data=self.data)
# On doit avoir un message d'erreur
self.assertEqual(
o_arc.exception.message,
f"Le serveur de l'API Entrepôt ({self.url}) n'est pas joignable. Cela peut être dû à un problème de configuration si elle a changée récemment."
+ " Sinon, c'est un problème sur l'API Entrepôt : consultez l'état du service pour en savoir plus "
+ f": {Config().get_str('store_api', 'check_status_url')}.",
)
# On a dû faire le max de requête
self.assertEqual(o_mock.call_count, Config().get_int("store_api", "nb_attempts"), "o_mock.call_count == max")

def test_url_request_http_error(self) -> None:
"""Test de url_request dans le cadre où on a une HTTPError."""
# On mock...
Expand All @@ -350,6 +357,20 @@ def test_url_request_http_error(self) -> None:
# On a dû faire 1 seule requête
self.assertEqual(o_mock.call_count, 1, "o_mock.call_count == 1")

def test_url_request_code_autre(self) -> None:
"""Test de url_request dans le cadre où on code retour non pris en charge."""
# On mock...
with requests_mock.Mocker() as o_mock:
o_mock.post(self.url, status_code=1)
# On s'attend à une exception
with self.assertRaises(GpfSdkError) as o_arc:
# Lancement de la requête
ApiRequester().url_request(self.url, ApiRequester.POST, params=self.param, data=self.data)
# On doit avoir un message d'erreur
self.assertEqual(o_arc.exception.message, "L'exécution d'une requête a échoué après 3 tentatives.")
# On a dû faire 1 seule requête
self.assertEqual(o_mock.call_count, 3, "o_mock.call_count == 3")

def test_url_request_url_required(self) -> None:
"""Test de url_request dans le cadre où on a une URLRequired."""
# On mock...
Expand Down