""" Tests for the hosts manager module. This module tests permission management, edit mode operations, and safe file modifications with backup and validation. """ import pytest import tempfile import subprocess from pathlib import Path from unittest.mock import Mock, patch, MagicMock from src.hosts.core.manager import PermissionManager, HostsManager, EditModeError from src.hosts.core.models import HostEntry, HostsFile class TestPermissionManager: """Test the PermissionManager class.""" def test_init(self): """Test PermissionManager initialization.""" pm = PermissionManager() assert not pm.has_sudo assert not pm._sudo_validated @patch('subprocess.run') def test_request_sudo_already_available(self, mock_run): """Test requesting sudo when already available.""" # Mock successful sudo -n true mock_run.return_value = Mock(returncode=0) pm = PermissionManager() success, message = pm.request_sudo() assert success assert "already available" in message assert pm.has_sudo assert pm._sudo_validated mock_run.assert_called_once_with( ['sudo', '-n', 'true'], capture_output=True, text=True, timeout=5 ) @patch('subprocess.run') def test_request_sudo_prompt_success(self, mock_run): """Test requesting sudo with password prompt success.""" # First call (sudo -n true) fails, second call (sudo -v) succeeds mock_run.side_effect = [ Mock(returncode=1), # sudo -n true fails Mock(returncode=0) # sudo -v succeeds ] pm = PermissionManager() success, message = pm.request_sudo() assert success assert "access granted" in message assert pm.has_sudo assert pm._sudo_validated assert mock_run.call_count == 2 @patch('subprocess.run') def test_request_sudo_denied(self, mock_run): """Test requesting sudo when access is denied.""" # Both calls fail mock_run.side_effect = [ Mock(returncode=1), # sudo -n true fails Mock(returncode=1) # sudo -v fails ] pm = PermissionManager() success, message = pm.request_sudo() assert not success assert "denied" in message assert not pm.has_sudo assert not pm._sudo_validated @patch('subprocess.run') def test_request_sudo_timeout(self, mock_run): """Test requesting sudo with timeout.""" mock_run.side_effect = subprocess.TimeoutExpired(['sudo', '-n', 'true'], 5) pm = PermissionManager() success, message = pm.request_sudo() assert not success assert "timed out" in message assert not pm.has_sudo @patch('subprocess.run') def test_request_sudo_exception(self, mock_run): """Test requesting sudo with exception.""" mock_run.side_effect = Exception("Test error") pm = PermissionManager() success, message = pm.request_sudo() assert not success assert "Test error" in message assert not pm.has_sudo @patch('subprocess.run') def test_validate_permissions_success(self, mock_run): """Test validating permissions successfully.""" mock_run.return_value = Mock(returncode=0) pm = PermissionManager() pm.has_sudo = True result = pm.validate_permissions("/etc/hosts") assert result mock_run.assert_called_once_with( ['sudo', '-n', 'test', '-w', '/etc/hosts'], capture_output=True, timeout=5 ) @patch('subprocess.run') def test_validate_permissions_no_sudo(self, mock_run): """Test validating permissions without sudo.""" pm = PermissionManager() pm.has_sudo = False result = pm.validate_permissions("/etc/hosts") assert not result mock_run.assert_not_called() @patch('subprocess.run') def test_validate_permissions_failure(self, mock_run): """Test validating permissions failure.""" mock_run.return_value = Mock(returncode=1) pm = PermissionManager() pm.has_sudo = True result = pm.validate_permissions("/etc/hosts") assert not result @patch('subprocess.run') def test_validate_permissions_exception(self, mock_run): """Test validating permissions with exception.""" mock_run.side_effect = Exception("Test error") pm = PermissionManager() pm.has_sudo = True result = pm.validate_permissions("/etc/hosts") assert not result @patch('subprocess.run') def test_release_sudo(self, mock_run): """Test releasing sudo permissions.""" pm = PermissionManager() pm.has_sudo = True pm._sudo_validated = True pm.release_sudo() assert not pm.has_sudo assert not pm._sudo_validated mock_run.assert_called_once_with(['sudo', '-k'], capture_output=True, timeout=5) @patch('subprocess.run') def test_release_sudo_exception(self, mock_run): """Test releasing sudo with exception.""" mock_run.side_effect = Exception("Test error") pm = PermissionManager() pm.has_sudo = True pm._sudo_validated = True pm.release_sudo() # Should still reset state even if command fails assert not pm.has_sudo assert not pm._sudo_validated class TestHostsManager: """Test the HostsManager class.""" def test_init(self): """Test HostsManager initialization.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) assert not manager.edit_mode assert manager._backup_path is None assert manager.parser.file_path == Path(temp_file.name) @patch('src.hosts.core.manager.HostsManager._create_backup') def test_enter_edit_mode_success(self, mock_backup): """Test entering edit mode successfully.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) # Mock permission manager manager.permission_manager.request_sudo = Mock(return_value=(True, "Success")) manager.permission_manager.validate_permissions = Mock(return_value=True) success, message = manager.enter_edit_mode() assert success assert "enabled" in message assert manager.edit_mode mock_backup.assert_called_once() def test_enter_edit_mode_already_in_edit(self): """Test entering edit mode when already in edit mode.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = True success, message = manager.enter_edit_mode() assert success assert "Already in edit mode" in message def test_enter_edit_mode_sudo_failure(self): """Test entering edit mode with sudo failure.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) # Mock permission manager failure manager.permission_manager.request_sudo = Mock(return_value=(False, "Denied")) success, message = manager.enter_edit_mode() assert not success assert "Cannot enter edit mode" in message assert not manager.edit_mode def test_enter_edit_mode_permission_validation_failure(self): """Test entering edit mode with permission validation failure.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) # Mock permission manager manager.permission_manager.request_sudo = Mock(return_value=(True, "Success")) manager.permission_manager.validate_permissions = Mock(return_value=False) success, message = manager.enter_edit_mode() assert not success assert "Cannot write to hosts file" in message assert not manager.edit_mode @patch('src.hosts.core.manager.HostsManager._create_backup') def test_enter_edit_mode_backup_failure(self, mock_backup): """Test entering edit mode with backup failure.""" mock_backup.side_effect = Exception("Backup failed") with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) # Mock permission manager manager.permission_manager.request_sudo = Mock(return_value=(True, "Success")) manager.permission_manager.validate_permissions = Mock(return_value=True) success, message = manager.enter_edit_mode() assert not success assert "Failed to create backup" in message assert not manager.edit_mode def test_exit_edit_mode_success(self): """Test exiting edit mode successfully.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = True manager._backup_path = Path("/tmp/backup") # Mock permission manager manager.permission_manager.release_sudo = Mock() success, message = manager.exit_edit_mode() assert success assert "disabled" in message assert not manager.edit_mode assert manager._backup_path is None manager.permission_manager.release_sudo.assert_called_once() def test_exit_edit_mode_not_in_edit(self): """Test exiting edit mode when not in edit mode.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = False success, message = manager.exit_edit_mode() assert success assert "Already in read-only mode" in message def test_exit_edit_mode_exception(self): """Test exiting edit mode with exception.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = True # Mock permission manager to raise exception manager.permission_manager.release_sudo = Mock(side_effect=Exception("Test error")) success, message = manager.exit_edit_mode() assert not success assert "Test error" in message def test_toggle_entry_success(self): """Test toggling entry successfully.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = True hosts_file = HostsFile() entry = HostEntry("127.0.0.1", ["localhost"], is_active=True) hosts_file.entries.append(entry) success, message = manager.toggle_entry(hosts_file, 0) assert success assert "active to inactive" in message assert not hosts_file.entries[0].is_active def test_toggle_entry_not_in_edit_mode(self): """Test toggling entry when not in edit mode.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = False hosts_file = HostsFile() success, message = manager.toggle_entry(hosts_file, 0) assert not success assert "Not in edit mode" in message def test_toggle_entry_invalid_index(self): """Test toggling entry with invalid index.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = True hosts_file = HostsFile() success, message = manager.toggle_entry(hosts_file, 0) assert not success assert "Invalid entry index" in message def test_move_entry_up_success(self): """Test moving entry up successfully.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = True hosts_file = HostsFile() entry1 = HostEntry("127.0.0.1", ["localhost"]) entry2 = HostEntry("192.168.1.1", ["router"]) hosts_file.entries.extend([entry1, entry2]) success, message = manager.move_entry_up(hosts_file, 1) assert success assert "moved up" in message assert hosts_file.entries[0].hostnames[0] == "router" assert hosts_file.entries[1].hostnames[0] == "localhost" def test_move_entry_up_invalid_index(self): """Test moving entry up with invalid index.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = True hosts_file = HostsFile() entry = HostEntry("127.0.0.1", ["localhost"]) hosts_file.entries.append(entry) success, message = manager.move_entry_up(hosts_file, 0) assert not success assert "Cannot move entry up" in message def test_move_entry_down_success(self): """Test moving entry down successfully.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = True hosts_file = HostsFile() entry1 = HostEntry("127.0.0.1", ["localhost"]) entry2 = HostEntry("192.168.1.1", ["router"]) hosts_file.entries.extend([entry1, entry2]) success, message = manager.move_entry_down(hosts_file, 0) assert success assert "moved down" in message assert hosts_file.entries[0].hostnames[0] == "router" assert hosts_file.entries[1].hostnames[0] == "localhost" def test_move_entry_down_invalid_index(self): """Test moving entry down with invalid index.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = True hosts_file = HostsFile() entry = HostEntry("127.0.0.1", ["localhost"]) hosts_file.entries.append(entry) success, message = manager.move_entry_down(hosts_file, 0) assert not success assert "Cannot move entry down" in message def test_update_entry_success(self): """Test updating entry successfully.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = True hosts_file = HostsFile() entry = HostEntry("127.0.0.1", ["localhost"]) hosts_file.entries.append(entry) success, message = manager.update_entry( hosts_file, 0, "192.168.1.1", ["newhost"], "New comment" ) assert success assert "updated successfully" in message assert hosts_file.entries[0].ip_address == "192.168.1.1" assert hosts_file.entries[0].hostnames == ["newhost"] assert hosts_file.entries[0].comment == "New comment" def test_update_entry_invalid_data(self): """Test updating entry with invalid data.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = True hosts_file = HostsFile() entry = HostEntry("127.0.0.1", ["localhost"]) hosts_file.entries.append(entry) success, message = manager.update_entry( hosts_file, 0, "invalid-ip", ["newhost"] ) assert not success assert "Invalid entry data" in message @patch('tempfile.NamedTemporaryFile') @patch('subprocess.run') @patch('os.unlink') def test_save_hosts_file_success(self, mock_unlink, mock_run, mock_temp): """Test saving hosts file successfully.""" # Mock temporary file mock_temp_file = Mock() mock_temp_file.name = "/tmp/test.hosts" mock_temp_file.__enter__ = Mock(return_value=mock_temp_file) mock_temp_file.__exit__ = Mock(return_value=None) mock_temp.return_value = mock_temp_file # Mock subprocess success mock_run.return_value = Mock(returncode=0) with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = True manager.permission_manager.has_sudo = True hosts_file = HostsFile() entry = HostEntry("127.0.0.1", ["localhost"]) hosts_file.entries.append(entry) success, message = manager.save_hosts_file(hosts_file) assert success assert "saved successfully" in message mock_run.assert_called_once() mock_unlink.assert_called_once_with("/tmp/test.hosts") def test_save_hosts_file_not_in_edit_mode(self): """Test saving hosts file when not in edit mode.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = False hosts_file = HostsFile() success, message = manager.save_hosts_file(hosts_file) assert not success assert "Not in edit mode" in message def test_save_hosts_file_no_sudo(self): """Test saving hosts file without sudo.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = True manager.permission_manager.has_sudo = False hosts_file = HostsFile() success, message = manager.save_hosts_file(hosts_file) assert not success assert "No sudo permissions" in message @patch('subprocess.run') def test_restore_backup_success(self, mock_run): """Test restoring backup successfully.""" mock_run.return_value = Mock(returncode=0) with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = True # Create a mock backup file with tempfile.NamedTemporaryFile(delete=False) as backup_file: manager._backup_path = Path(backup_file.name) try: success, message = manager.restore_backup() assert success assert "restored successfully" in message mock_run.assert_called_once() finally: # Clean up manager._backup_path.unlink() def test_restore_backup_not_in_edit_mode(self): """Test restoring backup when not in edit mode.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = False success, message = manager.restore_backup() assert not success assert "Not in edit mode" in message def test_restore_backup_no_backup(self): """Test restoring backup when no backup exists.""" with tempfile.NamedTemporaryFile() as temp_file: manager = HostsManager(temp_file.name) manager.edit_mode = True manager._backup_path = None success, message = manager.restore_backup() assert not success assert "No backup available" in message @patch('subprocess.run') @patch('tempfile.gettempdir') @patch('time.time') def test_create_backup_success(self, mock_time, mock_tempdir, mock_run): """Test creating backup successfully.""" mock_time.return_value = 1234567890 mock_tempdir.return_value = "/tmp" mock_run.side_effect = [ Mock(returncode=0), # cp command Mock(returncode=0) # chmod command ] # Create a real temporary file for testing with tempfile.NamedTemporaryFile(delete=False) as temp_file: temp_file.write(b"test content") temp_path = temp_file.name try: manager = HostsManager(temp_path) manager._create_backup() expected_backup = Path("/tmp/hosts-manager-backups/hosts.backup.1234567890") assert manager._backup_path == expected_backup assert mock_run.call_count == 2 finally: # Clean up Path(temp_path).unlink() @patch('subprocess.run') def test_create_backup_failure(self, mock_run): """Test creating backup with failure.""" mock_run.return_value = Mock(returncode=1, stderr="Permission denied") # Create a real temporary file for testing with tempfile.NamedTemporaryFile(delete=False) as temp_file: temp_file.write(b"test content") temp_path = temp_file.name try: manager = HostsManager(temp_path) with pytest.raises(Exception) as exc_info: manager._create_backup() assert "Failed to create backup" in str(exc_info.value) finally: # Clean up Path(temp_path).unlink()