585 lines
22 KiB
Python
585 lines
22 KiB
Python
"""
|
|
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 disabled input widgets
|
|
mock_details_display = Mock()
|
|
mock_edit_form = Mock()
|
|
mock_ip_input = Mock()
|
|
mock_hostname_input = Mock()
|
|
mock_comment_input = Mock()
|
|
mock_active_checkbox = Mock()
|
|
|
|
def mock_query_one(selector, widget_type=None):
|
|
if selector == "#entry-details-display":
|
|
return mock_details_display
|
|
elif selector == "#entry-edit-form":
|
|
return mock_edit_form
|
|
elif selector == "#details-ip-input":
|
|
return mock_ip_input
|
|
elif selector == "#details-hostname-input":
|
|
return mock_hostname_input
|
|
elif selector == "#details-comment-input":
|
|
return mock_comment_input
|
|
elif selector == "#details-active-checkbox":
|
|
return mock_active_checkbox
|
|
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 input widgets were updated with entry data
|
|
mock_details_display.remove_class.assert_called_with("hidden")
|
|
mock_edit_form.add_class.assert_called_with("hidden")
|
|
assert mock_ip_input.value == "127.0.0.1"
|
|
assert mock_hostname_input.value == "localhost, local"
|
|
assert mock_comment_input.value == "Test comment"
|
|
assert mock_active_checkbox.value
|
|
|
|
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 disabled input widgets
|
|
mock_details_display = Mock()
|
|
mock_edit_form = Mock()
|
|
mock_ip_input = Mock()
|
|
mock_hostname_input = Mock()
|
|
mock_comment_input = Mock()
|
|
mock_active_checkbox = Mock()
|
|
|
|
def mock_query_one(selector, widget_type=None):
|
|
if selector == "#entry-details-display":
|
|
return mock_details_display
|
|
elif selector == "#entry-edit-form":
|
|
return mock_edit_form
|
|
elif selector == "#details-ip-input":
|
|
return mock_ip_input
|
|
elif selector == "#details-hostname-input":
|
|
return mock_hostname_input
|
|
elif selector == "#details-comment-input":
|
|
return mock_comment_input
|
|
elif selector == "#details-active-checkbox":
|
|
return mock_active_checkbox
|
|
return Mock()
|
|
|
|
app.query_one = mock_query_one
|
|
app.hosts_file = HostsFile()
|
|
|
|
app.update_entry_details()
|
|
|
|
# Verify widgets show empty state placeholders
|
|
mock_details_display.remove_class.assert_called_with("hidden")
|
|
mock_edit_form.add_class.assert_called_with("hidden")
|
|
assert mock_ip_input.value == ""
|
|
assert mock_ip_input.placeholder == "No entries loaded"
|
|
assert mock_hostname_input.value == ""
|
|
assert mock_hostname_input.placeholder == "No entries loaded"
|
|
assert mock_comment_input.value == ""
|
|
assert mock_comment_input.placeholder == "No entries loaded"
|
|
assert not mock_active_checkbox.value
|
|
|
|
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.action_show_help_panel = Mock()
|
|
|
|
app.action_help()
|
|
|
|
# Should call the built-in help action
|
|
app.action_show_help_panel.assert_called_once()
|
|
|
|
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 "question_mark" in binding_keys # Help binding (? key)
|
|
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()
|