"""Blockbuster checks for blocking sqlite/sync work on the asyncio loop. FastAPI runs ``def`` route handlers in a thread pool, so typical sync routes do not block the event loop. ``async def`` handlers must move sync SQLModel / sqlite work off the loop with ``asyncio.to_thread``. Seed DB: ``deploy/hf/seed/hf_bundled_store.db`` (small HF demo catalog). """ from __future__ import annotations import asyncio import shutil import sys from pathlib import Path import httpx import pytest from blockbuster import blockbuster_ctx from fastapi.testclient import TestClient _SEED = Path(__file__).resolve().parent.parent / "deploy/hf/seed/hf_bundled_store.db" @pytest.fixture() def blockbuster_app(tmp_path, monkeypatch): if not _SEED.is_file(): pytest.skip(f"Missing seed DB: {_SEED}") dest = tmp_path / "blockbuster_store.db" shutil.copyfile(_SEED, dest) monkeypatch.setenv("KINK_STORE_PATH", str(dest)) monkeypatch.setenv("KINK_SKIP_HEAVY_WARM", "1") monkeypatch.setenv("KINK_HF_REQUIRE_FULL_CATALOG", "0") sys.modules.pop("api", None) import api as api_mod api_mod._backend_impl = None api_mod._get_backend() return api_mod def test_async_post_users_does_not_block_loop_under_blockbuster(blockbuster_app): """``POST /users`` parses async body, then creates the user off the event loop.""" async def inner(): transport = httpx.ASGITransport(app=blockbuster_app.app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: with blockbuster_ctx(scanned_modules=["api"]): r = await client.post("/users", json={}) assert r.status_code == 200 assert r.json()["id"] asyncio.run(inner()) def test_sync_get_health_ok_under_blockbuster(blockbuster_app): """Sync ``GET /health`` runs in Starlette's thread pool — no loop blocking.""" async def inner(): transport = httpx.ASGITransport(app=blockbuster_app.app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: with blockbuster_ctx(scanned_modules=["api"]): r = await client.get("/health") assert r.status_code == 200 assert r.json().get("ok") is True asyncio.run(inner()) def test_health_catalog_sample_returns_kink_id(blockbuster_app) -> None: """``GET /health/catalog-sample`` reads one row (used by deploy smoke, not full ``list_kinks``).""" client = TestClient(blockbuster_app.app) r = client.get("/health/catalog-sample") assert r.status_code == 200 body = r.json() assert body.get("ok") is True assert body.get("sample_kink_id") kid = body["sample_kink_id"] d = client.get(f"/kinks/{kid}") assert d.status_code == 200 assert d.json().get("id") == kid def test_kinks_batch_matches_single_get(blockbuster_app) -> None: """Discover prefetches full cards via ``POST /kinks/batch`` — same payload as ``GET /kinks/:id``.""" client = TestClient(blockbuster_app.app) kid = client.get("/health/catalog-sample").json()["sample_kink_id"] one = client.get(f"/kinks/{kid}").json() bat = client.post("/kinks/batch", json={"ids": [kid, "__missing__"]}).json()["items"] assert len(bat) == 1 assert bat[0] == one