"""Verify direct connections, write completion, and disconnect handling.""" import socket import types import unittest from unittest.mock import AsyncMock, MagicMock, patch from controller.transport import RfcommStream, open_stream class TransportTests(unittest.TestCase): def setUp(self): self.mock_socket = MagicMock() # Replace this adapter's socket module reference rather than globally # mocking socket.socket, which also breaks asyncio's internal sockets # (particularly Windows' Proactor event loop). isolated_socket = types.SimpleNamespace( socket=self.mock_socket, AF_BLUETOOTH=32, BTPROTO_RFCOMM=3, SOCK_STREAM=socket.SOCK_STREAM, timeout=TimeoutError, ) replacement = patch("controller.transport.socket", isolated_socket) replacement.start() self.addCleanup(replacement.stop) self.peer = self.mock_socket.return_value def test_direct_address_bypasses_serial_and_closes(self): with patch.dict("sys.modules", {"serial": None}): with open_stream("12:34:56:78:9A:BC", 1) as stream: self.assertEqual(stream.write(b"packet"), 6) self.peer.connect.assert_called_once_with(("12:34:56:78:9A:BC", 1)) self.peer.sendall.assert_called_once_with(b"packet") self.peer.close.assert_called_once() def test_idle_is_not_disconnect(self): with RfcommStream("12:34:56:78:9A:BC") as stream: self.peer.recv.side_effect = TimeoutError() self.assertEqual(stream.read(1024), b"") self.peer.recv.side_effect = None self.peer.recv.return_value = b"" with self.assertRaises(ConnectionError): stream.read(1024) def test_connect_failure_closes_socket(self): self.peer.connect.side_effect = OSError("connection refused") with self.assertRaises(OSError): with RfcommStream("12:34:56:78:9A:BC"): self.fail("Failed connection entered context") self.peer.close.assert_called_once() def test_connect_timeout_identifies_phase_and_closes(self): self.peer.connect.side_effect = TimeoutError("timed out") with self.assertRaisesRegex(TimeoutError, "before the Samsung handshake"): with RfcommStream("12:34:56:78:9A:BC"): self.fail("Timed out connection entered context") self.peer.close.assert_called_once() def test_write_timeout_identifies_phase(self): with RfcommStream("12:34:56:78:9A:BC") as stream: self.peer.sendall.side_effect = TimeoutError("timed out") with self.assertRaisesRegex(TimeoutError, "sending a Samsung packet"): stream.write(b"packet") def test_prepared_connection_retains_windows_handle(self): device = MagicMock() prepare = AsyncMock(return_value=device) report = MagicMock() with patch("controller.windows_bluetooth.prepare_device", prepare): with open_stream("12:34:56:78:9A:BC", 1, prepare=True, report=report): device.close.assert_not_called() prepare.assert_awaited_once_with("12:34:56:78:9A:BC", report) self.peer.connect.assert_called_once_with(("12:34:56:78:9A:BC", 1)) device.close.assert_called_once() def test_prepared_connect_failure_releases_both_handles(self): device = MagicMock() # The base adapter's socket cleanup is covered separately. Mock its # entry here so asyncio's own internal sockets do not affect counts. with ( patch("controller.windows_bluetooth.prepare_device", AsyncMock(return_value=device)), patch.object(RfcommStream, "__enter__", side_effect=OSError("host down")), ): with self.assertRaises(OSError): with open_stream("12:34:56:78:9A:BC", 1, prepare=True): self.fail("Failed connection entered context") device.close.assert_called_once()