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_firecrawl_auth.py 8.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. from unittest.mock import MagicMock, patch
  2. import pytest
  3. import requests
  4. from services.auth.firecrawl.firecrawl import FirecrawlAuth
  5. class TestFirecrawlAuth:
  6. @pytest.fixture
  7. def valid_credentials(self):
  8. """Fixture for valid bearer credentials"""
  9. return {"auth_type": "bearer", "config": {"api_key": "test_api_key_123"}}
  10. @pytest.fixture
  11. def auth_instance(self, valid_credentials):
  12. """Fixture for FirecrawlAuth instance with valid credentials"""
  13. return FirecrawlAuth(valid_credentials)
  14. def test_should_initialize_with_valid_bearer_credentials(self, valid_credentials):
  15. """Test successful initialization with valid bearer credentials"""
  16. auth = FirecrawlAuth(valid_credentials)
  17. assert auth.api_key == "test_api_key_123"
  18. assert auth.base_url == "https://api.firecrawl.dev"
  19. assert auth.credentials == valid_credentials
  20. def test_should_initialize_with_custom_base_url(self):
  21. """Test initialization with custom base URL"""
  22. credentials = {
  23. "auth_type": "bearer",
  24. "config": {"api_key": "test_api_key_123", "base_url": "https://custom.firecrawl.dev"},
  25. }
  26. auth = FirecrawlAuth(credentials)
  27. assert auth.api_key == "test_api_key_123"
  28. assert auth.base_url == "https://custom.firecrawl.dev"
  29. @pytest.mark.parametrize(
  30. ("auth_type", "expected_error"),
  31. [
  32. ("basic", "Invalid auth type, Firecrawl auth type must be Bearer"),
  33. ("x-api-key", "Invalid auth type, Firecrawl auth type must be Bearer"),
  34. ("", "Invalid auth type, Firecrawl auth type must be Bearer"),
  35. ],
  36. )
  37. def test_should_raise_error_for_invalid_auth_type(self, auth_type, expected_error):
  38. """Test that non-bearer auth types raise ValueError"""
  39. credentials = {"auth_type": auth_type, "config": {"api_key": "test_api_key_123"}}
  40. with pytest.raises(ValueError) as exc_info:
  41. FirecrawlAuth(credentials)
  42. assert str(exc_info.value) == expected_error
  43. @pytest.mark.parametrize(
  44. ("credentials", "expected_error"),
  45. [
  46. ({"auth_type": "bearer", "config": {}}, "No API key provided"),
  47. ({"auth_type": "bearer"}, "No API key provided"),
  48. ({"auth_type": "bearer", "config": {"api_key": ""}}, "No API key provided"),
  49. ({"auth_type": "bearer", "config": {"api_key": None}}, "No API key provided"),
  50. ],
  51. )
  52. def test_should_raise_error_for_missing_api_key(self, credentials, expected_error):
  53. """Test that missing or empty API key raises ValueError"""
  54. with pytest.raises(ValueError) as exc_info:
  55. FirecrawlAuth(credentials)
  56. assert str(exc_info.value) == expected_error
  57. @patch("services.auth.firecrawl.firecrawl.requests.post")
  58. def test_should_validate_valid_credentials_successfully(self, mock_post, auth_instance):
  59. """Test successful credential validation"""
  60. mock_response = MagicMock()
  61. mock_response.status_code = 200
  62. mock_post.return_value = mock_response
  63. result = auth_instance.validate_credentials()
  64. assert result is True
  65. expected_data = {
  66. "url": "https://example.com",
  67. "includePaths": [],
  68. "excludePaths": [],
  69. "limit": 1,
  70. "scrapeOptions": {"onlyMainContent": True},
  71. }
  72. mock_post.assert_called_once_with(
  73. "https://api.firecrawl.dev/v1/crawl",
  74. headers={"Content-Type": "application/json", "Authorization": "Bearer test_api_key_123"},
  75. json=expected_data,
  76. )
  77. @pytest.mark.parametrize(
  78. ("status_code", "error_message"),
  79. [
  80. (402, "Payment required"),
  81. (409, "Conflict error"),
  82. (500, "Internal server error"),
  83. ],
  84. )
  85. @patch("services.auth.firecrawl.firecrawl.requests.post")
  86. def test_should_handle_http_errors(self, mock_post, status_code, error_message, auth_instance):
  87. """Test handling of various HTTP error codes"""
  88. mock_response = MagicMock()
  89. mock_response.status_code = status_code
  90. mock_response.json.return_value = {"error": error_message}
  91. mock_post.return_value = mock_response
  92. with pytest.raises(Exception) as exc_info:
  93. auth_instance.validate_credentials()
  94. assert str(exc_info.value) == f"Failed to authorize. Status code: {status_code}. Error: {error_message}"
  95. @pytest.mark.parametrize(
  96. ("status_code", "response_text", "has_json_error", "expected_error_contains"),
  97. [
  98. (403, '{"error": "Forbidden"}', True, "Failed to authorize. Status code: 403. Error: Forbidden"),
  99. (404, "", True, "Unexpected error occurred while trying to authorize. Status code: 404"),
  100. (401, "Not JSON", True, "Expecting value"), # JSON decode error
  101. ],
  102. )
  103. @patch("services.auth.firecrawl.firecrawl.requests.post")
  104. def test_should_handle_unexpected_errors(
  105. self, mock_post, status_code, response_text, has_json_error, expected_error_contains, auth_instance
  106. ):
  107. """Test handling of unexpected errors with various response formats"""
  108. mock_response = MagicMock()
  109. mock_response.status_code = status_code
  110. mock_response.text = response_text
  111. if has_json_error:
  112. mock_response.json.side_effect = Exception("Not JSON")
  113. mock_post.return_value = mock_response
  114. with pytest.raises(Exception) as exc_info:
  115. auth_instance.validate_credentials()
  116. assert expected_error_contains in str(exc_info.value)
  117. @pytest.mark.parametrize(
  118. ("exception_type", "exception_message"),
  119. [
  120. (requests.ConnectionError, "Network error"),
  121. (requests.Timeout, "Request timeout"),
  122. (requests.ReadTimeout, "Read timeout"),
  123. (requests.ConnectTimeout, "Connection timeout"),
  124. ],
  125. )
  126. @patch("services.auth.firecrawl.firecrawl.requests.post")
  127. def test_should_handle_network_errors(self, mock_post, exception_type, exception_message, auth_instance):
  128. """Test handling of various network-related errors including timeouts"""
  129. mock_post.side_effect = exception_type(exception_message)
  130. with pytest.raises(exception_type) as exc_info:
  131. auth_instance.validate_credentials()
  132. assert exception_message in str(exc_info.value)
  133. def test_should_not_expose_api_key_in_error_messages(self):
  134. """Test that API key is not exposed in error messages"""
  135. credentials = {"auth_type": "bearer", "config": {"api_key": "super_secret_key_12345"}}
  136. auth = FirecrawlAuth(credentials)
  137. # Verify API key is stored but not in any error message
  138. assert auth.api_key == "super_secret_key_12345"
  139. # Test various error scenarios don't expose the key
  140. with pytest.raises(ValueError) as exc_info:
  141. FirecrawlAuth({"auth_type": "basic", "config": {"api_key": "super_secret_key_12345"}})
  142. assert "super_secret_key_12345" not in str(exc_info.value)
  143. @patch("services.auth.firecrawl.firecrawl.requests.post")
  144. def test_should_use_custom_base_url_in_validation(self, mock_post):
  145. """Test that custom base URL is used in validation"""
  146. mock_response = MagicMock()
  147. mock_response.status_code = 200
  148. mock_post.return_value = mock_response
  149. credentials = {
  150. "auth_type": "bearer",
  151. "config": {"api_key": "test_api_key_123", "base_url": "https://custom.firecrawl.dev"},
  152. }
  153. auth = FirecrawlAuth(credentials)
  154. result = auth.validate_credentials()
  155. assert result is True
  156. assert mock_post.call_args[0][0] == "https://custom.firecrawl.dev/v1/crawl"
  157. @patch("services.auth.firecrawl.firecrawl.requests.post")
  158. def test_should_handle_timeout_with_retry_suggestion(self, mock_post, auth_instance):
  159. """Test that timeout errors are handled gracefully with appropriate error message"""
  160. mock_post.side_effect = requests.Timeout("The request timed out after 30 seconds")
  161. with pytest.raises(requests.Timeout) as exc_info:
  162. auth_instance.validate_credentials()
  163. # Verify the timeout exception is raised with original message
  164. assert "timed out" in str(exc_info.value)