Raise a clear client error for a refused request

socket_comms now raises DaemonError with the server message if the reply has the status ERROR. Before, the reply reached code that expects device data, and that code raised KeyError.
This commit is contained in:
tim
2026-10-10 10:59:35 +02:00
parent 0d5a080425
commit 9ab1d909ed
3 changed files with 50 additions and 0 deletions
+13
View File
@@ -45,6 +45,10 @@ def handle_registration(socket_path: str, client_name: str, debug: int = 0) -> d
return data return data
class DaemonError(RuntimeError):
"""The daemon answered, but it refused the request."""
def socket_comms(socket_path: str, request_data: dict[str, Any], debug: int = 0) -> dict[str, Any]: def socket_comms(socket_path: str, request_data: dict[str, Any], debug: int = 0) -> dict[str, Any]:
"""Send a JSON request to the daemon socket and return the decoded JSON response.""" """Send a JSON request to the daemon socket and return the decoded JSON response."""
response_data: dict[str, Any] = dict() response_data: dict[str, Any] = dict()
@@ -111,6 +115,15 @@ def socket_comms(socket_path: str, request_data: dict[str, Any], debug: int = 0)
if debug > 2: if debug > 2:
debugger("socket client: received {!r}".format(response_data)) debugger("socket client: received {!r}".format(response_data))
# For example, a daemon that is older than this client and does not know
# the command.
if isinstance(response_data, dict) and response_data.get("status") == "ERROR":
raise DaemonError(
"daemon refused {!r}: {}".format(
request_data.get("command"), response_data.get("message")
)
)
return response_data return response_data
+22
View File
@@ -91,6 +91,28 @@ class TestReadData:
call_args = mock_comms.call_args[0][1] call_args = mock_comms.call_args[0][1]
assert call_args.get("ups") == "myups" assert call_args.get("ups") == "myups"
@pytest.mark.parametrize("call", [
lambda client: client.list_devices("/fake/socket", "test"),
lambda client: client.read_data("/fake/socket", "test"),
], ids=["list_devices", "read_data"])
def test_error_reply_raises_the_daemon_message(self, call):
"""An ERROR reply must not reach the code that expects device data."""
from bmspy import client
reply = json.dumps({"status": "ERROR", "message": "unknown command: 'DEVICES'"}).encode()
sock = MagicMock()
sock.recv.side_effect = [struct.pack("!I", len(reply)), reply]
with patch("bmspy.client.socket.socket", return_value=sock):
with pytest.raises(client.DaemonError, match="unknown command: 'DEVICES'"):
call(client)
sock.close.assert_called()
def test_registration_logs_an_error_reply(self, capsys):
from bmspy import client
with patch("bmspy.client.socket_comms", side_effect=client.DaemonError("daemon refused 'REGISTER': no")), \
patch("bmspy.client.is_registered", False):
client.handle_registration("/fake/socket", "test")
assert "daemon refused 'REGISTER': no" in capsys.readouterr().out
def test_list_devices_returns_configured_names(self): def test_list_devices_returns_configured_names(self):
from bmspy.client import list_devices from bmspy.client import list_devices
with patch("bmspy.client.socket_comms", return_value={"devices": ["network", "nas"]}) as mock_comms: with patch("bmspy.client.socket_comms", return_value={"devices": ["network", "nas"]}) as mock_comms:
+15
View File
@@ -507,6 +507,21 @@ class TestServerMain:
finally: finally:
self._stop_server(sock_path, t) self._stop_server(sock_path, t)
def test_unknown_command_is_a_clear_error_in_the_client(self, tmp_path):
"""The real client, against a server that does not know the command."""
from bmspy import client
sock_path = str(tmp_path / "server_refused.sock")
ready = threading.Event()
t = self._make_server_thread(sock_path, ready, threading.Event())
t.start()
ready.wait(timeout=2)
try:
with pytest.raises(client.DaemonError, match="daemon refused 'NEWER': unknown command: 'NEWER'"):
client.socket_comms(sock_path, {"command": "NEWER", "client": "test"})
finally:
self._stop_server(sock_path, t)
def test_devices_command_lists_configured_devices(self, tmp_path): def test_devices_command_lists_configured_devices(self, tmp_path):
"""DEVICES names every configured device, through the real client.""" """DEVICES names every configured device, through the real client."""
from bmspy import client from bmspy import client