import copy import time from unittest.mock import MagicMock, PropertyMock, patch from unittest.mock import call from botocore.exceptions import ClientError from garcon_contrib.aws import garcon_dynamodb import pytest @pytest.fixture def throughput_settings(): """Basic table throughput settings.""" return { 'throughput': { 'read': 1, 'write': 3 }, 'global_indexes': { 'gsi-index': { 'read': 3, 'write': 1}} } @pytest.fixture def table_with_description(throughput_settings): """Abridged version of DynamoDB table description.""" class Table: def __init__(self): self.provisioned_throughput = { 'LastDecreaseDateTime': 1542207327.997, 'WriteCapacityUnits': throughput_settings['throughput']['write'], 'NumberOfDecreasesToday': 1, 'ReadCapacityUnits': throughput_settings['throughput']['read'], 'LastIncreaseDateTime': 1542197608.957} self.global_secondary_indexes = [{ 'IndexName': 'gsi-index', 'Projection': {'ProjectionType': 'ALL'}, 'ItemCount': 0, 'ProvisionedThroughput': { 'LastDecreaseDateTime': 1542207498.857, 'WriteCapacityUnits': throughput_settings[ 'global_indexes']['gsi-index']['write'], 'NumberOfDecreasesToday': 3, 'ReadCapacityUnits': throughput_settings[ 'global_indexes']['gsi-index']['read'], 'LastIncreaseDateTime': 1542207498.857}}, ] return Table() @pytest.fixture def new_throughput_value(): """New throughput value different from all existing.""" @pytest.fixture def time_mock(monkeypatch): """Mock time module.""" sleep_mock = MagicMock() monkeypatch.setattr(time, 'sleep', sleep_mock) return sleep_mock @patch.object(garcon_dynamodb, "boto3") @patch.object(garcon_dynamodb, "time") def test__sleep_if_table_is_busy(time_mock, boto3_mock): """Test _sleep_if_table_is_busy.""" activity_mock = MagicMock() table_model_mock = boto3_mock.resource.return_value.Table.return_value table_model_mock.table_name = 'some_table' values = ['UPDATING', 'UPDATING', 'READY'] type(table_model_mock).table_status = PropertyMock(side_effect=values) returned = garcon_dynamodb._sleep_if_table_is_busy( table_model_mock, activity=activity_mock) assert returned == table_model_mock assert activity_mock.logger.info.call_args_list == [ call('Waiting for the DynamoDB table some_table' ' to be ready (current status: UPDATING)'), call('Waiting for the DynamoDB table some_table' ' to be ready (current status: UPDATING)'), ] assert activity_mock.heartbeat.call_args_list == [ call(details='Table some_table is not ready yet'), call(details='Table some_table is not ready yet'), ] assert time_mock.sleep.call_count == 2 def test__update_table(throughput_settings): """Test _update_table.""" table_model_mock = MagicMock() garcon_dynamodb._update_table( table_model_mock, throughput_settings, MagicMock()) table_model_mock.update.assert_called_once_with( **throughput_settings) def test_update_table_params_success(throughput_settings, time_mock): """Test update_table_params.""" table_model_mock = MagicMock() garcon_dynamodb._update_table_params( table_model_mock, throughput_settings) table_model_mock.update.assert_called_once_with( **throughput_settings) def test_update_table_params_with_one_exception( monkeypatch, throughput_settings, time_mock): """Test update_table_params.""" table_model_mock = MagicMock() exception = ClientError( error_response={ 'Error': { 'Code': 'ThrottlingException', 'Message': "Doesn't matter."} }, operation_name='UpdateTable') _update_table_mock = MagicMock(side_effect=[exception, True]) table_model_mock.update = _update_table_mock monkeypatch.setattr( garcon_dynamodb, '_settings_for_update', MagicMock(return_value=throughput_settings)) monkeypatch.setattr( garcon_dynamodb, '_sleep_if_table_is_busy', MagicMock()) garcon_dynamodb._update_table_params( table_model_mock, throughput_settings, MagicMock()) _update_table_mock.assert_has_calls([ call(**throughput_settings), call(**throughput_settings)]) def test__table_current_settings(table_with_description, throughput_settings): """Test DynamoDB description transformation by _table_current_settings.""" expected_result = throughput_settings result = garcon_dynamodb._table_current_settings(table_with_description) assert result == expected_result def test__settings_for_update_same_table_and_index_throughput( table_with_description, throughput_settings): """Test _settings_for_update when there is nothing to update.""" result = garcon_dynamodb._settings_for_update( table_with_description, throughput_settings) assert result == {} def test__settings_for_update_diff_table_and_same_index_throughput( table_with_description, throughput_settings): """Test _settings_for_update when table throughput has to be updated.""" new_settings = copy.deepcopy(throughput_settings) new_settings['throughput']['read'] = 4 expected_result = dict(ProvisionedThroughput={ 'ReadCapacityUnits': new_settings['throughput']['read'], 'WriteCapacityUnits': 3}) result = garcon_dynamodb._settings_for_update( table_with_description, new_settings) assert result == expected_result def test__settings_for_update_same_table_and_diff_index_throughput( table_with_description, throughput_settings): """Test _settings_for_update when index throughput has to be updated.""" new_settings = copy.deepcopy(throughput_settings) new_settings['global_indexes']['gsi-index']['read'] = 4 expected_result = dict(GlobalSecondaryIndexUpdates=[ {'Update': { 'IndexName': 'gsi-index', 'ProvisionedThroughput': { 'ReadCapacityUnits': new_settings['global_indexes']['gsi-index']['read'], 'WriteCapacityUnits': 1 }}}]) result = garcon_dynamodb._settings_for_update( table_with_description, new_settings) assert result == expected_result def test__settings_for_update_change_throughput_write_only( table_with_description, throughput_settings): """Test _settings_for_update when table read throughput wasn't passed.""" new_settings = copy.deepcopy(throughput_settings) new_settings['throughput']['write'] = 4 del new_settings['throughput']['read'] expected_result = {'ProvisionedThroughput': { 'ReadCapacityUnits': throughput_settings['throughput']['read'], 'WriteCapacityUnits': new_settings['throughput']['write'] }} result = garcon_dynamodb._settings_for_update( table_with_description, new_settings) assert result == expected_result def test__settings_for_update_change_index_read_only( table_with_description, throughput_settings): """Test _settings_for_update when index write throughput wasn't passed.""" new_settings = copy.deepcopy(throughput_settings) new_settings['global_indexes']['gsi-index']['read'] = 4 del new_settings['global_indexes']['gsi-index']['write'] expected_result = {'GlobalSecondaryIndexUpdates': [ {'Update': { 'IndexName': 'gsi-index', 'ProvisionedThroughput': { 'ReadCapacityUnits': new_settings['global_indexes']['gsi-index']['read'], 'WriteCapacityUnits': throughput_settings['global_indexes'] ['gsi-index']['write']}}} ]} result = garcon_dynamodb._settings_for_update( table_with_description, new_settings) assert result == expected_result def test_update_table_params_with_six_exceptions( monkeypatch, throughput_settings, time_mock): """Test update_table_params when 6 ThrottlingException in row.""" table_model_mock = MagicMock() exception = ClientError( error_response={ 'Error': { 'Code': 'ThrottlingException', 'Message': "Doesn't matter."} }, operation_name='UpdateTable') _update_table_mock = MagicMock( side_effect=[ exception, exception, exception, exception, exception, exception, True]) table_model_mock.update = _update_table_mock monkeypatch.setattr( garcon_dynamodb, '_settings_for_update', MagicMock(return_value=throughput_settings)) monkeypatch.setattr( garcon_dynamodb, '_sleep_if_table_is_busy', MagicMock()) with pytest.raises(garcon_dynamodb.DynamoUpdateError): garcon_dynamodb._update_table_params( table_model_mock, throughput_settings, MagicMock()) @patch.object(garcon_dynamodb.boto3, "resource") def test_wait_for_table_task_completion(resource_mock, monkeypatch): """Test wait_for_table_task_completion.""" activity_mock = MagicMock() _sleep_if_table_is_busy_mock = MagicMock() monkeypatch.setattr( garcon_dynamodb, '_sleep_if_table_is_busy', _sleep_if_table_is_busy_mock) garcon_dynamodb.wait_for_table_task_completion(activity_mock, 'some_table') _sleep_if_table_is_busy_mock.assert_called_with( 'some_table', activity=activity_mock)