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