"""Integration tests.""" import pytest import requests from playlist.queries.constants import DEFAULT_PAGE_SIZE from tests.integration.config import QA_BASE_URL from tests.integration.consts.permissions import ( ALL_PERMISSIONS_EMPLOYEE_HEADERS, SUBACCOUNT_LABEL_AND_LABEL_PARTICIPANT_PERMISSION_HEADERS, ) from tests.integration.utils import get gpid = "c7f5f3bc-ceb6-41b4-bb31-a9f8b14c95d7" isrc = "QM7282022872" # Dynamite - BTS (https://insights.theorchard.com/song/QM7282022872/playlists) store_playlist_id = "37i9dQZF1DX08mhnhv6g9b" # This is BTS (https://open.spotify.com/playlist/37i9dQZF1DX08mhnhv6g9b) store_id = "286" # Spotify class TestPlacements: """Test /placements""" def test_isrc_filtered(self): response = requests.get( f"{QA_BASE_URL}/placements?isrc={isrc}", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["placements"] assert len(response_body) == DEFAULT_PAGE_SIZE def test_limit(self): response = requests.get( f"{QA_BASE_URL}/placements?isrc={isrc}&limit=1", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["placements"] assert len(response_body) == 1 def test_total_count(self): querystring = { "global_participant_id": "193f82f8-3944-4449-8962-f7f4b41d90a1", "limit": "50", "offset": "0", "playlist_type": ["ALGORITHMIC", "CURATED", "EDITORIAL", "PERSONALIZED"], "sort_direction": "DESC", "sort_key": "streams_all_time", } response = requests.get( f"{QA_BASE_URL}/placements", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, params=querystring, ) r = response.json() total_count = r["data"]["total_count"] assert total_count > 40 def test_sort_by_total_playlist_streams(self): querystring = { "isrc": "GBKPL2205058", "limit": "50", "offset": "0", "playlist_type": ["ALGORITHMIC", "CURATED", "EDITORIAL", "PERSONALIZED"], "sort_direction": "DESC", "sort_key": "total_playlist_streams_last_7_days", } response = requests.get( f"{QA_BASE_URL}/placements", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, params=querystring, ) r = response.json() total_count = r["data"]["total_count"] assert total_count > 1 response_placements = r["data"]["placements"] for i, placement in enumerate(response_placements): if i > 0: previous_placement = response_placements[i - 1][ "total_playlist_streams_last_7_days" ] assert ( placement["total_playlist_streams_last_7_days"] <= previous_placement ) class TestRecentPlacements: """Test /placements/label/recent""" def test_employee_permissions(self): response = get( f"{QA_BASE_URL}/placements/recent?distributor=theorchard&label_id=26760&sort_direction=DESC&sort_key=last_added_on_date&store_id=1&store_id=286", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["placements"] assert len(response_body) == DEFAULT_PAGE_SIZE def test_storefront_enabled_with_total_count(self): """Test that storefront_enabled parameter works and returns valid total_count""" response = get( f"{QA_BASE_URL}/placements/recent?distributor=theorchard&label_id=26760&sort_direction=DESC&sort_key=last_added_on_date&store_id=1&storefront_enabled=true", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"] assert "placements" in response_body assert "total_count" in response_body assert isinstance(response_body["total_count"], int) assert response_body["total_count"] >= 0 assert len(response_body["placements"]) <= DEFAULT_PAGE_SIZE class TestPlaylistTracklist: """Test /playlist//placements (tracklist endpoint)""" def test_no_duplicate_positions(self): """Verify that each position appears only once in the tracklist""" response = requests.get( f"{QA_BASE_URL}/playlist/37i9dQZF1DXcBWIGoYBM5M/placements?store_id=286&limit=200", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 placements = response.json()["data"]["placements"] # Extract all positions (excluding nulls) positions = [ p["current_position"] for p in placements if p["current_position"] is not None ] # Check for duplicates assert len(positions) == len( set(positions) ), f"Found duplicate positions: {[p for p in set(positions) if positions.count(p) > 1]}" def test_no_duplicate_isrcs(self): """Verify that each ISRC appears only once in the tracklist""" response = requests.get( f"{QA_BASE_URL}/playlist/37i9dQZF1DXcBWIGoYBM5M/placements?store_id=286&limit=200", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 placements = response.json()["data"]["placements"] # Extract all ISRCs isrcs = [p["isrc"] for p in placements] # Check for duplicates assert len(isrcs) == len( set(isrcs) ), f"Found duplicate ISRCs: {[i for i in set(isrcs) if isrcs.count(i) > 1]}" def test_no_null_positions_in_editorial_playlist(self): """Verify that editorial playlists don't return tracks with null positions""" response = requests.get( f"{QA_BASE_URL}/playlist/37i9dQZF1DXcBWIGoYBM5M/placements?store_id=286&limit=200", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 placements = response.json()["data"]["placements"] # Check that no tracks have null positions null_position_tracks = [ p["isrc"] for p in placements if p["current_position"] is None ] assert ( len(null_position_tracks) == 0 ), f"Found tracks with null positions: {null_position_tracks}" def test_pagination(self): """Test that pagination works correctly""" response = requests.get( f"{QA_BASE_URL}/playlist/{store_playlist_id}/placements?store_id={store_id}&limit=10&offset=0", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"] assert len(response_body["placements"]) <= 10 assert "total_count" in response_body def test_gsr_split_response_structure(self): """Test that response includes both placements and placeholder_placements arrays""" response = requests.get( f"{QA_BASE_URL}/playlist/37i9dQZF1DXcBWIGoYBM5M/placements?store_id=286&limit=200", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 data = response.json()["data"] # Verify both arrays exist assert "placements" in data assert "placeholder_placements" in data assert "total_count" in data # Verify they're arrays assert isinstance(data["placements"], list) assert isinstance(data["placeholder_placements"], list) # Verify placements have ISRCs (not gsr_id field) if len(data["placements"]) > 0: assert "isrc" in data["placements"][0] assert "gsr_id" not in data["placements"][0] # Verify placeholders have track_name, artist_name (Chartmetric fields) if len(data["placeholder_placements"]) > 0: placeholder = data["placeholder_placements"][0] assert "isrc" in placeholder assert "track_name" in placeholder assert "gsr_id" not in placeholder class TestPlacementPositions: """Test /playlist//placement//positions""" url = f"/playlist/{store_playlist_id}/placement/{isrc}/positions" def test_start_and_end_date_filtered(self): response = requests.get( f"{QA_BASE_URL}{self.url}?start_date=2021-01-01&days=20&store_id={store_id}", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"] position_time_series = response_body["positions"] assert len(position_time_series) == 20 class TestPlacementStreams: """Test /playlist//placement//streams""" url = f"/playlist/{store_playlist_id}/placement/{isrc}/streams" def test_start_and_end_date_filtered(self): response = requests.get( f"{QA_BASE_URL}{self.url}?start_date=2021-01-10&days=6&store_id={store_id}", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200, response.text response_body = response.json()["data"] position_time_series = response_body["streams"] assert len(position_time_series) == 6 class TestPlacementsBreakdown: """Test /placements/breakdown""" def test_isrc_filtered(self): response = requests.get( f"{QA_BASE_URL}/placements/breakdown?isrc={isrc}", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 total = response.json()["data"]["total_count"] assert total >= 0 def test_global_participant_filtered(self): response = requests.get( f"{QA_BASE_URL}/placements/breakdown?global_participant_id={gpid}", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 total = response.json()["data"]["total_count"] assert total >= 0 class TestTotalVsPlaylistTimeSeries: """Test /placements/total_vs_playlist_streams_by_store""" url = "/placements/total_vs_playlist_streams_by_store" def test_date_filtered(self): response = requests.get( f"{QA_BASE_URL}{self.url}?isrc=QMFME2004132&store_id=1&start_date=2021-05-20&days=14", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"] time_series = response_body["stores"] assert len(time_series) == 1 def test_global_participant_id_filtered(self): response = requests.get( f"{QA_BASE_URL}{self.url}?global_participant_id=90097833-cc23-470e-8643-2268e156497f&store_id=1&start_date=2021-09-10&days=1", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"] time_series = response_body["stores"] assert len(time_series) == 1 class TestPlacement: """Test /playlist//placement/""" def test_isrc_filtered(self): response = requests.get( f"{QA_BASE_URL}/playlist/{store_playlist_id}/placement/{isrc}?store_id={store_id}", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["placement"] assert response_body class TestPlacementsBreakdownByCountry: """Test /playlist//placement//breakdown-by-country""" def test_minimal(self): response = requests.get( f"{QA_BASE_URL}/playlist/{store_playlist_id}/placement/{isrc}/breakdown-by-country?store_id=286", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["countries"] assert response_body class TestPlacementsCount: """Test /placements_count""" def test_total_count(self): querystring = { "global_participant_id": "193f82f8-3944-4449-8962-f7f4b41d90a1", "playlist_type": ["ALGORITHMIC", "CURATED", "EDITORIAL", "PERSONALIZED"], } response = requests.get( f"{QA_BASE_URL}/placements_count", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, params=querystring, ) r = response.json() total_count = r["data"]["total_count"] assert total_count >= 0 class TestSoundRecordingTopPlacements: """Test sound-recording//top-placements""" def test_for_default(self): response = requests.get( f"{QA_BASE_URL}/sound-recording/{isrc}/top-placements", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["placements"] assert len(response_body) == 12 def test_limit(self): response = requests.get( f"{QA_BASE_URL}/sound-recording/{isrc}/top-placements?limit=1", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["placements"] assert len(response_body) == 1 def test_total_count(self): querystring = { "limit": "12", "offset": "0", "playlist_type": ["CURATED"], "playlist_appearances": "current", "sort_direction": "DESC", "sort_key": "streams_all_time", } response = requests.get( f"{QA_BASE_URL}/sound-recording/{isrc}/top-placements", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, params=querystring, ) r = response.json() total_count = r["data"]["total_count"] assert total_count > 10 class TestSoundRecordingPlacementAnalticsBulk: """Test sound-recording/placement-analytics-bulk""" playlist_placements = [ { "isrc": "QMFME2364182", "store_id": "286", "store_playlist_id": "37i9dQZF1DXcBWIGoYBM5M", }, ] playlist_placements_with_storefront = [ { "isrc": "QMFME2364182", "store_id": "1", "store_playlist_id": "pl.56b8dbaf59a9471bbf0af144dc7a0f2b", "storefront": "US", }, ] playlist_placements_with_and_without_storefront = [ { "isrc": "QMFME2364182", "store_id": "286", "store_playlist_id": "37i9dQZF1DXcBWIGoYBM5M", }, { "isrc": "QMFME2364182", "store_id": "1", "store_playlist_id": "pl.56b8dbaf59a9471bbf0af144dc7a0f2b", "storefront": "US", }, ] def test_for_default(self): input_data = { "playlist_placements": self.playlist_placements, "params": {}, } response = requests.post( f"{QA_BASE_URL}/sound-recording/placement-analytics-bulk", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["placements"] assert len(response_body) == 1 def test_for_missing_playlist_placements(self): input_data = { "playlist_placements": [], "params": {}, } response = requests.post( f"{QA_BASE_URL}/sound-recording/placement-analytics-bulk", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 400 response_body = response.json() assert response_body == {"error": "Missing playlist_placements"} def test_for_default_with_storefront(self): input_data = { "playlist_placements": self.playlist_placements_with_storefront, "params": {}, } response = requests.post( f"{QA_BASE_URL}/sound-recording/placement-analytics-bulk", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["placements"] assert len(response_body) == 1 def test_for_default_with_and_without_storefront(self): input_data = { "playlist_placements": self.playlist_placements_with_and_without_storefront, "params": {}, } response = requests.post( f"{QA_BASE_URL}/sound-recording/placement-analytics-bulk", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["placements"] assert len(response_body) == 2 class TestSoundRecordingPlacementStreamsBulk: """Test sound-recording/placement-streams-bulk""" playlist_placements = [ { "isrc": "QMFME2364182", "store_id": "286", "store_playlist_id": "37i9dQZF1DXcBWIGoYBM5M", }, ] playlist_placements_with_storefront = [ { "isrc": "QMFME2364182", "store_id": "1", "store_playlist_id": "pl.56b8dbaf59a9471bbf0af144dc7a0f2b", "storefront": "US", }, ] playlist_placements_with_and_without_storefront = [ { "isrc": "QMFME2364182", "store_id": "286", "store_playlist_id": "37i9dQZF1DXcBWIGoYBM5M", }, { "isrc": "QMFME2364182", "store_id": "1", "store_playlist_id": "pl.56b8dbaf59a9471bbf0af144dc7a0f2b", "storefront": "US", }, ] def test_for_default(self): input_data = { "playlist_placements": self.playlist_placements, "params": {}, } response = requests.post( f"{QA_BASE_URL}/sound-recording/placement-streams-bulk", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["placements"] assert len(response_body) >= 0 # May return empty if no streams data def test_for_missing_playlist_placements(self): input_data = { "playlist_placements": [], "params": {}, } response = requests.post( f"{QA_BASE_URL}/sound-recording/placement-streams-bulk", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 400 response_body = response.json() assert response_body == {"error": "Missing playlist_placements"} def test_for_default_with_storefront(self): input_data = { "playlist_placements": self.playlist_placements_with_storefront, "params": {}, } response = requests.post( f"{QA_BASE_URL}/sound-recording/placement-streams-bulk", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["placements"] assert len(response_body) >= 0 # May return empty if no streams data def test_for_default_with_and_without_storefront(self): input_data = { "playlist_placements": self.playlist_placements_with_and_without_storefront, "params": {}, } response = requests.post( f"{QA_BASE_URL}/sound-recording/placement-streams-bulk", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["placements"] assert len(response_body) >= 0 # May return empty if no streams data def test_with_stream_countries_filter(self): input_data = { "playlist_placements": self.playlist_placements, "params": { "stream_countries": ["US", "CA"], }, } response = requests.post( f"{QA_BASE_URL}/sound-recording/placement-streams-bulk", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["placements"] assert len(response_body) >= 0 # May return empty if no streams data def test_response_contains_expected_stream_fields(self): """Verify the response contains the expected stream-related fields.""" input_data = { "playlist_placements": self.playlist_placements, "params": {}, } response = requests.post( f"{QA_BASE_URL}/sound-recording/placement-streams-bulk", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["placements"] if len(response_body) > 0: placement = response_body[0] # Check that stream fields are present (may be null) expected_fields = [ "isrc", "store_id", "store_playlist_id", "streams_all_time", "streams_last_365_days", "streams_last_183_days", "streams_last_28_days", "streams_last_7_days", "streams_last_1_day", ] for field in expected_fields: assert field in placement, f"Expected field '{field}' not in response" class TestPlaylistMetadataBulk: """Test playlist-metadata-bulk""" playlists = [ { "store_id": "286", "store_playlist_id": "37i9dQZF1DXcBWIGoYBM5M", }, ] playlists_with_storefront = [ { "store_id": "1", "store_playlist_id": "pl.56b8dbaf59a9471bbf0af144dc7a0f2b", "storefront": "US", }, ] playlists_with_and_without_storefront = [ { "store_id": "286", "store_playlist_id": "37i9dQZF1DXcBWIGoYBM5M", }, { "store_id": "1", "store_playlist_id": "pl.56b8dbaf59a9471bbf0af144dc7a0f2b", "storefront": "US", }, ] def test_for_default(self): input_data = { "playlists": self.playlists, } response = requests.post( f"{QA_BASE_URL}/playlist-metadata-bulk", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["playlists"] assert len(response_body) == 1 def test_for_missing_playlist_placements(self): input_data = { "playlists": [], } response = requests.post( f"{QA_BASE_URL}/playlist-metadata-bulk", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 400 response_body = response.json() assert response_body == {"error": "Missing playlists"} def test_for_default_with_storefront(self): input_data = { "playlists": self.playlists_with_storefront, } response = requests.post( f"{QA_BASE_URL}/playlist-metadata-bulk", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["playlists"] assert len(response_body) == 1 def test_for_default_with_and_without_storefront(self): input_data = { "playlists": self.playlists_with_and_without_storefront, } response = requests.post( f"{QA_BASE_URL}/playlist-metadata-bulk", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["playlists"] assert len(response_body) == 2 class TestPlaylistAnalyticsBulk: """Test playlist-analytics-bulk""" playlists = [ { "store_id": "286", "store_playlist_id": "37i9dQZF1DXcBWIGoYBM5M", }, ] playlists_with_storefront = [ { "store_id": "1", "store_playlist_id": "pl.56b8dbaf59a9471bbf0af144dc7a0f2b", "storefront": "US", }, ] playlists_with_and_without_storefront = [ { "store_id": "286", "store_playlist_id": "37i9dQZF1DXcBWIGoYBM5M", }, { "store_id": "1", "store_playlist_id": "pl.56b8dbaf59a9471bbf0af144dc7a0f2b", "storefront": "US", }, ] def test_for_default(self): input_data = { "playlists": self.playlists, } response = requests.post( f"{QA_BASE_URL}/playlist/analytics-bulk", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["playlists"] assert len(response_body) == 1 def test_for_missing_playlist_placements(self): input_data = { "playlists": [], } response = requests.post( f"{QA_BASE_URL}/playlist/analytics-bulk", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 400 response_body = response.json() assert response_body == {"error": "Missing playlists"} def test_for_default_with_storefront(self): input_data = { "playlists": self.playlists_with_storefront, } response = requests.post( f"{QA_BASE_URL}/playlist/analytics-bulk", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["playlists"] assert len(response_body) == 1 def test_for_default_with_and_without_storefront(self): input_data = { "playlists": self.playlists_with_and_without_storefront, } response = requests.post( f"{QA_BASE_URL}/playlist/analytics-bulk", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["playlists"] assert len(response_body) == 2 class TestPlaylistAnalyticsTimeseriesBulk: """Test playlist-analytics-bulk-timeseries""" playlists = [ { "store_id": "286", "store_playlist_id": "37i9dQZF1DXcBWIGoYBM5M", }, ] playlists_with_storefront = [ { "store_id": "1", "store_playlist_id": "pl.56b8dbaf59a9471bbf0af144dc7a0f2b", "storefront": "US", }, ] playlists_with_and_without_storefront = [ { "store_id": "286", "store_playlist_id": "37i9dQZF1DXcBWIGoYBM5M", }, { "store_id": "1", "store_playlist_id": "pl.56b8dbaf59a9471bbf0af144dc7a0f2b", "storefront": "US", }, ] def test_for_default(self): input_data = { "playlists": self.playlists, } response = requests.post( f"{QA_BASE_URL}/playlist/analytics-bulk-timeseries", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["playlists"] assert len(response_body) == 1 def test_for_missing_playlist_placements(self): input_data = { "playlists": [], } response = requests.post( f"{QA_BASE_URL}/playlist/analytics-bulk-timeseries", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 400 response_body = response.json() assert response_body == {"error": "Missing playlists"} def test_for_default_with_storefront(self): input_data = { "playlists": self.playlists_with_storefront, } response = requests.post( f"{QA_BASE_URL}/playlist/analytics-bulk-timeseries", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["playlists"] assert len(response_body) == 1 def test_for_default_with_and_without_storefront(self): input_data = { "playlists": self.playlists_with_and_without_storefront, } response = requests.post( f"{QA_BASE_URL}/playlist/analytics-bulk-timeseries", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["playlists"] assert len(response_body) == 2 def test_for_default_with_by_dimension_market(self): input_data = { "playlists": self.playlists_with_and_without_storefront, "by_dimension": "MARKET", "start_date": "2026-01-01", "end_date": "2026-01-05", } response = requests.post( f"{QA_BASE_URL}/playlist/analytics-bulk-timeseries", json=input_data, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["playlists"] assert len(response_body) == 2 dimensions = response_body[0]["dimensions"] assert len(dimensions) > 0 streams_array = dimensions[0]["streams_array"] assert len(streams_array) > 0 assert "activity_date" in streams_array[0] and "streams" in streams_array[0] listeners_array = dimensions[0]["listeners_array"] assert len(listeners_array) > 0 assert ( "activity_date" in listeners_array[0] and "listeners" in listeners_array[0] ) class TestPlaylistDemographics: """Test /playlist//demographics/""" # Spotify playlist for testing store_playlist_id = "37i9dQZF1DX2apWzyECwyZ" store_id = "286" def test_basic_demographics(self): """Test basic demographics endpoint returns expected structure""" response = requests.get( f"{QA_BASE_URL}/playlist/{self.store_playlist_id}/demographics/?store_id={self.store_id}", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["demographics"] # Should return at least one record assert len(response_body) >= 0 # If we have data, check structure if len(response_body) > 0: demo = response_body[0] assert "total_streams" in demo assert "percent_male" in demo assert "percent_female" in demo assert "percent_under_18" in demo assert "percent_18_22" in demo assert "percent_23_27" in demo assert "percent_28_34" in demo assert "percent_35_44" in demo assert "percent_45_59" in demo assert "percent_60_plus" in demo def test_demographics_by_country(self): """Test demographics by country returns country breakdown""" response = requests.get( f"{QA_BASE_URL}/playlist/{self.store_playlist_id}/demographics/?store_id={self.store_id}&by_country=true", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"]["demographics"] # Should return country-level data if len(response_body) > 0: demo = response_body[0] assert "country_code" in demo assert "total_streams" in demo assert "percent_male" in demo assert "percent_female" in demo def test_demographics_missing_store_id(self): """Test that store_id is required""" response = requests.get( f"{QA_BASE_URL}/playlist/{self.store_playlist_id}/demographics/", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 400 assert "store_id is required" in response.json()["error"] def test_demographics_with_date_range(self): """Test demographics with custom date range""" response = requests.get( f"{QA_BASE_URL}/playlist/{self.store_playlist_id}/demographics/?store_id={self.store_id}&start_date=2025-01-01&end_date=2025-01-31", headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 response_body = response.json()["data"] assert "demographics" in response_body