"""Integration tests for historical tracklist endpoint /playlist/{id}/placements/on-date.""" import pytest import requests 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, ) # Test data SPOTIFY_TOP_50_USA = "37i9dQZF1DXcBWIGoYBM5M" # Top 50 - USA SPOTIFY_STORE_ID = 286 APPLE_MUSIC_STORE_ID = 1 TEST_DATE = "2025-10-31" class TestPlacementsByStorePlaylistIdOnDate: """Test /playlist/{store_playlist_id}/placements/on-date endpoint.""" def test_requires_authentication(self): """Test that endpoint requires authentication headers.""" response = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, }, ) assert response.status_code == 401 assert response.json()["error"] == "Unauthorised" def test_returns_data_with_valid_permissions(self): """Test that endpoint returns data when authenticated.""" response = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 data = response.json()["data"] assert "placements" in data assert "total_count" in data assert len(data["placements"]) > 0 assert data["total_count"] > 0 def test_no_duplicate_positions(self): """Test that each position appears only once in results.""" response = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "limit": 100, # Get enough to check for duplicates }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 placements = response.json()["data"]["placements"] # Check that each position appears only once positions = [p["current_position"] for p in placements] assert len(positions) == len(set(positions)), "Found duplicate positions" def test_no_duplicate_isrcs_at_same_position(self): """Test that the same ISRC doesn't appear twice at the same position.""" response = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "limit": 100, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 placements = response.json()["data"]["placements"] # Build a dict of position -> list of ISRCs position_isrcs = {} for p in placements: pos = p["current_position"] isrc = p["isrc"] if pos not in position_isrcs: position_isrcs[pos] = [] position_isrcs[pos].append(isrc) # Check no position has multiple ISRCs for pos, isrcs in position_isrcs.items(): assert len(isrcs) == 1, f"Position {pos} has multiple ISRCs: {isrcs}" def test_pagination_default_limit(self): """Test that default pagination limit is 20.""" response = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 data = response.json()["data"] assert len(data["placements"]) == 20 assert data["total_count"] > 20 # This playlist has more than 20 tracks def test_pagination_custom_limit(self): """Test that custom limit parameter works.""" response = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "limit": 10, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 data = response.json()["data"] assert len(data["placements"]) == 10 def test_pagination_offset(self): """Test that offset parameter works for pagination.""" # Get first page response1 = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "limit": 10, "offset": 0, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response1.status_code == 200 page1 = response1.json()["data"]["placements"] # Get second page response2 = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "limit": 10, "offset": 10, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response2.status_code == 200 page2 = response2.json()["data"]["placements"] # Ensure pages don't overlap page1_isrcs = {p["isrc"] for p in page1} page2_isrcs = {p["isrc"] for p in page2} assert len(page1_isrcs.intersection(page2_isrcs)) == 0 def test_positions_are_sequential(self): """Test that positions are returned in order.""" response = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "limit": 100, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 placements = response.json()["data"]["placements"] # Check positions are in ascending order positions = [p["current_position"] for p in placements] assert positions == sorted(positions) def test_required_fields_present(self): """Test that all expected fields are present in response.""" response = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "limit": 1, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 placement = response.json()["data"]["placements"][0] # Required fields required_fields = [ "isrc", "store_id", "store_playlist_id", "current_position", "previous_position", "peak_position", "position_change", "last_added_on_date", "days_on_playlist", "removed_on", "storefront_count", ] for field in required_fields: assert field in placement, f"Missing required field: {field}" def test_streaming_data_aggregation(self): """Test that streaming data is properly aggregated across permissions.""" response = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "limit": 100, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 placements = response.json()["data"]["placements"] # Find placements with streaming data placements_with_streams = [ p for p in placements if p.get("streams_all_time") is not None ] if len(placements_with_streams) > 0: # Verify streaming fields are numeric and non-negative for p in placements_with_streams: assert isinstance(p["streams_all_time"], (int, float)) assert p["streams_all_time"] >= 0 # If we have streams, completion rates should be between 0 and 1 if p.get("completion_rate_all_time") is not None: assert 0 <= p["completion_rate_all_time"] <= 1 def test_apple_music_storefront_support(self): """Test that Apple Music with storefront parameter works.""" # Use a known Apple Music playlist for testing apple_playlist_id = "pl.5ee8333dbe944d9f9151e97d92d1afa9" # Today's Hits response = requests.get( f"{QA_BASE_URL}/playlist/{apple_playlist_id}/placements/on-date", params={ "store_id": APPLE_MUSIC_STORE_ID, "storefront": "us", "target_date": TEST_DATE, "limit": 10, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) # Should either return data or 404 if not tracked assert response.status_code == 200 if response.status_code == 200: data = response.json()["data"] assert "placements" in data if len(data["placements"]) > 0: assert data["placements"][0]["store_id"] == APPLE_MUSIC_STORE_ID else: assert data["total_count"] == 0 def test_position_change_calculation(self): """Test that position_change is correctly calculated.""" response = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "limit": 100, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 placements = response.json()["data"]["placements"] for p in placements: current = p["current_position"] previous = p["previous_position"] change = p["position_change"] # If both positions exist, verify calculation if current is not None and previous is not None and change is not None: # position_change = previous - current # Negative = improved (moved up), Positive = dropped (moved down) expected_change = previous - current assert change == expected_change, ( f"Position change mismatch for {p['isrc']}: " f"current={current}, previous={previous}, " f"change={change}, expected={expected_change}" ) def test_first_play_date_is_earliest(self): """Test that first_play is the earliest date when aggregating multiple records.""" response = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "limit": 100, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 placements = response.json()["data"]["placements"] # Check that first_play is a valid date when present from datetime import datetime for p in placements: if p.get("first_play"): # Parse date to ensure it's valid first_play = datetime.fromisoformat(p["first_play"]) assert isinstance(first_play, datetime) # If last_added_on_date is also present, first_play should be on or after # (track gets added, then gets its first play) if p.get("last_added_on_date"): last_added = datetime.fromisoformat( p["last_added_on_date"].replace("+00:00", "") ) # In practice, first_play is often before last_added due to historical data # Just verify both are valid datetime objects assert isinstance(last_added, datetime) @pytest.mark.skip( reason="Needs IN-16530 implemented for correct permission filtering behavior." ) def test_permission_filtering(self): """Test that different permissions return different streaming data. (Skipped until IN-16530 is implemented)""" pass def test_stream_countries_filter(self): """Test that stream_countries parameter filters streaming data correctly.""" # Get data without stream_countries filter response_no_filter = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "limit": 50, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) # Get data with stream_countries filter for US only response_us_only = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "streams_country": ["US"], "limit": 50, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) # Get data with stream_countries filter for multiple countries response_multi_country = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "streams_country": ["US", "CA", "MX"], "limit": 50, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response_no_filter.status_code == 200 assert response_us_only.status_code == 200 assert response_multi_country.status_code == 200 # All should return the same tracks (position data is not filtered) no_filter_isrcs = { p["isrc"] for p in response_no_filter.json()["data"]["placements"] } us_only_isrcs = { p["isrc"] for p in response_us_only.json()["data"]["placements"] } multi_country_isrcs = { p["isrc"] for p in response_multi_country.json()["data"]["placements"] } assert no_filter_isrcs == us_only_isrcs == multi_country_isrcs # But streaming numbers should differ when filtered # US-only should generally have lower or equal streams compared to no filter no_filter_placements = response_no_filter.json()["data"]["placements"] us_only_placements = response_us_only.json()["data"]["placements"] multi_country_placements = response_multi_country.json()["data"]["placements"] # Create lookup by ISRC for comparison no_filter_map = {p["isrc"]: p for p in no_filter_placements} us_only_map = {p["isrc"]: p for p in us_only_placements} multi_country_map = {p["isrc"]: p for p in multi_country_placements} # Check a few tracks to verify streaming data differs for isrc in list(no_filter_isrcs)[:5]: # Check first 5 tracks no_filter_track = no_filter_map.get(isrc) us_only_track = us_only_map.get(isrc) multi_country_track = multi_country_map.get(isrc) # If streaming data exists for this track if ( no_filter_track and no_filter_track.get("streams_all_time") is not None and us_only_track and us_only_track.get("streams_all_time") is not None ): # US-only streams should be <= total streams (may be equal if track only has US streams) assert ( us_only_track["streams_all_time"] <= no_filter_track["streams_all_time"] ), f"US-only streams should not exceed total streams for {isrc}" # Multi-country streams should be >= US-only (includes more countries) if ( multi_country_track and multi_country_track.get("streams_all_time") is not None ): assert ( multi_country_track["streams_all_time"] >= us_only_track["streams_all_time"] ), f"Multi-country streams should be >= US-only for {isrc}" def test_curator_countries_filter(self): """Test that curator_countries parameter works with historical data.""" # Get data with curator_countries filter response = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "curator_country": ["US"], "limit": 50, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 placements = response.json()["data"]["placements"] assert len(placements) > 0 # Verify we got results (curator_countries filters streaming data by country) # The Top 50 USA playlist should have US curator country assert placements[0]["store_id"] == int(SPOTIFY_STORE_ID) class TestPlacementsByStorePlaylistId: """Test /playlist/{store_playlist_id}/placements endpoint (current placements).""" # TODO: Re-enable with resilient test data approach # This test is brittle because it relies on hard-coded ISRC values from a live # Spotify playlist that changes frequently. The playlist "A Nonsense Christmas" # (37i9dQZF1DWWvvyNmW9V9a) has had its tracklist updated, causing the expected # ISRC at position 1 to change from USUM72222808 to GBKPL2205058. # # The underlying logic being tested (removed_on < last_added_on_date filter) is # correct, but the test needs to be refactored to either: # 1. Query the data dynamically and verify behavior patterns instead of specific ISRCs # 2. Use controlled test fixtures with known re-added tracks # 3. Query for tracks with removed_on < last_added_on_date and verify they appear @pytest.mark.skip( reason=( "Brittle test using live Spotify playlist data with hard-coded ISRCs. " "The test assumes specific tracks will be re-added to playlist 37i9dQZF1DWWvvyNmW9V9a " "(A Nonsense Christmas), but playlist content changes frequently causing test failures. " "The underlying filter logic (removed_on < last_added_on_date) is correct. " "Needs refactoring to either: (1) query dynamically and verify patterns instead of " "specific ISRCs, (2) use controlled test fixtures, or (3) use a more stable test playlist." ) ) def test_readded_tracks_appear_in_current_placements(self): """Test that tracks removed and then re-added appear in current placements. This is a regression test for the bug where tracks with: - removed_on date < last_added_on_date (track was removed then re-added) Were incorrectly filtered out by the playlist_appearance_filters macro. The fix changed the filter from: (removed_on is null) To: (removed_on is null or removed_on < last_added_on_date) """ # Use a known playlist with re-added tracks # Playlist: 37i9dQZF1DWWvvyNmW9V9a (A Nonsense Christmas) # ISRC: GBKPL2205058 at position 1 # - removed_on: 2024-12-28 # - last_added_on_date: 2025-11-28 (re-added after removal) playlist_id = "37i9dQZF1DWWvvyNmW9V9a" expected_isrc = "GBKPL2205058" # Position 1 # Query without stream_countries filter response_no_filter = requests.get( f"{QA_BASE_URL}/playlist/{playlist_id}/placements", params={ "store_id": SPOTIFY_STORE_ID, "limit": 10, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response_no_filter.status_code == 200 placements_no_filter = response_no_filter.json()["data"]["placements"] # Position 1 should be present somewhere in the results assert len(placements_no_filter) > 0 position_1_track = next( (p for p in placements_no_filter if p["current_position"] == 1), None ) assert position_1_track is not None, "Position 1 should be present in results" assert position_1_track["isrc"] == expected_isrc # Query with stream_countries filter (the original bug scenario) response_with_country = requests.get( f"{QA_BASE_URL}/playlist/{playlist_id}/placements", params={ "store_id": SPOTIFY_STORE_ID, "limit": 10, "streams_country": ["GB"], }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response_with_country.status_code == 200 placements_with_country = response_with_country.json()["data"]["placements"] # Position 1 should STILL be present even when filtering by GB streams # (even though it may have no GB streaming data) assert len(placements_with_country) > 0 position_1_track_with_country = next( (p for p in placements_with_country if p["current_position"] == 1), None ) assert ( position_1_track_with_country is not None ), "Position 1 should be present with country filter" assert position_1_track_with_country["isrc"] == expected_isrc # Verify the track appears in both queries isrcs_no_filter = {p["isrc"] for p in placements_no_filter} isrcs_with_country = {p["isrc"] for p in placements_with_country} assert ( expected_isrc in isrcs_no_filter ), f"Re-added track {expected_isrc} missing without country filter" assert ( expected_isrc in isrcs_with_country ), f"Re-added track {expected_isrc} missing with country filter" # TODO: Re-enable after IN-16556-Dataload-placement-position-timeseries-data is merged # This test is timing out (504) because the query references previous_position_date # field that doesn't exist yet in the underlying tables. The dataload branch adds # this field to support the 14-day position trend feature. @pytest.mark.skip( reason="Waiting for IN-16556-Dataload-placement-position-timeseries-data to be merged" ) def test_days_on_playlist_never_negative(self): """Test that days_on_playlist is never negative for any track. This is a regression test for the bug where tracks that were removed and re-added would have negative days_on_playlist values due to incorrect aggregation using min(last_added_on_date) and min(removed_on) instead of max. The calculation should be: datediff('days', max(last_added_on_date), ifnull(max(removed_on), current_timestamp())) Not: datediff('days', min(last_added_on_date), ifnull(min(removed_on), current_timestamp())) """ # Use the same playlist with re-added tracks playlist_id = "37i9dQZF1DWWvvyNmW9V9a" response = requests.get( f"{QA_BASE_URL}/playlist/{playlist_id}/placements", params={ "store_id": SPOTIFY_STORE_ID, "limit": 100, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 placements = response.json()["data"]["placements"] # Check that all days_on_playlist values are positive for p in placements: days_on_playlist = p.get("days_on_playlist") if days_on_playlist is not None: assert days_on_playlist >= 0, ( f"Track {p['isrc']} at position {p['current_position']} " f"has negative days_on_playlist: {days_on_playlist}. " f"last_added_on_date: {p.get('last_added_on_date')}, " f"removed_on: {p.get('removed_on')}" ) def test_inferred_compilation_flag_disabled_by_default(self): """Test that inferred_compilation is not included when flag is not set.""" response = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "limit": 10, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 placements = response.json()["data"]["placements"] # Verify inferred_compilation field is NOT present for placement in placements: assert "inferred_compilation" not in placement def test_inferred_compilation_flag_when_explicitly_disabled(self): """Test that inferred_compilation is not included when flag is false.""" response = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "insights_playlist_page_hide_compilation_art": "false", "limit": 10, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 placements = response.json()["data"]["placements"] # Verify inferred_compilation field is NOT present for placement in placements: assert "inferred_compilation" not in placement def test_inferred_compilation_flag_when_enabled(self): """Test that inferred_compilation is included when flag is true.""" response = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "insights_playlist_page_hide_compilation_art": "true", "limit": 10, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 placements = response.json()["data"]["placements"] # Verify inferred_compilation field IS present assert len(placements) > 0 for placement in placements: assert "inferred_compilation" in placement # Field should be boolean or null assert placement["inferred_compilation"] in [True, False, None] def test_inferred_compilation_values_are_valid(self): """Test that inferred_compilation values are boolean or null.""" response = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "insights_playlist_page_hide_compilation_art": "true", "limit": 50, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 placements = response.json()["data"]["placements"] # Check that all values are valid for placement in placements: value = placement.get("inferred_compilation") assert value in [ True, False, None, ], f"Invalid inferred_compilation value for ISRC {placement['isrc']}: {value}" def test_inferred_compilation_with_storefront_enabled(self): """Test that inferred_compilation works with storefront_enabled.""" apple_playlist_id = "pl.5ee8333dbe944d9f9151e97d92d1afa9" # Today's Hits response = requests.get( f"{QA_BASE_URL}/playlist/{apple_playlist_id}/placements/on-date", params={ "store_id": APPLE_MUSIC_STORE_ID, "target_date": TEST_DATE, "storefront": "us", "insights_playlist_page_hide_compilation_art": "true", "limit": 10, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) # Endpoint may not have data for this specific date/playlist combo if response.status_code == 200: placements = response.json()["data"]["placements"] if len(placements) > 0: # Verify inferred_compilation field is present for placement in placements: assert "inferred_compilation" in placement def test_inferred_compilation_with_permissions(self): """Test that inferred_compilation works with permission filtering.""" response = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "insights_playlist_page_hide_compilation_art": "true", "limit": 20, }, headers=SUBACCOUNT_LABEL_AND_LABEL_PARTICIPANT_PERMISSION_HEADERS, ) assert response.status_code == 200 placements = response.json()["data"]["placements"] # With restricted permissions, may get fewer results # But inferred_compilation should still be present if len(placements) > 0: for placement in placements: assert "inferred_compilation" in placement def test_no_duplicate_isrcs_with_compilation_flag(self): """Test that compilation flag doesn't create duplicate ISRCs.""" response = requests.get( f"{QA_BASE_URL}/playlist/{SPOTIFY_TOP_50_USA}/placements/on-date", params={ "store_id": SPOTIFY_STORE_ID, "target_date": TEST_DATE, "insights_playlist_page_hide_compilation_art": "true", "limit": 100, }, headers=ALL_PERMISSIONS_EMPLOYEE_HEADERS, ) assert response.status_code == 200 placements = response.json()["data"]["placements"] # Check that each ISRC appears only once isrcs = [p["isrc"] for p in placements] assert len(isrcs) == len(set(isrcs)), "Found duplicate ISRCs" # Check that each position appears only once positions = [p["current_position"] for p in placements] assert len(positions) == len(set(positions)), "Found duplicate positions"