177 lines
5.9 KiB
Python
177 lines
5.9 KiB
Python
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
|