"""Tests for the dashboard widget system: service-bound + built-in widgets.""" from __future__ import annotations from types import SimpleNamespace from unittest.mock import patch import pytest from cryptography.fernet import Fernet from fastapi.testclient import TestClient from media_library_viewer_api.dependencies import get_settings_store from media_library_viewer_api.main import app from media_library_viewer_api.services.settings_store import SettingsStore from media_library_viewer_api.widgets.sources import ( AlertmanagerWidgetSource, BackupsWidgetSource, GrafanaWidgetSource, JellyfinWidgetSource, ServiceRecord, StaticWidgetSource, ) TEST_KEY = Fernet.generate_key().decode() @pytest.fixture(autouse=True) def _encryption_key(monkeypatch): monkeypatch.setenv("MANAGE_ENCRYPTION_KEY", TEST_KEY) yield @pytest.fixture def client(tmp_path): """FastAPI test client with a fresh settings store and auth disabled.""" store = SettingsStore(tmp_path / "settings.sqlite") store.ensure_defaults() app.dependency_overrides[get_settings_store] = lambda: store auth_settings = SimpleNamespace(auth_enabled=False) with patch("media_library_viewer_api.auth.get_settings", return_value=auth_settings): yield TestClient(app) app.dependency_overrides.clear() def _make_grafana_service(client, name="Production Grafana", **config_overrides): config = {"base_url": "https://grafana.example.com"} config.update(config_overrides) return client.post( "/api/services/instances", json={"service_type": "grafana", "name": name, "config": config, "enabled": True}, ).json() # --------------------------------------------------------------------------- # Built-in kinds + built-in widget CRUD # --------------------------------------------------------------------------- def test_list_builtin_kinds(client): response = client.get("/api/widgets/builtin") assert response.status_code == 200 kinds = {item["kind"] for item in response.json()} assert kinds == {"backups", "static"} def test_create_and_read_static_widget(client): response = client.post( "/api/widgets/instances", json={ "widget_kind": "static", "title": "Note", "config": {"text": "hello"}, }, ) assert response.status_code == 201 created = response.json() assert created["widget_kind"] == "static" assert created["service_id"] is None assert created["config"]["text"] == "hello" listed = client.get("/api/widgets/instances").json() assert len(listed) == 1 assert listed[0]["id"] == created["id"] def test_create_backups_widget(client): response = client.post( "/api/widgets/instances", json={"widget_kind": "backups", "title": "Backups", "config": {}}, ) assert response.status_code == 201 def test_widget_filtering_by_service_id_and_scope(client): """Test ?service_id= and ?scope= query params on GET /api/widgets/instances.""" service = _make_grafana_service(client) # Create a dashboard-scoped (built-in) widget + a service-scoped widget. client.post( "/api/widgets/instances", json={"widget_kind": "static", "title": "Note", "config": {"text": "hi"}}, ) client.post( "/api/widgets/instances", json={ "service_id": service["id"], "widget_kind": "link", "title": "Dash", "config": {"dashboard_uid": "o"}, }, ) # No filter: both widgets. all_widgets = client.get("/api/widgets/instances").json() assert len(all_widgets) == 2 # Filter by service_id: only the service-scoped one. by_service = client.get(f"/api/widgets/instances?service_id={service['id']}").json() assert len(by_service) == 1 assert by_service[0]["service_id"] == service["id"] # scope=dashboard: only the built-in (NULL service_id). dashboard_scope = client.get("/api/widgets/instances?scope=dashboard").json() assert len(dashboard_scope) == 1 assert dashboard_scope[0]["service_id"] is None # scope=service: only the non-null service_id widget. service_scope = client.get("/api/widgets/instances?scope=service").json() assert len(service_scope) == 1 assert service_scope[0]["service_id"] == service["id"] def test_unknown_builtin_kind_rejected(client): response = client.post( "/api/widgets/instances", json={"widget_kind": "bogus", "title": "x", "config": {}}, ) assert response.status_code == 422 def test_credential_key_in_config_rejected(client): response = client.post( "/api/widgets/instances", json={"widget_kind": "static", "title": "x", "config": {"api_key": "leak"}}, ) assert response.status_code == 422 # --------------------------------------------------------------------------- # Service-bound widget CRUD # --------------------------------------------------------------------------- def test_create_service_bound_widget(client): service = _make_grafana_service(client) response = client.post( "/api/widgets/instances", json={ "service_id": service["id"], "widget_kind": "link", "title": "Dashboard", "config": {"dashboard_uid": "overview"}, }, ) assert response.status_code == 201 created = response.json() assert created["service_id"] == service["id"] assert created["widget_kind"] == "link" def test_service_bound_widget_unknown_kind_rejected(client): service = _make_grafana_service(client) response = client.post( "/api/widgets/instances", json={ "service_id": service["id"], "widget_kind": "metric", "title": "x", "config": {}, }, ) assert response.status_code == 422 def test_service_bound_widget_service_not_found_rejected(client): response = client.post( "/api/widgets/instances", json={ "service_id": "missing", "widget_kind": "link", "title": "x", "config": {"dashboard_uid": "u"}, }, ) assert response.status_code == 422 def test_service_bound_widget_invalid_config_rejected(client): service = _make_grafana_service(client) response = client.post( "/api/widgets/instances", json={ "service_id": service["id"], "widget_kind": "link", "title": "x", "config": {"dashboard_uid": ""}, # empty still validates; use bad type }, ) # Empty string passes Pydantic; force a real failure with a bad type. response = client.post( "/api/widgets/instances", json={ "service_id": service["id"], "widget_kind": "link", "title": "x", "config": {"dashboard_uid": 123}, }, ) assert response.status_code == 422 def test_update_and_delete_widget(client): created = client.post( "/api/widgets/instances", json={"widget_kind": "static", "title": "Note", "config": {"text": "a"}}, ).json() updated = client.put( f"/api/widgets/instances/{created['id']}", json={"widget_kind": "static", "title": "Note2", "config": {"text": "b"}}, ).json() assert updated["title"] == "Note2" assert client.delete(f"/api/widgets/instances/{created['id']}").status_code == 200 assert client.get("/api/widgets/instances").json() == [] def test_update_nonexistent_returns_404(client): response = client.put( "/api/widgets/instances/missing", json={"widget_kind": "static", "title": "x", "config": {}}, ) assert response.status_code == 404 def test_update_id_mismatch_returns_400(client): created = client.post( "/api/widgets/instances", json={"widget_kind": "static", "title": "x", "config": {}}, ).json() response = client.put( f"/api/widgets/instances/{created['id']}", json={"id": "other", "widget_kind": "static", "title": "x", "config": {}}, ) assert response.status_code == 400 # --------------------------------------------------------------------------- # Data endpoint # --------------------------------------------------------------------------- def test_fetch_static_widget_data(client): created = client.post( "/api/widgets/instances", json={"widget_kind": "static", "title": "Note", "config": {"text": "hello"}}, ).json() response = client.get(f"/api/widgets/instances/{created['id']}/data") assert response.status_code == 200 body = response.json() assert body["data"]["text"] == "hello" assert body["error"] is None def test_fetch_backups_widget_data(client): created = client.post( "/api/widgets/instances", json={"widget_kind": "backups", "title": "Backups", "config": {}}, ).json() response = client.get(f"/api/widgets/instances/{created['id']}/data") assert response.status_code == 200 assert "total_jobs" in response.json()["data"] def test_fetch_grafana_link_widget_data(client): service = _make_grafana_service(client) created = client.post( "/api/widgets/instances", json={ "service_id": service["id"], "widget_kind": "link", "title": "Dashboard", "config": {"dashboard_uid": "overview", "panel_id": 2}, }, ).json() response = client.get(f"/api/widgets/instances/{created['id']}/data") assert response.status_code == 200 assert response.json()["data"]["url"] == "https://grafana.example.com/d/overview?viewPanel=2" def test_fetch_widget_service_not_found(client): service = _make_grafana_service(client) created = client.post( "/api/widgets/instances", json={ "service_id": service["id"], "widget_kind": "link", "title": "x", "config": {"dashboard_uid": "u"}, }, ).json() # Deleting the service cascade-deletes its widgets, so the widget is gone. client.delete(f"/api/services/instances/{service['id']}") assert client.get("/api/widgets/instances").json() == [] assert client.get(f"/api/widgets/instances/{created['id']}/data").status_code == 404 def test_fetch_widget_service_disabled(client): service = _make_grafana_service(client) created = client.post( "/api/widgets/instances", json={ "service_id": service["id"], "widget_kind": "link", "title": "x", "config": {"dashboard_uid": "u"}, }, ).json() client.put( f"/api/services/instances/{service['id']}", json={ "service_type": "grafana", "name": service["name"], "config": {"base_url": "https://grafana.example.com"}, "enabled": False, }, ) response = client.get(f"/api/widgets/instances/{created['id']}/data") assert response.status_code == 200 assert "disabled" in response.json()["error"] def test_fetch_widget_not_found(client): assert client.get("/api/widgets/instances/missing/data").status_code == 404 # --------------------------------------------------------------------------- # Adapter unit tests # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_grafana_adapter_builds_url(): adapter = GrafanaWidgetSource() service = ServiceRecord(id="s", service_type="grafana", name="g", config={"base_url": "http://g:3000"}) result = await adapter.fetch(service, "link", {"dashboard_uid": "ov"}) assert result["url"] == "http://g:3000/d/ov" result = await adapter.fetch(service, "link", {"dashboard_uid": "ov", "panel_id": 4}) assert result["url"] == "http://g:3000/d/ov?viewPanel=4" @pytest.mark.asyncio async def test_grafana_adapter_missing_service(): adapter = GrafanaWidgetSource() result = await adapter.fetch(None, "link", {"dashboard_uid": "ov"}) assert "error" in result @pytest.mark.asyncio async def test_alertmanager_adapter_summarizes_alerts(): adapter = AlertmanagerWidgetSource() service = ServiceRecord(id="s", service_type="alertmanager", name="am", config={"base_url": "http://am:9093"}) payload = SimpleNamespace( raise_for_status=lambda: None, json=lambda: { "status": "success", "data": [ { "labels": {"alertname": "DiskFull", "severity": "critical"}, "annotations": {"summary": "disk full"}, "startsAt": "2026-06-23T00:00:00Z", "status": "firing", } ], }, ) with patch("media_library_viewer_api.widgets.sources.requests.get", return_value=payload): result = await adapter.fetch(service, "active_alerts", {}) assert result["total"] == 1 assert result["by_severity"]["critical"] == 1 assert result["alerts"][0]["name"] == "DiskFull" @pytest.mark.asyncio async def test_alertmanager_adapter_applies_severity_filter(): adapter = AlertmanagerWidgetSource() service = ServiceRecord(id="s", service_type="alertmanager", name="am", config={"base_url": "http://am:9093"}) payload = SimpleNamespace( raise_for_status=lambda: None, json=lambda: { "status": "success", "data": [ {"labels": {"alertname": "A", "severity": "critical"}, "annotations": {}, "status": "firing"}, {"labels": {"alertname": "B", "severity": "warning"}, "annotations": {}, "status": "firing"}, ], }, ) with patch("media_library_viewer_api.widgets.sources.requests.get", return_value=payload): result = await adapter.fetch(service, "active_alerts", {"severity_filter": "critical"}) assert result["total"] == 1 assert result["alerts"][0]["name"] == "A" @pytest.mark.asyncio async def test_alertmanager_adapter_sends_bearer_token(): adapter = AlertmanagerWidgetSource() service = ServiceRecord( id="s", service_type="alertmanager", name="am", config={"base_url": "http://am:9093"}, secrets={"api_key": "tok"}, ) payload = SimpleNamespace(raise_for_status=lambda: None, json=lambda: {"status": "success", "data": []}) with patch("media_library_viewer_api.widgets.sources.requests.get", return_value=payload) as mock_get: result = await adapter.fetch(service, "active_alerts", {}) assert result["total"] == 0 _, kwargs = mock_get.call_args assert kwargs["headers"]["Authorization"] == "Bearer tok" @pytest.mark.asyncio async def test_alertmanager_adapter_missing_service(): adapter = AlertmanagerWidgetSource() result = await adapter.fetch(None, "active_alerts", {}) assert "error" in result @pytest.mark.asyncio async def test_static_adapter(): adapter = StaticWidgetSource() result = await adapter.fetch(None, "static", {"text": "hi"}) assert result == {"text": "hi"} @pytest.mark.asyncio async def test_backups_adapter(client): store = app.dependency_overrides[get_settings_store]() with patch("media_library_viewer_api.widgets.sources.get_settings_store", return_value=store): adapter = BackupsWidgetSource() result = await adapter.fetch(None, "backups", {}) assert "total_jobs" in result @pytest.mark.asyncio async def test_ssh_task_adapter_missing_service(): from media_library_viewer_api.widgets.sources import SshTaskWidgetSource adapter = SshTaskWidgetSource() result = await adapter.fetch(None, "task_output", {"task_id": "t1"}) assert "error" in result @pytest.mark.asyncio async def test_ssh_task_adapter_records_history_on_run(client): store = app.dependency_overrides[get_settings_store]() # Save a task and an ssh_tasks service instance. task = store.upsert_task( { "name": "echo", "task_type": "shell", "content": "echo hi", "enabled": True, "default_service_id": "", } ) service = store.upsert_service( {"service_type": "ssh_tasks", "name": "box", "config": {"host": "h", "username": "u"}, "enabled": True} ) fake_result = SimpleNamespace(exit_status=0, stdout="hi\n", stderr="") fake_client = SimpleNamespace(run=lambda *a, **k: fake_result) from media_library_viewer_api.widgets.sources import SshTaskWidgetSource adapter = SshTaskWidgetSource() service_record = ServiceRecord( id=service["id"], service_type="ssh_tasks", name="box", config={"host": "h", "username": "u"} ) with ( patch("media_library_viewer_api.widgets.sources.get_settings_store", return_value=store), patch("media_library_viewer_api.services.task_runner.build_ssh_client", return_value=fake_client), ): result = await adapter.fetch(service_record, "task_output", {"task_id": task["id"]}) assert result["exit_status"] == 0 runs = store.list_service_task_runs(service_id=service["id"]) assert len(runs) == 1 assert runs[0]["status"] == "success" # --------------------------------------------------------------------------- # New widget kind tests (jellyfin now_playing + grafana panel) # --------------------------------------------------------------------------- def test_jellyfin_definition_has_now_playing_widget(): from media_library_viewer_api.integrations.registry import get_service_definition definition = get_service_definition("jellyfin") kinds = {wk.kind for wk in definition.widget_kinds} assert "now_playing" in kinds assert "activity" in kinds def test_grafana_definition_has_chart_widget(): from media_library_viewer_api.integrations.registry import get_service_definition definition = get_service_definition("grafana") kinds = {wk.kind for wk in definition.widget_kinds} assert "chart" in kinds assert "link" in kinds @pytest.mark.asyncio async def test_grafana_adapter_chart_queries_datasource(): """Chart widget should POST to /api/ds/query and normalize the response.""" from unittest.mock import MagicMock, patch adapter = GrafanaWidgetSource() service = ServiceRecord( id="s", service_type="grafana", name="g", config={"base_url": "http://g:3000", "timeout_seconds": 5}, secrets={"api_key": "tok"}, ) mock_resp = MagicMock() mock_resp.json.return_value = { "results": { "A": { "frames": [ { "data": {"values": [[1000, 2000], [0.5, 0.8]]}, "schema": {"fields": [{"name": "Time"}, {"name": "cpu_usage"}]}, } ] } } } mock_resp.raise_for_status = MagicMock() with patch("media_library_viewer_api.widgets.sources.requests.post", return_value=mock_resp): result = await adapter.fetch( service, "chart", {"query": "rate(cpu[5m])", "datasource_uid": "prometheus"}, ) assert "series" in result assert len(result["series"]) == 1 assert result["series"][0]["label"] == "cpu_usage" assert result["series"][0]["points"] == [ {"t": 1000, "v": 0.5}, {"t": 2000, "v": 0.8}, ] @pytest.mark.asyncio async def test_grafana_adapter_chart_extracts_prometheus_labels(): """Multiple Prometheus series should get unique labels from frame metadata.""" from unittest.mock import MagicMock, patch adapter = GrafanaWidgetSource() service = ServiceRecord( id="s", service_type="grafana", name="g", config={"base_url": "http://g:3000", "timeout_seconds": 5}, secrets={"api_key": "tok"}, ) mock_resp = MagicMock() mock_resp.json.return_value = { "results": { "A": { "frames": [ { "data": {"values": [[1000], [0.5]]}, "schema": { "fields": [ {"name": "Time"}, { "name": "Value", "labels": { "instance": "server1:9100", "mode": "iowait", }, }, ] }, }, { "data": {"values": [[1000], [0.3]]}, "schema": { "fields": [ {"name": "Time"}, { "name": "Value", "labels": { "instance": "server2:9100", "mode": "iowait", }, }, ] }, }, ] } } } mock_resp.raise_for_status = MagicMock() with patch("media_library_viewer_api.widgets.sources.requests.post", return_value=mock_resp): result = await adapter.fetch(service, "chart", {"query": "rate(cpu[5m])"}) assert len(result["series"]) == 2 assert result["series"][0]["label"] == "instance=server1:9100 mode=iowait" assert result["series"][1]["label"] == "instance=server2:9100 mode=iowait" @pytest.mark.asyncio async def test_grafana_adapter_chart_requires_api_key(): adapter = GrafanaWidgetSource() service = ServiceRecord( id="s", service_type="grafana", name="g", config={"base_url": "http://g:3000"}, ) result = await adapter.fetch(service, "chart", {"query": "up"}) assert "error" in result assert "api_key" in result["error"] @pytest.mark.asyncio async def test_grafana_adapter_chart_handles_http_failure(): from unittest.mock import patch import requests as req_mod adapter = GrafanaWidgetSource() service = ServiceRecord( id="s", service_type="grafana", name="g", config={"base_url": "http://g:3000", "timeout_seconds": 2}, secrets={"api_key": "tok"}, ) with patch( "media_library_viewer_api.widgets.sources.requests.post", side_effect=req_mod.ConnectionError("refused"), ): result = await adapter.fetch(service, "chart", {"query": "up"}) assert "error" in result assert "failed" in result["error"].lower() @pytest.mark.asyncio async def test_jellyfin_now_playing_filters_active_sessions(): """now_playing should exclude idle (no NowPlayingItem) and paused sessions.""" adapter = JellyfinWidgetSource() service = ServiceRecord( id="s", service_type="jellyfin", name="jf", config={"base_url": "http://jf:8096"}, secrets={"api_key": "k"}, ) playing_session = { "UserName": "alice", "NowPlayingItem": {"Name": "Movie", "Type": "Movie"}, "PlayState": {"IsPaused": False}, "DeviceName": "Web", } paused_session = { "UserName": "bob", "NowPlayingItem": {"Name": "Show", "Type": "Episode"}, "PlayState": {"IsPaused": True}, "DeviceName": "TV", } idle_session = { "UserName": "carol", "PlayState": {"IsPaused": False}, "DeviceName": "Phone", } mock_client = SimpleNamespace(sessions=lambda: [playing_session, paused_session, idle_session]) with patch("media_library_viewer_api.widgets.sources.JellyfinClient", return_value=mock_client): result = await adapter.fetch(service, "now_playing", {}) sessions = result["sessions"] assert len(sessions) == 1 assert sessions[0]["user"] == "alice" assert sessions[0]["state"] == "playing" @pytest.mark.asyncio async def test_jellyfin_activity_shows_all_sessions(): """activity (default) should include idle and paused sessions.""" adapter = JellyfinWidgetSource() service = ServiceRecord( id="s", service_type="jellyfin", name="jf", config={"base_url": "http://jf:8096"}, secrets={"api_key": "k"}, ) mock_client = SimpleNamespace( sessions=lambda: [ {"UserName": "alice", "NowPlayingItem": {"Name": "M"}, "PlayState": {"IsPaused": False}}, {"UserName": "bob", "PlayState": {"IsPaused": False}}, ] ) with patch("media_library_viewer_api.widgets.sources.JellyfinClient", return_value=mock_client): result = await adapter.fetch(service, "activity", {}) assert len(result["sessions"]) == 2 # --------------------------------------------------------------------------- # Widget references (live-link widgets across dashboards) # --------------------------------------------------------------------------- @pytest.fixture def widget_ref_client(monkeypatch): """TestClient with an isolated SettingsStore + encryption key.""" monkeypatch.setenv( "MANAGE_ENCRYPTION_KEY", Fernet.generate_key().decode(), ) from media_library_viewer_api.services.secrets import reset_encryption_key_cache reset_encryption_key_cache() import tempfile from pathlib import Path store = SettingsStore(str(Path(tempfile.mkdtemp()) / "test.db")) store.ensure_defaults() def get_store_override(): return store app.dependency_overrides[get_settings_store] = get_store_override client = TestClient(app) yield client, store app.dependency_overrides.pop(get_settings_store, None) def test_widget_reference_lifecycle(widget_ref_client): """Create a widget, reference it on 'main', verify it appears, delete reference.""" client, store = widget_ref_client # Create a service-bound widget (simulating one on a Grafana Overview). store.upsert_service( { "service_type": "grafana", "name": "Grafana", "config": {"base_url": "https://grafana.example.com"}, "secrets": {"api_key": "tok"}, "enabled": True, }, ) service = store.list_services("grafana")[0] widget = store.upsert_widget( { "service_id": service["id"], "widget_kind": "chart", "title": "CPU IOWait", "config": {"query": "rate(cpu[5m])", "datasource_uid": "prometheus"}, "enabled": True, "sort_order": 0, } ) # Reference it on "main" dashboard. resp = client.post( "/api/widgets/references", json={ "dashboard_scope": "main", "widget_id": widget["id"], "sort_order": 5, }, ) assert resp.status_code == 201 ref = resp.json() assert ref["dashboard_scope"] == "main" assert ref["widget_id"] == widget["id"] ref_id = ref["id"] # List references for "main" — should include our widget. resp = client.get("/api/widgets/references", params={"dashboard_scope": "main"}) assert resp.status_code == 200 refs = resp.json() assert len(refs) == 1 assert refs[0]["widget"]["title"] == "CPU IOWait" # Delete the reference. resp = client.delete(f"/api/widgets/references/{ref_id}") assert resp.status_code == 200 assert resp.json()["status"] == "deleted" # Reference is gone, original widget still exists. resp = client.get("/api/widgets/references", params={"dashboard_scope": "main"}) assert len(resp.json()) == 0 assert store.get_widget(widget["id"]) is not None def test_widget_reference_detach(widget_ref_client): """Detach clones the widget into a standalone instance and removes the reference.""" client, store = widget_ref_client store.upsert_service( { "service_type": "grafana", "name": "Grafana", "config": {"base_url": "https://grafana.example.com"}, "secrets": {"api_key": "tok"}, "enabled": True, }, ) service = store.list_services("grafana")[0] widget = store.upsert_widget( { "service_id": service["id"], "widget_kind": "chart", "title": "Memory", "config": {"query": "mem", "datasource_uid": "prometheus"}, "enabled": True, "sort_order": 0, } ) # Reference on "main". resp = client.post( "/api/widgets/references", json={ "dashboard_scope": "main", "widget_id": widget["id"], }, ) ref_id = resp.json()["id"] # Detach. resp = client.post(f"/api/widgets/references/{ref_id}/detach") assert resp.status_code == 200 cloned = resp.json() assert cloned["title"] == "Memory" assert cloned["widget_kind"] == "chart" assert cloned["service_id"] == service["id"] # Fix 2: preserves service binding assert cloned["config"]["query"] == "mem" assert cloned["id"] != widget["id"] # new independent widget # Reference is gone. refs = client.get("/api/widgets/references", params={"dashboard_scope": "main"}).json() assert len(refs) == 0 # Original still exists. assert store.get_widget(widget["id"]) is not None def test_widget_reference_update_sort_order(widget_ref_client): """PUT /references/{id} updates only the reference's sort_order (Fix 1).""" client, store = widget_ref_client widget_a = store.upsert_widget( { "service_id": None, "widget_kind": "static", "title": "A", "config": {"text": "a"}, "enabled": True, "sort_order": 0, } ) widget_b = store.upsert_widget( { "service_id": None, "widget_kind": "static", "title": "B", "config": {"text": "b"}, "enabled": True, "sort_order": 1, } ) # Two references on the same dashboard scope. resp = client.post( "/api/widgets/references", json={ "dashboard_scope": "named:test", "widget_id": widget_a["id"], "sort_order": 0, }, ) ref_a = resp.json() resp = client.post( "/api/widgets/references", json={ "dashboard_scope": "named:test", "widget_id": widget_b["id"], "sort_order": 1, }, ) ref_b = resp.json() # Swap sort orders via PUT (per-dashboard reorder). resp = client.put(f"/api/widgets/references/{ref_a['id']}?sort_order=1") assert resp.status_code == 200 assert resp.json()["sort_order"] == 1 resp = client.put(f"/api/widgets/references/{ref_b['id']}?sort_order=0") assert resp.status_code == 200 assert resp.json()["sort_order"] == 0 # Widget instances themselves are unchanged. assert store.get_widget(widget_a["id"])["sort_order"] == 0 assert store.get_widget(widget_b["id"])["sort_order"] == 1