from datetime import UTC, datetime, timedelta import pytest from backend.app.models import ImageRecord, Tag, TagKind from backend.app.models.tag import image_tag pytestmark = pytest.mark.integration async def _seed(db, count: int = 3): base = datetime.now(UTC) for i in range(count): r = ImageRecord( path=f"/images/x/{i}.jpg", sha256=f"x{i:063d}", size_bytes=1, mime="image/jpeg", width=1, height=1, origin="imported_filesystem", integrity_status="unknown", ) r.created_at = base - timedelta(minutes=i) r.effective_date = r.created_at # no post → tracks created_at (0035) db.add(r) await db.flush() await db.commit() @pytest.mark.asyncio async def test_scroll_returns_images(client, db): await _seed(db) resp = await client.get("/api/gallery/scroll?limit=10") assert resp.status_code == 200 body = await resp.get_json() assert len(body["images"]) == 3 assert "next_cursor" in body assert "date_groups" in body @pytest.mark.asyncio async def test_scroll_rejects_bad_limit(client): resp = await client.get("/api/gallery/scroll?limit=notanumber") assert resp.status_code == 400 @pytest.mark.asyncio async def test_timeline_endpoint(client, db): await _seed(db) resp = await client.get("/api/gallery/timeline") assert resp.status_code == 200 body = await resp.get_json() assert sum(b["count"] for b in body) == 3 @pytest.mark.asyncio async def test_jump_requires_year_month(client): resp = await client.get("/api/gallery/jump") assert resp.status_code == 400 @pytest.mark.asyncio async def test_image_detail_404_when_missing(client): resp = await client.get("/api/gallery/image/99999") assert resp.status_code == 404 @pytest.mark.asyncio async def test_scroll_sort_param_reverses(client, db): await _seed(db, 3) newest = await (await client.get("/api/gallery/scroll?limit=10&sort=newest")).get_json() oldest = await (await client.get("/api/gallery/scroll?limit=10&sort=oldest")).get_json() ids_new = [i["id"] for i in newest["images"]] ids_old = [i["id"] for i in oldest["images"]] assert ids_old == list(reversed(ids_new)) @pytest.mark.asyncio async def test_scroll_media_param(client, db): await _seed(db, 2) # only image/jpeg seeded resp = await client.get("/api/gallery/scroll?limit=10&media=video") assert resp.status_code == 200 assert (await resp.get_json())["images"] == [] @pytest.mark.asyncio async def test_facets_endpoint(client, db): await _seed(db, 3) # filesystem images: no artist, no tags, no platform resp = await client.get("/api/gallery/facets") assert resp.status_code == 200 body = await resp.get_json() assert body["total"] == 3 assert body["untagged"] == 3 assert body["no_artist"] == 3 assert "platforms" in body assert body["date_min"] is not None assert body["date_max"] is not None @pytest.mark.asyncio async def test_facets_rejects_bad_date(client): resp = await client.get("/api/gallery/facets?date_from=notadate") assert resp.status_code == 400 @pytest.mark.asyncio async def test_scroll_untagged_param(client, db): await _seed(db, 2) resp = await client.get("/api/gallery/scroll?untagged=1&limit=10") assert resp.status_code == 200 assert len((await resp.get_json())["images"]) == 2 @pytest.mark.asyncio async def test_scroll_date_from_param(client, db): await _seed(db, 2) # all created ~now # A future date_from excludes everything. resp = await client.get("/api/gallery/scroll?date_from=2099-01-01&limit=10") assert resp.status_code == 200 assert (await resp.get_json())["images"] == [] async def _seed_tagged(db): """3 images: [0]→a, [1]→b, [2]→x. Returns (records, {a,b,x} tag ids).""" records = [] base = datetime.now(UTC) for i in range(3): r = ImageRecord( path=f"/images/t/{i}.jpg", sha256=f"t{i:063d}", size_bytes=1, mime="image/jpeg", width=1, height=1, origin="imported_filesystem", integrity_status="unknown", ) r.created_at = base - timedelta(minutes=i) r.effective_date = r.created_at db.add(r) records.append(r) a = Tag(name="apia", kind=TagKind.general) b = Tag(name="apib", kind=TagKind.general) x = Tag(name="apix", kind=TagKind.general) db.add_all([a, b, x]) await db.flush() await db.execute(image_tag.insert().values([ {"image_record_id": records[0].id, "tag_id": a.id, "source": "manual"}, {"image_record_id": records[1].id, "tag_id": b.id, "source": "manual"}, {"image_record_id": records[2].id, "tag_id": x.id, "source": "manual"}, ])) await db.commit() return records, {"a": a.id, "b": b.id, "x": x.id} @pytest.mark.asyncio async def test_scroll_tag_or_param(client, db): # ?tag_or=a,b is one OR-group → images carrying a OR b (not x-only). records, ids = await _seed_tagged(db) resp = await client.get(f"/api/gallery/scroll?tag_or={ids['a']},{ids['b']}&limit=10") assert resp.status_code == 200 got = {i["id"] for i in (await resp.get_json())["images"]} assert got == {records[0].id, records[1].id} @pytest.mark.asyncio async def test_scroll_tag_not_param(client, db): # ?tag_not=x excludes the x-tagged image. records, ids = await _seed_tagged(db) resp = await client.get(f"/api/gallery/scroll?tag_not={ids['x']}&limit=10") assert resp.status_code == 200 got = {i["id"] for i in (await resp.get_json())["images"]} assert got == {records[0].id, records[1].id} @pytest.mark.asyncio async def test_scroll_repeated_tag_or_groups_anded(client, db): # Two ?tag_or= groups AND together: (a) AND (b) → no single image has both. records, ids = await _seed_tagged(db) resp = await client.get( f"/api/gallery/scroll?tag_or={ids['a']}&tag_or={ids['b']}&limit=10" ) assert resp.status_code == 200 assert (await resp.get_json())["images"] == []