Projet

Général

Profil

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

Nicolas Roche, 31 juillet 2021 01:56

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   | 154 ++++++++-
 passerelle/contrib/toulouse_smart/schemas.py  | 126 ++++++++
 .../toulouse_smart/create_intervention.json   |  54 ++++
 tests/test_toulouse_smart.py                  | 293 +++++++++++++++++-
 5 files changed, 680 insertions(+), 3 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
22 23
from django.utils.text import slugify
23 24
from django.utils.timezone import now
24 25
from django.utils.translation import ugettext_lazy as _
25 26
from requests import RequestException
26 27

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

  
33
from . import schemas
34

  
35

  
36
class ToulouseSmartErrorRetry(APIError):
37
    pass
38

  
32 39

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

  
36 43
    webservice_base_url = models.URLField(_('Webservice Base URL'))
37 44

  
38 45
    log_requests_errors = False
39 46

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

  
124 131
    @endpoint(
125 132
        name='get-intervention',
126 133
        methods=['get'],
127 134
        description=_('Retrieve an intervention'),
128 135
        perm='can_access',
129 136
        parameters={
......
131 138
        },
132 139
    )
133 140
    def get_intervention(self, request, id):
134 141
        url = self.webservice_base_url + 'v1/intervention/%s' % id
135 142
        response = self.request(url)
136 143
        doc = ET.fromstring(response.content)
137 144
        return {'data': xml.to_json(doc)}
138 145

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

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

  
242
    def create_intervention_job(self, *args, **kwargs):
243
        wcs_request = self.wcs_requests.get(pk=kwargs['pk'])
244
        try:
245
            wcs_request.push()
246
        except ToulouseSmartErrorRetry:
247
            raise SkipJob()
248

  
139 249

  
140 250
class Cache(models.Model):
141 251
    resource = models.ForeignKey(
142 252
        verbose_name=_('Resource'),
143 253
        to=ToulouseSmartResource,
144 254
        on_delete=models.CASCADE,
145 255
        related_name='cache_entries',
146 256
    )
147 257

  
148 258
    key = models.CharField(_('Key'), max_length=64)
149 259

  
150 260
    timestamp = models.DateTimeField(_('Timestamp'), auto_now=True)
151 261

  
152 262
    value = JSONField(_('Value'), default=dict)
263

  
264

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

  
286
    def push(self):
287
        url = self.resource.webservice_base_url + 'v1/intervention'
288
        try:
289
            response = self.resource.request(url, json=self.payload)
290
        except ToulouseSmartErrorRetry as e:
291
            self.result = str(e)
292
            self.save()
293
            raise
294
        try:
295
            self.result = response.json()
296
        except ValueError:
297
            err_desc = 'invalid json, got: %s' % response.text
298
            self.result = err_desc
299
            self.save()
300
            raise ToulouseSmartErrorRetry(err_desc)
301
        self.status = 'sent'
302
        self.save()
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 (
34
    ToulouseSmartErrorRetry,
35
    ToulouseSmartResource,
36
    WcsRequest,
37
)
29 38

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

  
32 41

  
33 42
@pytest.fixture
34 43
def smart(db):
35 44
    return utils.make_resource(
36 45
        ToulouseSmartResource,
......
73 82
    return decorator
74 83

  
75 84

  
76 85
def get_xml_file(filename):
77 86
    with open(os.path.join(TEST_BASE_DIR, "%s.xml" % filename), 'rb') as desc:
78 87
        return desc.read()
79 88

  
80 89

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

  
94

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

  
85 99

  
86 100
INTERVENTION_TYPES = '''<List>
87 101
   <item>
88 102
       <id>1234</id>
......
227 241
@mock_response(
228 242
    ['/v1/intervention', None, None, 404],
229 243
)
230 244
def test_get_intervention_wrond_id(app, smart):
231 245
    resp = app.get(URL + 'get-intervention?id=3f0558bd-7d85-49a8-97e4-d07bc7f8dc9b')
232 246
    assert resp.json['err']
233 247
    assert 'failed to get' in resp.json['err_desc']
234 248
    assert '404' in resp.json['err_desc']
249

  
250

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

  
271

  
272
FIELDS_PAYLOAD = {
273
    'coin_raw': [
274
        {
275
            'field1': 'Candélabre',
276
            'field1_raw': 'Candélabre',
277
            'field2': '42',
278
        },
279
    ],
280
}
281

  
282

  
283
CREATE_INTERVENTION_PAYLOAD = {
284
    'fields': FIELDS_PAYLOAD,
285
    'extra': CREATE_INTERVENTION_PAYLOAD_EXTRA,
286
}
287

  
288
UUID = uuid.UUID('12345678123456781234567812345678')
289

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

  
310

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

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

  
332

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

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

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

  
368

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

  
376

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

  
383

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

  
394

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

  
405

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

  
416

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

  
436

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

  
455

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

  
465
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD, status=400)
466
    assert resp.json['err']
467
    assert 'already created' in resp.json['err_desc']
468

  
469

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

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

  
494

  
495
@mock_response(
496
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
497
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, None, 500],
498
)
499
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
500
def test_create_intervention_inconsistency_id_error(mocked_uuid4, app, freezer, smart):
501
    freezer.move_to('2021-07-08 00:00:00')
502
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
503
    wcs_request = smart.wcs_requests.get(uuid=UUID)
504
    assert wcs_request.status == 'registered'
505
    job = Job.objects.get(method_name='create_intervention_job')
506
    assert job.status == 'registered'
507

  
508
    freezer.move_to('2021-07-08 00:00:03')
509
    wcs_request.delete()
510
    smart.jobs()
511
    job = Job.objects.get(method_name='create_intervention_job')
512
    assert job.status == 'failed'
513

  
514

  
515
@mock_response(
516
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
517
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, 'not json content'],
518
)
519
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
520
def test_create_intervention_content_error(mocked_uuid, 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
    assert 'invalid json' in wcs_request.result
235
-