"""Transfer-ownership tests for the demographics endpoints. ISRC BX69Y2100018 (the canonical ISRC — see conftest.ISRC) belongs only to product 5244974, so /sound-recording//demographics and /demographics?isrc= are clean probes of the product's transfer scoping. Both route through `demographics.sql` under the v2 `permissions_filter`. Demographic rows are sparser than raw streams — not every streamed day reports age/gender — so, like test_sound_recording.TestSoundRecordingStreamsBreakdown, these are *directional* checks: a former owner must surface no demographic counts for a window after their cutoff. The data-present direction is left to the streams suites, where the signal is dense and unambiguous. """ from __future__ import annotations import pytest from analytics import config from tests.integration.endpoints.conftest import assert_endpoint from tests.integration.transfer_ownership.conftest import ( ARTIST_PROFILES, ISRC, PROBES, TRANSFER_PROFILES, WINDOW_PROBES, expected_visibility, profile_key_for, ) def _demographics_total(payload: dict) -> float: """Sum every age + gender bucket count in a demographics payload. An empty result is `{"age": {}, "gender": {}}`, so the sum is 0. """ demographics = payload.get("demographics") or {} buckets = { **(demographics.get("age") or {}), **(demographics.get("gender") or {}), } return sum((value or 0) for value in buckets.values()) class TestSoundRecordingDemographics: """/sound-recording//demographics — DAILY grain, v2 permissions_filter.""" @pytest.mark.parametrize("hdrs", TRANSFER_PROFILES) @pytest.mark.parametrize("probe", WINDOW_PROBES) def test_no_forward_leak(self, hdrs, probe): """A former owner surfaces no demographic counts past their cutoff.""" base = config.DEMOGRAPHICS_URL.replace("", ISRC) payload = assert_endpoint( f"{base}?{PROBES[probe]}", headers=hdrs, items_key=None ) for field in ("isrc", "demographics", "sources"): assert field in payload, f"missing field: {field}" profile_key = profile_key_for(hdrs) if expected_visibility(profile_key, probe) == "empty": total = _demographics_total(payload) assert total == 0, ( f"{profile_key} leaked {total} demographic counts for probe {probe}" ) class TestSoundRecordingDemographicsArtist: """/sound-recording//demographics — participation-axis no-forward-leak. Directional check (demographic rows are sparse, see module docstring): the originating artist (former participant) must surface no demographic counts for a window after its move cutoff. """ @pytest.mark.parametrize("hdrs", ARTIST_PROFILES) @pytest.mark.parametrize("probe", WINDOW_PROBES) def test_no_forward_leak(self, hdrs, probe): """A former participant surfaces no demographic counts past its cutoff.""" base = config.DEMOGRAPHICS_URL.replace("", ISRC) payload = assert_endpoint( f"{base}?{PROBES[probe]}", headers=hdrs, items_key=None ) for field in ("isrc", "demographics", "sources"): assert field in payload, f"missing field: {field}" profile_key = profile_key_for(hdrs) if expected_visibility(profile_key, probe) == "empty": total = _demographics_total(payload) assert total == 0, ( f"{profile_key} leaked {total} demographic counts for probe {probe}" ) class TestDemographics2: """/demographics?isrc= — same `demographics.sql` path, isrc selector. Exercises the bare /demographics endpoint through its `isrc` selector, the one selector that pins a single product (5244974) and so can verify the transfer time-slice rather than just smoke-test the query. """ @pytest.mark.parametrize("hdrs", TRANSFER_PROFILES) @pytest.mark.parametrize("probe", WINDOW_PROBES) def test_no_forward_leak(self, hdrs, probe): """A former owner surfaces no demographic counts past their cutoff.""" url = f"{config.DEMOGRAPHICS_2_URL}?isrc={ISRC}&{PROBES[probe]}" payload = assert_endpoint(url, headers=hdrs, items_key=None) assert "demographics" in payload, "missing field: demographics" profile_key = profile_key_for(hdrs) if expected_visibility(profile_key, probe) == "empty": total = _demographics_total(payload) assert total == 0, ( f"{profile_key} leaked {total} demographic counts for probe {probe}" ) class TestDemographics2Artist: """/demographics?isrc= — participation-axis no-forward-leak. The bare /demographics endpoint reached through its `isrc` selector, gated by the viewer's participation branch. Directional: the originating artist surfaces no demographic counts after its move cutoff. """ @pytest.mark.parametrize("hdrs", ARTIST_PROFILES) @pytest.mark.parametrize("probe", WINDOW_PROBES) def test_no_forward_leak(self, hdrs, probe): """A former participant surfaces no demographic counts past its cutoff.""" url = f"{config.DEMOGRAPHICS_2_URL}?isrc={ISRC}&{PROBES[probe]}" payload = assert_endpoint(url, headers=hdrs, items_key=None) assert "demographics" in payload, "missing field: demographics" profile_key = profile_key_for(hdrs) if expected_visibility(profile_key, probe) == "empty": total = _demographics_total(payload) assert total == 0, ( f"{profile_key} leaked {total} demographic counts for probe {probe}" )