"""Tests for marketing_intelligence.api.routers.campaigns.""" from __future__ import annotations from typing import Any from unittest.mock import AsyncMock, patch import pytest from fastapi.testclient import TestClient _ROUTER_MOD = "marketing_intelligence.api.routers.campaigns" _LIFESPAN_MOD = "marketing_intelligence.storage.manifest" _VALID_BODY = { "prompt": "How is this track used?", "artist_name": "Test Artist", "tracks": ["track one", "track two"], "sample_size": 10, } # ──────────────────────────── fixtures ──────────────────────────────────────── @pytest.fixture(autouse=True) def _patch_lifespan() -> Any: """Prevent the lifespan from calling S3/local storage on startup.""" with patch( f"{_LIFESPAN_MOD}.find_running_runs", new_callable=AsyncMock, return_value=[], ): yield @pytest.fixture(autouse=True) def _patch_discovery() -> Any: with ( patch( f"{_ROUTER_MOD}.run_discovery", new_callable=AsyncMock, ) as mock_disc, patch( f"{_ROUTER_MOD}.run_discovery_relevant", new_callable=AsyncMock, ) as mock_rel, ): yield mock_disc, mock_rel # ──────────────────────────── POST /campaigns ───────────────────────────────── class TestCreateCampaign: def test_returns_201_started(self, test_client: TestClient) -> None: with patch(f"{_ROUTER_MOD}.count_runs", new_callable=AsyncMock, return_value=0): resp = test_client.post("/campaigns", json=_VALID_BODY) assert resp.status_code == 200 body = resp.json() assert body["status"] == "started" assert body["artist"] == "Test Artist" assert body["tracks"] == 2 def test_run_id_increments(self, test_client: TestClient) -> None: with patch(f"{_ROUTER_MOD}.count_runs", new_callable=AsyncMock, return_value=3): resp = test_client.post("/campaigns", json=_VALID_BODY) assert resp.json()["run_id"].endswith("_4") def test_run_id_starts_at_1(self, test_client: TestClient) -> None: with patch(f"{_ROUTER_MOD}.count_runs", new_callable=AsyncMock, return_value=0): resp = test_client.post("/campaigns", json=_VALID_BODY) assert resp.json()["run_id"].endswith("_1") def test_missing_required_field_returns_422(self, test_client: TestClient) -> None: resp = test_client.post("/campaigns", json={"artist_name": "X", "tracks": []}) assert resp.status_code == 422 def test_empty_tracks_list(self, test_client: TestClient) -> None: body = {**_VALID_BODY, "tracks": []} with patch(f"{_ROUTER_MOD}.count_runs", new_callable=AsyncMock, return_value=0): resp = test_client.post("/campaigns", json=body) assert resp.status_code == 200 assert resp.json()["tracks"] == 0 # ──────────────────────────── POST /campaigns/relevant ──────────────────────── class TestCreateRelevantCampaign: def test_returns_started(self, test_client: TestClient) -> None: with patch(f"{_ROUTER_MOD}.count_runs", new_callable=AsyncMock, return_value=0): resp = test_client.post("/campaigns/relevant", json=_VALID_BODY) assert resp.status_code == 200 body = resp.json() assert body["status"] == "started" assert body["artist"] == "Test Artist" assert body["tracks"] == 2 def test_run_id_uses_artist_key(self, test_client: TestClient) -> None: with patch(f"{_ROUTER_MOD}.count_runs", new_callable=AsyncMock, return_value=0): resp = test_client.post("/campaigns/relevant", json=_VALID_BODY) assert "test_artist" in resp.json()["run_id"] def test_multi_word_artist_key_normalised(self, test_client: TestClient) -> None: body = {**_VALID_BODY, "artist_name": "My Cool Artist"} with patch(f"{_ROUTER_MOD}.count_runs", new_callable=AsyncMock, return_value=0): resp = test_client.post("/campaigns/relevant", json=body) assert "my_cool_artist" in resp.json()["run_id"] # ──────────────────────────── GET /campaigns/{id}/{run}/status ──────────────── class TestCampaignRunStatus: def test_returns_manifest_when_found(self, test_client: TestClient) -> None: manifest = { "status": "completed", "campaign_id": "artist", "run_id": "artist_1", } with patch( f"{_ROUTER_MOD}.get_manifest", new_callable=AsyncMock, return_value=manifest ): resp = test_client.get("/campaigns/artist/artist_1/status") assert resp.status_code == 200 assert resp.json()["status"] == "completed" def test_returns_404_when_not_found(self, test_client: TestClient) -> None: with patch( f"{_ROUTER_MOD}.get_manifest", new_callable=AsyncMock, return_value=None ): resp = test_client.get("/campaigns/artist/missing_run/status") assert resp.status_code == 404 def test_404_detail_contains_ids(self, test_client: TestClient) -> None: with patch( f"{_ROUTER_MOD}.get_manifest", new_callable=AsyncMock, return_value=None ): resp = test_client.get("/campaigns/artist/bad_run/status") assert "artist" in resp.json()["message"]