Add comprehensive tests for configuration management and main TUI components
This commit is contained in:
parent
79069bc2ea
commit
0051932014
4 changed files with 1008 additions and 1 deletions
|
@ -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
292
tests/test_config.py
Normal 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
229
tests/test_config_modal.py
Normal 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
484
tests/test_main.py
Normal 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()
|
Loading…
Add table
Add a link
Reference in a new issue