"""Tests for the HMRC Connector.""" import os import re from unittest.mock import MagicMock, Mock, patch import pytest import requests from payee.connectors import uk_gov from payee.constants.constants import ( HMRC_AUTH_URL, HMRC_CHECK_VAT_URL, HMRC_TOKEN_CACHE_KEY, HMRC_TOKEN_CACHE_TIMEOUT_DELTA, ) from payee.constants.error import ( ERROR_HMRC_API_UNREACHABLE, ERROR_VAT_HMRC_API_CHECK_FAILED, ERROR_VAT_UK_VAT_FORMAT, ) from payee.utils.exception import NoHmrcCredentialsException ERROR_SHORT_VAT = '123' ERROR_PREFIX_VAT = 'IT123456789' ERROR_NOT_EXISTED_VAT = '999999999' ERROR_LONG_VAT = '123123123456789' CORRECT_VAT = 'GB123456789' CORRECT_WITHOUT_PREFIX_VAT = '123456789' FAKE_HEADERS = {'header1': 'val1', 'key': 'value'} FAKE_URL = 'http://someurl.com' @pytest.mark.parametrize( 'client_id,client_secret,cache_value,token,expires_in,is_error', ( (None, None, None, None, None, False), ('cl_id', None, None, None, None, False), (None, 'cl_sec', None, None, None, False), ('cl_id', 'cl_sec', 'cache1', None, None, False), ('cl_id', 'cl_sec', None, 'tkn1', 489, False), ('cl_id', 'cl_sec', None, None, None, True), ), ) @patch('payee.connectors.uk_gov.requests') @patch('payee.connectors.uk_gov.get_uwsgi_cache_object') @patch('payee.connectors.uk_gov.Config') def test_get_hmrc_token_from_api( mock_config: Mock, mock_get_uwsgi_cache_object: Mock, mock_requests: Mock, client_id: str | None, client_secret: str | None, cache_value: str | None, token: str | None, expires_in: int | None, is_error: bool, ) -> None: """Test _get_hmrc_token_from_api function.""" test_url = 'http://abc.test' mock_config.HMRC_API_CLIENT_ID = client_id mock_config.HMRC_API_CLIENT_SECRET = client_secret mock_config.HMRC_API_URL = test_url mock_get_uwsgi_cache_object.return_value.has.return_value = cache_value is not None mock_get_uwsgi_cache_object.return_value.get.return_value = cache_value if is_error: mock_requests.post.return_value.raise_for_status.side_effect = Exception( 'test_exc' ) mock_requests.post.return_value.json.return_value = { 'access_token': token, 'expires_in': expires_in, } if is_error: with pytest.raises(Exception): uk_gov._get_hmrc_token_from_api() else: result = uk_gov._get_hmrc_token_from_api() assert result == cache_value or token if client_id and client_secret: mock_get_uwsgi_cache_object.assert_called_once_with() mock_get_uwsgi_cache_object.return_value.has.assert_called_once_with( HMRC_TOKEN_CACHE_KEY ) else: mock_get_uwsgi_cache_object.assert_not_called() mock_get_uwsgi_cache_object.return_value.has.assert_not_called() if client_id and client_secret and cache_value is not None: mock_get_uwsgi_cache_object.return_value.get.assert_called_once_with( HMRC_TOKEN_CACHE_KEY ) else: mock_get_uwsgi_cache_object.return_value.get.assert_not_called() if client_id and client_secret and cache_value is None: mock_requests.post.assert_called_once_with( test_url + HMRC_AUTH_URL, data={ 'client_id': client_id, 'client_secret': client_secret, 'grant_type': 'client_credentials', 'scope': 'read:vat', }, ) mock_requests.post.return_value.raise_for_status.assert_called_once_with() else: mock_requests.post.assert_not_called() mock_requests.post.return_value.raise_for_status.assert_not_called() if client_id and client_secret and cache_value is None and not is_error: mock_requests.post.return_value.json.assert_called_once_with() mock_get_uwsgi_cache_object.return_value.set.assert_called_once_with( HMRC_TOKEN_CACHE_KEY, token, expires_in - HMRC_TOKEN_CACHE_TIMEOUT_DELTA ) else: mock_requests.post.return_value.json.assert_not_called() mock_get_uwsgi_cache_object.return_value.set.assert_not_called() @pytest.mark.parametrize( 'token_arn,is_error', ((None, False), ('test_arn', False), ('test_arn', True)) ) @patch('payee.connectors.uk_gov.secrets_manager_client') @patch('payee.connectors.uk_gov.Config') def test_get_hmrc_token_from_secrets( mock_config: Mock, mock_secrets_manager_client: Mock, token_arn: str | None, is_error: bool, ) -> None: """Test _get_hmrc_token_from_secrets function.""" token = 'test_token' mock_config.HMRC_API_AUTH_TOKEN_ARN = token_arn if is_error: mock_secrets_manager_client.get_secret_value.side_effect = Exception('test_exc') else: mock_secrets_manager_client.get_secret_value.return_value = { 'SecretString': token } if is_error: with pytest.raises(Exception): uk_gov._get_hmrc_token_from_secrets() else: result = uk_gov._get_hmrc_token_from_secrets() assert result == (token if token_arn else None) if token_arn: mock_secrets_manager_client.get_secret_value.assert_called_once_with( SecretId=token_arn, VersionStage='AWSCURRENT' ) else: mock_secrets_manager_client.get_secret_value.assert_not_called() @pytest.mark.parametrize( 'env_token,config_env,api_token,secrets_token,is_error,expected_result,api_called,secrets_called', ( (None, 'dev', None, None, True, None, True, False), ('env_t', 'dev', None, None, False, 'env_t', False, False), (None, 'dev', 'api_t', 'sec_t', False, 'api_t', True, False), (None, 'qa', 'api_t', 'sec_t', False, 'sec_t', False, True), (None, 'qa', 'api_t', None, False, 'api_t', True, True), ), ) @patch('payee.connectors.uk_gov._get_hmrc_token_from_api') @patch('payee.connectors.uk_gov._get_hmrc_token_from_secrets') @patch('payee.connectors.uk_gov.Config') def test_get_hmrc_token( mock_config: Mock, mock_get_hmrc_token_from_secrets: Mock, mock_get_hmrc_token_from_api: Mock, env_token: str | None, config_env: str, secrets_token: str | None, api_token: str | None, is_error: bool, expected_result: str | None, secrets_called: bool, api_called: bool, ) -> None: """Test get_hmrc_token function.""" if env_token: os.environ['HMRC_API_AUTH_TOKEN'] = env_token elif 'HMRC_API_AUTH_TOKEN' in os.environ: del os.environ['HMRC_API_AUTH_TOKEN'] mock_config.ENVIRONMENT = config_env mock_config.QA_ENVIRONMENT = 'qa' mock_config.UAT_ENVIRONMENT = 'uat' mock_config.PROD_ENVIRONMENT = 'prod' mock_get_hmrc_token_from_api.return_value = api_token mock_get_hmrc_token_from_secrets.return_value = secrets_token if is_error: with pytest.raises(NoHmrcCredentialsException): uk_gov.get_hmrc_token() else: result = uk_gov.get_hmrc_token() assert result == expected_result if api_called: mock_get_hmrc_token_from_api.assert_called_once_with() else: mock_get_hmrc_token_from_api.assert_not_called() if secrets_called: mock_get_hmrc_token_from_secrets.assert_called_once_with() else: mock_get_hmrc_token_from_secrets.assert_not_called() @pytest.mark.parametrize('is_error', (True, False)) @patch('payee.connectors.uk_gov.get_hmrc_token') def test_build_hmrc_headers(mock_get_hmrc_token: Mock, is_error: bool) -> None: """Test get_hmrc_token function.""" test_token = 'test_t' if is_error: mock_get_hmrc_token.side_effect = Exception('err1') else: mock_get_hmrc_token.return_value = test_token if is_error: with pytest.raises(Exception): uk_gov.build_hmrc_headers() else: result = uk_gov.build_hmrc_headers() assert result == { 'Accept': 'application/vnd.hmrc.2.0+json', 'Authorization': f'Bearer {test_token}', } mock_get_hmrc_token.assert_called_once_with() @pytest.mark.parametrize('vat', [ERROR_SHORT_VAT, ERROR_PREFIX_VAT, ERROR_LONG_VAT]) def test_validate_vat_number_error_format(vat: str) -> None: """Test external UK API for validate VAT format.""" with pytest.raises(Exception) as e: uk_gov.validate_vat_number(vat) assert str(e.value) == ERROR_VAT_UK_VAT_FORMAT @patch('payee.connectors.uk_gov.requests.get') @patch('payee.connectors.uk_gov.build_hmrc_headers') @patch('payee.connectors.uk_gov.Config') def test_validate_vat_number_not_exists( mock_config: Mock, mock_build_hmrc_headers: Mock, mock_get: Mock ) -> None: """Test external UK API for validate VAT existence.""" ext_vat_error = 'targetVrn does not match a registered company' mocked_response = MagicMock( status_code=404, json=MagicMock(return_value={'message': ext_vat_error}) ) mock_get.return_value = mocked_response mock_build_hmrc_headers.return_value = FAKE_HEADERS mock_config.HMRC_API_URL = FAKE_URL with pytest.raises(Exception) as e: uk_gov.validate_vat_number(ERROR_NOT_EXISTED_VAT) assert str(e.value) == ERROR_VAT_HMRC_API_CHECK_FAILED mock_get.assert_called_once_with( FAKE_URL + HMRC_CHECK_VAT_URL.format(vat_number=ERROR_NOT_EXISTED_VAT), headers=FAKE_HEADERS, ) mock_build_hmrc_headers.assert_called_once_with() @patch('payee.connectors.uk_gov.requests.get') @patch('payee.connectors.uk_gov.build_hmrc_headers') @patch('payee.connectors.uk_gov.Config') def test_validate_vat_number_timeout( mock_config: Mock, mock_build_hmrc_headers: Mock, mock_get: Mock ) -> None: """Test external UK API for validate VAT on timeout.""" ext_vat_error = 'Timeout' mock_get.side_effect = requests.exceptions.ConnectTimeout(ext_vat_error) mock_build_hmrc_headers.return_value = FAKE_HEADERS mock_config.HMRC_API_URL = FAKE_URL with pytest.raises(Exception) as e: uk_gov.validate_vat_number(ERROR_NOT_EXISTED_VAT) assert str(e.value) == ERROR_HMRC_API_UNREACHABLE.format(error=ext_vat_error) mock_get.assert_called_once_with( FAKE_URL + HMRC_CHECK_VAT_URL.format(vat_number=ERROR_NOT_EXISTED_VAT), headers=FAKE_HEADERS, ) mock_build_hmrc_headers.assert_called_once_with() @pytest.mark.parametrize('vat', [CORRECT_VAT, CORRECT_WITHOUT_PREFIX_VAT]) @patch('payee.connectors.uk_gov.requests.get') @patch('payee.connectors.uk_gov.build_hmrc_headers') @patch('payee.connectors.uk_gov.Config') def test_validate_vat_number_success( mock_config: Mock, mock_build_hmrc_headers: Mock, mock_get: Mock, vat: str ) -> None: """Test external UK API for validate VAT existence.""" expected_res = {'target': {'vatNumber': vat}} mocked_response = MagicMock( status_code=200, json=MagicMock(return_value=expected_res) ) mock_get.return_value = mocked_response mock_build_hmrc_headers.return_value = FAKE_HEADERS mock_config.HMRC_API_URL = FAKE_URL result = uk_gov.validate_vat_number(vat) assert result is True mock_get.assert_called_once_with( FAKE_URL + HMRC_CHECK_VAT_URL.format(vat_number=re.sub(r'\D', '', vat)), headers=FAKE_HEADERS, ) mock_build_hmrc_headers.assert_called_once_with()