"""Characterize webapp read layer (donor: query._fts_query, _where, search, facets, stats, photo, all_paths) against a seeded temp DB.""" from pathlib import Path import pytest import photo_analyzer as pa from webapp import query from conftest import seed_analyzed def test_fts_query_sanitization_goldens(): assert query._fts_query("beach sun") == '"beach"* "sun"*' assert query._fts_query('AND OR "quoted"') == '"AND"* "OR"* "quoted"*' assert query._fts_query("!!! ???") is None, "pure punctuation can't crash MATCH" assert query._fts_query("") is None assert query._fts_query("Straße") == '"Straße"*', "unicode words survive" def test_where_clause_goldens(): clauses, params = query._where({ "setting": "indoor", "people": "3+", "year_min": "2010", "has_location": "1"}) assert clauses == [ "p.setting = ?", "p.people_count >= 3", "p.approx_year >= ?", "p.location_hint IS NOT NULL AND TRIM(p.location_hint) != '' " "AND LOWER(p.location_hint) != 'null'"] assert params == ["indoor", 2010] assert query._where({}) == ([], []) @pytest.fixture def seeded(db): seed_analyzed(db, "/lib/album_a/beach.jpg", description="A sunny beach with palm trees.", tags=["beach", "palm"], setting="outdoor", people_count=2, approx_year=2015, location_hint="Rome, Italy") seed_analyzed(db, "/lib/album_a/city.jpg", description="A night city street.", tags=["city"], setting="outdoor", time_of_day="night", people_count=0, approx_year=2011) seed_analyzed(db, "/lib/album_b/dinner.jpg", description="Family dinner at home.", tags=["family", "dinner"], setting="indoor", people_count=4) pa.mark_exif_written(db, "/lib/album_b/dinner.jpg") pa.upsert_pending(db, "/lib/album_b/err.jpg") pa.mark_error(db, "/lib/album_b/err.jpg", "timeout") return db def test_search_fts_prefix_match(seeded): out = query.search(seeded, q="beach") assert out["total"] == 1 assert out["rows"][0]["path"] == "/lib/album_a/beach.jpg" assert out["rows"][0]["tags"] == ["beach", "palm"], "tags decoded from JSON" assert query.search(seeded, q="bea")["total"] == 1, "prefix term" def test_search_filters_and_browse(seeded): assert query.search(seeded)["total"] == 4, "empty query = browse all" assert query.search(seeded, filters={"setting": "indoor"})["total"] == 1 assert query.search(seeded, filters={"people": "3+"})["total"] == 1 assert query.search(seeded, filters={"year_min": 2012})["total"] == 1 assert query.search(seeded, filters={"has_location": "1"})["total"] == 1 assert query.search(seeded, q="city", filters={"setting": "indoor"})["total"] == 0, \ "search ANDs with filters" def test_search_paging(seeded): page = query.search(seeded, limit=2, offset=2) assert page["total"] == 4 assert len(page["rows"]) == 2 assert page["offset"] == 2 def test_photo_omits_raw_response(seeded): d = query.photo(seeded, "/lib/album_a/beach.jpg") assert d["description"] == "A sunny beach with palm trees." assert "raw_response" not in d assert query.photo(seeded, "/nope.jpg") is None def test_facets_and_albums(seeded): f = query.facets(seeded, Path("/lib")) assert (f["year_min"], f["year_max"]) == (2011, 2015) assert {a["album"]: a["count"] for a in f["albums"]} == { "album_a": 2, "album_b": 2} assert {v["value"]: v["count"] for v in f["setting"]} == { "outdoor": 2, "indoor": 1} def test_stats_album_progress_and_errors(seeded): s = query.stats(seeded, Path("/lib")) assert s["total"] == 4 assert s["status"] == {"analyzed": 2, "exif_written": 1, "error": 1} albums = {a["album"]: (a["done"], a["total"]) for a in s["albums"]} assert albums == {"album_a": (2, 2), "album_b": (1, 2)}, \ "done counts analyzed + exif_written" assert s["errors"] == [{"path": "/lib/album_b/err.jpg", "error": "timeout"}] def test_all_paths_is_the_image_endpoint_allowlist(seeded): assert query.all_paths(seeded) == { "/lib/album_a/beach.jpg", "/lib/album_a/city.jpg", "/lib/album_b/dinner.jpg", "/lib/album_b/err.jpg"}