mirror of
https://github.com/smittix/intercept.git
synced 2026-06-12 16:03:29 -07:00
Add Listening Post, improve setup and documentation
- Add Listening Post mode with frequency scanner and audio monitoring - Add dependency warning for aircraft dashboard listen feature - Auto-restart audio when switching frequencies - Fix toolbar overflow on aircraft dashboard custom frequency - Update setup script with full macOS/Debian support - Clean up README and documentation for clarity - Add sox and dump1090 to Dockerfile - Add comprehensive tool reference to HARDWARE.md - Add correlation, settings, and database utilities - Add new test files for routes, validation, correlation, database 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
"""Tests for device correlation engine."""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
class TestDeviceCorrelator:
|
||||
"""Tests for DeviceCorrelator class."""
|
||||
|
||||
def test_correlate_same_oui(self):
|
||||
"""Test correlation detects same OUI."""
|
||||
from utils.correlation import DeviceCorrelator
|
||||
|
||||
correlator = DeviceCorrelator(time_window_seconds=60)
|
||||
|
||||
wifi_devices = {
|
||||
'AA:BB:CC:11:22:33': {
|
||||
'first_seen': datetime.now(),
|
||||
'last_seen': datetime.now(),
|
||||
'essid': 'TestNetwork',
|
||||
'power': -65
|
||||
}
|
||||
}
|
||||
|
||||
bt_devices = {
|
||||
'AA:BB:CC:44:55:66': {
|
||||
'first_seen': datetime.now(),
|
||||
'last_seen': datetime.now(),
|
||||
'name': 'TestPhone',
|
||||
'rssi': -60
|
||||
}
|
||||
}
|
||||
|
||||
correlations = correlator.correlate(wifi_devices, bt_devices)
|
||||
|
||||
assert len(correlations) >= 1
|
||||
assert correlations[0]['wifi_mac'] == 'AA:BB:CC:11:22:33'
|
||||
assert correlations[0]['bt_mac'] == 'AA:BB:CC:44:55:66'
|
||||
assert correlations[0]['confidence'] > 0
|
||||
|
||||
def test_correlate_timing(self):
|
||||
"""Test correlation considers timing."""
|
||||
from utils.correlation import DeviceCorrelator
|
||||
|
||||
correlator = DeviceCorrelator(time_window_seconds=30)
|
||||
now = datetime.now()
|
||||
|
||||
# Devices appearing at the same time
|
||||
wifi_devices = {
|
||||
'11:22:33:44:55:66': {
|
||||
'first_seen': now,
|
||||
'last_seen': now,
|
||||
'essid': 'Network1'
|
||||
}
|
||||
}
|
||||
|
||||
bt_devices = {
|
||||
'77:88:99:AA:BB:CC': {
|
||||
'first_seen': now,
|
||||
'last_seen': now,
|
||||
'name': 'Device1'
|
||||
}
|
||||
}
|
||||
|
||||
correlations = correlator.correlate(wifi_devices, bt_devices)
|
||||
|
||||
# Should have some confidence from timing correlation
|
||||
if correlations:
|
||||
assert correlations[0]['confidence'] > 0
|
||||
|
||||
def test_correlate_no_overlap(self):
|
||||
"""Test no correlation when devices don't overlap."""
|
||||
from utils.correlation import DeviceCorrelator
|
||||
|
||||
correlator = DeviceCorrelator(
|
||||
time_window_seconds=30,
|
||||
min_confidence=0.6
|
||||
)
|
||||
|
||||
now = datetime.now()
|
||||
old = now - timedelta(hours=1)
|
||||
|
||||
wifi_devices = {
|
||||
'11:22:33:44:55:66': {
|
||||
'first_seen': old,
|
||||
'last_seen': old,
|
||||
'essid': 'OldNetwork'
|
||||
}
|
||||
}
|
||||
|
||||
bt_devices = {
|
||||
'77:88:99:AA:BB:CC': {
|
||||
'first_seen': now,
|
||||
'last_seen': now,
|
||||
'name': 'NewDevice'
|
||||
}
|
||||
}
|
||||
|
||||
correlations = correlator.correlate(wifi_devices, bt_devices)
|
||||
|
||||
# With high min_confidence and no OUI match, should be empty
|
||||
assert len(correlations) == 0
|
||||
|
||||
def test_correlate_manufacturer_match(self):
|
||||
"""Test correlation boosts confidence for same manufacturer."""
|
||||
from utils.correlation import DeviceCorrelator
|
||||
|
||||
correlator = DeviceCorrelator(time_window_seconds=60)
|
||||
now = datetime.now()
|
||||
|
||||
wifi_devices = {
|
||||
'11:22:33:44:55:66': {
|
||||
'first_seen': now,
|
||||
'last_seen': now,
|
||||
'manufacturer': 'Apple',
|
||||
'essid': 'Network'
|
||||
}
|
||||
}
|
||||
|
||||
bt_devices = {
|
||||
'77:88:99:AA:BB:CC': {
|
||||
'first_seen': now,
|
||||
'last_seen': now,
|
||||
'manufacturer': 'Apple',
|
||||
'name': 'iPhone'
|
||||
}
|
||||
}
|
||||
|
||||
correlations = correlator.correlate(wifi_devices, bt_devices)
|
||||
|
||||
# Should have correlation with bonus for manufacturer match
|
||||
assert len(correlations) >= 1
|
||||
|
||||
def test_correlate_empty_inputs(self):
|
||||
"""Test correlation handles empty inputs."""
|
||||
from utils.correlation import DeviceCorrelator
|
||||
|
||||
correlator = DeviceCorrelator()
|
||||
|
||||
# Empty WiFi
|
||||
assert correlator.correlate({}, {'AA:BB:CC:DD:EE:FF': {}}) == []
|
||||
|
||||
# Empty Bluetooth
|
||||
assert correlator.correlate({'AA:BB:CC:DD:EE:FF': {}}, {}) == []
|
||||
|
||||
# Both empty
|
||||
assert correlator.correlate({}, {}) == []
|
||||
|
||||
def test_correlate_sorting(self):
|
||||
"""Test correlations are sorted by confidence."""
|
||||
from utils.correlation import DeviceCorrelator
|
||||
|
||||
correlator = DeviceCorrelator(
|
||||
time_window_seconds=60,
|
||||
min_confidence=0.0
|
||||
)
|
||||
now = datetime.now()
|
||||
|
||||
wifi_devices = {
|
||||
'AA:BB:CC:11:11:11': {
|
||||
'first_seen': now,
|
||||
'last_seen': now,
|
||||
'manufacturer': 'Apple'
|
||||
},
|
||||
'11:22:33:44:55:66': {
|
||||
'first_seen': now,
|
||||
'last_seen': now
|
||||
}
|
||||
}
|
||||
|
||||
bt_devices = {
|
||||
'AA:BB:CC:22:22:22': {
|
||||
'first_seen': now,
|
||||
'last_seen': now,
|
||||
'manufacturer': 'Apple'
|
||||
},
|
||||
'77:88:99:AA:BB:CC': {
|
||||
'first_seen': now,
|
||||
'last_seen': now
|
||||
}
|
||||
}
|
||||
|
||||
correlations = correlator.correlate(wifi_devices, bt_devices)
|
||||
|
||||
if len(correlations) >= 2:
|
||||
# Should be sorted by confidence (highest first)
|
||||
assert correlations[0]['confidence'] >= correlations[1]['confidence']
|
||||
|
||||
|
||||
class TestGetCorrelations:
|
||||
"""Tests for get_correlations function."""
|
||||
|
||||
@patch('utils.correlation.correlator')
|
||||
@patch('utils.correlation.db_get_correlations')
|
||||
def test_get_correlations_live(self, mock_db, mock_correlator):
|
||||
"""Test get_correlations with live data."""
|
||||
from utils.correlation import get_correlations
|
||||
|
||||
mock_correlator.correlate.return_value = [
|
||||
{
|
||||
'wifi_mac': 'AA:AA:AA:AA:AA:AA',
|
||||
'bt_mac': 'BB:BB:BB:BB:BB:BB',
|
||||
'confidence': 0.8
|
||||
}
|
||||
]
|
||||
mock_db.return_value = []
|
||||
|
||||
wifi = {'AA:AA:AA:AA:AA:AA': {}}
|
||||
bt = {'BB:BB:BB:BB:BB:BB': {}}
|
||||
|
||||
results = get_correlations(
|
||||
wifi_devices=wifi,
|
||||
bt_devices=bt,
|
||||
include_historical=False
|
||||
)
|
||||
|
||||
assert len(results) == 1
|
||||
mock_correlator.correlate.assert_called_once()
|
||||
|
||||
@patch('utils.correlation.correlator')
|
||||
@patch('utils.correlation.db_get_correlations')
|
||||
def test_get_correlations_historical(self, mock_db, mock_correlator):
|
||||
"""Test get_correlations includes historical data."""
|
||||
from utils.correlation import get_correlations
|
||||
|
||||
mock_correlator.correlate.return_value = []
|
||||
mock_db.return_value = [
|
||||
{
|
||||
'wifi_mac': 'CC:CC:CC:CC:CC:CC',
|
||||
'bt_mac': 'DD:DD:DD:DD:DD:DD',
|
||||
'confidence': 0.7,
|
||||
'first_seen': '2024-01-01',
|
||||
'last_seen': '2024-01-02'
|
||||
}
|
||||
]
|
||||
|
||||
results = get_correlations(
|
||||
wifi_devices={},
|
||||
bt_devices={},
|
||||
include_historical=True
|
||||
)
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0]['wifi_mac'] == 'CC:CC:CC:CC:CC:CC'
|
||||
|
||||
@patch('utils.correlation.correlator')
|
||||
@patch('utils.correlation.db_get_correlations')
|
||||
def test_get_correlations_deduplication(self, mock_db, mock_correlator):
|
||||
"""Test get_correlations deduplicates live and historical."""
|
||||
from utils.correlation import get_correlations
|
||||
|
||||
# Same correlation from both sources
|
||||
mock_correlator.correlate.return_value = [
|
||||
{
|
||||
'wifi_mac': 'AA:AA:AA:AA:AA:AA',
|
||||
'bt_mac': 'BB:BB:BB:BB:BB:BB',
|
||||
'confidence': 0.8
|
||||
}
|
||||
]
|
||||
mock_db.return_value = [
|
||||
{
|
||||
'wifi_mac': 'AA:AA:AA:AA:AA:AA',
|
||||
'bt_mac': 'BB:BB:BB:BB:BB:BB',
|
||||
'confidence': 0.7,
|
||||
'first_seen': '2024-01-01',
|
||||
'last_seen': '2024-01-02'
|
||||
}
|
||||
]
|
||||
|
||||
wifi = {'AA:AA:AA:AA:AA:AA': {}}
|
||||
bt = {'BB:BB:BB:BB:BB:BB': {}}
|
||||
|
||||
results = get_correlations(
|
||||
wifi_devices=wifi,
|
||||
bt_devices=bt,
|
||||
include_historical=True
|
||||
)
|
||||
|
||||
# Should deduplicate - only one entry for the same device pair
|
||||
matching = [r for r in results
|
||||
if r['wifi_mac'] == 'AA:AA:AA:AA:AA:AA']
|
||||
assert len(matching) == 1
|
||||
|
||||
|
||||
class TestCorrelationReason:
|
||||
"""Tests for correlation reason generation."""
|
||||
|
||||
def test_reason_same_oui(self):
|
||||
"""Test reason includes OUI match."""
|
||||
from utils.correlation import DeviceCorrelator
|
||||
|
||||
correlator = DeviceCorrelator()
|
||||
now = datetime.now()
|
||||
|
||||
wifi_devices = {
|
||||
'AA:BB:CC:11:22:33': {
|
||||
'first_seen': now,
|
||||
'last_seen': now
|
||||
}
|
||||
}
|
||||
|
||||
bt_devices = {
|
||||
'AA:BB:CC:44:55:66': {
|
||||
'first_seen': now,
|
||||
'last_seen': now
|
||||
}
|
||||
}
|
||||
|
||||
correlations = correlator.correlate(wifi_devices, bt_devices)
|
||||
|
||||
if correlations:
|
||||
assert 'OUI' in correlations[0]['reason'] or 'same' in correlations[0]['reason'].lower()
|
||||
|
||||
def test_reason_timing(self):
|
||||
"""Test reason includes timing information."""
|
||||
from utils.correlation import DeviceCorrelator
|
||||
|
||||
correlator = DeviceCorrelator(time_window_seconds=60)
|
||||
now = datetime.now()
|
||||
|
||||
wifi_devices = {
|
||||
'11:22:33:44:55:66': {
|
||||
'first_seen': now,
|
||||
'last_seen': now
|
||||
}
|
||||
}
|
||||
|
||||
bt_devices = {
|
||||
'77:88:99:AA:BB:CC': {
|
||||
'first_seen': now + timedelta(seconds=5),
|
||||
'last_seen': now + timedelta(seconds=5)
|
||||
}
|
||||
}
|
||||
|
||||
correlations = correlator.correlate(wifi_devices, bt_devices)
|
||||
|
||||
# If correlation found, should mention timing
|
||||
if correlations and correlations[0]['confidence'] > 0.3:
|
||||
assert 'appeared' in correlations[0]['reason'] or 'timing' in correlations[0]['reason']
|
||||
@@ -0,0 +1,256 @@
|
||||
"""Tests for database utilities."""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
# Need to patch DB_PATH before importing database module
|
||||
@pytest.fixture(autouse=True)
|
||||
def temp_db():
|
||||
"""Use a temporary database for each test."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
test_db_path = Path(tmpdir) / 'test_intercept.db'
|
||||
test_db_dir = Path(tmpdir)
|
||||
|
||||
with patch('utils.database.DB_PATH', test_db_path), \
|
||||
patch('utils.database.DB_DIR', test_db_dir):
|
||||
# Import after patching
|
||||
from utils.database import init_db, close_db
|
||||
|
||||
init_db()
|
||||
yield test_db_path
|
||||
close_db()
|
||||
|
||||
|
||||
class TestSettingsCRUD:
|
||||
"""Tests for settings CRUD operations."""
|
||||
|
||||
def test_set_and_get_string(self, temp_db):
|
||||
"""Test setting and getting string values."""
|
||||
from utils.database import set_setting, get_setting
|
||||
|
||||
set_setting('test_key', 'test_value')
|
||||
assert get_setting('test_key') == 'test_value'
|
||||
|
||||
def test_set_and_get_int(self, temp_db):
|
||||
"""Test setting and getting integer values."""
|
||||
from utils.database import set_setting, get_setting
|
||||
|
||||
set_setting('int_key', 42)
|
||||
result = get_setting('int_key')
|
||||
assert result == 42
|
||||
assert isinstance(result, int)
|
||||
|
||||
def test_set_and_get_float(self, temp_db):
|
||||
"""Test setting and getting float values."""
|
||||
from utils.database import set_setting, get_setting
|
||||
|
||||
set_setting('float_key', 3.14)
|
||||
result = get_setting('float_key')
|
||||
assert result == 3.14
|
||||
assert isinstance(result, float)
|
||||
|
||||
def test_set_and_get_bool(self, temp_db):
|
||||
"""Test setting and getting boolean values."""
|
||||
from utils.database import set_setting, get_setting
|
||||
|
||||
set_setting('bool_true', True)
|
||||
set_setting('bool_false', False)
|
||||
|
||||
assert get_setting('bool_true') is True
|
||||
assert get_setting('bool_false') is False
|
||||
|
||||
def test_set_and_get_dict(self, temp_db):
|
||||
"""Test setting and getting dictionary values."""
|
||||
from utils.database import set_setting, get_setting
|
||||
|
||||
test_dict = {'name': 'test', 'value': 123, 'nested': {'a': 1}}
|
||||
set_setting('dict_key', test_dict)
|
||||
result = get_setting('dict_key')
|
||||
|
||||
assert result == test_dict
|
||||
assert result['nested']['a'] == 1
|
||||
|
||||
def test_set_and_get_list(self, temp_db):
|
||||
"""Test setting and getting list values."""
|
||||
from utils.database import set_setting, get_setting
|
||||
|
||||
test_list = [1, 2, 3, 'four', {'five': 5}]
|
||||
set_setting('list_key', test_list)
|
||||
result = get_setting('list_key')
|
||||
|
||||
assert result == test_list
|
||||
|
||||
def test_get_nonexistent_key(self, temp_db):
|
||||
"""Test getting a key that doesn't exist."""
|
||||
from utils.database import get_setting
|
||||
|
||||
assert get_setting('nonexistent') is None
|
||||
assert get_setting('nonexistent', 'default') == 'default'
|
||||
|
||||
def test_update_existing_setting(self, temp_db):
|
||||
"""Test updating an existing setting."""
|
||||
from utils.database import set_setting, get_setting
|
||||
|
||||
set_setting('update_key', 'original')
|
||||
assert get_setting('update_key') == 'original'
|
||||
|
||||
set_setting('update_key', 'updated')
|
||||
assert get_setting('update_key') == 'updated'
|
||||
|
||||
def test_delete_setting(self, temp_db):
|
||||
"""Test deleting a setting."""
|
||||
from utils.database import set_setting, get_setting, delete_setting
|
||||
|
||||
set_setting('delete_key', 'value')
|
||||
assert get_setting('delete_key') == 'value'
|
||||
|
||||
result = delete_setting('delete_key')
|
||||
assert result is True
|
||||
assert get_setting('delete_key') is None
|
||||
|
||||
def test_delete_nonexistent_setting(self, temp_db):
|
||||
"""Test deleting a setting that doesn't exist."""
|
||||
from utils.database import delete_setting
|
||||
|
||||
result = delete_setting('nonexistent_key')
|
||||
assert result is False
|
||||
|
||||
def test_get_all_settings(self, temp_db):
|
||||
"""Test getting all settings."""
|
||||
from utils.database import set_setting, get_all_settings
|
||||
|
||||
set_setting('key1', 'value1')
|
||||
set_setting('key2', 42)
|
||||
set_setting('key3', True)
|
||||
|
||||
all_settings = get_all_settings()
|
||||
|
||||
assert 'key1' in all_settings
|
||||
assert all_settings['key1'] == 'value1'
|
||||
assert all_settings['key2'] == 42
|
||||
assert all_settings['key3'] is True
|
||||
|
||||
|
||||
class TestSignalHistory:
|
||||
"""Tests for signal history operations."""
|
||||
|
||||
def test_add_and_get_signal_reading(self, temp_db):
|
||||
"""Test adding and retrieving signal readings."""
|
||||
from utils.database import add_signal_reading, get_signal_history
|
||||
|
||||
add_signal_reading('wifi', 'AA:BB:CC:DD:EE:FF', -65)
|
||||
add_signal_reading('wifi', 'AA:BB:CC:DD:EE:FF', -62)
|
||||
add_signal_reading('wifi', 'AA:BB:CC:DD:EE:FF', -70)
|
||||
|
||||
history = get_signal_history('wifi', 'AA:BB:CC:DD:EE:FF')
|
||||
|
||||
assert len(history) == 3
|
||||
# Results should be in chronological order
|
||||
assert history[0]['signal'] == -65
|
||||
assert history[1]['signal'] == -62
|
||||
assert history[2]['signal'] == -70
|
||||
|
||||
def test_signal_history_with_metadata(self, temp_db):
|
||||
"""Test signal readings with metadata."""
|
||||
from utils.database import add_signal_reading, get_signal_history
|
||||
|
||||
metadata = {'channel': 6, 'ssid': 'TestNetwork'}
|
||||
add_signal_reading('wifi', 'AA:BB:CC:DD:EE:FF', -65, metadata)
|
||||
|
||||
history = get_signal_history('wifi', 'AA:BB:CC:DD:EE:FF')
|
||||
|
||||
assert len(history) == 1
|
||||
assert history[0]['metadata'] == metadata
|
||||
|
||||
def test_signal_history_limit(self, temp_db):
|
||||
"""Test signal history respects limit parameter."""
|
||||
from utils.database import add_signal_reading, get_signal_history
|
||||
|
||||
for i in range(10):
|
||||
add_signal_reading('wifi', 'AA:BB:CC:DD:EE:FF', -60 - i)
|
||||
|
||||
history = get_signal_history('wifi', 'AA:BB:CC:DD:EE:FF', limit=5)
|
||||
assert len(history) == 5
|
||||
|
||||
def test_signal_history_different_devices(self, temp_db):
|
||||
"""Test signal history isolates different devices."""
|
||||
from utils.database import add_signal_reading, get_signal_history
|
||||
|
||||
add_signal_reading('wifi', 'AA:AA:AA:AA:AA:AA', -65)
|
||||
add_signal_reading('wifi', 'BB:BB:BB:BB:BB:BB', -70)
|
||||
|
||||
history_a = get_signal_history('wifi', 'AA:AA:AA:AA:AA:AA')
|
||||
history_b = get_signal_history('wifi', 'BB:BB:BB:BB:BB:BB')
|
||||
|
||||
assert len(history_a) == 1
|
||||
assert len(history_b) == 1
|
||||
assert history_a[0]['signal'] == -65
|
||||
assert history_b[0]['signal'] == -70
|
||||
|
||||
def test_cleanup_old_signal_history(self, temp_db):
|
||||
"""Test cleanup of old signal history."""
|
||||
from utils.database import add_signal_reading, cleanup_old_signal_history
|
||||
|
||||
add_signal_reading('wifi', 'AA:BB:CC:DD:EE:FF', -65)
|
||||
|
||||
# Cleanup with 0 hours should remove everything
|
||||
deleted = cleanup_old_signal_history(max_age_hours=0)
|
||||
# Note: This may or may not delete depending on timing
|
||||
assert isinstance(deleted, int)
|
||||
|
||||
|
||||
class TestDeviceCorrelations:
|
||||
"""Tests for device correlation operations."""
|
||||
|
||||
def test_add_and_get_correlation(self, temp_db):
|
||||
"""Test adding and retrieving correlations."""
|
||||
from utils.database import add_correlation, get_correlations
|
||||
|
||||
add_correlation(
|
||||
wifi_mac='AA:AA:AA:AA:AA:AA',
|
||||
bt_mac='BB:BB:BB:BB:BB:BB',
|
||||
confidence=0.85,
|
||||
metadata={'reason': 'timing'}
|
||||
)
|
||||
|
||||
correlations = get_correlations(min_confidence=0.5)
|
||||
|
||||
assert len(correlations) >= 1
|
||||
found = next(
|
||||
(c for c in correlations
|
||||
if c['wifi_mac'] == 'AA:AA:AA:AA:AA:AA'),
|
||||
None
|
||||
)
|
||||
assert found is not None
|
||||
assert found['bt_mac'] == 'BB:BB:BB:BB:BB:BB'
|
||||
assert found['confidence'] == 0.85
|
||||
|
||||
def test_correlation_confidence_filter(self, temp_db):
|
||||
"""Test correlation filtering by confidence."""
|
||||
from utils.database import add_correlation, get_correlations
|
||||
|
||||
add_correlation('AA:AA:AA:AA:AA:AA', 'BB:BB:BB:BB:BB:BB', 0.9)
|
||||
add_correlation('CC:CC:CC:CC:CC:CC', 'DD:DD:DD:DD:DD:DD', 0.4)
|
||||
|
||||
high_confidence = get_correlations(min_confidence=0.7)
|
||||
all_confidence = get_correlations(min_confidence=0.3)
|
||||
|
||||
assert len(high_confidence) == 1
|
||||
assert len(all_confidence) == 2
|
||||
|
||||
def test_correlation_upsert(self, temp_db):
|
||||
"""Test that correlations are updated on conflict."""
|
||||
from utils.database import add_correlation, get_correlations
|
||||
|
||||
add_correlation('AA:AA:AA:AA:AA:AA', 'BB:BB:BB:BB:BB:BB', 0.5)
|
||||
add_correlation('AA:AA:AA:AA:AA:AA', 'BB:BB:BB:BB:BB:BB', 0.9)
|
||||
|
||||
correlations = get_correlations(min_confidence=0.0)
|
||||
|
||||
matching = [c for c in correlations
|
||||
if c['wifi_mac'] == 'AA:AA:AA:AA:AA:AA']
|
||||
assert len(matching) == 1
|
||||
assert matching[0]['confidence'] == 0.9
|
||||
@@ -0,0 +1,376 @@
|
||||
"""Tests for Flask routes and API endpoints."""
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
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
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Comprehensive tests for validation utilities."""
|
||||
|
||||
import pytest
|
||||
from utils.validation import (
|
||||
validate_frequency,
|
||||
validate_gain,
|
||||
validate_device_index,
|
||||
validate_rtl_tcp_host,
|
||||
validate_rtl_tcp_port,
|
||||
)
|
||||
|
||||
|
||||
class TestFrequencyValidation:
|
||||
"""Tests for frequency validation."""
|
||||
|
||||
def test_valid_frequencies(self):
|
||||
"""Test valid frequency values."""
|
||||
assert validate_frequency('152.0') == '152.0'
|
||||
assert validate_frequency(152.0) == '152.0'
|
||||
assert validate_frequency('1090') == '1090'
|
||||
assert validate_frequency(433.92) == '433.92'
|
||||
|
||||
def test_frequency_range(self):
|
||||
"""Test frequency range limits."""
|
||||
# RTL-SDR typical range: 24MHz - 1766MHz
|
||||
assert validate_frequency('24') == '24'
|
||||
assert validate_frequency('1700') == '1700'
|
||||
|
||||
def test_invalid_frequencies(self):
|
||||
"""Test invalid frequency values."""
|
||||
with pytest.raises(ValueError):
|
||||
validate_frequency('')
|
||||
with pytest.raises(ValueError):
|
||||
validate_frequency('abc')
|
||||
with pytest.raises(ValueError):
|
||||
validate_frequency(-100)
|
||||
with pytest.raises(ValueError):
|
||||
validate_frequency(0)
|
||||
|
||||
|
||||
class TestGainValidation:
|
||||
"""Tests for gain validation."""
|
||||
|
||||
def test_valid_gains(self):
|
||||
"""Test valid gain values."""
|
||||
assert validate_gain('0') == '0'
|
||||
assert validate_gain('40') == '40'
|
||||
assert validate_gain(49.6) == '49.6'
|
||||
assert validate_gain('auto') == 'auto'
|
||||
|
||||
def test_invalid_gains(self):
|
||||
"""Test invalid gain values."""
|
||||
with pytest.raises(ValueError):
|
||||
validate_gain(-10)
|
||||
with pytest.raises(ValueError):
|
||||
validate_gain(100)
|
||||
with pytest.raises(ValueError):
|
||||
validate_gain('invalid')
|
||||
|
||||
|
||||
class TestDeviceIndexValidation:
|
||||
"""Tests for device index validation."""
|
||||
|
||||
def test_valid_indices(self):
|
||||
"""Test valid device indices."""
|
||||
assert validate_device_index('0') == '0'
|
||||
assert validate_device_index(0) == '0'
|
||||
assert validate_device_index('1') == '1'
|
||||
assert validate_device_index(3) == '3'
|
||||
|
||||
def test_invalid_indices(self):
|
||||
"""Test invalid device indices."""
|
||||
with pytest.raises(ValueError):
|
||||
validate_device_index(-1)
|
||||
with pytest.raises(ValueError):
|
||||
validate_device_index('abc')
|
||||
with pytest.raises(ValueError):
|
||||
validate_device_index(100)
|
||||
|
||||
|
||||
class TestRtlTcpHostValidation:
|
||||
"""Tests for RTL-TCP host validation."""
|
||||
|
||||
def test_valid_hosts(self):
|
||||
"""Test valid host values."""
|
||||
assert validate_rtl_tcp_host('localhost') == 'localhost'
|
||||
assert validate_rtl_tcp_host('127.0.0.1') == '127.0.0.1'
|
||||
assert validate_rtl_tcp_host('192.168.1.1') == '192.168.1.1'
|
||||
assert validate_rtl_tcp_host('server.example.com') == 'server.example.com'
|
||||
|
||||
def test_invalid_hosts(self):
|
||||
"""Test invalid host values."""
|
||||
with pytest.raises(ValueError):
|
||||
validate_rtl_tcp_host('')
|
||||
with pytest.raises(ValueError):
|
||||
validate_rtl_tcp_host('invalid host with spaces')
|
||||
with pytest.raises(ValueError):
|
||||
validate_rtl_tcp_host('host;rm -rf /')
|
||||
|
||||
|
||||
class TestRtlTcpPortValidation:
|
||||
"""Tests for RTL-TCP port validation."""
|
||||
|
||||
def test_valid_ports(self):
|
||||
"""Test valid port values."""
|
||||
assert validate_rtl_tcp_port(1234) == 1234
|
||||
assert validate_rtl_tcp_port('1234') == 1234
|
||||
assert validate_rtl_tcp_port(30003) == 30003
|
||||
assert validate_rtl_tcp_port(65535) == 65535
|
||||
|
||||
def test_invalid_ports(self):
|
||||
"""Test invalid port values."""
|
||||
with pytest.raises(ValueError):
|
||||
validate_rtl_tcp_port(0)
|
||||
with pytest.raises(ValueError):
|
||||
validate_rtl_tcp_port(-1)
|
||||
with pytest.raises(ValueError):
|
||||
validate_rtl_tcp_port(70000)
|
||||
with pytest.raises(ValueError):
|
||||
validate_rtl_tcp_port('abc')
|
||||
Reference in New Issue
Block a user