import pytest from backend.app.models import Artist, DownloadEvent, Source pytestmark = pytest.mark.integration @pytest.fixture async def seed(db): artist = Artist(name="Alice", slug="alice") db.add(artist) await db.flush() source = Source( artist_id=artist.id, platform="patreon", url="https://patreon.com/alice", enabled=True, config_overrides={}, ) db.add(source) await db.commit() return source @pytest.mark.asyncio async def test_check_202_creates_pending_event(client, seed, monkeypatch): monkeypatch.setattr( "backend.app.tasks.download.download_source.delay", lambda *a, **k: None, ) resp = await client.post(f"/api/sources/{seed.id}/check") assert resp.status_code == 202 body = await resp.get_json() assert body["status"] == "pending" assert isinstance(body["download_event_id"], int) @pytest.mark.asyncio async def test_check_404_for_unknown_source(client): resp = await client.post("/api/sources/99999/check") assert resp.status_code == 404 @pytest.mark.asyncio async def test_check_400_for_disabled_source(client, db, seed): seed.enabled = False await db.commit() resp = await client.post(f"/api/sources/{seed.id}/check") assert resp.status_code == 400 body = await resp.get_json() assert body["error"] == "source_disabled" @pytest.mark.asyncio async def test_check_409_when_already_running(client, db, seed, monkeypatch): monkeypatch.setattr( "backend.app.tasks.download.download_source.delay", lambda *a, **k: None, ) existing = DownloadEvent(source_id=seed.id, status="running") db.add(existing) await db.commit() await db.refresh(existing) resp = await client.post(f"/api/sources/{seed.id}/check") assert resp.status_code == 409 body = await resp.get_json() assert body["download_event_id"] == existing.id @pytest.mark.asyncio async def test_check_defers_when_platform_in_cooldown(client, db, seed, monkeypatch): """Bulk retries land here without ?force — when the source's platform has an active cooldown, return 202 with status='deferred' instead of creating an event. Lets the bulk path tally deferred-vs-queued in the toast so the operator can see rate-limit-induced failures at a glance.""" from backend.app.services.scheduler_service import set_platform_cooldown delays: list = [] monkeypatch.setattr( "backend.app.tasks.download.download_source.delay", lambda *a, **k: delays.append(a), ) await set_platform_cooldown(db, seed.platform, seconds=900) await db.commit() resp = await client.post(f"/api/sources/{seed.id}/check") assert resp.status_code == 202 body = await resp.get_json() assert body["status"] == "deferred" assert body["platform"] == seed.platform assert body["cooldown_until"] assert delays == [] # no dispatch when deferred @pytest.mark.asyncio async def test_check_force_overrides_cooldown(client, db, seed, monkeypatch): """Single-source RETRY click sends ?force=true — explicit operator override, used for rapid auth-fix testing without waiting on the cooldown.""" from backend.app.services.scheduler_service import set_platform_cooldown delays: list = [] monkeypatch.setattr( "backend.app.tasks.download.download_source.delay", lambda *a, **k: delays.append(a), ) await set_platform_cooldown(db, seed.platform, seconds=900) await db.commit() resp = await client.post(f"/api/sources/{seed.id}/check?force=true") assert resp.status_code == 202 body = await resp.get_json() assert body["status"] == "pending" assert isinstance(body["download_event_id"], int) assert len(delays) == 1 assert delays[0] == (seed.id,)