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/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/providers/claude_ai.py b/src/claudesync/providers/claude_ai.py index d7b5678..b459f74 100644 --- a/src/claudesync/providers/claude_ai.py +++ b/src/claudesync/providers/claude_ai.py @@ -61,30 +61,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 +112,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/providers/test_claude_ai.py b/tests/providers/test_claude_ai.py index 08e47f7..d7fbb7e 100644 --- a/tests/providers/test_claude_ai.py +++ b/tests/providers/test_claude_ai.py @@ -1,141 +1,144 @@ 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() + 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_make_request_success(self, mock_request): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"key": "value"} + mock_request.return_value = mock_response + + result = self.provider._make_request("GET", "/test") + + self.assertEqual(result, {"key": "value"}) + mock_request.assert_called_once() - @patch("claudesync.providers.claude_ai.click.prompt") + @patch('claudesync.providers.claude_ai.requests.request') + 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 = "test_session_key" - session_key = self.provider.login() - self.assertEqual(session_key, "test_session_key") + mock_prompt.return_value = "new_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": "", - }, + 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"} ] - 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.get_organizations() - @patch("claudesync.providers.claude_ai.requests.request") - def test_get_projects(self, mock_request): - mock_response = MagicMock() - mock_response.json.return_value = [ + 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"}, + {"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": "test1.txt", - "content": "Hello", - "created_at": "2023-01-01", - }, - { - "uuid": "file2", - "file_name": "test2.txt", - "content": "World", - "created_at": "2023-01-02", - }, + result = self.provider.get_projects("org1", include_archived=True) + + expected = [ + {"id": "proj1", "name": "Project 1", "archived_at": None}, + {"id": "proj2", "name": "Project 2", "archived_at": "2023-01-01"} ] - mock_request.return_value = mock_response + self.assertEqual(result, expected) - 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") + @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": "content1", "created_at": "2023-01-01"}, + {"uuid": "file2", "file_name": "test2.txt", "content": "content2", "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"} - mock_request.return_value = mock_response + result = self.provider.list_files("org1", "proj1") + + 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( + result = self.provider.delete_file("org1", "proj1", "file1") + + self.assertEqual(result, {"status": "deleted"}) + mock_make_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, + "/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__": - unittest.main() +if __name__ == '__main__': + unittest.main() \ No newline at end of file