Projet

Général

Profil

0005-toulouse_smart-add-create-intervention-endpoint-5523.patch

Nicolas Roche, 06 août 2021 13:00

Télécharger (31,8 ko)

Voir les différences:

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

 .../migrations/0002_auto_20210806_1242.py     |  56 ++++
 passerelle/contrib/toulouse_smart/models.py   | 159 ++++++++-
 passerelle/contrib/toulouse_smart/schemas.py  | 126 ++++++++
 .../toulouse_smart/create_intervention.json   |  54 ++++
 tests/test_toulouse_smart.py                  | 306 +++++++++++++++++-
 5 files changed, 693 insertions(+), 8 deletions(-)
 create mode 100644 passerelle/contrib/toulouse_smart/migrations/0002_auto_20210806_1242.py
 create mode 100644 passerelle/contrib/toulouse_smart/schemas.py
 create mode 100644 tests/data/toulouse_smart/create_intervention.json
passerelle/contrib/toulouse_smart/migrations/0002_auto_20210806_1242.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.29 on 2021-08-06 10:42
3
from __future__ import unicode_literals
4

  
5
import uuid
6

  
7
import django.contrib.postgres.fields.jsonb
8
import django.db.models.deletion
9
from django.db import migrations, models
10

  
11

  
12
class Migration(migrations.Migration):
13

  
14
    dependencies = [
15
        ('toulouse_smart', '0001_initial'),
16
    ]
17

  
18
    operations = [
19
        migrations.CreateModel(
20
            name='WcsRequest',
21
            fields=[
22
                ('wcs_form_api_url', models.CharField(max_length=256, primary_key=True, serialize=False)),
23
                ('wcs_form_number', models.CharField(max_length=16)),
24
                ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
25
                ('payload', django.contrib.postgres.fields.jsonb.JSONField(null=True)),
26
                ('result', django.contrib.postgres.fields.jsonb.JSONField(null=True)),
27
                (
28
                    'status',
29
                    models.CharField(
30
                        choices=[('registered', 'Registered'), ('sent', 'Sent')],
31
                        default='registered',
32
                        max_length=20,
33
                    ),
34
                ),
35
                (
36
                    'resource',
37
                    models.ForeignKey(
38
                        on_delete=django.db.models.deletion.CASCADE,
39
                        related_name='wcs_requests',
40
                        to='toulouse_smart.ToulouseSmartResource',
41
                        verbose_name='WcsRequest',
42
                    ),
43
                ),
44
            ],
45
        ),
46
        migrations.AlterField(
47
            model_name='cache',
48
            name='resource',
49
            field=models.ForeignKey(
50
                on_delete=django.db.models.deletion.CASCADE,
51
                related_name='cache_entries',
52
                to='toulouse_smart.ToulouseSmartResource',
53
                verbose_name='Resource',
54
            ),
55
        ),
56
    ]
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
from uuid import uuid4
18 19

  
19 20
import lxml.etree as ET
20 21
from django.contrib.postgres.fields import JSONField
21 22
from django.db import models
23
from django.db.transaction import atomic
24
from django.urls import reverse
22 25
from django.utils.text import slugify
23 26
from django.utils.timezone import now
24 27
from django.utils.translation import ugettext_lazy as _
25 28
from requests import RequestException
26 29

  
27
from passerelle.base.models import BaseResource, HTTPResource
30
from passerelle.base.models import BaseResource, HTTPResource, SkipJob
28 31
from passerelle.utils import xml
29 32
from passerelle.utils.api import endpoint
30 33
from passerelle.utils.jsonresponse import APIError
31 34

  
35
from . import schemas
36

  
32 37

  
33 38
class ToulouseSmartResource(BaseResource, HTTPResource):
34 39
    category = _('Business Process Connectors')
35 40

  
36 41
    webservice_base_url = models.URLField(_('Webservice Base URL'))
37 42

  
38 43
    log_requests_errors = False
39 44

  
......
104 109
                    {
105 110
                        'id': '',
106 111
                        'text': _('Service is unavailable'),
107 112
                        'disabled': True,
108 113
                    }
109 114
                ]
110 115
            }
111 116

  
112
    def request(self, url, json=None):
117
    def request(self, url, json=None, timeout=None):
113 118
        headers = {'Accept': 'application/json'}
114
        if json:
115
            headers['Content-Type'] = 'application/json'
116
            response = self.requests.post(url, headers=headers, json=json)
117
        else:
118
            response = self.requests.get(url, headers=headers)
119 119
        try:
120
            if json:
121
                headers['Content-Type'] = 'application/json'
122
                response = self.requests.post(url, headers=headers, timeout=timeout, json=json)
123
            else:
124
                response = self.requests.get(url, headers=headers, timeout=timeout)
120 125
            response.raise_for_status()
121 126
        except RequestException as e:
122 127
            raise APIError('failed to %s %s: %s' % ('post' if json else 'get', url, e))
123 128
        return response
124 129

  
125 130
    @endpoint(
126 131
        name='get-intervention',
127 132
        methods=['get'],
......
131 136
            'id': {'description': _('Intervention identifier')},
132 137
        },
133 138
    )
134 139
    def get_intervention(self, request, id):
135 140
        url = self.webservice_base_url + 'v1/intervention/%s' % id
136 141
        response = self.request(url)
137 142
        return {'data': response.json()}
138 143

  
144
    @atomic
145
    @endpoint(
146
        name='create-intervention',
147
        methods=['post'],
148
        description=_('Create an intervention'),
149
        perm='can_access',
150
        post={'request_body': {'schema': {'application/json': schemas.CREATE_SCHEMA}}},
151
    )
152
    def create_intervention(self, request, post_data):
153
        slug = post_data['slug']
154
        wcs_form_number = post_data['external_number']
155
        try:
156
            types = [x for x in self.get_intervention_types() if slugify(x['name']) == slug]
157
        except KeyError:
158
            raise APIError('Service is unavailable')
159
        if len(types) == 0:
160
            raise APIError("unknown '%s' block slug" % slug, http_status=400)
161
        intervention_type = types[0]
162
        wcs_block_varname = slugify(intervention_type['name']).replace('-', '_')
163
        try:
164
            block = post_data['fields']['%s_raw' % wcs_block_varname][0]
165
        except:
166
            raise APIError("cannot find '%s' block field content" % slug, http_status=400)
167
        data = {}
168
        cast = {'string': str, 'int': int, 'boolean': bool, 'item': str}
169
        for prop in intervention_type['properties']:
170
            name = prop['name'].lower()
171
            if block.get(name):
172
                try:
173
                    data[prop['name']] = cast[prop['type']](block[name])
174
                except ValueError:
175
                    raise APIError(
176
                        "cannot cast '%s' field to %s : '%s'" % (name, cast[prop['type']], block[name]),
177
                        http_status=400,
178
                    )
179
            elif prop['required']:
180
                raise APIError("'%s' field is required on '%s' block" % (name, slug), http_status=400)
181

  
182
        if self.wcs_requests.filter(wcs_form_api_url=post_data['form_api_url']):
183
            raise APIError(
184
                "'%s' intervention already created" % post_data['external_number'],
185
                http_status=400,
186
            )
187
        wcs_request = self.wcs_requests.create(
188
            wcs_form_api_url=post_data['form_api_url'],
189
            wcs_form_number=post_data['external_number'],
190
        )
191
        update_intervention_endpoint_url = request.build_absolute_uri(
192
            reverse(
193
                'generic-endpoint',
194
                kwargs={'connector': 'toulouse-smart', 'endpoint': 'update-intervention', 'slug': self.slug},
195
            )
196
        )
197
        wcs_request.payload = {
198
            'description': post_data['description'],
199
            'cityId': post_data['cityId'],
200
            'interventionCreated': post_data['interventionCreated'] + 'Z',
201
            'interventionDesired': post_data['interventionDesired'] + 'Z',
202
            'submitterFirstName': post_data['submitterFirstName'],
203
            'submitterLastName': post_data['submitterLastName'],
204
            'submitterMail': post_data['submitterMail'],
205
            'submitterPhone': post_data['submitterPhone'],
206
            'submitterAddress': post_data['submitterAddress'],
207
            'submitterType': post_data['submitterType'],
208
            'external_number': post_data['external_number'],
209
            'external_status': post_data['external_status'],
210
            'address': post_data['address'],
211
            'interventionData': data,
212
            'geom': {
213
                'type': 'Point',
214
                'coordinates': [post_data['lon'], post_data['lat']],
215
                'crs': 'EPSG:4326',
216
            },
217
            'interventionTypeId': intervention_type['id'],
218
            'notificationUrl': '%s?uuid=%s' % (update_intervention_endpoint_url, wcs_request.uuid),
219
        }
220
        wcs_request.save()
221
        if not wcs_request.push():
222
            self.add_job(
223
                'create_intervention_job',
224
                pk=wcs_request.pk,
225
                natural_id='wcs-request-%s' % wcs_request.pk,
226
            )
227
        return {
228
            'data': {
229
                'wcs_form_api_url': wcs_request.wcs_form_api_url,
230
                'wcs_form_number': wcs_request.wcs_form_number,
231
                'uuid': wcs_request.uuid,
232
                'payload': wcs_request.payload,
233
                'result': wcs_request.result,
234
                'status': wcs_request.status,
235
            }
236
        }
237

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

  
139 243

  
140 244
class Cache(models.Model):
141 245
    resource = models.ForeignKey(
142 246
        verbose_name=_('Resource'),
143 247
        to=ToulouseSmartResource,
144 248
        on_delete=models.CASCADE,
145 249
        related_name='cache_entries',
146 250
    )
147 251

  
148 252
    key = models.CharField(_('Key'), max_length=64)
149 253

  
150 254
    timestamp = models.DateTimeField(_('Timestamp'), auto_now=True)
151 255

  
152 256
    value = JSONField(_('Value'), default=dict)
257

  
258

  
259
class WcsRequest(models.Model):
260
    resource = models.ForeignKey(
261
        verbose_name=_('WcsRequest'),
262
        to=ToulouseSmartResource,
263
        on_delete=models.CASCADE,
264
        related_name='wcs_requests',
265
    )
266
    wcs_form_api_url = models.CharField(max_length=256, primary_key=True)
267
    wcs_form_number = models.CharField(max_length=16)
268
    uuid = models.UUIDField(default=uuid4, unique=True, editable=False)
269
    payload = JSONField(null=True)
270
    result = JSONField(null=True)
271
    status = models.CharField(
272
        max_length=20,
273
        default='registered',
274
        choices=(
275
            ('registered', _('Registered')),
276
            ('sent', _('Sent')),
277
        ),
278
    )
279

  
280
    def push(self):
281
        url = self.resource.webservice_base_url + 'v1/intervention'
282
        try:
283
            response = self.resource.request(url, json=self.payload)
284
        except APIError as e:
285
            self.result = str(e)
286
            self.save()
287
            return False
288
        try:
289
            self.result = response.json()
290
        except ValueError:
291
            err_desc = 'invalid json, got: %s' % response.text
292
            self.result = err_desc
293
            self.save()
294
            return False
295
        self.status = 'sent'
296
        self.save()
297
        return True
passerelle/contrib/toulouse_smart/schemas.py
1
# passerelle - uniform access to multiple data sources and services
2
# Copyright (C) 2021 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

  
18
CREATE_SCHEMA = {
19
    '$schema': 'http://json-schema.org/draft-04/schema#',
20
    'type': 'object',
21
    'properties': {
22
        'slug': {
23
            'description': "slug du block de champs intervention",
24
            'type': 'string',
25
        },
26
        'description': {
27
            'description': "Description de la demande",
28
            'type': 'string',
29
        },
30
        'lat': {
31
            'description': 'Latitude',
32
            'type': 'number',
33
        },
34
        'lon': {
35
            'description': 'Longitude',
36
            'type': 'number',
37
        },
38
        'cityId': {
39
            'description': 'Code INSEE de la commune',
40
            'type': 'string',
41
        },
42
        'safeguardRequired': {
43
            'description': 'Présence d’un danger ?',
44
            'type': 'boolean',
45
        },
46
        'interventionCreated': {
47
            'description': 'Date de création de la demande',
48
            'type': 'string',
49
            'pattern': '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}',
50
        },
51
        'interventionDesired': {
52
            'description': 'Date d’intervention souhaitée',
53
            'type': 'string',
54
            'pattern': '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}',
55
        },
56
        'submitterFirstName': {
57
            'description': 'Prénom du demandeur',
58
            'type': 'string',
59
        },
60
        'submitterLastName': {
61
            'description': 'Nom du demandeur',
62
            'type': 'string',
63
        },
64
        'submitterMail': {
65
            'description': 'Adresse mail du demandeur',
66
            'type': 'string',
67
        },
68
        'submitterPhone': {
69
            'description': 'Numéro de téléphone du demandeur',
70
            'type': 'string',
71
        },
72
        'submitterAddress': {
73
            'description': 'Adresse du demandeur',
74
            'type': 'string',
75
        },
76
        'submitterType': {
77
            'description': 'Type de demandeur (Commune, élu, usager…)',
78
            'type': 'string',
79
        },
80
        'onPrivateLand': {
81
            'description': 'Intervention sur le domaine privé ?',
82
            'type': 'boolean',
83
        },
84
        'checkDuplicated': {
85
            'description': 'Activation de la détection de doublon (laisser à false pour le moment)',
86
            'type': 'boolean',
87
        },
88
        'external_number': {
89
            'description': 'Numéro externe de la demande (numéro Publik : {{ form_number }})',
90
            'type': 'string',
91
            'pattern': '[0-9]+[0-9]+',
92
        },
93
        'external_status': {
94
            'description': 'Statut externe de la demande (statut Publik : {{ form_status }})',
95
            'type': 'string',
96
        },
97
        'address': {
98
            'description': 'Adresse de la demande (démarche dans Publik : {{ form_backoffice_url }})',
99
            'type': 'string',
100
        },
101
        'form_api_url': {
102
            'description': "L'adresse vers la vue API du formulaire : {{ form_api_url }}",
103
            'type': 'string',
104
        },
105
    },
106
    'required': [
107
        'slug',
108
        'description',
109
        'lat',
110
        'lon',
111
        'cityId',
112
        'interventionCreated',
113
        'interventionDesired',
114
        'submitterFirstName',
115
        'submitterLastName',
116
        'submitterMail',
117
        'submitterPhone',
118
        'submitterAddress',
119
        'submitterType',
120
        'external_number',
121
        'external_status',
122
        'address',
123
        'form_api_url',
124
    ],
125
    'merge_extra': True,
126
}
tests/data/toulouse_smart/create_intervention.json
1
{
2
  "id": "96bc8712-21a6-4fba-a511-740d6f1bd0bc",
3
  "name": "DI-20210707-0031",
4
  "description": "truc",
5
  "geom": {
6
    "type": "Point",
7
    "coordinates": [
8
      4.8546695709228525,
9
      45.75129501117154
10
    ],
11
    "crs": "EPSG:3943"
12
  },
13
  "interventionData": {},
14
  "safeguardRequired": false,
15
  "media": null,
16
  "safeguardDone": null,
17
  "interventionCreated": "2021-07-07T12:19:31.302Z",
18
  "interventionDesired": "2021-06-30T16:08:05Z",
19
  "interventionDone": null,
20
  "submitter": {
21
    "id": null,
22
    "lastName": "admin",
23
    "firstName": "admin12",
24
    "mail": "admin@localhost",
25
    "furtherInformation": null,
26
    "phoneNumber": "0699999999",
27
    "address": ""
28
  },
29
  "submitterType": "\u00e9lu",
30
  "organizations": [
31
    {
32
      "id": "f1378d8a-12bf-4c14-913f-22624b0ecab8",
33
      "name": "Direction des P\u00f4les"
34
    },
35
    {
36
      "id": "8ad4af63-70b5-416f-a75d-c510d83ce1bd",
37
      "name": "Transport Logistique"
38
    }
39
  ],
40
  "domain": null,
41
  "state": {
42
    "id": "e844e67f-5382-4c0f-94d8-56f618263485",
43
    "table": null,
44
    "stateLabel": "Nouveau",
45
    "closes": false
46
  },
47
  "interventionTypeId": "f72d370c-4d25-489f-956e-2a0d48433789",
48
  "onPrivateLand": false,
49
  "duplicates": null,
50
  "external_number": "174-4",
51
  "external_status": "statut1",
52
  "cityId": "12345",
53
  "address": "https://wcs.example.com/backoffice/management/foo/2/"
54
}
tests/test_toulouse_smart.py
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 functools
18 18
import io
19
import json
19 20
import os
21
import uuid
20 22
import zipfile
23
from copy import deepcopy
21 24

  
22 25
import httmock
23 26
import lxml.etree as ET
27
import mock
24 28
import pytest
25 29
import utils
30
from requests.exceptions import ReadTimeout
26 31
from test_manager import login
27 32

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

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

  
32 38

  
33 39
@pytest.fixture
34 40
def smart(db):
35 41
    return utils.make_resource(
36 42
        ToulouseSmartResource,
......
75 81
    return decorator
76 82

  
77 83

  
78 84
def get_json_file(filename):
79 85
    with open(os.path.join(TEST_BASE_DIR, "%s.json" % filename)) as desc:
80 86
        return desc.read()
81 87

  
82 88

  
89
def get_json_file(filename):
90
    with open(os.path.join(TEST_BASE_DIR, "%s.json" % filename)) as desc:
91
        return desc.read()
92

  
93

  
83 94
@mock_response(['/v1/type-intervention', None, b'<List></List>'])
84 95
def test_empty_intervention_types(smart):
85 96
    assert smart.get_intervention_types() == []
86 97

  
87 98

  
88 99
INTERVENTION_TYPES = '''<List>
89 100
   <item>
90 101
       <id>1234</id>
......
234 245
@mock_response(
235 246
    ['/v1/intervention', None, None, 404],
236 247
)
237 248
def test_get_intervention_wrond_id(app, smart):
238 249
    resp = app.get(URL + 'get-intervention?id=3f0558bd-7d85-49a8-97e4-d07bc7f8dc9b')
239 250
    assert resp.json['err']
240 251
    assert 'failed to get' in resp.json['err_desc']
241 252
    assert '404' in resp.json['err_desc']
253

  
254

  
255
CREATE_INTERVENTION_PAYLOAD_EXTRA = {
256
    'slug': 'coin',
257
    'description': 'coin coin',
258
    'lat': 48.833708,
259
    'lon': 2.323349,
260
    'cityId': '12345',
261
    'interventionCreated': '2021-06-30T16:08:05',
262
    'interventionDesired': '2021-06-30T16:08:05',
263
    'submitterFirstName': 'John',
264
    'submitterLastName': 'Doe',
265
    'submitterMail': 'john.doe@example.com',
266
    'submitterPhone': '0123456789',
267
    'submitterAddress': '3 rue des champs de blés',
268
    'submitterType': 'usager',
269
    'external_number': '42-2',
270
    'external_status': 'statut-1-wcs',
271
    'address': 'https://wcs.example.com/backoffice/management/foo/2/',
272
    'form_api_url': 'https://wcs.example.com/api/forms/foo/2/',
273
}
274

  
275

  
276
FIELDS_PAYLOAD = {
277
    'coin_raw': [
278
        {
279
            'field1': 'Candélabre',
280
            'field1_raw': 'Candélabre',
281
            'field2': '42',
282
        },
283
    ],
284
}
285

  
286

  
287
CREATE_INTERVENTION_PAYLOAD = {
288
    'fields': FIELDS_PAYLOAD,
289
    'extra': CREATE_INTERVENTION_PAYLOAD_EXTRA,
290
}
291

  
292
UUID = uuid.UUID('12345678123456781234567812345678')
293

  
294
CREATE_INTERVENTION_QUERY = {
295
    'description': 'coin coin',
296
    'cityId': '12345',
297
    'interventionCreated': '2021-06-30T16:08:05Z',
298
    'interventionDesired': '2021-06-30T16:08:05Z',
299
    'submitterFirstName': 'John',
300
    'submitterLastName': 'Doe',
301
    'submitterMail': 'john.doe@example.com',
302
    'submitterPhone': '0123456789',
303
    'submitterAddress': '3 rue des champs de bl\u00e9s',
304
    'submitterType': 'usager',
305
    'external_number': '42-2',
306
    'external_status': 'statut-1-wcs',
307
    'address': 'https://wcs.example.com/backoffice/management/foo/2/',
308
    'interventionData': {'FIELD1': 'Candélabre', 'FIELD2': 42},
309
    'geom': {'type': 'Point', 'coordinates': [2.323349, 48.833708], 'crs': 'EPSG:4326'},
310
    'interventionTypeId': '1234',
311
    'notificationUrl': 'http://testserver/toulouse-smart/test/update-intervention?uuid=%s' % str(UUID),
312
}
313

  
314

  
315
@mock_response(
316
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
317
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')],
318
)
319
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
320
def test_create_intervention(mocked_uuid4, app, smart):
321
    with pytest.raises(WcsRequest.DoesNotExist):
322
        smart.wcs_requests.get(uuid=UUID)
323

  
324
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
325
    assert str(UUID) in CREATE_INTERVENTION_QUERY['notificationUrl']
326
    assert not resp.json['err']
327
    assert resp.json['data']['uuid'] == str(UUID)
328
    assert resp.json['data']['wcs_form_api_url'] == 'https://wcs.example.com/api/forms/foo/2/'
329
    wcs_request = smart.wcs_requests.get(uuid=UUID)
330
    assert wcs_request.wcs_form_api_url == 'https://wcs.example.com/api/forms/foo/2/'
331
    assert wcs_request.wcs_form_number == '42-2'
332
    assert wcs_request.payload == CREATE_INTERVENTION_QUERY
333
    assert wcs_request.result == json.loads(get_json_file('create_intervention'))
334
    assert wcs_request.status == 'sent'
335

  
336

  
337
@mock_response(
338
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
339
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')],
340
)
341
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
342
def test_create_intervention_async(mocked_uuid4, app, smart):
343
    mocked_push = mock.patch(
344
        "passerelle.contrib.toulouse_smart.models.WcsRequest.push",
345
        return_value=False,
346
    )
347
    mocked_push.start()
348

  
349
    assert Job.objects.count() == 0
350
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
351
    assert str(UUID) in CREATE_INTERVENTION_QUERY['notificationUrl']
352
    assert not resp.json['err']
353
    assert resp.json['data']['uuid'] == str(UUID)
354
    assert resp.json['data']['wcs_form_api_url'] == 'https://wcs.example.com/api/forms/foo/2/'
355
    wcs_request = smart.wcs_requests.get(uuid=UUID)
356
    assert wcs_request.wcs_form_api_url == 'https://wcs.example.com/api/forms/foo/2/'
357
    assert wcs_request.wcs_form_number == '42-2'
358
    assert wcs_request.payload == CREATE_INTERVENTION_QUERY
359
    assert wcs_request.status == 'registered'
360

  
361
    mocked_push.stop()
362
    assert Job.objects.count() == 1
363
    job = Job.objects.get(method_name='create_intervention_job')
364
    assert job.status == 'registered'
365
    smart.jobs()
366
    job = Job.objects.get(method_name='create_intervention_job')
367
    assert job.status == 'completed'
368
    wcs_request = smart.wcs_requests.get(uuid=UUID)
369
    assert wcs_request.result == json.loads(get_json_file('create_intervention'))
370
    assert wcs_request.status == 'sent'
371

  
372

  
373
def test_create_intervention_wrong_payload(app, smart):
374
    payload = deepcopy(CREATE_INTERVENTION_PAYLOAD)
375
    del payload['extra']['slug']
376
    resp = app.post_json(URL + 'create-intervention/', params=payload, status=400)
377
    assert resp.json['err']
378
    assert "'slug' is a required property" in resp.json['err_desc']
379

  
380

  
381
@mock_response()
382
def test_create_intervention_types_unavailable(app, smart):
383
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
384
    assert resp.json['err']
385
    assert 'Service is unavailable' in resp.json['err_desc']
386

  
387

  
388
@mock_response(
389
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
390
)
391
def test_create_intervention_wrong_block_slug(app, smart):
392
    payload = deepcopy(CREATE_INTERVENTION_PAYLOAD)
393
    payload['extra']['slug'] = 'coin-coin'
394
    resp = app.post_json(URL + 'create-intervention/', params=payload, status=400)
395
    assert resp.json['err']
396
    assert "unknown 'coin-coin' block slug" in resp.json['err_desc']
397

  
398

  
399
@mock_response(
400
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
401
)
402
def test_create_intervention_no_block(app, smart):
403
    payload = deepcopy(CREATE_INTERVENTION_PAYLOAD)
404
    del payload['fields']['coin_raw']
405
    resp = app.post_json(URL + 'create-intervention/', params=payload, status=400)
406
    assert resp.json['err']
407
    assert "cannot find 'coin' block field content" in resp.json['err_desc']
408

  
409

  
410
@mock_response(
411
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
412
)
413
def test_create_intervention_cast_error(app, smart):
414
    payload = deepcopy(CREATE_INTERVENTION_PAYLOAD)
415
    payload['fields']['coin_raw'][0]['field2'] = 'not-an-integer'
416
    resp = app.post_json(URL + 'create-intervention/', params=payload, status=400)
417
    assert resp.json['err']
418
    assert "cannot cast 'field2' field to <class 'int'>" in resp.json['err_desc']
419

  
420

  
421
@mock_response(
422
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
423
)
424
def test_create_intervention_missing_value(app, smart):
425
    field_payload = {
426
        'coin_raw': [
427
            {
428
                'field1': 'Candélabre',
429
                'field1_raw': 'Candélabre',
430
                'field2': None,
431
            },
432
        ],
433
    }
434
    payload = deepcopy(CREATE_INTERVENTION_PAYLOAD)
435
    payload['fields'] = field_payload
436
    resp = app.post_json(URL + 'create-intervention/', params=payload, status=400)
437
    assert resp.json['err']
438
    assert "field is required on 'coin' block" in resp.json['err_desc']
439

  
440

  
441
@mock_response(
442
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
443
)
444
def test_create_intervention_missing_field(app, smart):
445
    field_payload = {
446
        'coin_raw': [
447
            {
448
                'field1': 'Candélabre',
449
                'field1_raw': 'Candélabre',
450
            },
451
        ],
452
    }
453
    payload = deepcopy(CREATE_INTERVENTION_PAYLOAD)
454
    payload['fields'] = field_payload
455
    resp = app.post_json(URL + 'create-intervention/', params=payload, status=400)
456
    assert resp.json['err']
457
    assert "field is required on 'coin' block" in resp.json['err_desc']
458

  
459

  
460
@mock_response(
461
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
462
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, None, 500],
463
)
464
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
465
def test_create_intervention_twice_error(mocked_uuid4, app, smart):
466
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
467
    assert not resp.json['err']
468

  
469
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD, status=400)
470
    assert resp.json['err']
471
    assert 'already created' in resp.json['err_desc']
472

  
473

  
474
@mock_response(
475
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
476
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, None, 500],
477
)
478
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
479
def test_create_intervention_transport_error(mocked_uuid, app, freezer, smart):
480
    freezer.move_to('2021-07-08 00:00:00')
481
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
482
    assert not resp.json['err']
483
    job = Job.objects.get(method_name='create_intervention_job')
484
    assert job.status == 'registered'
485
    wcs_request = smart.wcs_requests.get(uuid=UUID)
486
    assert wcs_request.status == 'registered'
487
    assert 'failed to post' in wcs_request.result
488

  
489
    freezer.move_to('2021-07-08 00:00:03')
490
    smart.jobs()
491
    job = Job.objects.get(method_name='create_intervention_job')
492
    assert job.status == 'registered'
493
    assert job.update_timestamp > job.creation_timestamp
494
    wcs_request = smart.wcs_requests.get(uuid=UUID)
495
    assert wcs_request.status == 'registered'
496
    assert 'failed to post' in wcs_request.result
497

  
498

  
499
@mock_response(
500
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
501
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, None, None, ReadTimeout('timeout')],
502
)
503
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
504
def test_create_intervention_timeout_error(mocked_uuid, app, smart):
505
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
506
    assert not resp.json['err']
507
    job = Job.objects.get(method_name='create_intervention_job')
508
    assert job.status == 'registered'
509
    wcs_request = smart.wcs_requests.get(uuid=UUID)
510
    assert wcs_request.status == 'registered'
511
    assert 'failed to post' in wcs_request.result
512
    assert 'timeout' in wcs_request.result
513

  
514

  
515
@mock_response(
516
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
517
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, None, 500],
518
)
519
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
520
def test_create_intervention_inconsistency_id_error(mocked_uuid4, app, freezer, smart):
521
    freezer.move_to('2021-07-08 00:00:00')
522
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
523
    wcs_request = smart.wcs_requests.get(uuid=UUID)
524
    assert wcs_request.status == 'registered'
525
    job = Job.objects.get(method_name='create_intervention_job')
526
    assert job.status == 'registered'
527

  
528
    freezer.move_to('2021-07-08 00:00:03')
529
    wcs_request.delete()
530
    smart.jobs()
531
    job = Job.objects.get(method_name='create_intervention_job')
532
    assert job.status == 'failed'
533

  
534

  
535
@mock_response(
536
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
537
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, 'not json content'],
538
)
539
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
540
def test_create_intervention_content_error(mocked_uuid, app, freezer, smart):
541
    freezer.move_to('2021-07-08 00:00:00')
542
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
543
    wcs_request = smart.wcs_requests.get(uuid=UUID)
544
    assert wcs_request.status == 'registered'
545
    assert 'invalid json' in wcs_request.result
242
-