G4: F17 — re-raise BLE pairing failure instead of swallowing

Why: When BLE pairing failed during connect(), the exception was caught
as a warning and connect() continued normally. This left the transport
in a half-usable state — the BLE link was up but the pairing handshake
never completed, so encrypted characteristics could silently fail.
Now the pairing exception disconnects the client and re-raises, giving
the caller a clean failure. Updated the pre-existing
test_ble_connection_with_pin_failed_pairing test to assert the re-raise
behavior instead of the old swallow-and-continue behavior.
Refs: Forensics report finding F17
This commit is contained in:
Matthew Wolter 2026-04-11 20:25:04 -07:00
parent fe0dcac90f
commit 76e2e54157
2 changed files with 15 additions and 13 deletions

View file

@ -116,9 +116,12 @@ class BLEConnection:
await self.client.pair()
logger.info("BLE pairing successful")
except Exception as e:
logger.warning(f"BLE pairing failed: {e}")
# Don't fail the connection if pairing fails, as the device
# might already be paired or not require pairing
logger.error(f"BLE pairing failed: {e}")
# A failed pairing leaves the transport in a half-usable
# state — re-raise so the caller gets a clean failure
# instead of a silently degraded connection.
await self.client.disconnect()
raise
except BleakDeviceNotFoundError:
return None

View file

@ -37,7 +37,7 @@ class TestBLEPinPairing(unittest.TestCase):
@patch("meshcore.ble_cx.BleakClient")
def test_ble_connection_with_pin_failed_pairing(self, mock_bleak_client):
"""Test BLE connection with PIN when pairing fails but connection continues"""
"""Test BLE connection with PIN when pairing fails — re-raises (F17)."""
# Arrange
mock_client_instance = self._get_mock_bleak_client()
mock_client_instance.pair = AsyncMock(side_effect=Exception("Pairing failed"))
@ -47,17 +47,16 @@ class TestBLEPinPairing(unittest.TestCase):
pin = "123456"
ble_conn = BLEConnection(address=address, pin=pin)
# Act
result = asyncio.run(ble_conn.connect())
# Assert
# Act & Assert — pairing failure now re-raises instead of being
# swallowed, because a half-usable transport is worse than a clean
# failure (forensics finding F17).
with self.assertRaises(Exception) as ctx:
asyncio.run(ble_conn.connect())
self.assertIn("Pairing failed", str(ctx.exception))
mock_client_instance.connect.assert_called_once()
mock_client_instance.pair.assert_called_once()
mock_client_instance.start_notify.assert_called_once_with(
UART_TX_CHAR_UUID, ble_conn.handle_rx
)
# Connection should still succeed even if pairing fails
self.assertEqual(result, address)
# disconnect should be called to clean up the failed connection
mock_client_instance.disconnect.assert_called_once()
@patch("meshcore.ble_cx.BleakClient")
def test_ble_connection_without_pin_no_pairing(self, mock_bleak_client):