api: simplify push_rdv()/upload_rdvs() (#89609)
gitea/ants-hub/pipeline/head This commit looks good Details

* all errors are transmitted through exceptions
* last_upload is always updated even if no API call is necessary
* report when DELETE returns rowcount=0 or POST is not success
This commit is contained in:
Benjamin Dauvergne 2024-04-16 01:20:44 +02:00
parent 99a25b6df9
commit dbffe4bef0
2 changed files with 174 additions and 29 deletions

View File

@ -6,7 +6,7 @@ import re
import requests
from django.db import OperationalError
from django.db.models import F, Q
from django.db.transaction import atomic, set_rollback
from django.db.transaction import atomic
from ants_hub.data.models import Config, RendezVous
from ants_hub.timezone import localtime, now
@ -75,7 +75,8 @@ class APIDoublon:
]
response = self.request(method='POST', endpoint='appointments', params=params)
try:
return response['success']
if not response['success']:
raise RuntimeError('creation failed, success is not true')
except Exception as e:
raise ANTSError(str(e), {'response': response})
@ -89,7 +90,8 @@ class APIDoublon:
]
response = self.request(method='DELETE', endpoint='appointments', params=params)
try:
return 'rowcount' in response
if not bool(response['rowcount']):
raise RuntimeError('deletion failed, rowcount is zero')
except Exception as e:
raise ANTSError(str(e), {'response': response})
@ -132,38 +134,27 @@ def push_rdv(rdv):
api_doublon = APIDoublon()
delete = False
create = False
existing = api_doublon.existing_rdv(rdv)
if rdv.canceled:
action = 'deleted'
if existing:
delete = True
if not existing:
# Tout est déjà bon, on ne fait rien
return False
api_doublon.delete_rdv(rdv)
return 'deleted'
else:
action = 'created'
if len(existing) > 1:
delete = True
create = True
# Il y a des doublons pour la même référence meeting_point/date on supprime tout
api_doublon.delete_rdv(rdv)
elif existing:
if existing[0]['management_url'] != rdv.get_gestion_url_for_ants():
# L'URL de gestion a changé, on supprime le RdV existant
delete = True
create = True
api_doublon.delete_rdv(rdv)
else:
return True
else:
create = True
success = True
if delete:
success = success and api_doublon.delete_rdv(rdv)
if create:
success = success and api_doublon.create_rdv(rdv)
return success and (delete or create) and action
# Tout est déjà bon, on ne fait rien
return False
api_doublon.create_rdv(rdv)
return 'created'
def get_rdvs_to_upload():
@ -184,14 +175,14 @@ def upload_rdvs():
except RendezVous.DoesNotExist:
continue
try:
RendezVous.objects.filter(pk=rdv.pk).update(last_upload=start)
result = push_rdv(rdv)
if result:
# si result == False, c'est un rejeu inutile, on ne log rien, mais on met à jour last_upload
logger.info(f'{result} rdv(%s) of lieu %s', rdv, rdv.lieu)
continue
rdv.last_upload = start
rdv.save(update_fields=['last_upload'])
except ANTSError as e:
logger.warning('unable to push rdv(%s) of lieu %s: %r', rdv, rdv.lieu, e)
set_rollback(True)
except OperationalError:
pass

View File

@ -663,6 +663,91 @@ class TestAPIV2Push:
assert post_response.call_count == 1
assert delete_response.call_count == 1
@responses.activate
@pytest.mark.parametrize(
'response_kwargs',
[
{
'json': {},
'status': 200,
},
{
'json': None,
'status': 200,
},
{
'json': '',
'status': 200,
},
{
'json': {'detail': []},
'status': 422,
},
{
'status': 500,
},
],
ids=[
'JSON is empty dict',
'JSON is null',
'JSON is empty string',
'HTTP 422 with details',
'HTTP 500',
],
)
def test_delete_duplicates_error(self, response_kwargs, db, freezer):
assert RendezVous.objects.count() == 1
assert RendezVous.objects.filter(canceled__isnull=True).count() == 1
assert RendezVous.objects.filter(last_upload__isnull=True).count() == 1
responses.add(
responses.GET,
'https://api-coordination.rendezvouspasseport.ants.gouv.fr/api/status',
json={
'ABCD123456': {
'appointments': [
{
'meeting_point': 'Mairie',
'appointment_date': '2023-04-03T12:15:00',
},
{
'meeting_point': 'Mairie',
'appointment_date': '2023-04-03T12:15:00',
},
]
}
},
status=200,
match=[
responses.matchers.header_matcher({'x-rdv-opt-auth-token': 'abcd'}),
],
)
post_response = responses.add(
responses.POST,
'https://api-coordination.rendezvouspasseport.ants.gouv.fr/api/appointments',
json={'success': True},
status=200,
match=[responses.matchers.header_matcher({'x-rdv-opt-auth-token': 'abcd'})],
)
delete_response = responses.add(
responses.DELETE,
'https://api-coordination.rendezvouspasseport.ants.gouv.fr/api/appointments',
match=[responses.matchers.header_matcher({'x-rdv-opt-auth-token': 'abcd'})],
**response_kwargs,
)
RendezVous.objects.update(canceled=now(), last_update=now())
assert RendezVous.objects.count() == 1
assert RendezVous.objects.filter(last_upload__isnull=True).count() == 1
call_command('upload-rdvs')
assert post_response.call_count == 0
assert delete_response.call_count == 1
assert RendezVous.objects.count() == 1
assert RendezVous.objects.filter(last_upload__isnull=True).count() == 1
@responses.activate
def test_delete_management_url_changed(self, db, freezer):
assert RendezVous.objects.count() == 1
@ -709,6 +794,75 @@ class TestAPIV2Push:
assert post_response.call_count == 1
assert delete_response.call_count == 1
@responses.activate
def test_create_already_exists(self, db, freezer):
responses.add(
responses.GET,
'https://api-coordination.rendezvouspasseport.ants.gouv.fr/api/status',
json={
'ABCD123456': {
'status': 'validated',
'appointments': [
{
'meeting_point': 'Mairie',
'appointment_date': '2023-04-03T12:15:00',
'management_url': (
'https://ants-hub.entrouvert.org/rdv/saint-didier-1/'
'mairie-1/2023-04-03T12:15:00+02:00/gestion/7cace277-9157-4fbc-9705-45522984805d/'
),
}
],
}
},
status=200,
match=[
responses.matchers.header_matcher({'x-rdv-opt-auth-token': 'abcd'}),
],
)
post_response = responses.add(
responses.POST,
'https://api-coordination.rendezvouspasseport.ants.gouv.fr/api/appointments',
json={'success': True},
status=200,
match=[responses.matchers.header_matcher({'x-rdv-opt-auth-token': 'abcd'})],
)
assert RendezVous.objects.count() == 1
assert RendezVous.objects.filter(last_upload__isnull=True).count() == 1
call_command('upload-rdvs')
assert post_response.call_count == 0
assert RendezVous.objects.count() == 1
assert RendezVous.objects.filter(last_upload__isnull=False).count() == 1
@responses.activate
def test_delete_already_deleted(self, db, freezer):
responses.add(
responses.GET,
'https://api-coordination.rendezvouspasseport.ants.gouv.fr/api/status',
json={},
status=200,
match=[
responses.matchers.header_matcher({'x-rdv-opt-auth-token': 'abcd'}),
],
)
delete_response = responses.add(
responses.DELETE,
'https://api-coordination.rendezvouspasseport.ants.gouv.fr/api/appointments',
json={'rowcount': 1},
status=200,
match=[responses.matchers.header_matcher({'x-rdv-opt-auth-token': 'abcd'})],
)
RendezVous.objects.update(canceled=now(), last_update=now())
assert RendezVous.objects.count() == 1
assert RendezVous.objects.filter(last_upload__isnull=True).count() == 1
call_command('upload-rdvs')
assert delete_response.call_count == 0
assert RendezVous.objects.count() == 1
assert RendezVous.objects.filter(last_upload__isnull=False).count() == 1
class TestGetStatusOfPredemandes:
@pytest.fixture(autouse=True)