diff --git a/tabcmd/commands/auth/session.py b/tabcmd/commands/auth/session.py index 088865f4..189c2f7c 100644 --- a/tabcmd/commands/auth/session.py +++ b/tabcmd/commands/auth/session.py @@ -1,5 +1,6 @@ import getpass import json +import logging import os import requests @@ -104,13 +105,21 @@ def timeout_as_integer(logger, option_1, option_2): return result or 0 @staticmethod - def _read_password_from_file(filename): + def _read_password_from_file(filename, logger=None): + _logger = logger or logging.getLogger(__name__) credential = None - with open(str(filename), "r") as file_contents: - reader = file_contents.readlines() - for row in reader: - credential = row - return credential + try: + with open(str(filename), "r") as file_contents: + reader = file_contents.readlines() + for row in reader: + credential = row + except IOError as e: + Errors.exit_with_error( + _logger, + message=_("session.errors.password_file_not_found").format(filename), + exception=e, + ) + return credential def _allow_prompt(self): try: @@ -121,7 +130,14 @@ def _allow_prompt(self): def _create_new_credential(self, password, credential_type): if password is None: if self.password_file: - password = Session._read_password_from_file(self.password_file) + if not os.path.isfile(self.password_file): + Errors.exit_with_error( + self.logger, + message=_("session.errors.password_file_not_found").format(self.password_file), + ) + password = Session._read_password_from_file(self.password_file, self.logger) + if not password: + Errors.exit_with_error(self.logger, _("session.errors.missing_arguments").format("")) elif self._allow_prompt(): password = getpass.getpass(_("session.password")) else: @@ -141,7 +157,14 @@ def _create_new_token_credential(self): if self.token_value: token = self.token_value elif self.token_file: - token = Session._read_password_from_file(self.token_file) + if not os.path.isfile(self.token_file): + Errors.exit_with_error( + self.logger, + message=_("session.errors.token_file_not_found").format(self.token_file), + ) + token = Session._read_password_from_file(self.token_file, self.logger) + if not token: + Errors.exit_with_error(self.logger, _("session.errors.missing_arguments").format("token")) elif self._allow_prompt(): token = getpass.getpass("Token:") else: diff --git a/tabcmd/locales/en/LC_MESSAGES/tabcmd.mo b/tabcmd/locales/en/LC_MESSAGES/tabcmd.mo index 6bc6de81..a067f324 100644 Binary files a/tabcmd/locales/en/LC_MESSAGES/tabcmd.mo and b/tabcmd/locales/en/LC_MESSAGES/tabcmd.mo differ diff --git a/tabcmd/locales/en/tabcmd_messages_en.properties b/tabcmd/locales/en/tabcmd_messages_en.properties index 2fe0f59e..161c92d2 100644 --- a/tabcmd/locales/en/tabcmd_messages_en.properties +++ b/tabcmd/locales/en/tabcmd_messages_en.properties @@ -266,6 +266,8 @@ session.errors.invalid_redirect=Cannot redirect to invalid URL: {0} session.errors.keystore_pwdfile_create=Cannot create keystore password file, error: {0} session.errors.missing_arguments=Cannot sign in because of missing arguments: {0} session.errors.no_redirect_found=No redirection address in redirect response +session.errors.password_file_not_found=Password file not found: '{0}'. Check the path passed to --password-file. +session.errors.token_file_not_found=Token file not found: '{0}'. Check the path passed to --token-file. session.errors.no_sslcafile_found=Could not find a Certificate Authority (CA) file in these locations: ''{0}''. The CA file is used to help ensure secure communication with Tableau Server. For information on creating a CA file, see the Tableau Server help. To connect to Tableau Server without validating its SSL certificate, use the --no-certcheck flag. session.errors.script_no_password=Tabcmd was run from a script or IDE but no password was provided. A password must be provided as an argument to tabcmd in order to log in. session.errors.session_expired=Your session has expired diff --git a/tests/commands/test_session.py b/tests/commands/test_session.py index 646e2a99..e0de6707 100644 --- a/tests/commands/test_session.py +++ b/tests/commands/test_session.py @@ -530,6 +530,78 @@ def test_create_session_username_and_password_no_prompt(self, mock_tsc, mock_pas mock_pass.assert_not_called() +@mock.patch("tabcmd.commands.auth.session.json") +@mock.patch("tabcmd.commands.auth.session.os.path") +@mock.patch("builtins.open") +class MissingCredentialFileTests(unittest.TestCase): + """Tests that missing --password-file / --token-file paths exit with an actionable error.""" + + def _assert_missing_file_error(self, mock_exit, expected_key_fragment, expected_path): + """Assert exit_with_error was called with a message referencing the missing file.""" + call_kwargs = mock_exit.call_args + message = call_kwargs[1].get("message", "") or str(call_kwargs) + # Depending on whether locale .mo files are loaded, _() returns either the + # translated string (containing the path) or the raw i18n key. + self.assertTrue( + expected_path in message or expected_key_fragment in message, + "Expected path '{}' or key '{}' in error message, got: {}".format( + expected_path, expected_key_fragment, message + ), + ) + + def test_password_file_not_found_exits_with_actionable_error(self, mock_file, mock_path, mock_json): + mock_path.isfile.return_value = False + mock_path.exists.return_value = False + new_session = Session() + new_session.username = "uuuu" + new_session.password_file = "/nonexistent/cred.txt" + with mock.patch("tabcmd.commands.auth.session.Errors.exit_with_error", side_effect=SystemExit) as mock_exit: + with self.assertRaises(SystemExit): + new_session._create_new_credential(None, Session.PASSWORD_CRED_TYPE) + self._assert_missing_file_error(mock_exit, "password_file_not_found", "/nonexistent/cred.txt") + + def test_token_file_not_found_exits_with_actionable_error(self, mock_file, mock_path, mock_json): + mock_path.isfile.return_value = False + mock_path.exists.return_value = False + new_session = Session() + new_session.token_name = "mytoken" + new_session.token_file = "/nonexistent/token.txt" + with mock.patch("tabcmd.commands.auth.session.Errors.exit_with_error", side_effect=SystemExit) as mock_exit: + with self.assertRaises(SystemExit): + new_session._create_new_token_credential() + self._assert_missing_file_error(mock_exit, "token_file_not_found", "/nonexistent/token.txt") + + @mock.patch("tableauserverclient.Server") + def test_create_session_with_missing_password_file_exits(self, mock_tsc, mock_file, mock_path, mock_json): + mock_path.isfile.return_value = False + mock_path.exists.return_value = False + new_session = Session() + new_session.tableau_server = mock_tsc() + test_args = Namespace(**vars(args_to_mock)) + test_args.username = "uuuu" + test_args.server = "https://fake.server.com" + test_args.password_file = "/nonexistent/cred.txt" + with mock.patch("tabcmd.commands.auth.session.Errors.exit_with_error", side_effect=SystemExit) as mock_exit: + with self.assertRaises(SystemExit): + new_session.create_session(test_args, None) + self._assert_missing_file_error(mock_exit, "password_file_not_found", "/nonexistent/cred.txt") + + @mock.patch("tableauserverclient.Server") + def test_create_session_with_missing_token_file_exits(self, mock_tsc, mock_file, mock_path, mock_json): + mock_path.isfile.return_value = False + mock_path.exists.return_value = False + new_session = Session() + new_session.tableau_server = mock_tsc() + test_args = Namespace(**vars(args_to_mock)) + test_args.token_name = "mytoken" + test_args.server = "https://fake.server.com" + test_args.token_file = "/nonexistent/token.txt" + with mock.patch("tabcmd.commands.auth.session.Errors.exit_with_error", side_effect=SystemExit) as mock_exit: + with self.assertRaises(SystemExit): + new_session.create_session(test_args, None) + self._assert_missing_file_error(mock_exit, "token_file_not_found", "/nonexistent/token.txt") + + def _set_mock_tsc_not_signed_in(mock_tsc): tsc_in_test = mock.MagicMock(name="manually mocking tsc") tsc_in_test.is_signed_in.return_value = False # CreateSessionTests.return_False