diff --git a/tests/cli/test_api.py b/tests/cli/test_api.py index da37030..d1783ae 100644 --- a/tests/cli/test_api.py +++ b/tests/cli/test_api.py @@ -1,120 +1,86 @@ import unittest -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch +from datetime import datetime, timedelta +import click from click.testing import CliRunner from claudesync.cli.main import cli -from claudesync.exceptions import ProviderError +from claudesync.config_manager import ConfigManager class TestAPICLI(unittest.TestCase): def setUp(self): self.runner = CliRunner() - self.mock_config = MagicMock() - self.mock_provider = MagicMock() + self.config_mock = MagicMock(spec=ConfigManager) - @patch("claudesync.providers.base_claude_ai._get_session_key_expiry") @patch("claudesync.cli.api.get_provider") - @patch("claudesync.cli.main.ConfigManager") + def test_login_provider_error(self, mock_get_provider): + mock_get_provider.return_value = ["claude.ai", "claude.ai-curl"] + result = self.runner.invoke( + cli, ["api", "login", "invalid_provider"], obj=self.config_mock + ) + self.assertIn("Error: Unknown provider 'invalid_provider'", result.output) + self.assertEqual(result.exit_code, 0) + + @patch("claudesync.cli.api.get_provider") + @patch("claudesync.cli.api.click.confirm") @patch("claudesync.cli.api.org_select") @patch("claudesync.cli.api.proj_select") + @patch("claudesync.cli.api.project_create") + @patch("claudesync.cli.api.submodule_create") def test_login_success( self, + mock_submodule_create, + mock_project_create, mock_proj_select, mock_org_select, - mock_config_manager, + mock_confirm, mock_get_provider, - mock_get_session_key_expiry, ): - mock_config_manager.return_value = self.mock_config - mock_get_provider.return_value = self.mock_provider - mock_get_provider.side_effect = lambda x=None: ( - ["claude.ai"] if x is None else self.mock_provider - ) - expiry = "Tue, 03 Sep 2099 06:51:21 UTC" - self.mock_provider.login.return_value = ("test_session_key", expiry) - - mock_get_session_key_expiry.return_value = expiry - - result = self.runner.invoke(cli, ["api", "login", "claude.ai"]) - - self.assertEqual(result.exit_code, 0) - self.assertIn("Logged in successfully.", result.output) - self.mock_config.set_session_key.assert_called_once_with( - "test_session_key", expiry - ) - self.mock_config.set.assert_called_with("active_provider", "claude.ai") - mock_org_select.assert_called_once() - mock_proj_select.assert_called_once() - - @patch("claudesync.cli.api.get_provider") - @patch("claudesync.cli.main.ConfigManager") - def test_login_provider_error(self, mock_config_manager, mock_get_provider): - mock_config_manager.return_value = self.mock_config - mock_get_provider.return_value = self.mock_provider - mock_get_provider.side_effect = lambda x=None: ( - ["claude.ai"] if x is None else self.mock_provider - ) - self.mock_provider.login.side_effect = ProviderError("Login failed") - - result = self.runner.invoke(cli, ["api", "login", "claude.ai"]) - - self.assertEqual(result.exit_code, 0) - self.assertIn("Error: Login failed", result.output) - - @patch("claudesync.cli.main.ConfigManager") - def test_logout(self, mock_config_manager): - mock_config_manager.return_value = self.mock_config - - result = self.runner.invoke(cli, ["api", "logout"]) - - self.assertEqual(result.exit_code, 0) - self.assertIn("Logged out successfully.", result.output) - self.mock_config.set.assert_any_call("session_key", None) - self.mock_config.set.assert_any_call("active_provider", None) - self.mock_config.set.assert_any_call("active_organization_id", None) - - @patch("claudesync.cli.main.ConfigManager") - def test_ratelimit_set(self, mock_config_manager): - mock_config_manager.return_value = self.mock_config - - result = self.runner.invoke(cli, ["api", "ratelimit", "--delay", "1.5"]) - - self.assertEqual(result.exit_code, 0) - self.assertIn("Upload delay set to 1.5 seconds.", result.output) - self.mock_config.set.assert_called_once_with("upload_delay", 1.5) - - @patch("claudesync.cli.main.ConfigManager") - def test_ratelimit_negative_value(self, mock_config_manager): - mock_config_manager.return_value = self.mock_config - - result = self.runner.invoke(cli, ["api", "ratelimit", "--delay", "-1"]) - - self.assertEqual(result.exit_code, 0) - self.assertIn( - "Error: Upload delay must be a non-negative number.", result.output + # Mock provider instance + mock_provider = MagicMock() + mock_provider.login.return_value = ( + "mock_session_key", + datetime.now() + timedelta(days=30), ) - self.mock_config.set.assert_not_called() - - @patch("claudesync.cli.main.ConfigManager") - def test_max_filesize_set(self, mock_config_manager): - mock_config_manager.return_value = self.mock_config - - result = self.runner.invoke(cli, ["api", "max-filesize", "--size", "1048576"]) + mock_provider.get_organizations.return_value = [ + {"id": "org1", "name": "Test Org"} + ] + mock_provider.get_projects.return_value = [ + {"id": "proj1", "name": "Test Project"} + ] + + # Mock get_provider to return the list of providers and then the mock provider instance + mock_get_provider.side_effect = [["claude.ai", "claude.ai-curl"], mock_provider] + + # Mock user confirmations + mock_confirm.side_effect = [ + False, + True, + True, + ] # Don't use existing session, select existing project, delete remote files + + # Mock config operations + self.config_mock.get_session_key.return_value = None + self.config_mock.get.return_value = None + + # Mock organization and project selection + mock_org_select.return_value = None + mock_proj_select.return_value = None + + runner = CliRunner() + result = runner.invoke(cli, ["api", "login", "claude.ai"], obj=self.config_mock) self.assertEqual(result.exit_code, 0) - self.assertIn("Maximum file size set to 1048576 bytes.", result.output) - self.mock_config.set.assert_called_once_with("max_file_size", 1048576) + self.assertIn("Logged in successfully", result.output) - @patch("claudesync.cli.main.ConfigManager") - def test_max_filesize_negative_value(self, mock_config_manager): - mock_config_manager.return_value = self.mock_config + # Verify that organization select was invoked + mock_org_select.assert_called_once() - result = self.runner.invoke(cli, ["api", "max-filesize", "--size", "-1"]) + # Verify that project select was invoked + mock_proj_select.assert_called_once() - self.assertEqual(result.exit_code, 0) - self.assertIn( - "Error: Maximum file size must be a non-negative number.", result.output - ) - self.mock_config.set.assert_not_called() + # Verify that submodule create was not called (we selected an existing project) + mock_submodule_create.assert_not_called() if __name__ == "__main__": diff --git a/tests/cli/test_project.py b/tests/cli/test_project.py index 0ee044c..b27243a 100644 --- a/tests/cli/test_project.py +++ b/tests/cli/test_project.py @@ -1,296 +1,35 @@ -import pytest -from unittest.mock import patch, MagicMock, call +import unittest +from unittest.mock import patch, MagicMock from click.testing import CliRunner -from claudesync.cli.project import sync -from claudesync.exceptions import ProviderError +from claudesync.cli.main import cli +from claudesync.config_manager import ConfigManager -@pytest.fixture -def mock_config(): - config = MagicMock() - config.get.side_effect = lambda key, default=None: { - "active_organization_id": "org123", - "active_project_id": "proj456", - "active_project_name": "MainProject", - "local_path": "/path/to/project", - "submodule_detect_filenames": ["pom.xml", "build.gradle"], - }.get(key, default) - return config +class TestProjectCLI(unittest.TestCase): + def setUp(self): + self.runner = CliRunner() + self.config_mock = MagicMock(spec=ConfigManager) - -@pytest.fixture -def mock_provider(): - return MagicMock() - - -@pytest.fixture -def mock_sync_manager(): - return MagicMock() - - -@pytest.fixture -def mock_get_local_files(): - with patch("claudesync.cli.project.get_local_files") as mock: - yield mock - - -@pytest.fixture -def mock_detect_submodules(): - with patch("claudesync.cli.project.detect_submodules") as mock: - yield mock - - -class TestProjectCLI: @patch("claudesync.cli.project.validate_and_get_provider") - @patch("claudesync.cli.project.SyncManager") - @patch("os.path.abspath") - @patch("os.path.join") - @patch("os.makedirs") - def test_project_sync( - self, - mock_makedirs, - mock_path_join, - mock_path_abspath, - mock_sync_manager_class, - mock_validate_provider, - mock_config, - mock_provider, - mock_sync_manager, - mock_get_local_files, - mock_detect_submodules, - ): - # Setup - runner = CliRunner() - mock_validate_provider.return_value = mock_provider - mock_sync_manager_class.return_value = mock_sync_manager - - mock_provider.get_projects.return_value = [ - {"id": "proj456", "name": "MainProject"}, - {"id": "sub789", "name": "MainProject-SubModule-SubA"}, - ] - mock_provider.list_files.side_effect = [ - [ - { - "uuid": "file1", - "file_name": "main.py", - "content": "print('main')", - "created_at": "2023-01-01T00:00:00Z", - } - ], - [ - { - "uuid": "file2", - "file_name": "sub.py", - "content": "print('sub')", - "created_at": "2023-01-01T00:00:00Z", - } - ], - ] - - mock_get_local_files.side_effect = [{"main.py": "hash1"}, {"sub.py": "hash2"}] - - mock_detect_submodules.return_value = [("SubA", "pom.xml")] - - mock_path_abspath.side_effect = lambda x: x - mock_path_join.side_effect = lambda *args: "/".join(args) - - # Execute - result = runner.invoke(sync, obj=mock_config) - - # Assert - assert ( - result.exit_code == 0 - ), f"Exit code was {result.exit_code}, expected 0. Exception: {result.exception}" - assert "Main project 'MainProject' synced successfully." in result.output - assert "Syncing submodule 'SubA'..." in result.output - assert "Submodule 'SubA' synced successfully." in result.output - assert ( - "Project sync completed successfully, including available submodules." - in result.output - ) - - # Verify method calls - mock_validate_provider.assert_called_once_with( - mock_config, require_project=True - ) - mock_provider.get_projects.assert_called_once_with( - "org123", include_archived=False - ) - mock_detect_submodules.assert_called_once_with( - "/path/to/project", ["pom.xml", "build.gradle"] - ) - - assert mock_provider.list_files.call_count == 2 - mock_provider.list_files.assert_has_calls( - [call("org123", "proj456"), call("org123", "sub789")] - ) - - assert mock_get_local_files.call_count == 2 - mock_get_local_files.assert_has_calls( - [call("/path/to/project", None), call("/path/to/project/SubA", None)] - ) - - assert mock_sync_manager.sync.call_count == 2 - mock_sync_manager.sync.assert_has_calls( - [ - call( - {"main.py": "hash1"}, - [ - { - "uuid": "file1", - "file_name": "main.py", - "content": "print('main')", - "created_at": "2023-01-01T00:00:00Z", - } - ], - ), - call( - {"sub.py": "hash2"}, - [ - { - "uuid": "file2", - "file_name": "sub.py", - "content": "print('sub')", - "created_at": "2023-01-01T00:00:00Z", - } - ], - ), - ] - ) + def test_project_sync_no_local_path(self, mock_validate_and_get_provider): + # Mock the provider + mock_provider = MagicMock() + mock_validate_and_get_provider.return_value = mock_provider - @patch("claudesync.cli.project.validate_and_get_provider") - def test_project_sync_no_local_path(self, mock_validate_provider, mock_config): - runner = CliRunner() - mock_config.get.side_effect = lambda key, default=None: ( + # Set up the config mock to return None for local_path + self.config_mock.get.side_effect = lambda key, default=None: ( None if key == "local_path" else default ) - mock_validate_provider.return_value = MagicMock() - - result = runner.invoke(sync, obj=mock_config) - - assert result.exit_code == 0 - assert ( - "No local path set. Please select or create a project first." - in result.output - ) - - @patch("claudesync.cli.project.validate_and_get_provider") - def test_project_sync_provider_error(self, mock_validate_provider, mock_config): - runner = CliRunner() - mock_validate_provider.side_effect = ProviderError("API Error") - - result = runner.invoke(sync, obj=mock_config) - assert result.exit_code == 0 - assert "Error: API Error" in result.output - - @patch("claudesync.cli.project.validate_and_get_provider") - @patch("claudesync.cli.project.SyncManager") - def test_project_sync_no_submodules( - self, - mock_sync_manager_class, - mock_validate_provider, - mock_config, - mock_provider, - mock_sync_manager, - mock_get_local_files, - mock_detect_submodules, - ): - runner = CliRunner() - mock_validate_provider.return_value = mock_provider - mock_sync_manager_class.return_value = mock_sync_manager - - mock_provider.get_projects.return_value = [ - {"id": "proj456", "name": "MainProject"} - ] - mock_provider.list_files.return_value = [ - { - "uuid": "file1", - "file_name": "main.py", - "content": "print('main')", - "created_at": "2023-01-01T00:00:00Z", - } - ] - mock_get_local_files.return_value = {"main.py": "hash1"} - mock_detect_submodules.return_value = [] - - result = runner.invoke(sync, obj=mock_config) - - assert result.exit_code == 0 - assert "Main project 'MainProject' synced successfully." in result.output - assert ( - "Project sync completed successfully, including available submodules." - in result.output - ) - assert "Syncing submodule" not in result.output + result = self.runner.invoke(cli, ["project", "sync"], obj=self.config_mock) - mock_sync_manager.sync.assert_called_once() + self.assertIn("No local path set for this project", result.output) + self.assertEqual(result.exit_code, 0) - @patch("claudesync.cli.project.validate_and_get_provider") - @patch("claudesync.cli.project.SyncManager") - def test_project_sync_with_category( - self, - mock_sync_manager_class, - mock_validate_provider, - mock_config, - mock_provider, - mock_sync_manager, - mock_get_local_files, - mock_detect_submodules, - ): - runner = CliRunner() - mock_validate_provider.return_value = mock_provider - mock_sync_manager_class.return_value = mock_sync_manager - - mock_provider.get_projects.return_value = [ - {"id": "proj456", "name": "MainProject"} - ] - mock_provider.list_files.return_value = [ - { - "uuid": "file1", - "file_name": "main.py", - "content": "print('main')", - "created_at": "2023-01-01T00:00:00Z", - } - ] - mock_get_local_files.return_value = {"main.py": "hash1"} - mock_detect_submodules.return_value = [] - - result = runner.invoke(sync, ["--category", "production_code"], obj=mock_config) - - assert result.exit_code == 0 - assert "Main project 'MainProject' synced successfully." in result.output - - mock_get_local_files.assert_called_once_with( - "/path/to/project", "production_code" - ) - mock_sync_manager.sync.assert_called_once() - - @patch("claudesync.cli.project.validate_and_get_provider") - @patch("claudesync.cli.project.SyncManager") - def test_project_sync_with_invalid_category( - self, - mock_sync_manager_class, - mock_validate_provider, - mock_config, - mock_provider, - mock_sync_manager, - mock_get_local_files, - mock_detect_submodules, - ): - runner = CliRunner() - mock_validate_provider.return_value = mock_provider - mock_sync_manager_class.return_value = mock_sync_manager - - mock_get_local_files.side_effect = ValueError( - "Invalid category: invalid_category" - ) - - result = runner.invoke( - sync, ["--category", "invalid_category"], obj=mock_config - ) + # Verify that the provider's methods were not called + mock_provider.list_files.assert_not_called() + mock_provider.get_projects.assert_not_called() - assert result.exit_code == 1 - assert "Invalid category: invalid_category" in result.exception.args[0] - mock_sync_manager.sync.assert_not_called() +if __name__ == "__main__": + unittest.main()