Files
journal_organizer/tests/integration/test_real_api_endpoints.py
T

512 lines
20 KiB
Python
Raw Normal View History

"""
Integration tests for real Claude API endpoints
Tests validation with default Anthropic API, proxy server configurations, and different model selections
Requirements: 1.3, 2.4
"""
import os
import pytest
import asyncio
import tempfile
import yaml
from pathlib import Path
from typing import Dict, Any, Optional
from unittest.mock import patch, Mock
# Import the modules we're testing
from config_validation import ClaudeAPIConfig
from claude_api_client import ClaudeAPIClient
from error_handling import APIError
class TestRealAPIEndpoints:
"""Test with real API endpoints - requires valid API key"""
def setup_method(self):
"""Set up test fixtures"""
self.temp_dir = Path(tempfile.mkdtemp())
# Check if we have a real API key for testing
self.api_key = os.getenv('ANTHROPIC_API_KEY')
self.has_real_api_key = (
self.api_key and
self.api_key.startswith('sk-ant-') and
len(self.api_key) > 50
)
def teardown_method(self):
"""Clean up test fixtures"""
import shutil
shutil.rmtree(self.temp_dir)
@pytest.mark.skipif(
not os.getenv('ANTHROPIC_API_KEY'),
reason="Requires ANTHROPIC_API_KEY environment variable for real API testing"
)
@pytest.mark.asyncio
async def test_default_anthropic_api_validation(self):
"""Test validation with default Anthropic API"""
if not self.has_real_api_key:
pytest.skip("Requires valid ANTHROPIC_API_KEY for real API testing")
config = ClaudeAPIConfig(
api_key=self.api_key,
api_url="https://api.anthropic.com",
model="claude-3-5-sonnet-20241022"
)
client = ClaudeAPIClient(config)
# Test connection validation
is_valid = await client.validate_connection()
assert is_valid is True
# Test model availability
is_model_valid = await client.validate_model_availability()
assert is_model_valid is True
# Test comprehensive connectivity
results = await client.test_api_connectivity()
assert results['overall_status'] == 'success'
assert results['connection_test']['status'] == 'success'
assert results['model_test']['status'] == 'success'
assert results['authentication_test']['status'] == 'success'
@pytest.mark.skipif(
not os.getenv('ANTHROPIC_API_KEY'),
reason="Requires ANTHROPIC_API_KEY environment variable for real API testing"
)
@pytest.mark.asyncio
async def test_different_model_selections(self):
"""Test different Claude model selections work correctly"""
if not self.has_real_api_key:
pytest.skip("Requires valid ANTHROPIC_API_KEY for real API testing")
# Test different models that should be available
models_to_test = [
"claude-3-5-sonnet-20241022",
"claude-3-haiku-20240307",
# Note: claude-3-opus may not be available in all regions/accounts
]
for model in models_to_test:
config = ClaudeAPIConfig(
api_key=self.api_key,
api_url="https://api.anthropic.com",
model=model
)
client = ClaudeAPIClient(config)
try:
# Test that the model is available
is_valid = await client.validate_model_availability()
assert is_valid is True, f"Model {model} should be available"
# Test a simple API call with the model
response = await client.create_message([
{"role": "user", "content": "Hello"}
])
assert response is not None
assert hasattr(response, 'content')
assert len(response.content) > 0
except APIError as e:
# Some models might not be available in all regions/accounts
if "model" in str(e).lower() and "not found" in str(e).lower():
pytest.skip(f"Model {model} not available in this account/region")
else:
raise
@pytest.mark.asyncio
async def test_invalid_api_key_handling(self):
"""Test handling of invalid API keys"""
config = ClaudeAPIConfig(
api_key="sk-ant-invalid-key-12345678901234567890123456789012345678901234567890",
api_url="https://api.anthropic.com",
model="claude-3-5-sonnet-20241022"
)
client = ClaudeAPIClient(config)
with pytest.raises(APIError, match="Invalid API key or unauthorized access"):
await client.validate_connection()
@pytest.mark.asyncio
async def test_invalid_api_url_handling(self):
"""Test handling of invalid API URLs"""
config = ClaudeAPIConfig(
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
api_url="https://nonexistent-api.example.com",
model="claude-3-5-sonnet-20241022"
)
client = ClaudeAPIClient(config)
with pytest.raises(APIError, match="Connection error"):
await client.validate_connection()
@pytest.mark.asyncio
async def test_invalid_model_handling(self):
"""Test handling of invalid model names"""
if not self.has_real_api_key:
pytest.skip("Requires valid ANTHROPIC_API_KEY for real API testing")
config = ClaudeAPIConfig(
api_key=self.api_key,
api_url="https://api.anthropic.com",
model="claude-nonexistent-model"
)
client = ClaudeAPIClient(config)
with pytest.raises(APIError, match="Model.*not found"):
await client.validate_model_availability()
class TestProxyServerConfigurations:
"""Test proxy server configurations"""
def setup_method(self):
"""Set up test fixtures"""
self.temp_dir = Path(tempfile.mkdtemp())
def teardown_method(self):
"""Clean up test fixtures"""
import shutil
shutil.rmtree(self.temp_dir)
@pytest.mark.asyncio
async def test_localhost_proxy_configuration(self):
"""Test configuration for localhost proxy servers"""
config = ClaudeAPIConfig(
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
api_url="https://localhost:8080",
model="claude-3-5-sonnet-20241022"
)
client = ClaudeAPIClient(config)
# Test that SSL context is configured for localhost
async with client.create_http_session() as session:
# Should not raise SSL errors for localhost
assert session is not None
# Verify SSL context is configured for localhost
connector = session.connector
assert connector.ssl is not None
# For localhost, SSL verification should be disabled
assert not connector.ssl.check_hostname
@pytest.mark.asyncio
async def test_custom_proxy_url_configuration(self):
"""Test configuration for custom proxy URLs"""
proxy_urls = [
"https://proxy.example.com:8080",
"https://claude-proxy.internal:443",
"http://localhost:3128"
]
for proxy_url in proxy_urls:
config = ClaudeAPIConfig(
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
api_url=proxy_url,
model="claude-3-5-sonnet-20241022"
)
client = ClaudeAPIClient(config)
# Test client initialization
assert client.base_url == proxy_url
# Test client info
info = client.get_client_info()
assert info['api_url'] == proxy_url
assert info['is_custom_endpoint'] is True
def test_proxy_configuration_validation(self):
"""Test validation of proxy server configurations"""
# Valid proxy configurations
valid_configs = [
{
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
'api_url': 'https://proxy.example.com:8080',
'model': 'claude-3-5-sonnet-20241022'
},
{
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
'api_url': 'http://localhost:3128',
'model': 'claude-3-5-sonnet-20241022'
}
]
for config_data in valid_configs:
config = ClaudeAPIConfig(**config_data)
assert config.api_url == config_data['api_url']
# Invalid proxy configurations
invalid_configs = [
{
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
'api_url': 'ftp://proxy.example.com:8080', # Invalid protocol
'model': 'claude-3-5-sonnet-20241022'
},
{
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
'api_url': 'not-a-url', # Invalid URL format
'model': 'claude-3-5-sonnet-20241022'
}
]
for config_data in invalid_configs:
with pytest.raises(ValueError):
ClaudeAPIConfig(**config_data)
class TestConfigurationValidationClass:
"""Test configuration validation class methods"""
@pytest.mark.skipif(
not os.getenv('ANTHROPIC_API_KEY'),
reason="Requires ANTHROPIC_API_KEY environment variable for real API testing"
)
@pytest.mark.asyncio
async def test_validate_configuration_quick_test(self):
"""Test quick configuration validation"""
api_key = os.getenv('ANTHROPIC_API_KEY')
if not api_key or not api_key.startswith('sk-ant-'):
pytest.skip("Requires valid ANTHROPIC_API_KEY for real API testing")
config = ClaudeAPIConfig(
api_key=api_key,
api_url="https://api.anthropic.com",
model="claude-3-5-sonnet-20241022"
)
results = await ClaudeAPIClient.validate_configuration(config, quick_test=True)
assert results['config_valid'] is True
assert results['connection_valid'] is True
assert results['model_valid'] is True
assert len(results['errors']) == 0
@pytest.mark.skipif(
not os.getenv('ANTHROPIC_API_KEY'),
reason="Requires ANTHROPIC_API_KEY environment variable for real API testing"
)
@pytest.mark.asyncio
async def test_validate_configuration_comprehensive_test(self):
"""Test comprehensive configuration validation"""
api_key = os.getenv('ANTHROPIC_API_KEY')
if not api_key or not api_key.startswith('sk-ant-'):
pytest.skip("Requires valid ANTHROPIC_API_KEY for real API testing")
config = ClaudeAPIConfig(
api_key=api_key,
api_url="https://api.anthropic.com",
model="claude-3-5-sonnet-20241022"
)
results = await ClaudeAPIClient.validate_configuration(config, quick_test=False)
assert results['config_valid'] is True
assert results['connection_valid'] is True
assert results['model_valid'] is True
assert len(results['errors']) == 0
# Should have detailed test results
assert 'detailed_tests' in results
detailed = results['detailed_tests']
assert detailed['overall_status'] == 'success'
assert detailed['connection_test']['status'] == 'success'
assert detailed['model_test']['status'] == 'success'
assert detailed['authentication_test']['status'] == 'success'
@pytest.mark.asyncio
async def test_validate_configuration_invalid_key(self):
"""Test configuration validation with invalid API key"""
config = ClaudeAPIConfig(
api_key="sk-ant-invalid-key-12345678901234567890123456789012345678901234567890",
api_url="https://api.anthropic.com",
model="claude-3-5-sonnet-20241022"
)
results = await ClaudeAPIClient.validate_configuration(config, quick_test=True)
assert results['config_valid'] is True # Config format is valid
assert results['connection_valid'] is False # But connection fails
assert len(results['errors']) > 0
assert any('Invalid API key' in error for error in results['errors'])
@pytest.mark.asyncio
async def test_validate_configuration_custom_endpoint(self):
"""Test configuration validation with custom endpoint"""
config = ClaudeAPIConfig(
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
api_url="https://custom-claude-api.example.com",
model="claude-3-5-sonnet-20241022"
)
results = await ClaudeAPIClient.validate_configuration(config, quick_test=True)
assert results['config_valid'] is True
assert results['config_info']['is_custom_endpoint'] is True
assert results['config_info']['api_url'] == "https://custom-claude-api.example.com"
# Should have warning about custom endpoint
assert len(results['warnings']) > 0
assert any('custom API endpoint' in warning for warning in results['warnings'])
class TestEnvironmentVariableScenarios:
"""Test environment variable scenarios in real configurations"""
def setup_method(self):
"""Set up test fixtures"""
self.temp_dir = Path(tempfile.mkdtemp())
# Store original environment variables
self.original_env = {}
test_vars = ['TEST_CLAUDE_API_KEY', 'TEST_CLAUDE_API_URL', 'TEST_CLAUDE_MODEL']
for var in test_vars:
if var in os.environ:
self.original_env[var] = os.environ[var]
def teardown_method(self):
"""Clean up test fixtures"""
import shutil
shutil.rmtree(self.temp_dir)
# Clean up test environment variables
test_vars = ['TEST_CLAUDE_API_KEY', 'TEST_CLAUDE_API_URL', 'TEST_CLAUDE_MODEL']
for var in test_vars:
if var in os.environ:
del os.environ[var]
# Restore original environment variables
for var, value in self.original_env.items():
os.environ[var] = value
def create_test_config_file(self, config_data: Dict[str, Any]) -> Path:
"""Create a test configuration file"""
config_file = self.temp_dir / 'test_config.yaml'
with config_file.open('w') as f:
yaml.dump(config_data, f)
return config_file
def test_environment_variable_configuration_loading(self):
"""Test loading configuration with environment variables"""
# Set up environment variables
os.environ['TEST_CLAUDE_API_KEY'] = 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
os.environ['TEST_CLAUDE_API_URL'] = 'https://custom-api.example.com'
os.environ['TEST_CLAUDE_MODEL'] = 'claude-3-haiku-20240307'
config_data = {
'claude': {
'api_key': '${TEST_CLAUDE_API_KEY}',
'api_url': '${TEST_CLAUDE_API_URL}',
'model': '${TEST_CLAUDE_MODEL}'
}
}
config_file = self.create_test_config_file(config_data)
# Load configuration through the configuration loader
from configuration_loader import ConfigurationLoader
loader = ConfigurationLoader()
loaded_config = loader.load_config(config_file)
# Validate the loaded configuration
claude_config = ClaudeAPIConfig(**loaded_config['claude'])
assert claude_config.api_key == 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
assert claude_config.api_url == 'https://custom-api.example.com'
assert claude_config.model == 'claude-3-haiku-20240307'
def test_environment_variable_defaults_in_configuration(self):
"""Test environment variable defaults in configuration"""
config_data = {
'claude': {
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
'api_url': '${CLAUDE_API_URL:-https://api.anthropic.com}',
'model': '${CLAUDE_MODEL:-claude-3-5-sonnet-20241022}',
'max_tokens': '${CLAUDE_MAX_TOKENS:-4096}',
'temperature': '${CLAUDE_TEMPERATURE:-0.7}'
}
}
config_file = self.create_test_config_file(config_data)
# Load configuration through the configuration loader
from configuration_loader import ConfigurationLoader
loader = ConfigurationLoader()
loaded_config = loader.load_config(config_file)
# Should use defaults since environment variables are not set
claude_config = ClaudeAPIConfig(**loaded_config['claude'])
assert claude_config.api_url == 'https://api.anthropic.com'
assert claude_config.model == 'claude-3-5-sonnet-20241022'
assert claude_config.max_tokens == 4096
assert claude_config.temperature == 0.7
def test_mixed_environment_and_direct_configuration(self):
"""Test mixed configuration (some values from files, some from environment)"""
# Set only some environment variables
os.environ['TEST_CLAUDE_API_KEY'] = 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
os.environ['TEST_CLAUDE_API_URL'] = 'https://proxy.example.com'
config_data = {
'claude': {
'api_key': '${TEST_CLAUDE_API_KEY}',
'api_url': '${TEST_CLAUDE_API_URL}',
'model': 'claude-3-5-sonnet-20241022', # Direct value
'max_tokens': 8192, # Direct value
'temperature': '${CLAUDE_TEMPERATURE:-0.5}' # Default value
}
}
config_file = self.create_test_config_file(config_data)
# Load configuration through the configuration loader
from configuration_loader import ConfigurationLoader
loader = ConfigurationLoader()
loaded_config = loader.load_config(config_file)
claude_config = ClaudeAPIConfig(**loaded_config['claude'])
# Environment variables should be expanded
assert claude_config.api_key == 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
assert claude_config.api_url == 'https://proxy.example.com'
# Direct values should be preserved
assert claude_config.model == 'claude-3-5-sonnet-20241022'
assert claude_config.max_tokens == 8192
# Default should be used
assert claude_config.temperature == 0.5
def test_missing_required_environment_variable(self):
"""Test error handling for missing required environment variables"""
config_data = {
'claude': {
'api_key': '${MISSING_API_KEY}', # Required but not set
'api_url': 'https://api.anthropic.com',
'model': 'claude-3-5-sonnet-20241022'
}
}
config_file = self.create_test_config_file(config_data)
# Should raise error for missing required environment variable
from configuration_loader import ConfigurationLoader, EnvironmentVariableError
loader = ConfigurationLoader()
with pytest.raises(EnvironmentVariableError, match="Environment variable 'MISSING_API_KEY' is not set"):
loader.load_config(config_file)
if __name__ == "__main__":
# Run tests with pytest
pytest.main([__file__, "-v"])