"""Tests for application-level authorization flow.""" from contextlib import contextmanager from unittest.mock import Mock 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." )