# Copyright (C) 2022 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 json from unittest import mock import pytest from django.contrib.contenttypes.models import ContentType from django.urls import reverse from requests.exceptions import ReadTimeout import tests.utils from passerelle.apps.sivin.models import Resource from passerelle.base.models import AccessRight, ApiUser pytestmark = pytest.mark.django_db EURO5_INDEXES = [ ('0555*0651C Euro 4', False), ('0555*0651G Euro 5', True), ('0555*0651G EURO 5', True), ('0555*0874G (EURO 5)', True), ('134/2014EURO4', False), ('134/2014EURO5', True), ('175/2007*195/2013EURO6', True), ('200/55*2008/74EURO5', True), ('2005/55*51euro5', True), ('2006/51G-EUROV (G)', True), ('2006/96euro4', False), ('59/2009*2016/1718EURO6', True), ('595/2009*2018/932DEUROVI', True), ('595/2009*2018/932EURO', False), ('595/2009*627/2014EUROIV', False), ('70/220*1999/102EURO3', False), ('EURO 3', False), ('EURO III', False), ('EURO2', False), ('EURO5G', True), ('EURO6B', True), ('2001/100A', False), ] VEHICLE_DETAILS = { 'carrosserie': 'BERLINE', 'clEnvironPrf': '70/220 2001/100EURO3', 'codifVin': 'VF7FCKFVB26857835', 'genreVCG': 'VP', 'immatSiv': 'FS032GM', 'genreVPrf': 'VP', 'date1erCir': '2003-11-21', 'nSiren': '000000000', } VEHICLE_THEORICAL_FINITION = { 'carrosserie': 'COMBISPACE', 'clEnvironPrf': '2001/100A', 'codifVin': 'VF7GJRHYK93204774', 'genreVCG': 'VP', 'immatSiv': '01XT0747', 'genreVPrf': 'VP', 'date1erCir': '2004-11-19', 'nSiren': '000000000', } EXPIRED_TOKEN_MESSAGE = ( '' '900901Invalid Credentials' 'Invalid Credentials. Make sure you have provided the correct security credentials' '' ) TOKEN_401 = tests.utils.FakedResponse( content='{"error_description": "Client Authentication failed.", "error": "invalid_client"}', status_code=401, headers={'Content-Type': 'application/json'}, ) TOKEN = tests.utils.FakedResponse( content='{"access_token": "token_value", "scope": "default", "token_type": "Bearer", "expires_in": 3600}', status_code=200, headers={'Content-Type': 'application/json'}, ) EXPIRED_TOKEN = tests.utils.FakedResponse( content=EXPIRED_TOKEN_MESSAGE, status_code=500, ) VIN = tests.utils.FakedResponse( content=json.dumps(VEHICLE_DETAILS), status_code=200, headers={'Content-Type': 'application/json'}, ) VEHICLES = tests.utils.FakedResponse( content='{"vins":["VF1FB30A511331122"]}', status_code=200, headers={'Content-Type': 'application/json'} ) FINITION = tests.utils.FakedResponse( content=json.dumps(VEHICLE_THEORICAL_FINITION), status_code=200, headers={'Content-Type': 'application/json'}, ) @pytest.fixture def conn(): api_user = ApiUser.objects.create(username='sivin', keytype='API', key='sivinkey') connector = Resource.objects.create( title='Test', slug='test', consumer_key='key', consumer_secret='secret', environment='test' ) obj_type = ContentType.objects.get_for_model(Resource) AccessRight.objects.create( codename='can_access', apiuser=api_user, resource_type=obj_type, resource_pk=connector.pk ) return connector @mock.patch('passerelle.utils.Request.post') def test_no_api_key(mocked_post, app, conn): url = reverse( 'generic-endpoint', kwargs={'connector': 'sivin', 'endpoint': 'consultervehiculeparvin', 'slug': conn.slug}, ) app.get(url, params={'vin': 'vin'}, status=403) assert mocked_post.call_count == 0 @mock.patch('passerelle.utils.Request.post') def test_wrong_credentials(mocked_post, app, conn): mocked_post.return_value = TOKEN_401 url = reverse( 'generic-endpoint', kwargs={'connector': 'sivin', 'endpoint': 'consultervehiculeparvin', 'slug': conn.slug}, ) resp = app.get(url, params={'apikey': 'sivinkey', 'vin': 'VF1BA0E0514143067'}).json assert mocked_post.call_count == 1 assert mocked_post.call_args[0][0] == 'https://api.rec.sivin.fr/token' assert resp['err'] assert resp['err_desc'] == 'Failed to get token. Error: 401' @mock.patch('passerelle.utils.Request.post') def test_get_token(mocked_post, app, conn): mocked_post.return_value = TOKEN conn.get_token() assert mocked_post.call_count == 1 assert mocked_post.call_args[0][0] == 'https://api.rec.sivin.fr/token' # token is in cache so no more http hits conn.get_token() assert mocked_post.call_count == 1 @mock.patch('passerelle.utils.Request.post', side_effect=(TOKEN, VIN)) def test_get_details_by_vin(mocked_post, app, conn): url = reverse( 'generic-endpoint', kwargs={'connector': 'sivin', 'endpoint': 'consultervehiculeparvin', 'slug': conn.slug}, ) resp = app.get(url, params={'apikey': 'sivinkey', 'vin': 'VF1BA0E0514143067'}).json assert mocked_post.call_count == 2 assert not resp['err'] assert resp['data'] == VEHICLE_DETAILS @mock.patch('passerelle.utils.Request.post', side_effect=(TOKEN, VEHICLES)) def test_get_vehicles_by_siren(mocked_post, app, conn): url = reverse( 'generic-endpoint', kwargs={'connector': 'sivin', 'endpoint': 'consulterflotteparsiren', 'slug': conn.slug}, ) resp = app.get(url, params={'apikey': 'sivinkey', 'siren': '000399634'}).json assert mocked_post.call_count == 2 assert not resp['err'] for item in resp['data']: assert 'id' in item assert 'text' in item @mock.patch('passerelle.utils.Request.post', side_effect=(TOKEN, EXPIRED_TOKEN)) def test_get_with_expired_token(mocked_post, app, conn): url = reverse( 'generic-endpoint', kwargs={'connector': 'sivin', 'endpoint': 'consulterflotteparsiren', 'slug': conn.slug}, ) resp = app.get(url, params={'apikey': 'sivinkey', 'siren': '000399634'}).json assert mocked_post.call_count == 2 assert resp['err'] assert resp['err_desc'] == EXPIRED_TOKEN_MESSAGE @pytest.mark.parametrize('immat,sent_immat', [('747-xT 01', '01XT0747'), ('FD-734-hR', 'FD734HR')]) @mock.patch('passerelle.utils.Request.post', side_effect=(TOKEN, FINITION)) def test_get_vehicle_theorical_finition(mocked_post, app, conn, immat, sent_immat): url = reverse( 'generic-endpoint', kwargs={'connector': 'sivin', 'endpoint': 'consulterfinitiontheoriqueparimmat', 'slug': conn.slug}, ) resp = app.get(url, params={'apikey': 'sivinkey', 'immat': immat}).json assert mocked_post.call_count == 2 assert mocked_post.mock_calls[-1].kwargs['json'] == {'immat': sent_immat} assert not resp['err'] assert 'is_euro5' in resp['data'] resp['data'].pop('is_euro5') assert resp['data'] == VEHICLE_THEORICAL_FINITION @mock.patch('passerelle.utils.Request.post', side_effect=ReadTimeout('timeout')) def test_connection_timeout(mocked_post, app, conn): url = reverse( 'generic-endpoint', kwargs={'connector': 'sivin', 'endpoint': 'consulterflotteparsiren', 'slug': conn.slug}, ) resp = app.get(url, params={'apikey': 'sivinkey', 'siren': '000399634'}).json assert mocked_post.call_count == 1 assert resp['err'] assert ( resp['err_desc'] == 'failed to call https://api.rec.sivin.fr/sivin/v2/consulterflotteparsiren: timeout' ) @pytest.mark.parametrize('clEnvironPrf, is_euro5', EURO5_INDEXES) def test_compute_euro_index(app, conn, clEnvironPrf, is_euro5): assert conn.is_euro5(clEnvironPrf) == is_euro5