hosts/tests/test_main.py

993 lines
38 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()
# Mock the query_one method to avoid UI dependencies
mock_footer = Mock()
app.query_one = Mock(return_value=mock_footer)
# 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 footer status was updated
mock_footer.set_status.assert_called_once()
status_call = mock_footer.set_status.call_args[0][0]
assert "Read-only" in status_call
assert "2 entries" in status_call
assert "1 active" in status_call
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()
mock_footer = Mock()
def mock_query_one(selector, widget_type=None):
if selector == "#status-bar":
return mock_status_bar
elif selector == "#custom-footer":
return mock_footer
return Mock()
app.query_one = mock_query_one
# Add test hosts_file for footer status 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 footer status was updated with current status (not the custom message)
mock_footer.set_status.assert_called_once()
footer_status = mock_footer.set_status.call_args[0][0]
assert "2 entries" in footer_status
assert "Read-only" in footer_status
# 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_radio_set_event_handling_ip_entry(self):
"""Test radio set event handling for IP entry type."""
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.edit_handler.handle_entry_type_change = Mock()
# Create mock radio set event for IP entry
mock_radio_set = Mock()
mock_radio_set.id = "edit-entry-type-radio"
mock_pressed_radio = Mock()
mock_pressed_radio.id = "edit-ip-entry-radio"
event = Mock()
event.radio_set = mock_radio_set
event.pressed = mock_pressed_radio
app.on_radio_set_changed(event)
# Should handle IP entry type change
app.edit_handler.handle_entry_type_change.assert_called_once_with("ip")
def test_radio_set_event_handling_dns_entry(self):
"""Test radio set event handling for DNS entry type."""
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.edit_handler.handle_entry_type_change = Mock()
# Create mock radio set event for DNS entry
mock_radio_set = Mock()
mock_radio_set.id = "edit-entry-type-radio"
mock_pressed_radio = Mock()
mock_pressed_radio.id = "edit-dns-entry-radio"
event = Mock()
event.radio_set = mock_radio_set
event.pressed = mock_pressed_radio
app.on_radio_set_changed(event)
# Should handle DNS entry type change
app.edit_handler.handle_entry_type_change.assert_called_once_with("dns")
def test_entry_type_detection_ip_entry(self):
"""Test entry type detection for IP 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()
# Add IP entry (no DNS name)
app.hosts_file = HostsFile()
ip_entry = HostEntry(ip_address="127.0.0.1", hostnames=["localhost"])
app.hosts_file.add_entry(ip_entry)
app.selected_entry_index = 0
entry_type = app.edit_handler.get_current_entry_type()
assert entry_type == "ip"
def test_entry_type_detection_dns_entry(self):
"""Test entry type detection for DNS 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()
# Add DNS entry with DNS name
app.hosts_file = HostsFile()
dns_entry = HostEntry(ip_address="0.0.0.0", hostnames=["example"])
dns_entry.dns_name = "example.com"
app.hosts_file.add_entry(dns_entry)
app.selected_entry_index = 0
entry_type = app.edit_handler.get_current_entry_type()
assert entry_type == "dns"
def test_field_visibility_ip_type(self):
"""Test field visibility logic for IP entry type."""
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 section elements
mock_ip_section = Mock()
mock_dns_section = Mock()
def mock_query_one(selector):
if selector == "#edit-ip-section":
return mock_ip_section
elif selector == "#edit-dns-section":
return mock_dns_section
return Mock()
app.query_one = mock_query_one
app.edit_handler.update_field_visibility(show_ip=True, show_dns=False)
# IP section should be visible, DNS section hidden
mock_ip_section.remove_class.assert_called_with("hidden")
mock_dns_section.add_class.assert_called_with("hidden")
def test_field_visibility_dns_type(self):
"""Test field visibility logic for DNS entry type."""
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 section elements
mock_ip_section = Mock()
mock_dns_section = Mock()
def mock_query_one(selector):
if selector == "#edit-ip-section":
return mock_ip_section
elif selector == "#edit-dns-section":
return mock_dns_section
return Mock()
app.query_one = mock_query_one
app.edit_handler.update_field_visibility(show_ip=False, show_dns=True)
# DNS section should be visible, IP section hidden
mock_ip_section.add_class.assert_called_with("hidden")
mock_dns_section.remove_class.assert_called_with("hidden")
def test_populate_edit_form_with_ip_type_detection(self):
"""Test edit form population with IP type detection."""
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.entry_edit_mode = True
app.set_timer = Mock() # Mock set_timer to avoid event loop issues
# Add IP entry
app.hosts_file = HostsFile()
ip_entry = HostEntry(ip_address="127.0.0.1", hostnames=["localhost"])
app.hosts_file.add_entry(ip_entry)
app.selected_entry_index = 0
# Mock radio set and buttons
mock_radio_set = Mock()
mock_ip_radio = Mock()
mock_dns_radio = Mock()
def mock_query_one(selector):
if selector == "#edit-entry-type-radio":
return mock_radio_set
elif selector == "#edit-ip-entry-radio":
return mock_ip_radio
elif selector == "#edit-dns-entry-radio":
return mock_dns_radio
return Mock()
app.query_one = mock_query_one
app.edit_handler.handle_entry_type_change = Mock()
# Test that the method can be called without errors
try:
app.edit_handler.populate_edit_form_with_type_detection()
# Method executed successfully
assert True
except Exception as e:
# Method should not raise exceptions
assert False, f"Method raised unexpected exception: {e}"
def test_populate_edit_form_with_dns_type_detection(self):
"""Test edit form population with DNS type detection."""
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.entry_edit_mode = True
# Add DNS entry
app.hosts_file = HostsFile()
dns_entry = HostEntry(ip_address="0.0.0.0", hostnames=["example"])
dns_entry.dns_name = "example.com"
app.hosts_file.add_entry(dns_entry)
app.selected_entry_index = 0
# Mock radio set, buttons, and DNS input with proper value tracking
mock_radio_set = Mock()
mock_ip_radio = Mock()
mock_dns_radio = Mock()
# Use a simple object to track value assignment
class MockDNSInput:
def __init__(self):
self.value = ""
mock_dns_input = MockDNSInput()
def mock_query_one(selector, widget_type=None):
if selector == "#edit-entry-type-radio":
return mock_radio_set
elif selector == "#edit-ip-entry-radio":
return mock_ip_radio
elif selector == "#edit-dns-entry-radio":
return mock_dns_radio
elif selector == "#dns-name-input":
return mock_dns_input
return Mock()
app.query_one = mock_query_one
app.edit_handler.handle_entry_type_change = Mock()
# Mock the set_timer method to avoid event loop issues in tests
with patch.object(app, 'set_timer') as mock_set_timer:
app.edit_handler.populate_edit_form_with_type_detection()
# Verify timer was set with the correct callback
mock_set_timer.assert_called_once_with(0.1, app.edit_handler._delayed_radio_setup)
# Manually call the delayed setup to test the actual logic
app.edit_handler._delayed_radio_setup()
# Verify that the DNS radio was set to True (which should be the pressed button)
assert mock_dns_radio.value
assert not mock_ip_radio.value
assert mock_dns_input.value == "example.com"
app.edit_handler.handle_entry_type_change.assert_called_with("dns")
def test_edit_form_initialization_calls_type_detection(self):
"""Test that edit form initialization calls type detection."""
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 form elements
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 == "#ip-input":
return mock_ip_input
elif selector == "#hostname-input":
return mock_hostname_input
elif selector == "#comment-input":
return mock_comment_input
elif selector == "#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"])
app.hosts_file.add_entry(test_entry)
app.selected_entry_index = 0
# Mock the type detection method
app.edit_handler.populate_edit_form_with_type_detection = Mock()
app.details_handler.update_edit_form()
# Should call type detection method
app.edit_handler.populate_edit_form_with_type_detection.assert_called_once()
def test_dns_resolution_restricted_to_edit_mode(self):
"""Test that DNS resolution is only allowed in edit mode."""
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()
# Add test DNS entry
app.hosts_file = HostsFile()
dns_entry = HostEntry(ip_address="0.0.0.0", hostnames=["example"])
dns_entry.dns_name = "example.com"
app.hosts_file.add_entry(dns_entry)
app.selected_entry_index = 0
# Test 1: DNS resolution blocked in read-only mode (default)
assert app.edit_mode is False
# Test action_refresh_dns in read-only mode
app.action_refresh_dns()
app.update_status.assert_called_with(
"❌ Cannot resolve DNS names: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode."
)
# Reset mock
app.update_status.reset_mock()
# Test action_update_single_dns in read-only mode
app.action_update_single_dns()
app.update_status.assert_called_with(
"❌ Cannot resolve DNS names: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode."
)
# Test 2: DNS resolution allowed in edit mode
app.edit_mode = True
app.update_status.reset_mock()
# Mock DNS service and other dependencies
app.dns_service.resolve_entry_async = Mock()
app.manager.save_hosts_file = Mock(return_value=(True, "Success"))
app.table_handler.populate_entries_table = Mock()
app.details_handler.update_entry_details = Mock()
# Create a mock that properly handles and closes coroutines
def consume_coro(coro, **kwargs):
# If it's a coroutine, close it to prevent warnings
if hasattr(coro, 'close'):
coro.close()
return None
app.run_worker = Mock(side_effect=consume_coro)
# Test action_refresh_dns in edit mode - should proceed
app.action_refresh_dns()
# Should not show error message about read-only mode
error_calls = [call for call in app.update_status.call_args_list
if "read-only mode" in str(call)]
assert len(error_calls) == 0
# Should start DNS resolution
app.run_worker.assert_called()
# Reset mocks
app.update_status.reset_mock()
app.run_worker.reset_mock()
# Test action_update_single_dns in edit mode - should proceed
app.action_update_single_dns()
# Should not show error message about read-only mode
error_calls = [call for call in app.update_status.call_args_list
if "read-only mode" in str(call)]
assert len(error_calls) == 0
# Should start DNS resolution
app.run_worker.assert_called()
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()