Projet

Général

Profil

0001-astregs-add-initial-connector-33424.patch

Serghei Mihai, 25 juin 2019 19:50

Télécharger (20,5 ko)

Voir les différences:

Subject: [PATCH] astregs: add initial connector (#33424)

 passerelle/apps/astregs/__init__.py           |   0
 .../apps/astregs/migrations/0001_initial.py   |  64 +++++
 .../apps/astregs/migrations/__init__.py       |   0
 passerelle/apps/astregs/models.py             | 241 ++++++++++++++++++
 passerelle/settings.py                        |   1 +
 tests/test_astregs.py                         | 129 ++++++++++
 6 files changed, 435 insertions(+)
 create mode 100644 passerelle/apps/astregs/__init__.py
 create mode 100644 passerelle/apps/astregs/migrations/0001_initial.py
 create mode 100644 passerelle/apps/astregs/migrations/__init__.py
 create mode 100644 passerelle/apps/astregs/models.py
 create mode 100644 tests/test_astregs.py
passerelle/apps/astregs/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.20 on 2019-06-19 10:24
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6
import django.db.models.deletion
7

  
8

  
9
class Migration(migrations.Migration):
10

  
11
    initial = True
12

  
13
    dependencies = [
14
        ('base', '0012_job'),
15
    ]
16

  
17
    operations = [
18
        migrations.CreateModel(
19
            name='AstreGS',
20
            fields=[
21
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22
                ('title', models.CharField(max_length=50, verbose_name='Title')),
23
                ('description', models.TextField(verbose_name='Description')),
24
                ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
25
                ('wsdl_base_url', models.URLField(verbose_name='Webservices base URL')),
26
                ('username', models.CharField(max_length=32, verbose_name='Username')),
27
                ('password', models.CharField(max_length=32, verbose_name='Password')),
28
                ('organism', models.CharField(max_length=32, verbose_name='Organism')),
29
                ('budget', models.CharField(max_length=32, verbose_name='Budget')),
30
                ('exercice', models.CharField(max_length=32, verbose_name='Exercice')),
31
                ('users', models.ManyToManyField(blank=True, related_name='_astregs_users_+', related_query_name='+', to='base.ApiUser')),
32
            ],
33
            options={
34
                'verbose_name': 'AstresGS',
35
            },
36
        ),
37
        migrations.CreateModel(
38
            name='Link',
39
            fields=[
40
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
41
                ('name_id', models.CharField(max_length=32)),
42
                ('association_id', models.CharField(max_length=32)),
43
                ('created', models.DateTimeField(auto_now_add=True)),
44
                ('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='astregs.AstreGS')),
45
            ],
46
        ),
47
        migrations.CreateModel(
48
            name='LinkRequest',
49
            fields=[
50
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
51
                ('token', models.CharField(max_length=16)),
52
                ('association_id', models.CharField(max_length=32)),
53
                ('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='astregs.AstreGS')),
54
            ],
55
        ),
56
        migrations.AlterUniqueTogether(
57
            name='linkrequest',
58
            unique_together=set([('resource', 'token', 'association_id')]),
59
        ),
60
        migrations.AlterUniqueTogether(
61
            name='link',
62
            unique_together=set([('resource', 'name_id', 'association_id')]),
63
        ),
64
    ]
passerelle/apps/astregs/models.py
1
# -*- coding: utf-8 -*-
2
# Copyright (C) 2019  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from zeep.helpers import serialize_object
18

  
19
from django.db import models
20
from django.utils.translation import ugettext_lazy as _
21
from django.utils.crypto import get_random_string
22
from django.utils.text import slugify
23
from django.utils.six.moves.urllib import parse as urlparse
24
from django.http import Http404
25

  
26
from passerelle.base.models import BaseResource
27
from passerelle.utils.api import endpoint
28
from passerelle.utils.jsonresponse import APIError
29

  
30
LINK_CREATION_SCHEMA = {
31
    '$schema': 'http://json-schema.org/draft-03/schema#',
32
    'title': 'AstreGS Link',
33
    'description': '',
34
    'type': 'object',
35
    'properties': {
36
        'NameID': {
37
            'description': 'user name_id',
38
            'type': 'string',
39
            'required': True
40
        },
41
        'token': {
42
            'description': 'linking token',
43
            'type': 'string',
44
            'required': True
45
        }
46
    }
47
}
48

  
49

  
50
class AstreGS(BaseResource):
51
    wsdl_base_url = models.URLField(_('Webservices base URL'))
52
    username = models.CharField(_('Username'), max_length=32)
53
    password = models.CharField(_('Password'), max_length=32)
54
    organism = models.CharField('Organisme', max_length=32)
55
    budget = models.CharField('Budget', max_length=32)
56
    exercice = models.CharField('Exercice', max_length=32)
57

  
58
    category = _('Business Process Connectors')
59

  
60
    class Meta:
61
        verbose_name = u'AstresGS'
62

  
63
    def check_status(self):
64
        response = self.requests.get(self.wsdl_base_url)
65
        response.raise_for_status()
66

  
67
    @property
68
    def authentication(self):
69
        return {'USERNOM': self.username, 'USERPWD': self.password}
70

  
71
    @property
72
    def context(self):
73
        return {'Organisme': self.organism,
74
                'Budget': self.budget,
75
                'Exercice': self.exercice}
76

  
77
    def get_client(self, wsdl_name):
78
        url = urlparse.urljoin(self.wsdl_base_url, '%s?wsdl' % wsdl_name)
79
        client = self.soap_client(wsdl_url=url)
80
        parsed_wsdl_address = urlparse.urlparse(client.service._binding_options['address'])
81
        parsed_real_address = urlparse.urlparse(self.wsdl_base_url)
82
        client.service._binding_options['address'] = urlparse.urlunparse(
83
            parsed_real_address[:2] + parsed_wsdl_address[2:])
84
        return client
85

  
86

  
87
    @endpoint(description=_('Find associations by SIREN number'),
88
              perm='can_access',
89
              parameters={
90
                  'siren': {'description': _('SIREN Number'),
91
                            'example_value': '77567227216096'}
92
                  }
93
              )
94
    def associations(self, request, siren):
95
        client = self.get_client('RechercheTiersDetails')
96
        r = client.service.liste(Authentification=self.authentication,
97
                                 Contexte=self.context,
98
                                 Criteres={'siren': '%s*' % siren})
99
        data = []
100
        if r.liste:
101
            for item in r.liste.EnregRechercheTiersDetailsReturn:
102
                association_data = serialize_object(item)
103
                association_data['id'] = association_data['Numero_SIRET']
104
                association_data['text'] = '%(Numero_SIRET)s - %(Nom_enregistrement)s' % association_data
105
                association_data['code'] = association_data['Code_tiers']
106
                association_data['name'] = association_data['Nom_enregistrement']
107
                data.append(association_data)
108
        return {'data': data}
109

  
110
    @endpoint(description=_('Check if association exists by its SIRET number'),
111
              name='verify-association-presence-by-siret',
112
              perm='can_access',
113
              parameters={
114
                  'siret': {'description': _('SIRET Number'),
115
                            'example_value': '7756722721609600014'}
116
                  }
117
              )
118
    def verify_association_presence(self, request, siret):
119
        client = self.get_client('RechercheTiers')
120
        r = client.service.liste(Authentification=self.authentication,
121
                                 Contexte=self.context,
122
                                 Criteres={'siren': siret})
123
        if r.liste:
124
            return {'exists': True}
125
        return {'exists': False}
126

  
127
    @endpoint(name='get-association-link-means',
128
              description=_('Get association linking means'),
129
              perm='can_access',
130
              parameters={
131
                  'association_id': {'description': _('Association ID'),
132
                                     'example_value': '42435'}
133
                  }
134
    )
135
    def get_association_link_means(self, request, association_id):
136
        client = self.get_client('Tiers')
137
        r = client.service.Chargement({'Authentification': self.authentication,
138
                                       'Contexte': self.context,
139
                                       'TiersCle': {'CodeTiers': association_id}})
140
        data = []
141
        # assocation contact is defined in EncodeKeyContact attribute
142
        if not r.EncodeKeyContact:
143
            return {'data': data}
144

  
145
        client = self.get_client('Contact')
146
        r = client.service.Chargement({'Authentification': self.authentication,
147
                                       'Contexte': self.context,
148
                                       'ContactCle': {'idContact': r.EncodeKeyContact}})
149
        if r.AdresseMail or r.TelephoneMobile or r.RueVoie:
150
            link_request, created = LinkRequest.objects.get_or_create(resource=self,
151
                                                                      association_id=association_id,
152
                                                                      defaults={'token': get_random_string()})
153
        if r.AdresseMail:
154
            data.append({'id': 'email',
155
                         'text': r.AdresseMail,
156
                         'value': r.AdresseMail,
157
                         'token': link_request.token,
158
                         'type': 'email'})
159
        if r.TelephoneMobile:
160
            data.append({'id': 'mobile',
161
                         'text': r.TelephoneMobile,
162
                         'value': r.TelephoneMobile,
163
                         'token': link_request.token,
164
                         'type': 'mobile'})
165
        if r.RueVoie:
166
            full_address = r.RueVoie
167
            full_address += ', '
168
            if r.CodePostal:
169
                full_address += '%s ' % r.CodePostal
170
            if r.Ville:
171
                full_address += r.Ville
172
            data.append({'id': 'address',
173
                         'text': full_address,
174
                         'address': r.RueVoie,
175
                         'zicode': r.CodePostal,
176
                         'city': r.Ville,
177
                         'value': full_address,
178
                         'token': link_request.token,
179
                         'type': 'mail'})
180
        return {'data': data}
181

  
182
    @endpoint(name='link', perm='can_access',
183
              post={
184
                  'description': _('Create link with an association'),
185
                  'request_body': {
186
                      'schema': {
187
                          'application/json': LINK_CREATION_SCHEMA
188
                      }
189
                  }
190
              }
191
    )
192
    def link(self, request, post_data):
193
        try:
194
            link_request = LinkRequest.objects.get(resource=self, token=post_data['token'])
195
            # if link request found, create link
196
            link = Link.objects.create(resource=self, name_id=post_data['NameID'], association_id=link_request.association_id)
197
            # then delete link request
198
            link_request.delete()
199
            return {'link': link.id, 'created': link.created, 'association_id': link_request.association_id}
200
        except LinkRequest.DoesNotExist:
201
            raise Http404('token not found')
202

  
203

  
204
    @endpoint(description=_('Remove link to an association'),
205
              perm='can_access',
206
              parameters={
207
                  'NameID':{
208
                      'description': _('Publik NameID'),
209
                      'example_value': 'xyz24d934',
210
                  },
211
                  'association_id':{
212
                      'description': _('Association ID'),
213
                      'example_value': '12345',
214
                  }
215
              })
216
    def unlink(self, request, NameID, association_id):
217
        try:
218
            link = Link.objects.get(resource=self, name_id=NameID, association_id=association_id)
219
            link.delete()
220
            return {'deleted': True}
221
        except Link.DoesNotExist:
222
            raise Http404('link not found')
223

  
224

  
225
class Link(models.Model):
226
    resource = models.ForeignKey(AstreGS)
227
    name_id = models.CharField(max_length=32)
228
    association_id = models.CharField(max_length=32)
229
    created = models.DateTimeField(auto_now_add=True)
230

  
231
    class Meta:
232
        unique_together = ('resource', 'name_id', 'association_id')
233

  
234

  
235
class LinkRequest(models.Model):
236
    resource = models.ForeignKey(AstreGS)
237
    token = models.CharField(max_length=16)
238
    association_id = models.CharField(max_length=32)
239

  
240
    class Meta:
241
        unique_together = ('resource', 'token', 'association_id')
passerelle/settings.py
122 122
    'passerelle.apps.api_particulier',
123 123
    'passerelle.apps.arcgis',
124 124
    'passerelle.apps.arpege_ecp',
125
    'passerelle.apps.astregs',
125 126
    'passerelle.apps.atal',
126 127
    'passerelle.apps.atos_genesys',
127 128
    'passerelle.apps.base_adresse',
tests/test_astregs.py
1
# -*- coding: utf-8 -*-
2

  
3
import mock
4
import pytest
5

  
6
from passerelle.apps.astregs.models import AstreGS, LinkRequest, Link
7

  
8
from . import utils
9

  
10
BASE_URL = 'https://test-ws-astre-gs.departement06.fr/axis2/services/'
11

  
12

  
13
class FakeSearchDetailsService():
14
    def liste(self, Authentification, Contexte, Criteres):
15
        data = dict(Numero_SIRET='50043390900016',
16
                    Nom_enregistrement='ASSOCIATION OMNISPORTS DES MONTS D AZUR',
17
                    Code_tiers='173957'
18
        )
19
        liste = mock.Mock(EnregRechercheTiersDetailsReturn=[data])
20
        return mock.Mock(liste=liste)
21

  
22

  
23
class FakeSearchService():
24
    def liste(self, Authentification, Contexte, Criteres):
25
        if Criteres['siren'] == 'unknown':
26
            return mock.Mock(liste=False)
27
        return mock.Mock(liste=True)
28

  
29

  
30
class FakeTiersService():
31
    def Chargement(self, data):
32
        if data['TiersCle']['CodeTiers'] == '42':
33
            return mock.Mock(EncodeKeyContact='4242')
34
        return mock.Mock(EncodeKeyContact=None)
35

  
36

  
37
class FakeContactService():
38
    def Chargement(self, data):
39
        return mock.Mock(AdresseMail='foo@example.com',
40
                         TelephoneMobile='0607080900',
41
                         RueVoie='169 rue du Château',
42
                         CodePostal='75014',
43
                         Ville='Paris')
44

  
45

  
46
def search_side_effect(wsdl_url, **kwargs):
47
    if 'RechercheTiersDetail' in wsdl_url:
48
        return mock.Mock(service=FakeSearchDetailsService())
49
    return mock.Mock(service=FakeSearchService())
50

  
51
def contact_search_side_effect(wsdl_url, **kwargs):
52
    if 'Tiers' in wsdl_url:
53
        return mock.Mock(service=FakeTiersService())
54
    return mock.Mock(service=FakeContactService())
55

  
56

  
57
@pytest.fixture
58
def connector(db):
59
    return utils.make_resource(AstreGS,
60
            title='Test', slug='test',
61
            description='test', wsdl_base_url=BASE_URL,
62
            username='CS-FORML', password='secret',
63
            organism='CG06', budget='01',
64
            exercice='2019'
65
    )
66

  
67

  
68
@mock.patch('passerelle.apps.astregs.models.AstreGS.get_client', side_effect=search_side_effect)
69
def test_search_association_by_siren(client, connector, app):
70
    resp = app.get('/astregs/test/associations', params={'siren': '500433909'})
71
    assert isinstance(resp.json['data'], list)
72
    assert len(resp.json['data']) > 0
73
    assert resp.json['data'][0]['id'] == '50043390900016'
74
    assert resp.json['data'][0]['text'] == '50043390900016 - ASSOCIATION OMNISPORTS DES MONTS D AZUR'
75
    assert resp.json['data'][0]['code'] == '173957'
76
    assert resp.json['data'][0]['name'] == 'ASSOCIATION OMNISPORTS DES MONTS D AZUR'
77
    assert client.call_args[0][0] == 'RechercheTiersDetails'
78

  
79

  
80
@mock.patch('passerelle.apps.astregs.models.AstreGS.get_client', side_effect=search_side_effect)
81
def test_check_association_presence(client, connector, app):
82
    resp = app.get('/astregs/test/verify-association-presence-by-siret', params={'siret': '50043390900014'})
83
    assert resp.json['exists'] == True
84
    assert client.call_args[0][0] == 'RechercheTiers'
85
    resp = app.get('/astregs/test/verify-association-presence-by-siret', params={'siret': 'unknown'})
86
    assert resp.json['exists'] == False
87

  
88
@mock.patch('passerelle.apps.astregs.models.AstreGS.get_client', side_effect=contact_search_side_effect)
89
def test_association_linking_means(client, connector, app):
90
    resp = app.get('/astregs/test/get-association-link-means', params={'association_id': '000'})
91
    assert resp.json['data'] == []
92
    assert LinkRequest.objects.count() == 0
93

  
94
    resp = app.get('/astregs/test/get-association-link-means', params={'association_id': '42'})
95
    assert len(resp.json['data']) == 3
96
    assert LinkRequest.objects.filter(association_id='42').count() == 1
97
    for result in resp.json['data']:
98
        assert 'id' in result
99
        assert 'text' in result
100
        assert 'type' in result
101
        assert 'token' in result
102

  
103

  
104
@mock.patch('passerelle.apps.astregs.models.AstreGS.get_client', side_effect=contact_search_side_effect)
105
def test_link_user_to_association(client, connector, app):
106
    assert LinkRequest.objects.count() == 0
107
    resp = app.get('/astregs/test/get-association-link-means', params={'association_id': '42'})
108
    assert len(resp.json['data']) == 3
109
    token = resp.json['data'][0]['token']
110
    assert LinkRequest.objects.filter(association_id='42').count() == 1
111
    resp = app.post_json('/astregs/test/link', params={'token': token, 'NameID': 'user_name_id'})
112
    assert LinkRequest.objects.filter(association_id='42').count() == 0
113
    assert Link.objects.filter(name_id='user_name_id', association_id='42').count() == 1
114
    link = Link.objects.get(name_id='user_name_id', association_id='42')
115
    assert resp.json['association_id'] == link.association_id
116
    assert resp.json['link'] == link.pk
117
    assert 'created' in resp.json
118
    # try to link again
119
    resp = app.post_json('/astregs/test/link', params={'token': token, 'NameID': 'user_name_id'}, status=404)
120

  
121

  
122
@mock.patch('passerelle.apps.astregs.models.AstreGS.get_client', side_effect=contact_search_side_effect)
123
def test_unlink_user_from_association(client, connector, app):
124
    resp = app.get('/astregs/test/get-association-link-means', params={'association_id': '42'})
125
    token = resp.json['data'][0]['token']
126
    resp = app.post_json('/astregs/test/link', params={'token': token, 'NameID': 'user_name_id'})
127
    resp = app.get('/astregs/test/unlink', params={'NameID': 'user_name_id', 'association_id': '42'})
128
    assert resp.json['deleted']
129
    resp = app.get('/astregs/test/unlink', params={'NameID': 'user_name_id', 'association_id': '42'}, status=404)
0
-