"""Unit tests for channel metrics logic layer.""" import datetime import pytest from analytics.api import app from analytics.logic.channel_metrics import get_channel_metrics def snake_to_camel(snake_str): components = snake_str.split("_") return components[0] + "".join(x.title() for x in components[1:]) def convert_keys_to_camel_case(data): return {snake_to_camel(key): value for key, value in data.items()} @pytest.fixture def mock_get_channel_metrics_model_payload(): """Mock payload for channel_metrics model.""" return [ { "download_activity_date": "2020-07-01", "views": 452, "ad_fill_rate": 0.521909333333, "premium_watch_time_minutes": 59.19172, "watch_time_minutes": 1133.39209, "channel_id": "UCFg7-w47VVA3ozhvnR6P0YQ", "estimated_monetized_playbacks": 242, "cpm": 3.804, "average_view_duration_percentage": 67.0094, "rpm": 0.48513333333, "premium_views": 24, "estimated_partner_ad_revenue": 0.195, "estimated_partner_premium_revenue": 0.195, "average_view_duration_seconds": 144.07021, "impressions_ctr": 0.5, "comments": 123, }, { "download_activity_date": "2020-07-02", "views": 550, "ad_fill_rate": 0.5215, "premium_watch_time_minutes": 99.0137, "watch_time_minutes": 1374.26089, "channel_id": "UCFg7-w47VVA3ozhvnR6P0YQ", "estimated_monetized_playbacks": 299, "cpm": 3.0, "average_view_duration_percentage": 69.53155571429, "rpm": 0.531, "premium_views": 40, "estimated_partner_ad_revenue": 0.267, "estimated_partner_premium_revenue": 0.267, "average_view_duration_seconds": 149.49284857143, "impressions_ctr": 0.5, "comments": 123, }, ] @pytest.fixture def mock_get_aggregate_channel_metrics_model_payload(): """Mock payload for channel_metrics model.""" return { "views": 831466, "ad_fill_rate": 0.041527478809, "premium_watch_time_minutes": 18306.22677, "watch_time_minutes": 2055708.31002, "channel_id": "UCFg7-w47VVA3ozhvnR6P0YQ", "estimated_monetized_playbacks": 57082, "cpm": 1.96487999595, "average_view_duration_percentage": 64.524151133144, "rpm": 221.675325581588, "premium_views": 7163, "estimated_partner_ad_revenue": 229288.545, "estimated_partner_premium_revenue": 229288.545, "average_view_duration_seconds": 138.726923563806, "impressions_ctr": 0.5, "comments": 123, } @pytest.fixture def empty_aggregate_channel_metrics_payload(): """Payload for channel metrics with empty recent and aggregate metrics.""" return { "channel_metrics": [], "aggregate_channel_metrics": {}, "sources": [{"name": "YouTube", "id": 453}], } @pytest.fixture def expected_payload(): """Return expected payload from logic.""" return { "channel_metrics": [ { "views": 452, "rpm": 0.48513333333, "downloadActivityDate": "2020-07-01", "premiumViews": 24, "premiumWatchTimeMinutes": 59.19172, "averageViewDurationSeconds": 144.07021, "watchTimeMinutes": 1133.39209, "estimatedMonetizedPlaybacks": 242, "channelId": "UCFg7-w47VVA3ozhvnR6P0YQ", "averageViewDurationPercentage": 67.0094, "estimatedPartnerAdRevenue": 0.195, "estimatedPartnerPremiumRevenue": 0.195, "adFillRate": 0.521909333333, "cpm": 3.804, "impressionsCtr": 0.5, "comments": 123, }, { "views": 550, "rpm": 0.531, "downloadActivityDate": "2020-07-02", "premiumViews": 40, "premiumWatchTimeMinutes": 99.0137, "averageViewDurationSeconds": 149.49284857143, "watchTimeMinutes": 1374.26089, "estimatedMonetizedPlaybacks": 299, "channelId": "UCFg7-w47VVA3ozhvnR6P0YQ", "averageViewDurationPercentage": 69.53155571429, "estimatedPartnerAdRevenue": 0.267, "estimatedPartnerPremiumRevenue": 0.267, "adFillRate": 0.5215, "cpm": 3.0, "impressionsCtr": 0.5, "comments": 123, }, ], "aggregate_channel_metrics": { "views": 831466, "rpm": 221.675325581588, "premiumWatchTimeMinutes": 18306.22677, "estimatedMonetizedPlaybacks": 57082, "watchTimeMinutes": 2055708.31002, "averageViewDurationSeconds": 138.726923563806, "channelId": "UCFg7-w47VVA3ozhvnR6P0YQ", "averageViewDurationPercentage": 64.524151133144, "estimatedPartnerAdRevenue": 229288.545, "estimatedPartnerPremiumRevenue": 229288.545, "premiumViews": 7163, "adFillRate": 0.041527478809, "cpm": 1.96487999595, "impressionsCtr": 0.5, "comments": 123, }, "sources": [{"name": "YouTube", "id": 453}], } @pytest.fixture(autouse=True) def mock_context(): """Mock successful context.""" with app.test_request_context(): yield {} @pytest.fixture(autouse=True) def permissions(): """Return default employee permissions.""" yield { "permission_label_ids": [], "permission_subaccount_ids": [], "permission_artist_ids": [], "permission_label_participant_ids": [], } @pytest.fixture(autouse=True) def mock_video_store_ids(mocker): """Mock video store ids.""" mocker.patch( "analytics.logic.channel_metrics.store_availability.get_video_store_ids", return_value=[1, 2, 3], ) @pytest.fixture(autouse=True) def mock_video_sources(mocker): """Mock video store ids.""" mocker.patch( "analytics.logic.channel_metrics.store_availability.get_video_sources", return_value=[1, 2, 3], ) @pytest.fixture(autouse=True) def mock_add_outage_error_to_stores(mocker): """Mock add_outage_error_to_stores.""" mocker.patch( "analytics.logic.channel_metrics.add_outage_error_to_stores", return_value=[{"name": "YouTube", "id": 453}], ) @pytest.fixture(autouse=True) def mock_data_availability_get_date_range(mocker): """Mock data_availability.get_date_range.""" mocker.patch( "analytics.logic.channel_metrics.data_availability.get_date_range", return_value=(datetime.date(2020, 7, 1), datetime.date(2020, 7, 2)), ) def test_get_channel_metrics_empty_result(mocker, permissions): """Test get_channel_metrics when no store_ids are provided.""" mocker.patch( "analytics.logic.channel_metrics.ChannelMetrics.execute", side_effect=[[], [{}]] ) query_params = {"channel_id": "UCFg7-w47VVA3ozhvnR6P0YQ", "store_ids": []} result = get_channel_metrics(query_params, permissions).message assert result["sources"] == [{"name": "YouTube", "id": 453}] assert result["channel_metrics"] == [] assert result["aggregate_channel_metrics"] == {} def test_get_channel_metrics_with_aggregate_metrics( mocker, mock_get_channel_metrics_model_payload, mock_get_aggregate_channel_metrics_model_payload, permissions, ): """Test get_channel_metrics with aggregate metrics.""" mocker.patch( "analytics.logic.channel_metrics.store_availability.get_video_store_ids", return_value=[1, 2, 3], ) # The timeseries and aggregate queries are executed in parallel threads, # so call order is non-deterministic. Route payloads by the instance's # `is_timeseries` flag instead of by call index. def execute_by_query_type(self): if self.query_params.get("is_timeseries"): return mock_get_channel_metrics_model_payload return [mock_get_aggregate_channel_metrics_model_payload] mocker.patch( "analytics.logic.channel_metrics.ChannelMetrics.execute", autospec=True, side_effect=execute_by_query_type, ) query_params = {"channel_id": "UCFg7-w47VVA3ozhvnR6P0YQ", "store_ids": [1, 2]} result = get_channel_metrics(query_params, permissions).message assert result["sources"] == [{"name": "YouTube", "id": 453}] assert len(result["channel_metrics"]) == 2 # Check all fields for each channel metric for i, expected_metric in enumerate(mock_get_channel_metrics_model_payload): expected_metric = convert_keys_to_camel_case(expected_metric) for field, expected_value in expected_metric.items(): assert result["channel_metrics"][i][field] == expected_value # Check all fields for the aggregate channel metric mock_get_aggregate_channel_metrics_model_payload = convert_keys_to_camel_case( mock_get_aggregate_channel_metrics_model_payload ) for ( field, expected_value, ) in mock_get_aggregate_channel_metrics_model_payload.items(): assert result["aggregate_channel_metrics"][field] == expected_value def test_get_channel_metrics_all_time( mocker, mock_get_aggregate_channel_metrics_model_payload, permissions ): """Test get_channel_metrics with all time aggregate metrics.""" mocker.patch( "analytics.logic.channel_metrics.store_availability.get_video_store_ids", return_value=[1, 2, 3], ) mocker.patch( "analytics.logic.channel_metrics.ChannelMetrics.execute", side_effect=[[mock_get_aggregate_channel_metrics_model_payload]], ) query_params = {"channel_id": "UCFg7-w47VVA3ozhvnR6P0YQ", "store_ids": [1, 2]} query_params["all_time"] = True result = get_channel_metrics(query_params, permissions).message assert result["sources"] == [{"name": "YouTube", "id": 453}] # Check all fields for the all time aggregate channel metrics mock_get_aggregate_channel_metrics_model_payload = convert_keys_to_camel_case( mock_get_aggregate_channel_metrics_model_payload ) for ( field, expected_value, ) in mock_get_aggregate_channel_metrics_model_payload.items(): assert result["all_time_channel_metrics"][field] == expected_value