hosts/tests/test_manager.py

613 lines
22 KiB
Python

"""
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
from src.hosts.core.manager import PermissionManager, HostsManager
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("192.168.1.1", ["router"], is_active=True) # Non-default entry
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("10.0.0.1", ["test1"]) # Non-default entries
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] == "test1"
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("10.0.0.1", ["test1"]) # Non-default entries
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] == "test1"
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("10.0.0.1", ["test"]) # Non-default entry
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"]) # Default entry - cannot be modified
hosts_file.entries.append(entry)
success, message = manager.update_entry(
hosts_file, 0, "invalid-ip", ["newhost"]
)
assert not success
assert "Cannot modify default system entries" 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()