Refactor tests for PermissionManager, HostsManager, HostEntry, HostsFile, and HostsParser

- Updated test cases in test_manager.py to improve readability and consistency.
- Simplified assertions and mock setups in tests for PermissionManager.
- Enhanced test coverage for HostsManager, including edit mode and entry manipulation tests.
- Improved test structure in test_models.py for HostEntry and HostsFile, ensuring clarity in test cases.
- Refined test cases in test_parser.py for better organization and readability.
- Adjusted test_save_confirmation_modal.py to maintain consistency in mocking and assertions.
This commit is contained in:
Philip Henning 2025-08-14 17:32:02 +02:00
parent 43fa8c871a
commit 1fddff91c8
18 changed files with 1364 additions and 1038 deletions

View file

@ -16,259 +16,277 @@ 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'):
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'):
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 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
"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):
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):
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")
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):
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")
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):
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
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):
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"
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):
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
"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):
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.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):
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.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")
@ -277,225 +295,248 @@ class TestHostsManagerApp:
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):
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):
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):
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):
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"]))
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[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):
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"]))
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):
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):
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):
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:
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'):
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'):
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
@ -503,16 +544,17 @@ class TestHostsManagerApp:
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:
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()