Skip to main content
Glama
test_sitemap.py16.2 kB
"""Tests for network sitemap functionality.""" import json from unittest.mock import patch import pytest from src.homelab_mcp.sitemap import ( NetworkDevice, NetworkSiteMap, bulk_discover_and_store, discover_and_store, ) @pytest.fixture def temp_db(): """Create an in-memory database for testing.""" yield ":memory:" @pytest.fixture def sitemap(temp_db): """Create a NetworkSiteMap instance with temporary database.""" return NetworkSiteMap(db_path=temp_db, db_type="sqlite") @pytest.fixture def sample_ssh_discovery_success(): """Sample successful SSH discovery output.""" return json.dumps( { "status": "success", "hostname": "test-server", "connection_ip": "192.168.1.100", "data": { "cpu": {"model": "Intel Core i7-9700K", "cores": "8"}, "memory": { "total": "16G", "used": "8G", "free": "6G", "available": "12G", }, "disk": { "filesystem": "/dev/sda1", "size": "1T", "used": "400G", "available": "500G", "use_percent": "45%", "mount": "/", }, "network": [ {"name": "eth0", "state": "UP", "addresses": ["192.168.1.100"]} ], "uptime": "up 5 days, 2 hours, 30 minutes", "os": "Ubuntu 22.04.3 LTS", }, } ) @pytest.fixture def sample_ssh_discovery_error(): """Sample failed SSH discovery output.""" return json.dumps( { "status": "error", "connection_ip": "192.168.1.101", "error": "SSH authentication failed", } ) class TestNetworkDevice: """Test NetworkDevice dataclass.""" def test_network_device_creation(self): """Test creating a NetworkDevice instance.""" device = NetworkDevice( hostname="test-host", connection_ip="192.168.1.10", last_seen="2024-01-01T12:00:00", status="success", cpu_model="Intel i7", cpu_cores=4, ) assert device.hostname == "test-host" assert device.connection_ip == "192.168.1.10" assert device.status == "success" assert device.cpu_cores == 4 class TestNetworkSiteMap: """Test NetworkSiteMap class.""" def test_init_creates_database(self, temp_db): """Test that initialization creates database tables.""" sitemap = NetworkSiteMap(db_path=temp_db, db_type="sqlite") # For in-memory database, just test that we can get empty devices list # This confirms the database was initialized successfully devices = sitemap.get_all_devices() assert devices == [] def test_parse_discovery_output_success( self, sitemap, sample_ssh_discovery_success ): """Test parsing successful SSH discovery output.""" device = sitemap.parse_discovery_output(sample_ssh_discovery_success) assert device.hostname == "test-server" assert device.connection_ip == "192.168.1.100" assert device.status == "success" assert device.cpu_model == "Intel Core i7-9700K" assert device.cpu_cores == 8 assert device.memory_total == "16G" assert device.memory_used == "8G" assert device.disk_size == "1T" assert device.disk_use_percent == "45%" assert device.os_info == "Ubuntu 22.04.3 LTS" assert device.uptime == "up 5 days, 2 hours, 30 minutes" # Test network interfaces are stored as JSON network_data = json.loads(device.network_interfaces) assert len(network_data) == 1 assert network_data[0]["name"] == "eth0" assert network_data[0]["addresses"] == ["192.168.1.100"] def test_parse_discovery_output_error(self, sitemap, sample_ssh_discovery_error): """Test parsing failed SSH discovery output.""" device = sitemap.parse_discovery_output(sample_ssh_discovery_error) assert device.connection_ip == "192.168.1.101" assert device.status == "error" assert device.error_message == "SSH authentication failed" assert device.cpu_model is None assert device.memory_total is None def test_parse_invalid_json(self, sitemap): """Test parsing invalid JSON creates error device.""" device = sitemap.parse_discovery_output("invalid json") assert device.status == "error" assert "JSON parse error" in device.error_message def test_store_device_new(self, sitemap, sample_ssh_discovery_success): """Test storing a new device.""" device = sitemap.parse_discovery_output(sample_ssh_discovery_success) device_id = sitemap.store_device(device) assert isinstance(device_id, int) assert device_id > 0 # Verify device was stored devices = sitemap.get_all_devices() assert len(devices) == 1 assert devices[0]["hostname"] == "test-server" assert devices[0]["cpu_cores"] == 8 def test_store_device_update_existing(self, sitemap, sample_ssh_discovery_success): """Test updating an existing device.""" # Store initial device device = sitemap.parse_discovery_output(sample_ssh_discovery_success) device_id1 = sitemap.store_device(device) # Update device with new information updated_data = json.loads(sample_ssh_discovery_success) updated_data["data"]["memory"]["used"] = "12G" # Changed memory usage updated_device = sitemap.parse_discovery_output(json.dumps(updated_data)) device_id2 = sitemap.store_device(updated_device) # Should be same device ID (updated, not new) assert device_id1 == device_id2 # Verify only one device exists with updated info devices = sitemap.get_all_devices() assert len(devices) == 1 assert devices[0]["memory_used"] == "12G" def test_store_discovery_history(self, sitemap, sample_ssh_discovery_success): """Test storing discovery history.""" device = sitemap.parse_discovery_output(sample_ssh_discovery_success) device_id = sitemap.store_device(device) # Store history sitemap.store_discovery_history(device_id, sample_ssh_discovery_success) # Get history changes = sitemap.get_device_changes(device_id) assert len(changes) == 1 assert changes[0]["data"]["hostname"] == "test-server" def test_store_discovery_history_duplicate_detection( self, sitemap, sample_ssh_discovery_success ): """Test that duplicate discovery data is not stored.""" device = sitemap.parse_discovery_output(sample_ssh_discovery_success) device_id = sitemap.store_device(device) # Store same history twice sitemap.store_discovery_history(device_id, sample_ssh_discovery_success) sitemap.store_discovery_history(device_id, sample_ssh_discovery_success) # Should only have one entry changes = sitemap.get_device_changes(device_id) assert len(changes) == 1 def test_analyze_network_topology_empty(self, sitemap): """Test network analysis with no devices.""" analysis = sitemap.analyze_network_topology() assert analysis["total_devices"] == 0 assert analysis["online_devices"] == 0 assert analysis["offline_devices"] == 0 assert analysis["operating_systems"] == {} assert analysis["network_segments"] == {} def test_analyze_network_topology_with_devices( self, sitemap, sample_ssh_discovery_success ): """Test network analysis with devices.""" # Add a successful device device = sitemap.parse_discovery_output(sample_ssh_discovery_success) sitemap.store_device(device) # Add a failed device error_data = json.dumps( { "status": "error", "connection_ip": "192.168.1.102", "error": "Connection timeout", } ) error_device = sitemap.parse_discovery_output(error_data) sitemap.store_device(error_device) analysis = sitemap.analyze_network_topology() assert analysis["total_devices"] == 2 assert analysis["online_devices"] == 1 assert analysis["offline_devices"] == 1 assert "Ubuntu 22.04.3 LTS" in analysis["operating_systems"] assert analysis["operating_systems"]["Ubuntu 22.04.3 LTS"] == 1 assert "192.168.1.0/24" in analysis["network_segments"] def test_suggest_deployments_empty(self, sitemap): """Test deployment suggestions with no devices.""" suggestions = sitemap.suggest_deployments() assert suggestions["load_balancer_candidates"] == [] assert suggestions["database_candidates"] == [] assert suggestions["monitoring_targets"] == [] assert suggestions["upgrade_recommendations"] == [] def test_suggest_deployments_with_devices(self, sitemap): """Test deployment suggestions with capable devices.""" # Create a high-spec device high_spec_data = { "status": "success", "hostname": "high-spec-server", "connection_ip": "192.168.1.200", "data": { "cpu": {"cores": "16"}, "memory": {"total": "32G"}, "disk": {"use_percent": "20%"}, "os": "Ubuntu 22.04.3 LTS", }, } device = sitemap.parse_discovery_output(json.dumps(high_spec_data)) sitemap.store_device(device) suggestions = sitemap.suggest_deployments() # Should suggest this server for load balancing and database assert len(suggestions["load_balancer_candidates"]) == 1 assert ( suggestions["load_balancer_candidates"][0]["hostname"] == "high-spec-server" ) assert len(suggestions["database_candidates"]) == 1 assert suggestions["database_candidates"][0]["hostname"] == "high-spec-server" assert len(suggestions["monitoring_targets"]) == 1 class TestAsyncFunctions: """Test async discovery functions.""" @pytest.mark.asyncio @patch("src.homelab_mcp.ssh_tools.ssh_discover_system") async def test_discover_and_store( self, mock_ssh_discover, temp_db, sample_ssh_discovery_success ): """Test discover_and_store function.""" mock_ssh_discover.return_value = sample_ssh_discovery_success sitemap = NetworkSiteMap(db_path=temp_db, db_type="sqlite") result = await discover_and_store( sitemap, hostname="test-host", username="test-user", password="test-pass" ) # Verify SSH discovery was called mock_ssh_discover.assert_called_once_with( "test-host", "test-user", "test-pass", None, 22 ) # Verify result result_data = json.loads(result) assert result_data["status"] == "success" assert result_data["hostname"] == "test-server" assert result_data["discovery_status"] == "success" assert "device_id" in result_data # Verify device was stored devices = sitemap.get_all_devices() assert len(devices) == 1 assert devices[0]["hostname"] == "test-server" @pytest.mark.asyncio @patch("src.homelab_mcp.sitemap.discover_and_store") async def test_bulk_discover_and_store(self, mock_discover_and_store, temp_db): """Test bulk_discover_and_store function.""" # Mock successful discovery mock_discover_and_store.return_value = json.dumps( {"status": "success", "device_id": 1, "hostname": "test-host"} ) sitemap = NetworkSiteMap(db_path=temp_db, db_type="sqlite") targets = [ {"hostname": "host1", "username": "user1", "password": "pass1"}, {"hostname": "host2", "username": "user2", "password": "pass2"}, ] result = await bulk_discover_and_store(sitemap, targets) # Verify both targets were processed assert mock_discover_and_store.call_count == 2 result_data = json.loads(result) assert result_data["status"] == "success" assert result_data["total_targets"] == 2 assert len(result_data["results"]) == 2 @pytest.mark.asyncio @patch("src.homelab_mcp.sitemap.discover_and_store") async def test_bulk_discover_and_store_with_errors( self, mock_discover_and_store, temp_db ): """Test bulk discovery handling errors.""" # Mock one success, one failure mock_discover_and_store.side_effect = [ json.dumps({"status": "success", "device_id": 1, "hostname": "host1"}), Exception("Connection failed"), ] sitemap = NetworkSiteMap(db_path=temp_db, db_type="sqlite") targets = [ {"hostname": "host1", "username": "user1"}, {"hostname": "host2", "username": "user2"}, ] result = await bulk_discover_and_store(sitemap, targets) result_data = json.loads(result) assert result_data["status"] == "success" assert result_data["total_targets"] == 2 assert len(result_data["results"]) == 2 # First should succeed, second should have error assert result_data["results"][0]["status"] == "success" assert result_data["results"][1]["status"] == "error" assert "Connection failed" in result_data["results"][1]["error"] class TestDatabaseOperations: """Test database-specific operations.""" def test_database_schema_creation(self, temp_db): """Test that all required tables are created.""" sitemap = NetworkSiteMap(db_path=temp_db, db_type="sqlite") # Use the existing database adapter connection cursor = sitemap.db_adapter.connection.cursor() # Check that devices table exists cursor.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='devices'" ) assert cursor.fetchone() is not None # Check that discovery_history table exists cursor.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='discovery_history'" ) assert cursor.fetchone() is not None # Check that indexes exist cursor.execute("SELECT name FROM sqlite_master WHERE type='index'") indexes = [row[0] for row in cursor.fetchall()] assert any("idx_devices_hostname_ip" in idx for idx in indexes) assert any("idx_history_device_id" in idx for idx in indexes) def test_device_unique_constraint(self, sitemap, sample_ssh_discovery_success): """Test that hostname+connection_ip combination is unique.""" device = sitemap.parse_discovery_output(sample_ssh_discovery_success) # Store device twice - should update, not create duplicate device_id1 = sitemap.store_device(device) device_id2 = sitemap.store_device(device) assert device_id1 == device_id2 devices = sitemap.get_all_devices() assert len(devices) == 1 def test_get_device_changes_limit(self, sitemap, sample_ssh_discovery_success): """Test limiting device change history.""" device = sitemap.parse_discovery_output(sample_ssh_discovery_success) device_id = sitemap.store_device(device) # Store multiple different discovery results for i in range(5): modified_data = json.loads(sample_ssh_discovery_success) modified_data["data"]["memory"]["used"] = f"{i + 4}G" sitemap.store_discovery_history(device_id, json.dumps(modified_data)) # Test limit changes = sitemap.get_device_changes(device_id, limit=3) assert len(changes) <= 3 # Test default limit changes = sitemap.get_device_changes(device_id) assert len(changes) <= 10

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/washyu/mcp_python_server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server