"""Tests for task model.""" import datetime import importlib from time import sleep import flexmock from oto import response as oto_response from oto import status import pytest from sqlalchemy import exc from availability import config from availability.connectors import sql from availability.constants import error from availability.constants import models from availability.constants import stores from availability.models import product from availability.models import product_in_store from availability.models import store from availability.models import task TEST_STORE_ID = stores.STORE_ID_ITUNES def teardown_function(): """Teardown function that reloads config after every test case.""" importlib.reload(config) @pytest.fixture def task_data(): """Return dictionary with data suitable to create Task object.""" return { 'product_in_store_id': 1, 'status': models.TASK_STATUS_IN_QUEUE, } @pytest.fixture() def product_in_store_instance(): """Fixture for one ProductInStore instance.""" delay_days = 7 store_obj = store.Store( store_id=TEST_STORE_ID, name='test store', polling_delay_days=delay_days, poll_days_after_sales=delay_days) product_obj = product.Product( product_id=1, orchard_product_id=1, upc='819224018247', itunes_vendor_id=42, provider=models.ORCHARD) product_in_store_obj = product_in_store.ProductInStore( product_in_store_id=1, store_id=TEST_STORE_ID, product_id=1, store_internal_id='1', store_internal_status='any', status=models.RELEASE_STATUS_DELIVERED, sales_start_date=datetime.datetime.utcnow(), received_for_polling_date=( datetime.datetime.utcnow() - datetime.timedelta(days=delay_days+1)) ) with sql.session_scope() as session: session.add(store_obj) session.add(product_obj) session.add(product_in_store_obj) results = session.query(product_in_store.ProductInStore).all() assert len(results) == 1 return results[0] @pytest.fixture() def product_in_store_obj(): """Fixture for one ProductInStore instance.""" delay_days = 7 store_obj = store.Store( store_id=TEST_STORE_ID, name='test store', polling_delay_days=delay_days, poll_days_after_sales=delay_days) product_obj = product.Product( product_id=1, orchard_product_id=1, upc='819224018247', itunes_vendor_id=42, provider=models.ORCHARD) product_in_store_obj = product_in_store.ProductInStore( product_in_store_id=1, store_id=TEST_STORE_ID, product_id=1, store_internal_id='', store_internal_status='any', status=models.RELEASE_STATUS_DELIVERED, sales_start_date=datetime.datetime.utcnow(), received_for_polling_date=( datetime.datetime.utcnow() - datetime.timedelta(days=delay_days+1)) ) with sql.session_scope() as session: session.add(store_obj) session.add(product_obj) session.add(product_in_store_obj) results = session.query(product_in_store.ProductInStore).all() assert len(results) == 1 return results[0] def test_task_as_dict(test_database, task_data): """Assert Task object can convert itself to dict.""" task_obj = task.Task(**task_data) with sql.session_scope() as session: session.add(task_obj) with sql.session_scope() as session: task_obj = session.query(task.Task).first() task_data['task_id'] = 1 task_data['last_change_date'] = task_obj.last_change_date assert task_obj.as_dict() == task_data def test_manual_task_last_change_date_raises_exc(test_database, task_data): """Assert can not change Task.last_change_date directly.""" task_obj = task.Task(**task_data) with sql.session_scope() as session: session.add(task_obj) task_obj = session.query(task.Task).first() initial_change_date = task_obj.last_change_date sleep(1) manual_change_date = datetime.datetime.utcnow() with pytest.raises(AttributeError): task_obj.last_change_date = manual_change_date with sql.session_scope() as session: task_obj = session.query(task.Task).first() assert initial_change_date < manual_change_date assert initial_change_date == task_obj.last_change_date def test_create_task(test_database, task_data): """Assert can create task.""" before = datetime.datetime.utcnow() sleep(1) response = task.create_task(**task_data) sleep(1) after = datetime.datetime.utcnow() with sql.session_scope() as session: tasks = session.query(task.Task).all() assert len(tasks) == 1 task_obj = session.query(task.Task).first() assert before < task_obj.last_change_date < after task_data['task_id'] = 1 task_data['last_change_date'] = task_obj.last_change_date assert task_obj.as_dict() == task_data assert isinstance(response, oto_response.Response) assert response.message == task_data def test_create_task_handles_integrity_error(mocker, task_data): """Assert create_task handles IntegrityError.""" mock_task = mocker.patch('availability.models.task.Task') expected_exception = exc.IntegrityError( statement='test', params=None, orig=None) mock_task.side_effect = expected_exception response = task.create_task(**task_data) assert isinstance(response, oto_response.Response) assert response.errors['code'] == error.ERROR_CODE_MODEL_VALIDATION assert response.errors['message'] == str(expected_exception) def test_create_task_handles_sqlalchemy_error(mocker, task_data): """Assert create_task handles SQLAlchemyError.""" mock_task = mocker.patch('availability.models.task.Task') expected_exception = exc.SQLAlchemyError('critical error') mock_task.side_effect = expected_exception response = task.create_task(**task_data) assert isinstance(response, oto_response.Response) assert response.errors['message'] == str(expected_exception) def test_change_status_of_task(test_database, task_data): """Assert can change Task status.""" response = task.create_task(**task_data) creation_date = response.message['last_change_date'] sleep(1) response = task.change_status( response.message['product_in_store_id'], models.TASK_STATUS_OK) sleep(1) after_update_date = datetime.datetime.utcnow() with sql.session_scope() as session: task_obj = session.query(task.Task).first() assert task_obj.status == models.TASK_STATUS_OK assert creation_date < task_obj.last_change_date < after_update_date assert isinstance(response, oto_response.Response) assert response.status == status.NO_CONTENT def test_change_status_handles_not_found_task(test_database): """Assert change_status can properly respond when task not found.""" invalid_product_in_store_id = 111 response = task.change_status( invalid_product_in_store_id, models.TASK_STATUS_OK) assert response.status == 404 assert response.errors['message'] == error.ERROR_TASK_NOT_FOUND def test_change_status_handles_sqlalchemy_error(mocker, task_data): """Assert change_status handles SQLAlchemyError.""" mock_task = mocker.patch('availability.models.task.Task') expected_exception = exc.SQLAlchemyError('critical error') mock_task.side_effect = expected_exception response = task.create_task(**task_data) assert isinstance(response, oto_response.Response) assert response.errors['message'] == str(expected_exception) def test_change_status_of_task_when_multiple_products_exist( test_database, task_data): """Assert only latest task entry changed for given product_in_store.""" first_task = task.create_task(**task_data) second_task = task.create_task(**task_data) task.change_status( second_task.message['product_in_store_id'], models.TASK_STATUS_OK) with sql.session_scope() as session: first_task = session.query(task.Task).filter_by( task_id=first_task.message['task_id']).first() assert first_task.status == task_data['status'] second_task = session.query(task.Task).filter_by( task_id=second_task.message['task_id']).first() assert second_task.status == models.TASK_STATUS_OK @pytest.mark.parametrize( 'task_status, check_status, delta, check_store_id, expected_num_tasks', ( # No match: checking for another status. ( models.TASK_STATUS_IN_QUEUE, models.TASK_STATUS_PROCESSING, +1, TEST_STORE_ID, 0, ), # No match: in PROCESSING for less than the threshold. ( models.TASK_STATUS_PROCESSING, models.TASK_STATUS_PROCESSING, -1, TEST_STORE_ID, 0, ), # No match: another store. ( models.TASK_STATUS_PROCESSING, models.TASK_STATUS_PROCESSING, +1, TEST_STORE_ID + 1, 0, ), # Match: in IN_QUEUE for more than the threshold. ( models.TASK_STATUS_IN_QUEUE, models.TASK_STATUS_IN_QUEUE, +1, TEST_STORE_ID, 1, ), # Match: in PROCESSING for more than the threshold. ( models.TASK_STATUS_PROCESSING, models.TASK_STATUS_PROCESSING, +1, TEST_STORE_ID, 1, ), # Match: we do not check for any specific store. ( models.TASK_STATUS_PROCESSING, models.TASK_STATUS_PROCESSING, +1, None, 1 ), )) def test_get_stuck_tasks( task_status, check_status, delta, check_store_id, expected_num_tasks, test_database, product_in_store_instance): """Test get_stuck_tasks returns expected number of tasks.""" threshold = task.STATUS_TIMEOUT_MAPPING[check_status] last_change_date = datetime.datetime.utcnow() - datetime.timedelta( minutes=threshold + delta) task_obj = task.Task( product_in_store_id=product_in_store_instance.product_in_store_id, status=task_status, ) # Assign to the private attribute, which we should not do normally. task_obj._last_change_date = last_change_date with sql.session_scope() as session: session.add(task_obj) response = task.get_stuck_tasks( task_status=check_status, store_id=check_store_id) assert response assert len(response.message) == expected_num_tasks for item in response.message: assert isinstance(item, tuple) for attr in 'task_id', 'product_in_store_id': assert getattr(item, attr) == getattr(task_obj, attr) @pytest.mark.parametrize( 'task_status, expected_num_tasks', ( (models.TASK_STATUS_PROCESSING, 0), (models.TASK_STATUS_IN_QUEUE, 0), (models.TASK_STATUS_FAILED, 1), )) def test_get_failed_tasks( task_status, expected_num_tasks, test_database, product_in_store_obj): """Test get_failed_tasks returns expected number of tasks.""" task_obj = task.Task( product_in_store_id=product_in_store_obj.product_in_store_id, status=task_status) with sql.session_scope() as session: session.add(task_obj) response = task.get_failed_tasks(store_id=TEST_STORE_ID) assert response assert len(response.message) == expected_num_tasks for item in response.message: assert isinstance(item, tuple) for attr in 'task_id', 'product_in_store_id': assert getattr(item, attr) == getattr(task_obj, attr) def test_get_failed_tasks_passes_date_override(): """Test get_failed_tasks passes date to get_products_to_poll_query.""" kwargs = dict(current_date_override=datetime.datetime.utcnow()) filtered_query = flexmock.flexmock(all=lambda: None) mock_query = flexmock.flexmock(filter=lambda x: filtered_query) (flexmock.flexmock(product_in_store) .should_receive('get_products_to_poll_query') .with_args(TEST_STORE_ID, any_task_status=True, **kwargs) .and_return(mock_query) .once()) task.get_failed_tasks(store_id=TEST_STORE_ID, **kwargs)