You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_auth_integration.py 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. """
  2. API Key Authentication System Integration Tests
  3. """
  4. import json
  5. from concurrent.futures import ThreadPoolExecutor
  6. from unittest.mock import Mock, patch
  7. import pytest
  8. import requests
  9. from services.auth.api_key_auth_factory import ApiKeyAuthFactory
  10. from services.auth.api_key_auth_service import ApiKeyAuthService
  11. from services.auth.auth_type import AuthType
  12. class TestAuthIntegration:
  13. def setup_method(self):
  14. self.tenant_id_1 = "tenant_123"
  15. self.tenant_id_2 = "tenant_456" # For multi-tenant isolation testing
  16. self.category = "search"
  17. # Realistic authentication configurations
  18. self.firecrawl_credentials = {"auth_type": "bearer", "config": {"api_key": "fc_test_key_123"}}
  19. self.jina_credentials = {"auth_type": "bearer", "config": {"api_key": "jina_test_key_456"}}
  20. self.watercrawl_credentials = {"auth_type": "x-api-key", "config": {"api_key": "wc_test_key_789"}}
  21. @patch("services.auth.api_key_auth_service.db.session")
  22. @patch("services.auth.firecrawl.firecrawl.requests.post")
  23. @patch("services.auth.api_key_auth_service.encrypter.encrypt_token")
  24. def test_end_to_end_auth_flow(self, mock_encrypt, mock_http, mock_session):
  25. """Test complete authentication flow: request → validation → encryption → storage"""
  26. mock_http.return_value = self._create_success_response()
  27. mock_encrypt.return_value = "encrypted_fc_test_key_123"
  28. mock_session.add = Mock()
  29. mock_session.commit = Mock()
  30. args = {"category": self.category, "provider": AuthType.FIRECRAWL, "credentials": self.firecrawl_credentials}
  31. ApiKeyAuthService.create_provider_auth(self.tenant_id_1, args)
  32. mock_http.assert_called_once()
  33. call_args = mock_http.call_args
  34. assert "https://api.firecrawl.dev/v1/crawl" in call_args[0][0]
  35. assert call_args[1]["headers"]["Authorization"] == "Bearer fc_test_key_123"
  36. mock_encrypt.assert_called_once_with(self.tenant_id_1, "fc_test_key_123")
  37. mock_session.add.assert_called_once()
  38. mock_session.commit.assert_called_once()
  39. @patch("services.auth.firecrawl.firecrawl.requests.post")
  40. def test_cross_component_integration(self, mock_http):
  41. """Test factory → provider → HTTP call integration"""
  42. mock_http.return_value = self._create_success_response()
  43. factory = ApiKeyAuthFactory(AuthType.FIRECRAWL, self.firecrawl_credentials)
  44. result = factory.validate_credentials()
  45. assert result is True
  46. mock_http.assert_called_once()
  47. @patch("services.auth.api_key_auth_service.db.session")
  48. def test_multi_tenant_isolation(self, mock_session):
  49. """Ensure complete tenant data isolation"""
  50. tenant1_binding = self._create_mock_binding(self.tenant_id_1, AuthType.FIRECRAWL, self.firecrawl_credentials)
  51. tenant2_binding = self._create_mock_binding(self.tenant_id_2, AuthType.JINA, self.jina_credentials)
  52. mock_session.scalars.return_value.all.return_value = [tenant1_binding]
  53. result1 = ApiKeyAuthService.get_provider_auth_list(self.tenant_id_1)
  54. mock_session.scalars.return_value.all.return_value = [tenant2_binding]
  55. result2 = ApiKeyAuthService.get_provider_auth_list(self.tenant_id_2)
  56. assert len(result1) == 1
  57. assert result1[0].tenant_id == self.tenant_id_1
  58. assert len(result2) == 1
  59. assert result2[0].tenant_id == self.tenant_id_2
  60. @patch("services.auth.api_key_auth_service.db.session")
  61. def test_cross_tenant_access_prevention(self, mock_session):
  62. """Test prevention of cross-tenant credential access"""
  63. mock_session.query.return_value.where.return_value.first.return_value = None
  64. result = ApiKeyAuthService.get_auth_credentials(self.tenant_id_2, self.category, AuthType.FIRECRAWL)
  65. assert result is None
  66. def test_sensitive_data_protection(self):
  67. """Ensure API keys don't leak to logs"""
  68. credentials_with_secrets = {
  69. "auth_type": "bearer",
  70. "config": {"api_key": "super_secret_key_do_not_log", "secret": "another_secret"},
  71. }
  72. factory = ApiKeyAuthFactory(AuthType.FIRECRAWL, credentials_with_secrets)
  73. factory_str = str(factory)
  74. assert "super_secret_key_do_not_log" not in factory_str
  75. assert "another_secret" not in factory_str
  76. @patch("services.auth.api_key_auth_service.db.session")
  77. @patch("services.auth.firecrawl.firecrawl.requests.post")
  78. @patch("services.auth.api_key_auth_service.encrypter.encrypt_token")
  79. def test_concurrent_creation_safety(self, mock_encrypt, mock_http, mock_session):
  80. """Test concurrent authentication creation safety"""
  81. mock_http.return_value = self._create_success_response()
  82. mock_encrypt.return_value = "encrypted_key"
  83. mock_session.add = Mock()
  84. mock_session.commit = Mock()
  85. args = {"category": self.category, "provider": AuthType.FIRECRAWL, "credentials": self.firecrawl_credentials}
  86. results = []
  87. exceptions = []
  88. def create_auth():
  89. try:
  90. ApiKeyAuthService.create_provider_auth(self.tenant_id_1, args)
  91. results.append("success")
  92. except Exception as e:
  93. exceptions.append(e)
  94. with ThreadPoolExecutor(max_workers=5) as executor:
  95. futures = [executor.submit(create_auth) for _ in range(5)]
  96. for future in futures:
  97. future.result()
  98. assert len(results) == 5
  99. assert len(exceptions) == 0
  100. assert mock_session.add.call_count == 5
  101. assert mock_session.commit.call_count == 5
  102. @pytest.mark.parametrize(
  103. "invalid_input",
  104. [
  105. None, # Null input
  106. {}, # Empty dictionary - missing required fields
  107. {"auth_type": "bearer"}, # Missing config section
  108. {"auth_type": "bearer", "config": {}}, # Missing api_key
  109. ],
  110. )
  111. def test_invalid_input_boundary(self, invalid_input):
  112. """Test boundary handling for invalid inputs"""
  113. with pytest.raises((ValueError, KeyError, TypeError, AttributeError)):
  114. ApiKeyAuthFactory(AuthType.FIRECRAWL, invalid_input)
  115. @patch("services.auth.firecrawl.firecrawl.requests.post")
  116. def test_http_error_handling(self, mock_http):
  117. """Test proper HTTP error handling"""
  118. mock_response = Mock()
  119. mock_response.status_code = 401
  120. mock_response.text = '{"error": "Unauthorized"}'
  121. mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("Unauthorized")
  122. mock_http.return_value = mock_response
  123. # PT012: Split into single statement for pytest.raises
  124. factory = ApiKeyAuthFactory(AuthType.FIRECRAWL, self.firecrawl_credentials)
  125. with pytest.raises((requests.exceptions.HTTPError, Exception)):
  126. factory.validate_credentials()
  127. @patch("services.auth.api_key_auth_service.db.session")
  128. @patch("services.auth.firecrawl.firecrawl.requests.post")
  129. def test_network_failure_recovery(self, mock_http, mock_session):
  130. """Test system recovery from network failures"""
  131. mock_http.side_effect = requests.exceptions.RequestException("Network timeout")
  132. mock_session.add = Mock()
  133. mock_session.commit = Mock()
  134. args = {"category": self.category, "provider": AuthType.FIRECRAWL, "credentials": self.firecrawl_credentials}
  135. with pytest.raises(requests.exceptions.RequestException):
  136. ApiKeyAuthService.create_provider_auth(self.tenant_id_1, args)
  137. mock_session.commit.assert_not_called()
  138. @pytest.mark.parametrize(
  139. ("provider", "credentials"),
  140. [
  141. (AuthType.FIRECRAWL, {"auth_type": "bearer", "config": {"api_key": "fc_key"}}),
  142. (AuthType.JINA, {"auth_type": "bearer", "config": {"api_key": "jina_key"}}),
  143. (AuthType.WATERCRAWL, {"auth_type": "x-api-key", "config": {"api_key": "wc_key"}}),
  144. ],
  145. )
  146. def test_all_providers_factory_creation(self, provider, credentials):
  147. """Test factory creation for all supported providers"""
  148. try:
  149. auth_class = ApiKeyAuthFactory.get_apikey_auth_factory(provider)
  150. assert auth_class is not None
  151. factory = ApiKeyAuthFactory(provider, credentials)
  152. assert factory.auth is not None
  153. except ImportError:
  154. pytest.skip(f"Provider {provider} not implemented yet")
  155. def _create_success_response(self, status_code=200):
  156. """Create successful HTTP response mock"""
  157. mock_response = Mock()
  158. mock_response.status_code = status_code
  159. mock_response.json.return_value = {"status": "success"}
  160. mock_response.raise_for_status.return_value = None
  161. return mock_response
  162. def _create_mock_binding(self, tenant_id: str, provider: str, credentials: dict) -> Mock:
  163. """Create realistic database binding mock"""
  164. mock_binding = Mock()
  165. mock_binding.id = f"binding_{provider}_{tenant_id}"
  166. mock_binding.tenant_id = tenant_id
  167. mock_binding.category = self.category
  168. mock_binding.provider = provider
  169. mock_binding.credentials = json.dumps(credentials, ensure_ascii=False)
  170. mock_binding.disabled = False
  171. mock_binding.created_at = Mock()
  172. mock_binding.created_at.timestamp.return_value = 1640995200
  173. mock_binding.updated_at = Mock()
  174. mock_binding.updated_at.timestamp.return_value = 1640995200
  175. return mock_binding
  176. def test_integration_coverage_validation(self):
  177. """Validate integration test coverage meets quality standards"""
  178. core_scenarios = {
  179. "business_logic": ["end_to_end_auth_flow", "cross_component_integration"],
  180. "security": ["multi_tenant_isolation", "cross_tenant_access_prevention", "sensitive_data_protection"],
  181. "reliability": ["concurrent_creation_safety", "network_failure_recovery"],
  182. "compatibility": ["all_providers_factory_creation"],
  183. "boundaries": ["invalid_input_boundary", "http_error_handling"],
  184. }
  185. total_scenarios = sum(len(scenarios) for scenarios in core_scenarios.values())
  186. assert total_scenarios >= 10
  187. security_tests = core_scenarios["security"]
  188. assert "multi_tenant_isolation" in security_tests
  189. assert "sensitive_data_protection" in security_tests
  190. assert True