passerelle/tests/test_mdph13.py

507 lines
21 KiB
Python

# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
import datetime
import json
import logging
import httmock
import pytest
import requests
import requests.exceptions
import tests.utils
from passerelle.contrib.mdph13.models import Link, MDPH13Resource
from passerelle.utils.jsonresponse import APIError
NAME_ID = 'xyz'
FILE_NUMBER = '1234'
DOB = datetime.date(1993, 5, 4)
DOB_ISOFORMAT = '1993-05-04'
EMAIL = 'john.doe@example.com'
SECRET = 'secret'
IP = '88.34.56.56'
VALID_RESPONSE = json.dumps(
{
'err': 0,
'data': {
'numero': FILE_NUMBER,
'beneficiaire': {
'nom': 'Martini',
'prenom': 'Alfonso',
'tel_mobile': '06 01 02 03 04',
'tel_fixe': '04.01.02.03.04',
'date_de_naissance': '1951-03-23',
'email': 'martini.a@free.fr',
'entourage': [
{
'role': 'Père',
'nom': 'DUPONT Henri',
'tel_mobile': '0123232323',
'tel_fixe': '0202020202',
'email': 'henri.dupont@xyz.com',
},
{
'role': 'Mère',
'nom': 'DUPONT Marie',
'tel_mobile': '0123232323',
'tel_fixe': '0202020202',
'email': 'marie.dupont@xyz.com',
},
{
'role': 'Aidant',
'nom': 'ROBERT Fanny',
'tel_mobile': '0123232323',
'tel_fixe': '0202020202',
'email': 'frobert@xyz.com',
},
],
'adresse': {
'adresse_2': 'Bliblibli',
'adresse_3': 'Bliblibli',
'adresse_4': 'CHEMIN DE LA CARRAIRE',
'adresse_5': 'Bliblibli',
'code_postal': '13500',
'ville': 'MARTIGUES',
},
'incapacite': {'taux': 'Taux >=80%', 'date_fin_effet': '2019-06-30'},
},
'demandes': [
{
'numero': '1544740',
'date_demande': '2015-11-26',
'type_demande': 'Renouvellement',
'prestation': "Carte d'invalidité (de priorité) pour personne handicapée",
'statut': 'Instruction administrative terminée en attente de passage en évaluation',
'typologie': 'Demande En Cours',
'date_decision': None,
},
{
'numero': '1210524',
'date_demande': '2014-06-13',
'type_demande': 'Renouvellement',
'prestation': "Carte d'invalidité (de priorité) pour personne handicapée",
'statut': 'Décision prononcée et expédition réalisée (traitement terminé)',
'typologie': 'Traitée non expédiée',
'date_decision': '2014-07-10',
'date_debut_effet': '2014-08-01',
'date_fin_effet': '2016-05-01',
},
{
'numero': '1231345',
'date_demande': '2014-07-22',
'type_demande': 'Recours Gracieux',
'prestation': "Carte d'invalidité (de priorité) pour personne handicapée",
'statut': 'Décision prononcée et expédition réalisée (traitement terminé)',
'typologie': 'Traitée et expédiée',
'date_decision': '2014-09-17',
'date_debut_effet': '2014-08-01',
'date_fin_effet': '2016-05-01',
},
{
'numero': '666660',
'date_demande': '2012-08-13',
'type_demande': 'Recours Gracieux',
'prestation': "Carte d'invalidité (de priorité) pour personne handicapée",
'statut': 'Décision prononcée et expédition réalisée (traitement terminé)',
'typologie': 'Traitée et expédiée',
'date_decision': '2012-09-26',
'date_debut_effet': '2012-07-19',
'date_fin_effet': '2014-08-01',
},
{
'numero': '605280',
'date_demande': '2012-04-05',
'type_demande': '1ère demande',
'prestation': "Carte d'invalidité (de priorité) pour personne handicapée",
'statut': 'Décision prononcée et expédition réalisée (traitement terminé)',
'typologie': 'Traitée et expédiée',
'date_decision': '2012-07-19',
'date_debut_effet': '2012-07-19',
'date_fin_effet': '2014-05-01',
},
{
'numero': '1544741',
'date_demande': '2015-11-26',
'type_demande': 'Renouvellement',
'prestation': "Carte d'invalidité (de priorité) pour personne handicapée",
'statut': 'Décision prononcée et expédition réalisée (traitement terminé)',
'typologie': 'Traitée et expédiée',
'date_decision': '2015-12-22',
'date_debut_effet': '2016-05-01',
'date_fin_effet': '2026-05-01',
},
{
'numero': '1210526',
'date_demande': '2014-06-13',
'type_demande': 'Renouvellement',
'prestation': 'Carte européenne de Stationnement',
'statut': 'Décision prononcée et expédition réalisée (traitement terminé)',
'typologie': 'Traitée et expédiée',
'date_decision': '2014-07-04',
'date_debut_effet': '2014-05-01',
'date_fin_effet': '2015-05-01',
},
{
'numero': '605281',
'date_demande': '2012-04-05',
'type_demande': '1ère demande',
'prestation': 'Carte européenne de Stationnement',
'statut': 'Décision prononcée et expédition réalisée (traitement terminé)',
'typologie': 'Traitée et expédiée',
'date_decision': '2012-07-04',
'date_debut_effet': '2012-05-01',
'date_fin_effet': '2014-05-01',
},
],
},
}
)
DOSSIER_INCONNU = {
'status_code': 404,
'content': json.dumps(
{
'err': 1,
'err_code': 'dossier-inconnu',
}
),
}
SECRET_INVALIDE = {
'status_code': 404,
'content': json.dumps(
{
'err': 1,
'err_code': 'secret-invalide',
}
),
}
@pytest.fixture
def mdph13(db):
return tests.utils.make_resource(
MDPH13Resource,
title='Test 1',
slug='test1',
description='Connecteur de test',
webservice_base_url='http://cd13.fr/',
)
@pytest.fixture
def mock_http():
class MockHttp:
def __init__(self):
self.requests = []
self.responses = []
def add_response(self, response):
self.responses.append(response)
@property
def last_request(self):
return self.requests[-1]
def request_handler(self, url, request):
idx = len(self.requests)
self.requests.append(request)
response = self.responses[idx]
if isinstance(response, Exception):
raise response
if hasattr(response, '__call__'):
response = response(url, request)
return response
mock_http = MockHttp()
with httmock.HTTMock(httmock.urlmatch()(mock_http.request_handler)):
yield mock_http
def test_situation_dossier_url(mdph13):
assert mdph13.situation_dossier_url(1234) == 'http://cd13.fr/situation/dossier/1234'
def test_call_situation_dossier(mdph13, mock_http):
mock_http.add_response(VALID_RESPONSE)
mdph13.call_situation_dossier(1234, SECRET, DOB)
request = mock_http.last_request
headers = request.headers
url = request.url
assert url == 'http://cd13.fr/situation/dossier/1234'
assert headers['X-CD13-Secret'] == base64.b64encode(SECRET.encode('utf-8')).decode('ascii')
assert headers['X-CD13-DateNaissBenef'] == '1993-05-04'
assert headers['X-CD13-Email'] == 'appel-sans-utilisateur@cd13.fr'
assert 'X-CD13-IP' not in headers
def test_call_situation_dossier_with_email_and_ip(mdph13, mock_http):
mock_http.add_response(VALID_RESPONSE)
mdph13.call_situation_dossier(1234, SECRET, DOB, email=EMAIL, ip=IP)
request = mock_http.last_request
headers = request.headers
url = request.url
assert url == 'http://cd13.fr/situation/dossier/1234'
assert headers['X-CD13-Secret'] == base64.b64encode(SECRET.encode('utf-8')).decode('ascii')
assert headers['X-CD13-DateNaissBenef'] == DOB_ISOFORMAT
assert headers['X-CD13-Email'] == EMAIL
assert headers['X-CD13-IP'] == IP
def test_link_bad_file_number(mdph13):
with pytest.raises(APIError) as e:
mdph13.link(
request=None, NameID=NAME_ID, numero_dossier='x', secret=None, date_de_naissance=None, email=None
)
assert str(e.value) == 'numero_dossier must be a number'
def test_link_bad_date_de_naissance(app, mdph13):
url = '/mdph13/test1/link/?NameID=%s&numero_dossier=%s&date_de_naissance=%s&secret=%s&email=%s'
url = url % (NAME_ID, FILE_NUMBER, '34-45-6', None, None)
response = app.post(url, status=400)
assert response.json['err_class'] == 'passerelle.views.InvalidParameterValue'
assert (
response.json['err_desc'] == 'invalid value for parameter "date_de_naissance (YYYY-MM-DD expected)"'
)
def test_link_bad_email(mdph13):
with pytest.raises(APIError) as e:
mdph13.link(
request=None,
NameID=NAME_ID,
numero_dossier=FILE_NUMBER,
secret=None,
date_de_naissance=DOB_ISOFORMAT,
email='xxx@@vvv',
)
assert str(e.value) == 'email is not valid'
def test_link_nok_dossier_inconnu(app, mdph13, mock_http):
mock_http.add_response(DOSSIER_INCONNU)
url = '/mdph13/test1/link/?NameID=%s&numero_dossier=%s&date_de_naissance=%s&secret=%s&email=%s'
url = url % (NAME_ID, FILE_NUMBER, DOB_ISOFORMAT, SECRET, EMAIL)
response = app.post(url)
assert response.json['err_class'] == 'passerelle.utils.jsonresponse.APIError'
assert response.json['err_desc'] == 'dossier-inconnu'
def test_link_nok_secret_invalide(app, mdph13, mock_http):
mock_http.add_response(SECRET_INVALIDE)
url = '/mdph13/test1/link/?NameID=%s&numero_dossier=%s&date_de_naissance=%s&secret=%s&email=%s'
url = url % (NAME_ID, FILE_NUMBER, DOB_ISOFORMAT, SECRET, EMAIL)
response = app.post(url)
assert response.json['err_class'] == 'passerelle.utils.jsonresponse.APIError'
assert response.json['err_desc'] == 'secret-invalide'
def test_link_numero_dont_match(app, mdph13, mock_http):
mock_http.add_response(json.dumps({'err': 0, 'data': {'numero': '456'}}))
url = '/mdph13/test1/link/?NameID=%s&numero_dossier=%s&date_de_naissance=%s&secret=%s&email=%s'
url = url % (NAME_ID, FILE_NUMBER, DOB_ISOFORMAT, SECRET, EMAIL)
response = app.post(url)
assert response.json['err_class'] == 'passerelle.utils.jsonresponse.APIError'
assert response.json['err_desc'] == 'numero-must-match-numero-dossier'
def test_link_ok(app, mdph13, mock_http):
# check first time link
mock_http.add_response(VALID_RESPONSE)
assert not Link.objects.count()
url = '/mdph13/test1/link/?NameID=%s&numero_dossier=%s&date_de_naissance=%s&secret=%s&email=%s&ip=%s'
url = url % (NAME_ID, FILE_NUMBER, DOB_ISOFORMAT, SECRET, EMAIL, IP)
response = app.post(url)
link = Link.objects.get()
assert response.json == {'err': 0, 'link_id': link.pk, 'created': True, 'updated': False}
# check relinking with update
mock_http.add_response(VALID_RESPONSE)
url = '/mdph13/test1/link/?NameID=%s&numero_dossier=%s&date_de_naissance=%s&secret=%s&email=%s&ip=%s'
url = url % (NAME_ID, FILE_NUMBER, DOB_ISOFORMAT, SECRET + 'a', EMAIL, IP)
response = app.post(url)
assert response.json == {
'err': 0,
'link_id': link.pk,
'created': False,
'updated': True,
}
def test_unlink_nok_bad_link_id(mdph13):
with pytest.raises(APIError) as e:
mdph13.unlink(None, None, 'e')
assert str(e.value) == 'link_id-must-be-a-number'
def test_unlink_ok(mdph13):
link = Link.objects.create(
resource=mdph13, name_id=NAME_ID, file_number=FILE_NUMBER, secret=SECRET, dob=DOB
)
result = mdph13.unlink(None, NAME_ID, str(link.pk))
assert result['deleted'] == 1
result = mdph13.unlink(None, NAME_ID, str(link.pk))
assert result['deleted'] == 0
def test_unlink_with_delete(mdph13, app):
link = Link.objects.create(
resource=mdph13, name_id=NAME_ID, file_number=FILE_NUMBER, secret=SECRET, dob=DOB
)
response = app.delete('/mdph13/%s/unlink/?NameID=%s&link_id=%s' % (mdph13.slug, NAME_ID, link.pk))
assert response.json['deleted'] == 1
def test_unlink_all_ok(mdph13):
Link.objects.create(resource=mdph13, name_id=NAME_ID, file_number=FILE_NUMBER, secret=SECRET, dob=DOB)
Link.objects.create(resource=mdph13, name_id=NAME_ID, file_number='12345', secret=SECRET, dob=DOB)
result = mdph13.unlink(None, NAME_ID, 'all')
assert result['deleted'] == 2
result = mdph13.unlink(None, NAME_ID, 'all')
assert result['deleted'] == 0
def test_dossier_ok(mdph13, mock_http):
link = Link.objects.create(
resource=mdph13, name_id=NAME_ID, file_number=FILE_NUMBER, secret=SECRET, dob=DOB
)
mock_http.add_response(VALID_RESPONSE)
response = mdph13.dossiers(None, NAME_ID, EMAIL, ip=IP)
assert response['data']
assert response['data'][0]['id'] == str(link.pk)
assert response['data'][0]['numero_dossier'] == FILE_NUMBER
assert response['data'][0]['date_de_naissance'] == DOB.isoformat()
assert response['data'][0]['dossier']['numero'] == FILE_NUMBER
assert response['data'][0]['text'] == 'Alfonso Martini #%s' % FILE_NUMBER
assert len(response['data'][0]['dossier']['beneficiaire']['entourage']) == 2
assert len(response['data'][0]['dossier']['beneficiaire']['entourage']['parents']) == 2
assert len(response['data'][0]['dossier']['beneficiaire']['entourage']['aidants']) == 1
assert len(response['data'][0]['dossier']['demandes']) == 2
assert len(response['data'][0]['dossier']['demandes']['en_cours']) == 1
assert len(response['data'][0]['dossier']['demandes']['historique']) == 7
# check demands are ordered by date
assert response['data'][0]['dossier']['demandes']['historique'][0]['date_demande'] == '2015-11-26'
dates = [demande['date_demande'] for demande in response['data'][0]['dossier']['demandes']['historique']]
assert sorted(dates, reverse=True) == dates
# check no demandes
valid_response = json.loads(VALID_RESPONSE)
del valid_response['data']['demandes']
mock_http.add_response(json.dumps(valid_response))
response = mdph13.dossiers(None, NAME_ID, EMAIL, ip=IP)
assert response['data']
assert response['data'][0]['err'] == 0
assert 'demandes' not in response['data'][0]['dossier']
def test_dossier_with_link_id_ok(mdph13, mock_http):
link = Link.objects.create(
resource=mdph13, name_id=NAME_ID, file_number=FILE_NUMBER, secret=SECRET, dob=DOB
)
mock_http.add_response(VALID_RESPONSE)
response = mdph13.dossiers(None, NAME_ID, EMAIL, link_id=str(link.pk), ip=IP)
assert response['data']
assert response['data']['id'] == str(link.pk)
assert response['data']['numero_dossier'] == FILE_NUMBER
assert response['data']['date_de_naissance'] == DOB.isoformat()
assert response['data']['dossier']['numero'] == FILE_NUMBER
assert response['data']['text'] == 'Alfonso Martini #%s' % FILE_NUMBER
assert len(response['data']['dossier']['beneficiaire']['entourage']) == 2
assert len(response['data']['dossier']['beneficiaire']['entourage']['parents']) == 2
assert len(response['data']['dossier']['beneficiaire']['entourage']['aidants']) == 1
assert len(response['data']['dossier']['demandes']) == 2
assert len(response['data']['dossier']['demandes']['en_cours']) == 1
assert len(response['data']['dossier']['demandes']['historique']) == 7
def test_dossier_partial_failure(mdph13, mock_http):
link1 = Link.objects.create(
resource=mdph13, name_id=NAME_ID, file_number=FILE_NUMBER, secret=SECRET, dob=DOB
)
link2 = Link.objects.create(
resource=mdph13, name_id=NAME_ID, file_number=FILE_NUMBER + '2', secret=SECRET, dob=DOB
)
mock_http.add_response(VALID_RESPONSE)
VALID_RESPONSE2 = json.loads(VALID_RESPONSE).copy()
VALID_RESPONSE2['data']['numero'] = FILE_NUMBER + '2'
mock_http.add_response(json.dumps(VALID_RESPONSE2))
# test that display_name is updated if changed after any access
link2.refresh_from_db()
assert not link2.display_name
response = mdph13.dossiers(None, NAME_ID, EMAIL)
link2.refresh_from_db()
assert link2.display_name
mock_http.add_response(VALID_RESPONSE)
mock_http.add_response({'status_code': 500, 'content': ''})
response = mdph13.dossiers(None, NAME_ID, EMAIL, ip=IP)
assert response['data']
assert response['data'][0]['id'] == str(link1.pk)
assert response['data'][0]['err'] == 0
assert response['data'][0]['text'] == 'Alfonso Martini #' + FILE_NUMBER
assert response['data'][1]['id'] == str(link2.pk)
assert response['data'][1]['err'] == 1
# verify display_name is returned even in case of failure
assert response['data'][1]['text'] == 'Alfonso Martini #' + FILE_NUMBER + '2'
def test_dossier_bad_date(mdph13, mock_http):
link = Link.objects.create(
resource=mdph13, name_id=NAME_ID, file_number=FILE_NUMBER, secret=SECRET, dob=DOB
)
INVALID_RESPONSE = json.loads(VALID_RESPONSE)
INVALID_RESPONSE['data']['demandes'][0]['date_demande'] = 'xxx'
mock_http.add_response(json.dumps(INVALID_RESPONSE))
with pytest.raises(APIError) as exc_info:
mdph13.dossiers(None, NAME_ID, EMAIL, link_id=str(link.pk))
assert str(exc_info.value) == 'invalid-response-format'
def test_dossier_http_error(app, mdph13, mock_http, caplog):
url = '/mdph13/test1/link/?NameID=%s&numero_dossier=%s&date_de_naissance=%s&secret=%s&email=%s&ip=%s'
url = url % (NAME_ID, FILE_NUMBER, DOB, SECRET, EMAIL, IP)
mock_http.add_response({'status_code': 401, 'content': 'wtf', 'reason': 'Authentication required'})
response = app.post(url, status=500)
assert response.json['err_class'] == 'requests.exceptions.HTTPError'
for record in caplog.records:
if (
record.getMessage() == 'GET http://cd13.fr/situation/dossier/1234 (=> 401)'
and record.levelno == logging.ERROR
):
break
else:
assert False, '401 caplog error message expected'
assert hasattr(record.request, 'META')
def raise_ssl_error(url, request):
raise requests.exceptions.SSLError(request=request)
caplog.clear()
mock_http.add_response(raise_ssl_error)
response = app.post(url, status=500)
assert response.json['err_class'] == 'requests.exceptions.SSLError'
for record in caplog.records:
if (
record.getMessage() == 'GET http://cd13.fr/situation/dossier/1234 (=> SSLError())'
and record.levelno == logging.ERROR
):
break
else:
assert False, 'SSLError caplog error message expected'
assert hasattr(record.request, 'META')