fix(routes): clearing observers in the edit modal now persists

The route edit modal builds its PUT body with a ternary that collapses
an empty observer list to null:

    observer_public_keys: observerPublicKeys.length > 0 ? observerPublicKeys : null,

Pydantic parses null as None, and the PUT handler's guard

    if body.observer_public_keys is not None:
        _sync_observers(session, route, observer_nodes)

skips the sync entirely when the field is None. So removing all
observers in the modal sent null -> no DB change. Adding observers
worked because a non-empty array passed the guard and _sync_observers
deleted + recreated.

The None-means-skip semantic is correct for true partial updates, so
the fix is on the frontend: the edit modal is a full-form PUT, so it
must always send the array. When empty, it sends [], which Pydantic
parses as [] (not None), the guard passes, and _sync_observers deletes
every existing RouteObserver row.

Tests: add test_update_clear_observers_with_empty_list next to the
existing test_update_observers as a regression guard. It seeds one
observer via PUT, then PUTs observer_public_keys: [] and asserts both
the response and a fresh GET come back with an empty list.
This commit is contained in:
Louis King
2026-07-19 23:05:41 +01:00
parent ddf747fe68
commit d3f6d72775
2 changed files with 37 additions and 1 deletions
@@ -809,7 +809,7 @@ export async function render(container, params, router) {
enabled: enabledEl.checked,
reversible: reversibleEl.checked,
node_public_keys: nodePublicKeys,
observer_public_keys: observerPublicKeys.length > 0 ? observerPublicKeys : null,
observer_public_keys: observerPublicKeys,
};
const clearVal = clearEl.value.trim();
+36
View File
@@ -995,6 +995,42 @@ class TestUpdateRouteFields:
assert len(data["route_observers"]) == 1
assert data["route_observers"][0]["public_key"] == obs.public_key
def test_update_clear_observers_with_empty_list(
self, client_no_auth, api_db_session
):
"""Sending ``observer_public_keys: []`` must clear existing observers.
Regression guard for a frontend bug where the edit modal sent
``null`` instead of ``[]`` when the user removed all observers,
causing the backend's ``is not None`` guard to skip the sync.
"""
route = self._make_route(api_db_session)
obs = _make_node(api_db_session, "c" * 64, "Observer")
api_db_session.commit()
# Seed one observer.
client_no_auth.put(
f"/api/v1/routes/{route.id}",
json={"observer_public_keys": [obs.public_key]},
headers={"X-User-Roles": "admin"},
)
# Clear with an explicit empty list.
resp = client_no_auth.put(
f"/api/v1/routes/{route.id}",
json={"observer_public_keys": []},
headers={"X-User-Roles": "admin"},
)
assert resp.status_code == 200
assert resp.json()["route_observers"] == []
# Confirm persistence via a fresh GET.
detail = client_no_auth.get(
f"/api/v1/routes/{route.id}",
headers={"X-User-Roles": "admin"},
).json()
assert detail["route_observers"] == []
def test_update_unresolved_path_nodes_400(self, client_no_auth, api_db_session):
route = self._make_route(api_db_session)
api_db_session.commit()