diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py index ad1251f..109a351 100644 --- a/backend/app/api/__init__.py +++ b/backend/app/api/__init__.py @@ -20,6 +20,7 @@ def all_blueprints() -> list[Blueprint]: from .gallery import gallery_bp from .import_admin import import_admin_bp from .ml_admin import ml_admin_bp + from .provenance import provenance_bp from .settings import settings_bp from .showcase import showcase_bp from .suggestions import suggestions_bp @@ -27,6 +28,7 @@ def all_blueprints() -> list[Blueprint]: return [ api_bp, gallery_bp, + provenance_bp, tags_bp, artist_bp, showcase_bp, diff --git a/backend/app/api/provenance.py b/backend/app/api/provenance.py new file mode 100644 index 0000000..cb7799f --- /dev/null +++ b/backend/app/api/provenance.py @@ -0,0 +1,33 @@ +"""Provenance API: image -> posts, post -> header payload. + +Separate from the gallery/tag APIs by design (provenance is its own +system). Read-only. +""" + +from quart import Blueprint, jsonify + +from ..extensions import get_session +from ..services.provenance_service import ProvenanceService + +provenance_bp = Blueprint("provenance", __name__, + url_prefix="/api/provenance") + + +@provenance_bp.route("/image/", methods=["GET"]) +async def image_provenance(image_id: int): + async with get_session() as session: + svc = ProvenanceService(session) + payload = await svc.for_image(image_id) + if payload is None: + return jsonify({"error": "not found"}), 404 + return jsonify(payload) + + +@provenance_bp.route("/post/", methods=["GET"]) +async def post_provenance(post_id: int): + async with get_session() as session: + svc = ProvenanceService(session) + payload = await svc.for_post(post_id) + if payload is None: + return jsonify({"error": "not found"}), 404 + return jsonify(payload) diff --git a/tests/test_api_provenance.py b/tests/test_api_provenance.py new file mode 100644 index 0000000..328739e --- /dev/null +++ b/tests/test_api_provenance.py @@ -0,0 +1,84 @@ +from datetime import UTC, datetime + +import pytest + +from backend.app import create_app +from backend.app.models import ( + Artist, + ImageProvenance, + ImageRecord, + 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 + + +async def _seed_full(db): + rec = ImageRecord( + path="/images/p/1.jpg", sha256="p" + "0" * 63, + size_bytes=1, mime="image/jpeg", width=1, height=1, + origin="imported_filesystem", integrity_status="unknown", + ) + artist = Artist(name="Alice", slug="alice") + db.add_all([rec, artist]) + await db.flush() + source = Source(artist_id=artist.id, platform="patreon", + url="https://patreon.test/alice") + db.add(source) + await db.flush() + post = Post(source_id=source.id, external_post_id="555", + post_url="https://patreon.test/p/555", post_title="Set 1", + post_date=datetime(2023, 8, 1, tzinfo=UTC), + description="

hi

", attachment_count=2) + db.add(post) + await db.flush() + db.add(ImageProvenance(image_record_id=rec.id, post_id=post.id, + source_id=source.id)) + await db.flush() + await db.commit() + return rec.id, post.id + + +@pytest.mark.asyncio +async def test_image_provenance_endpoint(client, db): + image_id, post_id = await _seed_full(db) + resp = await client.get(f"/api/provenance/image/{image_id}") + assert resp.status_code == 200 + body = await resp.get_json() + assert body["image_id"] == image_id + assert len(body["provenance"]) == 1 + assert body["provenance"][0]["post"]["id"] == post_id + + +@pytest.mark.asyncio +async def test_image_provenance_404(client): + resp = await client.get("/api/provenance/image/999999") + assert resp.status_code == 404 + + +@pytest.mark.asyncio +async def test_post_provenance_endpoint(client, db): + _, post_id = await _seed_full(db) + resp = await client.get(f"/api/provenance/post/{post_id}") + assert resp.status_code == 200 + body = await resp.get_json() + assert body["post"]["id"] == post_id + assert body["artist"]["slug"] == "alice" + + +@pytest.mark.asyncio +async def test_post_provenance_404(client): + resp = await client.get("/api/provenance/post/999999") + assert resp.status_code == 404