"""api_widgets.py's config-save (form-urlencoded, dispatched by widget_type) and the photo-queue endpoints it shares the file with -- the moved-and-consolidated counterparts of the old frame-level api_config_save/api_queue/etc. No prior HTTP-level coverage existed for either the old or new shape of these endpoints; added after manual browser testing caught api_widget_config_save expecting a JSON body while the (unmodified, copied-over) dialog JS posts form-urlencoded data -- a real bug an HTTP-level test would have caught immediately.""" from __future__ import annotations from app.models import CalendarWidgetConfig, Frame, PhotoWidgetConfig, Widget from .conftest import csrf_headers _ASSETS = [{"id": "asset-1"}, {"id": "asset-2"}, {"id": "asset-3"}] def _photo_widget(db_session) -> Widget: return db_session.query(Widget).filter_by(frame_id=1, widget_type="photos").one() def _add_calendar_widget(db_session) -> Widget: import time widget = Widget(frame_id=1, widget_type="calendar", x=0, y=0, w=3, h=2, sort_order=1, created_at=time.time()) db_session.add(widget) db_session.flush() db_session.add(CalendarWidgetConfig(widget_id=widget.id)) db_session.commit() return widget def _mock_immich(monkeypatch): monkeypatch.setattr("app.routers.api_widgets.immich_client_for", lambda frame: object()) monkeypatch.setattr("app.routers.api_widgets.list_assets", lambda client, album_id: _ASSETS) # --- api_widget_config_save --- def test_config_save_updates_a_photos_widget(client, db_session): client.post("/setup", data={"username": "alice", "password": "hunter22"}) widget = _photo_widget(db_session) resp = client.post( f"/api/frames/1/widgets/{widget.id}/config", data={"album_id": "album-42", "order": "shuffle", "display_mode": "stretch_fill", "queue_target_len": "15"}, headers=csrf_headers(client), ) assert resp.status_code == 200, resp.text cfg = db_session.get(PhotoWidgetConfig, widget.id) assert cfg.album_id == "album-42" assert cfg.order == "shuffle" assert cfg.display_mode == "stretch_fill" assert cfg.queue_target_len == 15 def test_config_save_updates_a_calendar_widget(client, db_session): client.post("/setup", data={"username": "alice", "password": "hunter22"}) widget = _add_calendar_widget(db_session) resp = client.post( f"/api/frames/1/widgets/{widget.id}/config", data={"calendar_view": "week", "calendar_week_start": "1", "calendar_week_days": "5", "calendar_week_layout": "vertical", "calendar_weather_enabled": "true", "calendar_weather_units": "celsius", "calendar_tasks_enabled": "true"}, headers=csrf_headers(client), ) assert resp.status_code == 200, resp.text cfg = db_session.get(CalendarWidgetConfig, widget.id) assert cfg.view == "week" assert cfg.week_start == 1 assert cfg.week_days == 5 assert cfg.week_layout == "vertical" assert cfg.weather_enabled is True assert cfg.weather_units == "celsius" assert cfg.tasks_enabled is True def test_config_save_only_partially_updates_provided_fields(client, db_session): """Fields not present in the POST are left untouched -- the whole point of the partial-update convention (each dialog's own form only ever posts its own fields).""" client.post("/setup", data={"username": "alice", "password": "hunter22"}) widget = _photo_widget(db_session) cfg = db_session.get(PhotoWidgetConfig, widget.id) cfg.order = "shuffle" db_session.commit() resp = client.post( f"/api/frames/1/widgets/{widget.id}/config", data={"queue_target_len": "10"}, headers=csrf_headers(client), ) assert resp.status_code == 200, resp.text cfg = db_session.get(PhotoWidgetConfig, widget.id) assert cfg.queue_target_len == 10 assert cfg.order == "shuffle" # untouched def test_config_save_calendar_fields_are_a_no_op_on_a_photos_widget(client, db_session): """Posting calendar-shaped fields at a photos widget's config endpoint doesn't error -- it just doesn't apply, since the dispatch is purely by widget.widget_type.""" client.post("/setup", data={"username": "alice", "password": "hunter22"}) widget = _photo_widget(db_session) resp = client.post( f"/api/frames/1/widgets/{widget.id}/config", data={"calendar_view": "week"}, headers=csrf_headers(client), ) assert resp.status_code == 200, resp.text def test_config_save_404s_for_a_widget_id_that_does_not_exist(client, db_session): client.post("/setup", data={"username": "alice", "password": "hunter22"}) resp = client.post("/api/frames/1/widgets/999999/config", data={"order": "shuffle"}, headers=csrf_headers(client)) assert resp.status_code == 404 # --- photo queue (moved from the old frame-level /api/frames/{id}/queue) --- def test_queue_requires_a_configured_album(client, db_session): client.post("/setup", data={"username": "alice", "password": "hunter22"}) widget = _photo_widget(db_session) resp = client.get(f"/api/frames/1/widgets/{widget.id}/queue") assert resp.status_code == 400 def test_queue_returns_current_and_upcoming(client, db_session, monkeypatch): client.post("/setup", data={"username": "alice", "password": "hunter22"}) frame = db_session.get(Frame, 1) frame.immich_url = "http://immich.example.com" frame.immich_api_key = "key" widget = _photo_widget(db_session) cfg = db_session.get(PhotoWidgetConfig, widget.id) cfg.album_id = "album-1" db_session.commit() _mock_immich(monkeypatch) resp = client.get(f"/api/frames/1/widgets/{widget.id}/queue") assert resp.status_code == 200, resp.text data = resp.json() assert data["current"]["id"] == "asset-1" assert [u["id"] for u in data["upcoming"]] == ["asset-2", "asset-3"] assert data["control"]["you"] is True def test_queue_400s_when_widget_is_not_photos(client, db_session): client.post("/setup", data={"username": "alice", "password": "hunter22"}) widget = _add_calendar_widget(db_session) resp = client.get(f"/api/frames/1/widgets/{widget.id}/queue") assert resp.status_code == 400