Files
pigway-frame-flow/tests/test_source_browser.py
T

177 lines
5.9 KiB
Python
Raw Normal View History

import copy
from datetime import UTC, datetime
from io import BytesIO
import pytest
from PIL import Image
from pigway_frame_flow.providers.photo_sources import PhotoSourceInput
@pytest.fixture
def browser(client, service, monkeypatch):
photos = service.photo_sources
photos.save(
"family",
PhotoSourceInput(
name="Family",
kind="synology-photos",
location="albums",
people_filter=True,
person_ids=[7],
),
)
monkeypatch.setattr(photos.synology, "sid", lambda: "test-session")
monkeypatch.setattr(
photos, "catalog", lambda *a, **kw: pytest.fail("Must not scan full catalog")
)
rows = [
{
"id": i,
"type": "photo",
"filename": f"Photo-{i}.jpg",
"filesize": 2000000,
"time": int(datetime(2026, 10, 20, 2, tzinfo=UTC).timestamp()),
"indexed_time": int(datetime(2026, 10, 21, 2, tzinfo=UTC).timestamp()),
"additional": {
"folder": "albums/travel",
"resolution": {"width": 1600, "height": 1200},
"person": [{"id": 7, "name": "Person"}],
"thumbnail": {"cache_key": "x", "unit_id": i},
},
}
for i in range(1, 201)
]
calls = []
def request(api, method, version, **params):
calls.append(params)
assert params["limit"] <= 60
selected = rows
if params.get("person_id"):
selected = [
r
for r in rows
if any(
p["id"] == params["person_id"] for p in r["additional"]["person"]
)
]
return {
"list": copy.deepcopy(
selected[params["offset"] : params["offset"] + params["limit"]]
)
}
monkeypatch.setattr(photos.synology, "request", request)
return photos, rows, calls
def test_bounded_paging_preview_and_import(client, service, browser, monkeypatch):
photos, _, calls = browser
first = client.get("/api/v1/source-libraries/family/browse?limit=12").json()
assert len(first["items"]) == 12 and first["next_offset"] == 12 and len(calls) == 1
second = client.get(
"/api/v1/source-libraries/family/browse?limit=12&offset=12"
).json()
assert not {x["id"] for x in first["items"]} & {x["id"] for x in second["items"]}
image = BytesIO()
Image.new("RGB", (1600, 1200)).save(image, format="JPEG")
downloads = []
def download(row, **kwargs):
downloads.append(kwargs)
return image.getvalue()
monkeypatch.setattr(photos.synology, "download", download)
monkeypatch.setattr(
photos.synology,
"item_people",
lambda *a, **kw: pytest.fail("No full person album lookup"),
)
assert client.get(first["items"][0]["preview_url"]).status_code == 200
assert downloads[-1]["size"] == "m"
# Optional captions must use metadata already returned on this page.
source = photos.get("family").model_copy(update={"folder_caption": True})
photos.save("family", source)
client.get("/api/v1/source-libraries/family/browse?limit=12")
r = client.post(
"/api/v1/source-libraries/family/import",
json={"item_id": first["items"][0]["id"]},
)
assert r.status_code == 200, r.text
assert "size" not in downloads[-1] # Default XL for saved source pixels.
assert r.json()["caption"]["people"] == "Person"
def test_sparse_search_is_bounded_and_can_continue(client, browser):
_, _, calls = browser
r = client.get(
"/api/v1/source-libraries/family/browse?filename=not-found&limit=10"
).json()
assert r["items"] == [] and r["scanned"] == 40 and r["next_offset"] == 40
assert len(calls) == 4
def test_metadata_filters_and_source_scope(client, browser):
photos, rows, _ = browser
rows[0]["additional"]["folder"] = "albums-other/secret"
rows[1]["additional"]["person"] = [{"id": 8, "name": "Other"}]
rows[2]["filesize"] = 100
rows[3]["additional"]["resolution"]["width"] = 200
rows[4]["indexed_time"] = 1
params = {
"limit": 8,
"filename": "PHOTO-",
"folder": "TRAVEL",
"min_bytes": 1000000,
"max_bytes": 3000000,
"min_width": 1000,
"max_width": 2000,
"min_height": 1000,
"max_height": 2000,
"date_from": "2026-10-21",
"date_to": "2026-10-21",
"person_id": 7,
}
r = client.get("/api/v1/source-libraries/family/browse", params=params)
assert r.status_code == 200, r.text
assert {i["id"] for i in r.json()["items"]}.isdisjoint({"1", "2", "3", "4", "5"})
assert r.json()["items"]
assert photos.browser.cached("family", "1") is None
assert (
client.get("/api/v1/source-libraries/family/browse/1/preview").status_code
== 404
)
# Metadata already cached must not survive a source scope change.
item_id = r.json()["items"][0]["id"]
photos.save("family", photos.get("family").model_copy(update={"location": "other"}))
assert photos.browser.cached("family", item_id) is None
@pytest.mark.parametrize(
"query",
[
"limit=1000",
"offset=-1",
"min_width=-1",
"min_bytes=20&max_bytes=10",
"date_from=2026-10-22&date_to=2026-10-20",
"person_id=-1",
"date_field=unknown",
"unexpected=1",
],
)
def test_invalid_filters_rejected(client, browser, query):
assert (
client.get("/api/v1/source-libraries/family/browse?" + query).status_code == 422
)
def test_session_changes_invalidate_preview(client, browser, monkeypatch):
photos, _, _ = browser
item = client.get("/api/v1/source-libraries/family/browse?limit=1").json()["items"][
0
]
monkeypatch.setattr(photos.synology, "sid", lambda: "new-session")
assert client.get(item["preview_url"]).status_code == 404