diff --git a/passerelle/contrib/mdph13/__init__.py b/passerelle/contrib/mdph13/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/passerelle/contrib/mdph13/migrations/0001_initial.py b/passerelle/contrib/mdph13/migrations/0001_initial.py
new file mode 100644
index 00000000..96b76c9d
--- /dev/null
+++ b/passerelle/contrib/mdph13/migrations/0001_initial.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.10 on 2019-02-15 09:57
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('base', '0010_loggingparameters_trace_emails'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Link',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name_id', models.CharField(max_length=256, verbose_name='NameID')),
+ ('file_number', models.CharField(max_length=64, verbose_name='MDPH beneficiary file number')),
+ ('secret', models.CharField(max_length=64, verbose_name='MDPH beneficiary secret')),
+ ('dob', models.DateField(verbose_name='MDPH beneficiary date of birth')),
+ ('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')),
+ ],
+ options={
+ 'ordering': ['file_number'],
+ },
+ ),
+ migrations.CreateModel(
+ name='MDPH13Resource',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=50, verbose_name='Title')),
+ ('description', models.TextField(verbose_name='Description')),
+ ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
+ ('basic_auth_username', models.CharField(blank=True, max_length=128, verbose_name='Basic authentication username')),
+ ('basic_auth_password', models.CharField(blank=True, max_length=128, verbose_name='Basic authentication password')),
+ ('client_certificate', models.FileField(blank=True, null=True, upload_to=b'', verbose_name='TLS client certificate')),
+ ('trusted_certificate_authorities', models.FileField(blank=True, null=True, upload_to=b'', verbose_name='TLS trusted CAs')),
+ ('verify_cert', models.BooleanField(default=True, verbose_name='TLS verify certificates')),
+ ('http_proxy', models.CharField(blank=True, max_length=128, verbose_name='HTTP and HTTPS proxy')),
+ ('webservice_base_url', models.URLField(verbose_name='Webservice Base URL')),
+ ('users', models.ManyToManyField(blank=True, to='base.ApiUser')),
+ ],
+ options={
+ 'verbose_name': 'MDPH CD13',
+ },
+ ),
+ migrations.AddField(
+ model_name='link',
+ name='resource',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mdph13.MDPH13Resource'),
+ ),
+ migrations.AlterUniqueTogether(
+ name='link',
+ unique_together=set([('resource', 'name_id', 'file_number')]),
+ ),
+ ]
diff --git a/passerelle/contrib/mdph13/migrations/__init__.py b/passerelle/contrib/mdph13/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/passerelle/contrib/mdph13/models.py b/passerelle/contrib/mdph13/models.py
new file mode 100644
index 00000000..114828e6
--- /dev/null
+++ b/passerelle/contrib/mdph13/models.py
@@ -0,0 +1,340 @@
+# -*- coding: utf-8 -*-
+# passerelle - uniform access to multiple data sources and services
+# Copyright (C) 2019 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 .
+
+import base64
+import urlparse
+import datetime
+import re
+
+import requests
+
+from django.db import models, transaction
+from django.utils import six
+from django.utils.translation import ugettext_lazy as _
+
+from passerelle.utils.jsonresponse import APIError, to_json
+from passerelle.utils.api import endpoint
+from passerelle.base.models import BaseResource, HTTPResource
+
+
+class MDPH13Resource(BaseResource, HTTPResource):
+ category = _('Business Process Connectors')
+
+ webservice_base_url = models.URLField(_('Webservice Base URL'))
+
+ EMAIL_RE = re.compile(r'^[^@\s]+@[^@\s]+\.[^@\s]+$')
+
+ class Meta:
+ verbose_name = _('MDPH CD13')
+
+ def situation_dossier_url(self, file_number):
+ return urlparse.urljoin(self.webservice_base_url, 'situation/dossier/%s' % file_number)
+
+ def url_get(self, *args, **kwargs):
+ try:
+ response = None
+ response = self.requests.get(*args, **kwargs)
+ response.raise_for_status()
+ except requests.RequestException as e:
+ data = {'exception': six.text_type(e)}
+ if response is not None:
+ try:
+ if response.json():
+ data['body'] = response.json()
+ except ValueError:
+ body = response.content[:1000]
+ if len(response.content) > 1000:
+ body += b''
+ data['body'] = repr(body)
+ data['status_code'] = response.status_code
+ raise APIError('HTTP request failed', data=data)
+ try:
+ content = response.json()
+ except ValueError as e:
+ data = {'exception': six.text_type(e)}
+ data['body'] = '%r' % response.content[:1000]
+ raise APIError('HTTP request did not respond with JSON', data=data)
+ return content
+
+ def call_situation_dossier(self, file_number, secret, dob, email=None):
+ url = self.situation_dossier_url(file_number)
+ email = email or 'appel-sans-utilisateur@cd13.fr'
+ error_mapping = {
+ 'dossier-inconnu': 'dossier-inconnu',
+ 'secret-invalide': 'secret-invalide',
+ 'dateNaissance-erronee': 'date-de-naissance-erronee',
+ }
+ try:
+ content = self.url_get(
+ url,
+ headers={
+ 'X-CD13-Secret': base64.b64encode(secret.encode('utf-8')).decode('ascii'),
+ 'X-CD13-Email': email,
+ 'X-CD13-DateNaissBenef': dob.isoformat(),
+ })
+ except APIError as e:
+ if e.data and e.data.get('status_code') == 404 and isinstance(e.data.get('body'), dict):
+ content = e.data['body']
+ if content.get('err') != 0:
+ error_code = content.get('err_code')
+ err_desc = error_mapping.get(error_code, 'err != 0')
+ raise APIError(err_desc, data=e.data)
+ raise
+
+ if content.get('err') != 0:
+ error_code = content.get('err_code')
+ err_desc = error_mapping.get(error_code, 'err != 0')
+ raise APIError(err_desc, data=content)
+
+ data = content.get('data')
+ if not isinstance(data, dict):
+ raise APIError('data-must-be-a-dict', data=content)
+
+ if str(data.get('numero')) != str(file_number):
+ raise APIError('numero-must-match-numero-dossier', date=content)
+
+ # Reorganize entourage
+ beneficiaire = data.get('beneficiaire', {})
+ entourage = beneficiaire.get('entourage')
+ if entourage is not None:
+ if not isinstance(entourage, list):
+ raise APIError('entourage-must-be-a-list', data=content)
+ if not all(isinstance(person, dict) for person in entourage):
+ raise APIError('demandes-content-must-be-dicts', data=content)
+ new_entourage = {}
+ for person in entourage:
+ if not isinstance(person, dict):
+ raise APIError('entourage-content-must-be-dicts', data=content)
+ if person.get('role') in [u'Père', u'Mère']:
+ new_entourage.setdefault('parents', []).append(person)
+ else:
+ new_entourage.setdefault('aidants', []).append(person)
+ beneficiaire['entourage'] = new_entourage
+
+ # Reorganize demandes
+ demandes = data.get('demandes')
+ if demandes:
+ if not isinstance(demandes, list):
+ raise APIError('demandes-must-be-a-list', data=content)
+ if not all(isinstance(demande, dict) for demande in demandes):
+ raise APIError('demandes-content-must-be-dicts', data=content)
+ new_demandes = {}
+ typologies = {
+ u'demande en cours': 'en_cours',
+ u'traitée et expédiée': 'historique',
+ u'traitée non expédiée': 'historique',
+ }
+ if not all(isinstance(demande.get('typologie'), six.text_type) for demande in demandes):
+ raise APIError('typologie-must-be-a-string', data=content)
+ if not all(demande['typologie'].lower() in typologies for demande in demandes):
+ unknowns = set([demande['typologie'].lower() for demande in demandes]) - set(typologies.keys())
+ raise APIError('typologie-is-unknown',
+ data={
+ 'unknowns': list(unknowns),
+ 'choices': typologies.keys()
+ })
+ for demande in demandes:
+ new_demandes.setdefault(
+ typologies[demande['typologie'].lower()],
+ []
+ ).append(demande)
+ data['demandes'] = new_demandes
+ return data
+
+ def check_status(self):
+ try:
+ link = Link.objects.latest('created')
+ except Link.DoesNotExist:
+ return
+ # no email passed, it's a background check
+ link.get_file()
+
+ @endpoint(name='link',
+ methods=['post'],
+ description=_('Create link with an extranet account'),
+ perm='can_access',
+ parameters={
+ 'NameID': {
+ 'description': _('Publik NameID'),
+ 'example_value': 'xyz24d934',
+ },
+ 'numero_dossier': {
+ 'description': _('MDPH13 beneficiary file number'),
+ 'example_value': '1234',
+ },
+ 'secret': {
+ 'description': _('MDPH13 beneficiary secret'),
+ 'example_value': 'secret',
+ },
+ 'date_de_naissance': {
+ 'description': _('MDPH13 beneficiary date of birth'),
+ 'example_value': '1992-03-05',
+ },
+ 'email': {
+ 'description': _('Publik known email'),
+ 'example_value': 'john.doe@example.com',
+ },
+ })
+ def link(self, request, NameID, numero_dossier, secret, date_de_naissance, email):
+ file_number = numero_dossier.strip()
+ try:
+ int(file_number)
+ except ValueError:
+ raise APIError('numero_dossier must be a number', http_status=400)
+ try:
+ dob = datetime.datetime.strptime(date_de_naissance.strip(), '%Y-%m-%d').date()
+ except ValueError:
+ raise APIError('date_de_naissance must be a date YYYY-MM-DD', http_status=400)
+ email = email.strip()
+ if not self.EMAIL_RE.match(email):
+ raise APIError('email is not valid', http_status=400)
+ with transaction.atomic():
+ link, created = Link.objects.get_or_create(
+ resource=self,
+ name_id=NameID,
+ file_number=file_number,
+ defaults={
+ 'secret': secret,
+ 'dob': dob
+ })
+ updated = False
+ if not created:
+ if link.secret != secret or link.dob != dob:
+ link.secret = secret
+ link.dob = dob
+ updated = True
+ # email is necessary for audit purpose
+ link.get_file(email=email)
+ if updated:
+ link.save()
+ return {'link_id': link.pk, 'created': created, 'updated': updated}
+
+ @endpoint(name='unlink',
+ methods=['post', 'delete'],
+ description=_('Delete link with an extranet account'),
+ perm='can_access',
+ parameters={
+ 'NameID': {
+ 'description': _('Publik NameID'),
+ 'example_value': 'xyz24d934',
+ },
+ 'link_id': {
+ 'description': _('Identifier of the link'),
+ 'example_value': '1',
+ },
+ })
+ def unlink(self, request, NameID, link_id):
+ qs = Link.objects.filter(resource=self, name_id=NameID)
+ if link_id == 'all':
+ pass # unlink of all links
+ else:
+ try:
+ link_id = int(link_id.strip())
+ except ValueError:
+ raise APIError('link_id-must-be-a-number')
+ qs = qs.filter(pk=link_id)
+ count = qs.count()
+ qs.delete()
+ return {'deleted': count}
+
+ @endpoint(name='dossiers',
+ description=_('Get datas for all links, or for a specified one'),
+ perm='can_access',
+ parameters={
+ 'NameID': {
+ 'description': _('Publik NameID'),
+ 'example_value': 'xyz24d934',
+ },
+ 'email': {
+ 'description': _('Publik known email'),
+ 'example_value': 'john.doe@example.com',
+ },
+ 'link_id': {
+ 'description': _('Link identifier'),
+ 'example_value': '1',
+ }
+ })
+ def dossiers(self, request, NameID, email, link_id=None):
+ email = email.strip()
+ if not self.EMAIL_RE.match(email):
+ raise APIError('email is not valid', http_status=400)
+
+ qs = Link.objects.filter(
+ resource=self,
+ name_id=NameID)
+ if link_id:
+ try:
+ link_id = int(link_id)
+ except ValueError:
+ raise APIError('invalid-link-id', http_status=400)
+ qs = qs.filter(id=link_id)
+ data = []
+ for link in qs:
+ file_data = {
+ 'id': str(link.id),
+ 'err': 0,
+ }
+ try:
+ mdph_file = link.get_file(email=email)
+ except Exception as e:
+ if link_id:
+ raise
+ file_data.update(to_json().err_to_response(e))
+ else:
+ file_data.update({
+ 'numero_dossier': link.file_number,
+ 'date_de_naissance': link.dob.isoformat(),
+ 'dossier': mdph_file,
+ })
+ data.append(file_data)
+ if link_id:
+ return {'data': data[0] if data else None}
+ return {'data': data}
+
+
+class Link(models.Model):
+ resource = models.ForeignKey(
+ MDPH13Resource,
+ on_delete=models.CASCADE)
+ name_id = models.CharField(
+ verbose_name=_('NameID'),
+ max_length=256)
+ file_number = models.CharField(
+ max_length=64,
+ verbose_name=_('MDPH beneficiary file number'))
+ secret = models.CharField(
+ verbose_name=_('MDPH beneficiary secret'),
+ max_length=64)
+ dob = models.DateField(
+ verbose_name=_('MDPH beneficiary date of birth'))
+ created = models.DateTimeField(
+ verbose_name=_('Creation date'),
+ auto_now_add=True)
+
+ def get_file(self, email=None):
+ # email is necessary for audit purpose
+ return self.resource.call_situation_dossier(
+ file_number=self.file_number,
+ secret=self.secret,
+ dob=self.dob,
+ email=email)
+
+ class Meta:
+ unique_together = (
+ 'resource', 'name_id', 'file_number',
+ )
+ ordering = ['file_number']
diff --git a/tests/settings.py b/tests/settings.py
index f02f28a9..375daabd 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -27,6 +27,7 @@ INSTALLED_APPS += (
'passerelle.contrib.iws',
'passerelle.contrib.maarch',
'passerelle.contrib.mdel',
+ 'passerelle.contrib.mdph13',
'passerelle.contrib.meyzieu_newsletters',
'passerelle.contrib.nancypoll',
'passerelle.contrib.planitech',
diff --git a/tests/test_mdph13.py b/tests/test_mdph13.py
new file mode 100644
index 00000000..0fc98b39
--- /dev/null
+++ b/tests/test_mdph13.py
@@ -0,0 +1,462 @@
+# -*- coding: utf-8 -*-
+# 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 .
+
+import json
+import base64
+import datetime
+
+import httmock
+import pytest
+
+import utils
+
+from passerelle.utils.jsonresponse import APIError
+from passerelle.contrib.mdph13.models import MDPH13Resource, Link
+
+NAME_ID = 'xyz'
+FILE_NUMBER = '1234'
+DOB = datetime.date(1993, 5, 4)
+DOB_ISOFORMAT = '1993-05-04'
+EMAIL = 'john.doe@example.com'
+SECRET = 'secret'
+
+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": "2015-01-16"
+ },
+ {
+ "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 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(object):
+ 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)
+ return self.responses[idx]
+
+ 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'
+
+
+def test_call_situation_dossier_with_email(mdph13, mock_http):
+ mock_http.add_response(VALID_RESPONSE)
+ mdph13.call_situation_dossier(1234, SECRET, DOB, email=EMAIL)
+ 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
+
+
+def test_check_status_no_link(mdph13):
+ assert Link.objects.count() == 0
+ try:
+ mdph13.check_status()
+ except Exception as e:
+ pytest.fail('check_status() should not raise')
+
+
+def test_check_status_with_link_nok(mdph13, mock_http):
+ mock_http.add_response({'status_code': 500})
+ Link.objects.create(
+ resource=mdph13,
+ name_id=NAME_ID,
+ file_number=FILE_NUMBER,
+ secret=SECRET,
+ dob=DOB)
+ assert Link.objects.count() == 1
+ with pytest.raises(Exception):
+ mdph13.check_status()
+
+
+def test_check_status_with_link_ok(mdph13, mock_http):
+ mock_http.add_response(VALID_RESPONSE)
+ Link.objects.create(
+ resource=mdph13,
+ name_id=NAME_ID,
+ file_number=FILE_NUMBER,
+ secret=SECRET,
+ dob=DOB)
+ assert Link.objects.count() == 1
+ try:
+ mdph13.check_status()
+ except Exception:
+ pytest.fail('check_status() should not raise')
+
+
+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(mdph13):
+ with pytest.raises(APIError) as e:
+ mdph13.link(request=None, NameID=NAME_ID, numero_dossier=FILE_NUMBER, secret=None,
+ date_de_naissance='34-45-6', email=None)
+ assert str(e.value) == 'date_de_naissance must be a date YYYY-MM-DD'
+
+
+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(mdph13, mock_http):
+ mock_http.add_response(DOSSIER_INCONNU)
+ with pytest.raises(APIError) as e:
+ mdph13.link(request=None, NameID=NAME_ID, numero_dossier=FILE_NUMBER, secret=SECRET,
+ date_de_naissance=DOB_ISOFORMAT, email=EMAIL)
+ assert str(e.value) == 'dossier-inconnu'
+
+
+def test_link_nok_secret_invalide(mdph13, mock_http):
+ mock_http.add_response(SECRET_INVALIDE)
+ with pytest.raises(APIError) as e:
+ mdph13.link(request=None, NameID=NAME_ID, numero_dossier=FILE_NUMBER, secret=SECRET,
+ date_de_naissance=DOB_ISOFORMAT, email=EMAIL)
+ assert str(e.value) == 'secret-invalide'
+
+
+def test_link_numero_dont_match(mdph13, mock_http):
+ mock_http.add_response(json.dumps({'err': 0, 'data': {'numero': '456'}}))
+ with pytest.raises(APIError) as e:
+ mdph13.link(request=None, NameID=NAME_ID, numero_dossier=FILE_NUMBER, secret=SECRET,
+ date_de_naissance=DOB_ISOFORMAT, email=EMAIL)
+ assert str(e.value) == 'numero-must-match-numero-dossier'
+
+
+def test_link_ok(mdph13, mock_http):
+ # check first time link
+ mock_http.add_response(VALID_RESPONSE)
+ assert not Link.objects.count()
+ response = mdph13.link(request=None, NameID=NAME_ID,
+ numero_dossier=FILE_NUMBER, secret=SECRET,
+ date_de_naissance=DOB_ISOFORMAT, email=EMAIL)
+ link = Link.objects.get()
+ assert response == {
+ 'link_id': link.pk,
+ 'created': True,
+ 'updated': False
+ }
+ # check relinking with update
+ mock_http.add_response(VALID_RESPONSE)
+ response = mdph13.link(request=None, NameID=NAME_ID,
+ numero_dossier=FILE_NUMBER, secret=SECRET+'a',
+ date_de_naissance=DOB_ISOFORMAT, email=EMAIL)
+ assert response == {
+ '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_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)
+ 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 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
+
+
+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))
+ 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 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)
+ mock_http.add_response({'status_code': 500, 'content': ''})
+ response = mdph13.dossiers(None, NAME_ID, EMAIL)
+ assert response['data']
+ assert response['data'][0]['id'] == str(link1.pk)
+ assert response['data'][0]['err'] == 0
+ assert response['data'][1]['id'] == str(link2.pk)
+ assert response['data'][1]['err'] == 1
diff --git a/tox.ini b/tox.ini
index d795ed12..b7fb761f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -29,6 +29,7 @@ deps =
mohawk
pytest-freezegun
pytest-httpbin
+ pytest-localserver
commands =
django18: py.test {posargs: {env:FAST:} --junitxml=test_{envname}_results.xml --cov-report xml --cov-report html --cov=passerelle/ --cov-config .coveragerc tests/}
django18: ./pylint.sh passerelle/