Projet

Général

Profil

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

Nicolas Roche, 19 juillet 2021 10:22

Télécharger (13,4 ko)

Voir les différences:

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

 passerelle/contrib/toulouse_smart/models.py |  74 ++++++++-
 tests/test_toulouse_smart.py                | 164 ++++++++++++++++++++
 2 files changed, 237 insertions(+), 1 deletion(-)
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
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
from passerelle.base.models import BaseResource, HTTPResource
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'))
......
216 220
            raise APIError('invalid json, got: %s' % response.text)
217 221

  
218 222
        wcs_request.payload = payload
219 223
        wcs_request.result = result
220 224
        wcs_request.status = 'sent'
221 225
        wcs_request.save()
222 226
        return {'data': result, 'notificationUrl': notificationUrl}
223 227

  
228
    @endpoint(
229
        name='update-intervention',
230
        methods=['post'],
231
        description=_('Update an intervention status'),
232
        perm='can_access',
233
        parameters={
234
            'id': {'description': _('Intervention identifier')},
235
            'trigger': {'description': _('W.C.S trigger')},
236
        },
237
    )
238
    def update_intervention(self, request, uuid, trigger):
239
        try:
240
            wcs_request = self.wcs_requests.get(uuid=uuid)
241
        except WcsRequest.DoesNotExist:
242
            raise APIError("Cannot find intervention '%s'" % uuid)
243
        if wcs_request.status == 'updating':
244
            raise APIError(
245
                "'%s' intervention is still triggering on '%s'"
246
                % (wcs_request.wcs_form_number, wcs_request.trigger)
247
            )
248
        wcs_request.status = 'updating'
249
        wcs_request.trigger = trigger
250
        wcs_request.save()
251
        self.add_job(
252
            'update_intervention_job',
253
            wcs_form_number=wcs_request.wcs_form_number,
254
            trigger=trigger,
255
        )
256
        return {'err': 0}
257

  
258
    def update_intervention_job(self, *args, **kwargs):
259
        wcs_form_number = kwargs['wcs_form_number']
260
        trigger = kwargs['trigger']
261
        wcs_request = self.wcs_requests.get(wcs_form_number=wcs_form_number)
262
        base_url = '%sjump/trigger/%s' % (wcs_request.wcs_form_url, trigger)
263

  
264
        scheme, netloc, path, params, query, fragment = urlparse.urlparse(base_url)
265
        services = settings.KNOWN_SERVICES.get('wcs', {})
266
        service = None
267
        for service in services.values():
268
            remote_url = service.get('url')
269
            r_scheme, r_netloc, r_path, r_params, r_query, r_fragment = urlparse.urlparse(remote_url)
270
            if r_scheme == scheme and r_netloc == netloc:
271
                break
272
        else:
273
            wcs_request.status = 'failed'
274
            wcs_request.save()
275
            raise Exception('Cannot find wcs service for %s' % base_url)
276

  
277
        wcs_api = WcsApi(base_url, orig=service.get('orig'), key=service.get('secret'))
278
        headers = {
279
            'Content-Type': 'application/json',
280
            'Accept': 'application/json',
281
        }
282
        try:
283
            result = wcs_api.post_json({}, [], headers=headers)
284
        except WcsApiError as e:
285
            try:
286
                result = json.loads(e.args[3])
287
            except TypeError:
288
                raise SkipJob()  # transport error
289
        if result['err']:
290
            wcs_request.status = 'failed'
291
            wcs_request.save()
292
            raise Exception(result.get('err_class', 'wcs error'))
293
        wcs_request.status = 'updated'
294
        wcs_request.save()
295

  
224 296

  
225 297
class Cache(models.Model):
226 298
    resource = models.ForeignKey(
227 299
        verbose_name=_('Resource'),
228 300
        to=ToulouseSmartResource,
229 301
        on_delete=models.CASCADE,
230 302
        related_name='cache_entries',
231 303
    )
tests/test_toulouse_smart.py
24 24

  
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
from passerelle.base.models import Job
32 33
from passerelle.contrib.toulouse_smart.models import ToulouseSmartResource, WcsRequest
33 34

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

  
36 37

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

  
49 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

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

  
56 71
        def register(path, payload, content, status_code=200):
57 72
            @httmock.urlmatch(path=path)
......
440 455

  
441 456
    assert smart.wcs_requests.count() == 1
442 457
    wcs_request = smart.wcs_requests.get(uuid=UUID)
443 458
    assert wcs_request.status == 'received'
444 459

  
445 460
    # same uuid is re-used after an error
446 461
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
447 462
    assert smart.wcs_requests.count() == 1
463

  
464

  
465
WCS_RESPONSE_SUCCESS = '{"err": 0, "url": null}'
466
WCS_RESPONSE_ERROR = '{"err": 1, "err_class": "Access denied", "err_desc": null}'
467

  
468

  
469
@mock_response(
470
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
471
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')],
472
    ['/foo/2/jump/trigger/my-trigger-slug', None, WCS_RESPONSE_SUCCESS],
473
)
474
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
475
def test_update_intervention(mocked_uuid, app, freezer, smart, wcs_service):
476
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
477
    assert not resp.json['err']
478
    assert CREATE_INTERVENTION_QUERY['notificationUrl'] == 'update-intervention?uuid=%s' % str(UUID)
479

  
480
    freezer.move_to('2021-07-08 00:00:00')
481
    assert Job.objects.filter(method_name='update_intervention_job').count() == 0
482
    url = URL + 'update-intervention?uuid=%s&trigger=my-trigger-slug' % str(UUID)
483
    resp = app.post_json(url)
484
    assert not resp.json['err']
485
    assert Job.objects.filter(method_name='update_intervention_job').count() == 1
486
    job = Job.objects.get(method_name='update_intervention_job')
487
    assert job.status == 'registered'
488
    wcs_request = smart.wcs_requests.get(uuid=UUID)
489
    assert wcs_request.status == 'updating'
490
    assert wcs_request.trigger == 'my-trigger-slug'
491

  
492
    freezer.move_to('2021-07-08 00:00:03')
493
    smart.jobs()
494
    job = Job.objects.get(method_name='update_intervention_job')
495
    assert job.status == 'completed'
496
    wcs_request = smart.wcs_requests.get(uuid=UUID)
497
    assert wcs_request.status == 'updated'
498
    assert wcs_request.trigger == 'my-trigger-slug'
499

  
500

  
501
def test_update_intervention_wrong_uuid(app, smart):
502
    with pytest.raises(WcsRequest.DoesNotExist):
503
        smart.wcs_requests.get(uuid=UUID)
504

  
505
    url = URL + 'update-intervention?uuid=%s&trigger=my-trigger-slug' % str(UUID)
506
    resp = app.post_json(url)
507
    assert resp.json['err']
508
    assert 'Cannot find intervention' in resp.json['err_desc']
509

  
510

  
511
@mock_response(
512
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
513
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')],
514
)
515
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
516
def test_update_intervention_wrong_service(mocked_uuid, app, freezer, smart, wcs_service):
517
    wcs_service['default']['url'] = 'http://wrong.example.com'
518

  
519
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
520
    assert not resp.json['err']
521

  
522
    freezer.move_to('2021-07-08 00:00:00')
523
    url = URL + 'update-intervention?uuid=%s&trigger=my-trigger-slug' % str(UUID)
524
    resp = app.post_json(url)
525
    assert not resp.json['err']
526
    assert Job.objects.filter(method_name='update_intervention_job').count() == 1
527

  
528
    freezer.move_to('2021-07-08 00:00:03')
529
    smart.jobs()
530
    job = Job.objects.get(method_name='update_intervention_job')
531
    assert job.status == 'failed'
532
    assert 'Cannot find wcs service' in job.status_details['error_summary']
533
    wcs_request = smart.wcs_requests.get(uuid=UUID)
534
    assert wcs_request.status == 'failed'
535

  
536

  
537
@mock_response(
538
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
539
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')],
540
    ['/foo/2/jump/trigger/my-trigger-slug', None, WCS_RESPONSE_ERROR, 403],
541
)
542
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
543
def test_update_intervention_wrong_trigger(mocked_uuid, app, freezer, smart, wcs_service):
544
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
545
    assert not resp.json['err']
546

  
547
    freezer.move_to('2021-07-08 00:00:00')
548
    url = URL + 'update-intervention?uuid=%s&trigger=my-trigger-slug' % str(UUID)
549
    resp = app.post_json(url)
550
    assert not resp.json['err']
551

  
552
    freezer.move_to('2021-07-08 00:00:03')
553
    smart.jobs()
554
    job = Job.objects.get(method_name='update_intervention_job')
555
    assert job.status == 'failed'
556
    assert 'Access denied' in job.status_details['error_summary']
557
    wcs_request = smart.wcs_requests.get(uuid=UUID)
558
    assert wcs_request.status == 'failed'
559

  
560

  
561
@mock_response(
562
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
563
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')],
564
    ['/foo/2/jump/trigger/my-trigger-slug', None, None, 500],
565
)
566
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
567
def test_update_intervention_error_wcs(mocked_uuid, app, freezer, smart, wcs_service):
568
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
569
    assert not resp.json['err']
570

  
571
    freezer.move_to('2021-07-08 00:00:00')
572
    url = URL + 'update-intervention?uuid=%s&trigger=my-trigger-slug' % str(UUID)
573
    resp = app.post_json(url)
574
    assert not resp.json['err']
575

  
576
    freezer.move_to('2021-07-08 00:00:03')
577
    smart.jobs()
578
    job = Job.objects.get(method_name='update_intervention_job')
579
    assert job.status == 'registered'
580
    assert job.update_timestamp > job.creation_timestamp
581
    wcs_request = smart.wcs_requests.get(uuid=UUID)
582
    assert wcs_request.status == 'updating'
583

  
584

  
585
@mock_response(
586
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
587
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')],
588
    ['/foo/2/jump/trigger/trigger-1', None, None, 500],
589
)
590
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
591
def test_update_intervention_pending(mocked_uuid, app, freezer, smart, wcs_service):
592
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
593
    assert not resp.json['err']
594

  
595
    freezer.move_to('2021-07-08 00:00:00')
596
    url = URL + 'update-intervention?uuid=%s&trigger=trigger-1' % str(UUID)
597
    resp = app.post_json(url)
598
    assert not resp.json['err']
599

  
600
    freezer.move_to('2021-07-08 00:00:03')
601
    smart.jobs()
602
    job = Job.objects.get(method_name='update_intervention_job')
603
    assert job.status == 'registered'
604
    assert job.update_timestamp > job.creation_timestamp
605
    wcs_request = smart.wcs_requests.get(uuid=UUID)
606
    assert wcs_request.status == 'updating'
607

  
608
    url = URL + 'update-intervention?uuid=%s&trigger=trigger-2' % str(UUID)
609
    resp = app.post_json(url)
610
    assert resp.json['err']
611
    assert "still triggering on 'trigger-1'" in resp.json['err_desc']
448
-