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