v2.26.0: fix SSE fanout crash and branded logo FOUC

- Fix SSE fanout thread AttributeError when source queue is None during
  interpreter shutdown by snapshotting to local variable with null guard
- Fix branded "i" logo rendering oversized on first page load (FOUC) by
  adding inline width/height to SVG elements across 10 templates
- Bump version to 2.26.0 in config.py, pyproject.toml, and CHANGELOG.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-03-13 11:51:27 +00:00
parent 00362bcd57
commit e00fbfddc1
183 changed files with 2006 additions and 4243 deletions
+5 -4
View File
@@ -1,9 +1,11 @@
"""Pytest configuration and fixtures."""
import contextlib
import sqlite3
from unittest.mock import MagicMock, patch
import pytest
from app import app as flask_app
from routes import register_blueprints
@@ -80,9 +82,10 @@ def mock_app_state():
Provides mock process, queue, and lock objects on the app module.
"""
import app as app_module
import queue
import app as app_module
mock_process = MagicMock()
mock_process.poll.return_value = None
mock_queue = queue.Queue()
@@ -107,10 +110,8 @@ def mock_app_state():
for attr, orig in originals.items():
if orig is None:
try:
with contextlib.suppress(AttributeError):
delattr(app_module, attr)
except AttributeError:
pass
else:
setattr(app_module, attr, orig)
+5 -7
View File
@@ -12,12 +12,12 @@ Usage:
from __future__ import annotations
import argparse
import json
import random
import string
import threading
import time
from datetime import datetime, timezone
from flask import Flask, jsonify, request
app = Flask(__name__)
@@ -53,7 +53,7 @@ def generate_sensors() -> list[dict]:
"""Generate fake 433MHz sensor data."""
sensors = []
models = ['Acurite-Tower', 'Oregon-THGR122N', 'LaCrosse-TX141W', 'Ambient-F007TH']
for i in range(random.randint(2, 5)):
for _i in range(random.randint(2, 5)):
sensors.append({
'time': datetime.now(timezone.utc).isoformat(),
'model': random.choice(models),
@@ -71,7 +71,7 @@ def generate_wifi_networks() -> list[dict]:
networks = []
ssids = ['HomeNetwork', 'Linksys', 'NETGEAR', 'xfinitywifi', 'ATT-WIFI', 'CoffeeShop-Guest']
for ssid in random.sample(ssids, random.randint(3, 6)):
bssid = ':'.join(['%02X' % random.randint(0, 255) for _ in range(6)])
bssid = ':'.join([f'{random.randint(0, 255):02X}' for _ in range(6)])
networks.append({
'ssid': ssid,
'bssid': bssid,
@@ -89,7 +89,7 @@ def generate_bluetooth_devices() -> list[dict]:
devices = []
names = ['iPhone', 'Galaxy S21', 'AirPods', 'Tile Tracker', 'Fitbit', 'Unknown']
for _ in range(random.randint(2, 8)):
mac = ':'.join(['%02X' % random.randint(0, 255) for _ in range(6)])
mac = ':'.join([f'{random.randint(0, 255):02X}' for _ in range(6)])
devices.append({
'address': mac,
'name': random.choice(names),
@@ -209,9 +209,7 @@ def config():
'name': agent_name,
'port': request.environ.get('SERVER_PORT', 8021),
'push_enabled': False,
'modes_enabled': {m: True for m in [
'pager', 'sensor', 'adsb', 'ais', 'wifi', 'bluetooth'
]}
'modes_enabled': dict.fromkeys(['pager', 'sensor', 'adsb', 'ais', 'wifi', 'bluetooth'], True)
})
+1 -4
View File
@@ -17,10 +17,7 @@ Requirements:
"""
import argparse
import json
import sys
import time
from typing import Any
try:
import requests
@@ -258,7 +255,7 @@ class SmokeTests:
def run_all(self):
"""Run all smoke tests."""
print(f"\n{'='*60}")
print(f"BLUETOOTH API SMOKE TESTS")
print("BLUETOOTH API SMOKE TESTS")
print(f"Target: {self.base_url}")
print(f"{'='*60}")
+3 -6
View File
@@ -1,19 +1,16 @@
"""Tests for ACARS message translator."""
import pytest
from utils.acars_translator import (
ACARS_LABELS,
translate_label,
classify_message_type,
parse_position_report,
parse_engine_data,
parse_weather_data,
parse_oooi,
parse_position_report,
parse_weather_data,
translate_label,
translate_message,
)
# --- translate_label ---
class TestTranslateLabel:
+1 -2
View File
@@ -2,9 +2,8 @@
import queue
import threading
import time
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch, PropertyMock
from unittest.mock import MagicMock, patch
import pytest
+16 -12
View File
@@ -10,23 +10,27 @@ Tests cover:
import json
import os
import pytest
import tempfile
from unittest.mock import Mock, patch, MagicMock
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, AgentHTTPError, AgentConnectionError, create_client_from_agent
)
from utils.agent_client import AgentClient, AgentConnectionError, AgentHTTPError, create_client_from_agent
from utils.database import (
init_db, get_db_path, create_agent, get_agent, get_agent_by_name,
list_agents, update_agent, delete_agent, store_push_payload,
get_recent_payloads, cleanup_old_payloads
create_agent,
delete_agent,
get_agent,
get_agent_by_name,
get_recent_payloads,
init_db,
list_agents,
store_push_payload,
update_agent,
)
# =============================================================================
# AgentConfig Tests
# =============================================================================
@@ -559,8 +563,8 @@ class TestAgentClientIntegration:
@pytest.fixture
def mock_agent(self):
"""Start mock agent server for testing."""
from tests.mock_agent import app as mock_app
import threading
# Run mock agent in background
mock_app.config['TESTING'] = True
+2 -1
View File
@@ -19,13 +19,14 @@ Skip live tests:
import json
import os
import pytest
import shutil
import subprocess
import sys
import tempfile
import time
import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+11 -15
View File
@@ -10,14 +10,13 @@ Tests cover:
- Error handling and edge cases
"""
import contextlib
import os
import sys
import json
import time
from unittest.mock import MagicMock, patch
import pytest
import threading
from unittest.mock import Mock, patch, MagicMock
from datetime import datetime, timezone
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -34,10 +33,8 @@ def mode_manager():
yield manager
# Cleanup: stop all modes
for mode in list(manager.running_modes.keys()):
try:
with contextlib.suppress(Exception):
manager.stop_mode(mode)
except Exception:
pass
@pytest.fixture
@@ -139,14 +136,13 @@ class TestModeLifecycle:
mock_popen, mock_proc = mock_subprocess
# Mock glob for CSV file detection
with patch('glob.glob', return_value=[]):
with patch('tempfile.mkdtemp', return_value='/tmp/test'):
result = mode_manager.start_mode('wifi', {
'interface': 'wlan0',
'scan_type': 'quick'
})
# Quick scan returns data directly
assert result['status'] in ['started', 'error', 'success']
with patch('glob.glob', return_value=[]), patch('tempfile.mkdtemp', return_value='/tmp/test'):
result = mode_manager.start_mode('wifi', {
'interface': 'wlan0',
'scan_type': 'quick'
})
# Quick scan returns data directly
assert result['status'] in ['started', 'error', 'success']
def test_bluetooth_mode_lifecycle(self, mode_manager, mock_subprocess, mock_tools):
"""Bluetooth mode should start and stop cleanly."""
-1
View File
@@ -1,6 +1,5 @@
"""Tests for main application routes."""
import pytest
def test_index_page(client):
-1
View File
@@ -6,7 +6,6 @@ import pytest
from routes.aprs import parse_aprs_packet
_BASE_PACKET = "N0CALL-9>APRS,TCPIP*:@092345z4903.50N/07201.75W_090/000g005t077"
+3 -3
View File
@@ -1,8 +1,8 @@
import pytest
import json
import subprocess
from unittest.mock import MagicMock, patch
import pytest
from flask import Flask
from routes.bluetooth import bluetooth_bp, classify_bt_device, detect_tracker
+6 -4
View File
@@ -1,15 +1,17 @@
"""Unit tests for Bluetooth device aggregation."""
import pytest
from datetime import datetime, timedelta
from unittest.mock import MagicMock, patch
import pytest
from utils.bluetooth.aggregator import DeviceAggregator
from utils.bluetooth.models import BTObservation, BTDeviceAggregate
from utils.bluetooth.constants import (
MAX_RSSI_SAMPLES,
DEVICE_STALE_TIMEOUT as DEVICE_STALE_SECONDS,
)
from utils.bluetooth.constants import (
MAX_RSSI_SAMPLES,
)
from utils.bluetooth.models import BTObservation
@pytest.fixture
+4 -4
View File
@@ -1,13 +1,13 @@
"""API endpoint tests for Bluetooth v2 routes."""
import pytest
import json
from unittest.mock import MagicMock, patch, PropertyMock
from datetime import datetime
from unittest.mock import MagicMock, patch
import pytest
from flask import Flask
from routes.bluetooth_v2 import bluetooth_v2_bp
from utils.bluetooth.models import BTDeviceAggregate, ScanStatus, SystemCapabilities
from utils.bluetooth.models import BTDeviceAggregate, SystemCapabilities
@pytest.fixture
+14 -6
View File
@@ -1,18 +1,26 @@
"""Unit tests for Bluetooth heuristic detection."""
import pytest
from datetime import datetime, timedelta
from unittest.mock import MagicMock
from utils.bluetooth.heuristics import HeuristicsEngine
from utils.bluetooth.models import BTDeviceAggregate
import pytest
from utils.bluetooth.constants import (
BEACON_INTERVAL_MAX_VARIANCE as HEURISTIC_BEACON_VARIANCE_THRESHOLD,
)
from utils.bluetooth.constants import (
PERSISTENT_MIN_SEEN_COUNT as HEURISTIC_PERSISTENT_MIN_SEEN,
)
from utils.bluetooth.constants import (
PERSISTENT_WINDOW_SECONDS as HEURISTIC_PERSISTENT_WINDOW_SECONDS,
BEACON_INTERVAL_MAX_VARIANCE as HEURISTIC_BEACON_VARIANCE_THRESHOLD,
STRONG_RSSI_THRESHOLD as HEURISTIC_STRONG_STABLE_RSSI,
)
from utils.bluetooth.constants import (
STABLE_VARIANCE_THRESHOLD as HEURISTIC_STRONG_STABLE_VARIANCE,
)
from utils.bluetooth.constants import (
STRONG_RSSI_THRESHOLD as HEURISTIC_STRONG_STABLE_RSSI,
)
from utils.bluetooth.heuristics import HeuristicsEngine
from utils.bluetooth.models import BTDeviceAggregate
@pytest.fixture
+6 -6
View File
@@ -5,21 +5,21 @@ Tests device key stability, EMA smoothing, distance estimation,
band classification, and ring buffer functionality.
"""
import pytest
from datetime import datetime, timedelta
from unittest.mock import patch
import pytest
from utils.bluetooth.device_key import (
extract_key_type,
generate_device_key,
is_randomized_mac,
extract_key_type,
)
from utils.bluetooth.distance import (
DistanceEstimator,
ProximityBand,
RSSI_THRESHOLD_FAR,
RSSI_THRESHOLD_IMMEDIATE,
RSSI_THRESHOLD_NEAR,
RSSI_THRESHOLD_FAR,
DistanceEstimator,
ProximityBand,
)
from utils.bluetooth.ring_buffer import RingBuffer
+3 -3
View File
@@ -1,7 +1,5 @@
"""Tests for configuration module."""
import os
import pytest
class TestConfigEnvVars:
@@ -9,7 +7,7 @@ class TestConfigEnvVars:
def test_default_values(self):
"""Test that default values are set."""
from config import PORT, HOST, DEBUG
from config import DEBUG, HOST, PORT
assert PORT == 5050
assert HOST == '0.0.0.0'
@@ -22,6 +20,7 @@ class TestConfigEnvVars:
# Re-import to get new values
import importlib
import config
importlib.reload(config)
@@ -38,6 +37,7 @@ class TestConfigEnvVars:
monkeypatch.setenv('INTERCEPT_PORT', 'invalid')
import importlib
import config
importlib.reload(config)
+4 -2
View File
@@ -11,9 +11,10 @@ Tests cover:
import json
import os
import pytest
import sys
from unittest.mock import Mock, patch, MagicMock
from unittest.mock import Mock, patch
import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -51,6 +52,7 @@ def setup_db(tmp_path):
def app(setup_db):
"""Create Flask app with controller blueprint."""
from flask import Flask
from routes.controller import controller_bp
app = Flask(__name__)
+1 -2
View File
@@ -1,8 +1,7 @@
"""Tests for device correlation engine."""
import pytest
from datetime import datetime, timedelta
from unittest.mock import patch, MagicMock
from unittest.mock import patch
class TestDeviceCorrelator:
+13 -12
View File
@@ -1,11 +1,12 @@
"""Tests for database utilities."""
import os
import tempfile
import pytest
from pathlib import Path
from unittest.mock import patch
import pytest
# Need to patch DB_PATH before importing database module
@pytest.fixture(autouse=True)
def temp_db():
@@ -17,7 +18,7 @@ def temp_db():
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
from utils.database import close_db, init_db
init_db()
yield test_db_path
@@ -29,14 +30,14 @@ class TestSettingsCRUD:
def test_set_and_get_string(self, temp_db):
"""Test setting and getting string values."""
from utils.database import set_setting, get_setting
from utils.database import get_setting, set_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
from utils.database import get_setting, set_setting
set_setting('int_key', 42)
result = get_setting('int_key')
@@ -45,7 +46,7 @@ class TestSettingsCRUD:
def test_set_and_get_float(self, temp_db):
"""Test setting and getting float values."""
from utils.database import set_setting, get_setting
from utils.database import get_setting, set_setting
set_setting('float_key', 3.14)
result = get_setting('float_key')
@@ -54,7 +55,7 @@ class TestSettingsCRUD:
def test_set_and_get_bool(self, temp_db):
"""Test setting and getting boolean values."""
from utils.database import set_setting, get_setting
from utils.database import get_setting, set_setting
set_setting('bool_true', True)
set_setting('bool_false', False)
@@ -64,7 +65,7 @@ class TestSettingsCRUD:
def test_set_and_get_dict(self, temp_db):
"""Test setting and getting dictionary values."""
from utils.database import set_setting, get_setting
from utils.database import get_setting, set_setting
test_dict = {'name': 'test', 'value': 123, 'nested': {'a': 1}}
set_setting('dict_key', test_dict)
@@ -75,7 +76,7 @@ class TestSettingsCRUD:
def test_set_and_get_list(self, temp_db):
"""Test setting and getting list values."""
from utils.database import set_setting, get_setting
from utils.database import get_setting, set_setting
test_list = [1, 2, 3, 'four', {'five': 5}]
set_setting('list_key', test_list)
@@ -92,7 +93,7 @@ class TestSettingsCRUD:
def test_update_existing_setting(self, temp_db):
"""Test updating an existing setting."""
from utils.database import set_setting, get_setting
from utils.database import get_setting, set_setting
set_setting('update_key', 'original')
assert get_setting('update_key') == 'original'
@@ -102,7 +103,7 @@ class TestSettingsCRUD:
def test_delete_setting(self, temp_db):
"""Test deleting a setting."""
from utils.database import set_setting, get_setting, delete_setting
from utils.database import delete_setting, get_setting, set_setting
set_setting('delete_key', 'value')
assert get_setting('delete_key') == 'value'
@@ -120,7 +121,7 @@ class TestSettingsCRUD:
def test_get_all_settings(self, temp_db):
"""Test getting all settings."""
from utils.database import set_setting, get_all_settings
from utils.database import get_all_settings, set_setting
set_setting('key1', 'value1')
set_setting('key2', 42)
+8 -10
View File
@@ -9,17 +9,17 @@ from unittest.mock import MagicMock, patch
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from utils.constants import (
DEAUTH_ALERT_THRESHOLD,
DEAUTH_CRITICAL_THRESHOLD,
DEAUTH_DETECTION_WINDOW,
)
from utils.wifi.deauth_detector import (
DEAUTH_REASON_CODES,
DeauthAlert,
DeauthDetector,
DeauthPacketInfo,
DeauthTracker,
DeauthAlert,
DEAUTH_REASON_CODES,
)
from utils.constants import (
DEAUTH_DETECTION_WINDOW,
DEAUTH_ALERT_THRESHOLD,
DEAUTH_CRITICAL_THRESHOLD,
)
@@ -515,9 +515,7 @@ class TestDeauthDetectorIntegration:
return True
if 'Dot11Disas' in str(layer):
return False
if 'RadioTap' in str(layer):
return True
return False
return 'RadioTap' in str(layer)
mock_pkt.haslayer = haslayer_side_effect
+24 -48
View File
@@ -1,10 +1,11 @@
"""Tests for DSC database operations."""
import tempfile
import pytest
from pathlib import Path
from unittest.mock import patch
import pytest
@pytest.fixture(autouse=True)
def temp_db():
@@ -15,7 +16,7 @@ def temp_db():
with patch('utils.database.DB_PATH', test_db_path), \
patch('utils.database.DB_DIR', test_db_dir):
from utils.database import init_db, close_db
from utils.database import close_db, init_db
init_db()
yield test_db_path
@@ -27,7 +28,7 @@ class TestDSCAlertsCRUD:
def test_store_and_get_dsc_alert(self, temp_db):
"""Test storing and retrieving a DSC alert."""
from utils.database import store_dsc_alert, get_dsc_alert
from utils.database import get_dsc_alert, store_dsc_alert
alert_id = store_dsc_alert(
source_mmsi='232123456',
@@ -56,7 +57,7 @@ class TestDSCAlertsCRUD:
def test_store_minimal_alert(self, temp_db):
"""Test storing alert with only required fields."""
from utils.database import store_dsc_alert, get_dsc_alert
from utils.database import get_dsc_alert, store_dsc_alert
alert_id = store_dsc_alert(
source_mmsi='366000001',
@@ -81,7 +82,7 @@ class TestDSCAlertsCRUD:
def test_get_dsc_alerts_all(self, temp_db):
"""Test getting all alerts."""
from utils.database import store_dsc_alert, get_dsc_alerts
from utils.database import get_dsc_alerts, store_dsc_alert
store_dsc_alert('232123456', '100', 'DISTRESS')
store_dsc_alert('366000001', '120', 'URGENCY')
@@ -93,7 +94,7 @@ class TestDSCAlertsCRUD:
def test_get_dsc_alerts_by_category(self, temp_db):
"""Test filtering alerts by category."""
from utils.database import store_dsc_alert, get_dsc_alerts
from utils.database import get_dsc_alerts, store_dsc_alert
store_dsc_alert('232123456', '100', 'DISTRESS')
store_dsc_alert('232123457', '100', 'DISTRESS')
@@ -108,11 +109,7 @@ class TestDSCAlertsCRUD:
def test_get_dsc_alerts_by_acknowledged(self, temp_db):
"""Test filtering alerts by acknowledgement status."""
from utils.database import (
store_dsc_alert,
get_dsc_alerts,
acknowledge_dsc_alert
)
from utils.database import acknowledge_dsc_alert, get_dsc_alerts, store_dsc_alert
id1 = store_dsc_alert('232123456', '100', 'DISTRESS')
id2 = store_dsc_alert('366000001', '100', 'DISTRESS')
@@ -129,7 +126,7 @@ class TestDSCAlertsCRUD:
def test_get_dsc_alerts_by_mmsi(self, temp_db):
"""Test filtering alerts by source MMSI."""
from utils.database import store_dsc_alert, get_dsc_alerts
from utils.database import get_dsc_alerts, store_dsc_alert
store_dsc_alert('232123456', '100', 'DISTRESS')
store_dsc_alert('232123456', '120', 'URGENCY')
@@ -143,7 +140,7 @@ class TestDSCAlertsCRUD:
def test_get_dsc_alerts_pagination(self, temp_db):
"""Test alert pagination."""
from utils.database import store_dsc_alert, get_dsc_alerts
from utils.database import get_dsc_alerts, store_dsc_alert
# Create 10 alerts
for i in range(10):
@@ -164,7 +161,7 @@ class TestDSCAlertsCRUD:
def test_get_dsc_alerts_order(self, temp_db):
"""Test alerts are returned in reverse chronological order."""
from utils.database import store_dsc_alert, get_dsc_alerts
from utils.database import get_dsc_alerts, store_dsc_alert
id1 = store_dsc_alert('232123456', '100', 'DISTRESS')
id2 = store_dsc_alert('366000001', '100', 'DISTRESS')
@@ -182,11 +179,7 @@ class TestDSCAlertsCRUD:
def test_acknowledge_dsc_alert(self, temp_db):
"""Test acknowledging a DSC alert."""
from utils.database import (
store_dsc_alert,
get_dsc_alert,
acknowledge_dsc_alert
)
from utils.database import acknowledge_dsc_alert, get_dsc_alert, store_dsc_alert
alert_id = store_dsc_alert('232123456', '100', 'DISTRESS')
@@ -204,11 +197,7 @@ class TestDSCAlertsCRUD:
def test_acknowledge_dsc_alert_with_notes(self, temp_db):
"""Test acknowledging with notes."""
from utils.database import (
store_dsc_alert,
get_dsc_alert,
acknowledge_dsc_alert
)
from utils.database import acknowledge_dsc_alert, get_dsc_alert, store_dsc_alert
alert_id = store_dsc_alert('232123456', '100', 'DISTRESS')
@@ -227,11 +216,7 @@ class TestDSCAlertsCRUD:
def test_get_dsc_alert_summary(self, temp_db):
"""Test getting alert summary counts."""
from utils.database import (
store_dsc_alert,
get_dsc_alert_summary,
acknowledge_dsc_alert
)
from utils.database import acknowledge_dsc_alert, get_dsc_alert_summary, store_dsc_alert
# Create various alerts
store_dsc_alert('232123456', '100', 'DISTRESS')
@@ -264,12 +249,7 @@ class TestDSCAlertsCRUD:
def test_cleanup_old_dsc_alerts(self, temp_db):
"""Test cleanup function behavior."""
from utils.database import (
store_dsc_alert,
get_dsc_alerts,
acknowledge_dsc_alert,
cleanup_old_dsc_alerts
)
from utils.database import acknowledge_dsc_alert, cleanup_old_dsc_alerts, get_dsc_alerts, store_dsc_alert
# Create and acknowledge some alerts
id1 = store_dsc_alert('232123456', '100', 'DISTRESS')
@@ -294,11 +274,7 @@ class TestDSCAlertsCRUD:
def test_cleanup_preserves_unacknowledged(self, temp_db):
"""Test cleanup preserves unacknowledged alerts regardless of age."""
from utils.database import (
store_dsc_alert,
get_dsc_alerts,
cleanup_old_dsc_alerts
)
from utils.database import cleanup_old_dsc_alerts, get_dsc_alerts, store_dsc_alert
# Create unacknowledged alerts
store_dsc_alert('232123456', '100', 'DISTRESS')
@@ -314,7 +290,7 @@ class TestDSCAlertsCRUD:
def test_store_alert_with_raw_message(self, temp_db):
"""Test storing alert with raw message data."""
from utils.database import store_dsc_alert, get_dsc_alert
from utils.database import get_dsc_alert, store_dsc_alert
raw = '100023212345603660000110010010000000000127'
@@ -330,7 +306,7 @@ class TestDSCAlertsCRUD:
def test_store_alert_with_destination(self, temp_db):
"""Test storing alert with destination MMSI."""
from utils.database import store_dsc_alert, get_dsc_alert
from utils.database import get_dsc_alert, store_dsc_alert
alert_id = store_dsc_alert(
source_mmsi='232123456',
@@ -349,11 +325,11 @@ class TestDSCDatabaseIntegration:
def test_full_alert_lifecycle(self, temp_db):
"""Test complete lifecycle of a DSC alert."""
from utils.database import (
store_dsc_alert,
get_dsc_alert,
get_dsc_alerts,
acknowledge_dsc_alert,
get_dsc_alert_summary
get_dsc_alert,
get_dsc_alert_summary,
get_dsc_alerts,
store_dsc_alert,
)
# 1. Store a distress alert
@@ -396,7 +372,7 @@ class TestDSCDatabaseIntegration:
def test_multiple_vessel_alerts(self, temp_db):
"""Test handling alerts from multiple vessels."""
from utils.database import store_dsc_alert, get_dsc_alerts
from utils.database import get_dsc_alerts, store_dsc_alert
# Simulate multiple vessels in distress
vessels = [
@@ -405,7 +381,7 @@ class TestDSCDatabaseIntegration:
('351234567', 'Panama', 'COLLISION'),
]
for mmsi, country, nature in vessels:
for mmsi, _country, nature in vessels:
store_dsc_alert(
source_mmsi=mmsi,
format_code='100',
+3 -6
View File
@@ -1,21 +1,18 @@
"""Tests for the KiwiSDR WebSocket audio client."""
import struct
from unittest.mock import patch, MagicMock
import pytest
from unittest.mock import MagicMock, patch
from utils.kiwisdr import (
KiwiSDRClient,
KIWI_DEFAULT_PORT,
KIWI_SAMPLE_RATE,
KIWI_SND_HEADER_SIZE,
KIWI_DEFAULT_PORT,
MODE_FILTERS,
VALID_MODES,
KiwiSDRClient,
parse_host_port,
)
# ============================================
# parse_host_port tests
# ============================================
+9 -5
View File
@@ -9,10 +9,10 @@ Tests cover:
"""
import json
import pytest
from unittest.mock import Mock, patch, MagicMock
from datetime import datetime, timezone
from unittest.mock import Mock, patch
import pytest
# =============================================================================
# Utility Module Tests
@@ -173,9 +173,10 @@ class TestPSKParsing:
def test_parse_psk_base64(self):
"""Should decode base64 PSK."""
from utils.meshtastic import MeshtasticClient
import base64
from utils.meshtastic import MeshtasticClient
client = MeshtasticClient()
# 32-byte key encoded as base64
key = b'A' * 32
@@ -197,9 +198,10 @@ class TestPSKParsing:
def test_parse_psk_simple_passphrase(self):
"""Should hash simple passphrase to 32-byte key."""
from utils.meshtastic import MeshtasticClient
import hashlib
from utils.meshtastic import MeshtasticClient
client = MeshtasticClient()
result = client._parse_psk('simple:MySecretPassword')
@@ -218,9 +220,10 @@ class TestPSKParsing:
def test_parse_psk_raw_base64(self):
"""Should accept raw base64 without prefix."""
from utils.meshtastic import MeshtasticClient
import base64
from utils.meshtastic import MeshtasticClient
client = MeshtasticClient()
key = b'B' * 16
encoded = base64.b64encode(key).decode()
@@ -261,6 +264,7 @@ class TestMeshtasticRoutes:
def app(self):
"""Create Flask test app."""
from flask import Flask
from routes.meshtastic import meshtastic_bp
app = Flask(__name__)
+16 -14
View File
@@ -1,8 +1,10 @@
import pytest
from pathlib import Path
import importlib.metadata
import tomllib
import re
from pathlib import Path
import pytest
import tomllib
def get_root_path():
return Path(__file__).parent.parent
@@ -16,7 +18,7 @@ def parse_txt_requirements(file_path):
if not file_path.exists():
return set()
packages = set()
with open(file_path, "r") as f:
with open(file_path) as f:
for line in f:
line = line.strip()
if not line or line.startswith(("#", "-e", "git+", "-r")):
@@ -28,7 +30,7 @@ def parse_toml_section(data, section_type="main"):
"""Extracts full requirement strings from pyproject.toml including optional sections."""
packages = set()
project = data.get("project", {})
if section_type == "main":
deps = project.get("dependencies", [])
elif section_type == "optional":
@@ -37,7 +39,7 @@ def parse_toml_section(data, section_type="main"):
deps = project.get("optional-dependencies", {}).get("dev", [])
if not deps:
deps = data.get("dependency-groups", {}).get("dev", [])
for req in deps:
packages.add(_clean_string(req))
return packages
@@ -54,7 +56,7 @@ def test_dependency_files_integrity():
# Validate Production Sync (Main + Optionals)
txt_main = parse_txt_requirements(root / "requirements.txt")
toml_main = parse_toml_section(toml_data, "main") | parse_toml_section(toml_data, "optional")
assert txt_main == toml_main, (
f"Production version mismatch!\n"
f"Only in TXT: {txt_main - toml_main}\n"
@@ -75,10 +77,10 @@ def test_environment_vs_toml():
root = get_root_path()
with open(root / "pyproject.toml", "rb") as f:
data = tomllib.load(f)
all_declared = (
parse_toml_section(data, "main") |
parse_toml_section(data, "optional") |
parse_toml_section(data, "main") |
parse_toml_section(data, "optional") |
parse_toml_section(data, "dev")
)
_verify_installation(all_declared, "TOML")
@@ -87,7 +89,7 @@ def test_environment_vs_requirements():
"""3. Verifies that installed packages satisfy .txt requirements."""
root = get_root_path()
all_txt_deps = (
parse_txt_requirements(root / "requirements.txt") |
parse_txt_requirements(root / "requirements.txt") |
parse_txt_requirements(root / "requirements-dev.txt")
)
_verify_installation(all_txt_deps, "requirements.txt")
@@ -95,15 +97,15 @@ def test_environment_vs_requirements():
def _verify_installation(package_set, source_name):
"""Helper to check if declared versions match installed versions."""
missing_or_wrong = []
for req in package_set:
# Split name from version
parts = re.split(r'==|>=|~=|<=|>|<', req)
raw_name = parts[0].strip()
# CLEAN EXTRAS: "qrcode[pil]" -> "qrcode"
clean_name = re.sub(r'\[.*\]', '', raw_name)
try:
installed_ver = importlib.metadata.version(clean_name)
if "==" in req:
+2 -1
View File
@@ -1,8 +1,9 @@
"""Tests for Flask routes and API endpoints."""
import json
from unittest.mock import MagicMock, patch
import pytest
from unittest.mock import patch, MagicMock
@pytest.fixture(scope='session')
+2 -1
View File
@@ -2,7 +2,8 @@
from routes.listening_post import _rtl_fm_demod_mode as listening_post_rtl_mode
from utils.sdr.base import SDRDevice, SDRType
from utils.sdr.rtlsdr import RTLSDRCommandBuilder, _rtl_fm_demod_mode as builder_rtl_mode
from utils.sdr.rtlsdr import RTLSDRCommandBuilder
from utils.sdr.rtlsdr import _rtl_fm_demod_mode as builder_rtl_mode
def _dummy_rtlsdr_device() -> SDRDevice:
+10 -9
View File
@@ -1,7 +1,8 @@
from unittest.mock import MagicMock, patch
import pytest
import json
from unittest.mock import patch, MagicMock
from flask import Flask
from routes.satellite import satellite_bp
@@ -38,11 +39,11 @@ def test_fetch_celestrak_invalid_category(client):
def test_update_tle_success(mock_urlopen, client):
"""Simulate a successful response from CelesTrak."""
mock_content = (
"ISS (ZARYA)\n"
"1 25544U 98067A 23321.52083333 .00016717 00000-0 30171-3 0 9992\n"
"2 25544 51.6416 20.4567 0004561 45.3212 67.8912 15.49876543123456\n"
).encode('utf-8')
b"ISS (ZARYA)\n"
b"1 25544U 98067A 23321.52083333 .00016717 00000-0 30171-3 0 9992\n"
b"2 25544 51.6416 20.4567 0004561 45.3212 67.8912 15.49876543123456\n"
)
mock_response = MagicMock()
mock_response.read.return_value = mock_content
mock_response.__enter__.return_value = mock_response
@@ -58,7 +59,7 @@ def test_get_satellite_position_skyfield_error(mock_load, client):
"""Test behavior when Skyfield fails or data is missing."""
# Force the timescale load to fail
mock_load.side_effect = Exception("Skyfield error")
payload = {
"latitude": 51.5,
"longitude": -0.1,
@@ -79,4 +80,4 @@ def test_predict_passes_empty_cache(client):
}
response = client.post('/satellite/predict', json=payload)
assert response.status_code == 200
assert len(response.json['passes']) == 0
assert len(response.json['passes']) == 0
+1 -4
View File
@@ -13,12 +13,9 @@ Tests cover:
- Confidence level calculations
"""
import pytest
from utils.signal_guess import (
SignalGuessingEngine,
SignalGuessResult,
SignalAlternative,
Confidence,
SignalGuessingEngine,
guess_signal_type,
guess_signal_type_dict,
)
+3 -6
View File
@@ -4,14 +4,11 @@ from __future__ import annotations
import json
import os
import subprocess
import tempfile
from pathlib import Path
from unittest.mock import patch, MagicMock
from unittest.mock import MagicMock, patch
import pytest
from utils.subghz import SubGhzManager, SubGhzCapture
from utils.subghz import SubGhzCapture, SubGhzManager
@pytest.fixture
@@ -32,7 +29,7 @@ def manager(tmp_data_dir):
class TestSubGhzManagerInit:
def test_creates_data_dirs(self, tmp_path):
data_dir = tmp_path / 'new_subghz'
mgr = SubGhzManager(data_dir=data_dir)
SubGhzManager(data_dir=data_dir)
assert (data_dir / 'captures').is_dir()
def test_active_mode_idle(self, manager):
+1 -2
View File
@@ -2,8 +2,7 @@
from __future__ import annotations
import json
from unittest.mock import patch, MagicMock
from unittest.mock import MagicMock, patch
import pytest
+3 -3
View File
@@ -6,16 +6,16 @@ the signature engine correctly identifies them with appropriate confidence.
"""
import pytest
from utils.bluetooth.tracker_signatures import (
APPLE_COMPANY_ID,
TrackerConfidence,
TrackerSignatureEngine,
TrackerType,
TrackerConfidence,
detect_tracker,
get_tracker_engine,
APPLE_COMPANY_ID,
)
# =============================================================================
# SAMPLE PAYLOADS FROM REAL DEVICES
# =============================================================================
+2 -3
View File
@@ -1,9 +1,8 @@
"""Tests for utility modules."""
import pytest
from utils.process import is_valid_mac, is_valid_channel
from utils.dependencies import check_tool
from data.oui import get_manufacturer
from utils.dependencies import check_tool
from utils.process import is_valid_channel, is_valid_mac
class TestMacValidation:
+2 -1
View File
@@ -1,10 +1,11 @@
"""Comprehensive tests for validation utilities."""
import pytest
from utils.validation import (
validate_device_index,
validate_frequency,
validate_gain,
validate_device_index,
validate_rtl_tcp_host,
validate_rtl_tcp_port,
)
+2 -1
View File
@@ -1,6 +1,7 @@
"""Tests for the Waterfall / Spectrogram endpoints."""
from unittest.mock import patch, MagicMock
from unittest.mock import patch
import pytest
+3 -7
View File
@@ -6,20 +6,16 @@ and image handling.
from __future__ import annotations
import os
import tempfile
import threading
import time
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import patch, MagicMock, call, mock_open
import pytest
from unittest.mock import MagicMock, patch
from utils.weather_sat import (
WEATHER_SATELLITES,
CaptureProgress,
WeatherSatDecoder,
WeatherSatImage,
CaptureProgress,
WEATHER_SATELLITES,
get_weather_sat_decoder,
is_weather_sat_available,
)
+3 -2
View File
@@ -6,8 +6,9 @@ and ground track generation.
from __future__ import annotations
from datetime import datetime, timezone, timedelta
from datetime import datetime, timedelta, timezone
from unittest.mock import MagicMock, patch
import pytest
from utils.weather_sat_predict import _format_utc_iso, predict_passes
@@ -526,7 +527,7 @@ class TestPredictPasses:
patch('utils.weather_sat_predict.EarthSatellite'), \
patch('utils.weather_sat_predict.find_discrete', return_value=([], [])):
passes = predict_passes(lat=51.5, lon=-0.1, hours=24, min_elevation=15)
predict_passes(lat=51.5, lon=-0.1, hours=24, min_elevation=15)
# Should not raise
@patch('utils.weather_sat_predict.load')
+7 -8
View File
@@ -7,12 +7,11 @@ Covers all weather_sat endpoints: /status, /satellites, /start, /test-decode,
from __future__ import annotations
import json
from pathlib import Path
from unittest.mock import patch, MagicMock, mock_open
import pytest
from utils.weather_sat import WeatherSatImage, WEATHER_SATELLITES
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import MagicMock, patch
from utils.weather_sat import WeatherSatImage
class TestWeatherSatRoutes:
@@ -69,7 +68,7 @@ class TestWeatherSatRoutes:
"""POST /weather-sat/start successfully starts capture."""
with patch('routes.weather_sat.is_weather_sat_available', return_value=True), \
patch('routes.weather_sat.get_weather_sat_decoder') as mock_get, \
patch('routes.weather_sat.queue.Queue') as mock_queue:
patch('routes.weather_sat.queue.Queue'):
mock_decoder = MagicMock()
mock_decoder.is_running = False
@@ -207,7 +206,7 @@ class TestWeatherSatRoutes:
"""POST /weather-sat/start when SDR device is busy."""
with patch('routes.weather_sat.is_weather_sat_available', return_value=True), \
patch('routes.weather_sat.get_weather_sat_decoder') as mock_get, \
patch('app.claim_sdr_device', return_value='Device busy with pager') as mock_claim:
patch('app.claim_sdr_device', return_value='Device busy with pager'):
mock_decoder = MagicMock()
mock_decoder.is_running = False
@@ -548,7 +547,7 @@ class TestWeatherSatRoutes:
mock_get.return_value = mock_decoder
mock_send.return_value = MagicMock()
response = client.get('/weather-sat/images/test_image.png')
client.get('/weather-sat/images/test_image.png')
mock_send.assert_called_once()
call_args = mock_send.call_args
assert call_args[1]['mimetype'] == 'image/png'
+6 -6
View File
@@ -7,16 +7,16 @@ and automatic capture execution.
from __future__ import annotations
import threading
import time
from datetime import datetime, timezone, timedelta
from unittest.mock import patch, MagicMock, call
from datetime import datetime, timedelta, timezone
from unittest.mock import MagicMock, patch
import pytest
from utils.weather_sat_scheduler import (
WeatherSatScheduler,
ScheduledPass,
get_weather_sat_scheduler,
WeatherSatScheduler,
_parse_utc_iso,
get_weather_sat_scheduler,
)
@@ -742,8 +742,8 @@ class TestSchedulerConfiguration:
def test_config_constants(self):
"""Scheduler should have configuration constants."""
from utils.weather_sat_scheduler import (
WEATHER_SAT_SCHEDULE_REFRESH_MINUTES,
WEATHER_SAT_CAPTURE_BUFFER_SECONDS,
WEATHER_SAT_SCHEDULE_REFRESH_MINUTES,
)
assert isinstance(WEATHER_SAT_SCHEDULE_REFRESH_MINUTES, int)
+5 -4
View File
@@ -1,10 +1,11 @@
"""Tests for the HF/Shortwave WebSDR integration."""
from unittest.mock import patch, MagicMock
import pytest
from routes.websdr import _parse_gps_coord, _haversine
from utils.kiwisdr import parse_host_port
from unittest.mock import patch
import pytest
from routes.websdr import _haversine, _parse_gps_coord
from utils.kiwisdr import parse_host_port
# ============================================
# Helper function tests
+1 -1
View File
@@ -111,7 +111,7 @@ class TestWeFaxStations:
station_callsign='NOJ',
frequency_reference='invalid',
)
assert False, "Expected ValueError for invalid frequency_reference"
raise AssertionError("Expected ValueError for invalid frequency_reference")
except ValueError as exc:
assert 'frequency_reference' in str(exc)
+38 -35
View File
@@ -1,10 +1,13 @@
import pytest
import sys
import os
from unittest.mock import MagicMock, patch, mock_open
import sys
from unittest.mock import MagicMock, mock_open, patch
import pytest
from flask import Flask
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from routes.wifi import wifi_bp, parse_airodump_csv
from routes.wifi import parse_airodump_csv, wifi_bp
@pytest.fixture
def mock_app_module(mocker):
@@ -37,11 +40,11 @@ def test_parse_airodump_csv(mocker):
"Station MAC, First time seen, Last time seen, Power, # packets, BSSID, Probes\n"
"11:22:33:44:55:66, 2023-01-01, 2023-01-01, -60, 20, AA:BB:CC:DD:EE:FF, MyWiFi\n"
)
with patch("builtins.open", mock_open(read_data=csv_content)):
mocker.patch("routes.wifi.get_manufacturer", return_value="Apple")
networks, clients = parse_airodump_csv("dummy.csv")
assert "AA:BB:CC:DD:EE:FF" in networks
assert networks["AA:BB:CC:DD:EE:FF"]["essid"] == "MyWiFi"
assert "11:22:33:44:55:66" in clients
@@ -53,10 +56,10 @@ def test_get_interfaces(client, mocker):
"""Test the /interfaces endpoint."""
mocker.patch("routes.wifi.detect_wifi_interfaces", return_value=[{'name': 'wlan0', 'type': 'managed'}])
mocker.patch("routes.wifi.check_tool", return_value=True)
response = client.get('/wifi/interfaces')
data = response.get_json()
assert response.status_code == 200
assert len(data['interfaces']) == 1
assert data['tools']['airmon'] is True
@@ -67,18 +70,18 @@ def test_toggle_monitor_start_success(client, mocker):
mocker.patch("routes.wifi.check_tool", return_value=True)
mock_run = mocker.patch("routes.wifi.subprocess.run")
mock_run.return_value = MagicMock(stdout="enabled on [phy0]wlan0mon", stderr="", returncode=0)
with patch("os.path.exists", return_value=True):
response = client.post('/wifi/monitor', json={'action': 'start', 'interface': 'wlan0'})
assert response.status_code == 200
assert response.get_json()['status'] == 'success'
assert response.get_json()['monitor_interface'] == 'wlan0mon'
def test_start_scan_already_running(client, mock_app_module):
"""Test that we can't start a scan if one is already active."""
mock_app_module.wifi_process = MagicMock()
mock_app_module.wifi_process = MagicMock()
response = client.post('/wifi/scan/start', json={'interface': 'wlan0mon'})
data = response.get_json()
assert data['status'] == 'error'
@@ -86,21 +89,21 @@ def test_start_scan_already_running(client, mock_app_module):
def test_start_scan_execution(client, mock_app_module, mocker):
"""Test the full command construction of airodump-ng."""
mock_app_module.wifi_process = None
mock_app_module.wifi_process = None
mocker.patch("os.path.exists", return_value=True)
mocker.patch("routes.wifi.get_tool_path", return_value="/usr/bin/airodump-ng")
mock_popen = mocker.patch("routes.wifi.subprocess.Popen")
mock_proc = MagicMock()
mock_proc.poll.return_value = None
mock_proc.poll.return_value = None
mock_popen.return_value = mock_proc
payload = {'interface': 'wlan0mon', 'channel': 6, 'band': 'g'}
response = client.post('/wifi/scan/start', json=payload)
assert response.status_code == 200
assert response.get_json()['status'] == 'started'
args, _ = mock_popen.call_args
cmd = args[0]
assert "-c" in cmd and "6" in cmd
@@ -110,9 +113,9 @@ def test_stop_scan(client, mock_app_module):
"""Test terminating the scanning process."""
mock_proc = MagicMock()
mock_app_module.wifi_process = mock_proc
response = client.post('/wifi/scan/stop')
assert response.status_code == 200
assert response.get_json()['status'] == 'stopped'
mock_proc.terminate.assert_called_once()
@@ -123,14 +126,14 @@ def test_send_deauth_success(client, mock_app_module, mocker):
mocker.patch("routes.wifi.get_tool_path", return_value="/usr/bin/aireplay-ng")
mock_run = mocker.patch("routes.wifi.subprocess.run")
mock_run.return_value = MagicMock(returncode=0)
payload = {
'bssid': 'AA:BB:CC:DD:EE:FF',
'count': 10,
'interface': 'wlan0mon'
}
response = client.post('/wifi/deauth', json=payload)
assert response.status_code == 200
args, _ = mock_run.call_args
cmd = args[0]
@@ -145,10 +148,10 @@ def test_capture_handshake_start(client, mock_app_module, mocker):
mock_app_module.wifi_process = None
mocker.patch("routes.wifi.get_tool_path", return_value="/usr/bin/airodump-ng")
mock_popen = mocker.patch("routes.wifi.subprocess.Popen")
payload = {'bssid': 'AA:BB:CC:DD:EE:FF', 'channel': '6', 'interface': 'wlan0mon'}
response = client.post('/wifi/handshake/capture', json=payload)
assert response.status_code == 200
assert 'capture_file' in response.get_json()
assert mock_popen.called
@@ -158,13 +161,13 @@ def test_check_handshake_status_found(client, mocker):
mocker.patch("os.path.exists", return_value=True)
mocker.patch("os.path.getsize", return_value=1024)
mocker.patch("routes.wifi.get_tool_path", return_value="aircrack-ng")
mock_run = mocker.patch("routes.wifi.subprocess.run")
mock_run.return_value = MagicMock(stdout="WPA (1 handshake)", stderr="", returncode=0)
payload = {'file': '/tmp/intercept_handshake_test.cap', 'bssid': 'AA:BB:CC:DD:EE:FF'}
response = client.post('/wifi/handshake/status', json=payload)
assert response.get_json()['handshake_found'] is True
### --- PMKID TESTS --- ###
@@ -184,22 +187,22 @@ def test_crack_handshake_success(client, mocker):
"""Test successful password extraction using Regex."""
mocker.patch("os.path.exists", return_value=True)
mocker.patch("routes.wifi.get_tool_path", return_value="aircrack-ng")
mock_run = mocker.patch("routes.wifi.subprocess.run")
# Simulate the actual aircrack-ng success output
mock_run.return_value = MagicMock(
stdout="KEY FOUND! [ secret123 ]",
stderr="",
stdout="KEY FOUND! [ secret123 ]",
stderr="",
returncode=0
)
payload = {
'capture_file': '/tmp/intercept_handshake_test.cap',
'wordlist': '/home/user/passwords.txt',
'bssid': 'AA:BB:CC:DD:EE:FF'
}
response = client.post('/wifi/handshake/crack', json=payload)
data = response.get_json()
assert data['status'] == 'success'
assert data['password'] == 'secret123'
@@ -212,10 +215,10 @@ def test_get_wifi_networks(client, mock_app_module):
'AA:BB:CC:DD:EE:FF': {'essid': 'Home-WiFi', 'bssid': 'AA:BB:CC:DD:EE:FF'}
}
mock_app_module.wifi_handshakes = ['AA:BB:CC:DD:EE:FF']
response = client.get('/wifi/networks')
data = response.get_json()
assert len(data['networks']) == 1
assert data['networks'][0]['essid'] == 'Home-WiFi'
assert 'AA:BB:CC:DD:EE:FF' in data['handshakes']
assert 'AA:BB:CC:DD:EE:FF' in data['handshakes']