# passerelle - uniform access to multiple data sources and services # Copyright (C) 2021 Entr'ouvert # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import base64 import cgi # noqa pylint: disable=deprecated-module import functools import io import json import os import uuid import zipfile from copy import deepcopy from unittest import mock import httmock import lxml.etree as ET import pytest from django.utils.encoding import force_str from requests.exceptions import ConnectionError, ReadTimeout import tests.utils from passerelle.base.models import Job from passerelle.contrib.toulouse_smart.models import ( SmartRequest, ToulouseSmartResource, WcsRequest, WcsRequestFile, ) from passerelle.contrib.toulouse_smart.utils import localtz_to_utc, utc_to_localtz from tests.test_manager import login TEST_BASE_DIR = os.path.join(os.path.dirname(__file__), 'data', 'toulouse_smart') @pytest.fixture def smart(db, settings): settings.TIME_ZONE = 'Europe/Paris' settings.USE_TZ = True return tests.utils.make_resource( ToulouseSmartResource, title='Test', slug='test', description='Test', webservice_base_url='https://smart.example.com/', basic_auth_username='username', basic_auth_password='password', ) @pytest.fixture def wcs_service(settings): wcs_service = { 'default': { 'title': 'test', 'url': 'https://wcs.example.com', 'secret': 'xxx', 'orig': 'passerelle', }, } settings.KNOWN_SERVICES = {'wcs': wcs_service} return wcs_service def mock_response(*path_contents): def decorator(func): @httmock.urlmatch() def error(url, request): assert False, 'request to %s' % url.geturl() def register(path, payload, content, status_code=200, exception=None): @httmock.urlmatch(path=path) def handler(url, request): if payload: ctype, pdict = cgi.parse_header(request.headers['content-type']) if ctype == 'multipart/form-data': # here payload is an expected multipart contents list pdict['boundary'] = bytes(pdict['boundary'], 'utf-8') pdict['CONTENT-LENGTH'] = request.headers['Content-Length'] postvars = cgi.parse_multipart(io.BytesIO(request.body), pdict) for i, media_content in enumerate(postvars['media']): assert media_content == payload[i], ( 'wrong multipart content sent to %s' % url.geturl() ) else: assert json.loads(request.body) == payload, ( 'wrong payload sent to request to %s' % url.geturl() ) if exception: raise exception return httmock.response(status_code, content) return handler @functools.wraps(func) def wrapper(*args, **kwargs): handlers = [] for row in path_contents: handlers.append(register(*row)) handlers.append(error) with httmock.HTTMock(*handlers): return func(*args, **kwargs) return wrapper return decorator def get_json_file(filename): with open(os.path.join(TEST_BASE_DIR, '%s.json' % filename)) as desc: return desc.read() def get_media_file(filename): with open(os.path.join(TEST_BASE_DIR, '%s' % filename), 'rb') as desc: return desc.read() def test_save_daylight_time_change(settings): settings.TIME_ZONE = 'Europe/Paris' settings.USE_TZ = True assert localtz_to_utc('2022-10-30T02:19:48') == '2022-10-30T01:19:48+00:00' assert utc_to_localtz('2022-10-30T01:19:48+00:00') == '2022-10-30T02:19:48' @mock_response(['/v1/type-intervention', None, b'']) def test_empty_intervention_types(smart): assert smart.get_intervention_types() == [] INTERVENTION_TYPES = ''' 1234 coin TYPE-OBJET Champ 1 string false Ne sait pas Candélabre Mât Ne sait pas FIELD2 Champ 2 int true IGNORED-FIELD-HAVING-NO-TYPE Champ 3 IGNORED-FIELD-HAVING-UNKNOWN-TYPE Champ 3 plop 0002 empty '''.encode() @mock_response(['/v1/type-intervention', None, INTERVENTION_TYPES]) def test_model_intervention_types(smart): assert smart.get_intervention_types() == [ { 'id': '1234', 'name': 'coin', 'order': 1, 'properties': [ { 'name': 'TYPE-OBJET', 'displayName': 'Champ 1', 'required': False, 'type': 'item', 'defaultValue': 'Ne sait pas', 'restrictedValues': ['Candélabre', 'Mât', 'Ne sait pas'], }, {'name': 'FIELD2', 'displayName': 'Champ 2', 'required': True, 'type': 'int'}, ], }, { 'id': '0002', 'name': 'empty', 'order': 2, }, ] URL = '/toulouse-smart/test/' @mock_response(['/v1/type-intervention', None, INTERVENTION_TYPES]) def test_endpoint_intervention_types(app, smart): resp = app.get(URL + 'type-intervention') assert resp.json == { 'data': [ {'id': 'coin', 'text': 'coin', 'uuid': '1234'}, {'id': 'empty', 'text': 'empty', 'uuid': '0002'}, ], 'err': 0, } @mock_response() def test_endpoint_intervention_types_unavailable(app, smart): resp = app.get(URL + 'type-intervention') assert resp.json == {'data': [{'id': '', 'text': 'Service is unavailable', 'disabled': True}], 'err': 0} @mock_response(['/v1/type-intervention', None, INTERVENTION_TYPES]) def test_manage_intervention_types(app, smart, admin_user): login(app) resp = app.get('/manage' + URL + 'type-intervention/') assert [[td.text for td in tr.cssselect('td,th')] for tr in resp.pyquery('tr')] == [ ["Nom du type d'intervention", 'Nom', 'Type', 'Requis', 'Valeur par défaut'], ['1 - coin'], [None, 'TYPE-OBJET', 'item («Candélabre», «Mât», «Ne sait pas»)', '✘', 'Ne sait pas'], [None, 'FIELD2', 'int', '✔', None], ['2 - empty'], ] resp = resp.click('Export to blocks') with zipfile.ZipFile(io.BytesIO(resp.body)) as zip_file: assert zip_file.namelist() == ['block-coin.wcs'] with zip_file.open('block-coin.wcs') as fd: content = ET.tostring(ET.fromstring(fd.read()), pretty_print=True).decode() assert ( content == ''' coin coin 522697a9-de01-b198-9e37-58c35718203a item False type_objet validation summary Candélabre Mât Ne sait pas e72f251a-5eef-5b78-c35a-94b549510029 string True field2 validation summary digits ''' ) INTERVENTION_ID = json.loads(get_json_file('create_intervention'))['id'] @mock_response( ['/v1/intervention', None, get_json_file('create_intervention')], ) def test_get_intervention(app, smart): resp = app.get(URL + 'get-intervention?id=%s' % INTERVENTION_ID) assert not resp.json['err'] assert resp.json['data']['id'] == INTERVENTION_ID assert resp.json['data']['state'] == { 'id': 'e844e67f-5382-4c0f-94d8-56f618263485', 'table': None, 'stateLabel': 'Nouveau', 'closes': False, } assert resp.json['data']['interventionCreated'] == '2021-07-07T14:19:31.302000' assert resp.json['data']['interventionDesired'] == '2021-06-30T18:08:05' @mock_response( ['/v1/intervention', None, None, 500], ) def test_get_intervention_error_status(app, smart): resp = app.get(URL + 'get-intervention?id=%s' % INTERVENTION_ID) assert resp.json['err'] assert 'failed to get' in resp.json['err_desc'] @mock_response( ['/v1/intervention', None, None, 404], ) def test_get_intervention_wrond_id(app, smart): resp = app.get(URL + 'get-intervention?id=%s' % INTERVENTION_ID) assert resp.json['err'] assert 'failed to get' in resp.json['err_desc'] assert '404' in resp.json['err_desc'] CREATE_INTERVENTION_PAYLOAD_EXTRA = { 'slug': 'coin', 'description': 'coin coin', 'lat': 48.833708, 'lon': 2.323349, 'cityId': '12345', 'interventionCreated': '2021-06-30T18:08:05.500931+02:00', 'interventionDesired': '2021-06-30T18:08:05.500931+02:00', 'submitterFirstName': 'John', 'submitterLastName': 'Doe', 'submitterMail': 'john.doe@example.com', 'submitterPhone': '0123456789', 'submitterAddress': '3 rue des champs de blés', 'submitterType': 'usager', 'externalReferences': 'AlloToulouse', 'external_number': '42-2', 'external_status': 'statut-1-wcs', 'address': 'https://wcs.example.com/backoffice/management/foo/2/', 'form_api_url': 'https://wcs.example.com/api/forms/foo/2/', 'checkDuplicated': 'False', 'onPrivateLand': 'True', 'safeguardRequired': True, } FIELDS_PAYLOAD = { 'coin_raw': [ { 'type_objet': 'Candélabre', 'type_objet_raw': 'Candélabre', 'field2': '42', }, ], } CREATE_INTERVENTION_PAYLOAD = { 'fields': FIELDS_PAYLOAD, 'extra': CREATE_INTERVENTION_PAYLOAD_EXTRA, } UUID = uuid.UUID('12345678123456781234567812345678') CREATE_INTERVENTION_QUERY = { 'add_media_url': 'http://testserver/toulouse-smart/test/add-media?uuid=%s' % str(UUID), 'description': 'coin coin', 'cityId': '12345', 'interventionCreated': '2021-06-30T16:08:05.500931+00:00', 'interventionDesired': '2021-06-30T16:08:05.500931+00:00', 'submitterFirstName': 'John', 'submitterLastName': 'Doe', 'submitterMail': 'john.doe@example.com', 'submitterPhone': '0123456789', 'submitterAddress': '3 rue des champs de bl\u00e9s', 'submitterType': 'usager', 'externalReferences': 'AlloToulouse', 'external_number': '42-2', 'external_status': 'statut-1-wcs', 'address': 'https://wcs.example.com/backoffice/management/foo/2/', 'interventionData': {'TYPE-OBJET': 'Candélabre', 'FIELD2': 42}, 'geom': {'type': 'Point', 'coordinates': [2.323349, 48.833708], 'crs': 'EPSG:4326'}, 'interventionTypeId': '1234', 'notificationUrl': 'http://testserver/toulouse-smart/test/update-intervention?uuid=%s' % str(UUID), 'notification_url': 'http://testserver/toulouse-smart/test/update-intervention?uuid=%s' % str(UUID), 'onPrivateLand': 'true', 'safeguardRequired': 'true', } CREATE_INTERVENTION_QUERY_WITHOUT_PROPERTIES = deepcopy(CREATE_INTERVENTION_QUERY) CREATE_INTERVENTION_QUERY_WITHOUT_PROPERTIES['interventionTypeId'] = '0002' CREATE_INTERVENTION_QUERY_WITHOUT_PROPERTIES['interventionData'] = {} @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_create_intervention(mocked_uuid4, app, smart): with pytest.raises(WcsRequest.DoesNotExist): smart.wcs_requests.get(uuid=UUID) resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert str(UUID) in CREATE_INTERVENTION_QUERY['notification_url'] assert not resp.json['err'] assert resp.json['data']['uuid'] == str(UUID) assert resp.json['data']['wcs_form_api_url'] == 'https://wcs.example.com/api/forms/foo/2/' wcs_request = smart.wcs_requests.get(uuid=UUID) assert wcs_request.wcs_form_api_url == 'https://wcs.example.com/api/forms/foo/2/' assert wcs_request.wcs_form_number == '42-2' assert wcs_request.payload == CREATE_INTERVENTION_QUERY assert wcs_request.result['id'] == INTERVENTION_ID assert wcs_request.result['state'] == { 'id': 'e844e67f-5382-4c0f-94d8-56f618263485', 'table': None, 'stateLabel': 'Nouveau', 'closes': False, } assert wcs_request.result['interventionCreated'] == '2021-07-07T14:19:31.302000' assert wcs_request.result['interventionDesired'] == '2021-06-30T18:08:05' assert wcs_request.status == 'sent' @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention', CREATE_INTERVENTION_QUERY_WITHOUT_PROPERTIES, get_json_file('create_intervention')], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_create_intervention_without_properties(mocked_uuid4, app, smart): payload = deepcopy(CREATE_INTERVENTION_PAYLOAD) payload['extra']['slug'] = 'empty' payload['fields']['coin_raw'] = None resp = app.post_json(URL + 'create-intervention/', params=payload) assert not resp.json['err'] @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention', CREATE_INTERVENTION_QUERY_WITHOUT_PROPERTIES, get_json_file('create_intervention')], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_create_intervention_providing_empty_block(mocked_uuid4, app, smart): payload = deepcopy(CREATE_INTERVENTION_PAYLOAD) payload['extra']['slug'] = 'empty' payload['fields']['coin_raw'] = None payload['fields']['empty_raw'] = None resp = app.post_json(URL + 'create-intervention/', params=payload) assert not resp.json['err'] def test_create_intervention_wrong_payload(app, smart): payload = deepcopy(CREATE_INTERVENTION_PAYLOAD) del payload['extra']['slug'] resp = app.post_json(URL + 'create-intervention/', params=payload, status=400) assert resp.json['err'] assert "'slug' is a required property" in resp.json['err_desc'] @mock_response() def test_create_intervention_types_unavailable(app, smart): resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert resp.json['err'] assert 'Service is unavailable' in resp.json['err_desc'] @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ) def test_create_intervention_wrong_block_slug(app, smart): payload = deepcopy(CREATE_INTERVENTION_PAYLOAD) payload['extra']['slug'] = 'coin-coin' resp = app.post_json(URL + 'create-intervention/', params=payload, status=400) assert resp.json['err'] assert "unknown 'coin-coin' block slug" in resp.json['err_desc'] @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ) def test_create_intervention_no_block(app, smart): payload = deepcopy(CREATE_INTERVENTION_PAYLOAD) del payload['fields']['coin_raw'] resp = app.post_json(URL + 'create-intervention/', params=payload, status=400) assert resp.json['err'] assert resp.json['err_desc'] == "'field2' field is required on 'coin' block" @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ) def test_create_intervention_string_payload(app, smart): payload = deepcopy(CREATE_INTERVENTION_PAYLOAD) payload['fields']['coin_raw'] = 'plop' resp = app.post_json(URL + 'create-intervention/', params=payload, status=400) assert resp.json['err'] assert ( resp.json['err_desc'] == "cannot retrieve 'coin' block content from post data: got a where a dict was expected" ) @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ) def test_create_intervention_cast_error(app, smart): payload = deepcopy(CREATE_INTERVENTION_PAYLOAD) payload['fields']['coin_raw'][0]['field2'] = 'not-an-integer' resp = app.post_json(URL + 'create-intervention/', params=payload, status=400) assert resp.json['err'] assert "cannot cast 'field2' field to " in resp.json['err_desc'] @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ) def test_create_intervention_missing_value(app, smart): field_payload = { 'coin_raw': [ { 'type_objet': 'Candélabre', 'type_objet_raw': 'Candélabre', 'field2': None, }, ], } payload = deepcopy(CREATE_INTERVENTION_PAYLOAD) payload['fields'] = field_payload resp = app.post_json(URL + 'create-intervention/', params=payload, status=400) assert resp.json['err'] assert "field is required on 'coin' block" in resp.json['err_desc'] @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ) def test_create_intervention_missing_field(app, smart): field_payload = { 'coin_raw': [ { 'type_objet': 'Candélabre', 'type_objet_raw': 'Candélabre', }, ], } payload = deepcopy(CREATE_INTERVENTION_PAYLOAD) payload['fields'] = field_payload resp = app.post_json(URL + 'create-intervention/', params=payload, status=400) assert resp.json['err'] assert "field is required on 'coin' block" in resp.json['err_desc'] @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention', CREATE_INTERVENTION_QUERY, None, 500], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_create_intervention_twice(mocked_uuid4, app, smart): resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] assert resp.json['data']['status'] == 'registered' assert smart.wcs_requests.count() == 1 # re-create intervention after it success: no error is returned, but no new request is sent resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] assert resp.json['data']['status'] == 'registered' assert smart.wcs_requests.count() == 1 @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention', CREATE_INTERVENTION_QUERY, None, 500], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_create_intervention_transport_error(mocked_uuid, app, freezer, smart): freezer.move_to('2021-07-08 00:00:00') resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] job = Job.objects.get(method_name='create_intervention_job') assert job.status == 'registered' wcs_request = smart.wcs_requests.get(uuid=UUID) assert wcs_request.status == 'registered' assert 'failed to post' in wcs_request.result freezer.move_to('2021-07-08 00:00:03') smart.jobs() job = Job.objects.get(method_name='create_intervention_job') assert job.status == 'registered' assert job.update_timestamp > job.creation_timestamp wcs_request = smart.wcs_requests.get(uuid=UUID) assert wcs_request.status == 'registered' assert 'failed to post' in wcs_request.result @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention', CREATE_INTERVENTION_QUERY, None, None, ReadTimeout('timeout')], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_create_intervention_timeout_error(mocked_uuid, app, smart): resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] job = Job.objects.get(method_name='create_intervention_job') assert job.status == 'registered' wcs_request = smart.wcs_requests.get(uuid=UUID) assert wcs_request.status == 'registered' assert 'failed to post' in wcs_request.result assert 'timeout' in wcs_request.result @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention', CREATE_INTERVENTION_QUERY, None, 500], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_create_intervention_inconsistency_id_error(mocked_uuid4, app, freezer, smart): freezer.move_to('2021-07-08 00:00:00') resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] wcs_request = smart.wcs_requests.get(uuid=UUID) assert wcs_request.status == 'registered' job = Job.objects.get(method_name='create_intervention_job') assert job.status == 'registered' freezer.move_to('2021-07-08 00:00:03') wcs_request.delete() smart.jobs() job = Job.objects.get(method_name='create_intervention_job') assert job.status == 'failed' @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention', CREATE_INTERVENTION_QUERY, 'not json content'], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_create_intervention_content_error(mocked_uuid, app, freezer, smart): freezer.move_to('2021-07-08 00:00:00') resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] wcs_request = smart.wcs_requests.get(uuid=UUID) assert wcs_request.status == 'registered' assert 'invalid json' in wcs_request.result @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention', CREATE_INTERVENTION_QUERY, '400 Client Error', 400], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_create_intervention_client_error(mocked_uuid, app, freezer, smart): freezer.move_to('2021-07-08 00:00:00') resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] wcs_request = smart.wcs_requests.get(uuid=UUID) assert '400 Client Error' in wcs_request.result assert wcs_request.tries == 1 assert wcs_request.status == 'registered' assert wcs_request.smart_requests.count() == 0 job = Job.objects.get(method_name='create_intervention_job') assert job.status == 'registered' smart.jobs() wcs_request = smart.wcs_requests.get(uuid=UUID) assert wcs_request.tries == 2 assert wcs_request.status == 'registered' assert wcs_request.smart_requests.count() == 1 smart_request = wcs_request.smart_requests.latest('id') assert smart_request.payload['creation_response']['status'] == 'registered' smart.jobs() wcs_request = smart.wcs_requests.get(uuid=UUID) assert wcs_request.tries == 3 assert wcs_request.status == 'registered' assert wcs_request.smart_requests.count() == 2 smart_request = wcs_request.smart_requests.latest('id') assert smart_request.payload['creation_response']['status'] == 'registered' freezer.move_to('2021-07-08 01:00:01') smart.jobs() wcs_request = smart.wcs_requests.get(uuid=UUID) assert wcs_request.tries == 4 assert wcs_request.status == 'registered' assert wcs_request.smart_requests.count() == 3 smart_request = wcs_request.smart_requests.latest('id') assert smart_request.payload['creation_response']['status'] == 'registered' freezer.move_to('2021-07-09 01:00:02') smart.jobs() wcs_request = smart.wcs_requests.get(uuid=UUID) assert '400 Client Error' in wcs_request.result assert wcs_request.tries == 5 assert wcs_request.status == 'failed' job = Job.objects.get(method_name='create_intervention_job') assert job.status == 'failed' assert '400 Client Error' in job.status_details['error_summary'] assert wcs_request.smart_requests.count() == 4 smart_request = wcs_request.smart_requests.latest('id') assert smart_request.payload['creation_response']['status'] == 'failed' # re-create intervention after it fails: a new request is sent resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] assert resp.json['data']['status'] == 'registered' wcs_request = smart.wcs_requests.get(uuid=UUID) assert '400 Client Error' in wcs_request.result assert wcs_request.tries == 1 assert wcs_request.status == 'registered' @mock.patch('passerelle.utils.RequestSession.request') @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_create_intervention_timeout(mocked_uuid, mocked_get, app, freezer, smart): from tests.utils import FakedResponse mocked_get.side_effect = [ FakedResponse( headers={'Content-Type': 'application/xml; charset=charset=utf-8'}, status_code=200, content=INTERVENTION_TYPES, ), ReadTimeout('timeout'), FakedResponse( headers={'Content-Type': 'application/json'}, status_code=200, content=get_json_file('create_intervention'), ), ] # synchronous requests freezer.move_to('2021-07-08 00:00:00') resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] assert mocked_get.call_count == 2 assert mocked_get.call_args_list[0][1]['timeout'] == 10 assert mocked_get.call_args_list[1][1]['timeout'] == 10 # asynchronous request freezer.move_to('2021-07-09 01:00:02') smart.jobs() assert mocked_get.call_count == 3 assert mocked_get.call_args_list[2][1]['timeout'] == 25 UPDATE_INTERVENTION_PAYLOAD = { 'data': { 'status': 'close manque info', 'type_retour_cloture': 'Smart non Fait', 'libelle_cloture': "rien à l'adresse indiquée", 'commentaire_cloture': 'le commentaire', } } UPDATE_INTERVENTION_QUERY = UPDATE_INTERVENTION_PAYLOAD WCS_RESPONSE_SUCCESS = '{"err": 0, "url": null}' WCS_RESPONSE_ERROR = '{"err": 1, "err_class": "Access denied", "err_desc": null}' @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')], ['/api/forms/foo/2/hooks/update_intervention/', UPDATE_INTERVENTION_QUERY, WCS_RESPONSE_SUCCESS], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_update_intervention(mocked_uuid, app, smart, wcs_service): resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] assert CREATE_INTERVENTION_QUERY[ 'notification_url' ] == 'http://testserver/toulouse-smart/test/update-intervention?uuid=%s' % str(UUID) smart.wcs_requests.get(uuid=UUID) mocked_push = mock.patch( 'passerelle.contrib.toulouse_smart.models.SmartRequest.push', return_value=False, ) mocked_push.start() assert Job.objects.count() == 0 url = URL + 'update-intervention?uuid=%s' % str(UUID) resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] assert resp.json['data']['uuid'] == str(UUID) assert resp.json['data']['payload']['data']['type_retour_cloture'] == 'Smart non Fait' smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get() mocked_push.stop() assert Job.objects.count() == 1 job = Job.objects.get(method_name='update_intervention_job') assert job.status == 'registered' smart.jobs() job = Job.objects.get(method_name='update_intervention_job') assert job.status == 'completed' smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get() assert smart_request.result == {'err': 0, 'url': None} def test_update_intervention_wrong_uuid(app, smart): with pytest.raises(WcsRequest.DoesNotExist): smart.wcs_requests.get(uuid=UUID) url = URL + 'update-intervention?uuid=0123456789' resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD, status=400) assert resp.json['err'] assert 'is not a valid UUID.' in resp.json['err_desc'] assert SmartRequest.objects.count() == 0 url = URL + 'update-intervention?uuid=%s' % str(UUID) resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD, status=400) assert resp.json['err'] assert 'Cannot find intervention' in resp.json['err_desc'] assert SmartRequest.objects.count() == 0 @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_update_intervention_job_wrong_service(mocked_uuid, app, smart, wcs_service): resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] wcs_service['default']['url'] = 'http://wrong.example.com' url = URL + 'update-intervention?uuid=%s' % str(UUID) resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] smart.jobs() job = Job.objects.get(method_name='update_intervention_job') assert job.status == 'completed' smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get() assert 'Cannot find wcs service' in smart_request.result @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')], ['/api/forms/foo/2/hooks/update_intervention/', UPDATE_INTERVENTION_QUERY, WCS_RESPONSE_ERROR, 403], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_update_intervention_job_wcs_error(mocked_uuid, app, smart, wcs_service, caplog): resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] url = URL + 'update-intervention?uuid=%s' % str(UUID) resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] smart.jobs() job = Job.objects.get(method_name='update_intervention_job') assert job.status == 'completed' smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get() assert smart_request.result == {'err': 1, 'err_class': 'Access denied', 'err_desc': None} @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')], ['/api/forms/foo/2/hooks/update_intervention/', UPDATE_INTERVENTION_QUERY, 'bla', 500], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_update_intervention_job_wcs_error_not_json(mocked_uuid, app, freezer, smart, wcs_service): freezer.move_to('2021-07-08 00:00:00') resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] url = URL + 'update-intervention?uuid=%s' % str(UUID) resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] job = Job.objects.get(method_name='update_intervention_job') assert job.status == 'registered' smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get() assert smart_request.result is None freezer.move_to('2021-07-08 00:00:03') smart.jobs() job = Job.objects.get(method_name='update_intervention_job') assert job.status == 'registered' assert job.update_timestamp > job.creation_timestamp smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get() assert smart_request.result is None @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')], [ '/api/forms/foo/2/hooks/update_intervention/', UPDATE_INTERVENTION_QUERY, None, 500, ConnectionError('No address associated with hostname'), ], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_update_intervention_job_transport_error(mocked_uuid, app, freezer, smart, wcs_service): freezer.move_to('2021-07-08 00:00:00') resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] url = URL + 'update-intervention?uuid=%s' % str(UUID) resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] job = Job.objects.get(method_name='update_intervention_job') assert job.status == 'registered' smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get() assert smart_request.result is None freezer.move_to('2021-07-08 00:00:03') smart.jobs() job = Job.objects.get(method_name='update_intervention_job') assert job.status == 'registered' assert job.update_timestamp > job.creation_timestamp smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get() assert smart_request.result is None ADD_MEDIA_PAYLOAD = { 'files/0': { 'filename': '201x201.jpg', 'content_type': 'image/jpeg', 'content': force_str(base64.b64encode(get_media_file('201x201.jpg'))), }, 'files/1': None, } ADD_MEDIA_QUERY = [get_media_file('201x201.jpg')] @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention/%s/media' % INTERVENTION_ID, ADD_MEDIA_QUERY, 200], ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_add_media(mocked_uuid, app, smart): resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] url = resp.json['data']['payload']['add_media_url'] url = URL + 'add-media?uuid=%s' % str(UUID) resp = app.post_json(url, params=ADD_MEDIA_PAYLOAD) assert not resp.json['err'] assert resp.json['data']['uuid'] == str(UUID) assert resp.json['data']['nb_registered'] == 1 assert Job.objects.count() == 1 job = Job.objects.get(method_name='add_media_job') assert job.status == 'registered' wcs_request = smart.wcs_requests.get(uuid=UUID) wcs_request_file = wcs_request.files.get(**job.parameters) path = wcs_request_file.content.path assert os.path.isfile(path) with wcs_request_file.content.open('rb') as desc: assert desc.read() == get_media_file('201x201.jpg') # smart not responding mocked_push = mock.patch( 'passerelle.contrib.toulouse_smart.models.WcsRequestFile.push', return_value=False, ) mocked_push.start() job = Job.objects.get(method_name='add_media_job') assert job.status == 'registered' # smart responding mocked_push.stop() smart.jobs() job = Job.objects.get(method_name='add_media_job') assert job.status == 'completed' wcs_request_file = wcs_request.files.get(**job.parameters) with pytest.raises(ValueError, match='no file associated'): assert not wcs_request_file.content.path assert not os.path.isfile(path) def test_add_media_wrong_uuid(app, smart): with pytest.raises(WcsRequest.DoesNotExist): smart.wcs_requests.get(uuid=UUID) url = URL + 'add-media?uuid=%s' % str(UUID) resp = app.post_json(url, params=ADD_MEDIA_PAYLOAD, status=400) assert resp.json['err'] assert 'Cannot find intervention' in resp.json['err_desc'] assert WcsRequestFile.objects.count() == 0 @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention/%s/media' % json.loads(get_json_file('create_intervention'))['id'], None, None, 500], ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_add_media_error(mocked_uuid, app, freezer, smart): resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] freezer.move_to('2021-10-30 00:00:00') url = resp.json['data']['payload']['add_media_url'] resp = app.post_json(url, params=ADD_MEDIA_PAYLOAD) job = Job.objects.get(method_name='add_media_job') assert job.status == 'registered' freezer.move_to('2021-10-30 00:00:03') smart.jobs() job = Job.objects.get(method_name='add_media_job') assert job.status == 'registered' assert job.update_timestamp > job.creation_timestamp @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention/%s/media' % INTERVENTION_ID, None, None, None, ReadTimeout('timeout')], ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_add_media_timeout_error(mocked_uuid, app, freezer, smart): resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] freezer.move_to('2021-10-30 00:00:00') url = resp.json['data']['payload']['add_media_url'] resp = app.post_json(url, params=ADD_MEDIA_PAYLOAD) job = Job.objects.get(method_name='add_media_job') assert job.status == 'registered' freezer.move_to('2021-10-30 00:00:03') smart.jobs() job = Job.objects.get(method_name='add_media_job') assert job.status == 'registered' assert job.update_timestamp > job.creation_timestamp @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention/%s/media' % INTERVENTION_ID, ADD_MEDIA_QUERY, 200], ['/v1/intervention', CREATE_INTERVENTION_QUERY, '400 Client Error', 400], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_add_media_with_create_intervention_failure(mocked_uuid, app, smart): resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] url = resp.json['data']['payload']['add_media_url'] url = URL + 'add-media?uuid=%s' % str(UUID) resp = app.post_json(url, params=ADD_MEDIA_PAYLOAD) assert not resp.json['err'] assert resp.json['data']['uuid'] == str(UUID) assert resp.json['data']['nb_registered'] == 1 job = Job.objects.get(method_name='add_media_job') assert job.status == 'registered' # simulate failure on intervention creation wcs_request = smart.wcs_requests.get(uuid=UUID) wcs_request.status = 'failed' wcs_request.save() wcs_request_file = wcs_request.files.get(**job.parameters) path = wcs_request_file.content.path assert os.path.isfile(path) smart.jobs() job = Job.objects.get(method_name='add_media_job') assert job.status == 'failed' assert 'related wcs request failed' in job.status_details['error_summary'] UPDATE_INTERVENTION_QUERY_ON_ASYNC_CREATION = { 'creation_response': { 'wcs_form_api_url': CREATE_INTERVENTION_PAYLOAD_EXTRA['form_api_url'], 'wcs_form_number': CREATE_INTERVENTION_PAYLOAD_EXTRA['external_number'], 'uuid': str(UUID), 'payload': CREATE_INTERVENTION_QUERY, 'result': json.loads(get_json_file('create_intervention')), 'status': 'sent', 'tries': 1, } } UPDATE_INTERVENTION_QUERY_ON_ASYNC_CREATION['creation_response']['result'][ 'interventionCreated' ] = '2021-07-07T14:19:31.302000' UPDATE_INTERVENTION_QUERY_ON_ASYNC_CREATION['creation_response']['result'][ 'interventionDesired' ] = '2021-06-30T18:08:05' @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')], [ '/api/forms/foo/2/hooks/update_intervention/', UPDATE_INTERVENTION_QUERY_ON_ASYNC_CREATION, WCS_RESPONSE_SUCCESS, ], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_create_intervention_async(mocked_uuid4, app, smart, wcs_service): mocked_wcs_request_push = mock.patch( 'passerelle.contrib.toulouse_smart.models.WcsRequest.push', return_value=False, ) mocked_smart_request_push = mock.patch( 'passerelle.contrib.toulouse_smart.models.SmartRequest.push', return_value=False, ) # smart and wcs are down mocked_wcs_request_push.start() mocked_smart_request_push.start() assert Job.objects.count() == 0 resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert str(UUID) in CREATE_INTERVENTION_QUERY['notification_url'] assert not resp.json['err'] assert resp.json['data']['uuid'] == str(UUID) assert resp.json['data']['wcs_form_api_url'] == 'https://wcs.example.com/api/forms/foo/2/' wcs_request = smart.wcs_requests.get(uuid=UUID) assert wcs_request.wcs_form_api_url == 'https://wcs.example.com/api/forms/foo/2/' assert wcs_request.wcs_form_number == '42-2' assert wcs_request.payload == CREATE_INTERVENTION_QUERY assert wcs_request.status == 'registered' assert Job.objects.count() == 1 job = Job.objects.get(method_name='create_intervention_job') assert job.status == 'registered' # smart is up mocked_wcs_request_push.stop() smart.jobs() assert Job.objects.count() == 2 job = Job.objects.get(method_name='create_intervention_job') assert job.status == 'completed' wcs_request = smart.wcs_requests.get(uuid=UUID) assert wcs_request.result['id'] == INTERVENTION_ID assert wcs_request.status == 'sent' job = Job.objects.get(method_name='update_intervention_job') assert job.status == 'registered' # wcs is up mocked_smart_request_push.stop() smart.jobs() job = Job.objects.get(method_name='update_intervention_job') assert job.status == 'completed' smart_request = wcs_request.smart_requests.get() assert smart_request.payload['creation_response']['uuid'] == str(UUID) assert smart_request.payload['creation_response']['result']['id'] == INTERVENTION_ID assert smart_request.payload['creation_response']['status'] == 'sent' @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention/%s/media' % INTERVENTION_ID, ADD_MEDIA_QUERY, 200], ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_add_media_async(mocked_uuid4, app, smart, freezer): mocked_wcs_request_push = mock.patch( 'passerelle.contrib.toulouse_smart.models.WcsRequest.push', return_value=False, ) # smart is down freezer.move_to('2021-10-30 00:00:00') mocked_wcs_request_push.start() resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert not resp.json['err'] url = resp.json['data']['payload']['add_media_url'] resp = app.post_json(url, params=ADD_MEDIA_PAYLOAD) smart.jobs() job = Job.objects.get(method_name='create_intervention_job') assert job.status == 'registered' job = Job.objects.get(method_name='add_media_job') assert job.status == 'registered' assert str(job.after_timestamp) == '2021-10-30 00:10:00+00:00' # smart is up freezer.move_to('2021-10-30 00:00:03') mocked_wcs_request_push.stop() smart.jobs() job = Job.objects.get(method_name='create_intervention_job') assert job.status == 'completed' job = Job.objects.get(method_name='add_media_job') assert job.status == 'registered' # 10 minutes later freezer.move_to('2021-10-30 00:10:03') smart.jobs() job = Job.objects.get(method_name='add_media_job') assert job.status == 'completed' @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention', None, get_json_file('create_intervention')], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_create_intervention_multiple_step(mocked_uuid4, app, smart): app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) assert smart.wcs_requests.get(uuid=UUID, wcs_form_step='initial') NEW_UUID = uuid.UUID('a' * 32) mocked_uuid4.return_value = NEW_UUID app.post_json( URL + 'create-intervention/', params=dict(CREATE_INTERVENTION_PAYLOAD, form_step='reclamation-1') ) assert smart.wcs_requests.get(uuid=NEW_UUID, wcs_form_step='reclamation-1') def test_pk_change_migration(migration): old_apps = migration.before( [('toulouse_smart', '0005_auto_20220105_1514'), ('base', '0029_auto_20210202_1627')] ) Job = old_apps.get_model('base', 'Job') ContentType = old_apps.get_model('contenttypes', 'ContentType') ToulouseSmartResource = old_apps.get_model('toulouse_smart', 'ToulouseSmartResource') WcsRequest = old_apps.get_model('toulouse_smart', 'WcsRequest') WcsRequestFile = old_apps.get_model('toulouse_smart', 'WcsRequestFile') SmartRequest = old_apps.get_model('toulouse_smart', 'SmartRequest') tsr = ToulouseSmartResource.objects.create(slug='a', description='a') ct = ContentType.objects.get(app_label='toulouse_smart', model='toulousesmartresource') wcs_request = WcsRequest.objects.create( resource=tsr, wcs_form_api_url='https://example.com/1/', wcs_form_number='1' ) WcsRequestFile.objects.create(resource=wcs_request, filename='a', content_type='a', content='a/b') SmartRequest.objects.create(resource=wcs_request, payload={}, result={}) job = Job.objects.create( resource_type=ct, resource_pk=tsr.pk, method_name='create_intervention_job', parameters={ 'pk': wcs_request.pk, }, ) apps = migration.apply([('toulouse_smart', '0012_migrate_jobs')]) Job = apps.get_model('base', 'Job') ContentType = apps.get_model('contenttypes', 'ContentType') ToulouseSmartResource = apps.get_model('toulouse_smart', 'ToulouseSmartResource') WcsRequest = apps.get_model('toulouse_smart', 'WcsRequest') WcsRequestFile = apps.get_model('toulouse_smart', 'WcsRequestFile') wcs_request = WcsRequest.objects.get() job = Job.objects.get() assert job.parameters['pk'] == str(wcs_request.uuid) assert WcsRequest.objects.get(pk=job.parameters['pk']) assert wcs_request.files.get().resource_id == wcs_request.uuid assert wcs_request.smart_requests.get().resource_id == wcs_request.uuid CREATE_INTERNVENTION_WITH_NONE = json.dumps( dict(json.loads(get_json_file('create_intervention')), interventionCreated=None) ) @mock_response( ['/v1/type-intervention', None, INTERVENTION_TYPES], ['/v1/intervention', CREATE_INTERVENTION_QUERY, CREATE_INTERNVENTION_WITH_NONE], ) @mock.patch('django.db.models.fields.UUIDField.get_default', return_value=UUID) def test_create_intervention_none_dates(mocked_uuid4, app, smart): app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD) wcs_request = smart.wcs_requests.get(uuid=UUID) assert wcs_request.result['interventionCreated'] is None assert wcs_request.result['interventionDesired'] == '2021-06-30T18:08:05' assert wcs_request.status == 'sent'