Skip to content

Commit

Permalink
Enhance tests to reflect real world API returns
Browse files Browse the repository at this point in the history
  • Loading branch information
celine-m-s committed Oct 18, 2024
1 parent 994e398 commit 408259c
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 37 deletions.
62 changes: 41 additions & 21 deletions itou/utils/apis/api_particulier.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@
logger = logging.getLogger("APIParticulierClient")


class ShouldRetryException(httpx.HTTPStatusError):
"""
This exception can be used to ask Tenacity to retry
while attaching a response and a request to it.
"""

pass


class APIParticulierClient:
def __init__(self, job_seeker=None):
self.client = httpx.Client(
Expand Down Expand Up @@ -57,44 +66,52 @@ def _build_params_from(cls, job_seeker):
@tenacity.retry(
wait=tenacity.wait_fixed(2),
stop=tenacity.stop_after_attempt(4),
retry=tenacity.retry_if_exception_type(httpx.RequestError),
retry=tenacity.retry_if_exception_type(ShouldRetryException),
)
def _request(self, endpoint, params=None):
params = self._build_params_from(job_seeker=self.job_seeker)
response = self.client.get(endpoint, params=params)
# Too Many Requests, Server error
if response.status_code in (429, 504):
reason = response.json().get("reason")
logger.warning(f"{response.url=} {reason=}")
raise httpx.RequestError(message=reason)
error_message = None
# Too Many Requests
if response.status_code == 429:
errors = response.json().get("errors")
if errors:
error_message = errors[0]
raise ShouldRetryException(message=error_message, request=response.request, response=response)
# Bad params.
# Same as 503 except we don't retry.
elif response.status_code in (400, 401):
errors = response.json()["errors"]
raise httpx.HTTPStatusError(message=error_message, request=response.request, response=response)
# Service unavailable
elif response.status_code == 503:
errors = response.json()["errors"]
reason = errors[0].get("title")
for error in errors:
logger.warning(f"{response.url=} {error['title']}")
raise httpx.RequestError(message=reason)
error_message = response.json().get("error")
if error_message:
error_message = response.json().get("reason")
else:
errors = response.json().get("errors")
error_message = errors[0].get("title")
raise ShouldRetryException(message=error_message, request=response.request, response=response)
# Server error
elif response.status_code == 504:
error_message = response.json().get("reason")
raise ShouldRetryException(message=error_message, request=response.request, response=response)
else:
response.raise_for_status()
return response.json()

def revenu_solidarite_active(self):
data = {
"start_at": "",
"end_at": "",
"is_certified": "",
"raw_response": "",
}
data = {"start_at": "", "end_at": "", "is_certified": "", "raw_response": ""}
error_message = None
try:
data = self._request("/v2/revenu-solidarite-active")
except httpx.HTTPStatusError as exc: # not 5XX.
logger.debug(f"Beneficiary not found. {self.job_seeker.public_id=}")
data["raw_response"] = exc.response.json()
error_message = f"{exc.response.status_code}: {exc.response.json()} {exc.response.url=}"
except tenacity.RetryError as retry_err: # 429, 503 or 504
exc = retry_err.last_attempt._exception
data["raw_response"] = str(exc)
error_message = f"{exc.response.status_code}: {exc.response.json()} {exc.response.url=}"
except KeyError as exc:
data["raw_response"] = str(exc)
error_message = f"KeyError: {exc=}"
else:
data = {
"start_at": self.format_date(data["dateDebut"]),
Expand All @@ -103,4 +120,7 @@ def revenu_solidarite_active(self):
"raw_response": data,
}
finally:
if error_message:
data["raw_response"] = error_message
logger.warning(error_message)
return data
139 changes: 123 additions & 16 deletions tests/utils/apis/test_api_particulier.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def test_not_found(respx_mock):
job_seeker = JobSeekerFactory(born_in_france=True)
client = APIParticulierClient(job_seeker=job_seeker)
response = client.revenu_solidarite_active()
assert response["raw_response"] == rsa_not_found_mocker()
assert response["raw_response"].startswith(f"404: {rsa_not_found_mocker()}")
assert response["is_certified"] == ""
assert response["start_at"] == ""
assert response["end_at"] == ""
Expand All @@ -76,28 +76,116 @@ def test_not_found(respx_mock):
def test_service_unavailable(settings, respx_mock, mocker, caplog):
mocker.patch("tenacity.nap.time.sleep")
reason = "Erreur inconnue du fournisseur de données"
json = {
"errors": [
{
"code": "37999",
"title": reason,
"detail": "La réponse retournée par le fournisseur de données est invalide et inconnue de "
"notre service. L'équipe technique a été notifiée de cette erreur pour investigation.",
"source": None,
"meta": {"provider": "CNAV"},
}
]
}
respx_mock.get(RSA_ENDPOINT).respond(
503,
json={
"errors": [
{
"code": "37999",
"title": reason,
"detail": "La réponse retournée par le fournisseur de données est invalide et inconnue de notre"
"service. L'équipe technique a été notifiée de cette erreur pour investigation.",
"source": "null",
"meta": {"provider": "CNAV"},
}
]
},
json=json,
)
job_seeker = JobSeekerFactory(born_in_france=True)
client = APIParticulierClient(job_seeker=job_seeker)
response = client.revenu_solidarite_active()

assert reason in caplog.text
assert RSA_ENDPOINT in caplog.text
assert response["raw_response"] == reason
assert response["raw_response"].startswith(f"503: {json}")
assert response["is_certified"] == ""
assert response["start_at"] == ""
assert response["end_at"] == ""


def test_provider_unknown(settings, respx_mock, mocker, caplog):
mocker.patch("tenacity.nap.time.sleep")
reason = (
"La réponse retournée par le fournisseur de données est invalide et inconnue de notre service. L'équipe "
"technique a été notifiée de cette erreur pour investigation."
)
json = {
"error": "provider_unknown_error",
"reason": reason,
"message": reason,
}
respx_mock.get(RSA_ENDPOINT).respond(
503,
json=json,
)
job_seeker = JobSeekerFactory(born_in_france=True)
client = APIParticulierClient(job_seeker=job_seeker)
response = client.revenu_solidarite_active()

assert reason in caplog.text
assert RSA_ENDPOINT in caplog.text
assert response["raw_response"].startswith(f"503: {json}")
assert response["is_certified"] == ""
assert response["start_at"] == ""
assert response["end_at"] == ""


def test_bad_params(settings, respx_mock, mocker, caplog):
mocker.patch("tenacity.nap.time.sleep")
reason = "Entité non traitable"
json = {
"errors": [
{
"code": "00364",
"title": reason,
"detail": "Le sexe n'est pas correctement formaté (m ou f)",
"source": None,
"meta": {},
}
]
}
respx_mock.get(RSA_ENDPOINT).respond(
400,
json=json,
)
job_seeker = JobSeekerFactory(born_in_france=True)
client = APIParticulierClient(job_seeker=job_seeker)
response = client.revenu_solidarite_active()

assert reason in caplog.text
assert RSA_ENDPOINT in caplog.text
assert response["raw_response"].startswith(f"400: {json}")
assert response["is_certified"] == ""
assert response["start_at"] == ""
assert response["end_at"] == ""


def test_forbidden(settings, respx_mock, mocker, caplog):
mocker.patch("tenacity.nap.time.sleep")
reason = "Accès non autorisé"
json = {
"errors": [
{
"code": "50002",
"title": reason,
"detail": "Le jeton d'accès n'a pas été trouvé ou est expiré.",
"source": None,
"meta": {},
}
]
}
respx_mock.get(RSA_ENDPOINT).respond(
401,
json=json,
)
job_seeker = JobSeekerFactory(born_in_france=True)
client = APIParticulierClient(job_seeker=job_seeker)
response = client.revenu_solidarite_active()

assert reason in caplog.text
assert RSA_ENDPOINT in caplog.text
assert response["raw_response"].startswith(f"401: {json}")
assert response["is_certified"] == ""
assert response["start_at"] == ""
assert response["end_at"] == ""
Expand All @@ -106,15 +194,34 @@ def test_service_unavailable(settings, respx_mock, mocker, caplog):
def test_gateway_timeout(respx_mock, mocker, caplog):
mocker.patch("tenacity.nap.time.sleep", mocker.MagicMock())
reason = "The read operation timed out"
respx_mock.get(RSA_ENDPOINT).respond(504, json={"error": "null", "reason": reason, "message": "null"})
json = {"error": None, "reason": reason, "message": "null"}
respx_mock.get(RSA_ENDPOINT).respond(504, json=json)

job_seeker = JobSeekerFactory(born_in_france=True)
client = APIParticulierClient(job_seeker=job_seeker)
response = client.revenu_solidarite_active()

assert reason in caplog.text
assert RSA_ENDPOINT in caplog.text
assert response["raw_response"].startswith(f"504: {json}")
assert response["is_certified"] == ""
assert response["start_at"] == ""
assert response["end_at"] == ""


def test_too_many_requests(respx_mock, mocker, caplog):
mocker.patch("tenacity.nap.time.sleep", mocker.MagicMock())
reason = "Vous avez effectué trop de requêtes"
json = {"errors": ["Vous avez effectué trop de requêtes"]}
respx_mock.get(RSA_ENDPOINT).respond(429, json=json)

job_seeker = JobSeekerFactory(born_in_france=True)
client = APIParticulierClient(job_seeker=job_seeker)
response = client.revenu_solidarite_active()

assert reason in caplog.text
assert RSA_ENDPOINT in caplog.text
assert response["raw_response"] == reason
assert response["raw_response"].startswith(f"429: {json}")
assert response["is_certified"] == ""
assert response["start_at"] == ""
assert response["end_at"] == ""
Expand Down

0 comments on commit 408259c

Please sign in to comment.