""" Tests for Intercept Agent components. Tests cover: - AgentConfig parsing - AgentClient HTTP operations - Database agent CRUD operations - GPS integration """ import json import os import sys import tempfile from unittest.mock import Mock, patch import pytest sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from utils.agent_client import AgentClient, AgentConnectionError, AgentHTTPError, create_client_from_agent from utils.database import ( create_agent, delete_agent, get_agent, get_agent_by_name, get_recent_payloads, init_db, list_agents, store_push_payload, update_agent, ) # ============================================================================= # AgentConfig Tests # ============================================================================= class TestAgentConfig: """Tests for AgentConfig class.""" def test_default_values(self): """AgentConfig should have sensible defaults.""" from intercept_agent import AgentConfig config = AgentConfig() assert config.port == 8020 assert config.allow_cors is False assert config.push_enabled is False assert config.push_interval == 5 assert config.controller_url == "" assert "adsb" in config.modes_enabled assert "wifi" in config.modes_enabled assert config.modes_enabled["adsb"] is True def test_load_from_file_valid(self): """AgentConfig should load from valid INI file.""" from intercept_agent import AgentConfig config_content = """ [agent] name = test-sensor port = 8025 allowed_ips = 192.168.1.0/24, 10.0.0.1 allow_cors = true [controller] url = http://192.168.1.100:5050 api_key = secret123 push_enabled = true push_interval = 10 [modes] pager = false adsb = true wifi = true bluetooth = false """ with tempfile.NamedTemporaryFile(mode="w", suffix=".cfg", delete=False) as f: f.write(config_content) config_path = f.name try: config = AgentConfig() result = config.load_from_file(config_path) assert result is True assert config.name == "test-sensor" assert config.port == 8025 assert "192.168.1.0/24" in config.allowed_ips assert config.allow_cors is True assert config.controller_url == "http://192.168.1.100:5050" assert config.controller_api_key == "secret123" assert config.push_enabled is True assert config.push_interval == 10 assert config.modes_enabled["pager"] is False assert config.modes_enabled["adsb"] is True assert config.modes_enabled["bluetooth"] is False finally: os.unlink(config_path) def test_load_from_file_missing(self): """AgentConfig should handle missing file gracefully.""" from intercept_agent import AgentConfig config = AgentConfig() result = config.load_from_file("/nonexistent/path.cfg") assert result is False def test_to_dict(self): """AgentConfig should convert to dictionary.""" from intercept_agent import AgentConfig config = AgentConfig() config.name = "test" config.port = 9000 d = config.to_dict() assert d["name"] == "test" assert d["port"] == 9000 assert "modes_enabled" in d assert isinstance(d["modes_enabled"], dict) # ============================================================================= # AgentClient Tests # ============================================================================= class TestAgentClient: """Tests for AgentClient HTTP operations.""" def test_init(self): """AgentClient should initialize correctly.""" client = AgentClient("http://192.168.1.50:8020", api_key="secret") assert client.base_url == "http://192.168.1.50:8020" assert client.api_key == "secret" assert client.timeout == 60.0 def test_init_strips_trailing_slash(self): """AgentClient should strip trailing slash from URL.""" client = AgentClient("http://192.168.1.50:8020/") assert client.base_url == "http://192.168.1.50:8020" def test_headers_without_api_key(self): """Headers should not include API key if not provided.""" client = AgentClient("http://localhost:8020") headers = client._headers() assert "X-API-Key" not in headers assert "Content-Type" in headers def test_headers_with_api_key(self): """Headers should include API key if provided.""" client = AgentClient("http://localhost:8020", api_key="test-key") headers = client._headers() assert headers["X-API-Key"] == "test-key" @patch("utils.agent_client.requests.get") def test_get_capabilities(self, mock_get): """get_capabilities should parse JSON response.""" mock_response = Mock() mock_response.json.return_value = { "modes": {"adsb": True, "wifi": True}, "devices": [{"name": "RTL-SDR"}], "agent_version": "1.0.0", } mock_response.content = b"{}" mock_response.raise_for_status = Mock() mock_get.return_value = mock_response client = AgentClient("http://localhost:8020") caps = client.get_capabilities() assert caps["modes"]["adsb"] is True assert len(caps["devices"]) == 1 mock_get.assert_called_once() @patch("utils.agent_client.requests.get") def test_get_status(self, mock_get): """get_status should return status dict.""" mock_response = Mock() mock_response.json.return_value = {"running_modes": ["adsb", "sensor"], "uptime": 3600, "push_enabled": True} mock_response.content = b"{}" mock_response.raise_for_status = Mock() mock_get.return_value = mock_response client = AgentClient("http://localhost:8020") status = client.get_status() assert "adsb" in status["running_modes"] assert status["uptime"] == 3600 @patch("utils.agent_client.requests.get") def test_health_check_healthy(self, mock_get): """health_check should return True for healthy agent.""" mock_response = Mock() mock_response.json.return_value = {"status": "healthy"} mock_response.content = b"{}" mock_response.raise_for_status = Mock() mock_get.return_value = mock_response client = AgentClient("http://localhost:8020") assert client.health_check() is True @patch("utils.agent_client.requests.get") def test_health_check_unhealthy(self, mock_get): """health_check should return False for connection error.""" import requests mock_get.side_effect = requests.ConnectionError("Connection refused") client = AgentClient("http://localhost:8020") assert client.health_check() is False @patch("utils.agent_client.requests.post") def test_start_mode(self, mock_post): """start_mode should POST to correct endpoint.""" mock_response = Mock() mock_response.json.return_value = {"status": "started", "mode": "adsb"} mock_response.content = b"{}" mock_response.raise_for_status = Mock() mock_post.return_value = mock_response client = AgentClient("http://localhost:8020") result = client.start_mode("adsb", {"device_index": 0}) assert result["status"] == "started" mock_post.assert_called_once() call_url = mock_post.call_args[0][0] assert "/adsb/start" in call_url @patch("utils.agent_client.requests.post") def test_stop_mode(self, mock_post): """stop_mode should POST to stop endpoint.""" mock_response = Mock() mock_response.json.return_value = {"status": "stopped"} mock_response.content = b"{}" mock_response.raise_for_status = Mock() mock_post.return_value = mock_response client = AgentClient("http://localhost:8020") result = client.stop_mode("wifi") assert result["status"] == "stopped" @patch("utils.agent_client.requests.get") def test_get_mode_data(self, mock_get): """get_mode_data should return data snapshot.""" mock_response = Mock() mock_response.json.return_value = { "mode": "adsb", "data": [{"icao": "ABC123", "altitude": 35000}, {"icao": "DEF456", "altitude": 28000}], } mock_response.content = b"{}" mock_response.raise_for_status = Mock() mock_get.return_value = mock_response client = AgentClient("http://localhost:8020") result = client.get_mode_data("adsb") assert len(result["data"]) == 2 assert result["data"][0]["icao"] == "ABC123" @patch("utils.agent_client.requests.get") def test_connection_error_handling(self, mock_get): """Client should raise AgentConnectionError on connection failure.""" import requests mock_get.side_effect = requests.ConnectionError("Connection refused") client = AgentClient("http://localhost:8020") with pytest.raises(AgentConnectionError) as exc_info: client.get_capabilities() assert "Cannot connect" in str(exc_info.value) @patch("utils.agent_client.requests.get") def test_timeout_error_handling(self, mock_get): """Client should raise AgentConnectionError on timeout.""" import requests mock_get.side_effect = requests.Timeout("Request timed out") client = AgentClient("http://localhost:8020", timeout=5.0) with pytest.raises(AgentConnectionError) as exc_info: client.get_status() assert "timed out" in str(exc_info.value) @patch("utils.agent_client.requests.get") def test_http_error_handling(self, mock_get): """Client should raise AgentHTTPError on HTTP errors.""" import requests mock_response = Mock() mock_response.status_code = 500 mock_response.raise_for_status.side_effect = requests.HTTPError(response=mock_response) mock_get.return_value = mock_response client = AgentClient("http://localhost:8020") with pytest.raises(AgentHTTPError) as exc_info: client.get_capabilities() assert exc_info.value.status_code == 500 def test_create_client_from_agent(self): """create_client_from_agent should create configured client.""" agent = {"id": 1, "name": "test-agent", "base_url": "http://192.168.1.50:8020", "api_key": "secret123"} client = create_client_from_agent(agent) assert client.base_url == "http://192.168.1.50:8020" assert client.api_key == "secret123" # ============================================================================= # Database Agent CRUD Tests # ============================================================================= class TestDatabaseAgentCRUD: """Tests for database agent operations.""" @pytest.fixture(autouse=True) def setup_db(self, tmp_path): """Set up a temporary database for each test.""" import utils.database as db_module # Create temp database test_db_path = tmp_path / "test.db" original_db_path = db_module.DB_PATH db_module.DB_PATH = test_db_path db_module.DB_DIR = tmp_path # Clear any existing connection if hasattr(db_module._local, "connection") and db_module._local.connection: db_module._local.connection.close() db_module._local.connection = None # Initialize schema init_db() yield # Cleanup if hasattr(db_module._local, "connection") and db_module._local.connection: db_module._local.connection.close() db_module._local.connection = None db_module.DB_PATH = original_db_path def test_create_agent(self): """create_agent should insert new agent.""" agent_id = create_agent( name="sensor-1", base_url="http://192.168.1.50:8020", api_key="secret", description="Test sensor node" ) assert agent_id is not None assert agent_id > 0 def test_get_agent(self): """get_agent should retrieve agent by ID.""" agent_id = create_agent(name="sensor-1", base_url="http://192.168.1.50:8020") agent = get_agent(agent_id) assert agent is not None assert agent["name"] == "sensor-1" assert agent["base_url"] == "http://192.168.1.50:8020" assert agent["is_active"] is True def test_get_agent_not_found(self): """get_agent should return None for missing agent.""" agent = get_agent(99999) assert agent is None def test_get_agent_by_name(self): """get_agent_by_name should find agent by name.""" create_agent(name="unique-sensor", base_url="http://localhost:8020") agent = get_agent_by_name("unique-sensor") assert agent is not None assert agent["name"] == "unique-sensor" def test_get_agent_by_name_not_found(self): """get_agent_by_name should return None for missing name.""" agent = get_agent_by_name("nonexistent-sensor") assert agent is None def test_list_agents(self): """list_agents should return all active agents.""" create_agent(name="sensor-1", base_url="http://192.168.1.51:8020") create_agent(name="sensor-2", base_url="http://192.168.1.52:8020") create_agent(name="sensor-3", base_url="http://192.168.1.53:8020") agents = list_agents() assert len(agents) >= 3 names = [a["name"] for a in agents] assert "sensor-1" in names assert "sensor-2" in names def test_list_agents_active_only(self): """list_agents should filter inactive agents by default.""" agent_id = create_agent(name="inactive-sensor", base_url="http://localhost:8020") update_agent(agent_id, is_active=False) agents = list_agents(active_only=True) names = [a["name"] for a in agents] assert "inactive-sensor" not in names def test_update_agent(self): """update_agent should modify agent fields.""" agent_id = create_agent(name="sensor-1", base_url="http://localhost:8020") result = update_agent(agent_id, base_url="http://192.168.1.100:8020", description="Updated description") assert result is True agent = get_agent(agent_id) assert agent["base_url"] == "http://192.168.1.100:8020" assert agent["description"] == "Updated description" def test_update_agent_capabilities(self): """update_agent should update capabilities JSON.""" agent_id = create_agent(name="sensor-1", base_url="http://localhost:8020") caps = {"adsb": True, "wifi": True, "bluetooth": False} update_agent(agent_id, capabilities=caps) agent = get_agent(agent_id) assert agent["capabilities"]["adsb"] is True assert agent["capabilities"]["bluetooth"] is False def test_update_agent_gps_coords(self): """update_agent should update GPS coordinates.""" agent_id = create_agent(name="sensor-1", base_url="http://localhost:8020") gps = {"lat": 40.7128, "lon": -74.0060, "altitude": 10} update_agent(agent_id, gps_coords=gps) agent = get_agent(agent_id) assert agent["gps_coords"]["lat"] == 40.7128 assert agent["gps_coords"]["lon"] == -74.0060 def test_delete_agent(self): """delete_agent should remove agent and payloads.""" agent_id = create_agent(name="to-delete", base_url="http://localhost:8020") # Add a payload store_push_payload(agent_id, "adsb", {"aircraft": []}) # Delete result = delete_agent(agent_id) assert result is True assert get_agent(agent_id) is None def test_delete_agent_not_found(self): """delete_agent should return False for missing agent.""" result = delete_agent(99999) assert result is False # ============================================================================= # Database Push Payload Tests # ============================================================================= class TestDatabasePayloads: """Tests for push payload storage.""" @pytest.fixture(autouse=True) def setup_db(self, tmp_path): """Set up a temporary database for each test.""" import utils.database as db_module test_db_path = tmp_path / "test.db" original_db_path = db_module.DB_PATH db_module.DB_PATH = test_db_path db_module.DB_DIR = tmp_path if hasattr(db_module._local, "connection") and db_module._local.connection: db_module._local.connection.close() db_module._local.connection = None init_db() yield if hasattr(db_module._local, "connection") and db_module._local.connection: db_module._local.connection.close() db_module._local.connection = None db_module.DB_PATH = original_db_path def test_store_push_payload(self): """store_push_payload should insert payload.""" agent_id = create_agent(name="sensor-1", base_url="http://localhost:8020") payload = {"aircraft": [{"icao": "ABC123", "altitude": 35000}]} payload_id = store_push_payload(agent_id, "adsb", payload, "rtlsdr0") assert payload_id > 0 def test_get_recent_payloads(self): """get_recent_payloads should return stored payloads.""" agent_id = create_agent(name="sensor-1", base_url="http://localhost:8020") store_push_payload(agent_id, "adsb", {"aircraft": [{"icao": "A"}]}) store_push_payload(agent_id, "adsb", {"aircraft": [{"icao": "B"}]}) store_push_payload(agent_id, "wifi", {"networks": []}) # Get all payloads = get_recent_payloads(agent_id=agent_id) assert len(payloads) == 3 # Filter by scan_type adsb_payloads = get_recent_payloads(agent_id=agent_id, scan_type="adsb") assert len(adsb_payloads) == 2 def test_get_recent_payloads_includes_agent_name(self): """Payloads should include agent name.""" agent_id = create_agent(name="my-sensor", base_url="http://localhost:8020") store_push_payload(agent_id, "sensor", {"temperature": 22.5}) payloads = get_recent_payloads(agent_id=agent_id) assert len(payloads) > 0 assert payloads[0]["agent_name"] == "my-sensor" def test_get_recent_payloads_limit(self): """get_recent_payloads should respect limit.""" agent_id = create_agent(name="sensor-1", base_url="http://localhost:8020") for i in range(10): store_push_payload(agent_id, "sensor", {"temp": i}) payloads = get_recent_payloads(agent_id=agent_id, limit=5) assert len(payloads) == 5 # ============================================================================= # Integration Tests # ============================================================================= class TestAgentClientIntegration: """Integration tests using mock agent server.""" @pytest.fixture def mock_agent(self): """Start mock agent server for testing.""" from tests.mock_agent import app as mock_app # Run mock agent in background mock_app.config["TESTING"] = True # Using Flask's test client instead of actual server return mock_app.test_client() def test_mock_agent_capabilities(self, mock_agent): """Mock agent should return capabilities.""" response = mock_agent.get("/capabilities") assert response.status_code == 200 data = json.loads(response.data) assert "modes" in data assert data["modes"]["adsb"] is True def test_mock_agent_start_stop_mode(self, mock_agent): """Mock agent should start/stop modes.""" # Start response = mock_agent.post("/adsb/start", json={}) assert response.status_code == 200 data = json.loads(response.data) assert data["status"] == "started" # Check status response = mock_agent.get("/status") data = json.loads(response.data) assert "adsb" in data["running_modes"] # Stop response = mock_agent.post("/adsb/stop", json={}) assert response.status_code == 200 def test_mock_agent_data(self, mock_agent): """Mock agent should return data when mode is running.""" # Start mode first mock_agent.post("/adsb/start", json={}) response = mock_agent.get("/adsb/data") assert response.status_code == 200 data = json.loads(response.data) assert "data" in data # Data should be a list of aircraft assert isinstance(data["data"], list) # Cleanup mock_agent.post("/adsb/stop", json={}) # ============================================================================= # GPS Manager Tests # ============================================================================= class TestGPSManager: """Tests for GPS integration in agent.""" def test_gps_manager_init(self): """GPSManager should initialize without error.""" from intercept_agent import GPSManager gps = GPSManager() assert gps.position is None assert gps._running is False def test_gps_manager_position_format(self): """GPSManager position should have correct format when set.""" from intercept_agent import GPSManager gps = GPSManager() # Simulate a position update class MockPosition: latitude = 40.7128 longitude = -74.0060 altitude = 10.5 speed = 0.0 heading = 180.0 fix_quality = 2 gps._position = MockPosition() pos = gps.position assert pos is not None assert pos["lat"] == 40.7128 assert pos["lon"] == -74.0060 assert pos["altitude"] == 10.5