caluire-axel: link endpoint (#53704)

This commit is contained in:
Lauréline Guérin 2021-05-04 16:51:35 +02:00
parent 183e7700eb
commit 81ad4a3f79
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
15 changed files with 1146 additions and 0 deletions

View File

@ -0,0 +1,24 @@
import pytest
def pytest_addoption(parser):
parser.addoption("--url", help="Url of a passerelle Caluire Axel connector instance")
parser.addoption("--nameid", help="Publik Name ID")
parser.addoption("--firstname", help="first name of a user")
parser.addoption("--lastname", help="Last name of a user")
parser.addoption("--family", help="Family ID")
@pytest.fixture(scope='session')
def conn(request):
return request.config.getoption("--url")
@pytest.fixture(scope='session')
def user(request):
return {
'name_id': request.config.getoption("--nameid"),
'first_name': request.config.getoption("--firstname"),
'last_name': request.config.getoption("--lastname"),
'family': request.config.getoption("--family"),
}

View File

@ -0,0 +1,21 @@
import pprint
import requests
def test_link(conn, user):
name_id = user['name_id']
url = conn + '/link?NameID=%s' % name_id
payload = {
'IDENTFAMILLE': user['family'],
'NOM': user['last_name'],
'PRENOM': user['first_name'],
}
print("Creating link with the following payload:")
pprint.pprint(payload)
resp = requests.post(url, json=payload)
resp.raise_for_status()
res = resp.json()
pprint.pprint(res)
assert res['err'] == 0
print('\n')

View File

@ -0,0 +1,65 @@
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('base', '0029_auto_20210202_1627'),
]
operations = [
migrations.CreateModel(
name='CaluireAxel',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('title', models.CharField(max_length=50, verbose_name='Title')),
('slug', models.SlugField(unique=True, verbose_name='Identifier')),
('description', models.TextField(verbose_name='Description')),
(
'wsdl_url',
models.CharField(
help_text='Caluire Axel WSDL URL', max_length=128, verbose_name='WSDL URL'
),
),
(
'users',
models.ManyToManyField(
blank=True,
related_name='_caluireaxel_users_+',
related_query_name='+',
to='base.ApiUser',
),
),
],
options={
'verbose_name': 'Caluire Axel',
},
),
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)),
('family_id', models.CharField(max_length=128)),
('person_id', models.CharField(max_length=128)),
(
'resource',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to='caluire_axel.CaluireAxel'
),
),
],
options={
'unique_together': {('resource', 'name_id')},
},
),
]

View File

@ -0,0 +1,119 @@
# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2021 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/>.
from django.db import models
from django.utils.translation import ugettext_lazy as _
from passerelle.base.models import BaseResource
from passerelle.contrib.utils import axel
from passerelle.utils.api import endpoint
from passerelle.utils.jsonresponse import APIError
from . import schemas
class CaluireAxel(BaseResource):
wsdl_url = models.CharField(
max_length=128, blank=False, verbose_name=_('WSDL URL'), help_text=_('Caluire Axel WSDL URL')
)
category = _('Business Process Connectors')
_category_ordering = [_('Family')]
class Meta:
verbose_name = _('Caluire Axel')
def check_status(self):
response = self.requests.get(self.wsdl_url)
response.raise_for_status()
def check_individu(self, post_data):
family_id = post_data.pop('IDENTFAMILLE')
for key in ['NAISSANCE', 'CODEPOSTAL', 'VILLE', 'TEL', 'MAIL']:
post_data[key] = None
try:
result = schemas.find_individus(self, {'PORTAIL': {'FINDINDIVIDU': post_data}})
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
data={'xml_request': e.xml_request, 'xml_response': e.xml_response},
)
data = result.json_response['DATA']['PORTAIL']['FINDINDIVIDUS']
for individu in data.get('INDIVIDU') or []:
for famille in individu['FAMILLE']:
if famille['IDENTFAMILLE'] == family_id:
place = famille['PLACE']
if place not in ['1', '2']:
# not RL1 or RL2
raise APIError('Wrong place in family', err_code='family-place-error-%s' % place)
return individu, result
raise APIError('Person not found', err_code='not-found')
@endpoint(
display_category=_('Family'),
display_order=1,
description=_('Create link between user and Caluire Axel'),
perm='can_access',
parameters={
'NameID': {'description': _('Publik ID')},
},
post={
'request_body': {
'schema': {
'application/json': schemas.LINK_SCHEMA,
}
}
},
)
def link(self, request, NameID, post_data):
if not NameID:
raise APIError('NameID is empty', err_code='bad-request', http_status=400)
family_id = post_data['IDENTFAMILLE']
try:
data, result = self.check_individu(post_data)
except APIError as e:
if not hasattr(e, 'err_code') or e.err_code == 'error':
raise
raise APIError('Person not found', err_code='not-found')
link, created = self.link_set.get_or_create(
name_id=NameID, defaults={'family_id': family_id, 'person_id': data['IDENT']}
)
if not created and (link.family_id != family_id or link.person_id != data['IDENT']):
raise APIError('Data conflict', err_code='conflict')
return {
'link': link.pk,
'created': created,
'family_id': link.family_id,
'data': {
'xml_request': result.xml_request,
'xml_response': result.xml_response,
},
}
class Link(models.Model):
resource = models.ForeignKey(CaluireAxel, on_delete=models.CASCADE)
name_id = models.CharField(blank=False, max_length=256)
family_id = models.CharField(blank=False, max_length=128)
person_id = models.CharField(blank=False, max_length=128)
class Meta:
unique_together = ('resource', 'name_id')

View File

@ -0,0 +1,39 @@
# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2021 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 copy
import os
from passerelle.contrib.utils import axel
BASE_XSD_PATH = os.path.join(os.path.dirname(__file__), 'xsd')
class Operation(axel.Operation):
base_xsd_path = BASE_XSD_PATH
find_individus = Operation('FindIndividus')
LINK_SCHEMA = copy.deepcopy(
find_individus.request_schema['properties']['PORTAIL']['properties']['FINDINDIVIDU']
)
for key in ['NAISSANCE', 'CODEPOSTAL', 'VILLE', 'TEL', 'MAIL']:
LINK_SCHEMA['properties'].pop(key)
LINK_SCHEMA['required'].remove(key)
LINK_SCHEMA['properties']['IDENTFAMILLE'] = {'type': 'string', 'maxLength': 8}
LINK_SCHEMA['required'].append('IDENTFAMILLE')

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8" ?>
<xsd:schema xmlns:all="urn:AllAxelTypes" targetNamespace="urn:Adresse" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:adr="urn:Adresse">
<xsd:import schemaLocation="./AllAxelTypes.xsd" namespace="urn:AllAxelTypes"/>
<xsd:simpleType name="NPAIType">
<xsd:union memberTypes="all:ONType all:empty-string" />
</xsd:simpleType>
<xsd:complexType name="ADRESSEType">
<xsd:sequence>
<xsd:element name="ADRESSE3" type="all:COMPLEMENTType"/>
<xsd:element name="ADRESSE4" type="all:COMPLEMENTType"/>
<xsd:element name="NORUE" type="all:NUMVOIEType"/>
<xsd:element name="ADRESSE1" type="all:COMPLEMENTType"/>
<xsd:element name="ADRESSE2" type="all:COMPLEMENTType"/>
<xsd:element name="CODEPOSTAL" type="all:CODEPOSTALType"/>
<xsd:element name="VILLE" type="all:VILLEType"/>
<xsd:element name="PAYS" type="all:PAYSType"/>
<xsd:element name="NPAI" type="adr:NPAIType"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>

View File

@ -0,0 +1,301 @@
<?xml version="1.0" encoding="utf-8" ?>
<xsd:schema targetNamespace="urn:AllAxelTypes" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:all="urn:AllAxelTypes">
<xsd:simpleType name="empty-string">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="decimal-or-empty">
<xsd:union memberTypes="xsd:decimal all:empty-string" />
</xsd:simpleType>
<xsd:simpleType name="unsignedInt-or-empty">
<xsd:union memberTypes="xsd:unsignedInt all:empty-string" />
</xsd:simpleType>
<xsd:simpleType name="TELREQUIREDType">
<xsd:restriction base="xsd:string">
<xsd:minLength value="1" />
<xsd:maxLength value="10" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="TEL2REQUIREDType">
<xsd:restriction base="xsd:string">
<xsd:minLength value="1" />
<xsd:maxLength value="16" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="COURRIELREQUIREDType">
<xsd:restriction base="xsd:string" >
<xsd:pattern value="[^@]+@[^\.]+\..+" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="NOMREQUIREDType">
<xsd:restriction base="xsd:string">
<xsd:minLength value="1" />
<xsd:maxLength value="50" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="PRENOMREQUIREDType">
<xsd:restriction base="xsd:string">
<xsd:minLength value="1" />
<xsd:maxLength value="30" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="NOMType">
<xsd:restriction base="xsd:string">
<xsd:minLength value="0" />
<xsd:maxLength value="50" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="PRENOMType">
<xsd:restriction base="xsd:string">
<xsd:minLength value="0" />
<xsd:maxLength value="30" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="DATEREQUIREDType">
<xsd:restriction base="xsd:string">
<xsd:pattern value="((0[1-9])|([12][0-9])|(3[01]))/((0[1-9])|(1[012]))/((000[1-9])|(00[1-9][0-9])|(0[1-9][0-9]{2})|([1-9][0-9]{3}))" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="DATEType">
<xsd:union memberTypes="all:DATEREQUIREDType all:empty-string" />
</xsd:simpleType>
<xsd:simpleType name="DATETIMEType">
<xsd:restriction base="xsd:string">
<xsd:pattern value="((0[1-9])|([12][0-9])|(3[01]))/((0[1-9])|(1[012]))/((000[1-9])|(00[1-9][0-9])|(0[1-9][0-9]{2})|([1-9][0-9]{3})) (([01][0-9])|(2[0-3]))(:[0-5][0-9]){2}(\.[0-9]+)?" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="TIMEType">
<xsd:restriction base="xsd:string">
<xsd:pattern value="(([01][0-9])|(2[0-3]))(:[0-5][0-9])" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="IDENTType">
<xsd:restriction base="xsd:string">
<xsd:maxLength value="8" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="IDENTREQUIREDType">
<xsd:restriction base="xsd:string">
<xsd:minLength value="1"/>
<xsd:maxLength value="8"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="IDType">
<xsd:restriction base="xsd:string">
<xsd:maxLength value="10" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="IDREQUIREDType">
<xsd:restriction base="xsd:string">
<xsd:minLength value="1"/>
<xsd:maxLength value="10"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="TELType">
<xsd:union memberTypes="all:TELREQUIREDType all:empty-string" />
</xsd:simpleType>
<xsd:simpleType name="TEL2Type">
<xsd:union memberTypes="all:TEL2REQUIREDType all:empty-string" />
</xsd:simpleType>
<xsd:simpleType name="COURRIELType" >
<xsd:union memberTypes="all:COURRIELREQUIREDType all:empty-string" />
</xsd:simpleType>
<xsd:simpleType name="ANNEEType">
<xsd:restriction base="xsd:string">
<xsd:pattern value="[0-9]{4}"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="MOISType">
<xsd:restriction base="xsd:string">
<xsd:pattern value="[0-1][0-9]"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="OUINONREQUIREDType">
<xsd:restriction base="xsd:string">
<xsd:pattern value="[Oo][Uu][Ii]" />
<xsd:pattern value="[Nn][Oo][Nn]" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="BOOLEANREQUIREDType">
<xsd:restriction base="xsd:string">
<xsd:pattern value="0" />
<xsd:pattern value="1" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="OUINONType">
<xsd:restriction base="xsd:string">
<xsd:pattern value="|" />
<xsd:pattern value="[Oo][Uu][Ii]" />
<xsd:pattern value="[Nn][Oo][Nn]" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="NUMVOIEREQUIREDType">
<xsd:restriction base="xsd:string">
<xsd:minLength value="1" />
<xsd:maxLength value="5" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="NUMVOIEType">
<xsd:union memberTypes="all:NUMVOIEREQUIREDType all:empty-string" />
</xsd:simpleType>
<xsd:simpleType name="COMPLEMENTNUMType">
<xsd:restriction base="xsd:string">
<xsd:pattern value="|" />
<xsd:pattern value="[Bb][Ii][Ss]" />
<xsd:pattern value="[Tt][Ee][Rr]" />
<xsd:pattern value="[Qq][Uu][Aa][Tt][Ee][Rr]" />
<xsd:pattern value="[Qq][Uu][Ii][Nn][Qq][Uu][Ii][Ee][Ss]" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="TYPEVOIEType">
<xsd:restriction base="xsd:string">
<xsd:pattern value="[Aa][Vv][Ee][Nn][Uu][Ee]" />
<xsd:pattern value="[Rr][Uu][Ee]" />
<xsd:pattern value="[Ff][Aa][Uu][Bb][Oo][Uu][Rr][Gg]" />
<xsd:pattern value="[Cc][Hh][Ee][Mm][Ii][Nn]" />
<xsd:pattern value="[Cc][Oo][Uu][Rr][Ss]" />
<xsd:pattern value="[Bb][Oo][Uu][Ll][Ee][Vv][Aa][Rr][Dd]" />
<xsd:pattern value="[Ii][Mm][Pp][Aa][Ss][Ss][Ee]" />
<xsd:pattern value="[Ll][Ii][Ee][Uu] [Dd][Ii][Tt]" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="COMPLEMENTREQUIREDType">
<xsd:restriction base="xsd:string">
<xsd:minLength value="1" />
<xsd:maxLength value="40" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="COMPLEMENTType">
<xsd:union memberTypes="all:COMPLEMENTREQUIREDType all:empty-string" />
</xsd:simpleType>
<xsd:simpleType name="CODEPOSTALREQUIREDType">
<xsd:restriction base="xsd:string">
<xsd:pattern value="[0-9]{5}"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="VILLEREQUIREDType">
<xsd:restriction base="xsd:string">
<xsd:minLength value="1" />
<xsd:maxLength value="35" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="PAYSREQUIREDType">
<xsd:restriction base="xsd:string">
<xsd:minLength value="1" />
<xsd:maxLength value="30" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="CODEPOSTALType">
<xsd:union memberTypes="all:CODEPOSTALREQUIREDType all:empty-string" />
</xsd:simpleType>
<xsd:simpleType name="VILLEType">
<xsd:union memberTypes="all:VILLEREQUIREDType all:empty-string" />
</xsd:simpleType>
<xsd:simpleType name="PAYSType">
<xsd:union memberTypes="all:PAYSREQUIREDType all:empty-string" />
</xsd:simpleType>
<xsd:simpleType name="CODEINSEEVILLEREQUIREDType">
<xsd:restriction base="xsd:string">
<xsd:pattern value="[0-9]{5}"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="CODEINSEEVILLEType">
<xsd:union memberTypes="all:CODEINSEEVILLEREQUIREDType all:empty-string" />
</xsd:simpleType>
<xsd:simpleType name="ONEmptyType">
<xsd:union memberTypes="all:ONType all:empty-string" />
</xsd:simpleType>
<xsd:simpleType name="ONType">
<xsd:restriction base="xsd:string">
<xsd:pattern value="[Oo]" />
<xsd:pattern value="[Nn]" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="MONTANTREQUIREDType">
<xsd:restriction base="xsd:decimal">
<xsd:totalDigits value="10"/>
<xsd:fractionDigits value="2"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="MONTANTType">
<xsd:union memberTypes="all:MONTANTREQUIREDType all:empty-string" />
</xsd:simpleType>
<xsd:simpleType name="PRIXREQUIREDType">
<xsd:restriction base="xsd:decimal">
<xsd:totalDigits value="10"/>
<xsd:fractionDigits value="5"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="PRIXType">
<xsd:union memberTypes="all:PRIXREQUIREDType all:empty-string" />
</xsd:simpleType>
<xsd:simpleType name="LIBELLEType">
<xsd:restriction base="xsd:string">
<xsd:minLength value="0" />
<xsd:maxLength value="40" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="LIBELLE100Type">
<xsd:restriction base="xsd:string">
<xsd:minLength value="0" />
<xsd:maxLength value="100" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="FOOBARType"><!-- CUSTOM -->
<xsd:restriction base="xsd:string">
<xsd:minLength value="0" />
<xsd:maxLength value="256" />
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8" ?>
<xsd:schema xmlns:adr="urn:Adresse" xmlns:all="urn:AllAxelTypes" targetNamespace="urn:Individu" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:ind="urn:Individu" >
<xsd:import schemaLocation="./AllAxelTypes.xsd" namespace="urn:AllAxelTypes" />
<xsd:import schemaLocation="./Adresse.xsd" namespace="urn:Adresse" />
<xsd:simpleType name="SEXEType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="" />
<xsd:enumeration value="M" />
<xsd:enumeration value="F" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="CIVILITEType">
<xsd:restriction base="xsd:string">
<xsd:minLength value="0" />
<xsd:maxLength value="4" />
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="INDIVIDUType">
<xsd:sequence>
<xsd:element name="IDENT" type="all:IDENTType"/>
<xsd:element name="CIVILITE" type="ind:CIVILITEType"/>
<xsd:element name="NOM" type="all:NOMREQUIREDType"/>
<xsd:element name="PRENOM" type="all:PRENOMType"/>
<xsd:element name="NAISSANCE" type="all:DATEType"/>
<xsd:element name="SEXE" type="ind:SEXEType"/>
<xsd:element name="NOMJF" type="all:NOMType"/>
<xsd:element name="TELFIXE" type="all:TEL2Type"/>
<xsd:element name="TELPORTABLE" type="all:TEL2Type"/>
<xsd:element name="MAIL" type="all:COURRIELType"/>
<xsd:element name="CSP" type="all:FOOBARType"/><!-- CUSTOM -->
<xsd:element name="EMPLOYEUR" type="all:FOOBARType"/><!-- CUSTOM -->
<xsd:element name="VILLEEMP" type="all:FOOBARType"/><!-- CUSTOM -->
<xsd:element name="PAI" type="all:FOOBARType"/><!-- CUSTOM -->
<xsd:element name="GARDEALTERNEE" type="all:FOOBARType"/><!-- CUSTOM -->
<xsd:element name="ADRESSE" type="adr:ADRESSEType" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8" ?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:all="urn:AllAxelTypes">
<xsd:import schemaLocation="./AllAxelTypes.xsd" namespace="urn:AllAxelTypes" />
<xsd:complexType name="FINDINDIVIDUType">
<xsd:sequence>
<xsd:element ref="NOM" />
<xsd:element ref="PRENOM" />
<xsd:element ref="NAISSANCE" />
<xsd:element ref="CODEPOSTAL" />
<xsd:element ref="VILLE" />
<xsd:element ref="TEL" />
<xsd:element ref="MAIL" />
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="PORTAILType">
<xsd:sequence>
<xsd:element ref="FINDINDIVIDU" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="NOM" type="all:NOMREQUIREDType"/>
<xsd:element name="PRENOM" type="all:PRENOMREQUIREDType"/> <!-- CUSTOM, it should be PRENOMType -->
<xsd:element name="NAISSANCE" type="all:DATEType"/>
<xsd:element name="CODEPOSTAL" type="all:CODEPOSTALType"/>
<xsd:element name="VILLE" type="all:VILLEType"/>
<xsd:element name="TEL" type="all:TEL2Type"/>
<xsd:element name="MAIL" type="all:COURRIELType"/>
<xsd:element name="FINDINDIVIDU" type="FINDINDIVIDUType"/>
<xsd:element name="PORTAIL" type="PORTAILType"/>
</xsd:schema>

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8" ?>
<xsd:schema xmlns:all="urn:AllAxelTypes" xmlns:ind="urn:Individu" xmlns:xsd="http://www.w3.org/2001/XMLSchema" >
<xsd:import schemaLocation="./Individu.xsd" namespace="urn:Individu" />
<xsd:import schemaLocation="./AllAxelTypes.xsd" namespace="urn:AllAxelTypes" />
<xsd:redefine schemaLocation="./R_ShemaResultat.xsd">
<xsd:simpleType name="TYPEType">
<xsd:restriction base="TYPEType">
<xsd:enumeration value="FindIndividus" />
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="PORTAILType">
<xsd:complexContent>
<xsd:extension base="PORTAILType">
<xsd:sequence>
<xsd:element ref="FINDINDIVIDUS" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:redefine>
<xsd:complexType name="INDIType">
<xsd:complexContent>
<xsd:extension base="ind:INDIVIDUType">
<xsd:sequence>
<xsd:element ref="FAMILLE" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:complexType name="FAMILLEType">
<xsd:sequence>
<xsd:element ref="IDENTFAMILLE" />
<xsd:element ref="PLACE"/>
<xsd:element ref="SITUATION" minOccurs="0" maxOccurs="1"/><!-- CUSTOM -->
</xsd:sequence>
</xsd:complexType>
<xsd:simpleType name="PLACEType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="" />
<xsd:enumeration value="1" />
<xsd:enumeration value="2" />
<xsd:enumeration value="3" />
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="FINDINDIVIDUSType">
<xsd:sequence>
<xsd:element ref="CODE" />
<xsd:element ref="INDIVIDU" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>
<xsd:element name="CODE" type="xsd:integer"/>
<xsd:element name="INDIVIDU" type="INDIType"/>
<xsd:element name="FAMILLE" type="FAMILLEType"/>
<xsd:element name="IDENTFAMILLE" type="all:IDENTType"/>
<xsd:element name="PLACE" type="PLACEType"/>
<xsd:element name="SITUATION" type="all:FOOBARType"/><!-- CUSTOM -->
<xsd:element name="FINDINDIVIDUS" type="FINDINDIVIDUSType"/>
</xsd:schema>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8" ?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:all="urn:AllAxelTypes">
<xsd:import schemaLocation="./AllAxelTypes.xsd" namespace="urn:AllAxelTypes" />
<xsd:simpleType name="TYPEType">
<xsd:restriction base="xsd:string">
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="STATUSType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="OK" />
<xsd:enumeration value="NOK" />
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="PORTAILType">
</xsd:complexType>
<xsd:complexType name="RESULTATType">
<xsd:sequence>
<xsd:element ref="TYPE" />
<xsd:element ref="STATUS" />
<xsd:element ref="DATE" />
<xsd:element ref="COMMENTAIRES" />
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="DATAType">
<xsd:sequence>
<xsd:element ref="PORTAIL"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="PORTAILSERVICEType">
<xsd:sequence>
<xsd:element ref="RESULTAT" />
<xsd:element ref="DATA" />
</xsd:sequence>
</xsd:complexType>
<xsd:element name="TYPE" type="TYPEType"/>
<xsd:element name="STATUS" type="STATUSType"/>
<xsd:element name="DATE" type="all:DATETIMEType"/>
<xsd:element name="COMMENTAIRES" type="xsd:string"/>
<xsd:element name="PORTAIL" type="PORTAILType"/>
<xsd:element name="RESULTAT" type="RESULTATType"/>
<xsd:element name="DATA" type="DATAType"/>
<xsd:element name="PORTAILSERVICE" type="PORTAILSERVICEType"/>
</xsd:schema>

View File

@ -16,6 +16,7 @@ KNOWN_SERVICES = {
# include all contrib apps
INSTALLED_APPS += (
'passerelle.contrib.adict',
'passerelle.contrib.caluire_axel',
'passerelle.contrib.dpark',
'passerelle.contrib.fake_family',
'passerelle.contrib.gdema',

351
tests/test_caluire_axel.py Normal file
View File

@ -0,0 +1,351 @@
# -*- coding: utf-8 -*-
# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2021 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/>.
from contextlib import contextmanager
import mock
import pytest
import utils
import xmlschema
from passerelle.contrib.caluire_axel import schemas
from passerelle.contrib.caluire_axel.models import CaluireAxel, Link
from passerelle.contrib.utils.axel import AxelError
from passerelle.utils.soap import SOAPError
@pytest.fixture
def resource(db):
return utils.make_resource(
CaluireAxel, slug='test', wsdl_url='http://example.net/AXEL_WS/AxelWS.php?wsdl'
)
@pytest.fixture
def link_params():
return {
'IDENTFAMILLE': '12345',
'NOM': 'Doe',
'PRENOM': 'John',
}
@contextmanager
def mock_getdata(content, operation):
with mock.patch('passerelle.contrib.caluire_axel.models.CaluireAxel.soap_client') as client:
resp = '''
<?xml version="1.0"?>
<PORTAILSERVICE>
<RESULTAT>
<TYPE>%s</TYPE>
<STATUS>OK</STATUS>
<DATE>10/10/2010 10:10:01</DATE>
<COMMENTAIRES><![CDATA[]]></COMMENTAIRES>
</RESULTAT>
<DATA>
%s
</DATA>
</PORTAILSERVICE>
'''.strip() % (
operation,
content,
)
client.return_value.service.getData.return_value = resp
yield
def test_operation_status_error(resource):
resp = '''
<?xml version="1.0"?>
<PORTAILSERVICE>
<RESULTAT>
<TYPE>FindIndividus</TYPE>
<STATUS>NOK</STATUS>
<COMMENTAIRES><![CDATA[Foo reason]]></COMMENTAIRES>
</RESULTAT>
<DATA>
<PORTAIL/>
</DATA>
</PORTAILSERVICE>
'''.strip()
with mock.patch('passerelle.contrib.caluire_axel.models.CaluireAxel.soap_client') as client:
client.return_value.service.getData.return_value = resp
with pytest.raises(AxelError, match='Foo reason'):
schemas.find_individus(
resource,
{
'PORTAIL': {
'FINDINDIVIDU': {
'NOM': 'Doe',
'PRENOM': 'John',
'NAISSANCE': None,
'CODEPOSTAL': None,
'VILLE': None,
'TEL': None,
'MAIL': None,
}
}
},
)
@pytest.mark.parametrize(
'content',
[
'<PORTAIL><FINDINDIVIDUS/></PORTAIL>',
],
)
def test_operation_find_individus(resource, content):
with mock_getdata(content, 'FindIndividus'):
with pytest.raises(AxelError):
schemas.find_individus(
resource,
{
'PORTAIL': {
'FINDINDIVIDU': {
'NOM': 'Doe',
'PRENOM': 'John',
'NAISSANCE': None,
'CODEPOSTAL': None,
'VILLE': None,
'TEL': None,
'MAIL': None,
}
}
},
)
def test_link_endpoint_nameid_empty(app, resource, link_params):
resp = app.post_json('/caluire-axel/test/link?NameID=', params=link_params, status=400)
assert resp.json['err_desc'] == "NameID is empty"
assert resp.json['err'] == 'bad-request'
def test_link_endpoint_axel_error(app, resource, link_params):
with mock.patch('passerelle.contrib.caluire_axel.schemas.find_individus') as operation:
operation.side_effect = AxelError('FooBar')
resp = app.post_json('/caluire-axel/test/link?NameID=yyy', params=link_params)
assert resp.json['err_desc'] == "Axel error: FooBar"
assert resp.json['err'] == 'error'
assert resp.json['data'] == {'xml_request': None, 'xml_response': None}
# test xml_request and xml_response only for this endpoint
xml_request = """<PORTAIL>
<FINDINDIVIDU>
<NOM>Doe</NOM>
<PRENOM>John</PRENOM>
<NAISSANCE />
<CODEPOSTAL />
<VILLE />
<TEL />
<MAIL />
</FINDINDIVIDU>
</PORTAIL>
"""
with mock_getdata('', 'FindIndividus'):
with mock.patch('xmlschema.XMLSchema.validate') as xml_validate:
xml_validate.side_effect = xmlschema.XMLSchemaValidationError(None, None)
resp = app.post_json('/caluire-axel/test/link?NameID=yyy', params=link_params)
assert resp.json['err_desc'].startswith("Axel error: invalid request")
assert resp.json['err'] == 'error'
assert resp.json['data']['xml_request'] == xml_request
assert resp.json['data']['xml_response'] is None
xml_response = """<PORTAILSERVICE>
<RESULTAT>
<TYPE>FINDINDIVIDUS</TYPE>
<STATUS>NOK</STATUS>
<COMMENTAIRES>Foo reason</COMMENTAIRES>
</RESULTAT>
<DATA>
<PORTAIL />
</DATA>
</PORTAILSERVICE>"""
response = """
<?xml version="1.0"?>
<PORTAILSERVICE>
<RESULTAT>
<TYPE>FINDINDIVIDUS</TYPE>
<STATUS>NOK</STATUS>
<COMMENTAIRES><![CDATA[Foo reason]]></COMMENTAIRES>
</RESULTAT>
<DATA>
<PORTAIL/>
</DATA>
</PORTAILSERVICE>
""".strip()
with mock.patch('passerelle.contrib.caluire_axel.models.CaluireAxel.soap_client') as client:
client.return_value.service.getData.return_value = response
resp = app.post_json('/caluire-axel/test/link?NameID=yyy', params=link_params)
assert resp.json['err_desc'] == 'Axel error: Foo reason'
assert resp.json['err'] == 'error'
assert resp.json['data']['xml_request'] == xml_request
assert resp.json['data']['xml_response'] == xml_response
content = """<PORTAIL>
<FINDINDIVIDUS>
<INDIVIDU>
<IDENT>123</IDENT>
<NOM>Doe</NOM>
<PRENOM>John</PRENOM>
<FAMILLE>
<IDENTFAMILLE>12345</IDENTFAMILLE>
<PLACE>2</PLACE>
</FAMILLE>
</INDIVIDU>
</FINDINDIVIDUS>
</PORTAIL>"""
xml_response = (
"""<PORTAILSERVICE>
<RESULTAT>
<TYPE>FindIndividus</TYPE>
<STATUS>OK</STATUS>
<DATE>10/10/2010 10:10:01</DATE>
<COMMENTAIRES />
</RESULTAT>
<DATA>
%s
</DATA>
</PORTAILSERVICE>"""
% content
)
with mock_getdata(content, 'FindIndividus'):
with mock.patch('passerelle.contrib.utils.axel.AxelSchema.decode') as decode:
decode.side_effect = xmlschema.XMLSchemaValidationError(None, None)
resp = app.post_json('/caluire-axel/test/link?NameID=yyy', params=link_params)
assert resp.json['err_desc'].startswith("Axel error: invalid response")
assert resp.json['err'] == 'error'
assert resp.json['data']['xml_request'] == xml_request
assert resp.json['data']['xml_response'] == xml_response
with mock.patch('passerelle.contrib.caluire_axel.models.CaluireAxel.soap_client') as client:
client.side_effect = SOAPError('SOAP service is down')
resp = app.post_json('/caluire-axel/test/link?NameID=yyy', params=link_params)
assert resp.json['err_desc'] == "SOAP service is down"
@pytest.mark.parametrize(
'xml_response',
[
'',
"""<INDIVIDU>
<IDENT>123</IDENT>
<CIVILITE/><NOM>A</NOM><PRENOM/><NAISSANCE/><SEXE/><NOMJF/><TELFIXE/><TELPORTABLE/><MAIL/><CSP/><EMPLOYEUR/><VILLEEMP/><PAI/><GARDEALTERNEE/>
<FAMILLE>
<IDENTFAMILLE>12346</IDENTFAMILLE>
<PLACE>2</PLACE>
<SITUATION/>
</FAMILLE>
<FAMILLE>
<IDENTFAMILLE>12347</IDENTFAMILLE>
<PLACE>1</PLACE>
</FAMILLE>
</INDIVIDU>""",
"""<INDIVIDU>
<IDENT>123</IDENT>
<CIVILITE/><NOM>A</NOM><PRENOM/><NAISSANCE/><SEXE/><NOMJF/><TELFIXE/><TELPORTABLE/><MAIL/><CSP/><EMPLOYEUR/><VILLEEMP/><PAI/><GARDEALTERNEE/>
<FAMILLE>
<IDENTFAMILLE>12345</IDENTFAMILLE>
<PLACE>3</PLACE>
<SITUATION/>
</FAMILLE>
</INDIVIDU>""",
],
)
def test_link_endpoint_no_result(app, resource, link_params, xml_response):
content = (
'''<PORTAIL>
<FINDINDIVIDUS>
<CODE>42</CODE>
%s
</FINDINDIVIDUS>
</PORTAIL>'''
% xml_response
)
with mock_getdata(content, 'FindIndividus'):
resp = app.post_json('/caluire-axel/test/link?NameID=yyy', params=link_params)
assert resp.json['err_desc'] == "Person not found"
assert resp.json['err'] == 'not-found'
def test_link_endpoint_conflict(app, resource, link_params):
content = """<PORTAIL><FINDINDIVIDUS>
<CODE>42</CODE>
<INDIVIDU>
<IDENT>123</IDENT>
<CIVILITE/><NOM>A</NOM><PRENOM/><NAISSANCE/><SEXE/><NOMJF/><TELFIXE/><TELPORTABLE/><MAIL/><CSP/><EMPLOYEUR/><VILLEEMP/><PAI/><GARDEALTERNEE/>
<FAMILLE>
<IDENTFAMILLE>12345</IDENTFAMILLE>
<PLACE>2</PLACE>
<SITUATION/>
</FAMILLE>
</INDIVIDU>
</FINDINDIVIDUS></PORTAIL>"""
# existing link but family_id is wrong
link = Link.objects.create(resource=resource, name_id='yyy', family_id='YYY', person_id='42')
with mock_getdata(content, 'FindIndividus'):
resp = app.post_json('/caluire-axel/test/link?NameID=yyy', params=link_params)
assert resp.json['err_desc'] == "Data conflict"
assert resp.json['err'] == 'conflict'
# existing link but person_id is wrong
link.family_id = '12345'
link.person_id = '35'
link.save()
with mock_getdata(content, 'FindIndividus'):
resp = app.post_json('/caluire-axel/test/link?NameID=yyy', params=link_params)
assert resp.json['err_desc'] == "Data conflict"
assert resp.json['err'] == 'conflict'
@pytest.mark.parametrize('place', [1, 2])
def test_link_endpoint(app, resource, link_params, place):
content = (
"""<PORTAIL><FINDINDIVIDUS>
<CODE>42</CODE>
<INDIVIDU>
<IDENT>123</IDENT>
<CIVILITE/><NOM>A</NOM><PRENOM/><NAISSANCE/><SEXE/><NOMJF/><TELFIXE/><TELPORTABLE/><MAIL/><CSP/><EMPLOYEUR/><VILLEEMP/><PAI/><GARDEALTERNEE/>
<FAMILLE>
<IDENTFAMILLE>12345</IDENTFAMILLE>
<PLACE>%s</PLACE>
<SITUATION/>
</FAMILLE>
</INDIVIDU>
</FINDINDIVIDUS></PORTAIL>"""
% place
)
with mock_getdata(content, 'FindIndividus'):
resp = app.post_json('/caluire-axel/test/link?NameID=yyy', params=link_params)
assert set(resp.json.keys()) == set(['err', 'link', 'created', 'family_id', 'data'])
assert resp.json['err'] == 0
assert resp.json['family_id'] == '12345'
assert resp.json['created'] is True
assert 'xml_request' in resp.json['data']
assert 'xml_response' in resp.json['data']
# again
with mock_getdata(content, 'FindIndividus'):
resp = app.post_json('/caluire-axel/test/link?NameID=yyy', params=link_params)
assert set(resp.json.keys()) == set(['err', 'link', 'created', 'family_id', 'data'])
assert resp.json['err'] == 0
assert resp.json['family_id'] == '12345'
assert resp.json['created'] is False # link already exists
assert 'xml_request' in resp.json['data']
assert 'xml_response' in resp.json['data']