"""Tests for consumer of SQS messages.""" import json from moto import mock_aws from oto import response import pytest from availability import config from availability import exceptions import availability.connectors.sqs as sqs_queue from availability.connectors.sqs import JSONMessageExt from availability.constants import models from availability.logic.queue import consumer from tests import utils @pytest.fixture def sqs_message_data(): """Sample SQS message data.""" return { 'store_id': 1, 'product_in_store_id': 122, 'store_internal_id': 211, 'upc': '123456789012', 'task_id': 1 } @pytest.fixture def sqs_message(sqs_message_data): """Sample SQS message.""" with mock_aws(): connection = sqs_queue.get_connection() connection.create_queue(QueueName='test-queue') mock_queue = sqs_queue.get_queue('test-queue', connection) message_body = json.dumps(sqs_message_data) mock_queue.send_message(MessageBody=message_body) yield mock_queue.receive_messages() @pytest.fixture def mock_should_run(mocker): """Mock infinite loop condition to return False on second iteration.""" return mocker.patch( 'availability.logic.queue.consumer.should_run', side_effect=[True, False]) @mock_aws def test_process_messages_reads_only_one_message_at_a_time( mocker): """Assert only one message at a time is read.""" connection = sqs_queue.get_connection() connection.create_queue(QueueName='test-queue') mock_queue = sqs_queue.get_queue('test-queue', connection) message = { 'test': 'test-message' } message_body = json.dumps(message) mock_queue.send_message(MessageBody=message_body) mock_process_single_message = mocker.patch( 'availability.logic.queue.consumer.process_single_message') consumer.process_messages(mock_queue) mock_process_single_message.assert_called_with(message) def test_process_messages_reads_message_with_long_polling( mocker): """Assert long polling is used to retrieve message.""" mock_queue = mocker.Mock() message = [{ 'test': 'test-message' }] mock_queue.receive_messages.return_value = message mocker.patch('availability.logic.queue.consumer.process_single_message') mocker.patch('availability.connectors.sqs.JSONMessageExt.get_body') mocker.patch('availability.connectors.sqs.JSONMessageExt.delete') consumer.process_messages(mock_queue) mock_queue.receive_messages.assert_called_with( WaitTimeSeconds=config.SQS_WAIT_TIME_SECONDS, VisibilityTimeout=config.SQS_MESSAGE_VISIBILITY_TIMEOUT_SECONDS) def test_process_messages_deletes_message_after_read(mocker): """Assert message is deleted after read.""" mock_queue = mocker.Mock() delete_message = [{ 'Id': 'single_message', 'ReceiptHandle': {} }] mock_queue.receive_messages.return_value = delete_message mocker.patch('availability.logic.queue.consumer.process_single_message') mocker.patch('availability.connectors.sqs.JSONMessageExt.get_body') mock_msg_delete = mocker.patch( 'availability.connectors.sqs.JSONMessageExt.delete') consumer.process_messages(mock_queue) mock_msg_delete.assert_called() def test_process_messages_continues_if_no_message_found( mocker): """Assert message is checked whether it is None.""" mock_queue = mocker.Mock() mock_queue.receive_messages.return_value = None consumer.process_messages(mock_queue) mock_queue.delete_message.assert_not_called() def test_process_messages_invokes_single_message_processing( mocker, sqs_message_data): """Assert process_single_message is called with message body.""" mock_process_single_message = mocker.patch( 'availability.logic.queue.consumer.process_single_message') message_body = json.dumps(sqs_message_data) with utils.mock_availability_queue() as queue: queue.send_message(MessageBody=message_body) consumer.process_messages(queue) mock_process_single_message.assert_called_with(sqs_message_data) def test_process_single_message_updates_task_status(mocker, sqs_message_data): """Assert task statuses are updated in correct order.""" mocker.patch('availability.logic.queue.consumer.polling') mock_task = mocker.patch( 'availability.logic.queue.consumer.task') product_in_store_id = sqs_message_data['product_in_store_id'] expected_calls = [ mocker.call(product_in_store_id, models.TASK_STATUS_PROCESSING), mocker.call(product_in_store_id, models.TASK_STATUS_OK) ] consumer.process_single_message(sqs_message_data) mock_task.change_status.assert_has_calls(expected_calls) @pytest.mark.parametrize( ('side_effect', 'expected_final_status'), [ (None, models.TASK_STATUS_OK), (exceptions.UpdateProductStatusError, models.TASK_STATUS_FAILED), (exceptions.StoreRequestError, models.TASK_STATUS_FAILED), (exceptions.StoreResponseParseError, models.TASK_STATUS_FAILED), ] ) def test_process_single_message_sets_correct_task_status( side_effect, expected_final_status, mocker, sqs_message): """Assert task statuses is set to OK on success and FAILED on error.""" mocker.patch( 'availability.logic.queue.consumer.polling.update_product_status', side_effect=side_effect) mock_task = mocker.patch( 'availability.logic.queue.consumer.task') sqs_message = JSONMessageExt(sqs_message[0]) msg_body = sqs_message.get_body() product_in_store_id = msg_body['product_in_store_id'] expected_calls = [ mocker.call(product_in_store_id, models.TASK_STATUS_PROCESSING), mocker.call(product_in_store_id, expected_final_status) ] consumer.process_single_message(msg_body) mock_task.change_status.assert_has_calls(expected_calls) @pytest.mark.parametrize( 'side_effect', [ None, exceptions.UpdateProductStatusError, exceptions.StoreRequestError, exceptions.StoreResponseParseError, ] ) def test_task_status_response_is_checked_for_errors( side_effect, mocker, sqs_message): """Assert response from task.change_status is checked for errors.""" mocker.patch( 'availability.logic.queue.consumer.polling.update_product_status', side_effect=side_effect) mocker.patch( 'availability.logic.queue.consumer.task.change_status', return_value=response.create_fatal_response('something failed')) sqs_message = JSONMessageExt(sqs_message[0]) with pytest.raises(AssertionError): consumer.process_single_message(sqs_message.get_body()) def test_process_single_message_calls_update_product_status( mocker, sqs_message): """Assert update_product_status called with correct arguments.""" mocker.patch('availability.logic.queue.consumer.task') mock_polling = mocker.patch( 'availability.logic.queue.consumer.polling.update_product_status') sqs_message = JSONMessageExt(sqs_message[0]) msg_body = sqs_message.get_body() consumer.process_single_message(msg_body) mock_polling.assert_called_with(**msg_body)