"""routers/api_widgets.py's POST .../border endpoint (models.Widget. border_style/border_thickness/border_color_index) -- a Widget-level property, not a per-type config field, so this is exercised independently of any specific widget_type. End-to-end rendering (does a saved border actually show up in the composited panel) is covered separately in test_device_widget_dispatch.py-style tests below via the frame preview endpoint, the same real render_panel path /frame/image uses.""" from __future__ import annotations import io from PIL import Image from app.models import Frame, Widget from .conftest import csrf_headers, link_user, login, make_user def _widget_id(db_session, widget_type="photos") -> int: return db_session.query(Widget).filter_by(frame_id=1, widget_type=widget_type).one().id def test_new_widget_defaults_to_no_border(db_session): widget = db_session.query(Widget).filter_by(frame_id=1).one() assert widget.border_style == "none" assert widget.border_thickness == 3 assert widget.border_color_index == 0 def test_set_border_persists(client, db_session): client.post("/setup", data={"username": "alice", "password": "hunter22"}) widget_id = _widget_id(db_session) resp = client.post(f"/api/frames/1/widgets/{widget_id}/border", json={"border_style": "dashed", "border_thickness": 5, "border_color_index": 3}, headers=csrf_headers(client)) assert resp.status_code == 200, resp.text assert resp.json() == { "id": widget_id, "widget_type": "photos", "x": 0, "y": 0, "w": 8, "h": 5, "sort_order": 0, "border_style": "dashed", "border_thickness": 5, "border_color_index": 3, "locked": False, } widget = db_session.get(Widget, widget_id) assert (widget.border_style, widget.border_thickness, widget.border_color_index) == ("dashed", 5, 3) def test_set_border_thickness_clamps_to_range(client, db_session): client.post("/setup", data={"username": "alice", "password": "hunter22"}) widget_id = _widget_id(db_session) resp = client.post(f"/api/frames/1/widgets/{widget_id}/border", json={"border_style": "solid", "border_thickness": 999, "border_color_index": 0}, headers=csrf_headers(client)) assert resp.status_code == 200, resp.text assert resp.json()["border_thickness"] == 8 # image_pipeline.MAX_BORDER_THICKNESS resp = client.post(f"/api/frames/1/widgets/{widget_id}/border", json={"border_style": "solid", "border_thickness": -5, "border_color_index": 0}, headers=csrf_headers(client)) assert resp.status_code == 200, resp.text assert resp.json()["border_thickness"] == 1 # image_pipeline.MIN_BORDER_THICKNESS def test_set_border_rejects_unknown_style(client, db_session): client.post("/setup", data={"username": "alice", "password": "hunter22"}) widget_id = _widget_id(db_session) resp = client.post(f"/api/frames/1/widgets/{widget_id}/border", json={"border_style": "sparkly", "border_thickness": 3, "border_color_index": 0}, headers=csrf_headers(client)) assert resp.status_code == 400 assert "border_style" in resp.json()["detail"] def test_set_border_rejects_out_of_range_color_index(client, db_session): client.post("/setup", data={"username": "alice", "password": "hunter22"}) widget_id = _widget_id(db_session) resp = client.post(f"/api/frames/1/widgets/{widget_id}/border", json={"border_style": "solid", "border_thickness": 3, "border_color_index": 6}, headers=csrf_headers(client)) assert resp.status_code == 400 assert "border_color_index" in resp.json()["detail"] def test_set_border_404s_for_unknown_widget(client, db_session): client.post("/setup", data={"username": "alice", "password": "hunter22"}) resp = client.post("/api/frames/1/widgets/999999/border", json={"border_style": "solid", "border_thickness": 3, "border_color_index": 0}, headers=csrf_headers(client)) assert resp.status_code == 404 def test_set_border_unrelated_user_404s(client, db_session): client.post("/setup", data={"username": "alice", "password": "hunter22"}) make_user(db_session, "mallory") widget_id = _widget_id(db_session) client.cookies.clear() login(client, "mallory") resp = client.post(f"/api/frames/1/widgets/{widget_id}/border", json={"border_style": "solid", "border_thickness": 3, "border_color_index": 0}, headers=csrf_headers(client)) assert resp.status_code == 404 assert db_session.get(Widget, widget_id).border_style == "none" def test_set_border_linked_but_not_controlling_user_409s(client, db_session): client.post("/setup", data={"username": "alice", "password": "hunter22"}) bob = make_user(db_session, "bob") frame = db_session.get(Frame, 1) link_user(db_session, bob, frame) widget_id = _widget_id(db_session) client.cookies.clear() login(client, "bob") resp = client.post(f"/api/frames/1/widgets/{widget_id}/border", json={"border_style": "solid", "border_thickness": 3, "border_color_index": 0}, headers=csrf_headers(client)) assert resp.status_code == 409 assert resp.json()["detail"]["error"] == "not_controller" # --- actually renders (real render_panel path, same as /frame/image) ------ def _preview_pixels(client) -> Image.Image: resp = client.get("/api/frames/1/preview") assert resp.status_code == 200, resp.text return Image.open(io.BytesIO(resp.content)).convert("RGB") def test_solid_border_draws_the_configured_palette_color(client, db_session): client.post("/setup", data={"username": "alice", "password": "hunter22"}) widget_id = _widget_id(db_session) # color_index 3 == Red, see image_pipeline.PALETTE_LABELS/DEFAULT_PALETTE_RGB resp = client.post(f"/api/frames/1/widgets/{widget_id}/border", json={"border_style": "solid", "border_thickness": 6, "border_color_index": 3}, headers=csrf_headers(client)) assert resp.status_code == 200, resp.text img = _preview_pixels(client) assert img.getpixel((0, 0)) == (207, 0, 15) # DEFAULT_PALETTE_RGB[3], top-left corner of the stroke def test_no_border_leaves_the_edge_unmarked(client, db_session): """The default "none" style is a true no-op -- this is the negative case for test_solid_border_draws_the_configured_palette_color, using the exact same photo-widget fixture minus the border, so a red edge pixel there can only be the border, not something else in the render path already painting it that color.""" client.post("/setup", data={"username": "alice", "password": "hunter22"}) img = _preview_pixels(client) assert img.getpixel((0, 0)) != (207, 0, 15)