참고소스 수정본
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
"""
|
||||
Integration tests for agent method with mocked requests.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
|
||||
from firecrawl import FirecrawlApp
|
||||
|
||||
|
||||
class Founder(BaseModel):
|
||||
name: str = Field(description="Full name of the founder")
|
||||
role: Optional[str] = Field(None, description="Role or position")
|
||||
background: Optional[str] = Field(None, description="Professional background")
|
||||
|
||||
|
||||
class FoundersSchema(BaseModel):
|
||||
founders: List[Founder] = Field(description="List of founders")
|
||||
|
||||
|
||||
class TestAgent(unittest.TestCase):
|
||||
"""Integration tests for agent method."""
|
||||
|
||||
@patch('firecrawl.v2.utils.http_client.requests.post')
|
||||
@patch('firecrawl.v2.utils.http_client.requests.get')
|
||||
def test_agent_basic(self, mock_get, mock_post):
|
||||
"""Test basic agent call."""
|
||||
# Mock start agent response
|
||||
mock_start_response = MagicMock()
|
||||
mock_start_response.ok = True
|
||||
mock_start_response.status_code = 200
|
||||
mock_start_response.json.return_value = {
|
||||
"success": True,
|
||||
"id": "test-agent-123",
|
||||
"status": "processing"
|
||||
}
|
||||
mock_post.return_value = mock_start_response
|
||||
|
||||
# Mock get status response (completed)
|
||||
mock_status_response = MagicMock()
|
||||
mock_status_response.ok = True
|
||||
mock_status_response.status_code = 200
|
||||
mock_status_response.json.return_value = {
|
||||
"success": True,
|
||||
"id": "test-agent-123",
|
||||
"status": "completed",
|
||||
"data": {
|
||||
"founders": [
|
||||
{"name": "John Doe", "role": "CEO", "background": "Tech entrepreneur"},
|
||||
{"name": "Jane Smith", "role": "CTO", "background": "Software engineer"}
|
||||
]
|
||||
},
|
||||
"creditsUsed": 10,
|
||||
"expiresAt": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
mock_get.return_value = mock_status_response
|
||||
|
||||
app = FirecrawlApp(api_key="test-api-key")
|
||||
result = app.agent(
|
||||
prompt="Find the founders of Firecrawl",
|
||||
schema=FoundersSchema
|
||||
)
|
||||
|
||||
# Verify post was called with correct URL and data
|
||||
mock_post.assert_called_once()
|
||||
post_call_args = mock_post.call_args
|
||||
post_url = post_call_args[1]["url"] if "url" in post_call_args[1] else post_call_args[0][0]
|
||||
assert "/v2/agent" in str(post_url)
|
||||
|
||||
# Check request body
|
||||
request_body = post_call_args[1]["json"]
|
||||
assert request_body["prompt"] == "Find the founders of Firecrawl"
|
||||
assert "schema" in request_body
|
||||
assert request_body["schema"]["type"] == "object"
|
||||
assert "founders" in request_body["schema"]["properties"]
|
||||
|
||||
# Verify get was called to check status
|
||||
mock_get.assert_called()
|
||||
|
||||
# Check result
|
||||
assert result.status == "completed"
|
||||
assert result.data is not None
|
||||
|
||||
@patch('firecrawl.v2.utils.http_client.requests.post')
|
||||
def test_agent_with_urls(self, mock_post):
|
||||
"""Test agent call with URLs."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.ok = True
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"success": True,
|
||||
"status": "completed",
|
||||
"data": {"result": "done"}
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
app = FirecrawlApp(api_key="test-api-key")
|
||||
result = app.agent(
|
||||
urls=["https://example.com", "https://test.com"],
|
||||
prompt="Extract information",
|
||||
schema={"type": "object", "properties": {"info": {"type": "string"}}}
|
||||
)
|
||||
|
||||
# Check request body includes URLs
|
||||
post_call_args = mock_post.call_args
|
||||
request_body = post_call_args[1]["json"]
|
||||
assert request_body["urls"] == ["https://example.com", "https://test.com"]
|
||||
assert request_body["prompt"] == "Extract information"
|
||||
|
||||
@patch('firecrawl.v2.utils.http_client.requests.post')
|
||||
def test_agent_with_dict_schema(self, mock_post):
|
||||
"""Test agent call with dict schema."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.ok = True
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"success": True,
|
||||
"status": "completed",
|
||||
"data": {"result": "done"}
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "integer"}
|
||||
}
|
||||
}
|
||||
|
||||
app = FirecrawlApp(api_key="test-api-key")
|
||||
result = app.agent(
|
||||
prompt="Extract person data",
|
||||
schema=schema
|
||||
)
|
||||
|
||||
# Check request body includes schema
|
||||
post_call_args = mock_post.call_args
|
||||
request_body = post_call_args[1]["json"]
|
||||
assert request_body["schema"] == schema
|
||||
|
||||
@patch('firecrawl.v2.utils.http_client.requests.post')
|
||||
def test_agent_with_all_params(self, mock_post):
|
||||
"""Test agent call with all parameters."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.ok = True
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"success": True,
|
||||
"status": "completed",
|
||||
"data": {"result": "done"}
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
schema = {"type": "object"}
|
||||
urls = ["https://example.com"]
|
||||
|
||||
app = FirecrawlApp(api_key="test-api-key")
|
||||
result = app.agent(
|
||||
urls=urls,
|
||||
prompt="Complete test",
|
||||
schema=schema,
|
||||
integration="test-integration",
|
||||
max_credits=50,
|
||||
strict_constrain_to_urls=True,
|
||||
poll_interval=1,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# Check all parameters are in request body
|
||||
post_call_args = mock_post.call_args
|
||||
request_body = post_call_args[1]["json"]
|
||||
assert request_body["prompt"] == "Complete test"
|
||||
assert request_body["urls"] == urls
|
||||
assert request_body["schema"] == schema
|
||||
assert request_body["integration"] == "test-integration"
|
||||
assert request_body["maxCredits"] == 50
|
||||
assert request_body["strictConstrainToURLs"] is True
|
||||
|
||||
@patch('firecrawl.v2.utils.http_client.requests.post')
|
||||
def test_agent_pydantic_schema_normalization(self, mock_post):
|
||||
"""Test that Pydantic schemas are properly normalized."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.ok = True
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"success": True,
|
||||
"status": "completed",
|
||||
"data": {"result": "done"}
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
app = FirecrawlApp(api_key="test-api-key")
|
||||
result = app.agent(
|
||||
prompt="Find founders",
|
||||
schema=FoundersSchema
|
||||
)
|
||||
|
||||
# Check that schema was normalized to JSON schema format
|
||||
post_call_args = mock_post.call_args
|
||||
request_body = post_call_args[1]["json"]
|
||||
assert "schema" in request_body
|
||||
schema = request_body["schema"]
|
||||
assert schema["type"] == "object"
|
||||
assert "properties" in schema
|
||||
assert "founders" in schema["properties"]
|
||||
assert schema["properties"]["founders"]["type"] == "array"
|
||||
|
||||
@patch('firecrawl.v2.utils.http_client.requests.post')
|
||||
@patch('firecrawl.v2.utils.http_client.requests.get')
|
||||
def test_agent_url_construction(self, mock_get, mock_post):
|
||||
"""Test that agent requests are sent to correct URL."""
|
||||
# Mock start agent response
|
||||
mock_start_response = MagicMock()
|
||||
mock_start_response.ok = True
|
||||
mock_start_response.status_code = 200
|
||||
mock_start_response.json.return_value = {
|
||||
"success": True,
|
||||
"id": "test-agent-123",
|
||||
"status": "processing"
|
||||
}
|
||||
mock_post.return_value = mock_start_response
|
||||
|
||||
# Mock get status response
|
||||
mock_status_response = MagicMock()
|
||||
mock_status_response.ok = True
|
||||
mock_status_response.status_code = 200
|
||||
mock_status_response.json.return_value = {
|
||||
"success": True,
|
||||
"id": "test-agent-123",
|
||||
"status": "completed",
|
||||
"data": {"result": "done"}
|
||||
}
|
||||
mock_get.return_value = mock_status_response
|
||||
|
||||
app = FirecrawlApp(api_key="test-api-key", api_url="https://api.firecrawl.dev")
|
||||
result = app.agent(prompt="Test prompt")
|
||||
|
||||
# Check POST URL - requests.post is called with url as keyword arg
|
||||
post_call_args = mock_post.call_args
|
||||
post_url = post_call_args[1].get("url") if "url" in post_call_args[1] else post_call_args[0][0]
|
||||
assert "/v2/agent" in str(post_url)
|
||||
|
||||
# Check GET URL
|
||||
get_call_args = mock_get.call_args
|
||||
get_url = get_call_args[1].get("url") if "url" in get_call_args[1] else get_call_args[0][0]
|
||||
assert "/v2/agent/test-agent-123" in str(get_url)
|
||||
|
||||
@patch('firecrawl.v2.utils.http_client.requests.post')
|
||||
def test_agent_headers(self, mock_post):
|
||||
"""Test that agent requests include correct headers."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.ok = True
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"success": True,
|
||||
"status": "completed",
|
||||
"data": {"result": "done"}
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
app = FirecrawlApp(api_key="test-api-key")
|
||||
result = app.agent(prompt="Test prompt")
|
||||
|
||||
# Check headers
|
||||
post_call_args = mock_post.call_args
|
||||
headers = post_call_args[1]["headers"]
|
||||
assert "Authorization" in headers
|
||||
assert headers["Authorization"] == "Bearer test-api-key"
|
||||
assert headers["Content-Type"] == "application/json"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from firecrawl.v2.client import FirecrawlClient
|
||||
from firecrawl.v2.client_async import AsyncFirecrawlClient
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_firecrawl_api_key_env(monkeypatch):
|
||||
monkeypatch.delenv("FIRECRAWL_API_KEY", raising=False)
|
||||
yield
|
||||
|
||||
|
||||
def test_cloud_requires_api_key():
|
||||
with pytest.raises(ValueError):
|
||||
FirecrawlClient(api_url="https://api.firecrawl.dev")
|
||||
|
||||
|
||||
def test_self_host_allows_missing_api_key():
|
||||
client = FirecrawlClient(api_url="http://localhost:3000")
|
||||
assert client.http_client.api_key is None
|
||||
|
||||
|
||||
def test_async_cloud_requires_api_key():
|
||||
with pytest.raises(ValueError):
|
||||
AsyncFirecrawlClient(api_url="https://api.firecrawl.dev")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_self_host_allows_missing_api_key():
|
||||
client = AsyncFirecrawlClient(api_url="http://localhost:3000")
|
||||
try:
|
||||
assert client.http_client.api_key is None
|
||||
await client.async_http_client.close()
|
||||
finally:
|
||||
# Ensure the underlying HTTPX client is closed even if assertions fail
|
||||
if not client.async_http_client._client.is_closed:
|
||||
await client.async_http_client.close()
|
||||
@@ -0,0 +1,98 @@
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
import json
|
||||
import os
|
||||
from firecrawl import FirecrawlApp
|
||||
|
||||
class TestChangeTracking(unittest.TestCase):
|
||||
@patch('requests.post')
|
||||
def test_change_tracking_format(self, mock_post):
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
'success': True,
|
||||
'data': {
|
||||
'markdown': 'Test markdown content',
|
||||
'changeTracking': {
|
||||
'previousScrapeAt': '2023-01-01T00:00:00Z',
|
||||
'changeStatus': 'changed',
|
||||
'visibility': 'visible'
|
||||
}
|
||||
}
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
app = FirecrawlApp(api_key=os.environ.get('TEST_API_KEY', 'dummy-api-key-for-testing'))
|
||||
result = app.scrape_url('https://example.com', {
|
||||
'formats': ['markdown', 'changeTracking']
|
||||
})
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
self.assertEqual(kwargs['json']['formats'], ['markdown', 'changeTracking'])
|
||||
|
||||
self.assertEqual(result['changeTracking']['previousScrapeAt'], '2023-01-01T00:00:00Z')
|
||||
self.assertEqual(result['changeTracking']['changeStatus'], 'changed')
|
||||
self.assertEqual(result['changeTracking']['visibility'], 'visible')
|
||||
|
||||
@patch('requests.post')
|
||||
def test_change_tracking_options(self, mock_post):
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
'success': True,
|
||||
'data': {
|
||||
'markdown': 'Test markdown content',
|
||||
'changeTracking': {
|
||||
'previousScrapeAt': '2023-01-01T00:00:00Z',
|
||||
'changeStatus': 'changed',
|
||||
'visibility': 'visible',
|
||||
'diff': {
|
||||
'text': '@@ -1,1 +1,1 @@\n-old content\n+new content',
|
||||
'json': {
|
||||
'files': [{
|
||||
'from': None,
|
||||
'to': None,
|
||||
'chunks': [{
|
||||
'content': '@@ -1,1 +1,1 @@',
|
||||
'changes': [{
|
||||
'type': 'del',
|
||||
'content': '-old content',
|
||||
'del': True,
|
||||
'ln': 1
|
||||
}, {
|
||||
'type': 'add',
|
||||
'content': '+new content',
|
||||
'add': True,
|
||||
'ln': 1
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
}
|
||||
},
|
||||
'json': {
|
||||
'title': {
|
||||
'previous': 'Old Title',
|
||||
'current': 'New Title'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
app = FirecrawlApp(api_key=os.environ.get('TEST_API_KEY', 'dummy-api-key-for-testing'))
|
||||
result = app.scrape_url('https://example.com', {
|
||||
'formats': ['markdown', 'changeTracking'],
|
||||
'changeTrackingOptions': {
|
||||
'modes': ['git-diff', 'json'],
|
||||
'schema': {'type': 'object', 'properties': {'title': {'type': 'string'}}}
|
||||
}
|
||||
})
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
self.assertEqual(kwargs['json']['formats'], ['markdown', 'changeTracking'])
|
||||
self.assertEqual(kwargs['json']['changeTrackingOptions']['modes'], ['git-diff', 'json'])
|
||||
|
||||
self.assertEqual(result['changeTracking']['diff']['text'], '@@ -1,1 +1,1 @@\n-old content\n+new content')
|
||||
self.assertEqual(result['changeTracking']['json']['title']['previous'], 'Old Title')
|
||||
self.assertEqual(result['changeTracking']['json']['title']['current'], 'New Title')
|
||||
@@ -0,0 +1,117 @@
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
import os
|
||||
from firecrawl import FirecrawlApp
|
||||
|
||||
|
||||
class TestTimeoutConversion(unittest.TestCase):
|
||||
|
||||
@patch('requests.post')
|
||||
def test_scrape_url_timeout_conversion(self, mock_post):
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
'success': True,
|
||||
'data': {
|
||||
'markdown': 'Test content'
|
||||
}
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
app = FirecrawlApp(api_key=os.environ.get('TEST_API_KEY', 'dummy-api-key-for-testing'))
|
||||
app.scrape_url('https://example.com', timeout=60000)
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
self.assertEqual(kwargs['timeout'], 65.0)
|
||||
|
||||
@patch('requests.post')
|
||||
def test_scrape_url_default_timeout(self, mock_post):
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
'success': True,
|
||||
'data': {
|
||||
'markdown': 'Test content'
|
||||
}
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
app = FirecrawlApp(api_key=os.environ.get('TEST_API_KEY', 'dummy-api-key-for-testing'))
|
||||
app.scrape_url('https://example.com')
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
self.assertEqual(kwargs['timeout'], 35.0)
|
||||
|
||||
@patch('requests.post')
|
||||
def test_post_request_timeout_conversion(self, mock_post):
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
app = FirecrawlApp(api_key=os.environ.get('TEST_API_KEY', 'dummy-api-key-for-testing'))
|
||||
|
||||
data = {'timeout': 30000}
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
app._post_request('https://example.com/api', data, headers)
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
self.assertEqual(kwargs['timeout'], 35.0)
|
||||
|
||||
@patch('requests.post')
|
||||
def test_post_request_default_timeout(self, mock_post):
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
app = FirecrawlApp(api_key=os.environ.get('TEST_API_KEY', 'dummy-api-key-for-testing'))
|
||||
|
||||
data = {'timeout': 30000, 'url': 'https://example.com'}
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
app._post_request('https://example.com/api', data, headers)
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
self.assertEqual(kwargs['timeout'], 35.0)
|
||||
|
||||
@patch('requests.post')
|
||||
def test_timeout_edge_cases(self, mock_post):
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
'success': True,
|
||||
'data': {
|
||||
'markdown': 'Test content'
|
||||
}
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
app = FirecrawlApp(api_key=os.environ.get('TEST_API_KEY', 'dummy-api-key-for-testing'))
|
||||
|
||||
app.scrape_url('https://example.com', timeout=1000)
|
||||
args, kwargs = mock_post.call_args
|
||||
self.assertEqual(kwargs['timeout'], 6.0)
|
||||
|
||||
app.scrape_url('https://example.com', timeout=0)
|
||||
args, kwargs = mock_post.call_args
|
||||
self.assertEqual(kwargs['timeout'], 5.0)
|
||||
|
||||
@patch('requests.post')
|
||||
def test_post_request_no_timeout_key(self, mock_post):
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
app = FirecrawlApp(api_key=os.environ.get('TEST_API_KEY', 'dummy-api-key-for-testing'))
|
||||
|
||||
data = {'url': 'https://example.com'}
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
app._post_request('https://example.com/api', data, headers)
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
self.assertIsNone(kwargs['timeout'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user