Projet

Général

Profil

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

Nicolas Roche, 02 août 2021 15:07

Télécharger (19,8 ko)

Voir les différences:

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

 .../migrations/0003_smartrequest.py           |  48 +++++
 passerelle/contrib/toulouse_smart/models.py   | 107 +++++++++++
 passerelle/contrib/toulouse_smart/schemas.py  |  45 +++++
 tests/test_toulouse_smart.py                  | 168 +++++++++++++++++-
 4 files changed, 367 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-07-30 22:37
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
                    'status',
28
                    models.CharField(
29
                        choices=[('registered', 'Registered'), ('sent', 'Sent')],
30
                        default='registered',
31
                        max_length=20,
32
                    ),
33
                ),
34
                (
35
                    'resource',
36
                    models.ForeignKey(
37
                        on_delete=django.db.models.deletion.CASCADE,
38
                        related_name='smart_requests',
39
                        to='toulouse_smart.WcsRequest',
40
                        verbose_name='SmartRequest',
41
                    ),
42
                ),
43
            ],
44
            options={
45
                'ordering': ['pk', 'status'],
46
            },
47
        ),
48
    ]
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, transaction
25
from django.utils.six.moves.urllib import parse as urlparse
23 26
from django.utils.text import slugify
24 27
from django.utils.timezone import now
25 28
from django.utils.translation import ugettext_lazy as _
26 29
from requests import RequestException
27 30

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

  
33 37
from . import schemas
34 38

  
35 39

  
36 40
class ToulouseSmartResource(BaseResource, HTTPResource):
37 41
    category = _('Business Process Connectors')
38 42

  
39 43
    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
    @endpoint(
248
        name='update-intervention',
249
        methods=['post'],
250
        description=_('Update an intervention status'),
251
        parameters={
252
            'id': {'description': _('Intervention identifier')},
253
        },
254
        post={'request_body': {'schema': {'application/json': schemas.UPDATE_SCHEMA}}},
255
    )
256
    def update_intervention(self, request, uuid, post_data):
257
        with transaction.atomic():
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
            smart_request.status = 'registered'
264
            smart_request.save()
265
            if smart_request.push():
266
                smart_request = SmartRequest.objects.get(id=smart_request.id)
267
                if smart_request.status != 'sent':
268
                    raise APIError(smart_request.result)
269
            else:
270
                self.add_job(
271
                    'update_intervention_job',
272
                    id=smart_request.id,
273
                    natural_id='smart-request-%s' % smart_request.id,
274
                )
275
            smart_request = SmartRequest.objects.get(id=smart_request.id)
276
            return {
277
                'data': {
278
                    'wcs_form_api_url': wcs_request.wcs_form_api_url,
279
                    'wcs_form_number': wcs_request.wcs_form_number,
280
                    'uuid': wcs_request.uuid,
281
                    'payload': smart_request.payload,
282
                    'result': smart_request.result,
283
                    'status': smart_request.status,
284
                }
285
            }
286

  
287
    def update_intervention_job(self, *args, **kwargs):
288
        smart_request = SmartRequest.objects.get(id=kwargs['id'])
289
        if not smart_request.push():
290
            raise SkipJob()
291

  
243 292

  
244 293
class Cache(models.Model):
245 294
    resource = models.ForeignKey(
246 295
        verbose_name=_('Resource'),
247 296
        to=ToulouseSmartResource,
248 297
        on_delete=models.CASCADE,
249 298
        related_name='cache_entries',
250 299
    )
......
290 339
        except ValueError:
291 340
            err_desc = 'invalid json, got: %s' % response.text
292 341
            self.result = err_desc
293 342
            self.save()
294 343
            return False
295 344
        self.status = 'sent'
296 345
        self.save()
297 346
        return True
347

  
348

  
349
class SmartRequest(models.Model):
350
    resource = models.ForeignKey(
351
        verbose_name=_('SmartRequest'),
352
        to=WcsRequest,
353
        on_delete=models.CASCADE,
354
        related_name='smart_requests',
355
    )
356
    payload = JSONField(default=dict)
357
    result = JSONField(default=dict)
358
    status = models.CharField(
359
        max_length=20,
360
        default='registered',
361
        choices=(
362
            ('registered', _('Registered')),
363
            ('sent', _('Sent')),
364
        ),
365
    )
366

  
367
    class Meta:
368
        ordering = ['pk', 'status']
369

  
370
    def push(self):
371
        base_url = '%shooks/update-intervention/' % (self.resource.wcs_form_api_url)
372
        scheme, netloc, path, params, query, fragment = urlparse.urlparse(base_url)
373
        services = settings.KNOWN_SERVICES.get('wcs', {})
374
        service = None
375
        for service in services.values():
376
            remote_url = service.get('url')
377
            r_scheme, r_netloc, r_path, r_params, r_query, r_fragment = urlparse.urlparse(remote_url)
378
            if r_scheme == scheme and r_netloc == netloc:
379
                break
380
        else:
381
            err_desc = 'Cannot find wcs service for %s' % base_url
382
            self.result = err_desc
383
            self.save()
384
            return True
385

  
386
        wcs_api = WcsApi(base_url, orig=service.get('orig'), key=service.get('secret'))
387
        headers = {
388
            'Content-Type': 'application/json',
389
            'Accept': 'application/json',
390
        }
391
        try:
392
            result = wcs_api.post_json(self.payload, [], headers=headers)
393
        except WcsApiError as e:
394
            try:
395
                result = json.loads(e.args[3])
396
            except (ValueError):
397
                return False
398
        self.result = result
399
        if result['err']:
400
            self.save()
401
            return False
402
        self.status = 'sent'
403
        self.save()
404
        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
25 25
import httmock
26 26
import lxml.etree as ET
27 27
import mock
28 28
import pytest
29 29
import utils
30 30
from test_manager import login
31 31

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

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

  
37 37

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

  
50 50

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

  
64

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

  
57 71
        def register(path, payload, content, status_code=200):
58 72
            @httmock.urlmatch(path=path)
......
514 528
)
515 529
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
516 530
def test_create_intervention_content_error(mocked_uuid, app, freezer, smart):
517 531
    freezer.move_to('2021-07-08 00:00:00')
518 532
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
519 533
    wcs_request = smart.wcs_requests.get(uuid=UUID)
520 534
    assert wcs_request.status == 'registered'
521 535
    assert 'invalid json' in wcs_request.result
536

  
537

  
538
UPDATE_INTERVENTION_PAYLOAD = {
539
    'data': {
540
        'status': 'close manque info',
541
        'type_retour_cloture': 'Smart non Fait',
542
        'libelle_cloture': "rien à l'adresse indiquée",
543
        'commentaire_cloture': 'le commentaire',
544
    }
545
}
546
UPDATE_INTERVENTION_QUERY = UPDATE_INTERVENTION_PAYLOAD
547
WCS_RESPONSE_SUCCESS = '{"err": 0, "url": null}'
548
WCS_RESPONSE_ERROR = '{"err": 1, "err_class": "Access denied", "err_desc": null}'
549

  
550

  
551
@mock_response(
552
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
553
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')],
554
    ['/api/forms/foo/2/hooks/update-intervention/', UPDATE_INTERVENTION_QUERY, WCS_RESPONSE_SUCCESS],
555
)
556
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
557
def test_update_intervention(mocked_uuid, app, smart, wcs_service):
558
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
559
    assert not resp.json['err']
560
    assert CREATE_INTERVENTION_QUERY['notificationUrl'] == 'update-intervention?uuid=%s' % str(UUID)
561
    wcs_request = smart.wcs_requests.get(uuid=UUID)
562
    assert wcs_request.status == 'sent'
563

  
564
    url = URL + 'update-intervention?uuid=%s' % str(UUID)
565
    resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD)
566
    assert not resp.json['err']
567
    assert resp.json['data']['uuid'] == str(UUID)
568
    assert resp.json['data']['payload']['data']['type_retour_cloture'] == 'Smart non Fait'
569
    smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get()
570
    assert smart_request.status == 'sent'
571
    assert smart_request.result == {'err': 0, 'url': None}
572

  
573

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

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

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

  
602
    mocked_push.stop()
603
    assert Job.objects.count() == 1
604
    job = Job.objects.get(method_name='update_intervention_job')
605
    assert job.status == 'registered'
606
    smart.jobs()
607
    job = Job.objects.get(method_name='update_intervention_job')
608
    assert job.status == 'completed'
609
    smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get()
610
    assert smart_request.status == 'sent'
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 resp.json['err']
638
    assert 'Cannot find wcs service' in resp.json['err_desc']
639
    assert SmartRequest.objects.count() == 0
640

  
641

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

  
652
    url = URL + 'update-intervention?uuid=%s' % str(UUID)
653
    resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD)
654
    assert not resp.json['err']
655
    smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get()
656
    assert smart_request.status == 'registered'
657
    assert smart_request.result == {'err': 1, 'err_class': 'Access denied', 'err_desc': None}
658

  
659

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

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

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