"""Lambda test module.""" import datetime from botocore import exceptions as botocore_exceptions import pytest from constants import common_const from constants import common_fields from constants import fields from constants import sql import index EXISTING_ERROR_ORDER_IDS = ('1', 'two', '3', '4') @pytest.fixture def error_records(): """Fixture for expected get_error_records result.""" return [ { fields.VOO_PK: idx, fields.VOO_ORDER_ID: order_id, fields.VOO_LAST_UPDATED_AT: datetime.datetime(2017, 9, 23, 0, 0), fields.VOO_ERROR_TIMESTAMP: datetime.datetime(2017, 9, 22, 0, 0), } for idx, order_id in enumerate(EXISTING_ERROR_ORDER_IDS) ] # TODO: pass module name and move to common/tests/db_utils def patch_session_scope(mocker, return_value): """Patch session scope function.""" # session mock mocked_session = mocker.Mock() mocked_session.execute.return_value = return_value # context mock mocked_context = mocker.Mock() mocked_context.__enter__ = mocker.Mock( return_value=mocked_session) mocked_context.__exit__ = mocker.Mock(return_value=None) # session scope function mock mocked_session_scope = mocker.patch( 'connectors.vector_obtain_orders.session_scope') mocked_session_scope.return_value = mocked_context return mocked_session @pytest.mark.parametrize( ('error_ids', 'expected_num_records'), ( (EXISTING_ERROR_ORDER_IDS, len(EXISTING_ERROR_ORDER_IDS)), (EXISTING_ERROR_ORDER_IDS[:-1], len(EXISTING_ERROR_ORDER_IDS) - 1), ) ) def test_get_error_records( mocker, error_records, error_ids, expected_num_records): """Test retrieving error records.""" # Prepare mocked DB response data. error_records_iterable = ( r for r in error_records if r[fields.VOO_ORDER_ID] in error_ids) patch_session_scope(mocker, error_records_iterable) result = index.get_error_records(EXISTING_ERROR_ORDER_IDS) assert len(list(result)) == expected_num_records assert list(error_records_iterable) == list(result) @pytest.mark.parametrize( ('update_fails_at', 'resolve_fails_at', 'expected_reprocessed_records'), ( (None, None, error_records()), (0, None, error_records()[1:]), (None, 1, [error_records()[0]] + error_records()[2:]), ) ) def test_trigger_orders_reprocessing( mocker, error_records, update_fails_at, resolve_fails_at, expected_reprocessed_records): """Test error records are updated for reprocessing.""" update_order_error_timestamp_mock = mocker.patch( 'index.update_order_error_timestamp') update_order_error_timestamp_mock.side_effect = [ ValueError if idx == update_fails_at else None for idx, r in enumerate(error_records)] resolve_error_mock = mocker.patch('index.resolve_error') resolve_error_mock.side_effect = [ ValueError if idx == resolve_fails_at else None for idx, r in enumerate(error_records)] mocked_capture_message = mocker.patch( 'index.sentry.sentry_client.captureMessage') expected_reprocessed_order_ids = set([ r[fields.VOO_ORDER_ID] for r in expected_reprocessed_records]) assert expected_reprocessed_order_ids == index.trigger_orders_reprocessing( error_records) assert update_order_error_timestamp_mock.call_count == len(error_records) assert update_order_error_timestamp_mock.call_args_list == [ ( { 'vector_order_id': e[fields.VOO_ORDER_ID], 'timestamp': e[fields.VOO_ERROR_TIMESTAMP].strftime( common_const.JSON_DATETIME_FORMAT) }, ) for e in error_records] # resolve_errors() is expected to be called for every error record if there # was no error during the update. assert resolve_error_mock.call_args_list == [ ((e[fields.VOO_PK],),) for idx, e in enumerate(error_records) if idx != update_fails_at] assert mocked_capture_message.call_count == ( len(error_records) - len(expected_reprocessed_records)) @pytest.mark.parametrize( ('exc', 'is_expected_to_raise'), ( (None, False), (IndexError(), True), ( botocore_exceptions.ClientError( error_response={ 'Error': {'Code': 'ConditionalCheckFailedException'}}, operation_name=''), False ), ( botocore_exceptions.ClientError( error_response={}, operation_name=''), True ), ) ) def test_update_order_error_timestamp(mocker, exc, is_expected_to_raise): """Test error records are updated for reprocessing.""" dynamodb_mock = mocker.Mock() get_dynamodb_mock = mocker.patch('index.get_dynamodb') get_dynamodb_mock.return_value = dynamodb_mock table_mock = mocker.Mock() dynamodb_mock.Table.return_value = table_mock orders_table_name = mocker.patch('dynamodb_config.ORDERS_DDB_TABLE') table_mock.update_item = mocker.Mock() conditions_mock = mocker.patch('index.conditions') key_mock = mocker.Mock() conditions_mock.Key = key_mock vector_order_id = 'foo' error_timestamp = datetime.datetime.utcnow().strftime( common_const.JSON_DATETIME_FORMAT) if exc: table_mock.update_item.side_effect = exc if is_expected_to_raise: with pytest.raises(type(exc)): index.update_order_error_timestamp( vector_order_id=vector_order_id, timestamp=error_timestamp) else: index.update_order_error_timestamp( vector_order_id=vector_order_id, timestamp=error_timestamp) assert get_dynamodb_mock.call_count == 1 get_dynamodb_mock.assert_called_with() assert dynamodb_mock.Table.call_count == 1 dynamodb_mock.Table.assert_called_with(orders_table_name) assert table_mock.update_item.call_count == 1 assert table_mock.update_item.call_args == ({ 'Key': {common_fields.VO_ORDER_ID: vector_order_id}, 'ConditionExpression': key_mock( common_fields.VO_ORDER_ID).eq(vector_order_id), 'ReturnValues': 'UPDATED_NEW', 'UpdateExpression': 'SET {field} = :timestamp'.format( field=common_fields.VO_ERROR_TIMESTAMP), 'ExpressionAttributeValues': {':timestamp': error_timestamp}, },) def test_resolve_error(mocker): """Test resolving error record.""" # Prepare mocked DB response data. num_matched_rows = 1 error_record_id = 42 result = mocker.Mock() result.rowcount = num_matched_rows mocked_session = patch_session_scope(mocker, result) sql_text = 'foo' mocked_sqlalchemy_text = mocker.patch('index.sqlalchemy.text') mocked_sqlalchemy_text.return_value = sql_text logger_debug_mock = mocker.patch('index.common_config.logger.debug') index.resolve_error(error_record_id) assert mocked_sqlalchemy_text.called_with( sql.UPDATE_VECTOR_ORDER_SYNC_ERRORS_SET_RESOLVED) assert mocked_session.called_with((sql_text, error_record_id)) # We call logger.debug with parameters to format messages two times: # with error_record_id and then with num_matched_rows. debug_parameters = [ args[0][1] for args in logger_debug_mock.call_args_list] assert debug_parameters == [error_record_id, num_matched_rows] @pytest.mark.parametrize(('do_reprocess',), ((True,), (False,))) def test_handler(mocker, error_records, do_reprocess): """Test main handler method.""" # We select only a subset of records. passed_orders_limit = 3 # Pretend that records might not exist in the DB. existing_records_limit = passed_orders_limit - 1 # Pretend that some records were not found in the original DynamoDB table. reprocessed_orders_limit = existing_records_limit - 1 # IDs we pass to the main handler. passed_order_ids = [ err['vector_order_id'] for err in error_records[:passed_orders_limit]] get_error_records_mock = mocker.patch('index.get_error_records') get_error_records_mock.return_value = error_records[ :existing_records_limit] resolve_error_mock = mocker.patch('index.resolve_error') trigger_orders_reprocessing_mock = mocker.patch( 'index.trigger_orders_reprocessing') trigger_orders_reprocessing_mock.return_value = set(passed_order_ids[ :reprocessed_orders_limit]) handler_payload = dict( order_ids=passed_order_ids, do_reprocess=do_reprocess) index.handler(handler_payload, None) if do_reprocess: trigger_orders_reprocessing_mock.assert_called_with( error_records[:existing_records_limit]) return assert not trigger_orders_reprocessing_mock.called assert resolve_error_mock.call_count == existing_records_limit assert resolve_error_mock.call_args_list == [ ((e[fields.VOO_PK],),) for e in error_records[:existing_records_limit]]