refactor(dry-S1): hoist app/client test fixtures into conftest

Removed the app/client fixtures duplicated across 36 test files (two
variants: separate app + client(app), and a self-contained client() that
called create_app inline) and the now-unused create_app imports. Both
fixtures now live once in conftest.py. test_suggestions_bulk keeps its
import (builds the app inline in two tests); test_health drops its local
client + unused pytest_asyncio.

Net -415 lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 11:33:05 -04:00
parent eebc8e2413
commit def967a1a8
39 changed files with 15 additions and 430 deletions
+12
View File
@@ -18,6 +18,7 @@ from sqlalchemy.ext.asyncio import (
)
from sqlalchemy.orm import sessionmaker
from backend.app import create_app
from backend.app.models import Base
@@ -77,6 +78,17 @@ def db_sync(sync_engine):
session.rollback()
@pytest_asyncio.fixture
async def app():
return create_app()
@pytest_asyncio.fixture
async def client(app):
async with app.test_client() as c:
yield c
# Migration-seeded baseline (singleton config like import_settings /
# ml_settings), captured lazily at the FIRST integration test's setup —
# when the CI integration job has just run `alembic upgrade head` so the DB
-12
View File
@@ -8,24 +8,12 @@ import hashlib
import pytest
from backend.app import create_app
from backend.app.models import Artist, ImageRecord, Tag, TagKind
from backend.app.models.tag import image_tag
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
# --- Tier-C: POST /artists/<slug>/cascade-delete --------------------
-12
View File
@@ -1,23 +1,11 @@
import pytest
from backend.app import create_app
from backend.app.models import TagKind
from backend.app.services.tag_service import TagService
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.mark.asyncio
async def test_create_list_delete(client, db):
tag = await TagService(db).find_or_create("Canon", TagKind.character)
-12
View File
@@ -1,23 +1,11 @@
import pytest
from backend.app import create_app
from backend.app.models import TagAllowlist, TagKind
from backend.app.services.tag_service import TagService
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.mark.asyncio
async def test_list_and_patch_and_delete(client, db):
tag = await TagService(db).find_or_create("AL", TagKind.character)
-8
View File
@@ -1,18 +1,10 @@
import pytest
from backend.app import create_app
from backend.app.models import Artist
pytestmark = pytest.mark.integration
@pytest.fixture
async def client():
app = create_app()
async with app.test_client() as c:
yield c
@pytest.mark.asyncio
async def test_artist_404(client):
resp = await client.get("/api/artist/nope")
-12
View File
@@ -1,21 +1,9 @@
import pytest
from backend.app import create_app
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.mark.asyncio
async def test_create_artist_returns_201(client):
resp = await client.post("/api/artists", json={"name": "Alice"})
-12
View File
@@ -2,23 +2,11 @@
import pytest
from backend.app import create_app
from backend.app.models import Artist, ImageRecord, Source
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.fixture
async def seeded(db):
alice = Artist(name="alice-api", slug="alice-api", is_subscription=True)
-12
View File
@@ -1,22 +1,10 @@
import pytest
from backend.app import create_app
from backend.app.models import Artist, PostAttachment
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.mark.asyncio
async def test_download_streams_with_disposition(client, db, tmp_path):
blob = tmp_path / "pack.zip"
-12
View File
@@ -1,21 +1,9 @@
import pytest
from backend.app import create_app
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
async def _mk_tag(client, name, kind="general"):
r = await client.post("/api/tags", json={"name": name, "kind": kind})
return (await r.get_json())["id"]
-12
View File
@@ -11,7 +11,6 @@ import pytest
from PIL import Image
from sqlalchemy import func, select
from backend.app import create_app
from backend.app.celery_app import celery
from backend.app.models import ImageRecord, LibraryAuditRun
@@ -23,17 +22,6 @@ def disable_celery_eager(monkeypatch):
monkeypatch.setattr(celery.conf, "task_always_eager", False)
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
def _sha256_min_dim_token(min_w: int, min_h: int) -> str:
canon = f"{min_w}x{min_h}"
return f"delete-min-dim-{hashlib.sha256(canon.encode()).hexdigest()[:8]}"
-12
View File
@@ -1,6 +1,5 @@
import pytest
from backend.app import create_app
from backend.app.models import AppSetting
pytestmark = pytest.mark.integration
@@ -12,17 +11,6 @@ _NETSCAPE = (
)
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.fixture
async def ext_key(db):
# Seed an extension API key for tests that want to assert the
-12
View File
@@ -1,22 +1,10 @@
import pytest
from backend.app import create_app
from backend.app.models import Artist, DownloadEvent, Source
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.fixture
async def seed(db):
artist = Artist(name="Alice", slug="alice")
-12
View File
@@ -6,7 +6,6 @@ import pytest
import pytest_asyncio
from sqlalchemy import func, select
from backend.app import create_app
from backend.app import frontend as frontend_module
from backend.app.api import extension as extension_module
from backend.app.models import AppSetting, Artist, Source
@@ -14,17 +13,6 @@ from backend.app.models import AppSetting, Artist, Source
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest_asyncio.fixture
async def ext_key(db):
db.add(AppSetting(key="extension_api_key", value="test-ext-key"))
-12
View File
@@ -1,23 +1,11 @@
import pytest
from sqlalchemy import select
from backend.app import create_app
from backend.app.models import AppSetting
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.mark.asyncio
async def test_get_seeds_a_value(client, db):
# First call seeds the row (idempotent on repeat calls).
-12
View File
@@ -2,23 +2,11 @@ from datetime import UTC, datetime, timedelta
import pytest
from backend.app import create_app
from backend.app.models import ImageRecord
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
async def _seed(db, count: int = 3):
base = datetime.now(UTC)
for i in range(count):
-12
View File
@@ -2,7 +2,6 @@ from datetime import UTC, datetime, timedelta
import pytest
from backend.app import create_app
from backend.app.celery_app import celery
from backend.app.models import ImportBatch, ImportTask
@@ -16,17 +15,6 @@ def eager():
celery.conf.task_always_eager = False
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.mark.asyncio
async def test_status_when_idle(client):
resp = await client.get("/api/import/status")
-12
View File
@@ -6,7 +6,6 @@ import pytest
from werkzeug.datastructures import FileStorage
import backend.app.tasks.migration # noqa: F401 — register celery task
from backend.app import create_app
from backend.app.celery_app import celery
pytestmark = pytest.mark.integration
@@ -20,17 +19,6 @@ def _file_storage(payload: dict, filename: str = "export.json") -> FileStorage:
)
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
# Retired 2026-05-24 (FC-3h): `test_post_backup_returns_202_and_id`
# asserted the /api/migrate/backup endpoint, which was retired in FC-3h.
# Backup is now a first-class feature at /api/system/backup/*;
-12
View File
@@ -1,6 +1,5 @@
import pytest
from backend.app import create_app
from backend.app.celery_app import celery
from backend.app.models import TagKind
from backend.app.services.tag_service import TagService
@@ -15,17 +14,6 @@ def eager():
celery.conf.task_always_eager = False
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.mark.asyncio
async def test_get_and_patch_settings(client):
resp = await client.get("/api/ml/settings")
-12
View File
@@ -2,22 +2,10 @@ import re
import pytest
from backend.app import create_app
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.mark.asyncio
async def test_platforms_returns_gs_six(client):
resp = await client.get("/api/platforms")
-12
View File
@@ -8,23 +8,11 @@ from datetime import UTC, datetime
import pytest
from backend.app import create_app
from backend.app.models import Artist, Post, Source
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.fixture
async def seeded_post(db):
artist = Artist(name="alice-api", slug="alice-api")
-12
View File
@@ -2,7 +2,6 @@ from datetime import UTC, datetime
import pytest
from backend.app import create_app
from backend.app.models import (
Artist,
ImageProvenance,
@@ -14,17 +13,6 @@ from backend.app.models import (
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
async def _seed_full(db):
rec = ImageRecord(
path="/images/p/1.jpg", sha256="p" + "0" * 63,
-12
View File
@@ -1,21 +1,9 @@
import pytest
from backend.app import create_app
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
async def _series(client, name="Vol"):
r = await client.post("/api/tags", json={"name": name, "kind": "series"})
return (await r.get_json())["id"]
-12
View File
@@ -1,21 +1,9 @@
import pytest
from backend.app import create_app
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.mark.asyncio
async def test_get_import_settings_defaults(client):
resp = await client.get("/api/settings/import")
-12
View File
@@ -1,21 +1,9 @@
import pytest
from backend.app import create_app
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.mark.asyncio
async def test_get_includes_downloader_fields(client):
resp = await client.get("/api/settings/import")
-8
View File
@@ -1,18 +1,10 @@
import pytest
from backend.app import create_app
from backend.app.models import ImageRecord
pytestmark = pytest.mark.integration
@pytest.fixture
async def client():
app = create_app()
async with app.test_client() as c:
yield c
@pytest.mark.asyncio
async def test_showcase_returns_images(client, db):
for i in range(8):
-12
View File
@@ -1,22 +1,10 @@
import pytest
from backend.app import create_app
from backend.app.models import Artist, Source
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.fixture
async def artist(db):
a = Artist(name="Alice", slug="alice")
-12
View File
@@ -1,22 +1,10 @@
import pytest
from backend.app import create_app
from backend.app.models import Artist, DownloadEvent, Source
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.fixture
async def seed(db):
artist = Artist(name="Alice", slug="alice")
-12
View File
@@ -1,6 +1,5 @@
import pytest
from backend.app import create_app
from backend.app.celery_app import celery
from backend.app.models import ImageRecord, TagKind
from backend.app.services.tag_service import TagService
@@ -15,17 +14,6 @@ def eager():
celery.conf.task_always_eager = False
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
async def _img(db, preds):
img = ImageRecord(
path="/images/s.jpg", sha256="s" * 64, size_bytes=1,
-12
View File
@@ -10,24 +10,12 @@ from datetime import UTC, datetime, timedelta
import pytest
import pytest_asyncio
from backend.app import create_app
from backend.app.api import system_activity as activity_module
from backend.app.models import TaskRun
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.fixture(autouse=True)
def _reset_caches(monkeypatch):
"""Clear the module-level caches between tests so cache state from
-12
View File
@@ -5,23 +5,11 @@ import pytest
import pytest_asyncio
from sqlalchemy import select
from backend.app import create_app
from backend.app.models import BackupRun
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest_asyncio.fixture
async def _seed_runs(db):
"""Insert 4 BackupRun rows for paging/filter tests."""
-12
View File
@@ -1,21 +1,9 @@
import pytest
from backend.app import create_app
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
async def _mk(client, name, kind):
r = await client.post("/api/tags", json={"name": name, "kind": kind})
return (await r.get_json())["id"]
-12
View File
@@ -1,21 +1,9 @@
import pytest
from backend.app import create_app
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.mark.asyncio
async def test_autocomplete_empty(client):
resp = await client.get("/api/tags/autocomplete?q=")
-8
View File
@@ -1,18 +1,10 @@
import pytest
from backend.app import create_app
from backend.app.models import Tag, TagKind
pytestmark = pytest.mark.integration
@pytest.fixture
async def client():
app = create_app()
async with app.test_client() as c:
yield c
@pytest.mark.asyncio
async def test_directory_endpoint(client, db):
db.add(Tag(name="api_tag", kind=TagKind.general))
-12
View File
@@ -1,6 +1,5 @@
import pytest
from backend.app import create_app
from backend.app.celery_app import celery
pytestmark = pytest.mark.integration
@@ -13,17 +12,6 @@ def eager():
celery.conf.task_always_eager = False
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
@pytest.mark.asyncio
async def test_trigger_thumbnail_backfill(client):
r = await client.post("/api/thumbnails/backfill")
-8
View File
@@ -13,18 +13,10 @@ requests (no Origin header, or a regular https:// Origin) get nothing
import pytest
from backend.app import create_app
pytestmark = pytest.mark.integration
@pytest.fixture
async def client():
app = create_app()
async with app.test_client() as c:
yield c
@pytest.mark.asyncio
async def test_extension_preflight_returns_204_with_acl_headers(client):
resp = await client.options(
-12
View File
@@ -2,23 +2,11 @@ from datetime import UTC, datetime, timedelta
import pytest
from backend.app import create_app
from backend.app.models import Artist, ImageRecord, Source
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
async def _img(db, n):
rec = ImageRecord(
path=f"/images/ap/{n}.jpg", sha256=f"ap{n:062d}",
-12
View File
@@ -2,7 +2,6 @@ from datetime import UTC, datetime, timedelta
import pytest
from backend.app import create_app
from backend.app.models import (
Artist,
ImageProvenance,
@@ -15,17 +14,6 @@ from backend.app.services.gallery_service import GalleryService
pytestmark = pytest.mark.integration
@pytest.fixture
async def app():
return create_app()
@pytest.fixture
async def client(app):
async with app.test_client() as c:
yield c
async def _img(db, n):
base = datetime.now(UTC)
rec = ImageRecord(
-10
View File
@@ -1,16 +1,6 @@
"""Health endpoint smoke test."""
import pytest
import pytest_asyncio
from backend.app import create_app
@pytest_asyncio.fixture
async def client():
app = create_app()
async with app.test_client() as c:
yield c
@pytest.mark.asyncio
+3 -4
View File
@@ -1,5 +1,6 @@
import pytest
from backend.app import create_app
from backend.app.models import ImageRecord, TagKind
from backend.app.models.tag import image_tag
from backend.app.services.ml.suggestions import SuggestionService
@@ -86,8 +87,7 @@ async def test_consensus_threshold_clamped_and_empty_for_no_ids(db):
@pytest.mark.asyncio
async def test_bulk_suggestions_route(db):
from backend.app import create_app
tags = TagService(db)
await tags.find_or_create("sword", TagKind.general)
a = _img("i" * 64, {"sword": {"category": "general", "confidence": 0.97}})
@@ -107,8 +107,7 @@ async def test_bulk_suggestions_route(db):
@pytest.mark.asyncio
async def test_bulk_suggestions_requires_ids(db):
from backend.app import create_app
app = create_app()
async with app.test_client() as c:
resp = await c.post("/api/suggestions/bulk", json={})