Answer an invalid request with an error reply
An unknown command ended the server loop, so one bad request stopped the service for every client. The server now sends {"status": "ERROR", "message": ...} and continues. A request with no command, or one that is not a JSON object, gets the same reply.
The server tests stopped the server with an unknown command. They now stop it through accept().
This commit is contained in:
+13
-5
@@ -409,9 +409,12 @@ def main():
|
|||||||
print("socket ERROR: {}".format(e))
|
print("socket ERROR: {}".format(e))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
client = request_data["client"] or "unknown"
|
if not isinstance(request_data, dict):
|
||||||
|
request_data = {}
|
||||||
|
client = request_data.get("client") or "unknown"
|
||||||
|
command = request_data.get("command")
|
||||||
|
|
||||||
match request_data["command"]:
|
match command:
|
||||||
case "REGISTER":
|
case "REGISTER":
|
||||||
connected_clients.append(client)
|
connected_clients.append(client)
|
||||||
send_response(
|
send_response(
|
||||||
@@ -463,10 +466,15 @@ def main():
|
|||||||
send_response(connection, result, client, debug)
|
send_response(connection, result, client, debug)
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
debugger(
|
# Answer and continue: one bad request must not end the
|
||||||
"socket: invalid request from {}".format(request_data["client"])
|
# server for every other client.
|
||||||
|
debugger("socket: invalid request from {}".format(client))
|
||||||
|
send_response(
|
||||||
|
connection,
|
||||||
|
{"status": "ERROR", "message": "unknown command: {!r}".format(command)},
|
||||||
|
client,
|
||||||
|
debug,
|
||||||
)
|
)
|
||||||
break
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
if connection:
|
if connection:
|
||||||
|
|||||||
+43
-26
@@ -6,6 +6,8 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import serial
|
import serial
|
||||||
|
|
||||||
@@ -317,6 +319,22 @@ class TestReadRequestErrors:
|
|||||||
class TestServerMain:
|
class TestServerMain:
|
||||||
"""Test the server main() function by running it in a thread and sending real socket commands."""
|
"""Test the server main() function by running it in a thread and sending real socket commands."""
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _stoppable_accept(self):
|
||||||
|
"""Let a test end main(). The server itself has no command that stops it."""
|
||||||
|
self._stopping = stopping = threading.Event()
|
||||||
|
original_accept = socket_module.socket.accept
|
||||||
|
|
||||||
|
def accept_or_stop(sock):
|
||||||
|
connection, address = original_accept(sock)
|
||||||
|
if stopping.is_set():
|
||||||
|
connection.close()
|
||||||
|
raise KeyboardInterrupt
|
||||||
|
return connection, address
|
||||||
|
|
||||||
|
with patch.object(socket_module.socket, "accept", accept_or_stop):
|
||||||
|
yield
|
||||||
|
|
||||||
def _make_server_thread(self, sock_path: str, ready_event: threading.Event,
|
def _make_server_thread(self, sock_path: str, ready_event: threading.Event,
|
||||||
stop_event: threading.Event, **kwargs):
|
stop_event: threading.Event, **kwargs):
|
||||||
"""Run server main() in a thread with mocked serial and collect_data."""
|
"""Run server main() in a thread with mocked serial and collect_data."""
|
||||||
@@ -392,22 +410,25 @@ class TestServerMain:
|
|||||||
return json.loads(resp_data)
|
return json.loads(resp_data)
|
||||||
|
|
||||||
def _stop_server(self, sock_path: str, thread: threading.Thread) -> None:
|
def _stop_server(self, sock_path: str, thread: threading.Thread) -> None:
|
||||||
"""End main(), so that its thread and its patches do not outlive the test.
|
"""End main(), so that its threads and its patches do not outlive the test.
|
||||||
|
|
||||||
An invalid command breaks the server loop. No response follows.
|
One more connection wakes accept(), which then stops the server.
|
||||||
"""
|
"""
|
||||||
|
self._stopping.set()
|
||||||
if thread.is_alive():
|
if thread.is_alive():
|
||||||
sock = socket_module.socket(socket_module.AF_UNIX, socket_module.SOCK_STREAM)
|
sock = socket_module.socket(socket_module.AF_UNIX, socket_module.SOCK_STREAM)
|
||||||
try:
|
try:
|
||||||
sock.connect(sock_path)
|
sock.connect(sock_path)
|
||||||
payload = json.dumps({"command": "STOP", "client": "test"}).encode()
|
|
||||||
sock.sendall(struct.pack("!I", len(payload)) + payload)
|
|
||||||
except OSError:
|
except OSError:
|
||||||
pass # The server ended between the check and the connection.
|
pass # The server ended between the check and the connection.
|
||||||
finally:
|
finally:
|
||||||
sock.close()
|
sock.close()
|
||||||
thread.join(timeout=5)
|
thread.join(timeout=5)
|
||||||
assert not thread.is_alive()
|
assert not thread.is_alive()
|
||||||
|
for client_thread in threading.enumerate():
|
||||||
|
if client_thread.name == "client":
|
||||||
|
client_thread.join(timeout=5)
|
||||||
|
assert not client_thread.is_alive()
|
||||||
|
|
||||||
def test_register_command(self, tmp_path):
|
def test_register_command(self, tmp_path):
|
||||||
"""Test REGISTER command via server main()."""
|
"""Test REGISTER command via server main()."""
|
||||||
@@ -958,35 +979,31 @@ class TestServerMain:
|
|||||||
pass
|
pass
|
||||||
self._stop_server(sock_path, t)
|
self._stop_server(sock_path, t)
|
||||||
|
|
||||||
def test_invalid_command_breaks_loop(self, tmp_path, capsys):
|
@pytest.mark.parametrize("request_data,command", [
|
||||||
"""Test that an invalid command logs an error."""
|
({"command": "INVALID", "client": "test"}, "'INVALID'"),
|
||||||
|
({"client": "test"}, "None"),
|
||||||
|
(["GET"], "None"),
|
||||||
|
], ids=["unknown-command", "no-command", "not-an-object"])
|
||||||
|
def test_invalid_request_gets_an_error_and_the_server_continues(
|
||||||
|
self, tmp_path, capsys, request_data, command,
|
||||||
|
):
|
||||||
|
"""One bad request must not end the server for every other client."""
|
||||||
sock_path = str(tmp_path / "server_invalid.sock")
|
sock_path = str(tmp_path / "server_invalid.sock")
|
||||||
ready = threading.Event()
|
ready = threading.Event()
|
||||||
stop = threading.Event()
|
t = self._make_server_thread(sock_path, ready, threading.Event())
|
||||||
|
|
||||||
t = self._make_server_thread(sock_path, ready, stop)
|
|
||||||
t.start()
|
t.start()
|
||||||
ready.wait(timeout=2)
|
ready.wait(timeout=2)
|
||||||
import time as _time
|
|
||||||
_time.sleep(0.2)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Send invalid command - note: server breaks on invalid, so this may fail
|
assert self._send_command(sock_path, request_data) == {
|
||||||
import socket as _s, struct, json
|
"status": "ERROR", "message": "unknown command: " + command,
|
||||||
sock = _s.socket(_s.AF_UNIX, _s.SOCK_STREAM)
|
}
|
||||||
for _ in range(20):
|
assert t.is_alive()
|
||||||
try:
|
# The next client still gets its answer.
|
||||||
sock.connect(sock_path)
|
response = self._send_command(sock_path, {"command": "GET", "client": "test"})
|
||||||
break
|
assert "ttyUSB0" in response
|
||||||
except (OSError, ConnectionRefusedError):
|
|
||||||
_time.sleep(0.05)
|
|
||||||
payload = json.dumps({"command": "INVALID", "client": "test"}).encode()
|
|
||||||
sock.sendall(struct.pack("!I", len(payload)) + payload)
|
|
||||||
sock.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
finally:
|
finally:
|
||||||
self._stop_server(sock_path, t)
|
self._stop_server(sock_path, t)
|
||||||
|
assert "invalid request from" in capsys.readouterr().out
|
||||||
|
|
||||||
|
|
||||||
class TestSendResponseDebug:
|
class TestSendResponseDebug:
|
||||||
|
|||||||
Reference in New Issue
Block a user