Skip to content

Commit 4442bcb

Browse files
author
Kacper Zienkiewicz
committed
[#53543] server: tests: Add test for device stale connections
This is a regression test for stale device connections on the server after a device loses internet connection/dies without properly cleaning up the connection.
1 parent cfdf67c commit 4442bcb

1 file changed

Lines changed: 61 additions & 1 deletion

File tree

server/tests/test-server-ws.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
create_fake_device_token)
66
import simple_websocket
77
import pytest
8-
from rdfm.ws import receive_message, send_message
8+
from rdfm.ws import receive_message, send_message, decode_json, RDFM_WS_DUPLICATE_CONNECTION
99
from rdfm_mgmt_communication import CapabilityReport, DeviceAttachToManager
10+
import subprocess
11+
import signal
12+
from api.v1.middleware import WS_PING_INTERVAL
1013

1114

1215
FAKE_DEVICE_MAC = "00:00:00:00:00:00"
@@ -72,6 +75,20 @@ def spawn_shell_on_mock_device():
7275
pytest.fail(f"connecting to the shell WebSocket failed: {e}")
7376

7477

78+
def connect_interruptable_mock_device():
79+
""" Spawn a mock device that can be paused using SIGSTOP
80+
81+
To simulate a connection dropout we need a device mock that can be paused,
82+
without triggering the usual WebSocket disconnection mechanism. Calling
83+
client.close() on the above mocks would just gracefully shutdown the connection.
84+
"""
85+
process = subprocess.Popen(["python", "tests/scripts/device-websocket-loop.py"])
86+
# Hacky sleep to ensure the process starts
87+
time.sleep(5.0)
88+
89+
return process
90+
91+
7592
def test_ws_connect_device(process, connect_mock_device):
7693
""" This tests if the device WebSocket connection works properly
7794
@@ -161,3 +178,46 @@ def test_ws_shell_spawn_on_nonexistent_device(process):
161178
pytest.fail("shell connection did not close, despite the device not existing")
162179

163180
assert "not connected" in client.close_message, "close message should indicate the device does not exist"
181+
182+
183+
def test_ws_stale_connections_are_disconnected(process: subprocess.Popen):
184+
""" This tests if devices that have lost connection to the server get properly
185+
disconnected from the server.
186+
187+
This is important, as only one connection is allowed per device, and stale
188+
connections would prevent a device from accessing the device WS again.
189+
"""
190+
mock = connect_interruptable_mock_device()
191+
assert mock.poll() is None, "the mock device connected successfully"
192+
193+
# Send SIGSTOP to pause the mock
194+
mock.send_signal(signal.SIGSTOP)
195+
time.sleep(5.0)
196+
197+
# Sleep until the ping interval expires, at which point the server should
198+
# disconnect the device. The timeout occurs after two intervals (one during
199+
# which the Ping is sent, and after the other the connection is closed as
200+
# no Pong was received).
201+
time.sleep(WS_PING_INTERVAL * 2)
202+
203+
# Resume the mock
204+
mock.send_signal(signal.SIGCONT)
205+
time.sleep(5.0)
206+
207+
# The mock should now be disconnected from the server
208+
assert mock.poll() is not None, "the mock device has closed after timing out"
209+
210+
# Now reconnect to the server
211+
# Don't use the mock, as we need to check the closure status
212+
# If a stale connection is present, the connection will immediately
213+
# close with `4002: duplicate connections not allowed`
214+
try:
215+
client = simple_websocket.Client.connect(DEVICES_WS, headers={
216+
"Authorization": f"Bearer token={create_fake_device_token()}",
217+
})
218+
_ = client.receive()
219+
client.close()
220+
except simple_websocket.ConnectionClosed as e:
221+
if e.reason == RDFM_WS_DUPLICATE_CONNECTION:
222+
pytest.fail("REGRESSION: Device connection was not properly cleaned up")
223+
raise

0 commit comments

Comments
 (0)