""" Tests for the main TUI application. This module contains unit tests for the HostsManagerApp class, validating application behavior, navigation, and user interactions. """ import pytest import tempfile from unittest.mock import Mock, patch, MagicMock from pathlib import Path from textual.widgets import DataTable, Static from hosts.main import HostsManagerApp from hosts.core.models import HostEntry, HostsFile from hosts.core.parser import HostsParser from hosts.core.config import Config class TestHostsManagerApp: """Test cases for the HostsManagerApp class.""" def test_app_initialization(self): """Test application initialization.""" with patch('hosts.main.HostsParser'), patch('hosts.main.Config'): app = HostsManagerApp() assert app.title == "Hosts Manager" assert app.sub_title == "Read-only mode" assert app.edit_mode is False assert app.selected_entry_index == 0 assert app.sort_column == "" assert app.sort_ascending is True def test_app_compose_method_exists(self): """Test that app has compose method.""" with patch('hosts.main.HostsParser'), patch('hosts.main.Config'): app = HostsManagerApp() # Test that compose method exists and is callable assert hasattr(app, 'compose') assert callable(app.compose) def test_load_hosts_file_success(self): """Test successful hosts file loading.""" mock_parser = Mock(spec=HostsParser) mock_config = Mock(spec=Config) # Create test hosts file test_hosts = HostsFile() test_entry = HostEntry(ip_address="127.0.0.1", hostnames=["localhost"]) test_hosts.add_entry(test_entry) mock_parser.parse.return_value = test_hosts mock_parser.get_file_info.return_value = { 'path': '/etc/hosts', 'exists': True, 'size': 100 } with patch('hosts.main.HostsParser', return_value=mock_parser), \ patch('hosts.main.Config', return_value=mock_config): app = HostsManagerApp() app.populate_entries_table = Mock() app.update_entry_details = Mock() app.set_timer = Mock() app.load_hosts_file() # Verify hosts file was loaded assert len(app.hosts_file.entries) == 1 assert app.hosts_file.entries[0].ip_address == "127.0.0.1" mock_parser.parse.assert_called_once() def test_load_hosts_file_not_found(self): """Test handling of missing hosts file.""" mock_parser = Mock(spec=HostsParser) mock_config = Mock(spec=Config) mock_parser.parse.side_effect = FileNotFoundError("File not found") with patch('hosts.main.HostsParser', return_value=mock_parser), \ patch('hosts.main.Config', return_value=mock_config): app = HostsManagerApp() app.update_status = Mock() app.load_hosts_file() # Should handle error gracefully app.update_status.assert_called_with("Error: Hosts file not found") def test_load_hosts_file_permission_error(self): """Test handling of permission denied error.""" mock_parser = Mock(spec=HostsParser) mock_config = Mock(spec=Config) mock_parser.parse.side_effect = PermissionError("Permission denied") with patch('hosts.main.HostsParser', return_value=mock_parser), \ patch('hosts.main.Config', return_value=mock_config): app = HostsManagerApp() app.update_status = Mock() app.load_hosts_file() # Should handle error gracefully app.update_status.assert_called_with("Error: Permission denied") def test_populate_entries_table_logic(self): """Test populating DataTable logic without UI dependencies.""" mock_parser = Mock(spec=HostsParser) mock_config = Mock(spec=Config) mock_config.should_show_default_entries.return_value = True mock_config.is_default_entry.return_value = False with patch('hosts.main.HostsParser', return_value=mock_parser), \ patch('hosts.main.Config', return_value=mock_config): app = HostsManagerApp() # Mock the query_one method to return a mock table mock_table = Mock() app.query_one = Mock(return_value=mock_table) # Add test entries app.hosts_file = HostsFile() active_entry = HostEntry(ip_address="127.0.0.1", hostnames=["localhost"]) inactive_entry = HostEntry( ip_address="192.168.1.1", hostnames=["router"], is_active=False ) app.hosts_file.add_entry(active_entry) app.hosts_file.add_entry(inactive_entry) app.populate_entries_table() # Verify table methods were called mock_table.clear.assert_called_once_with(columns=True) mock_table.add_columns.assert_called_once() assert mock_table.add_row.call_count == 2 # Two entries added def test_update_entry_details_with_entry(self): """Test updating entry details pane.""" mock_parser = Mock(spec=HostsParser) mock_config = Mock(spec=Config) with patch('hosts.main.HostsParser', return_value=mock_parser), \ patch('hosts.main.Config', return_value=mock_config): app = HostsManagerApp() # Mock the query_one method mock_details = Mock() app.query_one = Mock(return_value=mock_details) # Add test entry app.hosts_file = HostsFile() test_entry = HostEntry( ip_address="127.0.0.1", hostnames=["localhost", "local"], comment="Test comment" ) app.hosts_file.add_entry(test_entry) app.selected_entry_index = 0 app.update_entry_details() # Verify update was called with content containing entry details mock_details.update.assert_called_once() call_args = mock_details.update.call_args[0][0] assert "127.0.0.1" in call_args assert "localhost, local" in call_args assert "Test comment" in call_args def test_update_entry_details_no_entries(self): """Test updating entry details with no entries.""" mock_parser = Mock(spec=HostsParser) mock_config = Mock(spec=Config) with patch('hosts.main.HostsParser', return_value=mock_parser), \ patch('hosts.main.Config', return_value=mock_config): app = HostsManagerApp() # Mock the query_one method mock_details = Mock() app.query_one = Mock(return_value=mock_details) app.hosts_file = HostsFile() app.update_entry_details() # Verify update was called with "No entries loaded" mock_details.update.assert_called_once_with("No entries loaded") def test_update_status_default(self): """Test status bar update with default information.""" mock_parser = Mock(spec=HostsParser) mock_config = Mock(spec=Config) mock_parser.get_file_info.return_value = { 'path': '/etc/hosts', 'exists': True, 'size': 100 } with patch('hosts.main.HostsParser', return_value=mock_parser), \ patch('hosts.main.Config', return_value=mock_config): app = HostsManagerApp() # Mock the query_one method mock_status = Mock() app.query_one = Mock(return_value=mock_status) # Add test entries app.hosts_file = HostsFile() app.hosts_file.add_entry(HostEntry(ip_address="127.0.0.1", hostnames=["localhost"])) app.hosts_file.add_entry(HostEntry( ip_address="192.168.1.1", hostnames=["router"], is_active=False )) app.update_status() # Verify status was updated mock_status.update.assert_called_once() call_args = mock_status.update.call_args[0][0] assert "Read-only mode" in call_args assert "2 entries" in call_args assert "1 active" in call_args def test_update_status_custom_message(self): """Test status bar update with custom message.""" mock_parser = Mock(spec=HostsParser) mock_config = Mock(spec=Config) with patch('hosts.main.HostsParser', return_value=mock_parser), \ patch('hosts.main.Config', return_value=mock_config): app = HostsManagerApp() # Mock the query_one method mock_status = Mock() app.query_one = Mock(return_value=mock_status) app.update_status("Custom status message") # Verify status was updated with custom message mock_status.update.assert_called_once_with("Custom status message") def test_action_reload(self): """Test reload action.""" mock_parser = Mock(spec=HostsParser) mock_config = Mock(spec=Config) with patch('hosts.main.HostsParser', return_value=mock_parser), \ patch('hosts.main.Config', return_value=mock_config): app = HostsManagerApp() app.load_hosts_file = Mock() app.update_status = Mock() app.action_reload() app.load_hosts_file.assert_called_once() app.update_status.assert_called_with("Hosts file reloaded") def test_action_help(self): """Test help action.""" mock_parser = Mock(spec=HostsParser) mock_config = Mock(spec=Config) with patch('hosts.main.HostsParser', return_value=mock_parser), \ patch('hosts.main.Config', return_value=mock_config): app = HostsManagerApp() app.update_status = Mock() app.action_help() # Should update status with help message app.update_status.assert_called_once() call_args = app.update_status.call_args[0][0] assert "Help:" in call_args def test_action_config(self): """Test config action opens modal.""" mock_parser = Mock(spec=HostsParser) mock_config = Mock(spec=Config) with patch('hosts.main.HostsParser', return_value=mock_parser), \ patch('hosts.main.Config', return_value=mock_config): app = HostsManagerApp() app.push_screen = Mock() app.action_config() # Should push config modal screen app.push_screen.assert_called_once() args = app.push_screen.call_args[0] assert len(args) >= 1 # ConfigModal instance def test_action_sort_by_ip_ascending(self): """Test sorting by IP address in ascending order.""" mock_parser = Mock(spec=HostsParser) mock_config = Mock(spec=Config) with patch('hosts.main.HostsParser', return_value=mock_parser), \ patch('hosts.main.Config', return_value=mock_config): app = HostsManagerApp() # Add test entries in reverse order app.hosts_file = HostsFile() app.hosts_file.add_entry(HostEntry(ip_address="192.168.1.1", hostnames=["router"])) app.hosts_file.add_entry(HostEntry(ip_address="127.0.0.1", hostnames=["localhost"])) app.hosts_file.add_entry(HostEntry(ip_address="10.0.0.1", hostnames=["test"])) app.populate_entries_table = Mock() app.update_status = Mock() app.action_sort_by_ip() # Check that entries are sorted with default entries on top assert app.hosts_file.entries[0].ip_address == "127.0.0.1" # Default entry first assert app.hosts_file.entries[1].ip_address == "10.0.0.1" # Then sorted non-defaults assert app.hosts_file.entries[2].ip_address == "192.168.1.1" assert app.sort_column == "ip" assert app.sort_ascending is True app.populate_entries_table.assert_called_once() def test_action_sort_by_hostname_ascending(self): """Test sorting by hostname in ascending order.""" mock_parser = Mock(spec=HostsParser) mock_config = Mock(spec=Config) with patch('hosts.main.HostsParser', return_value=mock_parser), \ patch('hosts.main.Config', return_value=mock_config): app = HostsManagerApp() # Add test entries in reverse alphabetical order app.hosts_file = HostsFile() app.hosts_file.add_entry(HostEntry(ip_address="127.0.0.1", hostnames=["zebra"])) app.hosts_file.add_entry(HostEntry(ip_address="192.168.1.1", hostnames=["alpha"])) app.hosts_file.add_entry(HostEntry(ip_address="10.0.0.1", hostnames=["beta"])) app.populate_entries_table = Mock() app.update_status = Mock() app.action_sort_by_hostname() # Check that entries are sorted alphabetically assert app.hosts_file.entries[0].hostnames[0] == "alpha" assert app.hosts_file.entries[1].hostnames[0] == "beta" assert app.hosts_file.entries[2].hostnames[0] == "zebra" assert app.sort_column == "hostname" assert app.sort_ascending is True app.populate_entries_table.assert_called_once() def test_data_table_row_highlighted_event(self): """Test DataTable row highlighting event handling.""" mock_parser = Mock(spec=HostsParser) mock_config = Mock(spec=Config) with patch('hosts.main.HostsParser', return_value=mock_parser), \ patch('hosts.main.Config', return_value=mock_config): app = HostsManagerApp() app.update_entry_details = Mock() # Mock the display_index_to_actual_index method to return the same index app.display_index_to_actual_index = Mock(return_value=2) # Create mock event with required parameters mock_table = Mock() mock_table.id = "entries-table" event = Mock() event.data_table = mock_table event.cursor_row = 2 app.on_data_table_row_highlighted(event) # Should update selected index and details assert app.selected_entry_index == 2 app.update_entry_details.assert_called_once() app.display_index_to_actual_index.assert_called_once_with(2) def test_data_table_header_selected_ip_column(self): """Test DataTable header selection for IP column.""" mock_parser = Mock(spec=HostsParser) mock_config = Mock(spec=Config) with patch('hosts.main.HostsParser', return_value=mock_parser), \ patch('hosts.main.Config', return_value=mock_config): app = HostsManagerApp() app.action_sort_by_ip = Mock() # Create mock event for IP column mock_table = Mock() mock_table.id = "entries-table" event = Mock() event.data_table = mock_table event.column_key = "IP Address" app.on_data_table_header_selected(event) app.action_sort_by_ip.assert_called_once() def test_restore_cursor_position_logic(self): """Test cursor position restoration logic.""" mock_parser = Mock(spec=HostsParser) mock_config = Mock(spec=Config) with patch('hosts.main.HostsParser', return_value=mock_parser), \ patch('hosts.main.Config', return_value=mock_config): app = HostsManagerApp() # Mock the query_one method to avoid UI dependencies mock_table = Mock() app.query_one = Mock(return_value=mock_table) app.update_entry_details = Mock() # Add test entries app.hosts_file = HostsFile() entry1 = HostEntry(ip_address="127.0.0.1", hostnames=["localhost"]) entry2 = HostEntry(ip_address="192.168.1.1", hostnames=["router"]) app.hosts_file.add_entry(entry1) app.hosts_file.add_entry(entry2) # Test the logic without UI dependencies # Find the index of entry2 target_index = None for i, entry in enumerate(app.hosts_file.entries): if entry.ip_address == entry2.ip_address and entry.hostnames == entry2.hostnames: target_index = i break # Should find the matching entry at index 1 assert target_index == 1 def test_app_bindings_defined(self): """Test that application has expected key bindings.""" with patch('hosts.main.HostsParser'), patch('hosts.main.Config'): app = HostsManagerApp() # Check that bindings are defined assert len(app.BINDINGS) >= 6 # Check specific bindings exist (handle both Binding objects and tuples) binding_keys = [] for binding in app.BINDINGS: if hasattr(binding, 'key'): # Binding object binding_keys.append(binding.key) elif isinstance(binding, tuple) and len(binding) >= 1: # Tuple format (key, action, description) binding_keys.append(binding[0]) assert "q" in binding_keys assert "r" in binding_keys assert "h" in binding_keys assert "i" in binding_keys assert "n" in binding_keys assert "c" in binding_keys assert "ctrl+c" in binding_keys def test_main_function(self): """Test main entry point function.""" with patch('hosts.main.HostsManagerApp') as mock_app_class: mock_app = Mock() mock_app_class.return_value = mock_app from hosts.main import main main() # Should create and run app mock_app_class.assert_called_once() mock_app.run.assert_called_once()