"""Tests for bank details handlers.""" from http import HTTPStatus from typing import Any from unittest.mock import MagicMock, Mock, patch from uuid import uuid4 from flask.testing import FlaskClient import pytest from payee.config import Config from payee.constants.error import ( ERROR_DONT_HAVE_PERMISSIONS, ERROR_NO_VALID_IDENTITY_IN_CONTEXT, ERROR_NOT_PERMITTED_IDENTITY, ERROR_PAYEE_BANK_DETAILS_NOT_FOUND, ) from payee.models.account_payee import AccountPayee from payee.models.bank_details import BankDetailsDocument from payee.utils.exception import RegisterWhitelabelProfileException from tests.conftest import mock_abacus_headers, mock_documents_headers from tests.utils.auth import MockJWTAuth from tests.utils.factories import ( AccountPayeeFactory, BankDetailsIndividualTypeFlattenFactory, BankDetailsIndividualTypeNestedFactory, BankDetailsIndividualTypeResponseObscuredFactory, PayeeCollaboratorFactory, PayeeFactory, ) account_payee_id = 123 account_payee_id_output = str(account_payee_id) @patch('payee.logic.payee.Payee') @patch('payee.logic.payee.AccountPayee') @patch('payee.utils.permissions.validate_record_owner', return_value=True) @patch('payee.blueprints.bank_details.has_secure_document_details') def test_get_account_payee_bank_details_documents_profile_success( mock_has_secure_document_details, mock_perm, account_payee_mock, payee_mock, fixture_client, mock_bypass_headers, ): """Test getting bank details.""" account_payee = AccountPayeeFactory.build(account_payee_id=account_payee_id) payee_mock.get_by_id.return_value = None account_payee_mock.get_by_id.return_value = account_payee banking_params = BankDetailsIndividualTypeFlattenFactory.build() mock_has_secure_document_details.return_value = BankDetailsDocument(banking_params) result = fixture_client.get( f'/account-payee/{account_payee.account_payee_id}/bank-details', headers=mock_bypass_headers, ) assert result.status_code == 200 mock_has_secure_document_details.assert_called_once_with( account_payee.payoneer_client_reference_id, BankDetailsDocument ) assert result.json == BankDetailsIndividualTypeResponseObscuredFactory.build( payoneer_client_reference_id=str(account_payee.account_payee_id), account_payee_id=str(account_payee.account_payee_id), ) @patch('payee.logic.payee.Payee') @patch('payee.logic.payee.AccountPayee') @patch('payee.utils.permissions.validate_record_owner', return_value=False) @patch('payee.blueprints.bank_details.has_secure_document_details') def test_get_account_payee_bank_details_documents_profile_failure_permissions( mock_has_secure_document_details, mock_perm, account_payee_mock, payee_mock, fixture_client, mock_bypass_headers, ): """Test getting bank details.""" account_payee = AccountPayeeFactory.build(account_payee_id=account_payee_id) payee_mock.get_by_id.return_value = None account_payee_mock.get_by_id.return_value = account_payee banking_params = BankDetailsIndividualTypeFlattenFactory.build() mock_has_secure_document_details.return_value = BankDetailsDocument(banking_params) result = fixture_client.get( f'/account-payee/{account_payee.account_payee_id}/bank-details', headers=mock_bypass_headers, ) assert result.status_code == HTTPStatus.UNAUTHORIZED assert not mock_has_secure_document_details.called assert result.json == { 'error': f'{ERROR_NO_VALID_IDENTITY_IN_CONTEXT}, {ERROR_DONT_HAVE_PERMISSIONS}' } @patch('payee.logic.payee.Payee') @patch('payee.logic.payee.AccountPayee') @patch('payee.utils.permissions.authorization_backend.is_authorized', return_value=True) @patch('payee.blueprints.bank_details.has_secure_document_details') def test_get_account_payee_bank_details_abacus_profile_success( mock_has_secure_document_details, mock_perm, account_payee_mock, payee_mock, fixture_client, mock_headers, ): """Test getting bank details.""" account_payee = AccountPayeeFactory.build(account_payee_id=account_payee_id) payee_mock.get_by_id.return_value = None account_payee_mock.get_by_id.return_value = account_payee banking_params = BankDetailsIndividualTypeFlattenFactory.build() mock_has_secure_document_details.return_value = BankDetailsDocument(banking_params) result = fixture_client.get( f'/account-payee/{account_payee.account_payee_id}/bank-details', headers=mock_headers, ) assert result.status_code == 200 mock_has_secure_document_details.assert_called_once_with( account_payee.payoneer_client_reference_id, BankDetailsDocument ) assert result.json == BankDetailsIndividualTypeResponseObscuredFactory.build( payoneer_client_reference_id=str(account_payee.account_payee_id), account_payee_id=str(account_payee.account_payee_id), ) @patch('payee.logic.payee.Payee') @patch('payee.logic.payee.AccountPayee') @patch( 'payee.utils.permissions.authorization_backend.is_authorized', return_value=False ) @patch('payee.blueprints.bank_details.has_secure_document_details') def test_get_account_payee_bank_details_abacus_profile_failure_permissions( mock_has_secure_document_details, mock_perm, account_payee_mock, payee_mock, fixture_client, mock_headers, ): """Test getting bank details.""" account_payee = AccountPayeeFactory.build(account_payee_id=account_payee_id) payee_mock.get_by_id.return_value = None account_payee_mock.get_by_id.return_value = account_payee banking_params = BankDetailsIndividualTypeFlattenFactory.build() mock_has_secure_document_details.return_value = BankDetailsDocument(banking_params) result = fixture_client.get( f'/account-payee/{account_payee.account_payee_id}/bank-details', headers=mock_headers, ) assert result.status_code == HTTPStatus.UNAUTHORIZED assert not mock_has_secure_document_details.called assert result.json == { 'error': f'{ERROR_NO_VALID_IDENTITY_IN_CONTEXT}, {ERROR_DONT_HAVE_PERMISSIONS}' } @patch('payee.logic.payee.Payee') @patch('payee.logic.payee.AccountPayee') @pytest.mark.parametrize( 'headers,mock_perm_func', [ ( mock_abacus_headers, 'payee.utils.permissions.authorization_backend.is_authorized', ), (mock_documents_headers, 'payee.utils.permissions.validate_record_owner'), ], ) @patch('payee.blueprints.bank_details.has_secure_document_details') def test_get_account_payee_bank_details_failure_permissions( mock_has_secure_document_details, account_payee_mock, payee_mock, headers, mock_perm_func, fixture_client, monkeypatch, ): """Test getting payee bank details failure permissions.""" monkeypatch.setattr(mock_perm_func, lambda *args, **kwargs: False) account_payee = AccountPayeeFactory.build(account_payee_id=account_payee_id) payee_mock.get_by_id.return_value = None account_payee_mock.get_by_id.return_value = account_payee result = fixture_client.get( f'/account-payee/{account_payee.account_payee_id}/bank-details', headers=headers(), ) assert result.status_code == 401 assert result.json == { 'error': f'{ERROR_NO_VALID_IDENTITY_IN_CONTEXT}, {ERROR_DONT_HAVE_PERMISSIONS}' } assert mock_has_secure_document_details.call_count == 0 @patch('payee.logic.payee.Payee') @patch('payee.logic.payee.AccountPayee') @patch('payee.utils.permissions.validate_record_owner', return_value=True) @patch('payee.blueprints.bank_details.has_secure_document_details') def test_get_account_payee_bank_details_failure_not_found( mock_has_secure_document_details, mock_perm, account_payee_mock, payee_mock, fixture_client, mock_bypass_headers, ): """Test getting payee bank details failure permissions.""" account_payee = AccountPayeeFactory.build(account_payee_id=account_payee_id) payee_mock.get_by_id.return_value = None account_payee_mock.get_by_id.return_value = account_payee mock_has_secure_document_details.return_value = None result = fixture_client.get( f'/payee/{account_payee.account_payee_id}/bank-details', headers=mock_bypass_headers, ) assert result.status_code == 404 message = ERROR_PAYEE_BANK_DETAILS_NOT_FOUND.format( id_name=account_payee.entity_id_name, payee_id=account_payee.account_payee_id ) assert result.text == f'"{message}"\n' mock_has_secure_document_details.assert_called_once_with( account_payee.payoneer_client_reference_id, BankDetailsDocument ) @patch('payee.logic.payee.Payee') @patch('payee.logic.payee.AccountPayee') @patch('payee.utils.permissions.validate_record_owner', return_value=True) @patch('payee.blueprints.bank_details.has_secure_document_details') def test_get_payee_bank_details_documents_profile_success( mock_has_secure_document_details, mock_perm, account_payee_mock, payee_mock, fixture_client, mock_bypass_headers, ): """Test getting bank details.""" payee = PayeeFactory.build() payee.get_typed_payee = MagicMock(return_value=PayeeCollaboratorFactory.build()) payee_mock.get_by_id.return_value = payee account_payee_mock.get_by_id.return_value = None banking_params = BankDetailsIndividualTypeFlattenFactory.build() mock_has_secure_document_details.return_value = BankDetailsDocument(banking_params) result = fixture_client.get( f'/payee/{payee.payee_id}/bank-details', headers=mock_bypass_headers, ) assert result.status_code == 200 mock_has_secure_document_details.assert_called_once_with( payee.payoneer_client_reference_id, BankDetailsDocument ) assert result.json == BankDetailsIndividualTypeResponseObscuredFactory.build( payoneer_client_reference_id=str(payee.payoneer_client_reference_id), account_payee_id=payee.payoneer_client_reference_id, ) @patch('payee.logic.payee.Payee') @patch('payee.logic.payee.AccountPayee') @patch('payee.utils.permissions.validate_record_owner', return_value=False) @patch('payee.blueprints.bank_details.has_secure_document_details') def test_get_payee_bank_details_documents_profile_failure_permissions( mock_has_secure_document_details, mock_perm, account_payee_mock, payee_mock, fixture_client, mock_bypass_headers, ): """Test getting bank details.""" payee = PayeeFactory.build() payee.get_typed_payee = MagicMock(return_value=PayeeCollaboratorFactory.build()) payee_mock.get_by_id.return_value = payee account_payee_mock.get_by_id.return_value = None banking_params = BankDetailsIndividualTypeFlattenFactory.build() mock_has_secure_document_details.return_value = BankDetailsDocument(banking_params) result = fixture_client.get( f'/payee/{payee.payee_id}/bank-details', headers=mock_bypass_headers, ) assert result.status_code == HTTPStatus.UNAUTHORIZED assert not mock_has_secure_document_details.called assert result.json == { 'error': f'{ERROR_NO_VALID_IDENTITY_IN_CONTEXT}, {ERROR_DONT_HAVE_PERMISSIONS}' } @patch('payee.logic.payee.Payee') @patch('payee.logic.payee.AccountPayee') @patch('payee.utils.permissions.authorization_backend.is_authorized', return_value=True) @patch('payee.blueprints.bank_details.has_secure_document_details') def test_get_payee_bank_details_abacus_profile_success( mock_has_secure_document_details, mock_perm, account_payee_mock, payee_mock, fixture_client, mock_headers, mock_identity_fields: dict[str, Any], ): """Test getting bank details.""" payee = PayeeFactory.build() payee.get_typed_payee = MagicMock(return_value=PayeeCollaboratorFactory.build()) payee_mock.get_by_id.return_value = payee account_payee_mock.get_by_id.return_value = None banking_params = ( BankDetailsIndividualTypeFlattenFactory.build() | mock_identity_fields ) mock_has_secure_document_details.return_value = BankDetailsDocument(banking_params) result = fixture_client.get( f'/payee/{payee.payee_id}/bank-details', headers=mock_headers, ) assert result.status_code == 200 mock_has_secure_document_details.assert_called_once_with( payee.payoneer_client_reference_id, BankDetailsDocument ) assert result.json == BankDetailsIndividualTypeResponseObscuredFactory.build( payoneer_client_reference_id=str(payee.payoneer_client_reference_id), account_payee_id=payee.payoneer_client_reference_id, modified_at=mock_identity_fields['modified_at'], ) @patch('payee.logic.payee.Payee') @patch('payee.logic.payee.AccountPayee') @patch( 'payee.utils.permissions.authorization_backend.is_authorized', return_value=False ) @patch('payee.blueprints.bank_details.has_secure_document_details') def test_get_payee_bank_details_abacus_profile_failure_permissions( mock_has_secure_document_details, mock_perm, account_payee_mock, payee_mock, fixture_client, mock_headers, ): """Test getting bank details.""" payee = PayeeFactory.build() payee.get_typed_payee = MagicMock(return_value=PayeeCollaboratorFactory.build()) payee_mock.get_by_id.return_value = payee account_payee_mock.get_by_id.return_value = None banking_params = BankDetailsIndividualTypeFlattenFactory.build() mock_has_secure_document_details.return_value = BankDetailsDocument(banking_params) result = fixture_client.get( f'/payee/{payee.payee_id}/bank-details', headers=mock_headers, ) assert result.status_code == HTTPStatus.UNAUTHORIZED assert not mock_has_secure_document_details.called assert result.json == { 'error': f'{ERROR_NO_VALID_IDENTITY_IN_CONTEXT}, {ERROR_DONT_HAVE_PERMISSIONS}' } @patch('payee.logic.payee.Payee') @patch('payee.logic.payee.AccountPayee') @pytest.mark.parametrize( 'headers,mock_perm_func', [ ( mock_abacus_headers, 'payee.utils.permissions.authorization_backend.is_authorized', ), (mock_documents_headers, 'payee.utils.permissions.validate_record_owner'), ], ) @patch('payee.blueprints.bank_details.has_secure_document_details') def test_get_payee_bank_details_failure_permissions( mock_has_secure_document_details, account_payee_mock, payee_mock, headers, mock_perm_func, fixture_client, monkeypatch, ): """Test getting payee bank details failure permissions.""" monkeypatch.setattr(mock_perm_func, lambda *args, **kwargs: False) payee = PayeeFactory.build() payee.get_typed_payee = MagicMock(return_value=PayeeCollaboratorFactory.build()) payee_mock.get_by_id.return_value = payee account_payee_mock.get_by_id.return_value = None result = fixture_client.get( f'/payee/{payee.payee_id}/bank-details', headers=headers(), ) assert result.status_code == 401 assert result.json == { 'error': f'{ERROR_NO_VALID_IDENTITY_IN_CONTEXT}, {ERROR_DONT_HAVE_PERMISSIONS}' } assert mock_has_secure_document_details.call_count == 0 @patch('payee.logic.payee.Payee') @patch('payee.logic.payee.AccountPayee') @patch('payee.utils.permissions.validate_record_owner', return_value=True) @patch('payee.blueprints.bank_details.has_secure_document_details') def test_get_payee_bank_details_failure_not_found( mock_has_secure_document_details, mock_perm, account_payee_mock, payee_mock, fixture_client, mock_bypass_headers, ): """Test getting payee bank details failure permissions.""" payee = PayeeFactory.build() payee.get_typed_payee = MagicMock(return_value=PayeeCollaboratorFactory.build()) payee_mock.get_by_id.return_value = payee account_payee_mock.get_by_id.return_value = None mock_has_secure_document_details.return_value = None result = fixture_client.get( f'/payee/{payee.payee_id}/bank-details', headers=mock_bypass_headers, ) assert result.status_code == 404 message = ERROR_PAYEE_BANK_DETAILS_NOT_FOUND.format( id_name=payee.entity_id_name, payee_id=payee.payee_id, ) assert result.text == f'"{message}"\n' mock_has_secure_document_details.assert_called_once_with( payee.payoneer_client_reference_id, BankDetailsDocument ) @patch('payee.models.account_payee.AccountPayee.get_by_id') @patch('payee.utils.permissions.validate_record_owner', return_value=True) @patch('payee.blueprints.bank_details.logic') def test_sync_payee_bank_details( mock_logic, mock_perm, mock_get_by_id, fixture_client, mock_config, mock_bypass_headers, ): """Test creating bank details.""" account_payee = AccountPayeeFactory.build(account_payee_id=account_payee_id) mock_get_by_id.return_value = account_payee req_params = BankDetailsIndividualTypeNestedFactory.build() exp_flatten_params = BankDetailsIndividualTypeFlattenFactory.build() mock_logic.sync_bank_details.return_value = ( BankDetailsIndividualTypeResponseObscuredFactory.build( account_payee_id=account_payee_id, payoneer_client_reference_id=account_payee.account_payee_id, ) ) result = fixture_client.post( f'/account-payee/{account_payee_id}/sync-whitelabel-profile', headers=mock_bypass_headers, json=req_params, ) assert result.status_code == 201 mock_logic.sync_bank_details.assert_called_once_with( account_payee, **exp_flatten_params, ) assert result.json == BankDetailsIndividualTypeResponseObscuredFactory.build( account_payee_id=str(account_payee_id), payoneer_client_reference_id=str(account_payee_id), ) @pytest.mark.parametrize( 'headers,mock_perm_func', [ ( mock_abacus_headers, 'payee.utils.permissions.authorization_backend.is_authorized', ), (mock_documents_headers, 'payee.utils.permissions.validate_record_owner'), ], ) @patch('payee.models.account_payee.AccountPayee.get_by_id') @patch('payee.blueprints.bank_details.logic') def test_sync_payee_bank_details_failure_permissions( mock_logic, mock_get_by_id, headers, mock_perm_func, fixture_client, monkeypatch ): """Test creating payee bank details failure permissions.""" payee = AccountPayee(account_payee_id=123) account_payee = AccountPayeeFactory.build(account_payee_id=payee.account_payee_id) mock_get_by_id.return_value = account_payee monkeypatch.setattr(mock_perm_func, lambda *args, **kwargs: False) result = fixture_client.post( f'/account-payee/{account_payee_id}/sync-whitelabel-profile', headers=headers(), ) assert result.status_code == 401 assert result.json == {'error': ERROR_DONT_HAVE_PERMISSIONS} assert mock_logic.sync_bank_details.call_count == 0 @patch('payee.blueprints.bank_details.logic') def test_save_payee_bank_details_success( mock_logic, fixture_client, mock_config, mock_jwt_auth ): """Test saving bank details.""" mock_jwt_auth.identity = Config.BANK_DETAILS_SERVICE_IDENTITIES[0] req_params = BankDetailsIndividualTypeNestedFactory.build() exp_flatten_params = BankDetailsIndividualTypeFlattenFactory.build() mock_logic.save_payee_bank_details.return_value = ( BankDetailsIndividualTypeResponseObscuredFactory.build( payoneer_client_reference_id=account_payee_id_output, account_payee_id=account_payee_id, ) ) result = fixture_client.post( f'/account-payee/{account_payee_id}/bank-details', headers=mock_jwt_auth.headers, json=req_params, ) assert result.status_code == 201, result.text mock_logic.save_payee_bank_details.assert_called_once_with( **{'account_payee_id': account_payee_id, **exp_flatten_params}, ) assert result.json == BankDetailsIndividualTypeResponseObscuredFactory.build( payoneer_client_reference_id=account_payee_id_output, account_payee_id=account_payee_id_output, ) @patch('payee.blueprints.bank_details.logic') def test_save_payee_bank_details_failure_permissions( mock_logic, fixture_client, mock_config, mock_jwt_auth ): """Test saving bank details.""" mock_jwt_auth.identity = str(uuid4()) req_params = BankDetailsIndividualTypeNestedFactory.build() mock_logic.save_payee_bank_details.return_value = ( BankDetailsIndividualTypeResponseObscuredFactory.build( account_payee_id=account_payee_id ) ) result = fixture_client.post( f'/account-payee/{account_payee_id}/bank-details', headers=mock_jwt_auth.headers, json=req_params, ) assert result.status_code == 401 assert result.json == { 'error': ERROR_NOT_PERMITTED_IDENTITY.format( jwt_identity=mock_jwt_auth.identity ) } assert not mock_logic.save_payee_bank_details.called @patch('payee.blueprints.bank_details.logic') def test_purge_payee_bank_details_success( mock_logic, fixture_client, mock_config, mock_jwt_auth ): """Test purging bank details.""" mock_jwt_auth.identity = Config.BANK_DETAILS_SERVICE_IDENTITIES[0] mock_logic.purge_bank_details_info.return_value = None result = fixture_client.delete( f'/account-payee/{account_payee_id}/bank-details', headers=mock_jwt_auth.headers, ) assert result.status_code == 204 mock_logic.purge_bank_details_info.assert_called_once_with(account_payee_id) @patch('payee.blueprints.bank_details.logic') def test_purge_payee_bank_details__failure_permissions( mock_logic, fixture_client, mock_config, mock_jwt_auth ): """Test purging bank details.""" mock_jwt_auth.identity = str(uuid4()) mock_logic.purge_bank_details_info.return_value = None result = fixture_client.delete( f'/account-payee/{account_payee_id}/bank-details', headers=mock_jwt_auth.headers, ) assert result.status_code == 401 assert result.json == { 'error': ERROR_NOT_PERMITTED_IDENTITY.format( jwt_identity=mock_jwt_auth.identity ) } assert not mock_logic.purge_bank_details_info.called @patch('payee.blueprints.bank_details.logic') def test_reset_banking_states( mock_logic: Mock, fixture_client: FlaskClient, mock_config: Config, mock_jwt_auth: MockJWTAuth, ): """Test reset_banking_states endpoint.""" mock_jwt_auth.identity = Config.PAYONEER_SERVICE_IDENTITIES[0] result = fixture_client.post( f'/account-payee/{account_payee_id}/reset-banking-states/', headers=mock_jwt_auth.headers, ) assert result.status_code == HTTPStatus.OK assert result.text == 'null\n' mock_logic.reset_banking_states.assert_called_once_with(account_payee_id) @patch('payee.blueprints.bank_details.logic') def test_reset_banking_states_failure_permissions( mock_logic: Mock, fixture_client: FlaskClient, mock_config: Config, mock_jwt_auth: MockJWTAuth, ): """Test reset_banking_states with wrong JWT identity.""" mock_jwt_auth.identity = str(uuid4()) result = fixture_client.post( f'/account-payee/{account_payee_id}/reset-banking-states/', headers=mock_jwt_auth.headers, ) assert result.status_code == HTTPStatus.UNAUTHORIZED assert result.json == { 'error': ERROR_NOT_PERMITTED_IDENTITY.format( jwt_identity=mock_jwt_auth.identity ) } assert not mock_logic.reset_banking_states.called @pytest.mark.parametrize('is_error', (True, False)) @patch('payee.blueprints.bank_details.logic') def test_register_whitelabel_profile( mock_logic: Mock, fixture_client: FlaskClient, mock_config: Config, mock_jwt_auth: MockJWTAuth, is_error: bool, ): """Test register_whitelabel_profile.""" mock_jwt_auth.identity = Config.BANK_DETAILS_SERVICE_IDENTITIES[0] if is_error: mock_logic.register_whitelabel_profile.side_effect = ( RegisterWhitelabelProfileException('test err') ) result = fixture_client.post( f'/account-payee/{account_payee_id}/register-whitelabel-profile', headers=mock_jwt_auth.headers, ) if is_error: assert result.status_code == HTTPStatus.UNPROCESSABLE_ENTITY assert result.json == {'code': 'error', 'message': 'test err'} else: assert result.status_code == HTTPStatus.OK assert result.text == 'null\n' mock_logic.register_whitelabel_profile.assert_called_once_with(account_payee_id)