mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-03-30 12:05:38 +00:00
- Updated `config.ini.example` to include new settings for edge loading and graph data capture, providing clearer guidance on usage. - Modified `MeshGraph` class to respect the new `graph_capture_enabled` setting, controlling the collection of edge data from incoming packets. - Adjusted message handling to ensure graph updates only occur when data capture is enabled, optimizing performance and resource usage. - Added tests to validate the new configuration options, ensuring proper functionality in various scenarios.
278 lines
10 KiB
Python
278 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Pytest fixtures for meshcore-bot tests
|
|
"""
|
|
|
|
import pytest
|
|
import sqlite3
|
|
import configparser
|
|
from unittest.mock import Mock, MagicMock, AsyncMock
|
|
from datetime import datetime, timedelta
|
|
from typing import Any, Optional
|
|
|
|
from modules.db_manager import DBManager
|
|
from modules.mesh_graph import MeshGraph
|
|
from modules.models import MeshMessage
|
|
from tests.helpers import create_test_repeater, create_test_edge, populate_test_graph
|
|
|
|
|
|
def mock_message(
|
|
content: str = "ping",
|
|
channel: Optional[str] = "general",
|
|
is_dm: bool = False,
|
|
sender_id: Optional[str] = "TestUser",
|
|
sender_pubkey: Optional[str] = None,
|
|
**kwargs: Any,
|
|
) -> MeshMessage:
|
|
"""Factory for creating MeshMessage instances in tests."""
|
|
return MeshMessage(
|
|
content=content,
|
|
channel=channel if not is_dm else None,
|
|
is_dm=is_dm,
|
|
sender_id=sender_id,
|
|
sender_pubkey=sender_pubkey,
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def minimal_config():
|
|
"""Minimal ConfigParser for command tests (Connection, Bot, Channels, Keywords)."""
|
|
config = configparser.ConfigParser()
|
|
config.add_section("Connection")
|
|
config.set("Connection", "connection_type", "serial")
|
|
config.set("Connection", "serial_port", "/dev/ttyUSB0")
|
|
config.add_section("Bot")
|
|
config.set("Bot", "bot_name", "TestBot")
|
|
config.set("Bot", "db_path", "meshcore_bot.db")
|
|
config.add_section("Channels")
|
|
config.set("Channels", "monitor_channels", "general,test,emergency")
|
|
config.set("Channels", "respond_to_dms", "true")
|
|
config.add_section("Keywords")
|
|
config.set("Keywords", "ping", "Pong!")
|
|
config.set("Keywords", "test", "ack")
|
|
return config
|
|
|
|
|
|
@pytest.fixture
|
|
def command_mock_bot(mock_logger, minimal_config):
|
|
"""Lightweight mock bot for command tests. No DB, no mesh_graph."""
|
|
bot = MagicMock()
|
|
bot.logger = mock_logger
|
|
bot.config = minimal_config
|
|
bot.translator = MagicMock()
|
|
|
|
def _mock_translate(key, **kwargs):
|
|
if kwargs:
|
|
return key + " " + " ".join(f"{k}={v}" for k, v in sorted(kwargs.items()))
|
|
return key
|
|
|
|
bot.translator.translate = Mock(side_effect=_mock_translate)
|
|
bot.translator.get_value = Mock(return_value=None)
|
|
bot.command_manager = MagicMock()
|
|
bot.command_manager.monitor_channels = ["general", "test", "emergency"]
|
|
bot.command_manager.send_response = AsyncMock(return_value=True)
|
|
return bot
|
|
|
|
|
|
@pytest.fixture
|
|
def command_mock_bot_with_db(mock_logger, minimal_config, tmp_path):
|
|
"""Command mock bot with db_manager for commands that need DB (e.g. StatsCommand)."""
|
|
bot = MagicMock()
|
|
bot.logger = mock_logger
|
|
bot.config = minimal_config
|
|
bot.translator = MagicMock()
|
|
|
|
def _mock_translate(key, **kwargs):
|
|
if kwargs:
|
|
return key + " " + " ".join(f"{k}={v}" for k, v in sorted(kwargs.items()))
|
|
return key
|
|
|
|
bot.translator.translate = Mock(side_effect=_mock_translate)
|
|
bot.translator.get_value = Mock(return_value=None)
|
|
bot.command_manager = MagicMock()
|
|
bot.command_manager.monitor_channels = ["general", "test", "emergency"]
|
|
bot.command_manager.send_response = AsyncMock(return_value=True)
|
|
bot.db_manager = MagicMock()
|
|
bot.db_manager.db_path = str(tmp_path / "test.db")
|
|
return bot
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_logger():
|
|
"""Create a mock logger for testing."""
|
|
logger = Mock()
|
|
logger.info = Mock()
|
|
logger.debug = Mock()
|
|
logger.warning = Mock()
|
|
logger.error = Mock()
|
|
return logger
|
|
|
|
|
|
@pytest.fixture
|
|
def test_config():
|
|
"""Create a test configuration with Path_Command settings."""
|
|
config = configparser.ConfigParser()
|
|
|
|
# Add Path_Command section with graph-related settings
|
|
config.add_section('Path_Command')
|
|
config.set('Path_Command', 'enabled', 'true')
|
|
config.set('Path_Command', 'graph_based_validation', 'true')
|
|
config.set('Path_Command', 'min_edge_observations', '3')
|
|
config.set('Path_Command', 'graph_write_strategy', 'immediate') # For faster tests
|
|
config.set('Path_Command', 'graph_batch_interval_seconds', '30')
|
|
config.set('Path_Command', 'graph_batch_max_pending', '100')
|
|
config.set('Path_Command', 'graph_startup_load_days', '0') # Don't load old data in tests
|
|
config.set('Path_Command', 'graph_edge_expiration_days', '7')
|
|
config.set('Path_Command', 'graph_capture_enabled', 'true')
|
|
config.set('Path_Command', 'graph_use_bidirectional', 'true')
|
|
config.set('Path_Command', 'graph_use_hop_position', 'true')
|
|
config.set('Path_Command', 'graph_multi_hop_enabled', 'true')
|
|
config.set('Path_Command', 'graph_multi_hop_max_hops', '2')
|
|
config.set('Path_Command', 'graph_geographic_combined', 'false')
|
|
config.set('Path_Command', 'graph_geographic_weight', '0.7')
|
|
config.set('Path_Command', 'graph_prefer_stored_keys', 'true')
|
|
config.set('Path_Command', 'star_bias_multiplier', '2.5')
|
|
|
|
# Add Bot section (for location if needed)
|
|
config.add_section('Bot')
|
|
config.set('Bot', 'bot_latitude', '47.6062')
|
|
config.set('Bot', 'bot_longitude', '-122.3321')
|
|
|
|
return config
|
|
|
|
|
|
@pytest.fixture
|
|
def test_db(mock_logger, tmp_path):
|
|
"""Create a file-based SQLite database for testing.
|
|
|
|
Uses tmp_path (not :memory:) so all connections share the same database.
|
|
SQLite :memory: creates a new empty DB per connection, causing isolation issues.
|
|
"""
|
|
db_path = str(tmp_path / "test.db")
|
|
|
|
# Create a minimal bot mock for DBManager
|
|
mock_bot = Mock()
|
|
mock_bot.logger = mock_logger
|
|
|
|
# Create DBManager with file-based database
|
|
db_manager = DBManager(mock_bot, db_path)
|
|
|
|
# Initialize mesh_connections table schema
|
|
db_manager.create_table('mesh_connections', '''
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
from_prefix TEXT NOT NULL,
|
|
to_prefix TEXT NOT NULL,
|
|
from_public_key TEXT,
|
|
to_public_key TEXT,
|
|
observation_count INTEGER DEFAULT 1,
|
|
first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
avg_hop_position REAL,
|
|
geographic_distance REAL,
|
|
UNIQUE(from_prefix, to_prefix)
|
|
''')
|
|
|
|
# Initialize complete_contact_tracking table schema (for repeater lookups)
|
|
db_manager.create_table('complete_contact_tracking', '''
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
public_key TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
role TEXT NOT NULL,
|
|
device_type TEXT,
|
|
first_heard TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
last_heard TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
advert_count INTEGER DEFAULT 1,
|
|
latitude REAL,
|
|
longitude REAL,
|
|
city TEXT,
|
|
state TEXT,
|
|
country TEXT,
|
|
raw_advert_data TEXT,
|
|
signal_strength REAL,
|
|
snr REAL,
|
|
hop_count INTEGER,
|
|
is_currently_tracked BOOLEAN DEFAULT 0,
|
|
last_advert_timestamp TIMESTAMP,
|
|
location_accuracy REAL,
|
|
contact_source TEXT DEFAULT 'advertisement',
|
|
out_path TEXT,
|
|
out_path_len INTEGER,
|
|
is_starred INTEGER DEFAULT 0
|
|
''')
|
|
|
|
# Create indexes (after tables are created)
|
|
# Create indexes (db_manager created tables in same db_path)
|
|
try:
|
|
with sqlite3.connect(db_path) as conn:
|
|
cursor = conn.cursor()
|
|
# Check if table exists before creating indexes
|
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='mesh_connections'")
|
|
if cursor.fetchone():
|
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_from_prefix ON mesh_connections(from_prefix)')
|
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_to_prefix ON mesh_connections(to_prefix)')
|
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_last_seen ON mesh_connections(last_seen)')
|
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='complete_contact_tracking'")
|
|
if cursor.fetchone():
|
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_complete_public_key ON complete_contact_tracking(public_key)')
|
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_complete_role ON complete_contact_tracking(role)')
|
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_complete_last_heard ON complete_contact_tracking(last_heard)')
|
|
conn.commit()
|
|
except Exception:
|
|
# Indexes are optional, continue if they fail
|
|
pass
|
|
|
|
yield db_manager
|
|
|
|
# Cleanup (tmp_path is automatically cleaned up by pytest)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_bot(mock_logger, test_config, test_db):
|
|
"""Create a mock bot instance with all necessary attributes."""
|
|
bot = Mock()
|
|
bot.logger = mock_logger
|
|
bot.config = test_config
|
|
bot.db_manager = test_db
|
|
bot.bot_root = '/tmp' # Dummy path for testing
|
|
|
|
# Mock repeater_manager if needed
|
|
bot.repeater_manager = Mock()
|
|
bot.repeater_manager.get_repeater_devices = Mock(return_value=[])
|
|
|
|
# Mock web_viewer_integration (optional, for edge notifications)
|
|
bot.web_viewer_integration = None
|
|
|
|
return bot
|
|
|
|
|
|
@pytest.fixture
|
|
def mesh_graph(mock_bot):
|
|
"""Create a MeshGraph instance for testing."""
|
|
# Ensure config is set to immediate strategy for tests
|
|
mock_bot.config.set('Path_Command', 'graph_write_strategy', 'immediate')
|
|
graph = MeshGraph(mock_bot)
|
|
# Ensure batch writer doesn't interfere with tests
|
|
if hasattr(graph, '_batch_thread') and graph._batch_thread:
|
|
graph._shutdown_event.set()
|
|
return graph
|
|
|
|
|
|
@pytest.fixture
|
|
def populated_mesh_graph(mesh_graph):
|
|
"""Create a MeshGraph instance with sample edges for testing."""
|
|
# Add some test edges
|
|
edges = [
|
|
create_test_edge('01', '7e', observation_count=5, last_seen=datetime.now()),
|
|
create_test_edge('7e', '86', observation_count=3, last_seen=datetime.now()),
|
|
create_test_edge('86', 'e0', observation_count=10, last_seen=datetime.now()),
|
|
create_test_edge('e0', '09', observation_count=2, last_seen=datetime.now()),
|
|
# Bidirectional edge
|
|
create_test_edge('01', '7a', observation_count=4, last_seen=datetime.now()),
|
|
create_test_edge('7a', '01', observation_count=4, last_seen=datetime.now()),
|
|
# Stale edge (old)
|
|
create_test_edge('7a', 'cf', observation_count=1, last_seen=datetime.now() - timedelta(days=30)),
|
|
]
|
|
populate_test_graph(mesh_graph, edges)
|
|
return mesh_graph
|