"""Unit tests for aggregate_stats logic layer.""" from datetime import datetime from unittest.mock import MagicMock from oto import response as oto_response from analytics.consts import analytics from analytics.logic import aggregate_stats from analytics.models import aggregate_stats as aggregate_stats_model from analytics.utils import date def test_fetch_transactions_subset(monkeypatch): """Try to fetch only a passed subset of valid transaction types.""" unprocessed_transactions = ['UT', 'UV', 'AV'] format_response = MagicMock() aggregate_stats_date_start = '2014-10-16' aggregate_stats_date_end = '2014-10-22' aggregate_stats_entity_id = '981883' aggregate_stats_entity_type = 'L' aggregate_stats_trans_types = unprocessed_transactions def fetch_stats( entity_type, entity_id, transaction_type, date_start, date_end): unprocessed_transactions.remove(transaction_type) assert entity_type == aggregate_stats_entity_type assert entity_id == aggregate_stats_entity_id assert date_start == aggregate_stats_date_start assert date_end == aggregate_stats_date_end return oto_response.Response(message=entity_type, status=200) monkeypatch.setattr(aggregate_stats, 'fetch_stats', fetch_stats) response = aggregate_stats.fetch_transactions( aggregate_stats_entity_type, aggregate_stats_entity_id, aggregate_stats_date_start, aggregate_stats_date_end, aggregate_stats_trans_types, format=format_response) assert not len(unprocessed_transactions) assert len(response.message) == 3 assert format_response.call_count == 3 def test_fetch_transactions_all_bad_types(monkeypatch): """Test fetch_transactions using invalid transaction type params.""" format_response = MagicMock() aggregate_stats_date_start = '2014-10-16' aggregate_stats_date_end = '2014-10-22' aggregate_stats_entity_id = '981883' aggregate_stats_entity_type = 'L' aggregate_stats_trans_types = 'ABEEFF,WEFWEFW,WGWGERGR,PTYJOTYJ'.split(',') unprocessed_transactions = [ transaction.abbr for transaction in analytics.TRANSACTION_TYPES.values()] def fetch_stats( entity_type, entity_id, transaction_type, date_start, date_end): unprocessed_transactions.remove(transaction_type) assert entity_type == aggregate_stats_entity_type assert entity_id == aggregate_stats_entity_id assert date_start == aggregate_stats_date_start assert date_end == aggregate_stats_date_end return oto_response.Response(message=entity_type, status=200) monkeypatch.setattr(aggregate_stats, 'fetch_stats', fetch_stats) response = aggregate_stats.fetch_transactions( aggregate_stats_entity_type, aggregate_stats_entity_id, aggregate_stats_date_start, aggregate_stats_date_end, aggregate_stats_trans_types, format=format_response) assert len(response.message) == len(analytics.TRANSACTION_TYPES) assert format_response.call_count == len(analytics.TRANSACTION_TYPES) assert not len(unprocessed_transactions) def test_fetch_one_transaction_type(monkeypatch): """Test fetch_transactions using one valid transaction type.""" unprocessed_transactions = ['UT'] format_response = MagicMock() aggregate_stats_date_start = '2014-10-16' aggregate_stats_date_end = '2014-10-22' aggregate_stats_entity_id = '981883' aggregate_stats_entity_type = 'L' aggregate_stats_trans_types = unprocessed_transactions def fetch_stats( entity_type, entity_id, transaction_type, date_start, date_end): unprocessed_transactions.remove(transaction_type) assert entity_type == aggregate_stats_entity_type assert entity_id == aggregate_stats_entity_id assert date_start == aggregate_stats_date_start assert date_end == aggregate_stats_date_end return oto_response.Response(message=entity_type, status=200) monkeypatch.setattr(aggregate_stats, 'fetch_stats', fetch_stats) response = aggregate_stats.fetch_transactions( aggregate_stats_entity_type, aggregate_stats_entity_id, aggregate_stats_date_start, aggregate_stats_date_end, aggregate_stats_trans_types, format=format_response) assert not len(unprocessed_transactions) assert len(response.message) == 1 assert format_response.call_count == 1 def test_fetch_transactions(monkeypatch): """Test fetch_transactions using empty list for transaction types.""" format_response = MagicMock() unprocessed_transactions = [ transaction.abbr for transaction in analytics.TRANSACTION_TYPES.values()] aggregate_stats_date_start = '2014-10-16' aggregate_stats_date_end = '2014-10-22' aggregate_stats_entity_id = '981883' aggregate_stats_entity_type = 'L' aggregate_stats_trans_types = [] def fetch_stats( entity_type, entity_id, transaction_type, date_start, date_end): unprocessed_transactions.remove(transaction_type) assert entity_type == aggregate_stats_entity_type assert entity_id == aggregate_stats_entity_id assert date_start == aggregate_stats_date_start assert date_end == aggregate_stats_date_end return oto_response.Response(message=entity_type, status=200) monkeypatch.setattr(aggregate_stats, 'fetch_stats', fetch_stats) response = aggregate_stats.fetch_transactions( aggregate_stats_entity_type, aggregate_stats_entity_id, aggregate_stats_date_start, aggregate_stats_date_end, aggregate_stats_trans_types, format=format_response) assert len(response.message) == len(analytics.TRANSACTION_TYPES) assert format_response.call_count == len(analytics.TRANSACTION_TYPES) assert not len(unprocessed_transactions) def test_fetch_stats_with_wrong_entity_type(): """Try to fetch the stats with a wrong entity type.""" response = aggregate_stats.fetch_stats( 'random', 'entity_id', 'transaction_type', '2014-10-01', '2014-10-12') assert response.status == 404 assert response.errors == 'random is not a valid index' def test_fetch_stats_with_wrong_dates(): """Try to fetch the stats with a wrong date order.""" response = aggregate_stats.fetch_stats( analytics.EntityTypes.LABEL, 'entity_id', 'transaction_type', '2014-10-01', '2013-10-01') assert response.errors assert response.status == 400 def test_fetch_stats_for_more_than_36_months(): """Try to fetch the stats for more than 13 months should fail.""" response = aggregate_stats.fetch_stats( analytics.EntityTypes.LABEL, 'label_id', 'transaction_type', '2011-11-01', '2015-02-01') assert response.errors assert response.status == 400 def test_fetch_stats(monkeypatch): """Try fetching stats.""" mock = MagicMock(return_value=oto_response.Response(message=['value'])) monkeypatch.setattr(aggregate_stats, '_fetch_stats_for_date_range', mock) response = aggregate_stats.fetch_stats( analytics.EntityTypes.LABEL, 'label_id', 'transaction_type', '2014-10-04', '2015-01-03') assert mock.called assert mock.call_count == 2 # (oct, nov, dec) and (jan) assert len(response.message) == 2 # matches the number of mock calls. assert response def test_fetch_stats_with_failure(monkeypatch): """Try fetching stats.""" mock = MagicMock(return_value=oto_response.Response( errors=['value'], status=500)) monkeypatch.setattr(aggregate_stats, '_fetch_stats_for_date_range', mock) response = aggregate_stats.fetch_stats( analytics.EntityTypes.LABEL, 'label_id', 'transaction_type', '2014-10-04', '2015-01-03') assert mock.called assert mock.call_count == 2 # (oct, nov, dec) and (jan) assert not response def test_fetch_stats_with_date_range(monkeypatch): """Try fetching stats.""" mock = MagicMock(return_value=oto_response.Response(message=[])) monkeypatch.setattr(aggregate_stats, '_fetch_stats_for_date_range', mock) aggregate_stats.fetch_stats( analytics.EntityTypes.LABEL, 'label_id', 'transaction_type', '2014-10-04', '2015-01-03') assert mock.called assert mock.call_count == 2 # (oct, nov, dec) and (jan) assert mock.mock_calls[0][2].get('start_date').day == 4 assert mock.mock_calls[1][2].get('end_date').day == 3 def test_fetch_stats_for_month(monkeypatch): """Fetch data for a month.""" mock = MagicMock(return_value=oto_response.Response()) monkeypatch.setattr(aggregate_stats_model, 'fetch', mock) start_date = datetime.now() end_date = datetime.now() label_id = 10 transaction_type = 'S' response = aggregate_stats._fetch_stats_for_date_range( analytics.EntityTypes.LABEL, label_id, transaction_type, start_date=start_date, end_date=end_date) assert response assert aggregate_stats_model.fetch.called mock_calls = aggregate_stats_model.fetch.mock_calls # Check the args assert mock_calls[0][1][0] == analytics.EntityTypes.LABEL assert mock_calls[0][1][1] == label_id assert mock_calls[0][1][2] == transaction_type assert len(mock_calls[0][1][3]) == 2 assert isinstance(mock_calls[0][1][3], tuple) def test_fetch_stats_for_month_with_dates(monkeypatch): """Fetch stats for month with a missing range key. It defaults to the date. """ mock = MagicMock(return_value=oto_response.Response()) monkeypatch.setattr(aggregate_stats_model, 'fetch', mock) today = datetime.now() label_id = 10 transaction_type = 'S' start_date = datetime(today.year, today.month, 4) end_date = datetime(today.year, today.month, 23) aggregate_stats._fetch_stats_for_date_range( analytics.EntityTypes.LABEL, label_id, transaction_type, start_date=start_date, end_date=end_date) mock_calls = aggregate_stats_model.fetch.mock_calls assert mock_calls[0][1][3][0] == ( '{year}-{month}'.format( year=start_date.year, month='%02d' % start_date.month)) aggregate_stats._fetch_stats_for_date_range( analytics.EntityTypes.LABEL, label_id, transaction_type, start_date=start_date, end_date=end_date) mock_calls = aggregate_stats_model.fetch.mock_calls assert mock_calls[1][1][3][1] == ( '{year}-{month}'.format( year=end_date.year, month='%02d' % end_date.month)) def test_format_response_for_analytics_frontend_with_no_data(): """Test format_response_for_analytics_frontend_with_no_data. If the response data is empty – or the data does not have any source, the response should be empty. """ response = aggregate_stats.format_response_for_analytics_frontend( dict(date_start='2014-10-20', date_end='2014-10-25')) assert not response response = aggregate_stats.format_response_for_analytics_frontend( dict(source=None, date_start='2014-10-20', date_end='2014-10-25')) assert not response def test_format_response(): """Test format_response. The format methods has more than one responsability: it fills the gaps for the missing dates, and it sums up data. """ transaction_id = 1 transaction_type = analytics.TRANSACTION_TYPES.get(transaction_id) label_id = 24060 metric_name = 'L:{}:{}'.format(label_id, transaction_type.abbr) data = dict( transaction_type_id=transaction_id, transaction_type=transaction_type, date_start='2014-02-03', date_end='2014-02-10', source=[{ 'metric_name': metric_name, 'month': '2014-02', '05': dict(all=4, paid=1), '09': dict(all=93, paid=5), '04': dict(all=59, paid=0)}]) response = aggregate_stats.format_response(data) # 03-10 included is 8 days assert response.get('total_units') == (4 + 93 + 59) assert response.get('transaction_abbr') == transaction_type.abbr assert response.get('transaction_name') == transaction_type.name assert response.get('sequence_id') == transaction_type.sequence_id expected_series = { date.get(day).timestamp(): val for day, val in { '2014-02-03': 0, '2014-02-04': 59, '2014-02-05': 4, '2014-02-06': 0, '2014-02-07': 0, '2014-02-08': 0, '2014-02-09': 93, '2014-02-10': 0 }.items()} assert dict(response.get('series')) == expected_series def test_format_response_for_analytics_without_data(): """Test formating the response without data.""" assert not aggregate_stats.format_response(None) def test_format_response_for_analytics_frontend(): """Test format_response_for_analytics_frontend. The format methods has more than one responsability: it fills the gaps for the missing dates, and it sums up data. """ transaction_id = 1 transaction_type = analytics.TRANSACTION_TYPES.get(transaction_id) label_id = 24060 metric_name = 'L:{}:{}'.format(label_id, transaction_type.abbr) data = dict( transaction_type_id=transaction_id, transaction_type=transaction_type, date_start='2014-02-03', date_end='2014-02-10', source=[{ 'metric_name': metric_name, 'month': '2014-02', '05': dict(all=4, paid=1), '09': dict(all=93, paid=5), '04': dict(all=59, paid=0)}]) response = aggregate_stats.format_response_for_analytics_frontend(data) assert len(response.get('lines', [])) == 1 current_line = response.get('lines')[0] # 03-10 included is 8 days assert current_line.get('activities') == response.get('activities') assert len(current_line.get('total_reported_sales_count')) == 8 assert current_line.get('activities') == (4 + 93 + 59) assert current_line.get('activities_paid') == (1 + 5 + 0) assert (current_line.get('all_line_activities') == current_line.get('activities')) expected_line = [ ('2014-02-03', 0), ('2014-02-04', 59), ('2014-02-05', 4), ('2014-02-06', 0), ('2014-02-07', 0), ('2014-02-08', 0), ('2014-02-09', 93), ('2014-02-10', 0)] for i, line in enumerate(expected_line): current_metric = current_line.get('total_reported_sales_count')[i] assert current_metric[0] == int(date.get(line[0]).timestamp()) assert current_metric[1] == line[1] def test_get_selected_trans_types(): """Test filter_transaction_types will return the 4 passed types.""" requested_trans_types = 'S,SR,VB,VR'.split(',') res = aggregate_stats.filter_transaction_types(requested_trans_types) assert len(res) == 4 def test_no_requested_trans_types(): """Test transaction types are optional param for filter_transaction_types. And when optional, all types are returned. """ requested_trans_types = [] res = aggregate_stats.filter_transaction_types(requested_trans_types) assert analytics.TRANSACTION_TYPES == res def test_bad_trans_types(): """Test filter_transaction_types ignores invalid transaction types.""" requested_trans_types = 'S,SR,VBBBBBB,VR'.split(',') res = aggregate_stats.filter_transaction_types(requested_trans_types) assert len(res) == 3 def test_all_bad_trans_types(): """Test filter_transaction_types can ignore all trans types. When all types passed are invalid, all are ignored and full default list of transaction types are returned instead. """ requested_trans_types = 'Swfewf,SRwjefie,VBBBBBB,VRyyyyy'.split(',') res = aggregate_stats.filter_transaction_types(requested_trans_types) assert res == analytics.TRANSACTION_TYPES def test_malformed_trans_type_string(): """Test filter_transaction_types will ignore malformed params.""" requested_trans_types = 'VR[];UT,'.split(',') res = aggregate_stats.filter_transaction_types(requested_trans_types) assert res == analytics.TRANSACTION_TYPES def test_one_passed_trans_type_returns_one_result(): """Test filter_transaction_types works with only one transaction type.""" requested_trans_types = 'S'.split(',') res = aggregate_stats.filter_transaction_types(requested_trans_types) assert len(res) == 1 def test_one_invalid_trans_type_returns_default_result(): """Test filter_transaction_types works with only one invalid trans type.""" requested_trans_types = 'S8998'.split(',') res = aggregate_stats.filter_transaction_types(requested_trans_types) assert res == analytics.TRANSACTION_TYPES def test_dupe_trans_type_returns_nondupe_result(): """Test filter_transaction_types will not return duplicate results.""" requested_trans_types = 'VR,VR'.split(',') res = aggregate_stats.filter_transaction_types(requested_trans_types) assert len(res) == 1