Projet

Général

Profil

0007-toulouse_smart-add-update-intervention-endpoint-5523.patch

Nicolas Roche, 06 août 2021 11:51

Télécharger (17,5 ko)

Voir les différences:

Subject: [PATCH 7/7] toulouse_smart: add update-intervention endpoint (#55230)

 .../migrations/0003_smartrequest.py           |  37 +++++
 passerelle/contrib/toulouse_smart/models.py   |  86 ++++++++++
 passerelle/contrib/toulouse_smart/schemas.py  |  45 ++++++
 tests/test_toulouse_smart.py                  | 147 +++++++++++++++++-
 4 files changed, 314 insertions(+), 1 deletion(-)
 create mode 100644 passerelle/contrib/toulouse_smart/migrations/0003_smartrequest.py
passerelle/contrib/toulouse_smart/migrations/0003_smartrequest.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.29 on 2021-08-06 09:30
3
from __future__ import unicode_literals
4

  
5
import django.contrib.postgres.fields.jsonb
6
import django.db.models.deletion
7
from django.db import migrations, models
8

  
9

  
10
class Migration(migrations.Migration):
11

  
12
    dependencies = [
13
        ('toulouse_smart', '0002_auto_20210731_0031'),
14
    ]
15

  
16
    operations = [
17
        migrations.CreateModel(
18
            name='SmartRequest',
19
            fields=[
20
                (
21
                    'id',
22
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
23
                ),
24
                ('payload', django.contrib.postgres.fields.jsonb.JSONField(default=dict)),
25
                ('result', django.contrib.postgres.fields.jsonb.JSONField(default=dict)),
26
                (
27
                    'resource',
28
                    models.ForeignKey(
29
                        on_delete=django.db.models.deletion.CASCADE,
30
                        related_name='smart_requests',
31
                        to='toulouse_smart.WcsRequest',
32
                        verbose_name='SmartRequest',
33
                    ),
34
                ),
35
            ],
36
        ),
37
    ]
passerelle/contrib/toulouse_smart/models.py
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU Affero General Public License for more details.
13 13
#
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17 17
import datetime
18
import json
18 19
from uuid import uuid4
19 20

  
20 21
import lxml.etree as ET
22
from django.conf import settings
21 23
from django.contrib.postgres.fields import JSONField
22 24
from django.db import models
23 25
from django.db.transaction import atomic
24 26
from django.urls import reverse
27
from django.utils.six.moves.urllib import parse as urlparse
25 28
from django.utils.text import slugify
26 29
from django.utils.timezone import now
27 30
from django.utils.translation import ugettext_lazy as _
28 31
from requests import RequestException
29 32

  
30 33
from passerelle.base.models import BaseResource, HTTPResource, SkipJob
31 34
from passerelle.utils import xml
32 35
from passerelle.utils.api import endpoint
33 36
from passerelle.utils.jsonresponse import APIError
37
from passerelle.utils.wcs import WcsApi, WcsApiError
34 38

  
35 39
from . import schemas
36 40

  
37 41

  
38 42
class ToulouseSmartResource(BaseResource, HTTPResource):
39 43
    category = _('Business Process Connectors')
40 44

  
41 45
    webservice_base_url = models.URLField(_('Webservice Base URL'))
......
235 239
            }
236 240
        }
237 241

  
238 242
    def create_intervention_job(self, *args, **kwargs):
239 243
        wcs_request = self.wcs_requests.get(pk=kwargs['pk'])
240 244
        if not wcs_request.push():
241 245
            raise SkipJob()
242 246

  
247
    @atomic
248
    @endpoint(
249
        name='update-intervention',
250
        methods=['post'],
251
        description=_('Update an intervention status'),
252
        parameters={
253
            'id': {'description': _('Intervention identifier')},
254
        },
255
        post={'request_body': {'schema': {'application/json': schemas.UPDATE_SCHEMA}}},
256
    )
257
    def update_intervention(self, request, uuid, post_data):
258
        try:
259
            wcs_request = self.wcs_requests.get(uuid=uuid)
260
        except WcsRequest.DoesNotExist:
261
            raise APIError("Cannot find intervention '%s'" % uuid, http_status=400)
262
        smart_request = wcs_request.smart_requests.create(payload=post_data)
263
        self.add_job(
264
            'update_intervention_job',
265
            id=smart_request.id,
266
            natural_id='smart-request-%s' % smart_request.id,
267
        )
268
        return {
269
            'data': {
270
                'wcs_form_api_url': wcs_request.wcs_form_api_url,
271
                'wcs_form_number': wcs_request.wcs_form_number,
272
                'uuid': wcs_request.uuid,
273
                'payload': smart_request.payload,
274
            }
275
        }
276

  
277
    def update_intervention_job(self, *args, **kwargs):
278
        smart_request = SmartRequest.objects.get(id=kwargs['id'])
279
        if not smart_request.push():
280
            raise SkipJob()
281

  
243 282

  
244 283
class Cache(models.Model):
245 284
    resource = models.ForeignKey(
246 285
        verbose_name=_('Resource'),
247 286
        to=ToulouseSmartResource,
248 287
        on_delete=models.CASCADE,
249 288
        related_name='cache_entries',
250 289
    )
......
290 329
        except ValueError:
291 330
            err_desc = 'invalid json, got: %s' % response.text
292 331
            self.result = err_desc
293 332
            self.save()
294 333
            return False
295 334
        self.status = 'sent'
296 335
        self.save()
297 336
        return True
337

  
338

  
339
class SmartRequest(models.Model):
340
    resource = models.ForeignKey(
341
        verbose_name=_('SmartRequest'),
342
        to=WcsRequest,
343
        on_delete=models.CASCADE,
344
        related_name='smart_requests',
345
    )
346
    payload = JSONField(default=dict)
347
    result = JSONField(default=dict)
348

  
349
    def get_wcs_api(self, base_url):
350
        scheme, netloc, path, params, query, fragment = urlparse.urlparse(base_url)
351
        services = settings.KNOWN_SERVICES.get('wcs', {})
352
        service = None
353
        for service in services.values():
354
            remote_url = service.get('url')
355
            r_scheme, r_netloc, r_path, r_params, r_query, r_fragment = urlparse.urlparse(remote_url)
356
            if r_scheme == scheme and r_netloc == netloc:
357
                break
358
        else:
359
            return None
360
        return WcsApi(base_url, orig=service.get('orig'), key=service.get('secret'))
361

  
362
    def push(self):
363
        headers = {
364
            'Content-Type': 'application/json',
365
            'Accept': 'application/json',
366
        }
367
        base_url = '%shooks/update-intervention/' % (self.resource.wcs_form_api_url)
368
        wcs_api = self.get_wcs_api(base_url)
369
        if not wcs_api:
370
            err_desc = 'Cannot find wcs service for %s' % base_url
371
            self.result = err_desc
372
            self.save()
373
            return True
374
        try:
375
            result = wcs_api.post_json(self.payload, [], headers=headers)
376
        except WcsApiError as e:
377
            try:
378
                result = json.loads(e.args[3])
379
            except (ValueError):
380
                return False
381
        self.result = result
382
        self.save()
383
        return True
passerelle/contrib/toulouse_smart/schemas.py
119 119
        'submitterType',
120 120
        'external_number',
121 121
        'external_status',
122 122
        'address',
123 123
        'form_api_url',
124 124
    ],
125 125
    'merge_extra': True,
126 126
}
127

  
128
UPDATE_SCHEMA = {
129
    '$schema': 'http://json-schema.org/draft-04/schema#',
130
    'type': 'object',
131
    'properties': {
132
        'data': {
133
            'type': 'object',
134
            'properties': {
135
                'status': {
136
                    'description': "Nouveau Statut de l'intervention",
137
                    'type': 'string',
138
                    'maxLength': 20,
139
                },
140
                'date_planification': {
141
                    'description': "Date de planification de l'intervention",
142
                    'type': 'string',
143
                    'pattern': '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}',
144
                },
145
                'date_mise_en_securite': {
146
                    'description': "Date de mise en securite de l'intervention",
147
                    'type': 'string',
148
                    'pattern': '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}',
149
                },
150
                'type_retour': {
151
                    'description': "Type du retour",
152
                    'type': 'string',
153
                },
154
                'type_retour_cloture': {
155
                    'description': "Type du retour de la clôture",
156
                    'type': 'string',
157
                },
158
                'libelle_cloture': {
159
                    'description': "Libellé de la clôture",
160
                    'type': 'string',
161
                },
162
                'commentaire_cloture': {
163
                    'description': "Commentaire de la clôture",
164
                    'type': 'string',
165
                },
166
            },
167
            'required': ['status'],
168
        },
169
    },
170
    'required': ['data'],
171
}
tests/test_toulouse_smart.py
26 26
import lxml.etree as ET
27 27
import mock
28 28
import pytest
29 29
import utils
30 30
from requests.exceptions import ReadTimeout
31 31
from test_manager import login
32 32

  
33 33
from passerelle.base.models import Job
34
from passerelle.contrib.toulouse_smart.models import ToulouseSmartResource, WcsRequest
34
from passerelle.contrib.toulouse_smart.models import SmartRequest, ToulouseSmartResource, WcsRequest
35 35

  
36 36
TEST_BASE_DIR = os.path.join(os.path.dirname(__file__), 'data', 'toulouse_smart')
37 37

  
38 38

  
39 39
@pytest.fixture
40 40
def smart(db):
41 41
    return utils.make_resource(
42 42
        ToulouseSmartResource,
......
44 44
        slug='test',
45 45
        description='Test',
46 46
        webservice_base_url='https://smart.example.com/',
47 47
        basic_auth_username='username',
48 48
        basic_auth_password='password',
49 49
    )
50 50

  
51 51

  
52
@pytest.fixture
53
def wcs_service(settings):
54
    wcs_service = {
55
        'default': {
56
            'title': 'test',
57
            'url': 'https://wcs.example.com',
58
            'secret': 'xxx',
59
            'orig': 'passerelle',
60
        },
61
    }
62
    settings.KNOWN_SERVICES = {'wcs': wcs_service}
63
    return wcs_service
64

  
65

  
52 66
def mock_response(*path_contents):
53 67
    def decorator(func):
54 68
        @httmock.urlmatch()
55 69
        def error(url, request):
56 70
            assert False, 'request to %s' % url.geturl()
57 71

  
58 72
        def register(path, payload, content, status_code=200, exception=None):
59 73
            @httmock.urlmatch(path=path)
......
538 552
)
539 553
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
540 554
def test_create_intervention_content_error(mocked_uuid, app, freezer, smart):
541 555
    freezer.move_to('2021-07-08 00:00:00')
542 556
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
543 557
    wcs_request = smart.wcs_requests.get(uuid=UUID)
544 558
    assert wcs_request.status == 'registered'
545 559
    assert 'invalid json' in wcs_request.result
560

  
561

  
562
UPDATE_INTERVENTION_PAYLOAD = {
563
    'data': {
564
        'status': 'close manque info',
565
        'type_retour_cloture': 'Smart non Fait',
566
        'libelle_cloture': "rien à l'adresse indiquée",
567
        'commentaire_cloture': 'le commentaire',
568
    }
569
}
570
UPDATE_INTERVENTION_QUERY = UPDATE_INTERVENTION_PAYLOAD
571
WCS_RESPONSE_SUCCESS = '{"err": 0, "url": null}'
572
WCS_RESPONSE_ERROR = '{"err": 1, "err_class": "Access denied", "err_desc": null}'
573

  
574

  
575
@mock_response(
576
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
577
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')],
578
    ['/api/forms/foo/2/hooks/update-intervention/', UPDATE_INTERVENTION_QUERY, WCS_RESPONSE_SUCCESS],
579
)
580
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
581
def test_update_intervention(mocked_uuid, app, smart, wcs_service):
582
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
583
    assert not resp.json['err']
584
    assert CREATE_INTERVENTION_QUERY[
585
        'notificationUrl'
586
    ] == 'http://testserver/toulouse-smart/test/update-intervention?uuid=%s' % str(UUID)
587
    wcs_request = smart.wcs_requests.get(uuid=UUID)
588

  
589
    mocked_push = mock.patch(
590
        "passerelle.contrib.toulouse_smart.models.SmartRequest.push",
591
        return_value=False,
592
    )
593
    mocked_push.start()
594

  
595
    assert Job.objects.count() == 0
596
    url = URL + 'update-intervention?uuid=%s' % str(UUID)
597
    resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD)
598
    assert not resp.json['err']
599
    assert resp.json['data']['uuid'] == str(UUID)
600
    assert resp.json['data']['payload']['data']['type_retour_cloture'] == 'Smart non Fait'
601
    smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get()
602

  
603
    mocked_push.stop()
604
    assert Job.objects.count() == 1
605
    job = Job.objects.get(method_name='update_intervention_job')
606
    assert job.status == 'registered'
607
    smart.jobs()
608
    job = Job.objects.get(method_name='update_intervention_job')
609
    assert job.status == 'completed'
610
    smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get()
611
    assert smart_request.result == {'err': 0, 'url': None}
612

  
613

  
614
def test_update_intervention_wrong_uuid(app, smart):
615
    with pytest.raises(WcsRequest.DoesNotExist):
616
        smart.wcs_requests.get(uuid=UUID)
617

  
618
    url = URL + 'update-intervention?uuid=%s' % str(UUID)
619
    resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD, status=400)
620
    assert resp.json['err']
621
    assert 'Cannot find intervention' in resp.json['err_desc']
622
    assert SmartRequest.objects.count() == 0
623

  
624

  
625
@mock_response(
626
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
627
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')],
628
)
629
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
630
def test_update_intervention_job_wrong_service(mocked_uuid, app, smart, wcs_service):
631
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
632
    assert not resp.json['err']
633

  
634
    wcs_service['default']['url'] = 'http://wrong.example.com'
635
    url = URL + 'update-intervention?uuid=%s' % str(UUID)
636
    resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD)
637
    assert not resp.json['err']
638
    smart.jobs()
639
    job = Job.objects.get(method_name='update_intervention_job')
640
    assert job.status == 'completed'
641
    smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get()
642
    assert 'Cannot find wcs service' in smart_request.result
643

  
644

  
645
@mock_response(
646
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
647
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')],
648
    ['/api/forms/foo/2/hooks/update-intervention/', UPDATE_INTERVENTION_QUERY, WCS_RESPONSE_ERROR, 403],
649
)
650
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
651
def test_update_intervention_job_wcs_error(mocked_uuid, app, smart, wcs_service):
652
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
653
    assert not resp.json['err']
654

  
655
    url = URL + 'update-intervention?uuid=%s' % str(UUID)
656
    resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD)
657
    assert not resp.json['err']
658
    smart.jobs()
659
    job = Job.objects.get(method_name='update_intervention_job')
660
    assert job.status == 'completed'
661
    smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get()
662
    assert smart_request.result == {'err': 1, 'err_class': 'Access denied', 'err_desc': None}
663

  
664

  
665
@mock_response(
666
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
667
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')],
668
    ['/api/forms/foo/2/hooks/update-intervention/', UPDATE_INTERVENTION_QUERY, 'bla', 500],
669
)
670
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
671
def test_update_intervention_job_transport_error(mocked_uuid, app, freezer, smart, wcs_service):
672
    freezer.move_to('2021-07-08 00:00:00')
673
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
674
    assert not resp.json['err']
675

  
676
    url = URL + 'update-intervention?uuid=%s' % str(UUID)
677
    resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD)
678
    assert not resp.json['err']
679
    job = Job.objects.get(method_name='update_intervention_job')
680
    assert job.status == 'registered'
681
    smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get()
682
    assert smart_request.result == {}
683

  
684
    freezer.move_to('2021-07-08 00:00:03')
685
    smart.jobs()
686
    job = Job.objects.get(method_name='update_intervention_job')
687
    assert job.status == 'registered'
688
    assert job.update_timestamp > job.creation_timestamp
689
    smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get()
690
    assert smart_request.result == {}
546
-