hosts/tests/test_app.py

146 lines
5.2 KiB
Python

"""Tests for application-level authorization flow."""
from contextlib import contextmanager
from unittest.mock import Mock, patch
from textual.app import SuspendNotSupported
from src.hosts.tui.app import HostsManagerApp
@contextmanager
def suspended_tui():
"""Stand in for Textual's terminal suspension in action tests."""
yield
class TestPrivilegedModeAuthorization:
"""Test the user-visible privileged-mode authorization flow."""
def test_interactive_authorization_suspends_and_restores_the_tui(self):
"""Test foreground PAM authorization runs while Textual is suspended."""
events = []
@contextmanager
def suspended_tui():
events.append("suspended")
yield
events.append("restored")
def authorize_interactively():
assert events == ["suspended"]
events.append("authorized")
return True, "Sudo access granted"
def finish_entering_edit_mode():
assert events == ["suspended", "authorized", "restored"]
events.append("edit mode enabled")
return True, "Edit mode enabled"
app = HostsManagerApp()
app.manager = Mock()
app.manager.enter_edit_mode.return_value = (
False,
"Interactive authorization required",
)
app.manager.authorize_interactively.side_effect = authorize_interactively
app.manager.finish_entering_edit_mode.side_effect = finish_entering_edit_mode
app.suspend = Mock(return_value=suspended_tui())
app.update_status = Mock()
app.action_toggle_edit_mode()
assert app.edit_mode
app.suspend.assert_called_once_with()
app.manager.enter_edit_mode.assert_called_once_with()
app.manager.authorize_interactively.assert_called_once_with()
app.manager.finish_entering_edit_mode.assert_called_once_with()
assert events == ["suspended", "authorized", "restored", "edit mode enabled"]
app.update_status.assert_called_once_with("Edit mode enabled")
def test_rejected_authorization_keeps_the_app_read_only(self):
"""Test a rejected PAM conversation reports the required message."""
app = HostsManagerApp()
app.manager = Mock()
app.manager.enter_edit_mode.return_value = (
False,
"Interactive authorization required",
)
app.manager.authorize_interactively.return_value = (
False,
"Authorization was not granted",
)
app.suspend = Mock(return_value=suspended_tui())
app.update_status = Mock()
app.action_toggle_edit_mode()
assert not app.edit_mode
app.update_status.assert_called_once_with(
"Authorization was not granted; remaining in Read-only Mode."
)
def test_interrupted_authorization_keeps_the_app_read_only(self):
"""Test interruption during PAM authorization restores read-only mode."""
events = []
@contextmanager
def suspended_tui():
events.append("suspended")
yield
events.append("restored")
app = HostsManagerApp()
app.manager = Mock()
app.manager.enter_edit_mode.return_value = (
False,
"Interactive authorization required",
)
app.manager.authorize_interactively.side_effect = KeyboardInterrupt()
app.suspend = Mock(return_value=suspended_tui())
app.update_status = Mock()
app.action_toggle_edit_mode()
assert not app.edit_mode
assert events == ["suspended", "restored"]
app.update_status.assert_called_once_with(
"Authorization was not granted; remaining in Read-only Mode."
)
def test_unsupported_suspension_keeps_the_app_read_only(self):
"""Test the user receives an actionable suspension failure message."""
app = HostsManagerApp()
app.manager = Mock()
app.manager.enter_edit_mode.return_value = (
False,
"Interactive authorization required",
)
app.suspend = Mock(side_effect=SuspendNotSupported())
app.update_status = Mock()
app.action_toggle_edit_mode()
assert not app.edit_mode
app.update_status.assert_called_once_with(
"Interactive authorization requires terminal suspension; remaining in Read-only Mode."
)
@patch("src.hosts.core.manager.subprocess.run")
def test_leaving_privileged_mode_preserves_the_sudo_timestamp(self, mock_run):
"""Test Ctrl+E clears app state without invalidating sudo globally."""
app = HostsManagerApp()
app.edit_mode = True
app.manager.edit_mode = True
app.manager.permission_manager.has_sudo = True
app.manager.permission_manager._sudo_validated = True
app.update_status = Mock()
app.action_toggle_edit_mode()
assert not app.edit_mode
assert not app.manager.edit_mode
assert not app.manager.permission_manager.has_sudo
assert not app.manager.permission_manager._sudo_validated
mock_run.assert_not_called()
app.update_status.assert_called_once_with("Edit mode disabled")