mirror of
https://github.com/smittix/intercept.git
synced 2026-06-17 09:59:47 -07:00
fix: stabilize test suite and repair frontend/backend wiring
- meshcore pin >=2.3.0 (EventType.STATS_CORE floor); setup.sh derives optional packages from requirements.txt; Python 3.10 warning - agent-mode wifi clients proxy route + bare-array response handling - remove dead AIS/ACARS/VDL2 SPA wiring and orphaned partials/CSS - agent TLE download to data/tle/ (was littering repo root as gp.php) - gate deferred background init off under pytest (mock-pollution race) - complete Popen mocks (context manager protocol, communicate tuples) - real pipe fds in weather-sat decoder tests (fd 10/11 collision caused 10s SQLite stalls); satellite tests no longer rewrite data/satellites.py - register 'live' pytest marker, excluded by default - update stale test assertions to current APIs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+177
-194
@@ -23,18 +23,19 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
# Fixtures
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def setup_db(tmp_path):
|
||||
"""Set up a temporary database."""
|
||||
import utils.database as db_module
|
||||
from utils.database import init_db
|
||||
|
||||
test_db_path = tmp_path / 'test.db'
|
||||
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:
|
||||
if hasattr(db_module._local, "connection") and db_module._local.connection:
|
||||
db_module._local.connection.close()
|
||||
db_module._local.connection = None
|
||||
|
||||
@@ -42,7 +43,7 @@ def setup_db(tmp_path):
|
||||
|
||||
yield
|
||||
|
||||
if hasattr(db_module._local, 'connection') and db_module._local.connection:
|
||||
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
|
||||
@@ -56,7 +57,7 @@ def app(setup_db):
|
||||
from routes.controller import controller_bp
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['TESTING'] = True
|
||||
app.config["TESTING"] = True
|
||||
app.register_blueprint(controller_bp)
|
||||
|
||||
return app
|
||||
@@ -72,13 +73,14 @@ def client(app):
|
||||
def sample_agent(setup_db):
|
||||
"""Create a sample agent in database."""
|
||||
from utils.database import create_agent
|
||||
|
||||
agent_id = create_agent(
|
||||
name='test-sensor',
|
||||
base_url='http://192.168.1.50:8020',
|
||||
api_key='test-key',
|
||||
description='Test sensor node',
|
||||
capabilities={'adsb': True, 'wifi': True},
|
||||
gps_coords={'lat': 40.7128, 'lon': -74.0060}
|
||||
name="test-sensor",
|
||||
base_url="http://192.168.1.50:8020",
|
||||
api_key="test-key",
|
||||
description="Test sensor node",
|
||||
capabilities={"adsb": True, "wifi": True},
|
||||
gps_coords={"lat": 40.7128, "lon": -74.0060},
|
||||
)
|
||||
return agent_id
|
||||
|
||||
@@ -87,125 +89,125 @@ def sample_agent(setup_db):
|
||||
# Agent CRUD Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestAgentCRUD:
|
||||
"""Tests for agent CRUD operations."""
|
||||
|
||||
def test_list_agents_empty(self, client):
|
||||
"""GET /controller/agents should return empty list initially."""
|
||||
response = client.get('/controller/agents')
|
||||
response = client.get("/controller/agents")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'success'
|
||||
assert data['agents'] == []
|
||||
assert data['count'] == 0
|
||||
assert data["status"] == "success"
|
||||
assert data["agents"] == []
|
||||
assert data["count"] == 0
|
||||
|
||||
def test_register_agent_success(self, client):
|
||||
"""POST /controller/agents should register new agent."""
|
||||
with patch('routes.controller.AgentClient') as MockClient:
|
||||
with patch("routes.controller.AgentClient") as MockClient:
|
||||
# Mock successful capability fetch
|
||||
mock_instance = Mock()
|
||||
mock_instance.get_capabilities.return_value = {
|
||||
'modes': {'adsb': True, 'wifi': True},
|
||||
'devices': [{'name': 'RTL-SDR'}]
|
||||
"modes": {"adsb": True, "wifi": True},
|
||||
"devices": [{"name": "RTL-SDR"}],
|
||||
}
|
||||
MockClient.return_value = mock_instance
|
||||
|
||||
response = client.post('/controller/agents',
|
||||
response = client.post(
|
||||
"/controller/agents",
|
||||
json={
|
||||
'name': 'new-sensor',
|
||||
'base_url': 'http://192.168.1.51:8020',
|
||||
'api_key': 'secret123',
|
||||
'description': 'New sensor node'
|
||||
"name": "new-sensor",
|
||||
"base_url": "http://192.168.1.51:8020",
|
||||
"api_key": "secret123",
|
||||
"description": "New sensor node",
|
||||
},
|
||||
content_type='application/json'
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'success'
|
||||
assert data['agent']['name'] == 'new-sensor'
|
||||
assert data["status"] == "success"
|
||||
assert data["agent"]["name"] == "new-sensor"
|
||||
|
||||
def test_register_agent_missing_name(self, client):
|
||||
"""POST /controller/agents should reject missing name."""
|
||||
response = client.post('/controller/agents',
|
||||
json={'base_url': 'http://localhost:8020'},
|
||||
content_type='application/json'
|
||||
response = client.post(
|
||||
"/controller/agents", json={"base_url": "http://localhost:8020"}, content_type="application/json"
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'name is required' in data['message']
|
||||
assert "name is required" in data["message"]
|
||||
|
||||
def test_register_agent_missing_url(self, client):
|
||||
"""POST /controller/agents should reject missing URL."""
|
||||
response = client.post('/controller/agents',
|
||||
json={'name': 'test-sensor'},
|
||||
content_type='application/json'
|
||||
)
|
||||
response = client.post("/controller/agents", json={"name": "test-sensor"}, content_type="application/json")
|
||||
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'Base URL is required' in data['message']
|
||||
assert "Base URL is required" in data["message"]
|
||||
|
||||
def test_register_agent_duplicate_name(self, client, sample_agent):
|
||||
"""POST /controller/agents should reject duplicate name."""
|
||||
response = client.post('/controller/agents',
|
||||
response = client.post(
|
||||
"/controller/agents",
|
||||
json={
|
||||
'name': 'test-sensor', # Same as sample_agent
|
||||
'base_url': 'http://192.168.1.60:8020'
|
||||
"name": "test-sensor", # Same as sample_agent
|
||||
"base_url": "http://192.168.1.60:8020",
|
||||
},
|
||||
content_type='application/json'
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 409
|
||||
data = json.loads(response.data)
|
||||
assert 'already exists' in data['message']
|
||||
assert "already exists" in data["message"]
|
||||
|
||||
def test_list_agents_with_agents(self, client, sample_agent):
|
||||
"""GET /controller/agents should return registered agents."""
|
||||
response = client.get('/controller/agents')
|
||||
response = client.get("/controller/agents")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = json.loads(response.data)
|
||||
assert data['count'] >= 1
|
||||
assert data["count"] >= 1
|
||||
|
||||
names = [a['name'] for a in data['agents']]
|
||||
assert 'test-sensor' in names
|
||||
names = [a["name"] for a in data["agents"]]
|
||||
assert "test-sensor" in names
|
||||
|
||||
def test_get_agent_detail(self, client, sample_agent):
|
||||
"""GET /controller/agents/<id> should return agent details."""
|
||||
response = client.get(f'/controller/agents/{sample_agent}')
|
||||
response = client.get(f"/controller/agents/{sample_agent}")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'success'
|
||||
assert data['agent']['name'] == 'test-sensor'
|
||||
assert data['agent']['capabilities']['adsb'] is True
|
||||
assert data["status"] == "success"
|
||||
assert data["agent"]["name"] == "test-sensor"
|
||||
assert data["agent"]["capabilities"]["adsb"] is True
|
||||
|
||||
def test_get_agent_not_found(self, client):
|
||||
"""GET /controller/agents/<id> should return 404 for missing agent."""
|
||||
response = client.get('/controller/agents/99999')
|
||||
response = client.get("/controller/agents/99999")
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_update_agent(self, client, sample_agent):
|
||||
"""PATCH /controller/agents/<id> should update agent."""
|
||||
response = client.patch(f'/controller/agents/{sample_agent}',
|
||||
json={'description': 'Updated description'},
|
||||
content_type='application/json'
|
||||
response = client.patch(
|
||||
f"/controller/agents/{sample_agent}",
|
||||
json={"description": "Updated description"},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['agent']['description'] == 'Updated description'
|
||||
assert data["agent"]["description"] == "Updated description"
|
||||
|
||||
def test_delete_agent(self, client, sample_agent):
|
||||
"""DELETE /controller/agents/<id> should remove agent."""
|
||||
response = client.delete(f'/controller/agents/{sample_agent}')
|
||||
response = client.delete(f"/controller/agents/{sample_agent}")
|
||||
assert response.status_code == 200
|
||||
|
||||
# Verify deleted
|
||||
response = client.get(f'/controller/agents/{sample_agent}')
|
||||
response = client.get(f"/controller/agents/{sample_agent}")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
@@ -213,345 +215,325 @@ class TestAgentCRUD:
|
||||
# Proxy Operation Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestProxyOperations:
|
||||
"""Tests for proxying operations to agents."""
|
||||
|
||||
def test_proxy_start_mode(self, client, sample_agent):
|
||||
"""POST /controller/agents/<id>/<mode>/start should proxy to agent."""
|
||||
with patch('routes.controller.create_client_from_agent') as mock_create:
|
||||
with patch("routes.controller.create_client_from_agent") as mock_create:
|
||||
mock_client = Mock()
|
||||
mock_client.start_mode.return_value = {'status': 'started', 'mode': 'adsb'}
|
||||
mock_client.start_mode.return_value = {"status": "started", "mode": "adsb"}
|
||||
mock_create.return_value = mock_client
|
||||
|
||||
response = client.post(
|
||||
f'/controller/agents/{sample_agent}/adsb/start',
|
||||
json={'device_index': 0},
|
||||
content_type='application/json'
|
||||
f"/controller/agents/{sample_agent}/adsb/start",
|
||||
json={"device_index": 0},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'success'
|
||||
assert data['mode'] == 'adsb'
|
||||
assert data["status"] == "success"
|
||||
assert data["mode"] == "adsb"
|
||||
|
||||
mock_client.start_mode.assert_called_once_with('adsb', {'device_index': 0})
|
||||
mock_client.start_mode.assert_called_once_with("adsb", {"device_index": 0})
|
||||
|
||||
def test_proxy_stop_mode(self, client, sample_agent):
|
||||
"""POST /controller/agents/<id>/<mode>/stop should proxy to agent."""
|
||||
with patch('routes.controller.create_client_from_agent') as mock_create:
|
||||
with patch("routes.controller.create_client_from_agent") as mock_create:
|
||||
mock_client = Mock()
|
||||
mock_client.stop_mode.return_value = {'status': 'stopped'}
|
||||
mock_client.stop_mode.return_value = {"status": "stopped"}
|
||||
mock_create.return_value = mock_client
|
||||
|
||||
response = client.post(
|
||||
f'/controller/agents/{sample_agent}/wifi/stop',
|
||||
content_type='application/json'
|
||||
)
|
||||
response = client.post(f"/controller/agents/{sample_agent}/wifi/stop", content_type="application/json")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'success'
|
||||
assert data["status"] == "success"
|
||||
|
||||
def test_proxy_get_mode_data(self, client, sample_agent):
|
||||
"""GET /controller/agents/<id>/<mode>/data should return data."""
|
||||
with patch('routes.controller.create_client_from_agent') as mock_create:
|
||||
with patch("routes.controller.create_client_from_agent") as mock_create:
|
||||
mock_client = Mock()
|
||||
mock_client.get_mode_data.return_value = {
|
||||
'mode': 'adsb',
|
||||
'data': [{'icao': 'ABC123'}]
|
||||
}
|
||||
mock_client.get_mode_data.return_value = {"mode": "adsb", "data": [{"icao": "ABC123"}]}
|
||||
mock_create.return_value = mock_client
|
||||
|
||||
response = client.get(f'/controller/agents/{sample_agent}/adsb/data')
|
||||
response = client.get(f"/controller/agents/{sample_agent}/adsb/data")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'success'
|
||||
assert 'agent_name' in data
|
||||
assert data['agent_name'] == 'test-sensor'
|
||||
assert data["status"] == "success"
|
||||
assert "agent_name" in data
|
||||
assert data["agent_name"] == "test-sensor"
|
||||
|
||||
def test_proxy_agent_not_found(self, client):
|
||||
"""Proxy operations should return 404 for missing agent."""
|
||||
response = client.post('/controller/agents/99999/adsb/start')
|
||||
response = client.post("/controller/agents/99999/adsb/start")
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_proxy_connection_error(self, client, sample_agent):
|
||||
"""Proxy should return 503 when agent unreachable."""
|
||||
from utils.agent_client import AgentConnectionError
|
||||
|
||||
with patch('routes.controller.create_client_from_agent') as mock_create:
|
||||
with patch("routes.controller.create_client_from_agent") as mock_create:
|
||||
mock_client = Mock()
|
||||
mock_client.start_mode.side_effect = AgentConnectionError("Connection refused")
|
||||
mock_create.return_value = mock_client
|
||||
|
||||
response = client.post(
|
||||
f'/controller/agents/{sample_agent}/adsb/start',
|
||||
json={},
|
||||
content_type='application/json'
|
||||
f"/controller/agents/{sample_agent}/adsb/start", json={}, content_type="application/json"
|
||||
)
|
||||
|
||||
assert response.status_code == 503
|
||||
data = json.loads(response.data)
|
||||
assert 'Cannot connect' in data['message']
|
||||
assert "Cannot connect" in data["message"]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Push Data Ingestion Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestPushIngestion:
|
||||
"""Tests for push data ingestion endpoint."""
|
||||
|
||||
def test_ingest_success(self, client, sample_agent):
|
||||
"""POST /controller/api/ingest should store payload."""
|
||||
payload = {
|
||||
'agent_name': 'test-sensor',
|
||||
'scan_type': 'adsb',
|
||||
'interface': 'rtlsdr0',
|
||||
'payload': {
|
||||
'aircraft': [{'icao': 'ABC123', 'altitude': 35000}]
|
||||
}
|
||||
"agent_name": "test-sensor",
|
||||
"scan_type": "adsb",
|
||||
"interface": "rtlsdr0",
|
||||
"payload": {"aircraft": [{"icao": "ABC123", "altitude": 35000}]},
|
||||
}
|
||||
|
||||
response = client.post('/controller/api/ingest',
|
||||
json=payload,
|
||||
headers={'X-API-Key': 'test-key'},
|
||||
content_type='application/json'
|
||||
response = client.post(
|
||||
"/controller/api/ingest", json=payload, headers={"X-API-Key": "test-key"}, content_type="application/json"
|
||||
)
|
||||
|
||||
assert response.status_code == 202
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'accepted'
|
||||
assert 'payload_id' in data
|
||||
assert data["status"] == "accepted"
|
||||
assert "payload_id" in data
|
||||
|
||||
def test_ingest_unknown_agent(self, client):
|
||||
"""POST /controller/api/ingest should reject unknown agent."""
|
||||
payload = {
|
||||
'agent_name': 'nonexistent-sensor',
|
||||
'scan_type': 'adsb',
|
||||
'payload': {}
|
||||
}
|
||||
payload = {"agent_name": "nonexistent-sensor", "scan_type": "adsb", "payload": {}}
|
||||
|
||||
response = client.post('/controller/api/ingest',
|
||||
json=payload,
|
||||
content_type='application/json'
|
||||
)
|
||||
response = client.post("/controller/api/ingest", json=payload, content_type="application/json")
|
||||
|
||||
assert response.status_code == 401
|
||||
data = json.loads(response.data)
|
||||
assert 'Unknown agent' in data['message']
|
||||
assert "Unknown agent" in data["message"]
|
||||
|
||||
def test_ingest_invalid_api_key(self, client, sample_agent):
|
||||
"""POST /controller/api/ingest should reject invalid API key."""
|
||||
payload = {
|
||||
'agent_name': 'test-sensor',
|
||||
'scan_type': 'adsb',
|
||||
'payload': {}
|
||||
}
|
||||
payload = {"agent_name": "test-sensor", "scan_type": "adsb", "payload": {}}
|
||||
|
||||
response = client.post('/controller/api/ingest',
|
||||
json=payload,
|
||||
headers={'X-API-Key': 'wrong-key'},
|
||||
content_type='application/json'
|
||||
response = client.post(
|
||||
"/controller/api/ingest", json=payload, headers={"X-API-Key": "wrong-key"}, content_type="application/json"
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
data = json.loads(response.data)
|
||||
assert 'Invalid API key' in data['message']
|
||||
assert "Invalid API key" in data["message"]
|
||||
|
||||
def test_ingest_missing_agent_name(self, client):
|
||||
"""POST /controller/api/ingest should require agent_name."""
|
||||
response = client.post('/controller/api/ingest',
|
||||
json={'scan_type': 'adsb', 'payload': {}},
|
||||
content_type='application/json'
|
||||
response = client.post(
|
||||
"/controller/api/ingest", json={"scan_type": "adsb", "payload": {}}, content_type="application/json"
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'agent_name required' in data['message']
|
||||
assert "agent_name required" in data["message"]
|
||||
|
||||
def test_get_payloads(self, client, sample_agent):
|
||||
"""GET /controller/api/payloads should return stored payloads."""
|
||||
# First ingest some data
|
||||
for i in range(3):
|
||||
client.post('/controller/api/ingest',
|
||||
client.post(
|
||||
"/controller/api/ingest",
|
||||
json={
|
||||
'agent_name': 'test-sensor',
|
||||
'scan_type': 'adsb',
|
||||
'payload': {'aircraft': [{'icao': f'TEST{i}'}]}
|
||||
"agent_name": "test-sensor",
|
||||
"scan_type": "adsb",
|
||||
"payload": {"aircraft": [{"icao": f"TEST{i}"}]},
|
||||
},
|
||||
headers={'X-API-Key': 'test-key'},
|
||||
content_type='application/json'
|
||||
headers={"X-API-Key": "test-key"},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
response = client.get(f'/controller/api/payloads?agent_id={sample_agent}')
|
||||
response = client.get(f"/controller/api/payloads?agent_id={sample_agent}")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = json.loads(response.data)
|
||||
assert data['count'] == 3
|
||||
assert data["count"] == 3
|
||||
|
||||
def test_get_payloads_filter_by_type(self, client, sample_agent):
|
||||
"""GET /controller/api/payloads should filter by scan_type."""
|
||||
# Ingest mixed data
|
||||
client.post('/controller/api/ingest',
|
||||
json={'agent_name': 'test-sensor', 'scan_type': 'adsb', 'payload': {}},
|
||||
headers={'X-API-Key': 'test-key'},
|
||||
content_type='application/json'
|
||||
client.post(
|
||||
"/controller/api/ingest",
|
||||
json={"agent_name": "test-sensor", "scan_type": "adsb", "payload": {}},
|
||||
headers={"X-API-Key": "test-key"},
|
||||
content_type="application/json",
|
||||
)
|
||||
client.post('/controller/api/ingest',
|
||||
json={'agent_name': 'test-sensor', 'scan_type': 'wifi', 'payload': {}},
|
||||
headers={'X-API-Key': 'test-key'},
|
||||
content_type='application/json'
|
||||
client.post(
|
||||
"/controller/api/ingest",
|
||||
json={"agent_name": "test-sensor", "scan_type": "wifi", "payload": {}},
|
||||
headers={"X-API-Key": "test-key"},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
response = client.get('/controller/api/payloads?scan_type=adsb')
|
||||
response = client.get("/controller/api/payloads?scan_type=adsb")
|
||||
data = json.loads(response.data)
|
||||
|
||||
assert all(p['scan_type'] == 'adsb' for p in data['payloads'])
|
||||
assert all(p["scan_type"] == "adsb" for p in data["payloads"])
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Location Estimation Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestLocationEstimation:
|
||||
"""Tests for device location estimation (trilateration)."""
|
||||
|
||||
def test_add_observation(self, client):
|
||||
"""POST /controller/api/location/observe should accept observation."""
|
||||
response = client.post('/controller/api/location/observe',
|
||||
response = client.post(
|
||||
"/controller/api/location/observe",
|
||||
json={
|
||||
'device_id': 'AA:BB:CC:DD:EE:FF',
|
||||
'agent_name': 'sensor-1',
|
||||
'agent_lat': 40.7128,
|
||||
'agent_lon': -74.0060,
|
||||
'rssi': -55
|
||||
"device_id": "AA:BB:CC:DD:EE:FF",
|
||||
"agent_name": "sensor-1",
|
||||
"agent_lat": 40.7128,
|
||||
"agent_lon": -74.0060,
|
||||
"rssi": -55,
|
||||
},
|
||||
content_type='application/json'
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'success'
|
||||
assert data['device_id'] == 'AA:BB:CC:DD:EE:FF'
|
||||
assert data["status"] == "success"
|
||||
assert data["device_id"] == "AA:BB:CC:DD:EE:FF"
|
||||
|
||||
def test_add_observation_missing_fields(self, client):
|
||||
"""POST /controller/api/location/observe should require all fields."""
|
||||
response = client.post('/controller/api/location/observe',
|
||||
response = client.post(
|
||||
"/controller/api/location/observe",
|
||||
json={
|
||||
'device_id': 'AA:BB:CC:DD:EE:FF',
|
||||
'rssi': -55
|
||||
"device_id": "AA:BB:CC:DD:EE:FF",
|
||||
"rssi": -55,
|
||||
# Missing agent_name, agent_lat, agent_lon
|
||||
},
|
||||
content_type='application/json'
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_estimate_location(self, client):
|
||||
"""POST /controller/api/location/estimate should compute location."""
|
||||
response = client.post('/controller/api/location/estimate',
|
||||
response = client.post(
|
||||
"/controller/api/location/estimate",
|
||||
json={
|
||||
'observations': [
|
||||
{'agent_lat': 40.7128, 'agent_lon': -74.0060, 'rssi': -55, 'agent_name': 'node-1'},
|
||||
{'agent_lat': 40.7135, 'agent_lon': -74.0055, 'rssi': -70, 'agent_name': 'node-2'},
|
||||
{'agent_lat': 40.7120, 'agent_lon': -74.0050, 'rssi': -62, 'agent_name': 'node-3'}
|
||||
"observations": [
|
||||
{"agent_lat": 40.7128, "agent_lon": -74.0060, "rssi": -55, "agent_name": "node-1"},
|
||||
{"agent_lat": 40.7135, "agent_lon": -74.0055, "rssi": -70, "agent_name": "node-2"},
|
||||
{"agent_lat": 40.7120, "agent_lon": -74.0050, "rssi": -62, "agent_name": "node-3"},
|
||||
],
|
||||
'environment': 'outdoor'
|
||||
"environment": "outdoor",
|
||||
},
|
||||
content_type='application/json'
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
# Should have computed a location
|
||||
if data['location']:
|
||||
assert 'lat' in data['location']
|
||||
assert 'lon' in data['location']
|
||||
if data["location"]:
|
||||
assert "latitude" in data["location"]
|
||||
assert "longitude" in data["location"]
|
||||
|
||||
def test_estimate_location_insufficient_data(self, client):
|
||||
"""Estimation should require at least 2 observations."""
|
||||
response = client.post('/controller/api/location/estimate',
|
||||
json={
|
||||
'observations': [
|
||||
{'agent_lat': 40.7128, 'agent_lon': -74.0060, 'rssi': -55, 'agent_name': 'node-1'}
|
||||
]
|
||||
},
|
||||
content_type='application/json'
|
||||
response = client.post(
|
||||
"/controller/api/location/estimate",
|
||||
json={"observations": [{"agent_lat": 40.7128, "agent_lon": -74.0060, "rssi": -55, "agent_name": "node-1"}]},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'At least 2' in data['message']
|
||||
assert "At least 2" in data["message"]
|
||||
|
||||
def test_get_device_location_not_found(self, client):
|
||||
"""GET /controller/api/location/<device_id> returns not_found for unknown device."""
|
||||
response = client.get('/controller/api/location/unknown-device')
|
||||
response = client.get("/controller/api/location/unknown-device")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'not_found'
|
||||
assert data['location'] is None
|
||||
assert data["status"] == "not_found"
|
||||
assert data["location"] is None
|
||||
|
||||
def test_get_all_locations(self, client):
|
||||
"""GET /controller/api/location/all should return all estimates."""
|
||||
response = client.get('/controller/api/location/all')
|
||||
response = client.get("/controller/api/location/all")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'success'
|
||||
assert 'devices' in data
|
||||
assert data["status"] == "success"
|
||||
assert "devices" in data
|
||||
|
||||
def test_get_devices_near(self, client):
|
||||
"""GET /controller/api/location/near should find nearby devices."""
|
||||
response = client.get(
|
||||
'/controller/api/location/near',
|
||||
query_string={'lat': 40.7128, 'lon': -74.0060, 'radius': 100}
|
||||
"/controller/api/location/near", query_string={"lat": 40.7128, "lon": -74.0060, "radius": 100}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'success'
|
||||
assert data['center']['lat'] == 40.7128
|
||||
assert data["status"] == "success"
|
||||
assert data["center"]["lat"] == 40.7128
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Agent Refresh Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestAgentRefresh:
|
||||
"""Tests for agent refresh operations."""
|
||||
|
||||
def test_refresh_agent_success(self, client, sample_agent):
|
||||
"""POST /controller/agents/<id>/refresh should update metadata."""
|
||||
with patch('routes.controller.create_client_from_agent') as mock_create:
|
||||
with patch("routes.controller.create_client_from_agent") as mock_create:
|
||||
mock_client = Mock()
|
||||
mock_client.refresh_metadata.return_value = {
|
||||
'healthy': True,
|
||||
'capabilities': {
|
||||
'modes': {'adsb': True, 'wifi': True, 'bluetooth': True},
|
||||
'devices': [{'name': 'RTL-SDR V3'}]
|
||||
"healthy": True,
|
||||
"capabilities": {
|
||||
"modes": {"adsb": True, "wifi": True, "bluetooth": True},
|
||||
"devices": [{"name": "RTL-SDR V3"}],
|
||||
},
|
||||
'status': {'running_modes': ['adsb']},
|
||||
'config': {}
|
||||
"status": {"running_modes": ["adsb"]},
|
||||
"config": {},
|
||||
}
|
||||
mock_create.return_value = mock_client
|
||||
|
||||
response = client.post(f'/controller/agents/{sample_agent}/refresh')
|
||||
response = client.post(f"/controller/agents/{sample_agent}/refresh")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'success'
|
||||
assert data['metadata']['healthy'] is True
|
||||
assert data["status"] == "success"
|
||||
assert data["metadata"]["healthy"] is True
|
||||
|
||||
def test_refresh_agent_unreachable(self, client, sample_agent):
|
||||
"""POST /controller/agents/<id>/refresh should return 503 if unreachable."""
|
||||
with patch('routes.controller.create_client_from_agent') as mock_create:
|
||||
with patch("routes.controller.create_client_from_agent") as mock_create:
|
||||
mock_client = Mock()
|
||||
mock_client.refresh_metadata.return_value = {'healthy': False}
|
||||
mock_client.refresh_metadata.return_value = {"healthy": False}
|
||||
mock_create.return_value = mock_client
|
||||
|
||||
response = client.post(f'/controller/agents/{sample_agent}/refresh')
|
||||
response = client.post(f"/controller/agents/{sample_agent}/refresh")
|
||||
|
||||
assert response.status_code == 503
|
||||
|
||||
@@ -560,6 +542,7 @@ class TestAgentRefresh:
|
||||
# SSE Stream Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestSSEStream:
|
||||
"""Tests for SSE streaming endpoint."""
|
||||
|
||||
@@ -567,5 +550,5 @@ class TestSSEStream:
|
||||
"""GET /controller/stream/all should exist and return SSE."""
|
||||
# Just verify the endpoint is accessible
|
||||
# Full SSE testing requires more complex setup
|
||||
response = client.get('/controller/stream/all')
|
||||
assert response.content_type == 'text/event-stream'
|
||||
response = client.get("/controller/stream/all")
|
||||
assert response.mimetype == "text/event-stream"
|
||||
|
||||
Reference in New Issue
Block a user