diff --git a/pyproject.toml b/pyproject.toml index 6ad3c6c..594e2e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "claudesync" -version = "0.3.5" +version = "0.3.6" authors = [ {name = "Jahziah Wagner", email = "jahziah.wagner+pypi@gmail.com"}, ] diff --git a/src/claudesync/cli/api.py b/src/claudesync/cli/api.py index e212ccb..f538c7b 100644 --- a/src/claudesync/cli/api.py +++ b/src/claudesync/cli/api.py @@ -41,7 +41,7 @@ def logout(config): click.echo("Logged out successfully.") -@click.command() +@api.command() @click.option("--delay", type=float, required=True, help="Upload delay in seconds") @click.pass_obj @handle_errors @@ -54,7 +54,7 @@ def ratelimit(config, delay): click.echo(f"Upload delay set to {delay} seconds.") -@click.command() +@api.command() @click.option("--size", type=int, required=True, help="Maximum file size in bytes") @click.pass_obj @handle_errors diff --git a/src/claudesync/cli/main.py b/src/claudesync/cli/main.py index 4822f56..0dfa66a 100644 --- a/src/claudesync/cli/main.py +++ b/src/claudesync/cli/main.py @@ -7,6 +7,7 @@ from .organization import organization from .project import project from .sync import ls, sync, schedule +from .api import ratelimit, max_filesize click_completion.init() diff --git a/src/claudesync/config_manager.py b/src/claudesync/config_manager.py index 99faaee..066a409 100644 --- a/src/claudesync/config_manager.py +++ b/src/claudesync/config_manager.py @@ -120,3 +120,16 @@ def get_headers(self): dict: The current headers configuration. """ return self.config.get("headers", {}) + + def update_cookies(self, new_cookies): + """ + Updates the cookies configuration with new values. + + Args: + new_cookies (dict): A dictionary containing the new cookie key-value pairs to update or add. + + This method updates the existing cookies with the new values provided, adds any new cookies, + and then saves the updated configuration to the file. + """ + self.config.setdefault("cookies", {}).update(new_cookies) + self._save_config() diff --git a/src/claudesync/provider_factory.py b/src/claudesync/provider_factory.py index 1335aea..b69beb0 100644 --- a/src/claudesync/provider_factory.py +++ b/src/claudesync/provider_factory.py @@ -1,10 +1,10 @@ -from .providers.claude_ai import ClaudeAIProvider - +# src/claudesync/provider_factory.py -# Import other providers here as they are added +from .providers.base_provider import BaseProvider +from .providers.claude_ai import ClaudeAIProvider -def get_provider(provider_name=None, session_key=None): +def get_provider(provider_name=None, session_key=None) -> BaseProvider: """ Retrieve an instance of a provider class based on the provider name and session key. @@ -13,12 +13,14 @@ def get_provider(provider_name=None, session_key=None): name is specified but not found in the registry, it raises a ValueError. If a session key is provided, it is passed to the provider class constructor. - Args: provider_name (str, optional): The name of the provider to retrieve. If None, returns a list of available - provider names. session_key (str, optional): The session key to be used by the provider for authentication. - Defaults to None. + Args: + provider_name (str, optional): The name of the provider to retrieve. If None, returns a list of available + provider names. + session_key (str, optional): The session key to be used by the provider for authentication. + Defaults to None. Returns: - object: An instance of the requested provider class if both provider_name and session_key are provided. + BaseProvider: An instance of the requested provider class if both provider_name and session_key are provided. list: A list of available provider names if provider_name is None. Raises: diff --git a/src/claudesync/providers/base_provider.py b/src/claudesync/providers/base_provider.py new file mode 100644 index 0000000..a9d3f21 --- /dev/null +++ b/src/claudesync/providers/base_provider.py @@ -0,0 +1,45 @@ +# src/claudesync/providers/base_provider.py + +from abc import ABC, abstractmethod + + +class BaseProvider(ABC): + @abstractmethod + def login(self): + """Authenticate with the provider and return a session key.""" + pass + + @abstractmethod + def get_organizations(self): + """Retrieve a list of organizations the user is a member of.""" + pass + + @abstractmethod + def get_projects(self, organization_id, include_archived=False): + """Retrieve a list of projects for a specified organization.""" + pass + + @abstractmethod + def list_files(self, organization_id, project_id): + """List all files within a specified project and organization.""" + pass + + @abstractmethod + def upload_file(self, organization_id, project_id, file_name, content): + """Upload a file to a specified project within an organization.""" + pass + + @abstractmethod + def delete_file(self, organization_id, project_id, file_uuid): + """Delete a file from a specified project within an organization.""" + pass + + @abstractmethod + def archive_project(self, organization_id, project_id): + """Archive a specified project within an organization.""" + pass + + @abstractmethod + def create_project(self, organization_id, name, description=""): + """Create a new project within a specified organization.""" + pass diff --git a/src/claudesync/providers/claude_ai.py b/src/claudesync/providers/claude_ai.py index d7b5678..8103ec4 100644 --- a/src/claudesync/providers/claude_ai.py +++ b/src/claudesync/providers/claude_ai.py @@ -6,13 +6,14 @@ import click import requests +from .base_provider import BaseProvider from ..config_manager import ConfigManager from ..exceptions import ProviderError logger = logging.getLogger(__name__) -class ClaudeAIProvider: +class ClaudeAIProvider(BaseProvider): """ A provider class for interacting with the Claude AI API. @@ -61,30 +62,36 @@ def _configure_logging(self): ) # Set logger instance to the specified log level def _make_request(self, method, endpoint, **kwargs): - """ - Sends a request to the specified endpoint using the given HTTP method. - - This method constructs a request to the Claude AI API, appending the specified endpoint to the base URL. - It sets up common headers and cookies for the request, including a session key for authentication. - Additional headers can be provided through `kwargs`. The method logs the request and response details - and handles common HTTP errors and JSON parsing errors. - - Args: - method (str): The HTTP method to use for the request (e.g., 'GET', 'POST'). - endpoint (str): The API endpoint to which the request is sent. - **kwargs: Arbitrary keyword arguments. Can include 'headers' to add or override default headers, - and any other request parameters. - - Returns: - dict or None: The parsed JSON response from the API if the response contains content; otherwise, None. - - Raises: - ProviderError: If the request fails, if the response status code indicates an error, - or if the response cannot be parsed as JSON. - """ url = f"{self.BASE_URL}{endpoint}" headers = self.config.get_headers() - cookies = {"sessionKey": self.session_key} + cookies = self.config.get("cookies", {}) + + # Update headers + headers.update( + { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0", + "Origin": "https://claude.ai", + "Referer": "https://claude.ai/projects", + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate, br, zstd", + "Accept-Language": "en-US,en;q=0.5", + "anthropic-client-sha": "unknown", + "anthropic-client-version": "unknown", + "Connection": "keep-alive", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-origin", + } + ) + + # Merge cookies + cookies.update( + { + "sessionKey": self.session_key, + "CH-prefers-color-scheme": "dark", + "anthropic-consent-preferences": '{"analytics":true,"marketing":true}', + } + ) if "headers" in kwargs: headers.update(kwargs.pop("headers")) @@ -106,6 +113,9 @@ def _make_request(self, method, endpoint, **kwargs): f"Response content: {response.text[:1000]}..." ) # Log first 1000 characters of response + # Update cookies with any new values from the response + self.config.update_cookies(response.cookies.get_dict()) + response.raise_for_status() if not response.content: diff --git a/tests/cli/test_api.py b/tests/cli/test_api.py index 5e6f2ba..6689dba 100644 --- a/tests/cli/test_api.py +++ b/tests/cli/test_api.py @@ -1,180 +1,97 @@ import unittest from unittest.mock import patch, MagicMock -from claudesync.providers.claude_ai import ClaudeAIProvider +from click.testing import CliRunner +from claudesync.cli.main import cli +from claudesync.exceptions import ConfigurationError, ProviderError +from claudesync.provider_factory import get_provider -class TestClaudeAIProvider(unittest.TestCase): +class TestAPICLI(unittest.TestCase): def setUp(self): - self.provider = ClaudeAIProvider("test_session_key") - - @patch("claudesync.providers.claude_ai.requests.request") - def test_get_organizations(self, mock_request): - mock_response = MagicMock() - mock_response.json.return_value = [ - { - "uuid": "org1", - "name": "Organization 1", - "settings": {}, - "capabilities": [], - "rate_limit_tier": "", - "billing_type": "", - "created_at": "", - "updated_at": "", - }, - { - "uuid": "org2", - "name": "Organization 2", - "settings": {}, - "capabilities": [], - "rate_limit_tier": "", - "billing_type": "", - "created_at": "", - "updated_at": "", - }, - ] - mock_request.return_value = mock_response - - organizations = self.provider.get_organizations() - - self.assertEqual(len(organizations), 2) - self.assertEqual(organizations[0]["id"], "org1") - self.assertEqual(organizations[0]["name"], "Organization 1") - self.assertEqual(organizations[1]["id"], "org2") - self.assertEqual(organizations[1]["name"], "Organization 2") - - @patch("claudesync.providers.claude_ai.requests.request") - def test_get_projects(self, mock_request): - mock_response = MagicMock() - mock_response.json.return_value = [ - {"uuid": "proj1", "name": "Project 1", "archived_at": None}, - {"uuid": "proj2", "name": "Project 2", "archived_at": "2023-01-01"}, - ] - mock_request.return_value = mock_response - - projects = self.provider.get_projects("org1", include_archived=True) - - self.assertEqual(len(projects), 2) - self.assertEqual(projects[0]["id"], "proj1") - self.assertEqual(projects[0]["name"], "Project 1") - self.assertIsNone(projects[0]["archived_at"]) - self.assertEqual(projects[1]["id"], "proj2") - self.assertEqual(projects[1]["name"], "Project 2") - self.assertEqual(projects[1]["archived_at"], "2023-01-01") - - @patch("claudesync.providers.claude_ai.requests.request") - def test_list_files(self, mock_request): - mock_response = MagicMock() - mock_response.json.return_value = [ - { - "uuid": "file1", - "file_name": "file1.txt", - "content": "content1", - "created_at": "2023-01-01", - }, - { - "uuid": "file2", - "file_name": "file2.py", - "content": "content2", - "created_at": "2023-01-02", - }, - ] - mock_request.return_value = mock_response - - files = self.provider.list_files("org1", "proj1") - - self.assertEqual(len(files), 2) - self.assertEqual(files[0]["uuid"], "file1") - self.assertEqual(files[0]["file_name"], "file1.txt") - self.assertEqual(files[0]["content"], "content1") - self.assertEqual(files[0]["created_at"], "2023-01-01") - self.assertEqual(files[1]["uuid"], "file2") - self.assertEqual(files[1]["file_name"], "file2.py") - self.assertEqual(files[1]["content"], "content2") - self.assertEqual(files[1]["created_at"], "2023-01-02") - - @patch("claudesync.providers.claude_ai.requests.request") - def test_upload_file(self, mock_request): - mock_response = MagicMock() - mock_response.json.return_value = { - "uuid": "new_file", - "file_name": "new_file.txt", - } - mock_request.return_value = mock_response - - result = self.provider.upload_file( - "org1", "proj1", "new_file.txt", "file content" - ) - - self.assertEqual(result["uuid"], "new_file") - self.assertEqual(result["file_name"], "new_file.txt") - mock_request.assert_called_once_with( - "POST", - "https://claude.ai/api/organizations/org1/projects/proj1/docs", - headers=unittest.mock.ANY, - cookies={"sessionKey": "test_session_key"}, - json={"file_name": "new_file.txt", "content": "file content"}, - ) - - @patch("claudesync.providers.claude_ai.requests.request") - def test_delete_file(self, mock_request): - mock_response = MagicMock() - mock_response.json.return_value = None - mock_request.return_value = mock_response - - self.provider.delete_file("org1", "proj1", "file1") - - mock_request.assert_called_once_with( - "DELETE", - "https://claude.ai/api/organizations/org1/projects/proj1/docs/file1", - headers=unittest.mock.ANY, - cookies={"sessionKey": "test_session_key"}, - ) - - @patch("claudesync.providers.claude_ai.requests.request") - def test_create_project(self, mock_request): - mock_response = MagicMock() - mock_response.json.return_value = {"uuid": "new_proj", "name": "New Project"} - mock_request.return_value = mock_response - - result = self.provider.create_project( - "org1", "New Project", "Project Description" - ) - - self.assertEqual(result["uuid"], "new_proj") - self.assertEqual(result["name"], "New Project") - mock_request.assert_called_once_with( - "POST", - "https://claude.ai/api/organizations/org1/projects", - headers=unittest.mock.ANY, - cookies={"sessionKey": "test_session_key"}, - json={ - "name": "New Project", - "description": "Project Description", - "is_private": True, - }, - ) - - @patch("claudesync.providers.claude_ai.requests.request") - def test_archive_project(self, mock_request): - mock_response = MagicMock() - mock_response.json.return_value = { - "uuid": "proj1", - "name": "Project 1", - "is_archived": True, - } - mock_request.return_value = mock_response - - result = self.provider.archive_project("org1", "proj1") - - self.assertTrue(result["is_archived"]) - mock_request.assert_called_once_with( - "PUT", - "https://claude.ai/api/organizations/org1/projects/proj1", - headers=unittest.mock.ANY, - cookies={"sessionKey": "test_session_key"}, - json={"is_archived": True}, - ) - - -if __name__ == "__main__": - unittest.main() + self.runner = CliRunner() + self.mock_config = MagicMock() + self.mock_provider = MagicMock() + + @patch('claudesync.cli.api.get_provider') + @patch('claudesync.cli.main.ConfigManager') + def test_login_success(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.return_value = 'test_session_key' + + 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.assert_any_call('session_key', 'test_session_key') + self.mock_config.set.assert_any_call('active_provider', 'claude.ai') + + @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) + 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']) + + 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) + + @patch('claudesync.cli.main.ConfigManager') + def test_max_filesize_negative_value(self, mock_config_manager): + mock_config_manager.return_value = self.mock_config + + result = self.runner.invoke(cli, ['api', 'max-filesize', '--size', '-1']) + + 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() + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/providers/test_claude_ai.py b/tests/providers/test_claude_ai.py index 08e47f7..ae0ce3f 100644 --- a/tests/providers/test_claude_ai.py +++ b/tests/providers/test_claude_ai.py @@ -1,140 +1,163 @@ import unittest from unittest.mock import patch, MagicMock +import json +import requests from claudesync.providers.claude_ai import ClaudeAIProvider +from claudesync.exceptions import ProviderError +from claudesync.config_manager import ConfigManager class TestClaudeAIProvider(unittest.TestCase): def setUp(self): - self.provider = ClaudeAIProvider() - - @patch("claudesync.providers.claude_ai.click.prompt") - def test_login(self, mock_prompt): - mock_prompt.return_value = "test_session_key" - session_key = self.provider.login() - self.assertEqual(session_key, "test_session_key") + self.provider = ClaudeAIProvider("test_session_key") + # Mock ConfigManager + self.mock_config = MagicMock(spec=ConfigManager) + self.mock_config.get_headers.return_value = {} + self.mock_config.get.return_value = {} + self.provider.config = self.mock_config @patch("claudesync.providers.claude_ai.requests.request") - def test_get_organizations(self, mock_request): + def test_make_request_success(self, mock_request): mock_response = MagicMock() - mock_response.json.return_value = [ - { - "uuid": "org1", - "name": "Organization 1", - "settings": {}, - "capabilities": [], - "rate_limit_tier": "", - "billing_type": "", - "created_at": "", - "updated_at": "", - }, - { - "uuid": "org2", - "name": "Organization 2", - "settings": {}, - "capabilities": [], - "rate_limit_tier": "", - "billing_type": "", - "created_at": "", - "updated_at": "", - }, - ] + mock_response.status_code = 200 + mock_response.json.return_value = {"key": "value"} mock_request.return_value = mock_response - organizations = self.provider.get_organizations() - self.assertEqual(len(organizations), 2) - self.assertEqual(organizations[0]["id"], "org1") - self.assertEqual(organizations[0]["name"], "Organization 1") - self.assertEqual(organizations[1]["id"], "org2") - self.assertEqual(organizations[1]["name"], "Organization 2") + result = self.provider._make_request("GET", "/test") + + self.assertEqual(result, {"key": "value"}) + mock_request.assert_called_once() @patch("claudesync.providers.claude_ai.requests.request") - def test_get_projects(self, mock_request): - mock_response = MagicMock() - mock_response.json.return_value = [ + def test_make_request_failure(self, mock_request): + mock_request.side_effect = requests.RequestException("Test error") + + with self.assertRaises(ProviderError): + self.provider._make_request("GET", "/test") + + @patch("claudesync.providers.claude_ai.click.prompt") + def test_login(self, mock_prompt): + mock_prompt.return_value = "new_session_key" + + result = self.provider.login() + + self.assertEqual(result, "new_session_key") + self.assertEqual(self.provider.session_key, "new_session_key") + + @patch("claudesync.providers.claude_ai.ClaudeAIProvider._make_request") + def test_get_organizations(self, mock_make_request): + mock_make_request.return_value = [ + {"uuid": "org1", "name": "Org 1"}, + {"uuid": "org2", "name": "Org 2"}, + ] + + result = self.provider.get_organizations() + + expected = [{"id": "org1", "name": "Org 1"}, {"id": "org2", "name": "Org 2"}] + self.assertEqual(result, expected) + + @patch("claudesync.providers.claude_ai.ClaudeAIProvider._make_request") + def test_get_projects(self, mock_make_request): + mock_make_request.return_value = [ {"uuid": "proj1", "name": "Project 1", "archived_at": None}, {"uuid": "proj2", "name": "Project 2", "archived_at": "2023-01-01"}, ] - mock_request.return_value = mock_response - projects = self.provider.get_projects("org1", include_archived=True) - self.assertEqual(len(projects), 2) - self.assertEqual(projects[0]["id"], "proj1") - self.assertEqual(projects[0]["name"], "Project 1") - self.assertIsNone(projects[0]["archived_at"]) - self.assertEqual(projects[1]["id"], "proj2") - self.assertEqual(projects[1]["name"], "Project 2") - self.assertEqual(projects[1]["archived_at"], "2023-01-01") + result = self.provider.get_projects("org1", include_archived=True) - @patch("claudesync.providers.claude_ai.requests.request") - def test_list_files(self, mock_request): - mock_response = MagicMock() - mock_response.json.return_value = [ + expected = [ + {"id": "proj1", "name": "Project 1", "archived_at": None}, + {"id": "proj2", "name": "Project 2", "archived_at": "2023-01-01"}, + ] + self.assertEqual(result, expected) + + @patch("claudesync.providers.claude_ai.ClaudeAIProvider._make_request") + def test_list_files(self, mock_make_request): + mock_make_request.return_value = [ { "uuid": "file1", "file_name": "test1.txt", - "content": "Hello", + "content": "content1", "created_at": "2023-01-01", }, { "uuid": "file2", "file_name": "test2.txt", - "content": "World", + "content": "content2", "created_at": "2023-01-02", }, ] - mock_request.return_value = mock_response - files = self.provider.list_files("org1", "proj1") - self.assertEqual(len(files), 2) - self.assertEqual(files[0]["uuid"], "file1") - self.assertEqual(files[0]["file_name"], "test1.txt") - self.assertEqual(files[0]["content"], "Hello") - self.assertEqual(files[0]["created_at"], "2023-01-01") + result = self.provider.list_files("org1", "proj1") - @patch("claudesync.providers.claude_ai.requests.request") - def test_upload_file(self, mock_request): - mock_response = MagicMock() - mock_response.json.return_value = {"uuid": "new_file"} - mock_request.return_value = mock_response + expected = [ + { + "uuid": "file1", + "file_name": "test1.txt", + "content": "content1", + "created_at": "2023-01-01", + }, + { + "uuid": "file2", + "file_name": "test2.txt", + "content": "content2", + "created_at": "2023-01-02", + }, + ] + self.assertEqual(result, expected) - result = self.provider.upload_file( - "org1", "proj1", "new_file.txt", "New content" + @patch("claudesync.providers.claude_ai.ClaudeAIProvider._make_request") + def test_upload_file(self, mock_make_request): + mock_make_request.return_value = {"uuid": "new_file", "file_name": "test.txt"} + + result = self.provider.upload_file("org1", "proj1", "test.txt", "content") + + self.assertEqual(result, {"uuid": "new_file", "file_name": "test.txt"}) + mock_make_request.assert_called_once_with( + "POST", + "/organizations/org1/projects/proj1/docs", + json={"file_name": "test.txt", "content": "content"}, ) - self.assertEqual(result["uuid"], "new_file") - @patch("claudesync.providers.claude_ai.requests.request") - def test_delete_file(self, mock_request): - mock_response = MagicMock() - mock_response.status_code = 204 - mock_request.return_value = mock_response + @patch("claudesync.providers.claude_ai.ClaudeAIProvider._make_request") + def test_delete_file(self, mock_make_request): + mock_make_request.return_value = {"status": "deleted"} - self.provider.delete_file("org1", "proj1", "file1") - mock_request.assert_called_once_with( - "DELETE", - f"{self.provider.BASE_URL}/organizations/org1/projects/proj1/docs/file1", - headers=unittest.mock.ANY, - cookies=unittest.mock.ANY, + result = self.provider.delete_file("org1", "proj1", "file1") + + self.assertEqual(result, {"status": "deleted"}) + mock_make_request.assert_called_once_with( + "DELETE", "/organizations/org1/projects/proj1/docs/file1" ) - @patch("claudesync.providers.claude_ai.requests.request") - def test_archive_project(self, mock_request): - mock_response = MagicMock() - mock_response.json.return_value = {"is_archived": True} - mock_request.return_value = mock_response + @patch("claudesync.providers.claude_ai.ClaudeAIProvider._make_request") + def test_archive_project(self, mock_make_request): + mock_make_request.return_value = {"uuid": "proj1", "is_archived": True} result = self.provider.archive_project("org1", "proj1") - self.assertTrue(result["is_archived"]) - @patch("claudesync.providers.claude_ai.requests.request") - def test_create_project(self, mock_request): - mock_response = MagicMock() - mock_response.json.return_value = {"uuid": "new_proj", "name": "New Project"} - mock_request.return_value = mock_response + self.assertEqual(result, {"uuid": "proj1", "is_archived": True}) + mock_make_request.assert_called_once_with( + "PUT", "/organizations/org1/projects/proj1", json={"is_archived": True} + ) + + @patch("claudesync.providers.claude_ai.ClaudeAIProvider._make_request") + def test_create_project(self, mock_make_request): + mock_make_request.return_value = {"uuid": "new_proj", "name": "New Project"} result = self.provider.create_project("org1", "New Project", "Description") - self.assertEqual(result["uuid"], "new_proj") - self.assertEqual(result["name"], "New Project") + + self.assertEqual(result, {"uuid": "new_proj", "name": "New Project"}) + mock_make_request.assert_called_once_with( + "POST", + "/organizations/org1/projects", + json={ + "name": "New Project", + "description": "Description", + "is_private": True, + }, + ) if __name__ == "__main__":