# 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 json import httmock import pytest from Cryptodome.Cipher import DES from Cryptodome.Util.Padding import pad, unpad from django.utils.encoding import force_bytes import tests.utils from passerelle.apps.esirius.models import ESirius from passerelle.utils.jsonresponse import APIError from tests.test_manager import login CREATE_APPOINTMENT_PAYLOAD = { 'beginDate': '2021-02-24', 'beginTime': '16:40', 'endDate': '2021-02-24', 'endTime': '17:00', 'comment': 'commentaire', 'isoLanguage': 'fr', 'needsConfirmation': 'False', 'rdvChannel': 'WEBSERVICES', 'receptionChannel': 'WS', 'serviceId': '9', 'siteCode': 'site1', 'resources/id': '1', 'resources/key': '17', 'resources/type': 'STATION', } GET_APPOINTMENT_RESPONSE = ''' { "beginDate" : "2021-02-26", "beginTime" : "16:40", "codeRDV" : "94PEP4", "comment" : "coucou", "endDate" : "2021-02-26", "endTime" : "17:00", "idSys" : 108840, "isoLanguage" : "fr", "motives" : [], "needsConfirmation" : false, "rdvChannel" : "EAPP0", "receptionChannel" : "WS", "resources" : { "id" : 29, "key" : "46", "name" : "C1", "type" : "STATION" }, "serviceId" : "39", "siteCode" : "site1", "siteIdSys" : 5, "user" : { "additionalPersonalIdentity" : [], "address" : {}, "civility" : "", "idSys" : "95897" } } ''' @pytest.fixture def connector(db): return tests.utils.setup_access_rights( ESirius.objects.create( slug='test', secret_id='xxx', secret_key='yyy', base_url='https://esirius.example.net' ) ) def get_endpoint(name): return tests.utils.generic_endpoint_url('esirius', name) @pytest.mark.freeze_time('2021-01-26 15:13:6.880') # epoch + 1611673986.88 s def test_token(connector): connector.secret_id = 'eAppointment' connector.secret_key = 'ES2I Info Caller Http Encryption Key' connector.save() @httmock.all_requests def esirius_mock(url, request): assert ( request.headers['token_info_caller'] == b'yM4zYAxT67Qvjd20riG3j0eu0t0Ku+HLlttj17Gul7zkruFaXX1J8BJ6sV2Ldgw40axfWh+ESAY=' ) return httmock.response(200) with httmock.HTTMock(esirius_mock): connector.request('an/uri/', method='get', params="somes") @pytest.mark.parametrize('secret_key', ['yyy', '']) def test_pre_request(connector, secret_key): @httmock.urlmatch(netloc='esirius.example.net', path='/an/uri/', method='GET') def esirius_mock(url, request): assert request.headers['Accept'] == 'application/json; charset=utf-8' assert bool(request.headers.get('token_info_caller')) == bool(secret_key) if secret_key: des_key = pad(force_bytes(secret_key), 8)[:8] cipher = DES.new(des_key, DES.MODE_ECB) msg = cipher.decrypt(base64.b64decode(request.headers['token_info_caller'])) token = json.loads(unpad(msg, 8)) assert set(token) == {'caller', 'createInfo'} assert token['caller'] == connector.secret_id return httmock.response(200) connector.secret_key = secret_key connector.save() with httmock.HTTMock(esirius_mock): connector.request('an/uri/', method='get', params="somes") @pytest.mark.parametrize( 'status_code, content, a_dict', [ (400, '{"message": "help"}', {'message': 'help'}), (500, 'not json', None), ], ) def test_post_request(connector, status_code, content, a_dict): @httmock.urlmatch(netloc='esirius.example.net', path='/an/uri/', method='GET') def esirius_mock(url, request): return httmock.response(status_code, content) with pytest.raises(APIError) as exc: with httmock.HTTMock(esirius_mock): connector.request('an/uri/', params="somes") assert exc.value.err assert exc.value.data['status_code'] == status_code assert exc.value.data['json_content'] == a_dict @pytest.mark.parametrize('status_code, is_up', [(200, True), (500, False), (503, False)]) @pytest.mark.parametrize('content', ['wathever', '{"message": "help"}']) def test_check_status(app, connector, status_code, is_up, content): @httmock.all_requests def esirius_mock(url, request): return httmock.response(status_code, content) if is_up: with httmock.HTTMock(esirius_mock): connector.check_status() else: with pytest.raises(APIError): with httmock.HTTMock(esirius_mock): connector.check_status() def test_create_appointment(app, connector): endpoint = get_endpoint('create-appointment') @httmock.urlmatch(netloc='esirius.example.net', path='/appointments/', method='POST') def esirius_mock(url, request): assert json.loads(request.body)['resources'] == {'id': '1', 'key': '17', 'type': 'STATION'} return httmock.response(200, b'94PEP4') with httmock.HTTMock(esirius_mock): resp = app.post_json(endpoint, params=CREATE_APPOINTMENT_PAYLOAD) assert not resp.json['err'] assert resp.json['data'] == {'id': '94PEP4', 'created': True} def test_create_appointment_error_404(app, connector): endpoint = get_endpoint('create-appointment') # payload not providing or providing an unconfigured serviceId payload = CREATE_APPOINTMENT_PAYLOAD del payload['serviceId'] @httmock.urlmatch(netloc='esirius.example.net', path='/appointments/', method='POST') def esirius_mock(url, request): return httmock.response( 404, { 'code': 'Not Found', 'type': 'com.es2i.planning.api.exception.NoService4RDVException', 'message': "Le rendez-vous {0} n'a pas créé", }, ) with httmock.HTTMock(esirius_mock): resp = app.post_json(endpoint, params=payload) assert resp.json['err'] assert resp.json['data']['status_code'] == 404 assert resp.json['data']['json_content'] == { 'code': 'Not Found', 'type': 'com.es2i.planning.api.exception.NoService4RDVException', 'message': "Le rendez-vous {0} n'a pas créé", } def test_create_appointment_error_500(app, connector): endpoint = get_endpoint('create-appointment') # payload not providing beginTime payload = {'beginDate': '2021-02-23'} @httmock.urlmatch(netloc='esirius.example.net', path='/appointments/', method='POST') def esirius_mock(url, request): return httmock.response(500, 'java stack') with httmock.HTTMock(esirius_mock): resp = app.post_json(endpoint, params=payload) assert resp.json['err'] assert resp.json['err_class'] == 'passerelle.utils.jsonresponse.APIError' assert resp.json['err_desc'] == "error status:500 None, content:'java stack'" assert resp.json['data']['status_code'] == 500 assert resp.json['data']['json_content'] is None def test_update_appointment(app, connector): endpoint = get_endpoint('update-appointment') @httmock.urlmatch(netloc='esirius.example.net', path='/appointments', method='PUT') def esirius_mock(url, request): assert 'codeRDV' in json.loads(request.body).keys() return httmock.response(200, b'') with httmock.HTTMock(esirius_mock): resp = app.post_json(endpoint + '?id=94PEP4', params=CREATE_APPOINTMENT_PAYLOAD) assert not resp.json['err'] assert resp.json['data'] == {'id': '94PEP4', 'updated': True} def test_update_appointment_error(app, connector): endpoint = get_endpoint('update-appointment') payload = CREATE_APPOINTMENT_PAYLOAD payload['idSys'] = 42 @httmock.urlmatch(netloc='esirius.example.net', path='/appointments', method='PUT') def esirius_mock(url, request): raise ResourceWarning with httmock.HTTMock(esirius_mock): resp = app.post_json(endpoint + '?id=94PEP4', params=payload, status=400) assert resp.json['err'] assert resp.json['err_class'] == 'passerelle.utils.jsonresponse.APIError' assert resp.json['err_desc'] == "idSys: 42 is not of type 'string'" def test_get_appointment(app, connector): endpoint = get_endpoint('get-appointment') @httmock.urlmatch(netloc='esirius.example.net', path='/appointments/94PEP4/', method='GET') def esirius_mock(url, request): return httmock.response(200, GET_APPOINTMENT_RESPONSE) with httmock.HTTMock(esirius_mock): resp = app.get(endpoint + '?id=94PEP4') assert not resp.json['err'] assert resp.json['data']['codeRDV'] == '94PEP4' assert resp.json['data'] == json.loads(GET_APPOINTMENT_RESPONSE) def test_get_appointment_error(app, connector): endpoint = get_endpoint('get-appointment') @httmock.urlmatch(netloc='esirius.example.net', path='/appointments/42OUPS/', method='GET') def esirius_mock(url, request): return httmock.response(404, '{"code":"Not Found", "message":"Le rendez-vous {0} n\'existe pas"}') with httmock.HTTMock(esirius_mock): resp = app.get(endpoint + '?id=42OUPS') assert resp.json['err'] assert resp.json['err_class'] == 'passerelle.utils.jsonresponse.APIError' assert resp.json['data']['status_code'] == 404 assert resp.json['data']['json_content'] == { "code": "Not Found", "message": "Le rendez-vous {0} n'existe pas", } def test_delete_appointment(app, connector): endpoint = get_endpoint('delete-appointment') @httmock.urlmatch(netloc='esirius.example.net', path='/appointments/94PEP4/', method='DELETE') def esirius_mock(url, request): return httmock.response(200, b'') with httmock.HTTMock(esirius_mock): resp = app.delete(endpoint + '?id=94PEP4') assert not resp.json['err'] assert resp.json['data'] == {'id': '94PEP4', 'deleted': True} def test_delete_appointment_error(app, connector): endpoint = get_endpoint('delete-appointment') @httmock.urlmatch(netloc='esirius.example.net', path='/appointments/42OUPS/', method='DELETE') def esirius_mock(url, request): return httmock.response(304, b'') with httmock.HTTMock(esirius_mock): resp = app.delete(endpoint + '?id=42OUPS') assert resp.json['err'] assert resp.json['err_desc'] == 'Appointment not found' def test_manager(db, app, admin_user, connector): url = '/%s/%s/' % (connector.get_connector_slug(), connector.slug) connector.title = 'Test' connector.description = 'Test eSirius' connector.save() login(app) resp = app.get(url) resp = resp.click('Edit') assert resp.html.find('div', {'id': 'id_secret_key_p'}).input['value'] == 'yyy' resp = resp.form.submit() assert ( 'DES key must be 8 bytes long' in resp.html.find('div', {'id': 'id_secret_key_p'}).find('div', {'class': 'error'}).text ) resp.form['secret_key'] = '8 bytes!' resp = resp.form.submit() assert ESirius.objects.get().secret_key == '8 bytes!' # accept an empty key resp = app.get(url) resp = resp.click('Edit') assert resp.html.find('div', {'id': 'id_secret_key_p'}).input['value'] == '8 bytes!' resp.form['secret_key'] = '' resp = resp.form.submit() assert ESirius.objects.get().secret_key == ''