"""Tests for Flask routes and API endpoints.""" import json from unittest.mock import MagicMock, patch import pytest @pytest.fixture(scope='session') def app(): """Create application for testing.""" import app as app_module from routes import register_blueprints from utils.database import init_db app_module.app.config['TESTING'] = True # Initialize database for settings tests init_db() # Register blueprints only if not already registered (normally done in main()) # Check if any blueprint is already registered to avoid re-registration if 'pager' not in app_module.app.blueprints: register_blueprints(app_module.app) return app_module.app @pytest.fixture def client(app): """Create test client.""" return app.test_client() class TestHealthEndpoint: """Tests for health check endpoint.""" def test_health_check(self, client): """Test health endpoint returns expected data.""" response = client.get('/health') assert response.status_code == 200 data = json.loads(response.data) assert data['status'] == 'healthy' assert 'version' in data assert 'uptime_seconds' in data assert 'processes' in data assert 'data' in data def test_health_process_status(self, client): """Test health endpoint reports process status.""" response = client.get('/health') data = json.loads(response.data) processes = data['processes'] assert 'pager' in processes assert 'sensor' in processes assert 'adsb' in processes assert 'wifi' in processes assert 'bluetooth' in processes class TestDevicesEndpoint: """Tests for devices endpoint.""" def test_get_devices(self, client): """Test getting device list.""" response = client.get('/devices') assert response.status_code == 200 data = json.loads(response.data) assert isinstance(data, list) @patch('app.SDRFactory.detect_devices') def test_devices_returns_list(self, mock_detect, client): """Test devices endpoint returns list format.""" mock_device = MagicMock() mock_device.to_dict.return_value = { 'index': 0, 'name': 'Test RTL-SDR', 'sdr_type': 'rtlsdr' } mock_detect.return_value = [mock_device] response = client.get('/devices') data = json.loads(response.data) assert len(data) == 1 assert data[0]['name'] == 'Test RTL-SDR' class TestDependenciesEndpoint: """Tests for dependencies endpoint.""" def test_get_dependencies(self, client): """Test getting dependency status.""" response = client.get('/dependencies') assert response.status_code == 200 data = json.loads(response.data) assert data['status'] == 'success' assert 'os' in data assert 'pkg_manager' in data assert 'modes' in data class TestSettingsEndpoints: """Tests for settings API endpoints.""" def test_get_settings(self, client): """Test getting all settings.""" response = client.get('/settings') assert response.status_code == 200 data = json.loads(response.data) assert data['status'] == 'success' assert 'settings' in data def test_save_settings(self, client): """Test saving settings.""" response = client.post( '/settings', data=json.dumps({'test_key': 'test_value'}), content_type='application/json' ) assert response.status_code == 200 data = json.loads(response.data) assert data['status'] == 'success' assert 'test_key' in data['saved'] def test_save_empty_settings(self, client): """Test saving empty settings returns error.""" response = client.post( '/settings', data=json.dumps({}), content_type='application/json' ) assert response.status_code == 400 def test_get_single_setting(self, client): """Test getting a single setting.""" # First save a setting client.post( '/settings', data=json.dumps({'my_setting': 'my_value'}), content_type='application/json' ) # Then retrieve it response = client.get('/settings/my_setting') assert response.status_code == 200 data = json.loads(response.data) assert data['status'] == 'success' assert data['value'] == 'my_value' def test_get_nonexistent_setting(self, client): """Test getting a setting that doesn't exist.""" response = client.get('/settings/nonexistent_key_xyz') assert response.status_code == 404 def test_update_setting(self, client): """Test updating a setting via PUT.""" response = client.put( '/settings/update_test', data=json.dumps({'value': 'updated_value'}), content_type='application/json' ) assert response.status_code == 200 data = json.loads(response.data) assert data['status'] == 'success' assert data['value'] == 'updated_value' def test_delete_setting(self, client): """Test deleting a setting.""" # First create a setting client.post( '/settings', data=json.dumps({'delete_me': 'value'}), content_type='application/json' ) # Then delete it response = client.delete('/settings/delete_me') assert response.status_code == 200 data = json.loads(response.data) assert data['status'] == 'success' assert data['deleted'] is True def test_save_observer_location_updates_env_and_runtime_defaults(self, client, monkeypatch, tmp_path): """Saving observer location should persist to .env and update in-memory defaults.""" import app as app_module import config from routes import adsb as adsb_routes from routes import ais as ais_routes from routes import settings as settings_routes with client.session_transaction() as sess: sess['logged_in'] = True env_path = tmp_path / '.env' monkeypatch.setattr(settings_routes, '_get_env_file_path', lambda: env_path) response = client.post( '/settings/observer-location', data=json.dumps({'lat': 48.0, 'lon': 16.16}), content_type='application/json' ) assert response.status_code == 200 data = json.loads(response.data) assert data['status'] == 'success' assert data['lat'] == 48.0 assert data['lon'] == 16.16 env_text = env_path.read_text() assert 'INTERCEPT_DEFAULT_LAT=48.0' in env_text assert 'INTERCEPT_DEFAULT_LON=16.16' in env_text assert config.DEFAULT_LATITUDE == 48.0 assert config.DEFAULT_LONGITUDE == 16.16 assert app_module.DEFAULT_LATITUDE == 48.0 assert app_module.DEFAULT_LONGITUDE == 16.16 assert adsb_routes.DEFAULT_LATITUDE == 48.0 assert adsb_routes.DEFAULT_LONGITUDE == 16.16 assert ais_routes.DEFAULT_LATITUDE == 48.0 assert ais_routes.DEFAULT_LONGITUDE == 16.16 def test_save_observer_location_rejects_invalid_values(self, client): """Observer location save should validate coordinates.""" with client.session_transaction() as sess: sess['logged_in'] = True response = client.post( '/settings/observer-location', data=json.dumps({'lat': 200, 'lon': 16.16}), content_type='application/json' ) assert response.status_code == 400 class TestCorrelationEndpoints: """Tests for correlation API endpoints.""" def test_get_correlations(self, client): """Test getting device correlations.""" response = client.get('/correlation') assert response.status_code == 200 data = json.loads(response.data) assert data['status'] == 'success' assert 'correlations' in data assert 'wifi_count' in data assert 'bt_count' in data def test_correlations_with_confidence_filter(self, client): """Test correlation endpoint respects confidence filter.""" response = client.get('/correlation?min_confidence=0.8') assert response.status_code == 200 data = json.loads(response.data) assert data['status'] == 'success' class TestListeningPostEndpoints: """Tests for listening post endpoints.""" def test_tools_check(self, client): """Test listening post tools availability check.""" response = client.get('/listening/tools') assert response.status_code == 200 data = json.loads(response.data) assert 'rtl_fm' in data assert 'available' in data def test_scanner_status(self, client): """Test scanner status endpoint.""" response = client.get('/listening/scanner/status') assert response.status_code == 200 data = json.loads(response.data) assert 'running' in data assert 'paused' in data assert 'current_freq' in data def test_presets(self, client): """Test scanner presets endpoint.""" response = client.get('/listening/presets') assert response.status_code == 200 data = json.loads(response.data) assert 'presets' in data assert len(data['presets']) > 0 # Check preset structure preset = data['presets'][0] assert 'name' in preset assert 'start' in preset assert 'end' in preset assert 'mod' in preset def test_scanner_stop_when_not_running(self, client): """Test stopping scanner when not running.""" response = client.post('/listening/scanner/stop') assert response.status_code == 200 data = json.loads(response.data) assert data['status'] == 'stopped' def test_activity_log(self, client): """Test getting activity log.""" response = client.get('/listening/scanner/log') assert response.status_code == 200 data = json.loads(response.data) assert 'log' in data assert 'total' in data def test_scanner_skip_when_not_running(self, client): """Test skip signal when scanner not running returns error.""" response = client.post('/listening/scanner/skip') assert response.status_code == 400 data = json.loads(response.data) assert data['status'] == 'error' class TestAudioEndpoints: """Tests for audio demodulation endpoints.""" def test_audio_status(self, client): """Test audio status endpoint.""" response = client.get('/listening/audio/status') assert response.status_code == 200 data = json.loads(response.data) assert 'running' in data assert 'frequency' in data assert 'modulation' in data def test_audio_stop_when_not_running(self, client): """Test stopping audio when not running.""" response = client.post('/listening/audio/stop') assert response.status_code == 200 data = json.loads(response.data) assert data['status'] == 'stopped' def test_audio_start_missing_frequency(self, client): """Test starting audio without frequency returns error.""" response = client.post( '/listening/audio/start', data=json.dumps({}), content_type='application/json' ) assert response.status_code == 400 data = json.loads(response.data) assert data['status'] == 'error' assert 'frequency' in data['message'].lower() def test_audio_start_invalid_modulation(self, client): """Test starting audio with invalid modulation returns error.""" response = client.post( '/listening/audio/start', data=json.dumps({ 'frequency': 98.1, 'modulation': 'invalid_mode' }), content_type='application/json' ) assert response.status_code == 400 data = json.loads(response.data) assert data['status'] == 'error' assert 'modulation' in data['message'].lower() def test_audio_stream_when_not_running(self, client): """Test audio stream when not running returns error.""" response = client.get('/listening/audio/stream') assert response.status_code == 400 data = json.loads(response.data) assert data['status'] == 'error' class TestExportEndpoints: """Tests for data export endpoints.""" def test_export_aircraft_json(self, client): """Test exporting aircraft data as JSON.""" response = client.get('/export/aircraft?format=json') assert response.status_code == 200 assert response.content_type == 'application/json' def test_export_aircraft_csv(self, client): """Test exporting aircraft data as CSV.""" response = client.get('/export/aircraft?format=csv') assert response.status_code == 200 assert 'text/csv' in response.content_type def test_export_wifi_json(self, client): """Test exporting WiFi data as JSON.""" response = client.get('/export/wifi?format=json') assert response.status_code == 200 assert response.content_type == 'application/json' def test_export_wifi_csv(self, client): """Test exporting WiFi data as CSV.""" response = client.get('/export/wifi?format=csv') assert response.status_code == 200 assert 'text/csv' in response.content_type def test_export_bluetooth_json(self, client): """Test exporting Bluetooth data as JSON.""" response = client.get('/export/bluetooth?format=json') assert response.status_code == 200 assert response.content_type == 'application/json' def test_export_bluetooth_csv(self, client): """Test exporting Bluetooth data as CSV.""" response = client.get('/export/bluetooth?format=csv') assert response.status_code == 200 assert 'text/csv' in response.content_type