Projet

Général

Profil

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

Nicolas Roche, 03 août 2021 19:03

Télécharger (30,6 ko)

Voir les différences:

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

 .../migrations/0002_auto_20210731_0031.py     |  56 ++++
 passerelle/contrib/toulouse_smart/models.py   | 148 ++++++++-
 passerelle/contrib/toulouse_smart/schemas.py  | 126 ++++++++
 .../toulouse_smart/create_intervention.json   |  54 ++++
 tests/test_toulouse_smart.py                  | 292 +++++++++++++++++-
 5 files changed, 674 insertions(+), 2 deletions(-)
 create mode 100644 passerelle/contrib/toulouse_smart/migrations/0002_auto_20210731_0031.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_20210731_0031.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.29 on 2021-07-30 22:31
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(default=dict)),
26
                ('result', django.contrib.postgres.fields.jsonb.JSONField(default=dict)),
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

  
......
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
        wcs_request = self.wcs_requests.get(wcs_form_number=wcs_form_number)
228
        return {
229
            'data': {
230
                'wcs_form_api_url': wcs_request.wcs_form_api_url,
231
                'wcs_form_number': wcs_request.wcs_form_number,
232
                'uuid': wcs_request.uuid,
233
                'payload': wcs_request.payload,
234
                'result': wcs_request.result,
235
                'status': wcs_request.status,
236
            }
237
        }
238

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

  
139 244

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

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

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

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

  
259

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

  
281
    def push(self):
282
        url = self.resource.webservice_base_url + 'v1/intervention'
283
        try:
284
            response = self.resource.request(url, json=self.payload)
285
        except APIError as e:
286
            self.result = str(e)
287
            self.save()
288
            return False
289
        try:
290
            self.result = response.json()
291
        except ValueError:
292
            err_desc = 'invalid json, got: %s' % response.text
293
            self.result = err_desc
294
            self.save()
295
            return False
296
        self.status = 'sent'
297
        self.save()
298
        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
26 30
from test_manager import login
27 31

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

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

  
32 37

  
33 38
@pytest.fixture
34 39
def smart(db):
35 40
    return utils.make_resource(
36 41
        ToulouseSmartResource,
......
48 53
        @httmock.urlmatch()
49 54
        def error(url, request):
50 55
            assert False, 'request to %s' % url.geturl()
51 56

  
52 57
        def register(path, payload, content, status_code=200):
53 58
            @httmock.urlmatch(path=path)
54 59
            def handler(url, request):
55 60
                if payload and json.loads(request.body) != payload:
61
                    import pdb
62

  
63
                    pdb.set_trace()
56 64
                    assert False, 'wrong payload sent to request to %s' % url.geturl()
57 65
                return httmock.response(status_code, content)
58 66

  
59 67
            return handler
60 68

  
61 69
        @functools.wraps(func)
62 70
        def wrapper(*args, **kwargs):
63 71
            handlers = []
......
73 81
    return decorator
74 82

  
75 83

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

  
80 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

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

  
85 98

  
86 99
INTERVENTION_TYPES = '''<List>
87 100
   <item>
88 101
       <id>1234</id>
......
232 245
@mock_response(
233 246
    ['/v1/intervention', None, None, 404],
234 247
)
235 248
def test_get_intervention_wrond_id(app, smart):
236 249
    resp = app.get(URL + 'get-intervention?id=3f0558bd-7d85-49a8-97e4-d07bc7f8dc9b')
237 250
    assert resp.json['err']
238 251
    assert 'failed to get' in resp.json['err_desc']
239 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, 500],
502
)
503
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
504
def test_create_intervention_inconsistency_id_error(mocked_uuid4, app, freezer, smart):
505
    freezer.move_to('2021-07-08 00:00:00')
506
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
507
    wcs_request = smart.wcs_requests.get(uuid=UUID)
508
    assert wcs_request.status == 'registered'
509
    job = Job.objects.get(method_name='create_intervention_job')
510
    assert job.status == 'registered'
511

  
512
    freezer.move_to('2021-07-08 00:00:03')
513
    wcs_request.delete()
514
    smart.jobs()
515
    job = Job.objects.get(method_name='create_intervention_job')
516
    assert job.status == 'failed'
517

  
518

  
519
@mock_response(
520
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
521
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, 'not json content'],
522
)
523
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
524
def test_create_intervention_content_error(mocked_uuid, app, freezer, smart):
525
    freezer.move_to('2021-07-08 00:00:00')
526
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
527
    wcs_request = smart.wcs_requests.get(uuid=UUID)
528
    assert wcs_request.status == 'registered'
529
    assert 'invalid json' in wcs_request.result
240
-