from datetime import UTC, datetime import pytest import sqlalchemy as sa from dirty_equals import IsDatetime from starlette.testclient import TestClient from resonance_engine.adapters.db import db from resonance_engine.dsp.enums import DSPClientName, DSPId from resonance_engine.dsp.models import DSPClient from resonance_engine.fandata.enums import FanConnectionStatus from resonance_engine.fandata.models import FanCollectionState, FanConnection from resonance_engine.fandata.utils import make_fan_id from tests.unit.helpers import create_model @pytest.mark.db def test_stats_returns_approx_breakdown(client: TestClient) -> None: for i in range(3): create_model(FanConnection, fan_id=f"active-{i}") create_model(FanConnection, fan_id="revoked-0", status=FanConnectionStatus.revoked) create_model( FanCollectionState, fan_id="active-0", dsp_id=DSPId.spotify, last_dsp_client_id=1, last_collected_at=datetime(2026, 5, 1, tzinfo=UTC), last_collection_error=None, ) db.session.execute(sa.text("ANALYZE fan_connection")) db.session.execute(sa.text("ANALYZE fan_collection_state")) response = client.get("/fandata/stats") assert response.status_code == 200 assert response.json() == { "connections": {"total": 4, "active": 3, "revoked": 1}, "collection": {"total": 1, "healthy": 1, "with_errors": 0}, "approximate": True, } @pytest.mark.db def test_create_connection_returns_row(client: TestClient) -> None: dsp_client = create_model( DSPClient, dsp_id=DSPId.spotify, name=DSPClientName.spotify_songwhip ) response = client.post( "/fandata/connections", json={ "email": "fan@example.com", "dsp_client_name": DSPClientName.spotify_songwhip, "token": "plaintext-token", }, ) assert response.status_code == 200 assert response.json() == { "fan_id": make_fan_id("fan@example.com"), "dsp_id": DSPId.spotify, "dsp_client_id": dsp_client.id, "status": FanConnectionStatus.active, "token_refreshed_at": None, "created_at": IsDatetime(iso_string=True), } @pytest.mark.db def test_create_connection_upserts_existing(client: TestClient) -> None: dsp_client = create_model( DSPClient, dsp_id=DSPId.spotify, name=DSPClientName.spotify_songwhip ) connection = create_model( FanConnection, fan_id=make_fan_id("fan@example.com"), dsp_id=DSPId.spotify, dsp_client_id=dsp_client.id, status=FanConnectionStatus.revoked, token_encrypted="old-token", ) response = client.post( "/fandata/connections", json={ "email": "fan@example.com", "dsp_client_name": DSPClientName.spotify_songwhip, "token": "new-token", }, ) assert response.status_code == 200 assert response.json() == { "fan_id": connection.fan_id, "dsp_id": connection.dsp_id, "dsp_client_id": connection.dsp_client_id, "status": connection.status, "token_refreshed_at": IsDatetime( approx=connection.token_refreshed_at, iso_string=True ), "created_at": IsDatetime(approx=connection.created_at, iso_string=True), } @pytest.mark.db def test_batch_create_connections_upserts_all(client: TestClient) -> None: dsp_client = create_model( DSPClient, dsp_id=DSPId.spotify, name=DSPClientName.spotify_songwhip ) response = client.post( "/fandata/connections/batch", json={ "items": [ { "email": "fan-1@example.com", "dsp_client_name": DSPClientName.spotify_songwhip, "token": "token-1", }, { "email": "fan-2@example.com", "dsp_client_name": DSPClientName.spotify_songwhip, "token": "token-2", }, ] }, ) assert response.status_code == 200 assert response.json() == {"count": 2} assert ( FanConnection.query.where(FanConnection.dsp_client_id == dsp_client.id).count() == 2 ) @pytest.mark.db def test_batch_create_connections_upserts_existing(client: TestClient) -> None: dsp_client = create_model( DSPClient, dsp_id=DSPId.spotify, name=DSPClientName.spotify_songwhip ) create_model( FanConnection, fan_id=make_fan_id("fan@example.com"), dsp_id=DSPId.spotify, dsp_client_id=dsp_client.id, status=FanConnectionStatus.revoked, token_encrypted="old-token", ) response = client.post( "/fandata/connections/batch", json={ "items": [ { "email": "fan@example.com", "dsp_client_name": DSPClientName.spotify_songwhip, "token": "new-token", } ] }, ) assert response.status_code == 200 assert response.json() == {"count": 1} conn = FanConnection.query.where( FanConnection.fan_id == make_fan_id("fan@example.com"), FanConnection.dsp_client_id == dsp_client.id, ).one() assert conn.status == FanConnectionStatus.active @pytest.mark.db def test_batch_create_connections_returns_400_for_unknown_client( client: TestClient, ) -> None: response = client.post( "/fandata/connections/batch", json={ "items": [ { "email": "fan@example.com", "dsp_client_name": DSPClientName.spotify_songwhip, "token": "token-1", } ] }, ) assert response.status_code == 200 assert response.json() == {"count": 0} @pytest.mark.db def test_batch_create_connections_returns_zero_for_empty(client: TestClient) -> None: response = client.post("/fandata/connections/batch", json={"items": []}) assert response.status_code == 200 assert response.json() == {"count": 0} @pytest.mark.db def test_create_connection_returns_400_for_unknown_client(client: TestClient) -> None: response = client.post( "/fandata/connections", json={ "email": "fan@example.com", "dsp_client_name": DSPClientName.spotify_songwhip, "token": "plaintext-token", }, ) assert response.status_code == 400 assert response.json() == { "code": "unknown_dsp_client", "message": f"DSP client {DSPClientName.spotify_songwhip} not found", }