| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- """
- API Key Authentication System Integration Tests
- """
-
- import json
- from concurrent.futures import ThreadPoolExecutor
- from unittest.mock import Mock, patch
-
- import pytest
- import requests
-
- from services.auth.api_key_auth_factory import ApiKeyAuthFactory
- from services.auth.api_key_auth_service import ApiKeyAuthService
- from services.auth.auth_type import AuthType
-
-
- class TestAuthIntegration:
- def setup_method(self):
- self.tenant_id_1 = "tenant_123"
- self.tenant_id_2 = "tenant_456" # For multi-tenant isolation testing
- self.category = "search"
-
- # Realistic authentication configurations
- self.firecrawl_credentials = {"auth_type": "bearer", "config": {"api_key": "fc_test_key_123"}}
- self.jina_credentials = {"auth_type": "bearer", "config": {"api_key": "jina_test_key_456"}}
- self.watercrawl_credentials = {"auth_type": "x-api-key", "config": {"api_key": "wc_test_key_789"}}
-
- @patch("services.auth.api_key_auth_service.db.session")
- @patch("services.auth.firecrawl.firecrawl.requests.post")
- @patch("services.auth.api_key_auth_service.encrypter.encrypt_token")
- def test_end_to_end_auth_flow(self, mock_encrypt, mock_http, mock_session):
- """Test complete authentication flow: request → validation → encryption → storage"""
- mock_http.return_value = self._create_success_response()
- mock_encrypt.return_value = "encrypted_fc_test_key_123"
- mock_session.add = Mock()
- mock_session.commit = Mock()
-
- args = {"category": self.category, "provider": AuthType.FIRECRAWL, "credentials": self.firecrawl_credentials}
- ApiKeyAuthService.create_provider_auth(self.tenant_id_1, args)
-
- mock_http.assert_called_once()
- call_args = mock_http.call_args
- assert "https://api.firecrawl.dev/v1/crawl" in call_args[0][0]
- assert call_args[1]["headers"]["Authorization"] == "Bearer fc_test_key_123"
-
- mock_encrypt.assert_called_once_with(self.tenant_id_1, "fc_test_key_123")
- mock_session.add.assert_called_once()
- mock_session.commit.assert_called_once()
-
- @patch("services.auth.firecrawl.firecrawl.requests.post")
- def test_cross_component_integration(self, mock_http):
- """Test factory → provider → HTTP call integration"""
- mock_http.return_value = self._create_success_response()
- factory = ApiKeyAuthFactory(AuthType.FIRECRAWL, self.firecrawl_credentials)
- result = factory.validate_credentials()
-
- assert result is True
- mock_http.assert_called_once()
-
- @patch("services.auth.api_key_auth_service.db.session")
- def test_multi_tenant_isolation(self, mock_session):
- """Ensure complete tenant data isolation"""
- tenant1_binding = self._create_mock_binding(self.tenant_id_1, AuthType.FIRECRAWL, self.firecrawl_credentials)
- tenant2_binding = self._create_mock_binding(self.tenant_id_2, AuthType.JINA, self.jina_credentials)
-
- mock_session.scalars.return_value.all.return_value = [tenant1_binding]
- result1 = ApiKeyAuthService.get_provider_auth_list(self.tenant_id_1)
-
- mock_session.scalars.return_value.all.return_value = [tenant2_binding]
- result2 = ApiKeyAuthService.get_provider_auth_list(self.tenant_id_2)
-
- assert len(result1) == 1
- assert result1[0].tenant_id == self.tenant_id_1
- assert len(result2) == 1
- assert result2[0].tenant_id == self.tenant_id_2
-
- @patch("services.auth.api_key_auth_service.db.session")
- def test_cross_tenant_access_prevention(self, mock_session):
- """Test prevention of cross-tenant credential access"""
- mock_session.query.return_value.where.return_value.first.return_value = None
-
- result = ApiKeyAuthService.get_auth_credentials(self.tenant_id_2, self.category, AuthType.FIRECRAWL)
-
- assert result is None
-
- def test_sensitive_data_protection(self):
- """Ensure API keys don't leak to logs"""
- credentials_with_secrets = {
- "auth_type": "bearer",
- "config": {"api_key": "super_secret_key_do_not_log", "secret": "another_secret"},
- }
-
- factory = ApiKeyAuthFactory(AuthType.FIRECRAWL, credentials_with_secrets)
- factory_str = str(factory)
-
- assert "super_secret_key_do_not_log" not in factory_str
- assert "another_secret" not in factory_str
-
- @patch("services.auth.api_key_auth_service.db.session")
- @patch("services.auth.firecrawl.firecrawl.requests.post")
- @patch("services.auth.api_key_auth_service.encrypter.encrypt_token")
- def test_concurrent_creation_safety(self, mock_encrypt, mock_http, mock_session):
- """Test concurrent authentication creation safety"""
- mock_http.return_value = self._create_success_response()
- mock_encrypt.return_value = "encrypted_key"
- mock_session.add = Mock()
- mock_session.commit = Mock()
-
- args = {"category": self.category, "provider": AuthType.FIRECRAWL, "credentials": self.firecrawl_credentials}
-
- results = []
- exceptions = []
-
- def create_auth():
- try:
- ApiKeyAuthService.create_provider_auth(self.tenant_id_1, args)
- results.append("success")
- except Exception as e:
- exceptions.append(e)
-
- with ThreadPoolExecutor(max_workers=5) as executor:
- futures = [executor.submit(create_auth) for _ in range(5)]
- for future in futures:
- future.result()
-
- assert len(results) == 5
- assert len(exceptions) == 0
- assert mock_session.add.call_count == 5
- assert mock_session.commit.call_count == 5
-
- @pytest.mark.parametrize(
- "invalid_input",
- [
- None, # Null input
- {}, # Empty dictionary - missing required fields
- {"auth_type": "bearer"}, # Missing config section
- {"auth_type": "bearer", "config": {}}, # Missing api_key
- ],
- )
- def test_invalid_input_boundary(self, invalid_input):
- """Test boundary handling for invalid inputs"""
- with pytest.raises((ValueError, KeyError, TypeError, AttributeError)):
- ApiKeyAuthFactory(AuthType.FIRECRAWL, invalid_input)
-
- @patch("services.auth.firecrawl.firecrawl.requests.post")
- def test_http_error_handling(self, mock_http):
- """Test proper HTTP error handling"""
- mock_response = Mock()
- mock_response.status_code = 401
- mock_response.text = '{"error": "Unauthorized"}'
- mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("Unauthorized")
- mock_http.return_value = mock_response
-
- # PT012: Split into single statement for pytest.raises
- factory = ApiKeyAuthFactory(AuthType.FIRECRAWL, self.firecrawl_credentials)
- with pytest.raises((requests.exceptions.HTTPError, Exception)):
- factory.validate_credentials()
-
- @patch("services.auth.api_key_auth_service.db.session")
- @patch("services.auth.firecrawl.firecrawl.requests.post")
- def test_network_failure_recovery(self, mock_http, mock_session):
- """Test system recovery from network failures"""
- mock_http.side_effect = requests.exceptions.RequestException("Network timeout")
- mock_session.add = Mock()
- mock_session.commit = Mock()
-
- args = {"category": self.category, "provider": AuthType.FIRECRAWL, "credentials": self.firecrawl_credentials}
-
- with pytest.raises(requests.exceptions.RequestException):
- ApiKeyAuthService.create_provider_auth(self.tenant_id_1, args)
-
- mock_session.commit.assert_not_called()
-
- @pytest.mark.parametrize(
- ("provider", "credentials"),
- [
- (AuthType.FIRECRAWL, {"auth_type": "bearer", "config": {"api_key": "fc_key"}}),
- (AuthType.JINA, {"auth_type": "bearer", "config": {"api_key": "jina_key"}}),
- (AuthType.WATERCRAWL, {"auth_type": "x-api-key", "config": {"api_key": "wc_key"}}),
- ],
- )
- def test_all_providers_factory_creation(self, provider, credentials):
- """Test factory creation for all supported providers"""
- try:
- auth_class = ApiKeyAuthFactory.get_apikey_auth_factory(provider)
- assert auth_class is not None
-
- factory = ApiKeyAuthFactory(provider, credentials)
- assert factory.auth is not None
- except ImportError:
- pytest.skip(f"Provider {provider} not implemented yet")
-
- def _create_success_response(self, status_code=200):
- """Create successful HTTP response mock"""
- mock_response = Mock()
- mock_response.status_code = status_code
- mock_response.json.return_value = {"status": "success"}
- mock_response.raise_for_status.return_value = None
- return mock_response
-
- def _create_mock_binding(self, tenant_id: str, provider: str, credentials: dict) -> Mock:
- """Create realistic database binding mock"""
- mock_binding = Mock()
- mock_binding.id = f"binding_{provider}_{tenant_id}"
- mock_binding.tenant_id = tenant_id
- mock_binding.category = self.category
- mock_binding.provider = provider
- mock_binding.credentials = json.dumps(credentials, ensure_ascii=False)
- mock_binding.disabled = False
-
- mock_binding.created_at = Mock()
- mock_binding.created_at.timestamp.return_value = 1640995200
- mock_binding.updated_at = Mock()
- mock_binding.updated_at.timestamp.return_value = 1640995200
-
- return mock_binding
-
- def test_integration_coverage_validation(self):
- """Validate integration test coverage meets quality standards"""
- core_scenarios = {
- "business_logic": ["end_to_end_auth_flow", "cross_component_integration"],
- "security": ["multi_tenant_isolation", "cross_tenant_access_prevention", "sensitive_data_protection"],
- "reliability": ["concurrent_creation_safety", "network_failure_recovery"],
- "compatibility": ["all_providers_factory_creation"],
- "boundaries": ["invalid_input_boundary", "http_error_handling"],
- }
-
- total_scenarios = sum(len(scenarios) for scenarios in core_scenarios.values())
- assert total_scenarios >= 10
-
- security_tests = core_scenarios["security"]
- assert "multi_tenant_isolation" in security_tests
- assert "sensitive_data_protection" in security_tests
- assert True
|