"""API / HTML integration tests (offline).""" from __future__ import annotations from datetime import UTC, datetime import pytest from app.config import get_settings from app.db import reset_engine from app.db.models import Base, Tracker from app.main import create_app from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine @pytest.fixture async def app_client(tmp_path, monkeypatch): db = tmp_path / "api.db" url = f"sqlite+aiosqlite:///{db.as_posix()}" monkeypatch.setenv("DATABASE_URL", url) monkeypatch.setenv("RUN_SCHEDULER", "false") monkeypatch.setenv("APP_NAME", "RastroTest") monkeypatch.setenv("ENABLE_IPV6", "false") get_settings.cache_clear() reset_engine() engine = create_async_engine(url) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) factory = async_sessionmaker(engine, expire_on_commit=False) async with factory() as session: session.add( Tracker( canonical_url="udp://demo.example:6969/announce", scheme="udp", hostname="demo.example", port=6969, path="/announce", current_status="up", first_seen_at=datetime.now(UTC), last_seen_at=datetime.now(UTC), last_checked_at=datetime.now(UTC), last_latency_ms=42, consecutive_failures=0, infrastructure_fingerprint="abc", supports_ipv4=True, supports_ipv6="not_tested", ) ) await session.commit() await engine.dispose() # Patch lifespan import to skip network bootstrap application = create_app() transport = ASGITransport(app=application) async with AsyncClient(transport=transport, base_url="http://test") as client: yield client get_settings.cache_clear() reset_engine() @pytest.mark.asyncio async def test_health_meta_and_pages(app_client: AsyncClient): r = await app_client.get("/healthz") assert r.status_code == 200 assert r.text == "ok" meta = await app_client.get("/api/v1/meta") assert meta.status_code == 200 body = meta.json() assert body["name"] == "RastroTest" assert "version" in body home = await app_client.get("/") assert home.status_code == 200 assert "demo.example" in home.text assert "RastroTest" in home.text trackers = await app_client.get("/api/v1/trackers") assert trackers.status_code == 200 assert trackers.json()["total"] >= 1 detail = await app_client.get("/api/v1/trackers/1") assert detail.status_code == 200 lists = await app_client.get("/lists") assert lists.status_code == 200 @pytest.mark.asyncio async def test_pages_follow_language(app_client: AsyncClient): home_en = await app_client.get("/", headers={"Accept-Language": "en-US,en;q=0.9"}) assert home_en.status_code == 200 assert 'lang="en"' in home_en.text assert "watched" in home_en.text home_pt = await app_client.get( "/", headers={"Accept-Language": "en"}, cookies={"lang": "pt"}, ) assert 'lang="pt-BR"' in home_pt.text assert "monitorados" in home_pt.text missing_en = await app_client.get("/trackers/99999", headers={"Accept-Language": "en"}) assert missing_en.status_code == 404 assert "not found" in missing_en.text.lower() missing_pt = await app_client.get("/trackers/99999", cookies={"lang": "pt"}) assert "não encontrado" in missing_pt.text @pytest.mark.asyncio async def test_lang_switch_sets_cookie(app_client: AsyncClient): r = await app_client.get("/lang/en", follow_redirects=False) assert r.status_code == 303 assert "lang=en" in r.headers.get("set-cookie", "") @pytest.mark.asyncio async def test_smart_list_etag(app_client: AsyncClient): first = await app_client.get("/api/v1/lists/smart.txt") assert first.status_code == 200 etag = first.headers.get("etag") assert etag second = await app_client.get("/api/v1/lists/smart.txt", headers={"If-None-Match": etag}) assert second.status_code == 304 live = await app_client.get("/api/v1/lists/live.txt") assert live.status_code == 200 assert "demo.example" in live.text