mirror of
https://github.com/smittix/intercept.git
synced 2026-07-06 08:38:14 -07:00
96172ca593
First-time run of ruff-format via pre-commit hook normalises quote style, trailing commas, and whitespace across 188 Python files. No logic changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
645 lines
22 KiB
Python
645 lines
22 KiB
Python
"""
|
|
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
|