"""Discovery fixtures include the owner's actual unnamed FE40 advertisement.""" import asyncio import queue import sys import types import unittest from unittest.mock import AsyncMock, patch from controller.discovery import ScanWorker, Speaker, scan_speakers, selected_address def advertisement(name, address, uuids=()): return ( types.SimpleNamespace(name=name, address=address), types.SimpleNamespace(local_name=name, service_uuids=list(uuids)), ) class DiscoveryTests(unittest.TestCase): def test_unnamed_model_advertisement_and_named_speaker_are_selectable(self): found = { "unnamed": advertisement( None, "12:34:56:78:9A:BC", ["0000fe40-0000-1000-8000-00805f9b34fb"] ), "named": advertisement("[AV] MX-T40", "12:34:56:78:9A:BD"), "unrelated": advertisement( "Headphones", "12:34:56:78:9A:BE", ["0000ffa0-0000-1000-8000-00805f9b34fb"] ), } discover = AsyncMock(return_value=found) with patch.dict( sys.modules, {"bleak": types.SimpleNamespace(BleakScanner=types.SimpleNamespace(discover=discover))}, ): speakers = asyncio.run(scan_speakers(timeout=3)) self.assertEqual(len(speakers), 2) self.assertEqual( {speaker.address for speaker in speakers}, {"12:34:56:78:9A:BC", "12:34:56:78:9A:BD"} ) unnamed = next(speaker for speaker in speakers if speaker.address.endswith("BC")) self.assertEqual(unnamed.name, "MX-T40 (model advertisement)") self.assertIn(unnamed.address, unnamed.label) discover.assert_awaited_once_with(timeout=3, return_adv=True) def test_choice_resolves_to_address_and_preserves_manual_input(self): speaker = Speaker("12:34:56:78:9A:BC", "MX-T40") self.assertEqual(selected_address(speaker.label, [speaker]), speaker.address) self.assertEqual(selected_address(" 12:34:56:78:9A:BD ", [speaker]), "12:34:56:78:9A:BD") def test_identical_names_remain_distinguishable(self): speakers = [Speaker("12:34:56:78:9A:BC", "MX-T40"), Speaker("12:34:56:78:9A:BD", "MX-T40")] self.assertNotEqual(speakers[0].label, speakers[1].label) self.assertEqual(selected_address(speakers[1].label, speakers), speakers[1].address) def test_scan_error_is_delivered_to_gui_queue(self): events = queue.Queue() with patch( "controller.discovery.scan_speakers", AsyncMock(side_effect=RuntimeError("Bluetooth is off")), ): ScanWorker(events).run() self.assertEqual(events.get_nowait(), ("scan_error", "Bluetooth is off")) def test_empty_scan_has_a_result_event(self): events = queue.Queue() with patch("controller.discovery.scan_speakers", AsyncMock(return_value=[])): ScanWorker(events).run() self.assertEqual(events.get_nowait(), ("scan_results", []))