Skip to content

Commit 461b3fc

Browse files
author
aviat cohen
committed
avoid re-login when set mode to interactive
1 parent db965a5 commit 461b3fc

3 files changed

Lines changed: 249 additions & 9 deletions

File tree

src/fabric_cli/commands/config/fab_config_set.py

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,64 @@ def _set_config(args: Namespace, key: str, value: Any, verbose: bool = True) ->
7676

7777
Context().cleanup_context_files(cleanup_all_stale=True, cleanup_current=True)
7878

79-
if (
80-
key == fab_constant.FAB_MODE
81-
and current_mode == fab_constant.FAB_MODE_COMMANDLINE
82-
and previous_mode == fab_constant.FAB_MODE_INTERACTIVE
83-
):
84-
utils_ui.print("Exiting interactive mode. Goodbye!")
85-
os._exit(0)
79+
# Enhanced mode transition handling
80+
if key == fab_constant.FAB_MODE:
81+
if (current_mode == fab_constant.FAB_MODE_INTERACTIVE
82+
and previous_mode == fab_constant.FAB_MODE_COMMANDLINE):
83+
# Handle command_line → interactive transition
84+
if _is_user_authenticated():
85+
utils_ui.print("Switching to interactive mode...")
86+
_start_interactive_mode(args)
87+
else:
88+
utils_ui.print("Please login first to use interactive mode")
89+
90+
elif (current_mode == fab_constant.FAB_MODE_COMMANDLINE
91+
and previous_mode == fab_constant.FAB_MODE_INTERACTIVE):
92+
# Handle interactive → command_line transition
93+
utils_ui.print("Exiting interactive mode. Goodbye!")
94+
os._exit(0)
95+
96+
97+
def _is_user_authenticated() -> bool:
98+
"""Check if user has valid authentication tokens"""
99+
try:
100+
from fabric_cli.core.fab_auth import FabAuth
101+
auth = FabAuth()
102+
# Try to get a token without interactive renewal
103+
token = auth.get_access_token(
104+
fab_constant.SCOPE_FABRIC_DEFAULT,
105+
interactive_renew=False
106+
)
107+
return token is not None
108+
except FabricCLIError as e:
109+
# Handle specific authentication errors
110+
if e.status_code in [
111+
fab_constant.ERROR_UNAUTHORIZED,
112+
fab_constant.ERROR_AUTHENTICATION_FAILED,
113+
]:
114+
return False
115+
raise e
116+
except Exception:
117+
return False
118+
119+
120+
def _start_interactive_mode(args: Namespace) -> None:
121+
"""Launch interactive mode with current parser context"""
122+
try:
123+
# Import parser setup from main module
124+
from fabric_cli.main import _create_parser_and_subparsers
125+
126+
parser, subparsers = _create_parser_and_subparsers()
127+
128+
from fabric_cli.core.fab_interactive import InteractiveCLI
129+
interactive_cli = InteractiveCLI(parser, subparsers)
130+
interactive_cli.start_interactive()
131+
132+
except (KeyboardInterrupt, EOFError):
133+
utils_ui.print("Interactive mode cancelled.")
134+
except Exception as e:
135+
utils_ui.print(f"Failed to start interactive mode: {str(e)}")
136+
utils_ui.print("Please restart the CLI to use interactive mode.")
86137

87138

88139
def _set_capacity(args: Namespace, value: str) -> None:

src/fabric_cli/main.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ def error(self, message):
168168
sys.exit(2)
169169

170170

171-
def main():
171+
def _create_parser_and_subparsers():
172+
"""Create parser and subparsers for reuse in interactive mode transition"""
172173
parser = CustomArgumentParser(description="Fabric CLI")
173174

174175
# -c option for command line execution
@@ -224,6 +225,12 @@ def main():
224225
)
225226
version_parser.set_defaults(func=fab_ui.print_version)
226227

228+
return parser, subparsers
229+
230+
231+
def main():
232+
parser, subparsers = _create_parser_and_subparsers()
233+
227234
argcomplete.autocomplete(parser, default_completer=None)
228235

229236
args = parser.parse_args()

tests/test_commands/test_config.py

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
33

4-
from unittest.mock import patch
4+
from unittest.mock import patch, MagicMock
5+
import pytest
56

67
import fabric_cli.core.fab_constant as constant
8+
from fabric_cli.core.fab_exceptions import FabricCLIError
79
from fabric_cli.errors import ErrorMessages
810
from tests.test_commands.commands_parser import CLIExecutor
911
from tests.test_commands.data.static_test_data import StaticTestData
@@ -171,3 +173,183 @@ def test_config_clear_cache_success(
171173
mock_print_done.assert_called_once()
172174

173175
# endregion
176+
177+
# region config MODE SWITCHING
178+
def test_success_config_set_mode_interactive_authenticated_success(
179+
self, mock_questionary_print, mock_fab_set_state_config, cli_executor: CLIExecutor
180+
):
181+
"""Test successful transition from command_line to interactive mode when authenticated"""
182+
with patch("fabric_cli.commands.config.fab_config_set._is_user_authenticated", return_value=True), \
183+
patch("fabric_cli.commands.config.fab_config_set._start_interactive_mode") as mock_start_interactive:
184+
185+
mock_fab_set_state_config(constant.FAB_MODE, constant.FAB_MODE_COMMANDLINE)
186+
187+
# Execute command
188+
cli_executor.exec_command(f"config set mode {constant.FAB_MODE_INTERACTIVE}")
189+
190+
# Assert
191+
mock_questionary_print.assert_called()
192+
mock_start_interactive.assert_called_once()
193+
assert mock_questionary_print.call_args[0][0] == 'Switching to interactive mode...'
194+
195+
196+
def test_config_set_mode_interactive_user_not_authenticated_failure(
197+
self, mock_fab_set_state_config, mock_questionary_print, cli_executor: CLIExecutor
198+
):
199+
"""Test transition from command_line to interactive mode when not authenticated"""
200+
with patch("fabric_cli.commands.config.fab_config_set._is_user_authenticated", return_value=False), \
201+
patch("fabric_cli.commands.config.fab_config_set._start_interactive_mode") as mock_start_interactive:
202+
203+
mock_fab_set_state_config(constant.FAB_MODE, constant.FAB_MODE_COMMANDLINE)
204+
205+
# Execute command
206+
cli_executor.exec_command(f"config set mode {constant.FAB_MODE_INTERACTIVE}")
207+
208+
# Assert
209+
mock_questionary_print.assert_called()
210+
assert mock_questionary_print.call_args[0][0] == "Please login first to use interactive mode"
211+
mock_start_interactive.assert_not_called()
212+
213+
def test_config_set_mode_command_line_from_interactive_success(
214+
self, mock_fab_set_state_config, mock_questionary_print, cli_executor: CLIExecutor
215+
):
216+
"""Test transition from interactive to command_line mode"""
217+
with patch("os._exit") as mock_exit:
218+
219+
mock_fab_set_state_config(constant.FAB_MODE, constant.FAB_MODE_INTERACTIVE)
220+
# Execute command
221+
cli_executor.exec_command(f"config set mode {constant.FAB_MODE_COMMANDLINE}")
222+
223+
# Assert
224+
mock_questionary_print.assert_called()
225+
assert mock_questionary_print.call_args[0][0] == "Exiting interactive mode. Goodbye!"
226+
mock_exit.assert_called_once_with(0)
227+
228+
def test_is_user_authenticated_with_valid_token_success(self):
229+
"""Test _is_user_authenticated returns True when user has valid token"""
230+
from fabric_cli.commands.config.fab_config_set import _is_user_authenticated
231+
232+
with patch("fabric_cli.core.fab_auth.FabAuth") as mock_fab_auth:
233+
mock_auth_instance = MagicMock()
234+
mock_auth_instance.get_access_token.return_value = "valid_token"
235+
mock_fab_auth.return_value = mock_auth_instance
236+
237+
result = _is_user_authenticated()
238+
239+
assert result is True
240+
mock_auth_instance.get_access_token.assert_called_once_with(
241+
constant.SCOPE_FABRIC_DEFAULT, interactive_renew=False
242+
)
243+
244+
def test_is_user_authenticated_with_no_token_failure(self):
245+
"""Test _is_user_authenticated returns False when user has no token"""
246+
from fabric_cli.commands.config.fab_config_set import _is_user_authenticated
247+
248+
with patch("fabric_cli.core.fab_auth.FabAuth") as mock_fab_auth:
249+
mock_auth_instance = MagicMock()
250+
mock_auth_instance.get_access_token.return_value = None
251+
mock_fab_auth.return_value = mock_auth_instance
252+
253+
result = _is_user_authenticated()
254+
255+
assert result is False
256+
257+
def test_is_user_authenticated_with_authentication_error_failure(self):
258+
"""Test _is_user_authenticated returns False when authentication fails"""
259+
from fabric_cli.commands.config.fab_config_set import _is_user_authenticated
260+
261+
with patch("fabric_cli.core.fab_auth.FabAuth") as mock_fab_auth:
262+
mock_auth_instance = MagicMock()
263+
mock_auth_instance.get_access_token.side_effect = FabricCLIError(
264+
"Authentication failed", constant.ERROR_AUTHENTICATION_FAILED
265+
)
266+
mock_fab_auth.return_value = mock_auth_instance
267+
268+
result = _is_user_authenticated()
269+
270+
assert result is False
271+
272+
def test_is_user_authenticated_with_unexpected_error_failure(self):
273+
"""Test _is_user_authenticated returns False on unexpected error"""
274+
from fabric_cli.commands.config.fab_config_set import _is_user_authenticated
275+
276+
with patch("fabric_cli.core.fab_auth.FabAuth") as mock_fab_auth:
277+
mock_auth_instance = MagicMock()
278+
mock_auth_instance.get_access_token.side_effect = Exception("Unexpected error")
279+
mock_fab_auth.return_value = mock_auth_instance
280+
281+
result = _is_user_authenticated()
282+
283+
assert result is False
284+
285+
def test_start_interactive_mode_success(self):
286+
"""Test _start_interactive_mode successfully launches interactive CLI"""
287+
from fabric_cli.commands.config.fab_config_set import _start_interactive_mode
288+
from argparse import Namespace
289+
290+
args = Namespace()
291+
292+
with patch("fabric_cli.main._create_parser_and_subparsers") as mock_create_parser, \
293+
patch("fabric_cli.core.fab_interactive.InteractiveCLI") as mock_interactive_cli:
294+
295+
mock_parser = MagicMock()
296+
mock_subparsers = MagicMock()
297+
mock_create_parser.return_value = (mock_parser, mock_subparsers)
298+
299+
mock_cli_instance = MagicMock()
300+
mock_interactive_cli.return_value = mock_cli_instance
301+
302+
_start_interactive_mode(args)
303+
304+
# Assert
305+
mock_create_parser.assert_called_once()
306+
mock_interactive_cli.assert_called_once_with(mock_parser, mock_subparsers)
307+
mock_cli_instance.start_interactive.assert_called_once()
308+
309+
def test_start_interactive_mode_keyboard_interrupt_success(self, mock_questionary_print):
310+
"""Test _start_interactive_mode handles KeyboardInterrupt gracefully"""
311+
from fabric_cli.commands.config.fab_config_set import _start_interactive_mode
312+
from argparse import Namespace
313+
314+
args = Namespace()
315+
316+
with patch("fabric_cli.main._create_parser_and_subparsers") as mock_create_parser, \
317+
patch("fabric_cli.core.fab_interactive.InteractiveCLI") as mock_interactive_cli:
318+
319+
mock_parser = MagicMock()
320+
mock_subparsers = MagicMock()
321+
mock_create_parser.return_value = (mock_parser, mock_subparsers)
322+
323+
mock_cli_instance = MagicMock()
324+
mock_cli_instance.start_interactive.side_effect = KeyboardInterrupt()
325+
mock_interactive_cli.return_value = mock_cli_instance
326+
327+
_start_interactive_mode(args)
328+
329+
# Assert
330+
mock_questionary_print.call_args[0][0] == "Interactive mode cancelled."
331+
332+
def test_start_interactive_mode_exception_handling_failure(self, mock_questionary_print):
333+
"""Test _start_interactive_mode handles general exceptions"""
334+
from fabric_cli.commands.config.fab_config_set import _start_interactive_mode
335+
from argparse import Namespace
336+
337+
args = Namespace()
338+
339+
with patch("fabric_cli.main._create_parser_and_subparsers") as mock_create_parser, \
340+
patch("fabric_cli.core.fab_interactive.InteractiveCLI") as mock_interactive_cli:
341+
342+
mock_parser = MagicMock()
343+
mock_subparsers = MagicMock()
344+
mock_create_parser.return_value = (mock_parser, mock_subparsers)
345+
346+
mock_cli_instance = MagicMock()
347+
mock_cli_instance.start_interactive.side_effect = Exception("Test error")
348+
mock_interactive_cli.return_value = mock_cli_instance
349+
350+
_start_interactive_mode(args)
351+
352+
# Assert
353+
mock_questionary_print.call_args[0][0] == "Please restart the CLI to use interactive mode."
354+
355+
# endregion

0 commit comments

Comments
 (0)