"""Tests for the KiwiSDR WebSocket audio client.""" import struct from unittest.mock import patch, MagicMock import pytest from utils.kiwisdr import ( KiwiSDRClient, KIWI_SAMPLE_RATE, KIWI_SND_HEADER_SIZE, KIWI_DEFAULT_PORT, MODE_FILTERS, VALID_MODES, 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()