"""Tests for the KiwiSDR WebSocket audio client.""" import struct from unittest.mock import MagicMock, patch from utils.kiwisdr import ( KIWI_DEFAULT_PORT, KIWI_SAMPLE_RATE, KIWI_SND_HEADER_SIZE, MODE_FILTERS, VALID_MODES, KiwiSDRClient, parse_host_port, ) # ============================================ # parse_host_port tests # ============================================ def test_parse_host_port_basic(): """Should parse host:port from a simple URL.""" assert parse_host_port("http://kiwi.example.com:8073") == ("kiwi.example.com", 8073) def test_parse_host_port_no_port(): """Should default to 8073 when port is missing.""" assert parse_host_port("http://kiwi.example.com") == ("kiwi.example.com", KIWI_DEFAULT_PORT) def test_parse_host_port_https(): """Should strip https:// prefix.""" assert parse_host_port("https://secure.kiwi.com:9090") == ("secure.kiwi.com", 9090) def test_parse_host_port_ws(): """Should strip ws:// prefix.""" assert parse_host_port("ws://kiwi.local:8074") == ("kiwi.local", 8074) def test_parse_host_port_with_path(): """Should strip trailing path from URL.""" assert parse_host_port("http://kiwi.com:8073/some/path") == ("kiwi.com", 8073) def test_parse_host_port_bare_host(): """Should handle bare hostname without protocol.""" assert parse_host_port("kiwi.local") == ("kiwi.local", KIWI_DEFAULT_PORT) def test_parse_host_port_bare_host_with_port(): """Should handle bare hostname with port.""" assert parse_host_port("kiwi.local:8074") == ("kiwi.local", 8074) def test_parse_host_port_empty(): """Should handle empty/None input.""" assert parse_host_port("") == ("", KIWI_DEFAULT_PORT) def test_parse_host_port_invalid_port(): """Should default port for non-numeric port.""" assert parse_host_port("http://kiwi.com:abc") == ("kiwi.com", KIWI_DEFAULT_PORT) # ============================================ # SND frame parsing tests # ============================================ def _make_snd_frame(smeter_raw: int, pcm_samples: list[int]) -> bytes: """Build a mock KiwiSDR SND binary frame.""" header = b"SND" # 3 bytes: magic header += b"\x00" # 1 byte: flags header += struct.pack(">I", 42) # 4 bytes: sequence number header += struct.pack(">h", smeter_raw) # 2 bytes: S-meter # PCM data: 16-bit signed LE pcm = b"".join(struct.pack(" 0 assert high > low def test_mode_filter_lsb_negative(): """LSB filter should be in negative passband.""" low, high = MODE_FILTERS["lsb"] assert low < 0 assert high < 0 # ============================================ # Connection tests with mocked WebSocket # ============================================ @patch("utils.kiwisdr.WEBSOCKET_CLIENT_AVAILABLE", True) @patch("utils.kiwisdr.websocket") def test_client_connect_success(mock_ws_module): """Connect should establish a WebSocket connection.""" mock_ws = MagicMock() mock_ws_module.WebSocket.return_value = mock_ws client = KiwiSDRClient(host="kiwi.local", port=8073) result = client.connect(7000, "am") assert result is True assert client.connected is True assert client.frequency_khz == 7000 assert client.mode == "am" # Verify WebSocket was created and connected mock_ws_module.WebSocket.assert_called_once() mock_ws.connect.assert_called_once() # Verify protocol messages were sent calls = [str(c) for c in mock_ws.send.call_args_list] auth_sent = any("SET auth" in c for c in calls) compression_sent = any("SET compression=0" in c for c in calls) mod_sent = any("SET mod=am" in c and "freq=7000" in c for c in calls) assert auth_sent, "Auth message not sent" assert compression_sent, "Compression message not sent" assert mod_sent, "Tune message not sent" # Cleanup client.disconnect() @patch("utils.kiwisdr.WEBSOCKET_CLIENT_AVAILABLE", True) @patch("utils.kiwisdr.websocket") def test_client_connect_failure(mock_ws_module): """Connect should handle connection failures.""" mock_ws = MagicMock() mock_ws.connect.side_effect = ConnectionRefusedError("Connection refused") mock_ws_module.WebSocket.return_value = mock_ws client = KiwiSDRClient(host="unreachable.local", port=8073) result = client.connect(7000, "am") assert result is False assert client.connected is False @patch("utils.kiwisdr.WEBSOCKET_CLIENT_AVAILABLE", True) @patch("utils.kiwisdr.websocket") def test_client_tune_success(mock_ws_module): """Tune should send the correct SET mod command.""" mock_ws = MagicMock() mock_ws_module.WebSocket.return_value = mock_ws client = KiwiSDRClient(host="kiwi.local", port=8073) client.connect(7000, "am") mock_ws.send.reset_mock() result = client.tune(14000, "usb") assert result is True assert client.frequency_khz == 14000 assert client.mode == "usb" tune_calls = [str(c) for c in mock_ws.send.call_args_list] assert any("SET mod=usb" in c and "freq=14000" in c for c in tune_calls) client.disconnect() @patch("utils.kiwisdr.WEBSOCKET_CLIENT_AVAILABLE", True) @patch("utils.kiwisdr.websocket") def test_client_invalid_mode_fallback(mock_ws_module): """Connect with invalid mode should fall back to AM.""" mock_ws = MagicMock() mock_ws_module.WebSocket.return_value = mock_ws client = KiwiSDRClient(host="kiwi.local", port=8073) client.connect(7000, "invalid_mode") assert client.mode == "am" client.disconnect() @patch("utils.kiwisdr.WEBSOCKET_CLIENT_AVAILABLE", True) @patch("utils.kiwisdr.websocket") def test_client_ws_url_format(mock_ws_module): """WebSocket URL should follow KiwiSDR format.""" mock_ws = MagicMock() mock_ws_module.WebSocket.return_value = mock_ws client = KiwiSDRClient(host="test.kiwi.com", port=8074) client.connect(7000, "am") ws_url = mock_ws.connect.call_args[0][0] assert ws_url.startswith("ws://test.kiwi.com:8074/") assert ws_url.endswith("/SND") client.disconnect()