"""Unit tests for the streams breakdown logic layer.""" import datetime from unittest.mock import patch import pytest from analytics.api import app from analytics.constants.parameters import ALL_TIME from analytics.logic import streams_breakdown SOURCES = [ {"id": 286, "name": "Spotify"}, {"id": 1, "name": "Apple Music"}, {"id": 716, "name": "Amazon Unlimited"}, ] SOURCE_OF_STREAMS = { "active": {"value": 0.2, "growth_percentage": None, "total": 2}, "passive": {"value": 0.2, "growth_percentage": None, "total": 2}, "collection": {"value": 0.4, "growth_percentage": None, "total": 4}, "unknown": {"value": 0.2, "growth_percentage": None, "total": 2}, "sources": SOURCES, } SOURCE_OF_STREAMS_ZERO_STREAMS = { "active": {"value": 0, "growth_percentage": None, "total": 0}, "passive": {"value": 0, "growth_percentage": None, "total": 0}, "collection": {"value": 0, "growth_percentage": None, "total": 0}, "unknown": {"value": 0, "growth_percentage": None, "total": 0}, "sources": SOURCES, } STREAMS_BY_SUBSCRIPTION = { "subscription": {"value": 0.5, "growth_percentage": None, "total": 5}, "ad_supported": {"value": 0.2, "growth_percentage": None, "total": 2}, "mid_tier": {"value": 0.2, "growth_percentage": None, "total": 2}, "sources": SOURCES, } STREAMS_BY_SUBSCRIPTION_ZERO_STREAMS = { "subscription": {"value": 0, "growth_percentage": None, "total": 0}, "ad_supported": {"value": 0, "growth_percentage": None, "total": 0}, "mid_tier": {"value": 0, "growth_percentage": None, "total": 0}, "sources": SOURCES, } EMPTY_BREAKDOWN_BODY = { "source_of_streams": { "active": {}, "passive": {}, "collection": {}, "unknown": {}, "sources": SOURCES, }, "streams_by_subscription": { "subscription": {}, "ad_supported": {}, "mid_tier": {}, "sources": SOURCES, }, } @pytest.fixture(autouse=True) def mock_context(): """Run in Flask request context.""" with app.test_request_context(): yield @pytest.fixture(autouse=True) def no_cache(): """Mock out caching of response.""" with patch("analytics.connectors.redis.client.get") as get: get.return_value = None yield get @pytest.fixture(autouse=True) def mock_sources(mocker): """Mock add_outage_error_to_stores + store_availability to return SOURCES.""" mocker.patch( "analytics.logic.streams_breakdown.add_outage_error_to_stores", return_value=SOURCES, ) mocker.patch( "analytics.logic.streams_breakdown.store_availability.get_sources", return_value=SOURCES, ) mocker.patch( "analytics.logic.streams_breakdown.store_availability.get_store_ids", return_value=[1, 286], ) @pytest.fixture def mock_breakdown_row(): """Single aggregated row returned by StreamsBreakdown.execute.""" return { "isrc": "TEST", "streams_passive": 2, "streams_active": 2, "streams_collection": 4, "subscription": 5, "ad_supported": 2, "mid_tier": 2, "streams": 10, } @pytest.fixture def mock_breakdown_row_zero_streams(): """Single aggregated row with zero streams.""" return { "isrc": "TEST", "streams_passive": 0, "streams_active": 0, "streams_collection": 0, "subscription": 0, "ad_supported": 0, "mid_tier": 0, "streams": 0, } @pytest.fixture def permissions(): """Return permissions with label access.""" return { "permission_label_ids": [7123], "permission_artist_ids": None, "permission_subaccount_ids": None, "permission_label_participant_ids": None, "permission_feed_ids": [1, 2], } @pytest.fixture def mock_get_max_available_date_success(): """Mock get_max_available_date.""" with patch( "analytics.utils.data_availability.get_max_available_date" ) as get_max_available_date: get_max_available_date.return_value = "2018-07-06" yield get_max_available_date def _query_params(countries=None, store_ids=None, start_date=None, end_date=None): return { "isrc": "TEST", "country_ids": countries or [], "store_ids": store_ids or [], "start_date": start_date, "end_date": end_date, "distributors": ["theorchard", "sme", "awal"], } class TestGetStreamsBreakdownSuccess: """Test get_streams_breakdown with a non-empty query result.""" def test_default_date_range( self, mocker, permissions, mock_breakdown_row, mock_get_max_available_date_success, ): """No start/end date falls back to HIGHWATERMARK - 7 days.""" mocker.patch( "analytics.logic.streams_breakdown.StreamsBreakdown.execute", return_value=[mock_breakdown_row], ) response = streams_breakdown.get_streams_breakdown(_query_params(), permissions) assert response.status == 200 assert response.message == { "isrc": "TEST", "source_of_streams": SOURCE_OF_STREAMS, "streams_by_subscription": STREAMS_BY_SUBSCRIPTION, } def test_with_start_and_end_date(self, mocker, permissions, mock_breakdown_row): """Explicit start/end date is passed through.""" mocker.patch( "analytics.logic.streams_breakdown.StreamsBreakdown.execute", return_value=[mock_breakdown_row], ) response = streams_breakdown.get_streams_breakdown( _query_params( start_date=datetime.date(2019, 11, 1), end_date=datetime.date(2019, 12, 1), ), permissions, ) assert response.status == 200 assert response.message == { "isrc": "TEST", "source_of_streams": SOURCE_OF_STREAMS, "streams_by_subscription": STREAMS_BY_SUBSCRIPTION, } def test_with_countries(self, mocker, permissions, mock_breakdown_row): """country_ids is forwarded to the query.""" mocker.patch( "analytics.logic.streams_breakdown.StreamsBreakdown.execute", return_value=[mock_breakdown_row], ) response = streams_breakdown.get_streams_breakdown( _query_params( countries=["DE", "NO", "US"], start_date=datetime.date(2019, 11, 1), end_date=datetime.date(2019, 12, 1), ), permissions, ) assert response.status == 200 assert response.message == { "isrc": "TEST", "source_of_streams": SOURCE_OF_STREAMS, "streams_by_subscription": STREAMS_BY_SUBSCRIPTION, } def test_with_store_ids(self, mocker, permissions, mock_breakdown_row): """store_ids intersect with available stores.""" mocker.patch( "analytics.logic.streams_breakdown.StreamsBreakdown.execute", return_value=[mock_breakdown_row], ) response = streams_breakdown.get_streams_breakdown( _query_params( store_ids=[1, 286], start_date=datetime.date(2019, 11, 1), end_date=datetime.date(2019, 12, 1), ), permissions, ) assert response.status == 200 assert response.message == { "isrc": "TEST", "source_of_streams": SOURCE_OF_STREAMS, "streams_by_subscription": STREAMS_BY_SUBSCRIPTION, } def test_zero_streams( self, mocker, permissions, mock_breakdown_row_zero_streams, mock_get_max_available_date_success, ): """Zero-streams row produces zero-valued breakdown.""" mocker.patch( "analytics.logic.streams_breakdown.StreamsBreakdown.execute", return_value=[mock_breakdown_row_zero_streams], ) response = streams_breakdown.get_streams_breakdown(_query_params(), permissions) assert response.status == 200 assert response.message == { "isrc": "TEST", "source_of_streams": SOURCE_OF_STREAMS_ZERO_STREAMS, "streams_by_subscription": STREAMS_BY_SUBSCRIPTION_ZERO_STREAMS, } class TestGetStreamsBreakdownEmpty: """Test get_streams_breakdown when the query returns no rows.""" def test_default_date_range( self, mocker, permissions, mock_get_max_available_date_success, ): """Empty query result returns empty breakdown body with sources.""" mocker.patch( "analytics.logic.streams_breakdown.StreamsBreakdown.execute", return_value=[], ) response = streams_breakdown.get_streams_breakdown(_query_params(), permissions) assert response.status == 200 assert response.message == {"isrc": "TEST", **EMPTY_BREAKDOWN_BODY} def test_with_dates_and_countries(self, mocker, permissions): """Empty query with explicit params still returns empty body.""" mocker.patch( "analytics.logic.streams_breakdown.StreamsBreakdown.execute", return_value=[], ) response = streams_breakdown.get_streams_breakdown( _query_params( countries=["DE", "NO"], start_date=datetime.date(2019, 11, 1), end_date=datetime.date(2019, 12, 1), ), permissions, ) assert response.status == 200 assert response.message == {"isrc": "TEST", **EMPTY_BREAKDOWN_BODY} class TestGetStreamsBreakdownAllTime: """Test get_streams_breakdown with start_date=ALL_TIME.""" def test_all_time_invokes_query_without_start_date( self, mocker, permissions, mock_breakdown_row ): """ALL_TIME sentinel sets all_time=True and omits start_date.""" MockQuery = mocker.patch("analytics.logic.streams_breakdown.StreamsBreakdown") MockQuery.return_value.execute.return_value = [mock_breakdown_row] response = streams_breakdown.get_streams_breakdown( _query_params( start_date=ALL_TIME, end_date=datetime.date(2019, 12, 1), ), permissions, ) assert response.status == 200 assert response.message == { "isrc": "TEST", "source_of_streams": SOURCE_OF_STREAMS, "streams_by_subscription": STREAMS_BY_SUBSCRIPTION, } MockQuery.return_value.execute.assert_called_once() query_input = MockQuery.call_args[0][0] assert query_input["all_time"] is True assert "start_date" not in query_input assert query_input["end_date"] == "2019-12-01" class TestGetStreamsBreakdownEmptyStoreIds: """Test get_streams_breakdown when store_ids intersection yields empty.""" def test_empty_store_ids_returns_empty_body_without_query( self, mocker, permissions ): """Unavailable store_ids → empty body, query never called.""" execute = mocker.patch( "analytics.logic.streams_breakdown.StreamsBreakdown.execute" ) response = streams_breakdown.get_streams_breakdown( _query_params( store_ids=[99999], start_date=datetime.date(2019, 11, 1), end_date=datetime.date(2019, 12, 1), ), permissions, ) assert response.status == 200 assert response.message == {"isrc": "TEST", **EMPTY_BREAKDOWN_BODY} execute.assert_not_called()