Skip to content

Commit

Permalink
fixup! _make_request error output
Browse files Browse the repository at this point in the history
  • Loading branch information
jahwag committed Aug 2, 2024
1 parent 2eeec68 commit 6983713
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 136 deletions.
12 changes: 10 additions & 2 deletions src/claudesync/providers/claude_ai_curl.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def _prepare_headers(self):
"-H",
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
"-H",
f"Cookie: sessionKey={self.session_key};",
f"Cookie: sessionKey={self.session_key}",
"-H",
"Content-Type: application/json",
]
Expand All @@ -53,13 +53,21 @@ def _process_result(self, result, headers):

http_status_code = result.stdout[-3:]
response_body = result.stdout[:-3].strip()
self.logger.debug(f"Got HTTP {http_status_code}")

if http_status_code.startswith("2"):
if http_status_code == "204":
# HTTP 204 No Content
return None
if not response_body:
self.logger.warn(f"Got HTTP {http_status_code} but empty response")
return None

try:
return json.loads(response_body)
except json.JSONDecodeError as e:
error_message = (
f"Failed to parse JSON response: {e}. Response content: {response_body}. Request "
f"Failed to parse JSON response: {response_body}. Reason: {e}. Response content: {response_body}. Request "
f"headers: {headers}"
)
self.logger.error(error_message)
Expand Down
22 changes: 15 additions & 7 deletions src/claudesync/syncmanager.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import os
import time
import logging
from datetime import datetime, timezone

import click
from tqdm import tqdm

from claudesync.utils import compute_md5_hash

logger = logging.getLogger(__name__)


class SyncManager:
def __init__(self, provider, config):
Expand Down Expand Up @@ -106,7 +108,7 @@ def update_existing_file(
"""
remote_checksum = compute_md5_hash(remote_file["content"])
if local_checksum != remote_checksum:
click.echo(f"Updating {local_file} on remote...")
logger.info(f"Updating {local_file} on remote...")
with tqdm(total=2, desc=f"Updating {local_file}", leave=False) as pbar:
self.provider.delete_file(
self.active_organization_id,
Expand Down Expand Up @@ -139,7 +141,7 @@ def upload_new_file(self, local_file, synced_files):
local_file (str): Name of the local file to be uploaded.
synced_files (set): Set of file names that have been synchronized.
"""
click.echo(f"Uploading new file {local_file} to remote...")
logger.info(f"Uploading new file {local_file} to remote...")
with open(
os.path.join(self.local_path, local_file), "r", encoding="utf-8"
) as file:
Expand Down Expand Up @@ -174,7 +176,9 @@ def update_local_timestamps(self, remote_files, synced_files):
remote_file["created_at"].replace("Z", "+00:00")
).timestamp()
os.utime(local_file_path, (remote_timestamp, remote_timestamp))
click.echo(f"Updated timestamp on local file {local_file_path}")
logger.info(
f"Updated timestamp on local file {local_file_path}"
)
pbar.update(1)

def sync_remote_to_local(self, remote_file, remote_files_to_delete, synced_files):
Expand Down Expand Up @@ -221,7 +225,9 @@ def update_existing_local_file(
remote_file["created_at"].replace("Z", "+00:00")
)
if remote_mtime > local_mtime:
click.echo(f"Updating local file {remote_file['file_name']} from remote...")
logger.info(
f"Updating local file {remote_file['file_name']} from remote..."
)
with tqdm(
total=1, desc=f"Updating {remote_file['file_name']}", leave=False
) as pbar:
Expand All @@ -246,7 +252,9 @@ def create_new_local_file(
remote_files_to_delete (set): Set of remote file names to be considered for deletion.
synced_files (set): Set of file names that have been synchronized.
"""
click.echo(f"Creating new local file {remote_file['file_name']} from remote...")
logger.info(
f"Creating new local file {remote_file['file_name']} from remote..."
)
with tqdm(
total=1, desc=f"Creating {remote_file['file_name']}", leave=False
) as pbar:
Expand All @@ -267,7 +275,7 @@ def delete_remote_files(self, file_to_delete, remote_files):
file_to_delete (str): Name of the remote file to be deleted.
remote_files (list): List of dictionaries representing remote files.
"""
click.echo(f"Deleting {file_to_delete} from remote...")
logger.info(f"Deleting {file_to_delete} from remote...")
remote_file = next(
rf for rf in remote_files if rf["file_name"] == file_to_delete
)
Expand Down
154 changes: 32 additions & 122 deletions tests/providers/test_base_claude_ai.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import unittest
from unittest.mock import patch

from claudesync.exceptions import ProviderError
from unittest.mock import patch, MagicMock
from claudesync.providers.base_claude_ai import BaseClaudeAIProvider


Expand All @@ -10,149 +8,61 @@ class TestBaseClaudeAIProvider(unittest.TestCase):
def setUp(self):
self.provider = BaseClaudeAIProvider("test_session_key")

@patch("click.prompt")
@patch.object(BaseClaudeAIProvider, "get_organizations")
def test_login(self, mock_get_organizations, mock_prompt):
# Test successful login on first attempt
mock_prompt.return_value = "sk-ant-valid_session_key"
mock_get_organizations.return_value = [{"id": "org1", "name": "Org 1"}]
@patch("claudesync.providers.base_claude_ai.click.echo")
@patch("claudesync.providers.base_claude_ai.click.prompt")
def test_login(self, mock_prompt, mock_echo):
mock_prompt.return_value = "sk-ant-test123"
self.provider.get_organizations = MagicMock(
return_value=[{"id": "org1", "name": "Test Org"}]
)

result = self.provider.login()

self.assertEqual(result, "sk-ant-valid_session_key")
self.assertEqual(self.provider.session_key, "sk-ant-valid_session_key")
mock_prompt.assert_called_once()
mock_get_organizations.assert_called_once()

# Reset mocks for next test
mock_prompt.reset_mock()
mock_get_organizations.reset_mock()

# Test invalid session key followed by valid session key
mock_prompt.side_effect = ["invalid_key", "sk-ant-valid_session_key"]
mock_get_organizations.side_effect = [
ProviderError("Invalid session key"),
[{"id": "org1", "name": "Org 1"}],
]
self.assertEqual(result, "sk-ant-test123")
self.assertEqual(self.provider.session_key, "sk-ant-test123")
mock_echo.assert_called()
mock_prompt.assert_called_with("Please enter your sessionKey", type=str)

@patch("claudesync.providers.base_claude_ai.click.echo")
@patch("claudesync.providers.base_claude_ai.click.prompt")
def test_login_invalid_key(self, mock_prompt, mock_echo):
mock_prompt.side_effect = ["invalid_key", "sk-ant-test123"]
self.provider.get_organizations = MagicMock(
return_value=[{"id": "org1", "name": "Test Org"}]
)

result = self.provider.login()

self.assertEqual(result, "sk-ant-valid_session_key")
self.assertEqual(self.provider.session_key, "sk-ant-valid_session_key")
self.assertEqual(result, "sk-ant-test123")
self.assertEqual(mock_prompt.call_count, 2)
self.assertEqual(mock_get_organizations.call_count, 2)

# Reset mocks for next test
mock_prompt.reset_mock()
mock_get_organizations.reset_mock()

# Test when get_organizations returns an empty list
mock_prompt.return_value = "sk-ant-valid_session_key"
mock_get_organizations.return_value = []

result = self.provider.login()

self.assertEqual(result, "sk-ant-valid_session_key")
self.assertEqual(self.provider.session_key, "sk-ant-valid_session_key")
mock_prompt.assert_called_once()
mock_get_organizations.assert_called_once()

@patch.object(BaseClaudeAIProvider, "_make_request")
@patch("claudesync.providers.base_claude_ai.BaseClaudeAIProvider._make_request")
def test_get_organizations(self, mock_make_request):
mock_make_request.return_value = [
{"uuid": "org1", "name": "Org 1", "capabilities": ["chat", "claude_pro"]},
{"uuid": "org2", "name": "Org 2", "capabilities": ["chat"]},
{
"uuid": "org3",
"name": "Org 3",
"capabilities": ["chat", "claude_pro", "other"],
},
{"uuid": "org4", "name": "Org 4", "capabilities": ["other"]},
{"uuid": "org3", "name": "Org 3", "capabilities": ["chat", "claude_pro"]},
]
result = self.provider.get_organizations()
expected = [{"id": "org1", "name": "Org 1"}, {"id": "org3", "name": "Org 3"}]
self.assertEqual(result, expected)

@patch.object(BaseClaudeAIProvider, "_make_request")
def test_get_organizations_no_valid_orgs(self, mock_make_request):
mock_make_request.return_value = [
{"uuid": "org1", "name": "Org 1", "capabilities": ["api"]},
{"uuid": "org2", "name": "Org 2", "capabilities": ["chat"]},
]
result = self.provider.get_organizations()
self.assertEqual(result, [])

@patch.object(BaseClaudeAIProvider, "_make_request")
def test_get_organizations_error(self, mock_make_request):
mock_make_request.return_value = None
with self.assertRaises(ProviderError):
self.provider.get_organizations()
self.assertEqual(len(result), 2)
self.assertEqual(result[0]["id"], "org1")
self.assertEqual(result[1]["id"], "org3")

@patch.object(BaseClaudeAIProvider, "_make_request")
@patch("claudesync.providers.base_claude_ai.BaseClaudeAIProvider._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": "proj3", "name": "Project 3", "archived_at": None},
]
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"},
]
self.assertEqual(result, expected)

@patch.object(BaseClaudeAIProvider, "_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",
},
]
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)

@patch.object(BaseClaudeAIProvider, "_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",
{"file_name": "test.txt", "content": "content"},
)
result = self.provider.get_projects("org1")

@patch.object(BaseClaudeAIProvider, "_make_request")
def test_delete_file(self, mock_make_request):
mock_make_request.return_value = {"status": "deleted"}
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"
)
self.assertEqual(len(result), 2)
self.assertEqual(result[0]["id"], "proj1")
self.assertEqual(result[1]["id"], "proj3")

def test_make_request_not_implemented(self):
with self.assertRaises(NotImplementedError):
Expand Down
36 changes: 31 additions & 5 deletions tests/providers/test_claude_ai_curl.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,50 @@
import subprocess
from claudesync.providers.claude_ai_curl import ClaudeAICurlProvider
from claudesync.exceptions import ProviderError
import json


class TestClaudeAICurlProvider(unittest.TestCase):

def setUp(self):
self.provider = ClaudeAICurlProvider("test_session_key")

@patch("subprocess.run")
def test_make_request_success(self, mock_run):
mock_result = MagicMock()
mock_result.stdout = '{"key": "value"}'
mock_result.returncode = 0
mock_run.return_value = mock_result
# Prepare mock response
mock_response = MagicMock()
mock_response.stdout = json.dumps({"key": "value"}) + "200"
mock_response.returncode = 0
mock_run.return_value = mock_response

# Make the request
result = self.provider._make_request("GET", "/test")

# Assert the result
self.assertEqual(result, {"key": "value"})

# Assert that subprocess.run was called with the correct arguments
mock_run.assert_called_once()
args, kwargs = mock_run.call_args
self.assertIn("curl", args[0])
self.assertIn("https://claude.ai/api/test", args[0])
self.assertIn("--compressed", args[0])
self.assertIn("-s", args[0])
self.assertIn("-S", args[0])
self.assertIn("-w", args[0])
self.assertIn("%{http_code}", args[0])
self.assertIn("-H", args[0])
self.assertIn(
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
args[0],
)
self.assertIn("Cookie: sessionKey=test_session_key", args[0])
self.assertIn("Content-Type: application/json", args[0])

# Assert that the correct kwargs were passed to subprocess.run
self.assertTrue(kwargs.get("capture_output"))
self.assertTrue(kwargs.get("text"))
self.assertTrue(kwargs.get("check"))
self.assertEqual(kwargs.get("encoding"), "utf-8")

@patch("subprocess.run")
def test_make_request_failure(self, mock_run):
Expand Down

0 comments on commit 6983713

Please sign in to comment.