diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 7c8e3b2..19bdc9b 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -135,7 +135,18 @@ def client(db_session, monkeypatch): return list(self.roles.values()) def delete_role(self, rolename): + # Mirrors the real broker (roles.c role__remove_all_clients / + # role__remove_all_groups): deleteRole strips the role from + # every client and group that holds it. self.roles.pop(rolename, None) + for client in self.clients.values(): + client["roles"] = [ + r for r in client.get("roles", []) if r["rolename"] != rolename + ] + for group in self.groups.values(): + group["roles"] = [ + r for r in group.get("roles", []) if r["rolename"] != rolename + ] def set_role_acls(self, rolename, acls): self.roles[rolename]["acls"] = acls diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index c478894..81c149c 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -198,7 +198,27 @@ def test_group_role_assignment(admin_client): ) -def test_listener_management_and_preview(admin_client): +def test_role_deletion(admin_client): + """deleteRole removes the role everywhere (spec section 13): the broker's + dynsec deleteRole strips it from every client and group that holds it.""" + admin_client.post("/api/v1/mqtt/roles", json={"rolename": "obsolete"}) + admin_client.post("/api/v1/mqtt/clients", json={"username": "ps042"}) + admin_client.post("/api/v1/mqtt/clients/ps042/roles", json={"rolename": "obsolete"}) + admin_client.post("/api/v1/mqtt/groups", json={"groupname": "legacy"}) + admin_client.post("/api/v1/mqtt/groups/legacy/roles", json={"rolename": "obsolete"}) + + deleted = admin_client.delete("/api/v1/mqtt/roles/obsolete") + assert deleted.status_code == 204 + + # Role gone from the listing... + roles = {r["rolename"] for r in admin_client.get("/api/v1/mqtt/roles").json()} + assert "obsolete" not in roles + # ...and the broker's cleanup means clients/groups no longer hold it. + client = admin_client.get("/api/v1/mqtt/clients/ps042").json() + assert all(r["rolename"] != "obsolete" for r in client["roles"]) + groups = admin_client.get("/api/v1/mqtt/groups").json() + legacy = next(g for g in groups if g["groupname"] == "legacy") + assert all(r["rolename"] != "obsolete" for r in legacy["roles"]) response = admin_client.post( "/api/v1/broker/listeners", json={"name": "Production MQTT", "port": 1883, "protocol": "mqtt"}, diff --git a/compose.yaml b/compose.yaml index f6124b7..21f6f2a 100644 --- a/compose.yaml +++ b/compose.yaml @@ -59,6 +59,9 @@ services: - mosquitto_log:/mosquitto/log networks: - backend-net + ports: + - "${MQTT_PORT:-1883}:1883" + - "${MQTTS_PORT:-8883}:8883" # Release 1 ships an internal-only, plaintext listener on the backend # network for the management channel; production listeners are added # through the platform (spec §60). Nothing is published to the host. diff --git a/frontend/src/pages/Roles.tsx b/frontend/src/pages/Roles.tsx index bbc645b..9b5d6e8 100644 --- a/frontend/src/pages/Roles.tsx +++ b/frontend/src/pages/Roles.tsx @@ -91,6 +91,28 @@ export function RolesPage() { onError: (e: Error) => setError(e.message), }); + const remove = useMutation({ + mutationFn: (rolename: string) => api.delete(`/mqtt/roles/${rolename}`), + onSuccess: (_data, rolename) => { + // The broker's deleteRole removes the role from every client and + // group that holds it; refresh those lists so chips disappear. + queryClient.invalidateQueries({ queryKey: ["roles"] }); + queryClient.invalidateQueries({ queryKey: ["clients"] }); + queryClient.invalidateQueries({ queryKey: ["groups"] }); + setBanner( + `Role ${rolename} deleted and removed from all clients and groups.`, + ); + setError(null); + // Close the editor if the deleted role was open — the detail query + // would otherwise 404 against a role that no longer exists. + if (selected === rolename) { + setSelected(null); + setDirty(false); + } + }, + onError: (e: Error) => setError(e.message), + }); + const submit = (e: FormEvent) => { e.preventDefault(); if (name) create.mutate(name); @@ -176,7 +198,21 @@ export function RolesPage() { {r.rolename} - + {" "} + ))}