"""Store state logic unit tests.""" from decimal import Decimal from operator import attrgetter import pytest from freezegun import freeze_time from src.errors.feed_status import LoadFeedStatusError from src.errors.store_state import StoreStateLoadingError, StoreStateProcessingError, StoreStateSavingError from src.logic.store_state import get_latest_store_states, get_updated_states, load_store_states, save_store_states from src.models.store import Store from src.models.store_state import StoreState from tests.unit import data @pytest.mark.parametrize( 'store_states_kwargs_list, expected', ( pytest.param( [ dict(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ), ], { Store.SPOTIFY: { 'store_id': Decimal(286), 'last_available_date': data.DATE_STR, 'updated_at': data.TIME_STR, } }, id='single', ), pytest.param( [ dict(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ), dict(store=Store.APPLE_MUSIC, last_available_date=data.DATE_OBJ), ], { Store.SPOTIFY: { 'store_id': Decimal(286), 'last_available_date': data.DATE_STR, 'updated_at': data.TIME_STR, }, Store.APPLE_MUSIC: { 'store_id': Decimal(1), 'last_available_date': data.DATE_STR, 'updated_at': data.TIME_STR, }, }, id='multiple', ), ), ) def test_save_store_state_success(store_states_kwargs_list, expected, mocked_dynamodb_table): """Test saving store state success.""" store_states = [] with freeze_time(data.TIME_STR): store_states = [StoreState(**kwargs) for kwargs in store_states_kwargs_list] save_store_states(store_states) for store, expected_values in expected.items(): response = mocked_dynamodb_table.get_item(Key={'store_id': store.value}) item = response.get('Item') assert item == expected_values @pytest.mark.parametrize( 'store_states, expected, mocked_dynamodb_table', ( pytest.param([], 'No store states to save.', True, id='empty'), pytest.param( [ StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ), StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ), ], 'Store states are not unique.', True, id='not_unique', ), pytest.param( [ StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ), ], 'Could not save states in batch.', False, id='no_table', ), ), indirect=['mocked_dynamodb_table'], ) def test_save_store_state_failure(store_states, expected, mocked_dynamodb_table): """Test saving store state failure.""" with pytest.raises(StoreStateSavingError) as excinfo: save_store_states(store_states) assert str(excinfo.value) == expected @pytest.mark.parametrize( 'stores', ( pytest.param( [ Store.SPOTIFY, ], id='single', ), pytest.param( [ Store.SPOTIFY, Store.APPLE_MUSIC, ], id='multiple', ), ), ) def test_load_store_state_success(stores, mocked_dynamodb_table): """Test loading store state success.""" for store in stores: mocked_dynamodb_table.put_item( Item={ 'store_id': store.value, 'last_available_date': data.DATE_STR, 'updated_at': data.TIME_STR, } ) states = load_store_states(stores) assert states == [ StoreState(store=store, last_available_date=data.DATE_OBJ, updated_at=data.TIME_OBJ) for store in stores ] @pytest.mark.parametrize( 'stores, expected, mocked_dynamodb_table', ( pytest.param( [], 'No stores to load.', True, id='empty', ), pytest.param( [ Store.SPOTIFY, Store.SPOTIFY, ], 'Stores are not unique.', True, id='not_unique', ), pytest.param( [Store.SPOTIFY], 'Could not load states in batch.', False, id='no_table', ), ), indirect=['mocked_dynamodb_table'], ) def test_load_state_failure(stores, expected, mocked_dynamodb_table): """Test loading store state failure.""" with pytest.raises(StoreStateLoadingError) as excinfo: load_store_states(stores) assert str(excinfo.value) == expected @pytest.mark.parametrize( 'stores_to_request, mock_get_feed_statuses, expected_result', [ pytest.param( [Store.SPOTIFY], {'return_value': data.MOCK_FEEDS_POOL}, [StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ)], id='single_store_found', ), pytest.param( [Store.SPOTIFY, Store.PANDORA], {'return_value': data.MOCK_FEEDS_POOL}, [ StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ), StoreState(store=Store.PANDORA, last_available_date=data.DATE_OBJ_2), ], id='multiple_stores_found', ), pytest.param( [Store.APPLE_MUSIC], {'return_value': data.MOCK_FEEDS_POOL}, [StoreState(store=Store.APPLE_MUSIC, last_available_date=data.DATE_OBJ_1)], id='store_with_multiple_feeds_returns_earliest_date', ), pytest.param([Store.DEEZER], {'return_value': data.MOCK_FEEDS_POOL}, [], id='store_not_in_feeds_returns_empty'), pytest.param( [Store.AMAZON_MUSIC], {'return_value': data.MOCK_FEEDS_POOL}, [], id='store_with_only_excluded_feeds_is_not_returned', ), ], indirect=['mock_get_feed_statuses'], ) def test_get_latest_store_states_success(stores_to_request, mock_get_feed_statuses, expected_result): """Test successful scenarios for get_latest_store_states.""" result = get_latest_store_states(stores_to_request) result_sorted = sorted(result, key=attrgetter('store')) expected_sorted = sorted(expected_result, key=attrgetter('store')) assert result_sorted == expected_sorted @pytest.mark.parametrize( 'stores_to_request, mock_get_feed_statuses, expected_error, expected_error_message', [ pytest.param([], {'return_value': []}, StoreStateLoadingError, 'No stores to load.', id='empty_store_list'), pytest.param( [Store.SPOTIFY, Store.APPLE_MUSIC, Store.SPOTIFY], {'return_value': []}, StoreStateLoadingError, 'Stores are not unique.', id='duplicate_stores_in_list', ), pytest.param( [Store.SPOTIFY], # Input is valid {'side_effect': LoadFeedStatusError('API fetch failed')}, LoadFeedStatusError, 'API fetch failed', id='dependency_failure_get_feed_statuses', ), ], indirect=['mock_get_feed_statuses'], ) def test_get_latest_store_states_failure( stores_to_request, mock_get_feed_statuses, expected_error, expected_error_message ): """Test failure scenarios for get_latest_store_states.""" with pytest.raises(expected_error) as excinfo: get_latest_store_states(stores_to_request) assert str(excinfo.value) == expected_error_message @pytest.mark.parametrize( 'new_states, existing_states, expected_result', [ pytest.param( [StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ)], [StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ_1)], [StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ)], id='new_state_is_more_recent', ), pytest.param( [StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ_1)], [StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ)], [], id='new_state_is_older', ), pytest.param( [StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ)], [StoreState(store=Store.APPLE_MUSIC, last_available_date=data.DATE_OBJ_1)], [StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ)], id='new_state_for_store_that_does_not_exist', ), pytest.param( [StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ)], [StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ)], [], id='new_state_has_same_date', ), pytest.param( [], [StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ_1)], [], id='new_state_list_is_empty' ), pytest.param( [StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ)], [], [StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ)], id='existing_state_list_is_empty', ), pytest.param( [ StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ), StoreState(store=Store.APPLE_MUSIC, last_available_date=data.DATE_OBJ_1), StoreState(store=Store.PANDORA, last_available_date=data.DATE_OBJ), ], [ StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ_1), StoreState(store=Store.APPLE_MUSIC, last_available_date=data.DATE_OBJ), ], [ StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ), StoreState(store=Store.PANDORA, last_available_date=data.DATE_OBJ), ], id='mixed_scenario', ), ], ) def test_get_updated_states_success(new_states, existing_states, expected_result): """Test successful scenarios for get_updated_states.""" result = get_updated_states(new_states, existing_states) result_sorted = sorted(result, key=attrgetter('store')) expected_sorted = sorted(expected_result, key=attrgetter('store')) assert result_sorted == expected_sorted @pytest.mark.parametrize( 'new_states, existing_states, expected_error_message', [ pytest.param( [ StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ), StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ_1), ], [], 'New store states are not unique.', id='duplicates_in_new_states', ), pytest.param( [ StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ), ], [ StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ), StoreState(store=Store.SPOTIFY, last_available_date=data.DATE_OBJ_1), ], 'Existed store states are not unique.', id='duplicates_in_existing_states', ), ], ) def test_get_updated_states_failure(new_states, existing_states, expected_error_message): """Test failure scenarios for get_updated_states.""" with pytest.raises(StoreStateProcessingError) as excinfo: get_updated_states(new_states, existing_states) assert str(excinfo.value) == expected_error_message