"""Unit test for payment_group_payment_account handlers.""" from typing import Any from unittest.mock import call, MagicMock, patch from urllib.parse import urlencode from marshmallow import ValidationError import pytest from payment.blueprints.payment_group_payment_account import ( PaymentGroupPaymentAccountFilterParamsPostSchema, PaymentGroupPaymentAccountListResponseSchema, ) from payment.constants import constants from payment.schemas.payment_group_payment_account_detail import ( PaymentDebitCreditDataEntrySchema, ) from tests.utils.factories import ( PaymentDebitCreditDataEntryFactory, PaymentGroupPaymentAccountFactory, ) MOCK_PAYMENT_GROUP_PAYMENT_ACCOUNTS = [ { 'account_id': 1, 'balance_after_tax': '200.00', 'contracts_payable': [ {'contract_id': 1, 'currency_code': 'USD', 'current_balance': '200.00'} ], 'currency_code': 'USD', 'currency_name': 'US Dollar', 'current_balance': '200.00', 'current_statement_period_id': 1, 'last_payment': '400.00', 'last_statement_period_id': 12, 'note': None, 'payment_group_payment_account_id': 1, 'payment_group_payment_id': None, 'payoneer_program_id': 100158970, 'prior_payment_group_payment_id': None, 'tax_withholding': None, 'vat_amount': None, }, { 'account_id': 2, 'balance_after_tax': '230.00', 'contracts_payable': [ {'contract_id': 1, 'currency_code': 'USD', 'current_balance': '230.00'} ], 'currency_code': 'USD', 'currency_name': 'US Dollar', 'current_balance': '200.00', 'current_statement_period_id': 1, 'last_payment': '400.00', 'last_statement_period_id': 12, 'note': None, 'payment_group_payment_account_id': 2, 'payment_group_payment_id': None, 'payoneer_program_id': 100158970, 'prior_payment_group_payment_id': None, 'tax_withholding': None, 'vat_amount': None, }, ] @patch('payment.blueprints.payment_group_payment_account.logic') def test_delete_payment_group_payment_account( mock_logic, fixture_client, fresh_db, mock_accounts ): """Test handler to delete a specific payment_group_payment_account.""" mock_logic.delete_payment_group_payment_account.return_value = None payment_account = PaymentGroupPaymentAccountFactory.create() payment_account_id = payment_account.payment_group_payment_account_id res = fixture_client.delete(f'/payment-group-payment-account/{payment_account_id}/') assert res.status_code == 204 mock_logic.delete_payment_group_payment_account.assert_called_once_with( payment_account_id ) @patch('payment.blueprints.payment_group_payment_account.logic') def test_get_last_posted_payment_by_account(mock_logic, fixture_client): """Test handler to get an account's last posted payment.""" account_id = 123 mock_logic.get_last_posted_payment_by_account.return_value = None res = fixture_client.get( f'/payment-group-payment-account/last-payment/{account_id}' ) assert res.status_code == 200 mock_logic.get_last_posted_payment_by_account.assert_called_once_with(account_id) @patch('payment.blueprints.payment_group_payment_account.logic') def test_get_pending_payment_by_account(mock_logic, fixture_client): """Test handler to get an account's pending payment, if any.""" account_id = 123 mock_logic.get_pending_payment_by_account.return_value = None res = fixture_client.get( f'/payment-group-payment-account/pending-payment/{account_id}' ) assert res.status_code == 200 mock_logic.get_pending_payment_by_account.assert_called_once_with(account_id) @pytest.mark.parametrize( 'verify_rules_access_standalone_result,' 'authorize_resource_result,' 'expected_status_code,' 'expect_authorize_resource_called', [ pytest.param( True, False, 200, False, id='profile checks authorized request', ), pytest.param( False, False, 403, True, id='profile and pp checks failed to authorize request', ), pytest.param( False, True, 200, True, id='pp checks authorized request', ), ], ) @patch('payment.utils.authorization.authorize_resource') @patch('payment.utils.authorization.flask_request') @patch('payment.blueprints.payment_group_payment_account.logic') def test_get_pending_payment_by_account_authorization_checks( mock_logic: MagicMock, mock_flask_request: MagicMock, mock_authorize_resource: MagicMock, verify_rules_access_standalone_result: bool, authorize_resource_result: bool, expected_status_code: int, expect_authorize_resource_called: bool, fixture_client: Any, ) -> None: """Test endpoint uses authorization checks.""" account_id = 123 mock_logic.get_pending_payment_by_account.return_value = None mock_flask_request.verify_rules_access_standalone.return_value = ( verify_rules_access_standalone_result ) mock_authorize_resource.return_value = authorize_resource_result res = fixture_client.get( f'/payment-group-payment-account/pending-payment/{account_id}' ) assert res.status_code == expected_status_code if res.status_code != 200: res.json == {'code': 'authorization_error', 'message': 'Unauthorized'} mock_flask_request.verify_rules_access_standalone.assert_called_once() if expect_authorize_resource_called: mock_authorize_resource.assert_called_once_with( resource_id=account_id, resource_type='account', action='view_pending_payment', id_to_uuid_exchange_tenant={ 'tenant_type': 'account', 'tenant_id': account_id, }, ) else: mock_authorize_resource.assert_not_called() @patch('payment.blueprints.payment_group_payment_account.logic') def test_update_payment_group_payment_account( mock_logic, fixture_client, fresh_db, mock_accounts, mock_statement_periods ): """Test handler to update a specific payment_group_payment_account.""" payment_account = PaymentGroupPaymentAccountFactory.create() mock_logic.update_payment_group_payment_account.return_value = payment_account payment_account_id = payment_account.payment_group_payment_account_id put_body = {'note': 'blah blah blah'} res = fixture_client.put( f'/payment-group-payment-account/{payment_account_id}/', json=put_body ) assert res.status_code == 201 mock_logic.update_payment_group_payment_account.assert_called_once_with( payment_account_id, put_body.get('note') ) @pytest.mark.parametrize('payment_type', constants.PAYMENT_TYPES) @patch('payment.blueprints.payment_group_payment_account.logic') def test_get_batch_debit_data_success(mock_logic, payment_type, fixture_client, faker): """Test GET /payment-group-payment-account///credit.""" # noqa mock_data_entry = PaymentDebitCreditDataEntryFactory.build() mock_logic.get_credit_data.return_value = [mock_data_entry] payment_group_payment_account_id = faker.pyint() res = fixture_client.get( f'/payment-group-payment-account' f'/{payment_group_payment_account_id}/{payment_type}/credit' ) assert res.status_code == 200 assert mock_logic.get_credit_data.call_args_list == [ call(payment_group_payment_account_id, payment_type) ] assert res.json == PaymentDebitCreditDataEntrySchema().dump( [mock_data_entry], many=True ) @patch('payment.blueprints.payment_group_payment_account.logic') def test_get_batch_debit_data_failure(mock_logic, fixture_client, faker): """Test GET /payment-group-payment-account///credit.""" # noqa mock_data_entry = PaymentDebitCreditDataEntryFactory.build() mock_logic.get_credit_data.return_value = [mock_data_entry] payment_group_payment_account_id = faker.pyint() payment_type = faker.pystr(prefix='test_') res = fixture_client.get( f'/payment-group-payment-account' f'/{payment_group_payment_account_id}/{payment_type}/credit' ) assert res.status_code == 404 assert not mock_logic.get_credit_data.called @patch('payment.blueprints.payment_group_payment_account.logic') def test_get_paginated_list_of_last_payment_by_filters_success( mock_logic, fixture_client, faker ): """Test for get_paginated_list_of_last_payment_by_filters endpoint.""" account_ids = faker.pylist(value_types=[int]) limit = faker.pyint(min_value=1, max_value=300) offset = faker.pyint() params = { 'limit': limit, 'offset': offset, } body = { 'filters': { 'account_ids': account_ids, } } mock_result = { 'items': MOCK_PAYMENT_GROUP_PAYMENT_ACCOUNTS, 'total_count': len(MOCK_PAYMENT_GROUP_PAYMENT_ACCOUNTS), } mock_logic.get_last_payments.return_value = mock_result res = fixture_client.post( f'/payment-group-payment-account/last-payment/bulk/?{urlencode(params)}', json=body, ) assert res.status_code == 200, res.text assert res.json == mock_result assert mock_logic.get_last_payments.call_args_list == [ call( limit=limit, offset=offset, **body['filters'], ) ] @patch('payment.blueprints.payment_group_payment_account.logic') def test_get_paginated_list_of_last_payment_by_filters_failure( mock_logic, fixture_client ): """Test for get_paginated_list_of_last_payment_by_filters endpoint failure validation.""" account_ids = ['noint'] limit = 'noint' offset = 'noint' params = { 'limit': limit, 'offset': offset, } body = { 'filters': { 'account_ids': account_ids, } } res = fixture_client.post( f'/payment-group-payment-account/last-payment/bulk/?{urlencode(params)}' ) assert res.status_code == 400, res.text assert res.json == { 'code': 'error', 'message': { 'query': { 'limit': ['Not a valid integer.'], 'offset': ['Not a valid integer.'], } }, } assert not mock_logic.get_last_payments.called res = fixture_client.post( '/payment-group-payment-account/last-payment/bulk', json=body, ) assert res.status_code == 400, res.text assert res.json['code'] == 'error' assert 'account_ids' in res.json['message']['json']['filters'] assert not mock_logic.get_last_payments.called @patch('payment.blueprints.payment_group_payment_account.logic') def test_payments_search_post_body(mock_logic, fixture_client, mocker): """Test POST search with only body filters returns expected body.""" body = { 'filters': { 'account_ids': [4, 5], 'contract_ids': [21], 'payment_statuses': ['active', 'pending'], } } mock_result = {'items': MOCK_PAYMENT_GROUP_PAYMENT_ACCOUNTS, 'total_count': 0} mock_logic.payments_search.return_value = mock_result spy_validation_body = mocker.spy( PaymentGroupPaymentAccountFilterParamsPostSchema, 'load' ) spy_serialization = mocker.spy(PaymentGroupPaymentAccountListResponseSchema, 'dump') res = fixture_client.post( '/payment-group-payment-account/search?limit=15&offset=2', json=body, ) assert res.status_code == 200, res.text assert res.json == mock_result assert mock_logic.payments_search.call_args_list == [ call( limit=15, offset=2, account_ids=[4, 5], contract_ids=[21], payment_statuses=['active', 'pending'], ) ] assert spy_validation_body.called assert spy_serialization.called @patch('payment.blueprints.payment_group_payment_account.logic') def test_payments_search_empty(mock_logic, fixture_client, mocker): """Test search with no params returns expected empty body.""" mock_result = {'items': MOCK_PAYMENT_GROUP_PAYMENT_ACCOUNTS, 'total_count': 0} mock_logic.payments_search.return_value = mock_result spy_validation_body = mocker.spy( PaymentGroupPaymentAccountFilterParamsPostSchema, 'load' ) spy_serialization = mocker.spy(PaymentGroupPaymentAccountListResponseSchema, 'dump') res = fixture_client.post( '/payment-group-payment-account/search?limit=10&offset=0', json={}, ) assert res.status_code == 200, res.text assert res.json == mock_result assert mock_logic.payments_search.call_args_list == [call(limit=10, offset=0)] assert spy_validation_body.called assert spy_serialization.called @patch('payment.blueprints.payment_group_payment_account.logic') def test_payments_search_failure_validation_body(mock_logic, fixture_client, mocker): """Test search with no params returns expected empty body.""" mock_result = {'items': MOCK_PAYMENT_GROUP_PAYMENT_ACCOUNTS, 'total_count': 0} mock_logic.payments_search.return_value = mock_result spy_validation_body = mocker.spy( PaymentGroupPaymentAccountFilterParamsPostSchema, 'load' ) spy_validation_body.side_effect = ValidationError('test') spy_serialization = mocker.spy(PaymentGroupPaymentAccountListResponseSchema, 'dump') res = fixture_client.post( '/payment-group-payment-account/search?limit=10&offset=0', json={'filters': {'account_ids': ['noint']}}, ) assert res.status_code == 400, res.text assert not mock_logic.payments_search.called assert spy_validation_body.called assert not spy_serialization.called