feat: implement OAuth support for Anthropic using ccproxy and add related configuration options
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
"""Tests for ccproxy_manager module."""
|
||||
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from EvoScientist.ccproxy_manager import (
|
||||
is_ccproxy_available,
|
||||
check_ccproxy_auth,
|
||||
is_ccproxy_running,
|
||||
start_ccproxy,
|
||||
stop_ccproxy,
|
||||
ensure_ccproxy,
|
||||
setup_ccproxy_env,
|
||||
maybe_start_ccproxy,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# is_ccproxy_available
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestIsCcproxyAvailable:
|
||||
@patch("shutil.which", return_value="/usr/local/bin/ccproxy")
|
||||
def test_found(self, mock_which):
|
||||
assert is_ccproxy_available() is True
|
||||
mock_which.assert_called_once_with("ccproxy")
|
||||
|
||||
@patch("shutil.which", return_value=None)
|
||||
def test_not_found(self, mock_which):
|
||||
assert is_ccproxy_available() is False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# check_ccproxy_auth
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestCheckCcproxyAuth:
|
||||
@patch("subprocess.run")
|
||||
def test_valid_auth(self, mock_run):
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=0, stdout="Authenticated as user@example.com", stderr=""
|
||||
)
|
||||
valid, msg = check_ccproxy_auth()
|
||||
assert valid is True
|
||||
assert "Authenticated" in msg
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_invalid_auth(self, mock_run):
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=1, stdout="", stderr="Not authenticated"
|
||||
)
|
||||
valid, msg = check_ccproxy_auth()
|
||||
assert valid is False
|
||||
assert "Not authenticated" in msg
|
||||
|
||||
@patch("subprocess.run", side_effect=FileNotFoundError)
|
||||
def test_missing_binary(self, mock_run):
|
||||
valid, msg = check_ccproxy_auth()
|
||||
assert valid is False
|
||||
assert "not found" in msg
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# is_ccproxy_running
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestIsCcproxyRunning:
|
||||
@patch("httpx.get")
|
||||
def test_running(self, mock_get):
|
||||
mock_get.return_value = MagicMock(status_code=200)
|
||||
assert is_ccproxy_running(8000) is True
|
||||
|
||||
@patch("httpx.get")
|
||||
def test_not_running(self, mock_get):
|
||||
import httpx
|
||||
mock_get.side_effect = httpx.ConnectError("Connection refused")
|
||||
assert is_ccproxy_running(8000) is False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# start_ccproxy
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestStartCcproxy:
|
||||
@patch("EvoScientist.ccproxy_manager.is_ccproxy_running")
|
||||
@patch("subprocess.Popen")
|
||||
def test_success(self, mock_popen, mock_running):
|
||||
proc = MagicMock()
|
||||
proc.poll.return_value = None
|
||||
mock_popen.return_value = proc
|
||||
# First call: not running, second call: running
|
||||
mock_running.side_effect = [True]
|
||||
|
||||
result = start_ccproxy(8000)
|
||||
assert result is proc
|
||||
|
||||
@patch("EvoScientist.ccproxy_manager.is_ccproxy_running", return_value=False)
|
||||
@patch("EvoScientist.ccproxy_manager.time")
|
||||
@patch("subprocess.Popen")
|
||||
def test_timeout(self, mock_popen, mock_time, mock_running):
|
||||
proc = MagicMock()
|
||||
proc.poll.return_value = None
|
||||
mock_popen.return_value = proc
|
||||
# Simulate time passing beyond deadline
|
||||
mock_time.monotonic.side_effect = [0, 0, 11]
|
||||
mock_time.sleep = MagicMock()
|
||||
|
||||
with pytest.raises(RuntimeError, match="did not become healthy"):
|
||||
start_ccproxy(8000)
|
||||
|
||||
@patch("subprocess.Popen", side_effect=FileNotFoundError)
|
||||
def test_missing_binary(self, mock_popen):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
start_ccproxy(8000)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ensure_ccproxy
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestEnsureCcproxy:
|
||||
@patch("EvoScientist.ccproxy_manager.is_ccproxy_running", return_value=True)
|
||||
def test_already_running(self, mock_running):
|
||||
result = ensure_ccproxy(8000)
|
||||
assert result is None
|
||||
|
||||
@patch("EvoScientist.ccproxy_manager.start_ccproxy")
|
||||
@patch("EvoScientist.ccproxy_manager.is_ccproxy_running", return_value=False)
|
||||
def test_needs_start(self, mock_running, mock_start):
|
||||
proc = MagicMock()
|
||||
mock_start.return_value = proc
|
||||
result = ensure_ccproxy(8000)
|
||||
assert result is proc
|
||||
mock_start.assert_called_once_with(8000)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# setup_ccproxy_env
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestSetupCcproxyEnv:
|
||||
def test_sets_vars(self, monkeypatch):
|
||||
monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False)
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
|
||||
setup_ccproxy_env(8000)
|
||||
|
||||
assert os.environ["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:8000/claude"
|
||||
assert os.environ["ANTHROPIC_API_KEY"] == "ccproxy-oauth"
|
||||
|
||||
def test_overrides_existing(self, monkeypatch):
|
||||
"""Force-sets vars even if already configured (oauth takes priority)."""
|
||||
monkeypatch.setenv("ANTHROPIC_BASE_URL", "http://custom:9999")
|
||||
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-real-key")
|
||||
|
||||
setup_ccproxy_env(8000)
|
||||
|
||||
assert os.environ["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:8000/claude"
|
||||
assert os.environ["ANTHROPIC_API_KEY"] == "ccproxy-oauth"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# stop_ccproxy
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestStopCcproxy:
|
||||
def test_none_is_noop(self):
|
||||
stop_ccproxy(None) # Should not raise
|
||||
|
||||
def test_terminates_process(self):
|
||||
proc = MagicMock()
|
||||
stop_ccproxy(proc)
|
||||
proc.terminate.assert_called_once()
|
||||
proc.wait.assert_called_once_with(timeout=5)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# maybe_start_ccproxy
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestMaybeStartCcproxy:
|
||||
def test_api_key_mode_noop(self):
|
||||
config = MagicMock()
|
||||
config.anthropic_auth_mode = "api_key"
|
||||
assert maybe_start_ccproxy(config) is None
|
||||
|
||||
@patch("EvoScientist.ccproxy_manager.setup_ccproxy_env")
|
||||
@patch("EvoScientist.ccproxy_manager.ensure_ccproxy")
|
||||
@patch("EvoScientist.ccproxy_manager.check_ccproxy_auth", return_value=(True, "OK"))
|
||||
@patch("EvoScientist.ccproxy_manager.is_ccproxy_available", return_value=True)
|
||||
def test_oauth_mode_starts(self, mock_avail, mock_auth, mock_ensure, mock_env):
|
||||
proc = MagicMock()
|
||||
mock_ensure.return_value = proc
|
||||
config = MagicMock()
|
||||
config.anthropic_auth_mode = "oauth"
|
||||
|
||||
result = maybe_start_ccproxy(config)
|
||||
assert result is proc
|
||||
mock_env.assert_called_once()
|
||||
|
||||
@patch("EvoScientist.ccproxy_manager.is_ccproxy_available", return_value=False)
|
||||
def test_oauth_mode_raises_no_binary(self, mock_avail):
|
||||
config = MagicMock()
|
||||
config.anthropic_auth_mode = "oauth"
|
||||
with pytest.raises(RuntimeError, match="not found"):
|
||||
maybe_start_ccproxy(config)
|
||||
|
||||
@patch("EvoScientist.ccproxy_manager.check_ccproxy_auth", return_value=(False, "expired"))
|
||||
@patch("EvoScientist.ccproxy_manager.is_ccproxy_available", return_value=True)
|
||||
def test_oauth_mode_raises_no_auth(self, mock_avail, mock_auth):
|
||||
config = MagicMock()
|
||||
config.anthropic_auth_mode = "oauth"
|
||||
with pytest.raises(RuntimeError, match="not authenticated"):
|
||||
maybe_start_ccproxy(config)
|
||||
Reference in New Issue
Block a user