Add comprehensive tests for configuration management and main TUI components

This commit is contained in:
Philip Henning 2025-07-29 21:14:29 +02:00
parent 79069bc2ea
commit 0051932014
4 changed files with 1008 additions and 1 deletions

View file

@ -19,7 +19,7 @@
- ✅ **Entry management**: List view with proper formatting and status indicators - ✅ **Entry management**: List view with proper formatting and status indicators
- ✅ **Detail view**: Comprehensive entry details in right pane - ✅ **Detail view**: Comprehensive entry details in right pane
- ✅ **Navigation**: Smooth keyboard navigation with cursor position restoration - ✅ **Navigation**: Smooth keyboard navigation with cursor position restoration
- ✅ **Testing**: 42 comprehensive tests with 100% pass rate - ✅ **Testing**: 97 comprehensive tests with 100% pass rate
- ✅ **Code quality**: All ruff linting and formatting checks passing - ✅ **Code quality**: All ruff linting and formatting checks passing
- ✅ **Error handling**: Graceful handling of file access and parsing errors - ✅ **Error handling**: Graceful handling of file access and parsing errors
- ✅ **Status feedback**: Informative status bar with file and entry information - ✅ **Status feedback**: Informative status bar with file and entry information
@ -100,6 +100,8 @@
- ✅ **File integrity**: Perfect preservation of comments and formatting - ✅ **File integrity**: Perfect preservation of comments and formatting
- ✅ **Test coverage**: Comprehensive test suite catching all edge cases - ✅ **Test coverage**: Comprehensive test suite catching all edge cases
- ✅ **Development workflow**: Smooth uv-based development experience - ✅ **Development workflow**: Smooth uv-based development experience
- ✅ **Complete test implementation**: Added comprehensive tests for config and main TUI components
- ✅ **Test-driven development**: All 97 tests passing with full coverage of application functionality
## Technical Implementation Details ## Technical Implementation Details

292
tests/test_config.py Normal file
View file

@ -0,0 +1,292 @@
"""
Tests for the configuration management module.
This module contains unit tests for the Config class,
validating configuration loading, saving, and management functionality.
"""
import pytest
import tempfile
import json
import os
from pathlib import Path
from unittest.mock import patch, mock_open
from hosts.core.config import Config
class TestConfig:
"""Test cases for the Config class."""
def test_config_initialization(self):
"""Test basic config initialization with defaults."""
with patch.object(Config, 'load'):
config = Config()
# Check default settings
assert config.get("show_default_entries") is False
assert len(config.get("default_entries", [])) == 3
assert config.get("window_settings", {}).get("last_sort_column") == ""
assert config.get("window_settings", {}).get("last_sort_ascending") is True
def test_default_settings_structure(self):
"""Test that default settings have the expected structure."""
with patch.object(Config, 'load'):
config = Config()
default_entries = config.get("default_entries", [])
assert len(default_entries) == 3
# Check localhost entries
localhost_entries = [e for e in default_entries if e["hostname"] == "localhost"]
assert len(localhost_entries) == 2 # IPv4 and IPv6
# Check broadcasthost entry
broadcast_entries = [e for e in default_entries if e["hostname"] == "broadcasthost"]
assert len(broadcast_entries) == 1
assert broadcast_entries[0]["ip"] == "255.255.255.255"
def test_config_paths(self):
"""Test that config paths are set correctly."""
with patch.object(Config, 'load'):
config = Config()
expected_dir = Path.home() / ".config" / "hosts-manager"
expected_file = expected_dir / "config.json"
assert config.config_dir == expected_dir
assert config.config_file == expected_file
def test_get_existing_key(self):
"""Test getting an existing configuration key."""
with patch.object(Config, 'load'):
config = Config()
result = config.get("show_default_entries")
assert result is False
def test_get_nonexistent_key_with_default(self):
"""Test getting a nonexistent key with default value."""
with patch.object(Config, 'load'):
config = Config()
result = config.get("nonexistent_key", "default_value")
assert result == "default_value"
def test_get_nonexistent_key_without_default(self):
"""Test getting a nonexistent key without default value."""
with patch.object(Config, 'load'):
config = Config()
result = config.get("nonexistent_key")
assert result is None
def test_set_configuration_value(self):
"""Test setting a configuration value."""
with patch.object(Config, 'load'):
config = Config()
config.set("test_key", "test_value")
assert config.get("test_key") == "test_value"
def test_set_overwrites_existing_value(self):
"""Test that setting overwrites existing values."""
with patch.object(Config, 'load'):
config = Config()
# Set initial value
config.set("show_default_entries", True)
assert config.get("show_default_entries") is True
# Overwrite with new value
config.set("show_default_entries", False)
assert config.get("show_default_entries") is False
def test_is_default_entry_true(self):
"""Test identifying default entries correctly."""
with patch.object(Config, 'load'):
config = Config()
# Test localhost IPv4
assert config.is_default_entry("127.0.0.1", "localhost") is True
# Test localhost IPv6
assert config.is_default_entry("::1", "localhost") is True
# Test broadcasthost
assert config.is_default_entry("255.255.255.255", "broadcasthost") is True
def test_is_default_entry_false(self):
"""Test that non-default entries are not identified as default."""
with patch.object(Config, 'load'):
config = Config()
# Test custom entries
assert config.is_default_entry("192.168.1.1", "router") is False
assert config.is_default_entry("10.0.0.1", "test.local") is False
assert config.is_default_entry("127.0.0.1", "custom") is False
def test_should_show_default_entries_default(self):
"""Test default value for show_default_entries."""
with patch.object(Config, 'load'):
config = Config()
assert config.should_show_default_entries() is False
def test_should_show_default_entries_configured(self):
"""Test configured value for show_default_entries."""
with patch.object(Config, 'load'):
config = Config()
config.set("show_default_entries", True)
assert config.should_show_default_entries() is True
def test_toggle_show_default_entries(self):
"""Test toggling the show_default_entries setting."""
with patch.object(Config, 'load'), patch.object(Config, 'save') as mock_save:
config = Config()
# Initial state should be False
assert config.should_show_default_entries() is False
# Toggle to True
config.toggle_show_default_entries()
assert config.should_show_default_entries() is True
mock_save.assert_called_once()
# Toggle back to False
mock_save.reset_mock()
config.toggle_show_default_entries()
assert config.should_show_default_entries() is False
mock_save.assert_called_once()
def test_load_nonexistent_file(self):
"""Test loading config when file doesn't exist."""
with patch('pathlib.Path.exists', return_value=False):
config = Config()
# Should use defaults when file doesn't exist
assert config.get("show_default_entries") is False
def test_load_existing_file(self):
"""Test loading config from existing file."""
test_config = {
"show_default_entries": True,
"custom_setting": "custom_value"
}
with patch('pathlib.Path.exists', return_value=True), \
patch('builtins.open', mock_open(read_data=json.dumps(test_config))):
config = Config()
# Should load values from file
assert config.get("show_default_entries") is True
assert config.get("custom_setting") == "custom_value"
# Should still have defaults for missing keys
assert len(config.get("default_entries", [])) == 3
def test_load_invalid_json(self):
"""Test loading config with invalid JSON falls back to defaults."""
with patch('pathlib.Path.exists', return_value=True), \
patch('builtins.open', mock_open(read_data="invalid json")):
config = Config()
# Should use defaults when JSON is invalid
assert config.get("show_default_entries") is False
def test_load_file_io_error(self):
"""Test loading config with file I/O error falls back to defaults."""
with patch('pathlib.Path.exists', return_value=True), \
patch('builtins.open', side_effect=IOError("File error")):
config = Config()
# Should use defaults when file can't be read
assert config.get("show_default_entries") is False
def test_save_creates_directory(self):
"""Test that save creates config directory if it doesn't exist."""
with patch.object(Config, 'load'), \
patch('pathlib.Path.mkdir') as mock_mkdir, \
patch('builtins.open', mock_open()) as mock_file:
config = Config()
config.save()
# Should create directory with parents=True, exist_ok=True
mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
mock_file.assert_called_once()
def test_save_writes_json(self):
"""Test that save writes configuration as JSON."""
with patch.object(Config, 'load'), \
patch('pathlib.Path.mkdir'), \
patch('builtins.open', mock_open()) as mock_file:
config = Config()
config.set("test_key", "test_value")
config.save()
# Check that file was opened for writing
mock_file.assert_called_once_with(config.config_file, 'w')
# Check that JSON was written
handle = mock_file()
written_data = ''.join(call.args[0] for call in handle.write.call_args_list)
# Should be valid JSON containing our test data
parsed_data = json.loads(written_data)
assert parsed_data["test_key"] == "test_value"
def test_save_io_error_silent_fail(self):
"""Test that save silently fails on I/O error."""
with patch.object(Config, 'load'), \
patch('pathlib.Path.mkdir'), \
patch('builtins.open', side_effect=IOError("Write error")):
config = Config()
# Should not raise exception
config.save()
def test_save_directory_creation_error_silent_fail(self):
"""Test that save silently fails on directory creation error."""
with patch.object(Config, 'load'), \
patch('pathlib.Path.mkdir', side_effect=OSError("Permission denied")):
config = Config()
# Should not raise exception
config.save()
def test_integration_load_save_roundtrip(self):
"""Test complete load/save cycle with temporary file."""
with tempfile.TemporaryDirectory() as temp_dir:
config_dir = Path(temp_dir) / "hosts-manager"
config_file = config_dir / "config.json"
with patch.object(Config, '__init__', lambda self: None):
config = Config()
config.config_dir = config_dir
config.config_file = config_file
config._settings = config._load_default_settings()
# Modify some settings
config.set("show_default_entries", True)
config.set("custom_setting", "test_value")
# Save configuration
config.save()
# Verify file was created
assert config_file.exists()
# Create new config instance and load
config2 = Config()
config2.config_dir = config_dir
config2.config_file = config_file
config2._settings = config2._load_default_settings()
config2.load()
# Verify settings were loaded correctly
assert config2.get("show_default_entries") is True
assert config2.get("custom_setting") == "test_value"
# Verify defaults are still present
assert len(config2.get("default_entries", [])) == 3

229
tests/test_config_modal.py Normal file
View file

@ -0,0 +1,229 @@
"""
Tests for the configuration modal TUI component.
This module contains unit tests for the ConfigModal class,
validating modal behavior and configuration interaction.
"""
import pytest
from unittest.mock import Mock, patch
from textual.widgets import Checkbox, Button
from hosts.core.config import Config
from hosts.tui.config_modal import ConfigModal
class TestConfigModal:
"""Test cases for the ConfigModal class."""
def test_modal_initialization(self):
"""Test modal initialization with config."""
mock_config = Mock(spec=Config)
mock_config.should_show_default_entries.return_value = False
modal = ConfigModal(mock_config)
assert modal.config == mock_config
def test_modal_compose_method_exists(self):
"""Test that modal has compose method."""
mock_config = Mock(spec=Config)
mock_config.should_show_default_entries.return_value = True
modal = ConfigModal(mock_config)
# Test that compose method exists and is callable
assert hasattr(modal, 'compose')
assert callable(modal.compose)
def test_action_save_updates_config(self):
"""Test that save action updates configuration."""
mock_config = Mock(spec=Config)
mock_config.should_show_default_entries.return_value = False
modal = ConfigModal(mock_config)
modal.dismiss = Mock()
# Mock the checkbox query
mock_checkbox = Mock()
mock_checkbox.value = True
modal.query_one = Mock(return_value=mock_checkbox)
# Trigger save action
modal.action_save()
# Verify config was updated
mock_config.set.assert_called_once_with("show_default_entries", True)
mock_config.save.assert_called_once()
modal.dismiss.assert_called_once_with(True)
def test_action_save_preserves_false_state(self):
"""Test that save action preserves False checkbox state."""
mock_config = Mock(spec=Config)
mock_config.should_show_default_entries.return_value = True
modal = ConfigModal(mock_config)
modal.dismiss = Mock()
# Mock the checkbox query with False value
mock_checkbox = Mock()
mock_checkbox.value = False
modal.query_one = Mock(return_value=mock_checkbox)
# Trigger save action
modal.action_save()
# Verify the False value was saved
mock_config.set.assert_called_once_with("show_default_entries", False)
mock_config.save.assert_called_once()
modal.dismiss.assert_called_once_with(True)
def test_action_cancel_no_config_changes(self):
"""Test that cancel action doesn't modify configuration."""
mock_config = Mock(spec=Config)
mock_config.should_show_default_entries.return_value = False
modal = ConfigModal(mock_config)
modal.dismiss = Mock()
# Trigger cancel action
modal.action_cancel()
# Verify config was NOT updated
mock_config.set.assert_not_called()
mock_config.save.assert_not_called()
modal.dismiss.assert_called_once_with(False)
def test_save_button_pressed_event(self):
"""Test save button pressed event handling."""
mock_config = Mock(spec=Config)
mock_config.should_show_default_entries.return_value = False
modal = ConfigModal(mock_config)
modal.action_save = Mock()
# Create mock save button
save_button = Mock()
save_button.id = "save-button"
event = Button.Pressed(save_button)
modal.on_button_pressed(event)
modal.action_save.assert_called_once()
def test_cancel_button_pressed_event(self):
"""Test cancel button pressed event handling."""
mock_config = Mock(spec=Config)
mock_config.should_show_default_entries.return_value = False
modal = ConfigModal(mock_config)
modal.action_cancel = Mock()
# Create mock cancel button
cancel_button = Mock()
cancel_button.id = "cancel-button"
event = Button.Pressed(cancel_button)
modal.on_button_pressed(event)
modal.action_cancel.assert_called_once()
def test_unknown_button_pressed_ignored(self):
"""Test that unknown button presses are ignored."""
mock_config = Mock(spec=Config)
mock_config.should_show_default_entries.return_value = False
modal = ConfigModal(mock_config)
modal.action_save = Mock()
modal.action_cancel = Mock()
# Create a mock button with unknown ID
unknown_button = Mock()
unknown_button.id = "unknown-button"
event = Button.Pressed(unknown_button)
# Should not raise exception
modal.on_button_pressed(event)
# Should not trigger any actions
modal.action_save.assert_not_called()
modal.action_cancel.assert_not_called()
def test_modal_bindings_defined(self):
"""Test that modal has expected key bindings."""
mock_config = Mock(spec=Config)
modal = ConfigModal(mock_config)
# Check that bindings are defined
assert len(modal.BINDINGS) == 2
# Check specific bindings
binding_keys = [binding.key for binding in modal.BINDINGS]
assert "escape" in binding_keys
assert "enter" in binding_keys
binding_actions = [binding.action for binding in modal.BINDINGS]
assert "cancel" in binding_actions
assert "save" in binding_actions
def test_modal_css_defined(self):
"""Test that modal has CSS styling defined."""
mock_config = Mock(spec=Config)
modal = ConfigModal(mock_config)
# Check that CSS is defined
assert hasattr(modal, 'CSS')
assert isinstance(modal.CSS, str)
assert len(modal.CSS) > 0
# Check for key CSS classes
assert "config-container" in modal.CSS
assert "config-title" in modal.CSS
assert "button-row" in modal.CSS
def test_config_method_called_during_initialization(self):
"""Test that config method is called during modal setup."""
mock_config = Mock(spec=Config)
# Test with True
mock_config.should_show_default_entries.return_value = True
modal = ConfigModal(mock_config)
# Verify the config object is stored
assert modal.config == mock_config
# Test with False
mock_config.should_show_default_entries.return_value = False
modal = ConfigModal(mock_config)
# Verify the config object is stored
assert modal.config == mock_config
def test_compose_method_signature(self):
"""Test that compose method has the expected signature."""
mock_config = Mock(spec=Config)
mock_config.should_show_default_entries.return_value = False
modal = ConfigModal(mock_config)
# Test that compose method exists and has correct signature
import inspect
sig = inspect.signature(modal.compose)
assert len(sig.parameters) == 0 # No parameters except self
# Test return type annotation if present
if sig.return_annotation != inspect.Signature.empty:
from textual.app import ComposeResult
assert sig.return_annotation == ComposeResult
def test_modal_inheritance(self):
"""Test that ConfigModal properly inherits from ModalScreen."""
mock_config = Mock(spec=Config)
modal = ConfigModal(mock_config)
from textual.screen import ModalScreen
assert isinstance(modal, ModalScreen)
# Should have the config attribute
assert hasattr(modal, 'config')
assert modal.config == mock_config

484
tests/test_main.py Normal file
View file

@ -0,0 +1,484 @@
"""
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
assert app.hosts_file.entries[0].ip_address == "10.0.0.1"
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.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()
# 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()
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()