"""Integration tests for account endpoints. Endpoints tested: - /account//timeseries (TestAccountTimeseries) - /account//summary (TestAccountSummary) - /account//products (TestAccountProducts) - /account//top-content (TestAccountTopContent) - /account//video/summary (TestAccountVideoViews) - /account//video/timeseries (TestAccountVideoViews) Tests are organized by concern, not by cross-product of all dimensions. Every group parametrizes on ALL_HEADERS to verify access control everywhere. """ import pytest from analytics import config from analytics.config import BASE_URL from analytics.logic.account_timeseries import ( ACCOUNT_TABLES_SUMMARY, ACCOUNT_TABLES_TIMESERIES, ) from tests.integration.endpoints.conftest import ( ACCOUNTS, ALL_HEADERS, BAD_BUNNY, FRENCHKISS, INSIGHTS_ARTIST, NON_ARTIST_HEADERS, PAGINATION, RIMAS, assert_endpoint, assert_timeseries_keys, has_access, ) class TestAccountTimeseries: """Integration tests for /account//timeseries. Tests are organized into 9 groups by concern. Every group parametrizes on ALL_HEADERS (6 auth profiles) so access control is verified everywhere — authorized users see data, unauthorized see {"items": []}. Groups: 1. All types shape — every query_type returns correct keys (180 tests) 2. Country filter — table-switching logic works (180 tests) 3. Subscription — value recalculation for STREAMS (48 tests) 4. ID filter — ids= param on breakdown types (18 tests) 5. Store filter — store_ids= param (6 tests) 6. Data assertions — known Frenchkiss data canaries (30 tests) 7. Combined filters — country + subscription compose (180 tests) 8. Multi-account — subaccount + D3 vendor SQL paths (36 tests) """ START_DATE = "2022-04-05" END_DATE = "2022-07-05" def _url(self, account, query_type, extra=""): base = config.ACCOUNT_METRICS_TIME_SERIES_URL.replace( "", str(account["account_id"]) ) return ( f"{base}" f"?account_type={account['account_type']}&type={query_type}" f"&start_date={self.START_DATE}&end_date={self.END_DATE}{extra}" ) # ── Group 1: All types return correct response shape ──────────── # # 6 headers x 30 query_types = 180 tests. # Uses Frenchkiss account (known to have data for all types). @pytest.mark.parametrize("hdrs", ALL_HEADERS) @pytest.mark.parametrize("query_type", ACCOUNT_TABLES_TIMESERIES.keys()) def test_all_types_response_shape(self, hdrs, query_type): """Every timeseries type returns data with correct keys. Key shape rules: - All types: date, value - Breakdown types (_BY_): + id - Standard STREAMS: + skip_rate, saves - SUBSCRIPTION and SOS: no skip_rate, no saves - Downloads: no skip_rate, no saves """ payload = assert_endpoint( self._url(FRENCHKISS, query_type), headers=hdrs, expect_empty=not has_access(hdrs, FRENCHKISS["account_id"]), ) if has_access(hdrs, FRENCHKISS["account_id"]) and payload["items"]: assert_timeseries_keys(payload["items"][0].keys(), query_type) # ── Group 2: Artist cross-account access ───────────────────────── # # Artist (Local Natives) belongs to Frenchkiss (6971). Querying a # different label (RIMAS / 26760) must return empty items. def test_artist_no_access_to_other_label(self): """Artist profile gets empty results for a label they don't belong to.""" assert_endpoint( self._url(RIMAS, "ACCOUNT_STREAMS"), headers=INSIGHTS_ARTIST, expect_empty=True, ) # ── Group 3: Country filter table switching ───────────────────── # # 6 headers x 30 query_types = 180 tests. # When countries= is non-empty, get_account_table_for_timeseries picks # the "country" table variant. @pytest.mark.parametrize("hdrs", ALL_HEADERS) @pytest.mark.parametrize("query_type", ACCOUNT_TABLES_TIMESERIES.keys()) def test_country_filter(self, hdrs, query_type): """Country filter activates the country-variant Snowflake table.""" payload = assert_endpoint( self._url(FRENCHKISS, query_type, "&countries=GB&countries=DE"), headers=hdrs, expect_empty=not has_access(hdrs, FRENCHKISS["account_id"]), ) if has_access(hdrs, FRENCHKISS["account_id"]) and payload["items"]: assert_timeseries_keys(payload["items"][0].keys(), query_type) # ── Group 4: Subscription filter ──────────────────────────────── # # 6 headers x 2 types x 2 filter variants = 24 tests. # For STREAMS: subscription_types recalculates the value field. # For downloads: the param is accepted but has no effect. @pytest.mark.parametrize("hdrs", ALL_HEADERS) @pytest.mark.parametrize( "sub_filter", [ pytest.param("&subscription_types=ADSUPPORTED", id="adsupported"), pytest.param("&subscription_types=SUBSCRIPTION", id="subscription"), pytest.param("&subscription_types=MIDTIER", id="midtier"), pytest.param( "&subscription_types=ADSUPPORTED&subscription_types=SUBSCRIPTION", id="adsupported+subscription", ), ], ) @pytest.mark.parametrize( "query_type", ["ACCOUNT_STREAMS", "ACCOUNT_ALBUM_DOWNLOADS"], ) def test_subscription_filter(self, hdrs, query_type, sub_filter): """Subscription filter recalculates value for STREAMS types. For STREAMS: value = sum of matching sub_type columns. For downloads: subscription_types param is ignored (no error). Both paths must return valid data. """ payload = assert_endpoint( self._url(FRENCHKISS, query_type, sub_filter), headers=hdrs, expect_empty=not has_access(hdrs, FRENCHKISS["account_id"]), ) if has_access(hdrs, FRENCHKISS["account_id"]) and payload["items"]: assert_timeseries_keys(payload["items"][0].keys(), query_type) # ── Group 5: ID filter for breakdown types ────────────────────── # # 3 methods x 6 headers = 18 tests. # Each method tests a qualitatively different code path. @pytest.mark.parametrize("hdrs", ALL_HEADERS) def test_id_filter_subscription(self, hdrs): """ids=subscription filters BY_SUBSCRIPTION to one category. BY_SUBSCRIPTION uses Python-side _breakdown_by with sources=[ids]. All returned items must have id=='subscription'. """ payload = assert_endpoint( self._url( FRENCHKISS, "ACCOUNT_STREAMS_BY_SUBSCRIPTION", "&ids=subscription", ), headers=hdrs, expect_empty=not has_access(hdrs, FRENCHKISS["account_id"]), ) if has_access(hdrs, FRENCHKISS["account_id"]) and payload["items"]: assert all(i["id"] == "subscription" for i in payload["items"]) @pytest.mark.parametrize("hdrs", ALL_HEADERS) def test_id_filter_sos(self, hdrs): """ids=active filters BY_SOS to the 'active' category. Same _breakdown_by mechanism as subscription but with SOS sources. All returned items must have id=='active'. """ payload = assert_endpoint( self._url( FRENCHKISS, "ACCOUNT_STREAMS_BY_SOS", "&ids=active", ), headers=hdrs, expect_empty=not has_access(hdrs, FRENCHKISS["account_id"]), ) if has_access(hdrs, FRENCHKISS["account_id"]) and payload["items"]: assert all(i["id"] == "active" for i in payload["items"]) @pytest.mark.parametrize("hdrs", ALL_HEADERS) def test_id_filter_track_isrc(self, hdrs): """ids=USRC17607839 filters BY_TRACK to a single ISRC. BY_TRACK uses SQL-side WHERE clause for id filtering, unlike BY_SUBSCRIPTION/BY_SOS which filter in Python. """ payload = assert_endpoint( self._url( FRENCHKISS, "ACCOUNT_STREAMS_BY_TRACK", "&ids=USRC17607839", ), headers=hdrs, expect_empty=not has_access(hdrs, FRENCHKISS["account_id"]), ) if has_access(hdrs, FRENCHKISS["account_id"]) and payload["items"]: assert all(i["id"] == "USRC17607839" for i in payload["items"]) # ── Group 6: Store filter ─────────────────────────────────────── # # 6 tests (one per header). @pytest.mark.parametrize("hdrs", ALL_HEADERS) def test_store_filter(self, hdrs): """store_ids=286 filters BY_STORE to a single store. Verifies the WHERE store_id IN (...) SQL clause works. """ payload = assert_endpoint( self._url( FRENCHKISS, "ACCOUNT_STREAMS_BY_STORE", "&store_ids=286", ), headers=hdrs, expect_empty=not has_access(hdrs, FRENCHKISS["account_id"]), ) if has_access(hdrs, FRENCHKISS["account_id"]) and payload["items"]: assert payload["items"][0]["id"] == 286 # ── Group 7: Data-specific assertions (Frenchkiss) ────────────── # # 5 methods x 6 headers = 30 tests. # Canary tests for known data values. Catch data pipeline regressions. @pytest.mark.parametrize("hdrs", ALL_HEADERS) def test_data_country_codes(self, hdrs): """BY_COUNTRY for Frenchkiss includes GB, DE, and NO. Frenchkiss Records is known to have streams from these countries. Catches data pipeline regressions where country data disappears. """ payload = assert_endpoint( self._url(FRENCHKISS, "ACCOUNT_STREAMS_BY_COUNTRY"), headers=hdrs, expect_empty=not has_access(hdrs, FRENCHKISS["account_id"]), ) if has_access(hdrs, FRENCHKISS["account_id"]) and payload["items"]: country_codes = [item["id"] for item in payload["items"]] assert "GB" in country_codes assert "DE" in country_codes assert "NO" in country_codes @pytest.mark.parametrize("hdrs", ALL_HEADERS) def test_data_country_filter_exact(self, hdrs): """countries=GB&countries=DE returns exactly those two countries. Verifies the country filter narrows results to the requested set. """ payload = assert_endpoint( self._url( FRENCHKISS, "ACCOUNT_STREAMS_BY_COUNTRY", "&countries=GB&countries=DE", ), headers=hdrs, expect_empty=not has_access(hdrs, FRENCHKISS["account_id"]), ) if has_access(hdrs, FRENCHKISS["account_id"]) and payload["items"]: first_two = sorted([payload["items"][0]["id"], payload["items"][1]["id"]]) assert first_two == ["DE", "GB"] @pytest.mark.parametrize("hdrs", ALL_HEADERS) def test_data_subscription_breakdown(self, hdrs): """BY_SUBSCRIPTION breaks down into subscription, adsupported, midtier. The _breakdown_by function produces exactly these three categories in this order. Verifies the breakdown logic works end-to-end. """ payload = assert_endpoint( self._url(FRENCHKISS, "ACCOUNT_STREAMS_BY_SUBSCRIPTION"), headers=hdrs, expect_empty=not has_access(hdrs, FRENCHKISS["account_id"]), ) if has_access(hdrs, FRENCHKISS["account_id"]) and payload["items"]: item_ids = list(dict.fromkeys(i["id"] for i in payload["items"])) assert item_ids == ["subscription", "adsupported", "midtier"] @pytest.mark.parametrize("hdrs", ALL_HEADERS) def test_data_subscription_breakdown_with_country(self, hdrs): """BY_SUBSCRIPTION + country filter still produces all categories. Exercises the most complex code path: country table switch + Python-side _breakdown_by. Verifies they compose correctly. """ payload = assert_endpoint( self._url( FRENCHKISS, "ACCOUNT_STREAMS_BY_SUBSCRIPTION", "&countries=GB&countries=DE", ), headers=hdrs, expect_empty=not has_access(hdrs, FRENCHKISS["account_id"]), ) if has_access(hdrs, FRENCHKISS["account_id"]) and payload["items"]: item_ids = list(dict.fromkeys(i["id"] for i in payload["items"])) assert item_ids == ["subscription", "adsupported", "midtier"] @pytest.mark.parametrize("hdrs", ALL_HEADERS) def test_data_sos_breakdown(self, hdrs): """BY_SOS breaks down into active, passive, collection, unknown. Same _breakdown_by mechanism as subscription, different category set. """ payload = assert_endpoint( self._url(FRENCHKISS, "ACCOUNT_STREAMS_BY_SOS"), headers=hdrs, expect_empty=not has_access(hdrs, FRENCHKISS["account_id"]), ) if has_access(hdrs, FRENCHKISS["account_id"]) and payload["items"]: item_ids = list(dict.fromkeys(i["id"] for i in payload["items"])) assert item_ids == ["active", "passive", "collection", "unknown"] # ── Group 8: Combined country + subscription filter ───────────── # # 6 headers x 30 query_types = 180 tests. # Verifies that country (table switch) + subscription (value recalc) # compose without error. @pytest.mark.parametrize("hdrs", ALL_HEADERS) @pytest.mark.parametrize("query_type", ACCOUNT_TABLES_TIMESERIES.keys()) def test_country_and_subscription_combined(self, hdrs, query_type): """Country filter + subscription filter applied simultaneously. Country switches the Snowflake table; subscription recalculates values (for STREAMS types only). Both transformations must compose without error. For download types, subscription_types is harmlessly ignored. """ payload = assert_endpoint( self._url( FRENCHKISS, query_type, "&countries=GB&countries=DE" "&subscription_types=ADSUPPORTED", ), headers=hdrs, expect_empty=not has_access(hdrs, FRENCHKISS["account_id"]), ) if has_access(hdrs, FRENCHKISS["account_id"]) and payload["items"]: assert_timeseries_keys(payload["items"][0].keys(), query_type) # ── Group 9: Multi-account (subaccount + D3 vendor) ─────────────── # # 6 headers × 2 accounts × 3 types = 36 tests. # Exercises account_type=subaccount SQL paths (different AGGREGATION_FIELDS # mappings and JOINs) and D3 vendor access. @pytest.mark.parametrize("hdrs", ALL_HEADERS) @pytest.mark.parametrize("account", [BAD_BUNNY, RIMAS]) @pytest.mark.parametrize( "query_type", ["ACCOUNT_STREAMS", "ACCOUNT_STREAMS_BY_COUNTRY", "ACCOUNT_ALBUM_DOWNLOADS"], ) def test_multi_account_shape(self, hdrs, account, query_type): """Subaccount and D3 vendor accounts return correct response shape. BAD_BUNNY (subaccount) exercises the subaccount SQL path. RIMAS (D3 vendor) exercises vendor path with D3 access rules. Three representative types cover streams total, streams country-table, and downloads SQL template families. """ payload = assert_endpoint( self._url(account, query_type), headers=hdrs, expect_empty=not has_access(hdrs, account["account_id"]), ) if has_access(hdrs, account["account_id"]) and payload["items"]: assert_timeseries_keys(payload["items"][0].keys(), query_type) class TestAccountSummary: """Integration tests for /account//summary. Tests cover all summary types across all accounts and auth profiles, with filter combinations (country, subscription). Groups: 1. All summary types with filters (6 hdrs × 3 accounts × N types × 4 filters) """ def _url(self, account, summary_type, extra=""): base = config.ACCOUNT_METRICS_SUMMARY_URL.replace( "", str(account["account_id"]) ) return ( f"{base}" f"?account_type={account['account_type']}" f"&type={summary_type}&{PAGINATION}" f"{extra}" ) # ── Group 1: All summary types with filter variants ──────────── # # 6 headers × 3 accounts × N summary_types × 4 filters. @pytest.mark.parametrize("hdrs", ALL_HEADERS) @pytest.mark.parametrize("account", ACCOUNTS) @pytest.mark.parametrize("summary_type", ACCOUNT_TABLES_SUMMARY.keys()) @pytest.mark.parametrize( "extra", [ pytest.param("", id="no_filter"), pytest.param("&countries=US", id="US"), pytest.param("&subscription_types=ADSUPPORTED", id="adsupported"), pytest.param( "&subscription_types=ADSUPPORTED&subscription_types=SUBSCRIPTION", id="adsupported+subscription", ), pytest.param( "&countries=US&subscription_types=ADSUPPORTED", id="US+adsupported", ), pytest.param( "&countries=US" "&subscription_types=ADSUPPORTED&subscription_types=SUBSCRIPTION", id="US+adsupported+subscription", ), ], ) def test_summary(self, hdrs, account, summary_type, extra): """Every summary type returns correct shape for all accounts and filters. Key rules: - Vendors without subaccounts get empty results for SUBACCOUNT type. - All items must have streams >= 0. - Non-SOS/SUBSCRIPTION items must have album_downloads, track_downloads, saves, and skip_rate. """ url = self._url(account, summary_type, extra) if summary_type == "STORE": url += "&ids=1" # Vendors without subaccounts can't return SUBACCOUNT data is_vendor_without_subaccounts = ( account["account_type"] == "vendor" and summary_type == "SUBACCOUNT" and not account["has_subaccounts"] ) should_have_data = ( has_access(hdrs, account["account_id"]) and not is_vendor_without_subaccounts ) payload = assert_endpoint( url, headers=hdrs, expect_empty=not should_have_data, ) if should_have_data and payload["items"]: item = payload["items"][0] assert item["streams"] >= 0 if summary_type not in ("SOS", "SUBSCRIPTION"): assert "album_downloads" in item assert "track_downloads" in item assert "saves" in item assert "skip_rate" in item assert item["track_downloads"] > 0 assert item["skip_rate"] > 0 assert item["saves"] > 0 class TestAccountProducts: """Integration tests for /account//products. Verifies product listing across all accounts and auth profiles. The get_products logic routes to 4 query classes based on order_by (streams vs downloads) × date range (fixed vs custom period). Groups: 1. Streams fixed period — order_by=streams_all_time, no dates (18 tests) 2. Streams custom period — order_by=streams, with dates (18 tests) 3. Downloads fixed period — order_by=downloads_all_time, no dates (18 tests) 4. Downloads custom period — order_by=downloads, with dates (18 tests) """ def _url(self, account, order_by="streams_all_time", extra=""): base = config.ACCOUNT_PRODUCTS_URL.replace( "", str(account["account_id"]) ) return ( f"{base}" f"?account_type={account['account_type']}" f"&order_by={order_by}{extra}" ) # ── Group 1: Streams fixed period (no dates) ───────────────────── @pytest.mark.parametrize("hdrs", ALL_HEADERS) @pytest.mark.parametrize("account", ACCOUNTS) def test_products(self, hdrs, account): """Product list returns items with product_id for authorized users. Exercises AccountProductsByStreamsFixedPeriod query class. Unauthorized users see empty items list. """ payload = assert_endpoint( self._url(account), headers=hdrs, expect_empty=not has_access(hdrs, account["account_id"]), ) if payload["items"]: assert payload["items"][0].get("product_id") # ── Group 2: Streams custom period (with dates) ────────────────── @pytest.mark.parametrize("hdrs", ALL_HEADERS) @pytest.mark.parametrize("account", ACCOUNTS) def test_products_streams_custom_period(self, hdrs, account): """Products with date range exercises AccountProductsByStreamsCustomPeriod. Adding start_date/end_date switches from the rollup table to the daily streams table. """ payload = assert_endpoint( self._url( account, order_by="streams", extra="&start_date=2023-01-01&end_date=2023-12-31", ), headers=hdrs, expect_empty=not has_access(hdrs, account["account_id"]), ) if payload["items"]: assert payload["items"][0].get("product_id") # ── Group 3: Downloads fixed period (no dates) ─────────────────── @pytest.mark.parametrize("hdrs", ALL_HEADERS) @pytest.mark.parametrize("account", ACCOUNTS) def test_products_downloads_fixed_period(self, hdrs, account): """Products ordered by downloads exercises AccountProductsByDownloadsFixedPeriod. order_by=downloads_all_time matches the SQL column in products_by_downloads_fixed_period.sql. """ payload = assert_endpoint( self._url(account, order_by="downloads_all_time"), headers=hdrs, expect_empty=not has_access(hdrs, account["account_id"]), ) if payload["items"]: assert payload["items"][0].get("product_id") # ── Group 4: Downloads custom period (with dates) ──────────────── @pytest.mark.parametrize("hdrs", ALL_HEADERS) @pytest.mark.parametrize("account", ACCOUNTS) def test_products_downloads_custom_period(self, hdrs, account): """Products with date range + downloads exercises AccountProductsByDownloadsCustomPeriod. order_by=downloads matches the SQL column in products_by_downloads_custom_period.sql. """ payload = assert_endpoint( self._url( account, order_by="downloads", extra="&start_date=2023-01-01&end_date=2023-12-31", ), headers=hdrs, expect_empty=not has_access(hdrs, account["account_id"]), ) if payload["items"]: assert payload["items"][0].get("product_id") class TestAccountTopContent: """Integration tests for /account//top-content. Verifies top-N content lists (artists, products, songs, countries, stores) across all accounts and auth profiles. Groups: 1. Streams top content — default and custom date params (6 × 3 × 2 = 36 tests) 2. Downloads top content — exercises AccountTopContentDownloads (6 × 3 = 18 tests) """ TOP_KEYS = [ "topn_artists", "topn_products", "topn_songs", "topn_countries", "topn_stores", ] def _url(self, account, extra=""): base = config.ACCOUNT_TOP_CONTENT_URL.replace( "", str(account["account_id"]) ) return ( f"{base}" f"?account_type={account['account_type']}" f"&top_size=5&{extra}" ) # ── Group 1: Top content response shape ──────────────────────── # # 6 headers × 3 accounts × 2 date variants = 36 tests. @pytest.mark.parametrize("hdrs", ALL_HEADERS) @pytest.mark.parametrize("account", ACCOUNTS) @pytest.mark.parametrize( "extra", [ pytest.param("", id="defaults"), pytest.param( "start_date=2023-01-01&end_date=2023-12-31&top_size=7", id="custom_dates", ), ], ) def test_top_content(self, hdrs, account, extra): """Top content returns all five topn_ lists. Authorized users get populated lists with correct ordering. Unauthorized users get empty lists for all keys. """ payload = assert_endpoint( self._url(account, extra), headers=hdrs, items_key=None ) for key in self.TOP_KEYS: assert key in payload if has_access(hdrs, account["account_id"]): top_artists = payload["topn_artists"] assert isinstance(top_artists, list) if top_artists: values = [a["value"] for a in top_artists] assert values == sorted( values, reverse=True ), "topn_artists not sorted descending by value" else: for key in self.TOP_KEYS: assert payload[key] == [] # ── Group 2: Downloads top content ──────────────────────────────── # # 6 headers × 3 accounts = 18 tests. # Exercises AccountTopContentDownloads query class (different SQL # from the default streams path). @pytest.mark.parametrize("hdrs", ALL_HEADERS) @pytest.mark.parametrize("account", ACCOUNTS) def test_top_content_downloads(self, hdrs, account): """Top content with aggregation_type=downloads uses a different query class. AccountTopContentDownloads hits download tables instead of streaming tables. Date range is required (no all-time variant for downloads). """ payload = assert_endpoint( self._url( account, "aggregation_type=downloads" "&start_date=2023-01-01&end_date=2023-12-31", ), headers=hdrs, items_key=None, ) for key in self.TOP_KEYS: assert key in payload if has_access(hdrs, account["account_id"]): assert isinstance(payload["topn_artists"], list) else: for key in self.TOP_KEYS: assert payload[key] == [] class TestAccountVideoViews: """Integration tests for /account//video/summary and .../video/timeseries. Verifies video views across all vendor accounts, auth profiles, view types, variants, country filters, and date ranges. Groups: 1. All video view combinations (5 hdrs × 2 vendors × 2 endpoints × 5 variants × 3 views_types × 2 country filters × 2 date ranges = 600 tests) """ def _url(self, account, endpoint, variant, views_type, countries, dates): return ( f"{BASE_URL}/account/{account['account_id']}/video/{endpoint}" f"?account_type=vendor&type={variant}" f"&views_type={views_type}{countries}{dates}" ) # ── Group 1: All video view combinations ─────────────────────── # # 5 headers × 2 vendor accounts × 2 endpoints × 5 variants # × 3 views_types × 2 country filters × 2 date ranges. # Artist profiles are excluded — they don't have access to video endpoints. @pytest.mark.parametrize("hdrs", NON_ARTIST_HEADERS) @pytest.mark.parametrize( "account", [a for a in ACCOUNTS if a["account_type"] == "vendor"] ) @pytest.mark.parametrize( "endpoint,expect_date", [ pytest.param("summary", False, id="summary"), pytest.param("timeseries", True, id="timeseries"), ], ) @pytest.mark.parametrize( "variant", [ "COUNTRY", "PARTICIPANT", "TOTAL", "TRACK", "VIDEO", # "TRACK_FAMILY" is intentionally disabled. It is a dead variant: # no client requests it (0 calls in 90d of prod logs; the frontend's # resolvePOTDimension only ever emits PARTICIPANT/TRACK/COUNTRY/ # VIDEO/TOTAL, and graphql-analytics only passes `type` through). # Its query also fan-out-joins STREAMS_BY_TRACK_FEED_DISTRIBUTOR_ROLLUP # (~8 rows/ISRC) before aggregating, which both inflates views ~2.6x # and times out on QA's warehouse. Re-enable (and fix the query) # only if a client starts using it. See PR for full analysis. ], ) @pytest.mark.parametrize("views_type", ["ALL", "AD_SUPPORTED", "SUBSCRIPTION"]) @pytest.mark.parametrize( "countries", [ pytest.param("", id="all_countries"), pytest.param("&countries=US", id="US"), ], ) @pytest.mark.parametrize( "dates", [ pytest.param("&start_date=2024-03-01&end_date=2024-03-31", id="1month"), pytest.param("&start_date=2024-01-01&end_date=2024-03-31", id="3months"), ], ) def test_video_views( self, hdrs, account, endpoint, expect_date, variant, views_type, countries, dates, ): """Video views endpoint returns items with views >= 0. Summary items have no date key; timeseries items include date. Unauthorized users see empty items list. """ payload = assert_endpoint( self._url(account, endpoint, variant, views_type, countries, dates), headers=hdrs, expect_empty=not has_access(hdrs, account["account_id"]), ) if has_access(hdrs, account["account_id"]) and payload["items"]: item = payload["items"][0] assert item["views"] >= 0 assert ("date" in item) == expect_date