"""Centralized fixtures and helpers for endpoint integration tests.""" import pytest import requests from owsrequest.constants import headers as owsrequest_headers from tests.integration.request import request_get_cache # --------------------------------------------------------------------------- # Header sets # --------------------------------------------------------------------------- # full access INSIGHTS_EMPLOYEE = { owsrequest_headers.ORCHARD_PROFILE_ID: "1100", owsrequest_headers.ORCHARD_PROFILE_TYPE: "InsightsProfile", } # access to RIMAS (label_id = 26760), this is a D3 label (a label with subaccounts) INSIGHTS_D3 = { owsrequest_headers.ORCHARD_PROFILE_ID: "99918924", owsrequest_headers.ORCHARD_PROFILE_TYPE: "InsightsProfile", } # access to Frenchkiss Records (label_id = 6971) INSIGHTS_LABEL = { owsrequest_headers.ORCHARD_PROFILE_ID: "99918925", owsrequest_headers.ORCHARD_PROFILE_TYPE: "InsightsProfile", } # access to Bad Bunny subaccount (subaccount_id 46189), this is a subaccount of RIMAS (label_id = 26760) INSIGHTS_SUBACCOUNT = { owsrequest_headers.ORCHARD_PROFILE_ID: "99918926", owsrequest_headers.ORCHARD_PROFILE_TYPE: "InsightsProfile", } # access to Frenchkiss Records (label_id = 6971) and Bad Bunny subaccount (subaccount_id 46189) INSIGHTS_LABEL_AND_SUBACCOUNT = { owsrequest_headers.ORCHARD_PROFILE_ID: "99918927", owsrequest_headers.ORCHARD_PROFILE_TYPE: "InsightsProfile", } # access to artist (global_participant) Local Natives (95ee273c-09cb-411c-ae00-8806e9ca938e) INSIGHTS_ARTIST = { owsrequest_headers.ORCHARD_PROFILE_ID: "99918928", owsrequest_headers.ORCHARD_PROFILE_TYPE: "InsightsProfile", } ALL_HEADERS = [ pytest.param(INSIGHTS_EMPLOYEE, id="employee"), pytest.param(INSIGHTS_D3, id="d3"), pytest.param(INSIGHTS_LABEL, id="label"), pytest.param(INSIGHTS_SUBACCOUNT, id="subaccount"), pytest.param(INSIGHTS_LABEL_AND_SUBACCOUNT, id="label_and_subaccount"), pytest.param(INSIGHTS_ARTIST, id="artist"), ] # Artist profiles don't have access to video endpoints NON_ARTIST_HEADERS = [h for h in ALL_HEADERS if h.id != "artist"] # --------------------------------------------------------------------------- # Access rules # --------------------------------------------------------------------------- ACCESS_RULES = { 26760: [ INSIGHTS_EMPLOYEE, INSIGHTS_D3, INSIGHTS_LABEL_AND_SUBACCOUNT, ], # RIMAS Entertainment LLC 6971: [ INSIGHTS_EMPLOYEE, INSIGHTS_LABEL, INSIGHTS_LABEL_AND_SUBACCOUNT, INSIGHTS_ARTIST, ], # Frenchkiss Records 46189: [ INSIGHTS_EMPLOYEE, INSIGHTS_D3, INSIGHTS_SUBACCOUNT, INSIGHTS_LABEL_AND_SUBACCOUNT, ], # Bad Bunny (subaccount) } RIMAS = {"account_type": "vendor", "account_id": 26760, "has_subaccounts": True} BAD_BUNNY = { "account_type": "subaccount", "account_id": 46189, "has_subaccounts": False, } FRENCHKISS = {"account_type": "vendor", "account_id": 6971, "has_subaccounts": False} ACCOUNTS = [RIMAS, BAD_BUNNY, FRENCHKISS] # Frenchkiss-owned video, channel, and UGC ISRC with stable QA data — used by # emptiness tests to exercise the permissions_filter_video_or_channel macro. # Headers in ACCESS_RULES[6971] (excluding artist) see populated payloads; all # others see empty. FRENCHKISS_VIDEO_ID = "IqYgNiZdfh4" FRENCHKISS_CHANNEL_ID = "UCFg7-w47VVA3ozhvnR6P0YQ" FRENCHKISS_UGC_ISRC = "USJMZ2100003" FRENCHKISS_DATE_RANGE = "?start_date=2024-01-01&days=31" # --------------------------------------------------------------------------- # Common constants # --------------------------------------------------------------------------- PAGINATION = "limit=50&order_by=streams&order_dir=desc" # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def profile_id(headers): """Extract profile ID from a headers dict.""" return headers[owsrequest_headers.ORCHARD_PROFILE_ID] def has_access(headers, account_id): """Check if the given headers grant access to the given account (label or subaccount).""" return headers in ACCESS_RULES.get(account_id, []) def has_video_access(headers, account_id): """Like has_access, but excludes artist headers. Video and channel detail endpoints early-return empty for artist callers (no label_ids → guard fires before SQL runs), so artists never see data even when listed in ACCESS_RULES for a vendor. """ return headers != INSIGHTS_ARTIST and headers in ACCESS_RULES.get(account_id, []) def assert_timeseries_keys(keys, ts_type): """Assert expected keys on a timeseries item.""" assert "date" in keys assert "value" in keys if "_BY_" in ts_type: assert "id" in keys if "SOS" in ts_type or "SUBSCRIPTION" in ts_type: return if "STREAMS" in ts_type: assert "skip_rate" in keys assert "saves" in keys def assert_summary_keys(item, summary_type): """Assert expected keys on a summary item.""" assert "streams" in item if "SOS" in summary_type or summary_type == "SUBSCRIPTION": return assert "saves" in item assert "skip_rate" in item def assert_endpoint( url, *, headers, method="GET", json_body=None, status=200, items_key: str | None = "items", expect_keys=None, expect_payload_keys=None, expect_empty=False, min_items=None, item_validator=None, payload_validator=None, ): """Single assertion entry point for all endpoint tests. Returns the parsed JSON payload for additional ad-hoc assertions. """ if method == "GET": response = request_get_cache(url, headers=headers) elif method == "POST": response = requests.post(url, json=json_body, headers=headers) else: raise ValueError(f"Unsupported HTTP method {method!r}") assert response.status_code == status, ( f"Expected {status}, got {response.status_code}. " f"Reason: {response.reason}, URL: {response.url}" ) payload = response.json() if expect_payload_keys: for key in expect_payload_keys: assert key in payload, f"Missing top-level key: {key}" if items_key is not None: assert items_key in payload, f"Missing '{items_key}' in response" items = payload[items_key] assert isinstance(items, list), f"'{items_key}' is not a list" if expect_empty: assert len(items) == 0 return payload if min_items is not None: assert len(items) >= min_items if expect_keys and items: first = items[0] for key in expect_keys: assert key in first, f"Missing key '{key}' in first item" if item_validator and items: item_validator(items[0]) if payload_validator: payload_validator(payload) return payload