"""Tests for SubGHz transceiver routes.""" from __future__ import annotations from unittest.mock import MagicMock, patch import pytest from utils.subghz import SubGhzCapture @pytest.fixture def auth_client(client): """Client with logged-in session.""" with client.session_transaction() as sess: sess['logged_in'] = True return client class TestSubGhzRoutes: """Tests for /subghz/ endpoints.""" def test_get_status(self, client, auth_client): """GET /subghz/status returns manager status.""" with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.get_status.return_value = { 'mode': 'idle', 'hackrf_available': True, 'rtl433_available': True, 'sweep_available': True, } mock_get.return_value = mock_mgr response = auth_client.get('/subghz/status') assert response.status_code == 200 data = response.get_json() assert data['mode'] == 'idle' assert data['hackrf_available'] is True def test_get_presets(self, client, auth_client): """GET /subghz/presets returns frequency presets.""" response = auth_client.get('/subghz/presets') assert response.status_code == 200 data = response.get_json() assert 'presets' in data assert '433.92 MHz' in data['presets'] assert 'sample_rates' in data # ------ RECEIVE ------ def test_start_receive_success(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.start_receive.return_value = { 'status': 'started', 'frequency_hz': 433920000, 'sample_rate': 2000000, } mock_get.return_value = mock_mgr response = auth_client.post('/subghz/receive/start', json={ 'frequency_hz': 433920000, 'sample_rate': 2000000, 'lna_gain': 32, 'vga_gain': 20, }) assert response.status_code == 200 data = response.get_json() assert data['status'] == 'started' def test_start_receive_missing_frequency(self, client, auth_client): response = auth_client.post('/subghz/receive/start', json={}) assert response.status_code == 400 data = response.get_json() assert data['status'] == 'error' def test_start_receive_invalid_frequency(self, client, auth_client): response = auth_client.post('/subghz/receive/start', json={ 'frequency_hz': 'not_a_number', }) assert response.status_code == 400 def test_stop_receive(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.stop_receive.return_value = {'status': 'stopped'} mock_get.return_value = mock_mgr response = auth_client.post('/subghz/receive/stop') assert response.status_code == 200 def test_start_receive_trigger_params(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.start_receive.return_value = {'status': 'started'} mock_get.return_value = mock_mgr response = auth_client.post('/subghz/receive/start', json={ 'frequency_hz': 433920000, 'trigger_enabled': True, 'trigger_pre_ms': 400, 'trigger_post_ms': 900, }) assert response.status_code == 200 kwargs = mock_mgr.start_receive.call_args.kwargs assert kwargs['trigger_enabled'] is True assert kwargs['trigger_pre_ms'] == 400 assert kwargs['trigger_post_ms'] == 900 # ------ DECODE ------ def test_start_decode_success(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.start_decode.return_value = { 'status': 'started', 'frequency_hz': 433920000, } mock_get.return_value = mock_mgr response = auth_client.post('/subghz/decode/start', json={ 'frequency_hz': 433920000, }) assert response.status_code == 200 data = response.get_json() assert data['status'] == 'started' mock_mgr.start_decode.assert_called_once() kwargs = mock_mgr.start_decode.call_args.kwargs assert kwargs['decode_profile'] == 'weather' def test_start_decode_profile_all(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.start_decode.return_value = { 'status': 'started', 'frequency_hz': 433920000, } mock_get.return_value = mock_mgr response = auth_client.post('/subghz/decode/start', json={ 'frequency_hz': 433920000, 'decode_profile': 'all', }) assert response.status_code == 200 kwargs = mock_mgr.start_decode.call_args.kwargs assert kwargs['decode_profile'] == 'all' def test_start_decode_missing_freq(self, client, auth_client): response = auth_client.post('/subghz/decode/start', json={}) assert response.status_code == 400 def test_stop_decode(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.stop_decode.return_value = {'status': 'stopped'} mock_get.return_value = mock_mgr response = auth_client.post('/subghz/decode/stop') assert response.status_code == 200 # ------ TRANSMIT ------ def test_transmit_missing_capture_id(self, client, auth_client): response = auth_client.post('/subghz/transmit', json={}) assert response.status_code == 400 data = response.get_json() assert 'capture_id is required' in data['message'] def test_transmit_invalid_capture_id(self, client, auth_client): response = auth_client.post('/subghz/transmit', json={ 'capture_id': '../../../etc/passwd', }) assert response.status_code == 400 data = response.get_json() assert 'Invalid' in data['message'] def test_transmit_success(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.transmit.return_value = { 'status': 'transmitting', 'capture_id': 'abc123', 'frequency_hz': 433920000, 'max_duration': 10, } mock_get.return_value = mock_mgr response = auth_client.post('/subghz/transmit', json={ 'capture_id': 'abc123', 'tx_gain': 20, 'max_duration': 10, }) assert response.status_code == 200 data = response.get_json() assert data['status'] == 'transmitting' kwargs = mock_mgr.transmit.call_args.kwargs assert kwargs['start_seconds'] is None assert kwargs['duration_seconds'] is None def test_transmit_segment_params(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.transmit.return_value = { 'status': 'transmitting', 'capture_id': 'abc123', 'frequency_hz': 433920000, 'max_duration': 10, 'segment': {'start_seconds': 0.1, 'duration_seconds': 0.4}, } mock_get.return_value = mock_mgr response = auth_client.post('/subghz/transmit', json={ 'capture_id': 'abc123', 'tx_gain': 20, 'max_duration': 10, 'start_seconds': 0.1, 'duration_seconds': 0.4, }) assert response.status_code == 200 kwargs = mock_mgr.transmit.call_args.kwargs assert kwargs['start_seconds'] == 0.1 assert kwargs['duration_seconds'] == 0.4 def test_transmit_invalid_segment_param(self, client, auth_client): response = auth_client.post('/subghz/transmit', json={ 'capture_id': 'abc123', 'start_seconds': 'not-a-number', }) assert response.status_code == 400 def test_stop_transmit(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.stop_transmit.return_value = {'status': 'stopped'} mock_get.return_value = mock_mgr response = auth_client.post('/subghz/transmit/stop') assert response.status_code == 200 # ------ SWEEP ------ def test_start_sweep_success(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.start_sweep.return_value = { 'status': 'started', 'freq_start_mhz': 300, 'freq_end_mhz': 928, } mock_get.return_value = mock_mgr response = auth_client.post('/subghz/sweep/start', json={ 'freq_start_mhz': 300, 'freq_end_mhz': 928, }) assert response.status_code == 200 data = response.get_json() assert data['status'] == 'started' def test_start_sweep_invalid_range(self, client, auth_client): response = auth_client.post('/subghz/sweep/start', json={ 'freq_start_mhz': 928, 'freq_end_mhz': 300, # start > end }) assert response.status_code == 400 def test_stop_sweep(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.stop_sweep.return_value = {'status': 'stopped'} mock_get.return_value = mock_mgr response = auth_client.post('/subghz/sweep/stop') assert response.status_code == 200 # ------ CAPTURES ------ def test_list_captures_empty(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.list_captures.return_value = [] mock_get.return_value = mock_mgr response = auth_client.get('/subghz/captures') assert response.status_code == 200 data = response.get_json() assert data['count'] == 0 assert data['captures'] == [] def test_list_captures_with_data(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() cap = SubGhzCapture( capture_id='cap1', filename='test.iq', frequency_hz=433920000, sample_rate=2000000, lna_gain=32, vga_gain=20, timestamp='2026-01-01T00:00:00Z', ) mock_mgr.list_captures.return_value = [cap] mock_get.return_value = mock_mgr response = auth_client.get('/subghz/captures') assert response.status_code == 200 data = response.get_json() assert data['count'] == 1 assert data['captures'][0]['id'] == 'cap1' def test_get_capture(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() cap = SubGhzCapture( capture_id='cap2', filename='test2.iq', frequency_hz=315000000, sample_rate=2000000, lna_gain=32, vga_gain=20, timestamp='2026-01-01T00:00:00Z', ) mock_mgr.get_capture.return_value = cap mock_get.return_value = mock_mgr response = auth_client.get('/subghz/captures/cap2') assert response.status_code == 200 data = response.get_json() assert data['capture']['frequency_hz'] == 315000000 def test_get_capture_not_found(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.get_capture.return_value = None mock_get.return_value = mock_mgr response = auth_client.get('/subghz/captures/nonexistent') assert response.status_code == 404 def test_get_capture_invalid_id(self, client, auth_client): response = auth_client.get('/subghz/captures/bad-id!') assert response.status_code == 400 def test_delete_capture(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.delete_capture.return_value = True mock_get.return_value = mock_mgr response = auth_client.delete('/subghz/captures/cap1') assert response.status_code == 200 data = response.get_json() assert data['status'] == 'deleted' def test_trim_capture_success(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.trim_capture.return_value = { 'status': 'ok', 'capture': { 'id': 'trim_new', 'filename': 'trimmed.iq', 'frequency_hz': 433920000, 'sample_rate': 2000000, }, } mock_get.return_value = mock_mgr response = auth_client.post('/subghz/captures/cap1/trim', json={ 'start_seconds': 0.1, 'duration_seconds': 0.3, }) assert response.status_code == 200 kwargs = mock_mgr.trim_capture.call_args.kwargs assert kwargs['capture_id'] == 'cap1' assert kwargs['start_seconds'] == 0.1 assert kwargs['duration_seconds'] == 0.3 def test_trim_capture_invalid_param(self, client, auth_client): response = auth_client.post('/subghz/captures/cap1/trim', json={ 'start_seconds': 'bad', }) assert response.status_code == 400 def test_delete_capture_not_found(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.delete_capture.return_value = False mock_get.return_value = mock_mgr response = auth_client.delete('/subghz/captures/nonexistent') assert response.status_code == 404 def test_update_capture_label(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.update_capture_label.return_value = True mock_get.return_value = mock_mgr response = auth_client.patch('/subghz/captures/cap1', json={ 'label': 'Garage Remote', }) assert response.status_code == 200 data = response.get_json() assert data['label'] == 'Garage Remote' def test_update_capture_label_too_long(self, client, auth_client): response = auth_client.patch('/subghz/captures/cap1', json={ 'label': 'x' * 200, }) assert response.status_code == 400 def test_update_capture_not_found(self, client, auth_client): with patch('routes.subghz.get_subghz_manager') as mock_get: mock_mgr = MagicMock() mock_mgr.update_capture_label.return_value = False mock_get.return_value = mock_mgr response = auth_client.patch('/subghz/captures/nonexistent', json={ 'label': 'test', }) assert response.status_code == 404 # ------ SSE STREAM ------ def test_stream_endpoint(self, client, auth_client): """GET /subghz/stream returns SSE response.""" with patch('routes.subghz.sse_stream', return_value=iter([])): response = auth_client.get('/subghz/stream') assert response.status_code == 200 assert response.content_type.startswith('text/event-stream')