add MDPH13 connector (#30692)
This commit is contained in:
parent
6884d84f8c
commit
ad953198e9
|
@ -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')]),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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'<SNIPPED>'
|
||||||
|
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<SNIPPED>' % 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']
|
|
@ -27,6 +27,7 @@ INSTALLED_APPS += (
|
||||||
'passerelle.contrib.iws',
|
'passerelle.contrib.iws',
|
||||||
'passerelle.contrib.maarch',
|
'passerelle.contrib.maarch',
|
||||||
'passerelle.contrib.mdel',
|
'passerelle.contrib.mdel',
|
||||||
|
'passerelle.contrib.mdph13',
|
||||||
'passerelle.contrib.meyzieu_newsletters',
|
'passerelle.contrib.meyzieu_newsletters',
|
||||||
'passerelle.contrib.nancypoll',
|
'passerelle.contrib.nancypoll',
|
||||||
'passerelle.contrib.planitech',
|
'passerelle.contrib.planitech',
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
1
tox.ini
1
tox.ini
|
@ -29,6 +29,7 @@ deps =
|
||||||
mohawk
|
mohawk
|
||||||
pytest-freezegun
|
pytest-freezegun
|
||||||
pytest-httpbin
|
pytest-httpbin
|
||||||
|
pytest-localserver
|
||||||
commands =
|
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: 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/
|
django18: ./pylint.sh passerelle/
|
||||||
|
|
Loading…
Reference in New Issue