Projet

Général

Profil

0001-add-connector-for-API-Particulier-14838.patch

Benjamin Dauvergne, 07 mars 2017 11:27

Télécharger (16,5 ko)

Voir les différences:

Subject: [PATCH] add connector for API-Particulier (#14838)

 passerelle/apps/api_particulier/__init__.py        |   0
 .../api_particulier/migrations/0001_initial.py     |  31 +++
 .../apps/api_particulier/migrations/__init__.py    |   0
 passerelle/apps/api_particulier/models.py          | 147 ++++++++++++++
 passerelle/settings.py                             |   1 +
 tests/test_api_particulier.py                      | 214 +++++++++++++++++++++
 tests/utils.py                                     |  24 +++
 7 files changed, 417 insertions(+)
 create mode 100644 passerelle/apps/api_particulier/__init__.py
 create mode 100644 passerelle/apps/api_particulier/migrations/0001_initial.py
 create mode 100644 passerelle/apps/api_particulier/migrations/__init__.py
 create mode 100644 passerelle/apps/api_particulier/models.py
 create mode 100644 tests/test_api_particulier.py
passerelle/apps/api_particulier/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import models, migrations
5

  
6

  
7
class Migration(migrations.Migration):
8

  
9
    dependencies = [
10
        ('base', '0002_auto_20151009_0326'),
11
    ]
12

  
13
    operations = [
14
        migrations.CreateModel(
15
            name='APIParticulier',
16
            fields=[
17
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
18
                ('title', models.CharField(max_length=50)),
19
                ('slug', models.SlugField()),
20
                ('description', models.TextField()),
21
                ('log_level', models.CharField(default=b'NOTSET', max_length=10, verbose_name='Log Level', choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL'), (b'FATAL', b'FATAL')])),
22
                ('_platform', models.CharField(max_length=8, verbose_name='platform', choices=[(b'test', 'Test'), (b'prod', 'Production'), (b'dev', 'Development'), (b'mock', 'Mock')])),
23
                ('_api_key', models.CharField(default=b'', max_length=64, verbose_name='API key', blank=True)),
24
                ('users', models.ManyToManyField(to='base.ApiUser', blank=True)),
25
            ],
26
            options={
27
                'abstract': False,
28
            },
29
            bases=(models.Model,),
30
        ),
31
    ]
passerelle/apps/api_particulier/models.py
1
# passerelle.apps.api_particulier
2
# Copyright (C) 2017  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
'''Gateway to API-Particulier web-service from SGMAP:
18
   https://particulier.api.gouv.fr/
19
'''
20

  
21
from urlparse import urljoin
22
from collections import OrderedDict
23

  
24
from django.db import models
25
from django.utils.translation import ugettext_lazy as _
26

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

  
31

  
32
class APIParticulier(BaseResource):
33
    PLATFORMS = [
34
        {
35
            'name': 'test',
36
            'label': _('Test'),
37
            'url': 'https://particulier-test.api.gouv.fr/api/',
38
            'api_key': 'test-token',
39
        },
40
        {
41
            'name': 'prod',
42
            'label': _('Production'),
43
            'url': 'https://particulier.api.gouv.fr/api/'
44
        },
45
        {
46
            'name': 'dev',
47
            'label': _('Development'),
48
            'url': 'https://particulier-dev.api.gouv.fr/api/'
49
        },
50
        {
51
            'name': 'mock',
52
            'label': _('Mock'),
53
            'url': 'https://particulier-mock.api.gouv.fr/api/'
54
        },
55
    ]
56
    PLATFORMS = OrderedDict([(platform['name'], platform) for platform in PLATFORMS])
57

  
58
    _platform = models.CharField(
59
        verbose_name=_('platform'),
60
        max_length=8,
61
        choices=[(key, platform['label']) for key, platform in PLATFORMS.iteritems()])
62

  
63
    _api_key = models.CharField(
64
        max_length=64,
65
        default='',
66
        blank=True,
67
        verbose_name=_('API key'))
68

  
69
    @property
70
    def platform(self):
71
        return self.PLATFORMS[self._platform]
72

  
73
    @property
74
    def url(self):
75
        return self.platform['url']
76

  
77
    @property
78
    def api_key(self):
79
        return self.platform.get('api_key', self._api_key)
80

  
81
    def get(self, path, **kwargs):
82
        user = kwargs.pop('user', None)
83
        url = urljoin(self.url, path)
84
        headers = {'X-API-KEY': self.api_key}
85
        if user:
86
            headers['X-User'] = user
87
        response = self.requests.get(
88
            url,
89
            headers=headers,
90
            **kwargs)
91

  
92
        if response.status_code != 200:
93
            raise APIError(
94
                u'API-particulier platform "%s" returned non-200 code: %s' %
95
                (self._platform, response.status_code),
96
                data={
97
                    'platform': self._platform,
98
                    'code': response.status_code,
99
                    'content': repr(response.content[:1000]),
100
                })
101
        try:
102
            return response.json()
103
        except ValueError as e:
104
            content = repr(response.content[:1000])
105
            raise APIError(
106
                u'API-particulier platform "%s" returned non-JSON content: %s' %
107
                (self._platform, content),
108
                data={
109
                    'exception': unicode(e),
110
                    'platform': self._platform,
111
                    'content': content,
112
                })
113

  
114
    @endpoint(serializer_type='json-api', perm='can_access')
115
    def impots_svair(self, request, numero_fiscal, reference_avis, user=None):
116
        return self.get('impots/svair', params={
117
            'numeroFiscal': numero_fiscal,
118
            'referenceAvis': reference_avis,
119
        }, user=user)
120

  
121
    @endpoint(serializer_type='json-api', perm='can_access')
122
    def impots_adresse(self, request, numero_fiscal, reference_avis, user=None):
123
        return self.get('impots/adresse', params={
124
            'numeroFiscal': numero_fiscal,
125
            'referenceAvis': reference_avis,
126
        }, user=user)
127

  
128
    @endpoint(serializer_type='json-api', perm='can_access')
129
    def caf_qf(self, request, code_postal, numero_allocataire, user=None):
130
        return self.get('caf/qf', params={
131
            'codePostal': code_postal,
132
            'numeroAllocataire': numero_allocataire,
133
        }, user=user)
134

  
135
    @endpoint(serializer_type='json-api', perm='can_access')
136
    def caf_adresse(self, request, code_postal, numero_allocataire, user=None):
137
        return self.get('caf/adresse', params={
138
            'codePostal': code_postal,
139
            'numeroAllocataire': numero_allocataire,
140
        }, user=user)
141

  
142
    @endpoint(serializer_type='json-api', perm='can_access')
143
    def caf_famille(self, request, code_postal, numero_allocataire, user=None):
144
        return self.get('caf/famille', params={
145
            'codePostal': code_postal,
146
            'numeroAllocataire': numero_allocataire,
147
        }, user=user)
passerelle/settings.py
111 111
    'csvdatasource',
112 112
    'orange',
113 113
    'family',
114
    'api_particulier',
114 115
    # backoffice templates and static
115 116
    'gadjo',
116 117
)
tests/test_api_particulier.py
1
# -*- coding: utf-8 -*-
2

  
3
# tests/test_api_particulier.py
4
# Copyright (C) 2017  Entr'ouvert
5
#
6
# This program is free software: you can redistribute it and/or modify it
7
# under the terms of the GNU Affero General Public License as published
8
# by the Free Software Foundation, either version 3 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU Affero General Public License for more details.
15
#
16
# You should have received a copy of the GNU Affero General Public License
17
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18

  
19
import pytest
20
from httmock import urlmatch, HTTMock, response
21

  
22
from api_particulier.models import APIParticulier
23

  
24
from utils import make_ressource, endpoint_get
25

  
26
SVAIR_RESPONSE = {
27
    "declarant1": {
28
        "nom": "Martin",
29
        "nomNaissance": "Martin",
30
        "prenoms": "Pierre",
31
        "dateNaissance": "22/03/1985"
32
    },
33
    "declarant2": {
34
        "nom": "Martin",
35
        "nomNaissance": "Honore",
36
        "prenoms": "Marie",
37
        "dateNaissance": "03/04/1986"
38
    },
39
    "foyerFiscal": {
40
        "annee": 2015,
41
        "adresse": "12 rue Balzac 75008 Paris"
42
    },
43
    "dateRecouvrement": "10/10/2015",
44
    "dateEtablissement": "08/07/2015",
45
    "nombreParts": 2,
46
    "situationFamille": "Marié(e)s",
47
    "nombrePersonnesCharge": 2,
48
    "revenuBrutGlobal": 29880,
49
    "revenuImposable": 29880,
50
    "impotRevenuNetAvantCorrections": 2165,
51
    "montantImpot": 2165,
52
    "revenuFiscalReference": 29880,
53
    "anneeImpots": "2015",
54
    "anneeRevenus": "2014"
55
}
56

  
57
IMPOTS_ADRESSE = {
58
    "adresses": [
59
        {
60
            "adresse": {
61
                "citycode": "75108",
62
                "street": "Rue Balzac",
63
                "name": "12 Rue Balzac",
64
                "housenumber": "12",
65
                "city": "Paris",
66
                "type": "housenumber",
67
                "context": "75, Île-de-France",
68
                "score": 0.9401454545454544,
69
                "label": "12 Rue Balzac 75008 Paris",
70
                "postcode": "75008"
71
            },
72
            "geometry": {
73
                "type": "Point",
74
                "coordinates": [
75
                    2.300816,
76
                    48.873951
77
                ]
78
            }
79
        }
80
    ],
81
    "declarant1": {
82
        "nom": "Martin",
83
        "nomNaissance": "Martin",
84
        "prenoms": "Pierre",
85
        "dateNaissance": "22/03/1985"
86
    },
87
    "declarant2": {
88
        "nom": "Martin",
89
        "nomNaissance": "Honore",
90
        "prenoms": "Marie",
91
        "dateNaissance": "03/04/1986"
92
    },
93
    "foyerFiscal": {
94
        "annee": 2015,
95
        "adresse": "12 rue Balzac 75008 Paris"
96
    }
97
}
98

  
99

  
100
@urlmatch(netloc='^particulier.*\.api\.gouv\.fr$',
101
          path='^/api/impots/svair$')
102
def api_particulier_impots_svair(url, request):
103
    return response(200, SVAIR_RESPONSE, request=request)
104

  
105

  
106
@urlmatch(netloc='^particulier.*\.api\.gouv\.fr$',
107
          path='^/api/impots/adresse$')
108
def api_particulier_impots_adresse(url, request):
109
    return response(200, IMPOTS_ADRESSE, request=request)
110

  
111

  
112
@urlmatch(netloc='^particulier.*\.api\.gouv\.fr$')
113
def api_particulier_error_500(url, request):
114
    return response(500, 'something bad happened', request=request)
115

  
116
@urlmatch(netloc='^particulier.*\.api\.gouv\.fr$')
117
def api_particulier_error_not_json(url, request):
118
    return response(200, 'something bad happened', request=request)
119

  
120
@pytest.yield_fixture
121
def mock_api_particulier():
122
    with HTTMock(api_particulier_impots_svair, api_particulier_impots_adresse):
123
        yield None
124

  
125

  
126
@pytest.fixture
127
def ressource(db):
128
    return make_ressource(
129
        APIParticulier,
130
        slug='test',
131
        title='API Particulier Prod',
132
        description='API Particulier Prod',
133
        _platform='test')
134

  
135

  
136
def test_error(app, ressource, mock_api_particulier):
137
    with HTTMock(api_particulier_error_500):
138
        def do(endpoint, params):
139
            resp = endpoint_get(
140
                '/api-particulier/test/%s' % endpoint,
141
                app,
142
                ressource,
143
                endpoint,
144
                params=params)
145
            assert resp.status_code == 200
146
            assert resp.json['err'] == 1
147
            assert resp.json['data']['code'] == 500
148
        vector = [
149
            (['impots_svair', 'impots_adresse'], {
150
                'numero_fiscal': 12,
151
                'reference_avis': 15,
152
                'user': 'john.doe',
153
            }),
154
            (['caf_qf', 'caf_adresse', 'caf_famille'], {
155
                'code_postal': 12,
156
                'numero_allocataire': 15
157
            }),
158
        ]
159
        for endpoints, params in vector:
160
            for endpoint in endpoints:
161
                do(endpoint, params)
162
    with HTTMock(api_particulier_error_not_json):
163
        def do(endpoint, params):
164
            resp = endpoint_get(
165
                '/api-particulier/test/%s' % endpoint,
166
                app,
167
                ressource,
168
                endpoint,
169
                params=params)
170
            assert resp.status_code == 200
171
            assert resp.json['err'] == 1
172
            assert resp.json['data']['exception'] == 'No JSON object could be decoded'
173
        vector = [
174
            (['impots_svair', 'impots_adresse'], {
175
                'numero_fiscal': 12,
176
                'reference_avis': 15,
177
                'user': 'john.doe',
178
            }),
179
            (['caf_qf', 'caf_adresse', 'caf_famille'], {
180
                'code_postal': 12,
181
                'numero_allocataire': 15
182
            }),
183
        ]
184
        for endpoints, params in vector:
185
            for endpoint in endpoints:
186
                do(endpoint, params)
187

  
188

  
189
def test_impots_svair(app, ressource, mock_api_particulier):
190
    resp = endpoint_get(
191
        '/api-particulier/test/impots_svair',
192
        app,
193
        ressource,
194
        'impots_svair',
195
        params={
196
            'numero_fiscal': 12,
197
            'reference_avis': 15
198
        })
199
    assert resp.json['data']['montantImpot'] == 2165
200

  
201

  
202
def test_impots_adresse(app, ressource, mock_api_particulier):
203
    resp = endpoint_get(
204
        '/api-particulier/test/impots_adresse',
205
        app,
206
        ressource,
207
        'impots_adresse',
208
        params={
209
            'numero_fiscal': 12,
210
            'reference_avis': 15
211
        })
212
    assert resp.json['data']['adresses'][0]['adresse']['citycode'] == '75108'
213

  
214
# FIXME: CAF web services are currently broken, add test eventually when we can test them
tests/utils.py
39 39
    def mocked(url, request):
40 40
        return response
41 41
    return httmock.HTTMock(mocked)
42

  
43

  
44
def make_ressource(model_class, **kwargs):
45
    api, created = ApiUser.objects.get_or_create(
46
        username='all',
47
        keytype='',
48
        key='')
49
    ressource = model_class.objects.create(**kwargs)
50
    obj_type = ContentType.objects.get_for_model(model_class)
51
    AccessRight.objects.get_or_create(
52
        codename='can_access',
53
        apiuser=api,
54
        resource_type=obj_type,
55
        resource_pk=ressource.pk)
56
    return ressource
57

  
58

  
59
def endpoint_get(expected_url, app, ressource, endpoint, **kwargs):
60
    url = generic_endpoint_url(
61
        connector=ressource.__class__.get_connector_slug(),
62
        endpoint=endpoint,
63
        slug=ressource.slug)
64
    assert url == expected_url, 'endpoint URL has changed'
65
    return app.get(url, **kwargs)
42
-