Projet

Général

Profil

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

Nicolas Roche, 02 août 2021 15:07

Télécharger (30,1 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   | 149 ++++++++-
 passerelle/contrib/toulouse_smart/schemas.py  | 126 ++++++++
 .../toulouse_smart/create_intervention.json   |  54 ++++
 tests/test_toulouse_smart.py                  | 289 +++++++++++++++++-
 5 files changed, 671 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
from django.db import models
22
from django.db import models, transaction
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

  
32 35

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

  
36 39
    webservice_base_url = models.URLField(_('Webservice Base URL'))
37 40

  
38 41
    log_requests_errors = False
39 42

  
......
131 134
        },
132 135
    )
133 136
    def get_intervention(self, request, id):
134 137
        url = self.webservice_base_url + 'v1/intervention/%s' % id
135 138
        response = self.request(url)
136 139
        doc = ET.fromstring(response.content)
137 140
        return {'data': xml.to_json(doc)}
138 141

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

  
180
            wcs_request, created = self.wcs_requests.update_or_create(
181
                defaults={
182
                    'wcs_form_api_url': post_data['form_api_url'],
183
                    'wcs_form_number': post_data['external_number'],
184
                }
185
            )
186
            if not created:
187
                raise APIError(
188
                    "'%s' intervention already created: '%s' status"
189
                    % (
190
                        post_data['external_number'],
191
                        wcs_request.status,
192
                    ),
193
                    http_status=400,
194
                )
195
            wcs_request = self.wcs_requests.get(wcs_form_number=wcs_form_number)
196
            wcs_request.payload = {
197
                'description': post_data['description'],
198
                'cityId': post_data['cityId'],
199
                'interventionCreated': post_data['interventionCreated'] + 'Z',
200
                'interventionDesired': post_data['interventionDesired'] + 'Z',
201
                'submitterFirstName': post_data['submitterFirstName'],
202
                'submitterLastName': post_data['submitterLastName'],
203
                'submitterMail': post_data['submitterMail'],
204
                'submitterPhone': post_data['submitterPhone'],
205
                'submitterAddress': post_data['submitterAddress'],
206
                'submitterType': post_data['submitterType'],
207
                'external_number': post_data['external_number'],
208
                'external_status': post_data['external_status'],
209
                'address': post_data['address'],
210
                'interventionData': data,
211
                'geom': {
212
                    'type': 'Point',
213
                    'coordinates': [post_data['lon'], post_data['lat']],
214
                    'crs': 'EPSG:4326',
215
                },
216
                'interventionTypeId': intervention_type['id'],
217
                'notificationUrl': 'update-intervention?uuid=%s' % wcs_request.uuid,
218
            }
219
            wcs_request.save()
220
            if not wcs_request.push():
221
                self.add_job(
222
                    'create_intervention_job',
223
                    pk=wcs_request.pk,
224
                    natural_id='wcs-request-%s' % wcs_request.pk,
225
                )
226
            wcs_request = self.wcs_requests.get(wcs_form_number=wcs_form_number)
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(default=dict)
270
    result = JSONField(default=dict)
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
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,
......
73 78
    return decorator
74 79

  
75 80

  
76 81
def get_xml_file(filename):
77 82
    with open(os.path.join(TEST_BASE_DIR, "%s.xml" % filename), 'rb') as desc:
78 83
        return desc.read()
79 84

  
80 85

  
86
def get_json_file(filename):
87
    with open(os.path.join(TEST_BASE_DIR, "%s.json" % filename)) as desc:
88
        return desc.read()
89

  
90

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

  
85 95

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

  
246

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

  
267

  
268
FIELDS_PAYLOAD = {
269
    'coin_raw': [
270
        {
271
            'field1': 'Candélabre',
272
            'field1_raw': 'Candélabre',
273
            'field2': '42',
274
        },
275
    ],
276
}
277

  
278

  
279
CREATE_INTERVENTION_PAYLOAD = {
280
    'fields': FIELDS_PAYLOAD,
281
    'extra': CREATE_INTERVENTION_PAYLOAD_EXTRA,
282
}
283

  
284
UUID = uuid.UUID('12345678123456781234567812345678')
285

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

  
306

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

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

  
328

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

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

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

  
364

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

  
372

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

  
379

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

  
390

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

  
401

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

  
412

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

  
432

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

  
451

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

  
461
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD, status=400)
462
    assert resp.json['err']
463
    assert 'already created' in resp.json['err_desc']
464

  
465

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

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

  
490

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

  
504
    freezer.move_to('2021-07-08 00:00:03')
505
    wcs_request.delete()
506
    smart.jobs()
507
    job = Job.objects.get(method_name='create_intervention_job')
508
    assert job.status == 'failed'
509

  
510

  
511
@mock_response(
512
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
513
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, 'not json content'],
514
)
515
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
516
def test_create_intervention_content_error(mocked_uuid, app, freezer, smart):
517
    freezer.move_to('2021-07-08 00:00:00')
518
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
519
    wcs_request = smart.wcs_requests.get(uuid=UUID)
520
    assert wcs_request.status == 'registered'
521
    assert 'invalid json' in wcs_request.result
235
-