From cb13fd90137b3727db9129797f81cba4e71d2719 Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Tue, 21 Oct 2025 19:40:06 +0000 Subject: [PATCH 01/10] wip: move gcloud module and begins testing --- cli/casp/src/casp/tests/__init__.py | 0 cli/casp/src/casp/tests/utils/__init__.py | 0 cli/casp/src/casp/tests/utils/test_gcloud.py | 179 +++++++++++++++++++ cli/casp/src/casp/utils/gcloud.py | 102 +++++++++++ 4 files changed, 281 insertions(+) create mode 100644 cli/casp/src/casp/tests/__init__.py create mode 100644 cli/casp/src/casp/tests/utils/__init__.py create mode 100644 cli/casp/src/casp/tests/utils/test_gcloud.py create mode 100644 cli/casp/src/casp/utils/gcloud.py diff --git a/cli/casp/src/casp/tests/__init__.py b/cli/casp/src/casp/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cli/casp/src/casp/tests/utils/__init__.py b/cli/casp/src/casp/tests/utils/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cli/casp/src/casp/tests/utils/test_gcloud.py b/cli/casp/src/casp/tests/utils/test_gcloud.py new file mode 100644 index 00000000000..44f8125539d --- /dev/null +++ b/cli/casp/src/casp/tests/utils/test_gcloud.py @@ -0,0 +1,179 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the gcloud utility functions. + For running all the tests, use (from the root of the project): + python -m unittest discover -s cli/casp/src/casp/tests -v +""" + +import subprocess +import unittest +from unittest.mock import MagicMock, patch + +from google.auth import exceptions as auth_exceptions + +from casp.utils import gcloud + + +class GcloudUtilsTest(unittest.TestCase): + """Test gcloud utility functions.""" + + @patch('os.path.exists') + @patch('google.oauth2.credentials.Credentials.from_authorized_user_file') + def test_is_valid_credentials_valid(self, mock_from_file, mock_exists): + """Test _is_valid_credentials with a valid path.""" + mock_exists.return_value = True + mock_from_file.return_value = MagicMock() + self.assertTrue(gcloud._is_valid_credentials('valid/path')) + + @patch('os.path.exists') + def test_is_valid_credentials_not_exists(self, mock_exists): + """Test _is_valid_credentials with a non-existent path.""" + mock_exists.return_value = False + self.assertFalse(gcloud._is_valid_credentials('invalid/path')) + + @patch('os.path.exists') + @patch('google.oauth2.credentials.Credentials.from_authorized_user_file') + def test_is_valid_credentials_auth_error(self, mock_from_file, mock_exists): + """Test _is_valid_credentials with an auth exception.""" + mock_exists.return_value = True + mock_from_file.side_effect = auth_exceptions.DefaultCredentialsError + self.assertFalse(gcloud._is_valid_credentials('path')) + + @patch('os.path.exists') + @patch('google.oauth2.credentials.Credentials.from_authorized_user_file') + def test_is_valid_credentials_value_error(self, mock_from_file, mock_exists): + """Test _is_valid_credentials with a ValueError.""" + mock_exists.return_value = True + mock_from_file.side_effect = ValueError + self.assertFalse(gcloud._is_valid_credentials('path')) + + @patch('os.path.exists') + @patch('google.oauth2.credentials.Credentials.from_authorized_user_file') + def test_is_valid_credentials_key_error(self, mock_from_file, mock_exists): + """Test _is_valid_credentials with a KeyError.""" + mock_exists.return_value = True + mock_from_file.side_effect = KeyError + self.assertFalse(gcloud._is_valid_credentials('path')) + + @patch('casp.utils.gcloud._is_valid_credentials') + @patch('subprocess.run') + def test_run_gcloud_login_success(self, mock_run, mock_is_valid): + """Test _run_gcloud_login successful.""" + mock_run.return_value = MagicMock() + mock_is_valid.return_value = True + self.assertTrue(gcloud._run_gcloud_login()) + mock_run.assert_called_with( + ['gcloud', 'auth', 'application-default', 'login'], check=True) + mock_is_valid.assert_called_with(gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + + @patch('subprocess.run') + def test_run_gcloud_login_file_not_found(self, mock_run): + """Test _run_gcloud_login with gcloud not found.""" + mock_run.side_effect = FileNotFoundError + self.assertFalse(gcloud._run_gcloud_login()) + + @patch('subprocess.run') + def test_run_gcloud_login_failed(self, mock_run): + """Test _run_gcloud_login with a failed login command.""" + mock_run.side_effect = subprocess.CalledProcessError(1, 'cmd') + self.assertFalse(gcloud._run_gcloud_login()) + + @patch('click.prompt') + @patch('casp.utils.gcloud._is_valid_credentials') + def test_prompt_for_custom_path_valid(self, mock_is_valid, mock_prompt): + """Test _prompt_for_custom_path with a valid path.""" + mock_prompt.return_value = '/valid/path' + mock_is_valid.return_value = True + self.assertEqual(gcloud._prompt_for_custom_path(), '/valid/path') + + @patch('click.prompt') + @patch('casp.utils.gcloud._is_valid_credentials') + def test_prompt_for_custom_path_invalid(self, mock_is_valid, mock_prompt): + """Test _prompt_for_custom_path with an invalid path.""" + mock_prompt.return_value = '/invalid/path' + mock_is_valid.return_value = False + self.assertIsNone(gcloud._prompt_for_custom_path()) + + @patch('click.prompt') + def test_prompt_for_custom_path_empty(self, mock_prompt): + """Test _prompt_for_custom_path with empty input.""" + mock_prompt.return_value = '' + self.assertIsNone(gcloud._prompt_for_custom_path()) + + @patch('casp.utils.gcloud._is_valid_credentials') + def test_get_credentials_path_default_valid(self, mock_is_valid): + """Test get_credentials_path when default is valid.""" + mock_is_valid.return_value = True + self.assertEqual( + gcloud.get_credentials_path(), gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + mock_is_valid.assert_called_with(gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + + @patch('casp.utils.gcloud._prompt_for_custom_path') + @patch('casp.utils.gcloud._run_gcloud_login') + @patch('click.confirm') + @patch('casp.utils.gcloud._is_valid_credentials') + def test_get_credentials_path_login_success(self, mock_is_valid, + mock_confirm, mock_login, + mock_prompt): + """Test get_credentials_path with successful login.""" + mock_is_valid.return_value = False + mock_confirm.return_value = True + mock_login.return_value = True + self.assertEqual( + gcloud.get_credentials_path(), gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + mock_prompt.assert_not_called() + + @patch('casp.utils.gcloud._prompt_for_custom_path') + @patch('casp.utils.gcloud._run_gcloud_login') + @patch('click.confirm') + @patch('casp.utils.gcloud._is_valid_credentials') + def test_get_credentials_path_login_fail_then_custom(self, mock_is_valid, + mock_confirm, + mock_login, + mock_prompt): + """Test get_credentials_path with failed login then custom path.""" + mock_is_valid.return_value = False + mock_confirm.return_value = True + mock_login.return_value = False + mock_prompt.return_value = '/custom/path' + self.assertEqual(gcloud.get_credentials_path(), '/custom/path') + + @patch('casp.utils.gcloud._prompt_for_custom_path') + @patch('casp.utils.gcloud._run_gcloud_login') + @patch('click.confirm') + @patch('casp.utils.gcloud._is_valid_credentials') + def test_get_credentials_path_no_login_then_custom(self, mock_is_valid, + mock_confirm, mock_login, + mock_prompt): + """Test get_credentials_path with no login then custom path.""" + mock_is_valid.return_value = False + mock_confirm.return_value = False + mock_prompt.return_value = '/custom/path' + self.assertEqual(gcloud.get_credentials_path(), '/custom/path') + mock_login.assert_not_called() + + @patch('casp.utils.gcloud._prompt_for_custom_path') + @patch('click.confirm') + @patch('casp.utils.gcloud._is_valid_credentials') + def test_get_credentials_path_all_fail(self, mock_is_valid, mock_confirm, + mock_prompt): + """Test get_credentials_path when all methods fail.""" + mock_is_valid.return_value = False + mock_confirm.return_value = False + mock_prompt.return_value = None + self.assertIsNone(gcloud.get_credentials_path()) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/cli/casp/src/casp/utils/gcloud.py b/cli/casp/src/casp/utils/gcloud.py new file mode 100644 index 00000000000..572d4b29fd7 --- /dev/null +++ b/cli/casp/src/casp/utils/gcloud.py @@ -0,0 +1,102 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""gcloud utility functions.""" + +import os +import subprocess + +import click +from google.auth import exceptions as auth_exceptions +from google.oauth2 import credentials + +DEFAULT_GCLOUD_CREDENTIALS_PATH = os.path.expanduser( + '~/.config/gcloud/application_default_credentials.json') + + +def _is_valid_credentials(path: str) -> bool: + """Returns True if the path points to a valid credentials file.""" + if not path or not os.path.exists(path): + return False + try: + credentials.Credentials.from_authorized_user_file(path) + return True + except (auth_exceptions.DefaultCredentialsError, ValueError, KeyError): + return False + + +def _run_gcloud_login() -> bool: + """ + Runs the gcloud login command and returns True on success. + """ + try: + subprocess.run( + ['gcloud', 'auth', 'application-default', 'login'], check=True) + # After login, re-validate the default file. + return _is_valid_credentials(DEFAULT_GCLOUD_CREDENTIALS_PATH) + except FileNotFoundError: + click.secho( + 'Error: gcloud command not found. Please ensure it is installed and ' + 'in your PATH.', + fg='red') + return False + except subprocess.CalledProcessError: + click.secho('Error: gcloud login failed.', fg='red') + return False + + +def _prompt_for_custom_path() -> str | None: + """ + Prompts the user for a custom credentials path and returns it if valid. + """ + path = click.prompt( + 'Enter path to your credentials file (or press Ctrl+C to cancel)', + default='', + show_default=False, + type=click.Path(exists=True, dir_okay=False, resolve_path=True)) + + if not path: + return None + + if _is_valid_credentials(path): + return path + + click.secho('Error: The provided credentials file is not valid.', fg='red') + return None + + +def get_credentials_path() -> str | None: + """ + Finds a valid gcloud credentials path, prompting the user if needed. + + This function orchestrates the process of finding credentials by first + checking the default path, then offering an interactive login, and finally + allowing the user to specify a custom path. + + Returns: + The path to a valid credentials file, or None if one cannot be found. + """ + if _is_valid_credentials(DEFAULT_GCLOUD_CREDENTIALS_PATH): + return DEFAULT_GCLOUD_CREDENTIALS_PATH + + click.secho( + 'Default gcloud credentials not found or are invalid.', fg='yellow') + + if click.confirm('Do you want to log in with gcloud now?'): + if _run_gcloud_login(): + return DEFAULT_GCLOUD_CREDENTIALS_PATH + + click.secho( + '\nLogin was skipped or failed. You can provide a direct path instead.', + fg='yellow') + return _prompt_for_custom_path() From a9d466988f13b2ad77b11693be546ee74774c776 Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Wed, 22 Oct 2025 17:00:17 +0000 Subject: [PATCH 02/10] tests: rewrite tests following the guidelines --- cli/casp/src/casp/tests/utils/test_gcloud.py | 242 ++++++++++--------- 1 file changed, 129 insertions(+), 113 deletions(-) diff --git a/cli/casp/src/casp/tests/utils/test_gcloud.py b/cli/casp/src/casp/tests/utils/test_gcloud.py index 44f8125539d..e0c1e6c1339 100644 --- a/cli/casp/src/casp/tests/utils/test_gcloud.py +++ b/cli/casp/src/casp/tests/utils/test_gcloud.py @@ -1,4 +1,4 @@ -# Copyright 2025 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,168 +12,184 @@ # See the License for the specific language governing permissions and # limitations under the License. """Tests for the gcloud utility functions. + For running all the tests, use (from the root of the project): - python -m unittest discover -s cli/casp/src/casp/tests -v + python -m unittest discover -s cli/casp/src/casp/tests -v """ import subprocess import unittest -from unittest.mock import MagicMock, patch +from unittest.mock import patch, Mock from google.auth import exceptions as auth_exceptions - from casp.utils import gcloud -class GcloudUtilsTest(unittest.TestCase): - """Test gcloud utility functions.""" +class IsValidCredentialsTest(unittest.TestCase): + """Tests for _is_valid_credentials.""" - @patch('os.path.exists') - @patch('google.oauth2.credentials.Credentials.from_authorized_user_file') - def test_is_valid_credentials_valid(self, mock_from_file, mock_exists): - """Test _is_valid_credentials with a valid path.""" - mock_exists.return_value = True - mock_from_file.return_value = MagicMock() + @patch('os.path.exists', return_value=True, autospec=True) + @patch('google.oauth2.credentials.Credentials.from_authorized_user_file', autospec=True) + def test_valid_credentials(self, mock_from_file, mock_exists): + """Tests with a valid credentials file.""" + mock_from_file.return_value = Mock() self.assertTrue(gcloud._is_valid_credentials('valid/path')) + mock_exists.assert_called_once_with('valid/path') + mock_from_file.assert_called_once_with('valid/path') - @patch('os.path.exists') - def test_is_valid_credentials_not_exists(self, mock_exists): - """Test _is_valid_credentials with a non-existent path.""" - mock_exists.return_value = False + @patch('os.path.exists', return_value=False, autospec=True) + def test_path_does_not_exist(self, mock_exists): + """Tests with a non-existent path.""" self.assertFalse(gcloud._is_valid_credentials('invalid/path')) + mock_exists.assert_called_once_with('invalid/path') - @patch('os.path.exists') - @patch('google.oauth2.credentials.Credentials.from_authorized_user_file') - def test_is_valid_credentials_auth_error(self, mock_from_file, mock_exists): - """Test _is_valid_credentials with an auth exception.""" - mock_exists.return_value = True + @patch('os.path.exists', return_value=True, autospec=True) + @patch('google.oauth2.credentials.Credentials.from_authorized_user_file', autospec=True) + def test_auth_error(self, mock_from_file, mock_exists): + """Tests with an auth exception.""" mock_from_file.side_effect = auth_exceptions.DefaultCredentialsError self.assertFalse(gcloud._is_valid_credentials('path')) + mock_exists.assert_called_once_with('path') + mock_from_file.assert_called_once_with('path') - @patch('os.path.exists') - @patch('google.oauth2.credentials.Credentials.from_authorized_user_file') - def test_is_valid_credentials_value_error(self, mock_from_file, mock_exists): - """Test _is_valid_credentials with a ValueError.""" - mock_exists.return_value = True + @patch('os.path.exists', return_value=True, autospec=True) + @patch('google.oauth2.credentials.Credentials.from_authorized_user_file', autospec=True) + def test_value_error(self, mock_from_file, mock_exists): + """Tests with a ValueError.""" mock_from_file.side_effect = ValueError self.assertFalse(gcloud._is_valid_credentials('path')) + mock_exists.assert_called_once_with('path') + mock_from_file.assert_called_once_with('path') - @patch('os.path.exists') - @patch('google.oauth2.credentials.Credentials.from_authorized_user_file') - def test_is_valid_credentials_key_error(self, mock_from_file, mock_exists): - """Test _is_valid_credentials with a KeyError.""" - mock_exists.return_value = True - mock_from_file.side_effect = KeyError - self.assertFalse(gcloud._is_valid_credentials('path')) + def test_empty_path(self): + """Tests with an empty path string.""" + self.assertFalse(gcloud._is_valid_credentials('')) + + def test_none_path(self): + """Tests with a None path.""" + self.assertFalse(gcloud._is_valid_credentials(None)) + + +class RunGcloudLoginTest(unittest.TestCase): + """Tests for _run_gcloud_login.""" - @patch('casp.utils.gcloud._is_valid_credentials') - @patch('subprocess.run') - def test_run_gcloud_login_success(self, mock_run, mock_is_valid): - """Test _run_gcloud_login successful.""" - mock_run.return_value = MagicMock() - mock_is_valid.return_value = True + @patch('casp.utils.gcloud._is_valid_credentials', return_value=True, autospec=True) + @patch('subprocess.run', autospec=True) + def test_login_success(self, mock_run, mock_is_valid): + """Tests successful gcloud login.""" self.assertTrue(gcloud._run_gcloud_login()) - mock_run.assert_called_with( + mock_run.assert_called_once_with( ['gcloud', 'auth', 'application-default', 'login'], check=True) - mock_is_valid.assert_called_with(gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + mock_is_valid.assert_called_once_with(gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) - @patch('subprocess.run') - def test_run_gcloud_login_file_not_found(self, mock_run): - """Test _run_gcloud_login with gcloud not found.""" + @patch('subprocess.run', autospec=True) + @patch('click.secho', autospec=True) + def test_gcloud_not_found(self, mock_secho, mock_run): + """Tests with gcloud command not found.""" mock_run.side_effect = FileNotFoundError self.assertFalse(gcloud._run_gcloud_login()) - - @patch('subprocess.run') - def test_run_gcloud_login_failed(self, mock_run): - """Test _run_gcloud_login with a failed login command.""" + mock_secho.assert_called_once() + args, _ = mock_secho.call_args + self.assertIn('gcloud command not found', args[0]) + + @patch('subprocess.run', autospec=True) + @patch('click.secho', autospec=True) + def test_login_failed(self, mock_secho, mock_run): + """Tests with a failed login command.""" mock_run.side_effect = subprocess.CalledProcessError(1, 'cmd') self.assertFalse(gcloud._run_gcloud_login()) + mock_secho.assert_called_once_with('Error: gcloud login failed.', fg='red') + + +class PromptForCustomPathTest(unittest.TestCase): + """Tests for _prompt_for_custom_path.""" - @patch('click.prompt') - @patch('casp.utils.gcloud._is_valid_credentials') - def test_prompt_for_custom_path_valid(self, mock_is_valid, mock_prompt): - """Test _prompt_for_custom_path with a valid path.""" + @patch('click.prompt', autospec=True) + @patch('casp.utils.gcloud._is_valid_credentials', return_value=True, autospec=True) + def test_valid_path(self, mock_is_valid, mock_prompt): + """Tests with a valid custom path.""" mock_prompt.return_value = '/valid/path' - mock_is_valid.return_value = True self.assertEqual(gcloud._prompt_for_custom_path(), '/valid/path') + mock_is_valid.assert_called_once_with('/valid/path') - @patch('click.prompt') - @patch('casp.utils.gcloud._is_valid_credentials') - def test_prompt_for_custom_path_invalid(self, mock_is_valid, mock_prompt): - """Test _prompt_for_custom_path with an invalid path.""" + @patch('click.prompt', autospec=True) + @patch('casp.utils.gcloud._is_valid_credentials', return_value=False, autospec=True) + @patch('click.secho', autospec=True) + def test_invalid_path(self, mock_secho, mock_is_valid, mock_prompt): + """Tests with an invalid custom path.""" mock_prompt.return_value = '/invalid/path' - mock_is_valid.return_value = False self.assertIsNone(gcloud._prompt_for_custom_path()) + mock_is_valid.assert_called_once_with('/invalid/path') + mock_secho.assert_called_once_with( + 'Error: The provided credentials file is not valid.', fg='red') - @patch('click.prompt') - def test_prompt_for_custom_path_empty(self, mock_prompt): - """Test _prompt_for_custom_path with empty input.""" + @patch('click.prompt', autospec=True) + def test_empty_path(self, mock_prompt): + """Tests with empty input from prompt.""" mock_prompt.return_value = '' self.assertIsNone(gcloud._prompt_for_custom_path()) - @patch('casp.utils.gcloud._is_valid_credentials') - def test_get_credentials_path_default_valid(self, mock_is_valid): - """Test get_credentials_path when default is valid.""" - mock_is_valid.return_value = True + +class GetCredentialsPathTest(unittest.TestCase): + """Tests for get_credentials_path.""" + + @patch('casp.utils.gcloud._is_valid_credentials', return_value=True, autospec=True) + def test_default_path_valid(self, mock_is_valid): + """Tests when the default credentials path is valid.""" self.assertEqual( gcloud.get_credentials_path(), gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) - mock_is_valid.assert_called_with(gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) - - @patch('casp.utils.gcloud._prompt_for_custom_path') - @patch('casp.utils.gcloud._run_gcloud_login') - @patch('click.confirm') - @patch('casp.utils.gcloud._is_valid_credentials') - def test_get_credentials_path_login_success(self, mock_is_valid, - mock_confirm, mock_login, - mock_prompt): - """Test get_credentials_path with successful login.""" - mock_is_valid.return_value = False - mock_confirm.return_value = True - mock_login.return_value = True + mock_is_valid.assert_called_once_with(gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + + @patch('casp.utils.gcloud._prompt_for_custom_path', autospec=True) + @patch('casp.utils.gcloud._run_gcloud_login', return_value=True, autospec=True) + @patch('click.confirm', return_value=True, autospec=True) + @patch('casp.utils.gcloud._is_valid_credentials', return_value=False, autospec=True) + def test_login_success(self, mock_is_valid, mock_confirm, mock_login, + mock_prompt): + """Tests successful login after default path fails.""" self.assertEqual( gcloud.get_credentials_path(), gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + mock_is_valid.assert_called_once_with(gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + mock_confirm.assert_called_once() + mock_login.assert_called_once() mock_prompt.assert_not_called() - @patch('casp.utils.gcloud._prompt_for_custom_path') - @patch('casp.utils.gcloud._run_gcloud_login') - @patch('click.confirm') - @patch('casp.utils.gcloud._is_valid_credentials') - def test_get_credentials_path_login_fail_then_custom(self, mock_is_valid, - mock_confirm, - mock_login, - mock_prompt): - """Test get_credentials_path with failed login then custom path.""" - mock_is_valid.return_value = False - mock_confirm.return_value = True - mock_login.return_value = False - mock_prompt.return_value = '/custom/path' + @patch('casp.utils.gcloud._prompt_for_custom_path', return_value='/custom/path', autospec=True) + @patch('casp.utils.gcloud._run_gcloud_login', return_value=False, autospec=True) + @patch('click.confirm', return_value=True, autospec=True) + @patch('casp.utils.gcloud._is_valid_credentials', return_value=False, autospec=True) + def test_login_fail_then_custom_path(self, mock_is_valid, mock_confirm, + mock_login, mock_prompt): + """Tests providing a custom path after a failed login.""" self.assertEqual(gcloud.get_credentials_path(), '/custom/path') - - @patch('casp.utils.gcloud._prompt_for_custom_path') - @patch('casp.utils.gcloud._run_gcloud_login') - @patch('click.confirm') - @patch('casp.utils.gcloud._is_valid_credentials') - def test_get_credentials_path_no_login_then_custom(self, mock_is_valid, - mock_confirm, mock_login, - mock_prompt): - """Test get_credentials_path with no login then custom path.""" - mock_is_valid.return_value = False - mock_confirm.return_value = False - mock_prompt.return_value = '/custom/path' + mock_is_valid.assert_called_once_with(gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + mock_confirm.assert_called_once() + mock_login.assert_called_once() + mock_prompt.assert_called_once() + + @patch('casp.utils.gcloud._prompt_for_custom_path', return_value='/custom/path', autospec=True) + @patch('casp.utils.gcloud._run_gcloud_login', autospec=True) + @patch('click.confirm', return_value=False, autospec=True) + @patch('casp.utils.gcloud._is_valid_credentials', return_value=False, autospec=True) + def test_decline_login_then_custom_path(self, mock_is_valid, mock_confirm, + mock_login, mock_prompt): + """Tests providing a custom path after declining to log in.""" self.assertEqual(gcloud.get_credentials_path(), '/custom/path') + mock_is_valid.assert_called_once_with(gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + mock_confirm.assert_called_once() mock_login.assert_not_called() + mock_prompt.assert_called_once() - @patch('casp.utils.gcloud._prompt_for_custom_path') - @patch('click.confirm') - @patch('casp.utils.gcloud._is_valid_credentials') - def test_get_credentials_path_all_fail(self, mock_is_valid, mock_confirm, - mock_prompt): - """Test get_credentials_path when all methods fail.""" - mock_is_valid.return_value = False - mock_confirm.return_value = False - mock_prompt.return_value = None + @patch('casp.utils.gcloud._prompt_for_custom_path', return_value=None, autospec=True) + @patch('click.confirm', return_value=False, autospec=True) + @patch('casp.utils.gcloud._is_valid_credentials', return_value=False, autospec=True) + def test_all_fail(self, mock_is_valid, mock_confirm, mock_prompt): + """Tests when all methods to get credentials fail.""" self.assertIsNone(gcloud.get_credentials_path()) + mock_is_valid.assert_called_once_with(gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + mock_confirm.assert_called_once() + mock_prompt.assert_called_once() if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() From 6f9bc2759634ca62859328890b7d6fe5b9a2f95a Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Wed, 22 Oct 2025 18:36:30 +0000 Subject: [PATCH 03/10] refactor: remove unecessary comentary --- cli/casp/src/casp/utils/gcloud.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cli/casp/src/casp/utils/gcloud.py b/cli/casp/src/casp/utils/gcloud.py index 572d4b29fd7..ae239c5b6ef 100644 --- a/cli/casp/src/casp/utils/gcloud.py +++ b/cli/casp/src/casp/utils/gcloud.py @@ -79,10 +79,6 @@ def get_credentials_path() -> str | None: """ Finds a valid gcloud credentials path, prompting the user if needed. - This function orchestrates the process of finding credentials by first - checking the default path, then offering an interactive login, and finally - allowing the user to specify a custom path. - Returns: The path to a valid credentials file, or None if one cannot be found. """ From 807c2534509621b888d2caef137244258293b16d Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Wed, 22 Oct 2025 18:46:42 +0000 Subject: [PATCH 04/10] fix: year in comentary --- cli/casp/src/casp/tests/utils/test_gcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/casp/src/casp/tests/utils/test_gcloud.py b/cli/casp/src/casp/tests/utils/test_gcloud.py index e0c1e6c1339..09acd5b64f2 100644 --- a/cli/casp/src/casp/tests/utils/test_gcloud.py +++ b/cli/casp/src/casp/tests/utils/test_gcloud.py @@ -1,4 +1,4 @@ -# Copyright 2024 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 105381090f265a5b2e44fb2d60d191a014ce6fd6 Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Tue, 28 Oct 2025 12:35:09 +0000 Subject: [PATCH 05/10] refactor: add copyright notice in __init__.py files --- cli/casp/src/casp/__init__.py | 13 +++++++++++++ cli/casp/src/casp/commands/__init__.py | 13 +++++++++++++ cli/casp/src/casp/tests/__init__.py | 13 +++++++++++++ cli/casp/src/casp/tests/utils/__init__.py | 13 +++++++++++++ 4 files changed, 52 insertions(+) diff --git a/cli/casp/src/casp/__init__.py b/cli/casp/src/casp/__init__.py index e69de29bb2d..85f1f407178 100644 --- a/cli/casp/src/casp/__init__.py +++ b/cli/casp/src/casp/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. \ No newline at end of file diff --git a/cli/casp/src/casp/commands/__init__.py b/cli/casp/src/casp/commands/__init__.py index e69de29bb2d..85f1f407178 100644 --- a/cli/casp/src/casp/commands/__init__.py +++ b/cli/casp/src/casp/commands/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. \ No newline at end of file diff --git a/cli/casp/src/casp/tests/__init__.py b/cli/casp/src/casp/tests/__init__.py index e69de29bb2d..85f1f407178 100644 --- a/cli/casp/src/casp/tests/__init__.py +++ b/cli/casp/src/casp/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. \ No newline at end of file diff --git a/cli/casp/src/casp/tests/utils/__init__.py b/cli/casp/src/casp/tests/utils/__init__.py index e69de29bb2d..85f1f407178 100644 --- a/cli/casp/src/casp/tests/utils/__init__.py +++ b/cli/casp/src/casp/tests/utils/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. \ No newline at end of file From 4473653dcf691d820572f3d2192e3480674965af Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Tue, 28 Oct 2025 14:52:00 +0000 Subject: [PATCH 06/10] fix: replace expected excpetions and fix tests --- cli/casp/src/casp/tests/utils/test_gcloud.py | 9 --------- cli/casp/src/casp/utils/gcloud.py | 4 +++- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/cli/casp/src/casp/tests/utils/test_gcloud.py b/cli/casp/src/casp/tests/utils/test_gcloud.py index 09acd5b64f2..ce2bd98871d 100644 --- a/cli/casp/src/casp/tests/utils/test_gcloud.py +++ b/cli/casp/src/casp/tests/utils/test_gcloud.py @@ -47,15 +47,6 @@ def test_path_does_not_exist(self, mock_exists): @patch('google.oauth2.credentials.Credentials.from_authorized_user_file', autospec=True) def test_auth_error(self, mock_from_file, mock_exists): """Tests with an auth exception.""" - mock_from_file.side_effect = auth_exceptions.DefaultCredentialsError - self.assertFalse(gcloud._is_valid_credentials('path')) - mock_exists.assert_called_once_with('path') - mock_from_file.assert_called_once_with('path') - - @patch('os.path.exists', return_value=True, autospec=True) - @patch('google.oauth2.credentials.Credentials.from_authorized_user_file', autospec=True) - def test_value_error(self, mock_from_file, mock_exists): - """Tests with a ValueError.""" mock_from_file.side_effect = ValueError self.assertFalse(gcloud._is_valid_credentials('path')) mock_exists.assert_called_once_with('path') diff --git a/cli/casp/src/casp/utils/gcloud.py b/cli/casp/src/casp/utils/gcloud.py index ae239c5b6ef..04c50be37e4 100644 --- a/cli/casp/src/casp/utils/gcloud.py +++ b/cli/casp/src/casp/utils/gcloud.py @@ -27,11 +27,13 @@ def _is_valid_credentials(path: str) -> bool: """Returns True if the path points to a valid credentials file.""" if not path or not os.path.exists(path): + click.secho('Error: No valid credentials file found.', fg='red') return False try: credentials.Credentials.from_authorized_user_file(path) return True - except (auth_exceptions.DefaultCredentialsError, ValueError, KeyError): + except ValueError as e: + click.secho(f'Error: {e}', fg='red') return False From 559e78aea3df2911725888938319c0c8f85cbb66 Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Tue, 28 Oct 2025 16:56:46 +0000 Subject: [PATCH 07/10] refactor: lint and format --- cli/casp/src/casp/__init__.py | 2 +- cli/casp/src/casp/commands/__init__.py | 2 +- cli/casp/src/casp/tests/__init__.py | 2 +- cli/casp/src/casp/tests/utils/__init__.py | 2 +- cli/casp/src/casp/tests/utils/test_gcloud.py | 125 +++++++++++++------ cli/casp/src/casp/utils/gcloud.py | 1 - 6 files changed, 89 insertions(+), 45 deletions(-) diff --git a/cli/casp/src/casp/__init__.py b/cli/casp/src/casp/__init__.py index 85f1f407178..c37e93b74bf 100644 --- a/cli/casp/src/casp/__init__.py +++ b/cli/casp/src/casp/__init__.py @@ -10,4 +10,4 @@ # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and -# limitations under the License. \ No newline at end of file +# limitations under the License. diff --git a/cli/casp/src/casp/commands/__init__.py b/cli/casp/src/casp/commands/__init__.py index 85f1f407178..c37e93b74bf 100644 --- a/cli/casp/src/casp/commands/__init__.py +++ b/cli/casp/src/casp/commands/__init__.py @@ -10,4 +10,4 @@ # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and -# limitations under the License. \ No newline at end of file +# limitations under the License. diff --git a/cli/casp/src/casp/tests/__init__.py b/cli/casp/src/casp/tests/__init__.py index 85f1f407178..c37e93b74bf 100644 --- a/cli/casp/src/casp/tests/__init__.py +++ b/cli/casp/src/casp/tests/__init__.py @@ -10,4 +10,4 @@ # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and -# limitations under the License. \ No newline at end of file +# limitations under the License. diff --git a/cli/casp/src/casp/tests/utils/__init__.py b/cli/casp/src/casp/tests/utils/__init__.py index 85f1f407178..c37e93b74bf 100644 --- a/cli/casp/src/casp/tests/utils/__init__.py +++ b/cli/casp/src/casp/tests/utils/__init__.py @@ -10,4 +10,4 @@ # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and -# limitations under the License. \ No newline at end of file +# limitations under the License. diff --git a/cli/casp/src/casp/tests/utils/test_gcloud.py b/cli/casp/src/casp/tests/utils/test_gcloud.py index ce2bd98871d..b793f603a40 100644 --- a/cli/casp/src/casp/tests/utils/test_gcloud.py +++ b/cli/casp/src/casp/tests/utils/test_gcloud.py @@ -19,9 +19,9 @@ import subprocess import unittest -from unittest.mock import patch, Mock +from unittest.mock import Mock +from unittest.mock import patch -from google.auth import exceptions as auth_exceptions from casp.utils import gcloud @@ -29,56 +29,64 @@ class IsValidCredentialsTest(unittest.TestCase): """Tests for _is_valid_credentials.""" @patch('os.path.exists', return_value=True, autospec=True) - @patch('google.oauth2.credentials.Credentials.from_authorized_user_file', autospec=True) + @patch( + 'google.oauth2.credentials.Credentials.from_authorized_user_file', + autospec=True) def test_valid_credentials(self, mock_from_file, mock_exists): """Tests with a valid credentials file.""" mock_from_file.return_value = Mock() - self.assertTrue(gcloud._is_valid_credentials('valid/path')) + self.assertTrue(gcloud._is_valid_credentials('valid/path')) # pylint: disable=protected-access mock_exists.assert_called_once_with('valid/path') mock_from_file.assert_called_once_with('valid/path') @patch('os.path.exists', return_value=False, autospec=True) def test_path_does_not_exist(self, mock_exists): """Tests with a non-existent path.""" - self.assertFalse(gcloud._is_valid_credentials('invalid/path')) + self.assertFalse(gcloud._is_valid_credentials('invalid/path')) # pylint: disable=protected-access mock_exists.assert_called_once_with('invalid/path') @patch('os.path.exists', return_value=True, autospec=True) - @patch('google.oauth2.credentials.Credentials.from_authorized_user_file', autospec=True) + @patch( + 'google.oauth2.credentials.Credentials.from_authorized_user_file', + autospec=True) def test_auth_error(self, mock_from_file, mock_exists): """Tests with an auth exception.""" mock_from_file.side_effect = ValueError - self.assertFalse(gcloud._is_valid_credentials('path')) + self.assertFalse(gcloud._is_valid_credentials('path')) # pylint: disable=protected-access mock_exists.assert_called_once_with('path') mock_from_file.assert_called_once_with('path') def test_empty_path(self): """Tests with an empty path string.""" - self.assertFalse(gcloud._is_valid_credentials('')) + self.assertFalse(gcloud._is_valid_credentials('')) # pylint: disable=protected-access def test_none_path(self): """Tests with a None path.""" - self.assertFalse(gcloud._is_valid_credentials(None)) + self.assertFalse(gcloud._is_valid_credentials(None)) # pylint: disable=protected-access class RunGcloudLoginTest(unittest.TestCase): """Tests for _run_gcloud_login.""" - @patch('casp.utils.gcloud._is_valid_credentials', return_value=True, autospec=True) + @patch( + 'casp.utils.gcloud._is_valid_credentials', + return_value=True, + autospec=True) @patch('subprocess.run', autospec=True) def test_login_success(self, mock_run, mock_is_valid): """Tests successful gcloud login.""" - self.assertTrue(gcloud._run_gcloud_login()) + self.assertTrue(gcloud._run_gcloud_login()) # pylint: disable=protected-access mock_run.assert_called_once_with( ['gcloud', 'auth', 'application-default', 'login'], check=True) - mock_is_valid.assert_called_once_with(gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + mock_is_valid.assert_called_once_with( + gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) @patch('subprocess.run', autospec=True) @patch('click.secho', autospec=True) def test_gcloud_not_found(self, mock_secho, mock_run): """Tests with gcloud command not found.""" mock_run.side_effect = FileNotFoundError - self.assertFalse(gcloud._run_gcloud_login()) + self.assertFalse(gcloud._run_gcloud_login()) # pylint: disable=protected-access mock_secho.assert_called_once() args, _ = mock_secho.call_args self.assertIn('gcloud command not found', args[0]) @@ -88,7 +96,7 @@ def test_gcloud_not_found(self, mock_secho, mock_run): def test_login_failed(self, mock_secho, mock_run): """Tests with a failed login command.""" mock_run.side_effect = subprocess.CalledProcessError(1, 'cmd') - self.assertFalse(gcloud._run_gcloud_login()) + self.assertFalse(gcloud._run_gcloud_login()) # pylint: disable=protected-access mock_secho.assert_called_once_with('Error: gcloud login failed.', fg='red') @@ -96,20 +104,26 @@ class PromptForCustomPathTest(unittest.TestCase): """Tests for _prompt_for_custom_path.""" @patch('click.prompt', autospec=True) - @patch('casp.utils.gcloud._is_valid_credentials', return_value=True, autospec=True) + @patch( + 'casp.utils.gcloud._is_valid_credentials', + return_value=True, + autospec=True) def test_valid_path(self, mock_is_valid, mock_prompt): """Tests with a valid custom path.""" mock_prompt.return_value = '/valid/path' - self.assertEqual(gcloud._prompt_for_custom_path(), '/valid/path') + self.assertEqual(gcloud._prompt_for_custom_path(), '/valid/path') # pylint: disable=protected-access mock_is_valid.assert_called_once_with('/valid/path') @patch('click.prompt', autospec=True) - @patch('casp.utils.gcloud._is_valid_credentials', return_value=False, autospec=True) + @patch( + 'casp.utils.gcloud._is_valid_credentials', + return_value=False, + autospec=True) @patch('click.secho', autospec=True) def test_invalid_path(self, mock_secho, mock_is_valid, mock_prompt): """Tests with an invalid custom path.""" mock_prompt.return_value = '/invalid/path' - self.assertIsNone(gcloud._prompt_for_custom_path()) + self.assertIsNone(gcloud._prompt_for_custom_path()) # pylint: disable=protected-access mock_is_valid.assert_called_once_with('/invalid/path') mock_secho.assert_called_once_with( 'Error: The provided credentials file is not valid.', fg='red') @@ -118,66 +132,97 @@ def test_invalid_path(self, mock_secho, mock_is_valid, mock_prompt): def test_empty_path(self, mock_prompt): """Tests with empty input from prompt.""" mock_prompt.return_value = '' - self.assertIsNone(gcloud._prompt_for_custom_path()) + self.assertIsNone(gcloud._prompt_for_custom_path()) # pylint: disable=protected-access class GetCredentialsPathTest(unittest.TestCase): """Tests for get_credentials_path.""" - @patch('casp.utils.gcloud._is_valid_credentials', return_value=True, autospec=True) + @patch( + 'casp.utils.gcloud._is_valid_credentials', + return_value=True, + autospec=True) def test_default_path_valid(self, mock_is_valid): """Tests when the default credentials path is valid.""" - self.assertEqual( - gcloud.get_credentials_path(), gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) - mock_is_valid.assert_called_once_with(gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + self.assertEqual(gcloud.get_credentials_path(), + gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + mock_is_valid.assert_called_once_with( + gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) @patch('casp.utils.gcloud._prompt_for_custom_path', autospec=True) - @patch('casp.utils.gcloud._run_gcloud_login', return_value=True, autospec=True) + @patch( + 'casp.utils.gcloud._run_gcloud_login', return_value=True, autospec=True) @patch('click.confirm', return_value=True, autospec=True) - @patch('casp.utils.gcloud._is_valid_credentials', return_value=False, autospec=True) + @patch( + 'casp.utils.gcloud._is_valid_credentials', + return_value=False, + autospec=True) def test_login_success(self, mock_is_valid, mock_confirm, mock_login, mock_prompt): """Tests successful login after default path fails.""" - self.assertEqual( - gcloud.get_credentials_path(), gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) - mock_is_valid.assert_called_once_with(gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + self.assertEqual(gcloud.get_credentials_path(), + gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + mock_is_valid.assert_called_once_with( + gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) mock_confirm.assert_called_once() mock_login.assert_called_once() mock_prompt.assert_not_called() - @patch('casp.utils.gcloud._prompt_for_custom_path', return_value='/custom/path', autospec=True) - @patch('casp.utils.gcloud._run_gcloud_login', return_value=False, autospec=True) + @patch( + 'casp.utils.gcloud._prompt_for_custom_path', + return_value='/custom/path', + autospec=True) + @patch( + 'casp.utils.gcloud._run_gcloud_login', return_value=False, autospec=True) @patch('click.confirm', return_value=True, autospec=True) - @patch('casp.utils.gcloud._is_valid_credentials', return_value=False, autospec=True) + @patch( + 'casp.utils.gcloud._is_valid_credentials', + return_value=False, + autospec=True) def test_login_fail_then_custom_path(self, mock_is_valid, mock_confirm, - mock_login, mock_prompt): + mock_login, mock_prompt): """Tests providing a custom path after a failed login.""" self.assertEqual(gcloud.get_credentials_path(), '/custom/path') - mock_is_valid.assert_called_once_with(gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + mock_is_valid.assert_called_once_with( + gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) mock_confirm.assert_called_once() mock_login.assert_called_once() mock_prompt.assert_called_once() - @patch('casp.utils.gcloud._prompt_for_custom_path', return_value='/custom/path', autospec=True) + @patch( + 'casp.utils.gcloud._prompt_for_custom_path', + return_value='/custom/path', + autospec=True) @patch('casp.utils.gcloud._run_gcloud_login', autospec=True) @patch('click.confirm', return_value=False, autospec=True) - @patch('casp.utils.gcloud._is_valid_credentials', return_value=False, autospec=True) + @patch( + 'casp.utils.gcloud._is_valid_credentials', + return_value=False, + autospec=True) def test_decline_login_then_custom_path(self, mock_is_valid, mock_confirm, - mock_login, mock_prompt): + mock_login, mock_prompt): """Tests providing a custom path after declining to log in.""" self.assertEqual(gcloud.get_credentials_path(), '/custom/path') - mock_is_valid.assert_called_once_with(gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + mock_is_valid.assert_called_once_with( + gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) mock_confirm.assert_called_once() mock_login.assert_not_called() mock_prompt.assert_called_once() - @patch('casp.utils.gcloud._prompt_for_custom_path', return_value=None, autospec=True) + @patch( + 'casp.utils.gcloud._prompt_for_custom_path', + return_value=None, + autospec=True) @patch('click.confirm', return_value=False, autospec=True) - @patch('casp.utils.gcloud._is_valid_credentials', return_value=False, autospec=True) + @patch( + 'casp.utils.gcloud._is_valid_credentials', + return_value=False, + autospec=True) def test_all_fail(self, mock_is_valid, mock_confirm, mock_prompt): """Tests when all methods to get credentials fail.""" self.assertIsNone(gcloud.get_credentials_path()) - mock_is_valid.assert_called_once_with(gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) + mock_is_valid.assert_called_once_with( + gcloud.DEFAULT_GCLOUD_CREDENTIALS_PATH) mock_confirm.assert_called_once() mock_prompt.assert_called_once() diff --git a/cli/casp/src/casp/utils/gcloud.py b/cli/casp/src/casp/utils/gcloud.py index 04c50be37e4..3da72eea901 100644 --- a/cli/casp/src/casp/utils/gcloud.py +++ b/cli/casp/src/casp/utils/gcloud.py @@ -17,7 +17,6 @@ import subprocess import click -from google.auth import exceptions as auth_exceptions from google.oauth2 import credentials DEFAULT_GCLOUD_CREDENTIALS_PATH = os.path.expanduser( From 222dcee8403f721e274fdc0a3210eca497449cec Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Mon, 3 Nov 2025 13:13:57 +0000 Subject: [PATCH 08/10] fix: add orientation on running gcloud login --- cli/casp/src/casp/tests/utils/test_gcloud.py | 5 +++-- cli/casp/src/casp/utils/gcloud.py | 12 +++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/cli/casp/src/casp/tests/utils/test_gcloud.py b/cli/casp/src/casp/tests/utils/test_gcloud.py index b793f603a40..0ad9e593061 100644 --- a/cli/casp/src/casp/tests/utils/test_gcloud.py +++ b/cli/casp/src/casp/tests/utils/test_gcloud.py @@ -97,8 +97,9 @@ def test_login_failed(self, mock_secho, mock_run): """Tests with a failed login command.""" mock_run.side_effect = subprocess.CalledProcessError(1, 'cmd') self.assertFalse(gcloud._run_gcloud_login()) # pylint: disable=protected-access - mock_secho.assert_called_once_with('Error: gcloud login failed.', fg='red') - + mock_secho.assert_called_once() + args, _ = mock_secho.call_args + self.assertIn('gcloud login failed', args[0]) class PromptForCustomPathTest(unittest.TestCase): """Tests for _prompt_for_custom_path.""" diff --git a/cli/casp/src/casp/utils/gcloud.py b/cli/casp/src/casp/utils/gcloud.py index 3da72eea901..a46b40c5165 100644 --- a/cli/casp/src/casp/utils/gcloud.py +++ b/cli/casp/src/casp/utils/gcloud.py @@ -32,7 +32,7 @@ def _is_valid_credentials(path: str) -> bool: credentials.Credentials.from_authorized_user_file(path) return True except ValueError as e: - click.secho(f'Error: {e}', fg='red') + click.secho(f'Error when checking for valid credentials: {e}', fg='red') return False @@ -48,11 +48,17 @@ def _run_gcloud_login() -> bool: except FileNotFoundError: click.secho( 'Error: gcloud command not found. Please ensure it is installed and ' - 'in your PATH.', + 'in your PATH. ' + 'Or you can mannually run ' + '`gcloud auth application-default login`', fg='red') return False except subprocess.CalledProcessError: - click.secho('Error: gcloud login failed.', fg='red') + click.secho( + 'Error: gcloud login failed. ' + 'You can mannually run ' + '`gcloud auth application-default login`', + fg='red') return False From 8719402bbbe725e2c7de94c1a70a032fbcd4d017 Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Mon, 3 Nov 2025 13:35:31 +0000 Subject: [PATCH 09/10] refactor: add specific instructions for testing and update readme --- cli/casp/README.md | 8 ++++++++ cli/casp/src/casp/tests/utils/test_gcloud.py | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/cli/casp/README.md b/cli/casp/README.md index cb7637fc23e..ae1ee44b051 100644 --- a/cli/casp/README.md +++ b/cli/casp/README.md @@ -63,3 +63,11 @@ For example, to add a `my-command` command, follow these steps: Once you have completed these steps, the new command will be available as `casp my-command`. + +## Running Tests + +To run all unit tests for the `casp` CLI, use the following command from the root of the project: + +```bash +python -m unittest discover -s cli/casp/src/casp/tests -v +``` \ No newline at end of file diff --git a/cli/casp/src/casp/tests/utils/test_gcloud.py b/cli/casp/src/casp/tests/utils/test_gcloud.py index 0ad9e593061..f935e55c4ae 100644 --- a/cli/casp/src/casp/tests/utils/test_gcloud.py +++ b/cli/casp/src/casp/tests/utils/test_gcloud.py @@ -13,8 +13,8 @@ # limitations under the License. """Tests for the gcloud utility functions. - For running all the tests, use (from the root of the project): - python -m unittest discover -s cli/casp/src/casp/tests -v + For running, use (from the root of the project): + python -m unittest discover -s cli/casp/src/casp/tests -p test_gcloud.py -v """ import subprocess From ce5024af17e132638a19dd710c093c14588dbd4b Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Mon, 3 Nov 2025 13:40:06 +0000 Subject: [PATCH 10/10] lint: butler format --- cli/casp/src/casp/tests/utils/test_gcloud.py | 1 + cli/casp/src/casp/utils/gcloud.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cli/casp/src/casp/tests/utils/test_gcloud.py b/cli/casp/src/casp/tests/utils/test_gcloud.py index f935e55c4ae..6abc36c7cda 100644 --- a/cli/casp/src/casp/tests/utils/test_gcloud.py +++ b/cli/casp/src/casp/tests/utils/test_gcloud.py @@ -101,6 +101,7 @@ def test_login_failed(self, mock_secho, mock_run): args, _ = mock_secho.call_args self.assertIn('gcloud login failed', args[0]) + class PromptForCustomPathTest(unittest.TestCase): """Tests for _prompt_for_custom_path.""" diff --git a/cli/casp/src/casp/utils/gcloud.py b/cli/casp/src/casp/utils/gcloud.py index a46b40c5165..04b5327e343 100644 --- a/cli/casp/src/casp/utils/gcloud.py +++ b/cli/casp/src/casp/utils/gcloud.py @@ -55,10 +55,10 @@ def _run_gcloud_login() -> bool: return False except subprocess.CalledProcessError: click.secho( - 'Error: gcloud login failed. ' - 'You can mannually run ' - '`gcloud auth application-default login`', - fg='red') + 'Error: gcloud login failed. ' + 'You can mannually run ' + '`gcloud auth application-default login`', + fg='red') return False