Files
intercept/tests/test_drone_ops_routes.py
T
2026-02-20 17:02:16 +00:00

229 lines
8.6 KiB
Python

"""Tests for Drone Ops API routes."""
from __future__ import annotations
import pytest
import utils.database as db_mod
from utils.drone import get_drone_ops_service
def _set_identity(client, role: str, username: str = 'tester') -> None:
with client.session_transaction() as sess:
sess['logged_in'] = True
sess['role'] = role
sess['username'] = username
def _clear_drone_tables() -> None:
with db_mod.get_db() as conn:
conn.execute('DELETE FROM action_audit_log')
conn.execute('DELETE FROM action_approvals')
conn.execute('DELETE FROM action_requests')
conn.execute('DELETE FROM evidence_manifests')
conn.execute('DELETE FROM drone_incident_artifacts')
conn.execute('DELETE FROM drone_tracks')
conn.execute('DELETE FROM drone_correlations')
conn.execute('DELETE FROM drone_detections')
conn.execute('DELETE FROM drone_incidents')
conn.execute('DELETE FROM drone_sessions')
@pytest.fixture(scope='module', autouse=True)
def isolated_drone_db(tmp_path_factory):
original_db_dir = db_mod.DB_DIR
original_db_path = db_mod.DB_PATH
tmp_dir = tmp_path_factory.mktemp('drone_ops_db')
db_mod.DB_DIR = tmp_dir
db_mod.DB_PATH = tmp_dir / 'test_intercept.db'
if hasattr(db_mod._local, 'connection') and db_mod._local.connection is not None:
db_mod._local.connection.close()
db_mod._local.connection = None
db_mod.init_db()
yield
db_mod.close_db()
db_mod.DB_DIR = original_db_dir
db_mod.DB_PATH = original_db_path
db_mod._local.connection = None
@pytest.fixture(autouse=True)
def clean_drone_state():
db_mod.init_db()
_clear_drone_tables()
get_drone_ops_service().disarm_actions(actor='test-reset', reason='test setup')
yield
_clear_drone_tables()
get_drone_ops_service().disarm_actions(actor='test-reset', reason='test teardown')
def test_start_session_requires_operator_role(client):
_set_identity(client, role='viewer')
response = client.post('/drone-ops/session/start', json={'mode': 'passive'})
assert response.status_code == 403
data = response.get_json()
assert data['required_role'] == 'operator'
def test_session_lifecycle_and_status(client):
_set_identity(client, role='operator', username='op1')
started = client.post('/drone-ops/session/start', json={'mode': 'passive'})
assert started.status_code == 200
start_data = started.get_json()
assert start_data['status'] == 'success'
assert start_data['session']['mode'] == 'passive'
assert start_data['session']['active'] is True
status = client.get('/drone-ops/status')
assert status.status_code == 200
status_data = status.get_json()
assert status_data['status'] == 'success'
assert status_data['active_session'] is not None
assert status_data['active_session']['id'] == start_data['session']['id']
stopped = client.post('/drone-ops/session/stop', json={'id': start_data['session']['id']})
assert stopped.status_code == 200
stop_data = stopped.get_json()
assert stop_data['status'] == 'success'
assert stop_data['session']['active'] is False
def test_detection_ingest_visible_via_endpoint(client):
_set_identity(client, role='operator', username='op1')
start_resp = client.post('/drone-ops/session/start', json={'mode': 'passive'})
assert start_resp.status_code == 200
service = get_drone_ops_service()
service.ingest_event(
mode='wifi',
event={
'bssid': '60:60:1F:AA:BB:CC',
'ssid': 'DJI-OPS-TEST',
},
event_type='network_update',
)
_set_identity(client, role='viewer', username='viewer1')
response = client.get('/drone-ops/detections?source=wifi&min_confidence=0.5')
assert response.status_code == 200
data = response.get_json()
assert data['status'] == 'success'
assert data['count'] >= 1
detection = data['detections'][0]
assert detection['source'] == 'wifi'
assert detection['confidence'] >= 0.5
def test_incident_artifact_and_manifest_flow(client):
_set_identity(client, role='operator', username='op1')
created = client.post(
'/drone-ops/incidents',
json={'title': 'Unidentified UAS', 'severity': 'high'},
)
assert created.status_code == 201
incident = created.get_json()['incident']
incident_id = incident['id']
artifact_resp = client.post(
f'/drone-ops/incidents/{incident_id}/artifacts',
json={'artifact_type': 'detection', 'artifact_ref': '12345'},
)
assert artifact_resp.status_code == 201
_set_identity(client, role='analyst', username='analyst1')
manifest_resp = client.post(f'/drone-ops/evidence/{incident_id}/manifest', json={})
assert manifest_resp.status_code == 201
manifest = manifest_resp.get_json()['manifest']
assert manifest['manifest']['integrity']['algorithm'] == 'sha256'
assert len(manifest['manifest']['integrity']['digest']) == 64
_set_identity(client, role='viewer', username='viewer1')
listed = client.get(f'/drone-ops/evidence/{incident_id}/manifests')
assert listed.status_code == 200
listed_data = listed.get_json()
assert listed_data['count'] == 1
assert listed_data['manifests'][0]['id'] == manifest['id']
def test_action_execution_requires_arming_and_two_approvals(client):
_set_identity(client, role='operator', username='op1')
incident_resp = client.post('/drone-ops/incidents', json={'title': 'Action Gate Test'})
incident_id = incident_resp.get_json()['incident']['id']
request_resp = client.post(
'/drone-ops/actions/request',
json={
'incident_id': incident_id,
'action_type': 'wifi_deauth_test',
'payload': {'target': 'aa:bb:cc:dd:ee:ff'},
},
)
assert request_resp.status_code == 201
request_id = request_resp.get_json()['request']['id']
not_armed_resp = client.post(f'/drone-ops/actions/execute/{request_id}', json={})
assert not_armed_resp.status_code == 403
assert 'not armed' in not_armed_resp.get_json()['message'].lower()
arm_resp = client.post(
'/drone-ops/actions/arm',
json={'incident_id': incident_id, 'reason': 'controlled test'},
)
assert arm_resp.status_code == 200
assert arm_resp.get_json()['policy']['armed'] is True
insufficient_resp = client.post(f'/drone-ops/actions/execute/{request_id}', json={})
assert insufficient_resp.status_code == 400
assert 'insufficient approvals' in insufficient_resp.get_json()['message'].lower()
_set_identity(client, role='supervisor', username='supervisor-a')
approve_one = client.post(f'/drone-ops/actions/approve/{request_id}', json={'decision': 'approved'})
assert approve_one.status_code == 200
_set_identity(client, role='operator', username='op1')
still_insufficient = client.post(f'/drone-ops/actions/execute/{request_id}', json={})
assert still_insufficient.status_code == 400
_set_identity(client, role='supervisor', username='supervisor-b')
approve_two = client.post(f'/drone-ops/actions/approve/{request_id}', json={'decision': 'approved'})
assert approve_two.status_code == 200
assert approve_two.get_json()['request']['status'] == 'approved'
_set_identity(client, role='operator', username='op1')
executed = client.post(f'/drone-ops/actions/execute/{request_id}', json={})
assert executed.status_code == 200
assert executed.get_json()['request']['status'] == 'executed'
def test_passive_action_executes_after_single_approval(client):
_set_identity(client, role='operator', username='op1')
incident_resp = client.post('/drone-ops/incidents', json={'title': 'Passive Action Test'})
incident_id = incident_resp.get_json()['incident']['id']
request_resp = client.post(
'/drone-ops/actions/request',
json={'incident_id': incident_id, 'action_type': 'passive_spectrum_capture'},
)
request_id = request_resp.get_json()['request']['id']
arm_resp = client.post(
'/drone-ops/actions/arm',
json={'incident_id': incident_id, 'reason': 'passive validation'},
)
assert arm_resp.status_code == 200
_set_identity(client, role='supervisor', username='supervisor-a')
approve_resp = client.post(f'/drone-ops/actions/approve/{request_id}', json={'decision': 'approved'})
assert approve_resp.status_code == 200
assert approve_resp.get_json()['request']['status'] == 'approved'
_set_identity(client, role='operator', username='op1')
execute_resp = client.post(f'/drone-ops/actions/execute/{request_id}', json={})
assert execute_resp.status_code == 200
assert execute_resp.get_json()['request']['status'] == 'executed'