"""Test the DynamoDB connector of the reverse payout flow.""" import copy import decimal from unittest import mock from botocore import exceptions import pytest from accounting.flows.reserve_payouts.connectors import dynamodb @pytest.mark.parametrize('exception, expected_result, should_raise', [ (exceptions.BotoCoreError(), False, False), (exceptions.ClientError({}, 1), False, False), (Exception, False, True), (None, True, False), ]) @pytest.mark.parametrize('expected_table_name', [ 'test-table_name', 'dev-table-name2' ]) @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.setting') @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.dynamodb') def test_health_check( mock_util_dynamodb, mock_setting, expected_table_name, exception, expected_result, should_raise): """Test health_check function.""" mock_util_dynamodb.get_table_count.return_value = mock.MagicMock() if exception: mock_util_dynamodb.get_table_count.side_effect = exception mock_setting.DYNAMODB_TABLE = expected_table_name if should_raise: with pytest.raises(exception): dynamodb.health_check() else: result = dynamodb.health_check() assert result.bool == expected_result mock_util_dynamodb.get_table_count.assert_called_with(expected_table_name) @pytest.mark.parametrize('item_count', [0, 42, 4242]) @pytest.mark.parametrize('expected_table_name', [ 'test-table_name', 'dev-table-name2' ]) @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.setting') @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.dynamodb') def test_get_table_items_count( mock_util_dynamodb, mock_setting, expected_table_name, item_count): """Test get_table_items_count function.""" mock_util_dynamodb.get_table_count.return_value = item_count mock_setting.DYNAMODB_TABLE = expected_table_name result = dynamodb.get_table_items_count() assert result == item_count mock_util_dynamodb.get_table_count.assert_called_with(expected_table_name) @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.setting') @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.dynamodb') def test_get_table_label_ids(util_dynamodb, setting): """Test get_table_label_ids helper function.""" table_name = 'test_table_name' setting.DYNAMODB_TABLE = table_name mock_table = mock.MagicMock() items = [1, 2, 3] util_dynamodb.full_scan.return_value = {'Items': items} util_dynamodb.get_dynamodb_table.return_value = mock_table assert dynamodb.get_table_label_ids() == items util_dynamodb.get_dynamodb_table.assert_called_with(table_name) util_dynamodb.full_scan.assert_called_with( mock_table, Select='SPECIFIC_ATTRIBUTES', ProjectionExpression='label_id') @pytest.mark.parametrize('data, expected_result, quantize, rounding', [ ( # data {1: 2, 'a': 42.42424242}, # expected_result {1: 2, 'a': decimal.Decimal('42.42')}, decimal.Decimal('.01'), decimal.ROUND_DOWN ), ( {1: 2, 'a': 42.42424242}, {1: 2, 'a': decimal.Decimal('42.42424241')}, decimal.Decimal('.00000000'), decimal.ROUND_DOWN ), ( {1: 2, 'a': 42.42424242}, {1: 2, 'a': decimal.Decimal('42.42424242')}, decimal.Decimal('.00000000'), decimal.ROUND_HALF_UP ), ( {1: 2, 'a': 42.42424242}, {1: 2, 'a': decimal.Decimal('42.424')}, decimal.Decimal('.000'), decimal.ROUND_HALF_UP ), ( {1: 2, 'a': decimal.Decimal('42.42424242')}, {1: 2, 'a': decimal.Decimal('42.424242')}, decimal.Decimal('.000001'), decimal.ROUND_DOWN ), ]) @mock.patch( 'accounting.flows.reserve_payouts.connectors.dynamodb.setting') @mock.patch( 'accounting.flows.reserve_payouts.connectors.dynamodb.dynamodb_constants') def test_format_decimal_fields( constants, setting, data, expected_result, quantize, rounding): """Test type casting utility function format_decimal_fields.""" constants.DECIMAL_FIELDS = ('a', 'b') setting.DECIMAL_QUANTIZE = quantize setting.DECIMAL_ROUNDING = rounding data_copy = copy.deepcopy(data) assert dynamodb.format_decimal_fields(data) == expected_result # ensure that initial data is not affected assert data == data_copy @pytest.mark.parametrize('status', ['INIT', 'INIT2', 'UNPROCESSED']) @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.dynamodb') @mock.patch( 'accounting.flows.reserve_payouts.connectors.dynamodb.dynamodb_constants') def test_construct_vendor_item_default_status( constants, utils_dynamodb, status): """Test construct_vendor_item uses correct constant for default status.""" utils_dynamodb.get_ttl_value_for_item.return_value = 42 constants.DEFAULT_STATUS = status actual_result = dynamodb.construct_vendor_item(1, {}) expected_result = {'label_id': 1, 'processing_status': status, 'ttl': 42} assert actual_result == expected_result @pytest.mark.parametrize('key, vendor_data, expected_result', [ (1, {'field': 'value'}, {'label_id': 1, 'field': 'value', 'processing_status': 'INIT', 'ttl': 42}), (2, {'amount_in_original_currency': 23.32}, {'label_id': 2, 'amount_in_original_currency': decimal.Decimal('23.32'), 'processing_status': 'INIT', 'ttl': 42}), ]) @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.dynamodb') def test_construct_vendor_item( utils_dynamodb, key, vendor_data, expected_result): """Test construct_vendor_item function.""" utils_dynamodb.get_ttl_value_for_item.return_value = 42 actual_result = dynamodb.construct_vendor_item(key, vendor_data) assert actual_result == expected_result @mock.patch( 'accounting.flows.reserve_payouts.connectors.dynamodb' '.construct_vendor_item') @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.dynamodb') @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.setting') def test_batch_write_vendor_data( setting, util_dynamodb, construct_vendor_item): """Test batch_write_vendor_data function.""" table_name = 'test-table' setting.DYNAMODB_TABLE = table_name mock_table = mock.MagicMock() batch_writer = mock.MagicMock() batch_context_manager = mock.MagicMock() batch_writer.__enter__.return_value = batch_context_manager mock_table.batch_writer.return_value = batch_writer util_dynamodb.get_dynamodb_table.return_value = mock_table item_1 = mock.MagicMock() item_2 = mock.MagicMock() construct_vendor_item.side_effect = [item_1, item_2] # ensure that for loop is triggered 2 times. dummy_data = {1: 1, 2: 2} dynamodb.batch_write_vendor_data(dummy_data) util_dynamodb.get_dynamodb_table.assert_called_with(table_name) mock_table.batch_writer.assert_called_once() expected_construct_vendor_item_calls = [ mock.call(1, 1), mock.call(2, 2), ] construct_vendor_item.assert_has_calls( expected_construct_vendor_item_calls, any_order=True) expected_calls = [ mock.call(Item=item_1), mock.call(Item=item_2), ] batch_context_manager.put_item.assert_has_calls( expected_calls, any_order=True) @pytest.mark.parametrize('exception_class, exception', [ (exceptions.BotoCoreError, exceptions.BotoCoreError()), (exceptions.ClientError, exceptions.ClientError({}, 1)), (Exception, Exception()), ]) @mock.patch( 'accounting.flows.reserve_payouts.connectors.dynamodb' '.construct_vendor_item') @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.dynamodb') @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.setting') def test_batch_write_vendor_data_exception_raised( setting, util_dynamodb, construct_vendor_item, exception_class, exception): """Test batch_write_vendor_data raises exception.""" table_name = 'test-table' setting.DYNAMODB_TABLE = table_name mock_table = mock.MagicMock() batch_writer = mock.MagicMock() batch_context_manager = mock.MagicMock() batch_context_manager.put_item.side_effect = exception batch_writer.__enter__.return_value = batch_context_manager mock_table.batch_writer.return_value = batch_writer util_dynamodb.get_dynamodb_table.return_value = mock_table item_1 = mock.MagicMock() item_2 = mock.MagicMock() construct_vendor_item.side_effect = [item_1, item_2] # ensure that for loop is triggered 2 times. dummy_data = {1: 1, 2: 2} with pytest.raises(exception_class): dynamodb.batch_write_vendor_data(dummy_data) @pytest.mark.parametrize('vendor_id, processing_status', [ (10303, 'PROCESSING'), (4242, 'ERROR'), (99999, 'SUCCESS'), ]) @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.conditions') @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.dynamodb') @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.setting') def test_update_vendor_status( setting, util_dynamodb, conditions, vendor_id, processing_status): """Test update_vendor_status function.""" table_name = 'test-table' setting.DYNAMODB_TABLE = table_name mock_key = mock.MagicMock() mock_eq = mock.MagicMock() mock_key.eq.return_value = mock_eq conditions.Key.return_value = mock_key mock_table = mock.MagicMock() mock_update_item_result = mock.MagicMock() mock_table.update_item.return_value = mock_update_item_result util_dynamodb.get_dynamodb_table.return_value = mock_table expected_key = {'label_id': vendor_id} expected_expression = 'SET processing_status = :processing_status' expected_expr_attributes = {':processing_status': processing_status} result = dynamodb.update_vendor_status(vendor_id, processing_status) assert result == mock_update_item_result util_dynamodb.get_dynamodb_table.assert_called_with(table_name) conditions.Key.assert_called_with('label_id') mock_key.eq.assert_called_with(vendor_id) mock_table.update_item.assert_called_with( Key=expected_key, ConditionExpression=mock_eq, ReturnValues='NONE', UpdateExpression=expected_expression, ExpressionAttributeValues=expected_expr_attributes ) @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.dynamodb') def test_update_vendor_status_exception(util_dynamodb): """Test update_vendor_status raises exception.""" mock_table = mock.MagicMock() mock_table.update_item.side_effect = Exception() util_dynamodb.get_dynamodb_table.return_value = mock_table with pytest.raises(Exception): dynamodb.update_vendor_status(4242, 'PROCESSING') @pytest.mark.parametrize('item_count, scanned_count, expected_result', [ (0, 0, True), (2, 2, True), (1, 2, False), (2, 1, False), ]) @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.conditions') @mock.patch( 'accounting.flows.reserve_payouts.connectors.dynamodb.dynamodb_constants') @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.dynamodb') @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.setting') def test_validate_successful_items_count( setting, util_dynamodb, dynamodb_constants, conditions, item_count, scanned_count, expected_result): """Test validate_successful_items_count function.""" table_name = 'test-table' success_const = 'SUCCESS_TEST_STATUS' dynamodb_constants.SUCCESS = success_const setting.DYNAMODB_TABLE = table_name mock_attr = mock.MagicMock() mock_eq = mock.MagicMock() mock_attr.eq.return_value = mock_eq conditions.Attr.return_value = mock_attr mock_table = mock.MagicMock() util_dynamodb.full_scan.return_value = { 'Count': item_count, 'ScannedCount': scanned_count } util_dynamodb.get_dynamodb_table.return_value = mock_table result = dynamodb.validate_successful_items_count() assert result == expected_result util_dynamodb.get_dynamodb_table.assert_called_with(table_name) util_dynamodb.full_scan.assert_called_with( mock_table, Select='COUNT', FilterExpression=mock_eq) conditions.Attr.assert_called_with('processing_status') mock_attr.eq.assert_called_with(success_const) @mock.patch('accounting.flows.reserve_payouts.connectors.dynamodb.dynamodb') def test_validate_successful_items_count_exception(util_dynamodb): """Test validate_successful_items_count raises exception.""" mock_table = mock.MagicMock() util_dynamodb.full_scan.side_effect = Exception() util_dynamodb.get_dynamodb_table.return_value = mock_table with pytest.raises(Exception): dynamodb.validate_successful_items_count()