""" Tests for the main TUI application. This module contains unit tests for the HostsManagerApp class, validating application behavior, navigation, and user interactions. """ from unittest.mock import Mock, patch from hosts.tui.app 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.tui.app.HostsParser'), patch('hosts.tui.app.Config'): app = HostsManagerApp() assert app.title == "/etc/hosts Manager" assert app.sub_title == "" # Now set by update_status 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.tui.app.HostsParser'), patch('hosts.tui.app.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.tui.app.HostsParser', return_value=mock_parser), \ patch('hosts.tui.app.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("Hosts file not found") with patch('hosts.tui.app.HostsParser', return_value=mock_parser), \ patch('hosts.tui.app.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 loading hosts file: 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.tui.app.HostsParser', return_value=mock_parser), \ patch('hosts.tui.app.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 loading hosts file: 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.tui.app.HostsParser', return_value=mock_parser), \ patch('hosts.tui.app.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) mock_config.should_show_default_entries.return_value = True with patch('hosts.tui.app.HostsParser', return_value=mock_parser), \ patch('hosts.tui.app.Config', return_value=mock_config): app = HostsManagerApp() # Mock the query_one method to return DataTable mock mock_details_table = Mock() mock_details_table.columns = [] # Mock empty columns list mock_edit_form = Mock() def mock_query_one(selector, widget_type=None): if selector == "#entry-details-table": return mock_details_table elif selector == "#entry-edit-form": return mock_edit_form return Mock() app.query_one = mock_query_one # 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 DataTable operations were called mock_details_table.remove_class.assert_called_with("hidden") mock_edit_form.add_class.assert_called_with("hidden") mock_details_table.clear.assert_called_once() mock_details_table.add_column.assert_called() mock_details_table.add_row.assert_called() 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.tui.app.HostsParser', return_value=mock_parser), \ patch('hosts.tui.app.Config', return_value=mock_config): app = HostsManagerApp() # Mock the query_one method to return DataTable mock mock_details_table = Mock() mock_details_table.columns = [] # Mock empty columns list mock_edit_form = Mock() def mock_query_one(selector, widget_type=None): if selector == "#entry-details-table": return mock_details_table elif selector == "#entry-edit-form": return mock_edit_form return Mock() app.query_one = mock_query_one app.hosts_file = HostsFile() app.update_entry_details() # Verify DataTable operations were called for empty state mock_details_table.remove_class.assert_called_with("hidden") mock_edit_form.add_class.assert_called_with("hidden") mock_details_table.clear.assert_called_once() mock_details_table.add_column.assert_called_with("Field", key="field") mock_details_table.add_row.assert_called_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.tui.app.HostsParser', return_value=mock_parser), \ patch('hosts.tui.app.Config', return_value=mock_config): app = HostsManagerApp() # 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 sub_title was set correctly assert "Read-only mode" in app.sub_title assert "2 entries" in app.sub_title assert "1 active" in app.sub_title 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.tui.app.HostsParser', return_value=mock_parser), \ patch('hosts.tui.app.Config', return_value=mock_config): app = HostsManagerApp() # Mock set_timer and query_one to avoid event loop and UI issues app.set_timer = Mock() mock_status_bar = Mock() app.query_one = Mock(return_value=mock_status_bar) # Add test hosts_file for subtitle generation 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("Custom status message") # Verify status bar was updated with custom message mock_status_bar.update.assert_called_with("Custom status message") mock_status_bar.remove_class.assert_called_with("hidden") # Verify subtitle shows current status (not the custom message) assert "2 entries" in app.sub_title assert "Read-only mode" in app.sub_title # Verify timer was set for auto-clearing app.set_timer.assert_called_once() def test_action_reload(self): """Test reload action.""" mock_parser = Mock(spec=HostsParser) mock_config = Mock(spec=Config) with patch('hosts.tui.app.HostsParser', return_value=mock_parser), \ patch('hosts.tui.app.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.tui.app.HostsParser', return_value=mock_parser), \ patch('hosts.tui.app.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.tui.app.HostsParser', return_value=mock_parser), \ patch('hosts.tui.app.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.tui.app.HostsParser', return_value=mock_parser), \ patch('hosts.tui.app.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"])) # Mock the table_handler methods to avoid UI queries app.table_handler.populate_entries_table = Mock() app.table_handler.restore_cursor_position = Mock() app.update_status = Mock() app.action_sort_by_ip() # Check that entries are sorted by IP address assert app.hosts_file.entries[0].ip_address == "10.0.0.1" # Sorted by IP assert app.hosts_file.entries[1].ip_address == "127.0.0.1" assert app.hosts_file.entries[2].ip_address == "192.168.1.1" assert app.sort_column == "ip" assert app.sort_ascending is True app.table_handler.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.tui.app.HostsParser', return_value=mock_parser), \ patch('hosts.tui.app.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"])) # Mock the table_handler methods to avoid UI queries app.table_handler.populate_entries_table = Mock() app.table_handler.restore_cursor_position = 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.table_handler.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.tui.app.HostsParser', return_value=mock_parser), \ patch('hosts.tui.app.Config', return_value=mock_config): app = HostsManagerApp() # Mock the details_handler and table_handler methods app.details_handler.update_entry_details = Mock() app.table_handler.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.details_handler.update_entry_details.assert_called_once() app.table_handler.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.tui.app.HostsParser', return_value=mock_parser), \ patch('hosts.tui.app.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.tui.app.HostsParser', return_value=mock_parser), \ patch('hosts.tui.app.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.tui.app.HostsParser'), patch('hosts.tui.app.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()