613 lines
22 KiB
Python
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, 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("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()
|