Files
windyboy f7e54692a9 Initial project setup: Obsidian intelligent journal organizer
- Add core agent architecture with Command + Skill pattern
- Implement Claude API integration for content analysis
- Add Obsidian REST API integration for vault operations
- Create conversational interface (v2.0) with natural language processing
- Add comprehensive configuration management and validation
- Include project documentation and developer guides
- Set up testing framework with unit, integration, and property tests
- Add Kiro specs for Claude API configuration and code quality improvements
- Configure project steering files for development guidelines
2025-12-31 17:55:10 +08:00

512 lines
20 KiB
Python

"""
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"])