9d5793f0ce
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
133 lines
4.2 KiB
Python
133 lines
4.2 KiB
Python
"""FC-3d: tick_due_sources task tests.
|
|
|
|
Verifies (a) due sources are queued, (b) sources with a pending/running
|
|
DownloadEvent are skipped, (c) the tick is read-only on Source itself
|
|
(no last_checked_at write), and (d) the result dict shape.
|
|
"""
|
|
import pytest
|
|
from sqlalchemy import select
|
|
|
|
import backend.app.tasks.scan # noqa: F401 — side-effect: register celery task
|
|
from backend.app.celery_app import celery
|
|
from backend.app.models import Artist, DownloadEvent, Source
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
class _FakeTask:
|
|
"""Stand-in for the real Celery Task object — only `.delay()` matters."""
|
|
|
|
def __init__(self, sink: list):
|
|
self._sink = sink
|
|
|
|
def delay(self, source_id: int):
|
|
self._sink.append(source_id)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tick_returns_counts_dict_when_no_due_sources(db, monkeypatch):
|
|
"""Conftest TRUNCATEs after every integration test, so we start clean."""
|
|
delayed: list[int] = []
|
|
monkeypatch.setattr(
|
|
"backend.app.tasks.download.download_source", _FakeTask(delayed),
|
|
)
|
|
from backend.app.tasks.scan import _tick_due_sources_async
|
|
|
|
result = await _tick_due_sources_async()
|
|
assert set(result.keys()) == {"due", "queued", "skipped_in_flight", "tick_at"}
|
|
assert result["due"] == 0
|
|
assert result["queued"] == 0
|
|
assert result["skipped_in_flight"] == 0
|
|
assert delayed == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tick_queues_due_source(db, monkeypatch):
|
|
artist = Artist(name="tick-q", slug="tick-q", auto_check=True)
|
|
db.add(artist)
|
|
await db.flush()
|
|
src = Source(
|
|
artist_id=artist.id, platform="patreon", url="https://tick-q",
|
|
enabled=True, consecutive_failures=0,
|
|
)
|
|
db.add(src)
|
|
await db.commit()
|
|
|
|
delayed: list[int] = []
|
|
monkeypatch.setattr(
|
|
"backend.app.tasks.download.download_source", _FakeTask(delayed),
|
|
)
|
|
from backend.app.tasks.scan import _tick_due_sources_async
|
|
result = await _tick_due_sources_async()
|
|
assert result["queued"] >= 1
|
|
assert src.id in delayed
|
|
|
|
pending_count = (await db.execute(
|
|
select(DownloadEvent.id).where(
|
|
DownloadEvent.source_id == src.id,
|
|
DownloadEvent.status == "pending",
|
|
)
|
|
)).scalars().all()
|
|
assert len(pending_count) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tick_skips_in_flight_source(db, monkeypatch):
|
|
artist = Artist(name="tick-inf", slug="tick-inf", auto_check=True)
|
|
db.add(artist)
|
|
await db.flush()
|
|
src = Source(
|
|
artist_id=artist.id, platform="patreon", url="https://tick-inf",
|
|
enabled=True, consecutive_failures=0,
|
|
)
|
|
db.add(src)
|
|
await db.flush()
|
|
db.add(DownloadEvent(source_id=src.id, status="running"))
|
|
await db.commit()
|
|
|
|
delayed: list[int] = []
|
|
monkeypatch.setattr(
|
|
"backend.app.tasks.download.download_source", _FakeTask(delayed),
|
|
)
|
|
from backend.app.tasks.scan import _tick_due_sources_async
|
|
result = await _tick_due_sources_async()
|
|
assert src.id not in delayed
|
|
assert result["skipped_in_flight"] >= 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tick_does_not_modify_source_last_checked_at(db, monkeypatch):
|
|
"""The tick creates DownloadEvent(pending) but never writes Source columns.
|
|
Source.last_checked_at is owned by DownloadService.finalize.
|
|
"""
|
|
artist = Artist(name="tick-ro", slug="tick-ro", auto_check=True)
|
|
db.add(artist)
|
|
await db.flush()
|
|
src = Source(
|
|
artist_id=artist.id, platform="patreon", url="https://tick-ro",
|
|
enabled=True, consecutive_failures=0,
|
|
)
|
|
db.add(src)
|
|
await db.commit()
|
|
src_id = src.id
|
|
|
|
before = (await db.execute(
|
|
select(Source.last_checked_at).where(Source.id == src_id)
|
|
)).scalar_one()
|
|
|
|
monkeypatch.setattr(
|
|
"backend.app.tasks.download.download_source", _FakeTask([]),
|
|
)
|
|
from backend.app.tasks.scan import _tick_due_sources_async
|
|
await _tick_due_sources_async()
|
|
|
|
after = (await db.execute(
|
|
select(Source.last_checked_at).where(Source.id == src_id)
|
|
)).scalar_one()
|
|
assert before == after # still None / unchanged
|
|
|
|
|
|
def test_tick_task_is_registered():
|
|
"""Side-effect import at the top of this test file forces task module load."""
|
|
assert "backend.app.tasks.scan.tick_due_sources" in celery.tasks
|