"""Unit tests for the participant metrics logic layer.""" from unittest.mock import patch import pytest from analytics.api import app from analytics.logic import participant_metrics @pytest.fixture(autouse=True) def mock_request_context(): with app.test_request_context(): yield @pytest.fixture(autouse=True) def no_cache(): with patch("analytics.connectors.redis.client.get") as get: get.return_value = None yield get @pytest.fixture(autouse=True) def mock_store_ids(mocker): mocker.patch( "analytics.logic.participant_metrics.store_availability.get_store_ids", return_value=[1, 286], ) mocker.patch( "analytics.logic.participant_metrics.store_availability.is_apple_spotify_data_in_sync", return_value=True, ) @pytest.fixture def permissions(): 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 query_params(): return { "distributors": ["theorchard", "sme", "awal"], "country_ids": [], "store_ids": [], "label_ids": [], "subaccount_ids": [], "fin_label_ids": [], "upper_profit_center_ids": [], "global_participant_ids": [], "parent_companies": [], "company_brands": [], "service_tier": None, "order_by": "streams_7_days", "order_dir": "DESC", "limit": 25, "offset": 0, } @pytest.fixture def query_rows(): return [ { "id": "f67b0892-f0bc-4575-b23e-59566ccb44bc", "streams_1_day": 100000, "growth_percentage_1_day": 0.1, "streams_7_days": 50000, "growth_percentage_7_days": 0.5, "streams_28_days": 150000, "growth_percentage_28_days": 0.15, "streams_183_days": 1000000, "growth_percentage_183_days": 0.2, "streams_365_days": 2000000, "growth_percentage_365_days": 0.3, "streams_all_time": 5000000, "total_participants": 2, }, { "id": "f278acf-f0bc-98776-a56e-34792cch224", "streams_1_day": 80000, "growth_percentage_1_day": 0.05, "streams_7_days": 40000, "growth_percentage_7_days": 0.3, "streams_28_days": 120000, "growth_percentage_28_days": 0.1, "streams_183_days": 900000, "growth_percentage_183_days": 0.18, "streams_365_days": 1800000, "growth_percentage_365_days": 0.25, "streams_all_time": 4500000, "total_participants": 2, }, ] class TestGetParticipantMetrics: def test_succeeds(self, mocker, permissions, query_params, query_rows): mocker.patch( "analytics.logic.participant_metrics.ParticipantMetrics.execute", return_value=query_rows, ) response = participant_metrics.get_participant_metrics( query_params, permissions ) assert response.status == 200 assert response.message["total_participants"] == 2 assert len(response.message["metrics"]) == 2 first = response.message["metrics"][0] assert first["id"] == "f67b0892-f0bc-4575-b23e-59566ccb44bc" # total_participants must not leak into items assert "total_participants" not in first def test_returns_empty_when_no_available_stores( self, mocker, permissions, query_params ): mocker.patch( "analytics.logic.participant_metrics.store_availability.get_store_ids", return_value=[], ) response = participant_metrics.get_participant_metrics( query_params, permissions ) assert response.status == 200 assert response.message == {"metrics": [], "total_participants": 0} def test_returns_empty_when_request_store_ids_dont_match( self, mocker, permissions, query_params ): # request store_ids that don't intersect available stores params = {**query_params, "store_ids": [9999]} response = participant_metrics.get_participant_metrics(params, permissions) assert response.status == 200 assert response.message == {"metrics": [], "total_participants": 0} def test_returns_empty_metrics_when_query_returns_no_rows( self, mocker, permissions, query_params ): mocker.patch( "analytics.logic.participant_metrics.ParticipantMetrics.execute", return_value=[], ) response = participant_metrics.get_participant_metrics( query_params, permissions ) assert response.status == 200 assert response.message == {"metrics": [], "total_participants": 0} def test_passes_permissions_to_query( self, mocker, permissions, query_params, query_rows ): execute_mock = mocker.patch( "analytics.logic.participant_metrics.ParticipantMetrics.execute", return_value=query_rows, ) init_mock = mocker.patch.object( participant_metrics.ParticipantMetrics, "__init__", return_value=None, ) execute_mock.return_value = query_rows participant_metrics.get_participant_metrics(query_params, permissions) called_with = init_mock.call_args[0][0] assert called_with["permission_label_ids"] == [7123] assert called_with["distributors"] == ["theorchard", "sme", "awal"] assert called_with["store_ids"] == [1, 286] # available stores assert called_with["is_feed_data_available"] is True # Always rollup_grain=True — participant metrics reads # METRICS_BY_TRACK_PARTICIPANT_*_ROLLUP, widened by the # transfer-product-ownership dbt PR. assert called_with["rollup_grain"] is True @pytest.mark.parametrize( "input_dir,expected", [("desc", "DESC"), ("asc", "ASC"), ("DESC", "DESC"), ("ASC", "ASC")], ) def test_normalizes_order_dir_case( self, mocker, permissions, query_params, query_rows, input_dir, expected ): mocker.patch( "analytics.logic.participant_metrics.ParticipantMetrics.execute", return_value=query_rows, ) init_mock = mocker.patch.object( participant_metrics.ParticipantMetrics, "__init__", return_value=None, ) params = {**query_params, "order_dir": input_dir} participant_metrics.get_participant_metrics(params, permissions) assert init_mock.call_args[0][0]["order_dir"] == expected