test_website_service.py 60 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437
  1. from datetime import datetime
  2. from unittest.mock import MagicMock, patch
  3. import pytest
  4. from faker import Faker
  5. from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
  6. from services.website_service import (
  7. CrawlOptions,
  8. ScrapeRequest,
  9. WebsiteCrawlApiRequest,
  10. WebsiteCrawlStatusApiRequest,
  11. WebsiteService,
  12. )
  13. class TestWebsiteService:
  14. """Integration tests for WebsiteService using testcontainers."""
  15. @pytest.fixture
  16. def mock_external_service_dependencies(self):
  17. """Mock setup for external service dependencies."""
  18. with (
  19. patch("services.website_service.ApiKeyAuthService") as mock_api_key_auth_service,
  20. patch("services.website_service.FirecrawlApp") as mock_firecrawl_app,
  21. patch("services.website_service.WaterCrawlProvider") as mock_watercrawl_provider,
  22. patch("services.website_service.requests") as mock_requests,
  23. patch("services.website_service.redis_client") as mock_redis_client,
  24. patch("services.website_service.storage") as mock_storage,
  25. patch("services.website_service.encrypter") as mock_encrypter,
  26. ):
  27. # Setup default mock returns
  28. mock_api_key_auth_service.get_auth_credentials.return_value = {
  29. "config": {"api_key": "encrypted_api_key", "base_url": "https://api.example.com"}
  30. }
  31. mock_encrypter.decrypt_token.return_value = "decrypted_api_key"
  32. # Mock FirecrawlApp
  33. mock_firecrawl_instance = MagicMock()
  34. mock_firecrawl_instance.crawl_url.return_value = "test_job_id_123"
  35. mock_firecrawl_instance.check_crawl_status.return_value = {
  36. "status": "completed",
  37. "total": 5,
  38. "current": 5,
  39. "data": [{"source_url": "https://example.com", "title": "Test Page"}],
  40. }
  41. mock_firecrawl_app.return_value = mock_firecrawl_instance
  42. # Mock WaterCrawlProvider
  43. mock_watercrawl_instance = MagicMock()
  44. mock_watercrawl_instance.crawl_url.return_value = {"status": "active", "job_id": "watercrawl_job_123"}
  45. mock_watercrawl_instance.get_crawl_status.return_value = {
  46. "status": "completed",
  47. "job_id": "watercrawl_job_123",
  48. "total": 3,
  49. "current": 3,
  50. "data": [],
  51. }
  52. mock_watercrawl_instance.get_crawl_url_data.return_value = {
  53. "title": "WaterCrawl Page",
  54. "source_url": "https://example.com",
  55. "description": "Test description",
  56. "markdown": "# Test Content",
  57. }
  58. mock_watercrawl_instance.scrape_url.return_value = {
  59. "title": "Scraped Page",
  60. "content": "Test content",
  61. "url": "https://example.com",
  62. }
  63. mock_watercrawl_provider.return_value = mock_watercrawl_instance
  64. # Mock requests
  65. mock_response = MagicMock()
  66. mock_response.json.return_value = {"code": 200, "data": {"taskId": "jina_job_123"}}
  67. mock_requests.get.return_value = mock_response
  68. mock_requests.post.return_value = mock_response
  69. # Mock Redis
  70. mock_redis_client.setex.return_value = None
  71. mock_redis_client.get.return_value = str(datetime.now().timestamp())
  72. mock_redis_client.delete.return_value = None
  73. # Mock Storage
  74. mock_storage.exists.return_value = False
  75. mock_storage.load_once.return_value = None
  76. yield {
  77. "api_key_auth_service": mock_api_key_auth_service,
  78. "firecrawl_app": mock_firecrawl_app,
  79. "watercrawl_provider": mock_watercrawl_provider,
  80. "requests": mock_requests,
  81. "redis_client": mock_redis_client,
  82. "storage": mock_storage,
  83. "encrypter": mock_encrypter,
  84. }
  85. def _create_test_account(self, db_session_with_containers, mock_external_service_dependencies):
  86. """
  87. Helper method to create a test account with proper tenant setup.
  88. Args:
  89. db_session_with_containers: Database session from testcontainers infrastructure
  90. mock_external_service_dependencies: Mock dependencies
  91. Returns:
  92. Account: Created account instance
  93. """
  94. fake = Faker()
  95. # Create account
  96. account = Account(
  97. email=fake.email(),
  98. name=fake.name(),
  99. interface_language="en-US",
  100. status="active",
  101. )
  102. from extensions.ext_database import db
  103. db.session.add(account)
  104. db.session.commit()
  105. # Create tenant for the account
  106. tenant = Tenant(
  107. name=fake.company(),
  108. status="normal",
  109. )
  110. db.session.add(tenant)
  111. db.session.commit()
  112. # Create tenant-account join
  113. join = TenantAccountJoin(
  114. tenant_id=tenant.id,
  115. account_id=account.id,
  116. role=TenantAccountRole.OWNER.value,
  117. current=True,
  118. )
  119. db.session.add(join)
  120. db.session.commit()
  121. # Set current tenant for account
  122. account.current_tenant = tenant
  123. return account
  124. def test_document_create_args_validate_success(
  125. self, db_session_with_containers, mock_external_service_dependencies
  126. ):
  127. """
  128. Test successful argument validation for document creation.
  129. This test verifies:
  130. - Valid arguments are accepted without errors
  131. - All required fields are properly validated
  132. - Optional fields are handled correctly
  133. """
  134. # Arrange: Prepare valid arguments
  135. valid_args = {
  136. "provider": "firecrawl",
  137. "url": "https://example.com",
  138. "options": {
  139. "limit": 5,
  140. "crawl_sub_pages": True,
  141. "only_main_content": False,
  142. "includes": "blog,news",
  143. "excludes": "admin,private",
  144. "max_depth": 3,
  145. "use_sitemap": True,
  146. },
  147. }
  148. # Act: Validate arguments
  149. WebsiteService.document_create_args_validate(valid_args)
  150. # Assert: No exception should be raised
  151. # If we reach here, validation passed successfully
  152. def test_document_create_args_validate_missing_provider(
  153. self, db_session_with_containers, mock_external_service_dependencies
  154. ):
  155. """
  156. Test argument validation fails when provider is missing.
  157. This test verifies:
  158. - Missing provider raises ValueError
  159. - Proper error message is provided
  160. - Validation stops at first missing required field
  161. """
  162. # Arrange: Prepare arguments without provider
  163. invalid_args = {"url": "https://example.com", "options": {"limit": 5, "crawl_sub_pages": True}}
  164. # Act & Assert: Verify proper error handling
  165. with pytest.raises(ValueError) as exc_info:
  166. WebsiteService.document_create_args_validate(invalid_args)
  167. assert "Provider is required" in str(exc_info.value)
  168. def test_document_create_args_validate_missing_url(
  169. self, db_session_with_containers, mock_external_service_dependencies
  170. ):
  171. """
  172. Test argument validation fails when URL is missing.
  173. This test verifies:
  174. - Missing URL raises ValueError
  175. - Proper error message is provided
  176. - Validation continues after provider check
  177. """
  178. # Arrange: Prepare arguments without URL
  179. invalid_args = {"provider": "firecrawl", "options": {"limit": 5, "crawl_sub_pages": True}}
  180. # Act & Assert: Verify proper error handling
  181. with pytest.raises(ValueError) as exc_info:
  182. WebsiteService.document_create_args_validate(invalid_args)
  183. assert "URL is required" in str(exc_info.value)
  184. def test_crawl_url_firecrawl_success(self, db_session_with_containers, mock_external_service_dependencies):
  185. """
  186. Test successful URL crawling with Firecrawl provider.
  187. This test verifies:
  188. - Firecrawl provider is properly initialized
  189. - API credentials are retrieved and decrypted
  190. - Crawl parameters are correctly formatted
  191. - Job ID is returned with active status
  192. - Redis cache is properly set
  193. """
  194. # Arrange: Create test account and prepare request
  195. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  196. fake = Faker()
  197. # Mock current_user for the test
  198. with patch("services.website_service.current_user") as mock_current_user:
  199. mock_current_user.current_tenant_id = account.current_tenant.id
  200. # Create API request
  201. api_request = WebsiteCrawlApiRequest(
  202. provider="firecrawl",
  203. url="https://example.com",
  204. options={
  205. "limit": 10,
  206. "crawl_sub_pages": True,
  207. "only_main_content": True,
  208. "includes": "blog,news",
  209. "excludes": "admin,private",
  210. "max_depth": 2,
  211. "use_sitemap": True,
  212. },
  213. )
  214. # Act: Execute crawl operation
  215. result = WebsiteService.crawl_url(api_request)
  216. # Assert: Verify successful operation
  217. assert result is not None
  218. assert result["status"] == "active"
  219. assert result["job_id"] == "test_job_id_123"
  220. # Verify external service interactions
  221. mock_external_service_dependencies["api_key_auth_service"].get_auth_credentials.assert_called_once_with(
  222. account.current_tenant.id, "website", "firecrawl"
  223. )
  224. mock_external_service_dependencies["encrypter"].decrypt_token.assert_called_once_with(
  225. tenant_id=account.current_tenant.id, token="encrypted_api_key"
  226. )
  227. mock_external_service_dependencies["firecrawl_app"].assert_called_once_with(
  228. api_key="decrypted_api_key", base_url="https://api.example.com"
  229. )
  230. # Verify Redis cache was set
  231. mock_external_service_dependencies["redis_client"].setex.assert_called_once()
  232. def test_crawl_url_watercrawl_success(self, db_session_with_containers, mock_external_service_dependencies):
  233. """
  234. Test successful URL crawling with WaterCrawl provider.
  235. This test verifies:
  236. - WaterCrawl provider is properly initialized
  237. - API credentials are retrieved and decrypted
  238. - Crawl options are correctly passed to provider
  239. - Provider returns expected response format
  240. """
  241. # Arrange: Create test account and prepare request
  242. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  243. # Mock current_user for the test
  244. with patch("services.website_service.current_user") as mock_current_user:
  245. mock_current_user.current_tenant_id = account.current_tenant.id
  246. # Create API request
  247. api_request = WebsiteCrawlApiRequest(
  248. provider="watercrawl",
  249. url="https://example.com",
  250. options={
  251. "limit": 5,
  252. "crawl_sub_pages": False,
  253. "only_main_content": False,
  254. "includes": None,
  255. "excludes": None,
  256. "max_depth": None,
  257. "use_sitemap": False,
  258. },
  259. )
  260. # Act: Execute crawl operation
  261. result = WebsiteService.crawl_url(api_request)
  262. # Assert: Verify successful operation
  263. assert result is not None
  264. assert result["status"] == "active"
  265. assert result["job_id"] == "watercrawl_job_123"
  266. # Verify external service interactions
  267. mock_external_service_dependencies["api_key_auth_service"].get_auth_credentials.assert_called_once_with(
  268. account.current_tenant.id, "website", "watercrawl"
  269. )
  270. mock_external_service_dependencies["encrypter"].decrypt_token.assert_called_once_with(
  271. tenant_id=account.current_tenant.id, token="encrypted_api_key"
  272. )
  273. mock_external_service_dependencies["watercrawl_provider"].assert_called_once_with(
  274. api_key="decrypted_api_key", base_url="https://api.example.com"
  275. )
  276. def test_crawl_url_jinareader_success(self, db_session_with_containers, mock_external_service_dependencies):
  277. """
  278. Test successful URL crawling with JinaReader provider.
  279. This test verifies:
  280. - JinaReader provider handles single page crawling
  281. - API credentials are retrieved and decrypted
  282. - HTTP requests are made with proper headers
  283. - Response is properly parsed and returned
  284. """
  285. # Arrange: Create test account and prepare request
  286. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  287. # Mock current_user for the test
  288. with patch("services.website_service.current_user") as mock_current_user:
  289. mock_current_user.current_tenant_id = account.current_tenant.id
  290. # Create API request for single page crawling
  291. api_request = WebsiteCrawlApiRequest(
  292. provider="jinareader",
  293. url="https://example.com",
  294. options={
  295. "limit": 1,
  296. "crawl_sub_pages": False,
  297. "only_main_content": True,
  298. "includes": None,
  299. "excludes": None,
  300. "max_depth": None,
  301. "use_sitemap": False,
  302. },
  303. )
  304. # Act: Execute crawl operation
  305. result = WebsiteService.crawl_url(api_request)
  306. # Assert: Verify successful operation
  307. assert result is not None
  308. assert result["status"] == "active"
  309. assert result["data"] is not None
  310. # Verify external service interactions
  311. mock_external_service_dependencies["api_key_auth_service"].get_auth_credentials.assert_called_once_with(
  312. account.current_tenant.id, "website", "jinareader"
  313. )
  314. mock_external_service_dependencies["encrypter"].decrypt_token.assert_called_once_with(
  315. tenant_id=account.current_tenant.id, token="encrypted_api_key"
  316. )
  317. # Verify HTTP request was made
  318. mock_external_service_dependencies["requests"].get.assert_called_once_with(
  319. "https://r.jina.ai/https://example.com",
  320. headers={"Accept": "application/json", "Authorization": "Bearer decrypted_api_key"},
  321. )
  322. def test_crawl_url_invalid_provider(self, db_session_with_containers, mock_external_service_dependencies):
  323. """
  324. Test crawl operation fails with invalid provider.
  325. This test verifies:
  326. - Invalid provider raises ValueError
  327. - Proper error message is provided
  328. - Service handles unsupported providers gracefully
  329. """
  330. # Arrange: Create test account and prepare request with invalid provider
  331. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  332. # Mock current_user for the test
  333. with patch("services.website_service.current_user") as mock_current_user:
  334. mock_current_user.current_tenant_id = account.current_tenant.id
  335. # Create API request with invalid provider
  336. api_request = WebsiteCrawlApiRequest(
  337. provider="invalid_provider",
  338. url="https://example.com",
  339. options={"limit": 5, "crawl_sub_pages": False, "only_main_content": False},
  340. )
  341. # Act & Assert: Verify proper error handling
  342. with pytest.raises(ValueError) as exc_info:
  343. WebsiteService.crawl_url(api_request)
  344. assert "Invalid provider" in str(exc_info.value)
  345. def test_get_crawl_status_firecrawl_success(self, db_session_with_containers, mock_external_service_dependencies):
  346. """
  347. Test successful crawl status retrieval with Firecrawl provider.
  348. This test verifies:
  349. - Firecrawl status is properly retrieved
  350. - API credentials are retrieved and decrypted
  351. - Status data includes all required fields
  352. - Redis cache is properly managed for completed jobs
  353. """
  354. # Arrange: Create test account and prepare request
  355. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  356. # Mock current_user for the test
  357. with patch("services.website_service.current_user") as mock_current_user:
  358. mock_current_user.current_tenant_id = account.current_tenant.id
  359. # Create API request
  360. api_request = WebsiteCrawlStatusApiRequest(provider="firecrawl", job_id="test_job_id_123")
  361. # Act: Get crawl status
  362. result = WebsiteService.get_crawl_status_typed(api_request)
  363. # Assert: Verify successful operation
  364. assert result is not None
  365. assert result["status"] == "completed"
  366. assert result["job_id"] == "test_job_id_123"
  367. assert result["total"] == 5
  368. assert result["current"] == 5
  369. assert "data" in result
  370. assert "time_consuming" in result
  371. # Verify external service interactions
  372. mock_external_service_dependencies["api_key_auth_service"].get_auth_credentials.assert_called_once_with(
  373. account.current_tenant.id, "website", "firecrawl"
  374. )
  375. mock_external_service_dependencies["encrypter"].decrypt_token.assert_called_once_with(
  376. tenant_id=account.current_tenant.id, token="encrypted_api_key"
  377. )
  378. # Verify Redis cache was accessed and cleaned up
  379. mock_external_service_dependencies["redis_client"].get.assert_called_once()
  380. mock_external_service_dependencies["redis_client"].delete.assert_called_once()
  381. def test_get_crawl_status_watercrawl_success(self, db_session_with_containers, mock_external_service_dependencies):
  382. """
  383. Test successful crawl status retrieval with WaterCrawl provider.
  384. This test verifies:
  385. - WaterCrawl status is properly retrieved
  386. - API credentials are retrieved and decrypted
  387. - Provider returns expected status format
  388. - All required status fields are present
  389. """
  390. # Arrange: Create test account and prepare request
  391. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  392. # Mock current_user for the test
  393. with patch("services.website_service.current_user") as mock_current_user:
  394. mock_current_user.current_tenant_id = account.current_tenant.id
  395. # Create API request
  396. api_request = WebsiteCrawlStatusApiRequest(provider="watercrawl", job_id="watercrawl_job_123")
  397. # Act: Get crawl status
  398. result = WebsiteService.get_crawl_status_typed(api_request)
  399. # Assert: Verify successful operation
  400. assert result is not None
  401. assert result["status"] == "completed"
  402. assert result["job_id"] == "watercrawl_job_123"
  403. assert result["total"] == 3
  404. assert result["current"] == 3
  405. assert "data" in result
  406. # Verify external service interactions
  407. mock_external_service_dependencies["api_key_auth_service"].get_auth_credentials.assert_called_once_with(
  408. account.current_tenant.id, "website", "watercrawl"
  409. )
  410. mock_external_service_dependencies["encrypter"].decrypt_token.assert_called_once_with(
  411. tenant_id=account.current_tenant.id, token="encrypted_api_key"
  412. )
  413. def test_get_crawl_status_jinareader_success(self, db_session_with_containers, mock_external_service_dependencies):
  414. """
  415. Test successful crawl status retrieval with JinaReader provider.
  416. This test verifies:
  417. - JinaReader status is properly retrieved
  418. - API credentials are retrieved and decrypted
  419. - HTTP requests are made with proper parameters
  420. - Status data is properly formatted and returned
  421. """
  422. # Arrange: Create test account and prepare request
  423. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  424. # Mock current_user for the test
  425. with patch("services.website_service.current_user") as mock_current_user:
  426. mock_current_user.current_tenant_id = account.current_tenant.id
  427. # Create API request
  428. api_request = WebsiteCrawlStatusApiRequest(provider="jinareader", job_id="jina_job_123")
  429. # Act: Get crawl status
  430. result = WebsiteService.get_crawl_status_typed(api_request)
  431. # Assert: Verify successful operation
  432. assert result is not None
  433. assert result["status"] == "active"
  434. assert result["job_id"] == "jina_job_123"
  435. assert "total" in result
  436. assert "current" in result
  437. assert "data" in result
  438. assert "time_consuming" in result
  439. # Verify external service interactions
  440. mock_external_service_dependencies["api_key_auth_service"].get_auth_credentials.assert_called_once_with(
  441. account.current_tenant.id, "website", "jinareader"
  442. )
  443. mock_external_service_dependencies["encrypter"].decrypt_token.assert_called_once_with(
  444. tenant_id=account.current_tenant.id, token="encrypted_api_key"
  445. )
  446. # Verify HTTP request was made
  447. mock_external_service_dependencies["requests"].post.assert_called_once()
  448. def test_get_crawl_status_invalid_provider(self, db_session_with_containers, mock_external_service_dependencies):
  449. """
  450. Test crawl status retrieval fails with invalid provider.
  451. This test verifies:
  452. - Invalid provider raises ValueError
  453. - Proper error message is provided
  454. - Service handles unsupported providers gracefully
  455. """
  456. # Arrange: Create test account and prepare request with invalid provider
  457. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  458. # Mock current_user for the test
  459. with patch("services.website_service.current_user") as mock_current_user:
  460. mock_current_user.current_tenant_id = account.current_tenant.id
  461. # Create API request with invalid provider
  462. api_request = WebsiteCrawlStatusApiRequest(provider="invalid_provider", job_id="test_job_id_123")
  463. # Act & Assert: Verify proper error handling
  464. with pytest.raises(ValueError) as exc_info:
  465. WebsiteService.get_crawl_status_typed(api_request)
  466. assert "Invalid provider" in str(exc_info.value)
  467. def test_get_crawl_status_missing_credentials(self, db_session_with_containers, mock_external_service_dependencies):
  468. """
  469. Test crawl status retrieval fails when credentials are missing.
  470. This test verifies:
  471. - Missing credentials raises ValueError
  472. - Proper error message is provided
  473. - Service handles authentication failures gracefully
  474. """
  475. # Arrange: Create test account and prepare request
  476. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  477. # Mock current_user for the test
  478. with patch("services.website_service.current_user") as mock_current_user:
  479. mock_current_user.current_tenant_id = account.current_tenant.id
  480. # Mock missing credentials
  481. mock_external_service_dependencies["api_key_auth_service"].get_auth_credentials.return_value = None
  482. # Create API request
  483. api_request = WebsiteCrawlStatusApiRequest(provider="firecrawl", job_id="test_job_id_123")
  484. # Act & Assert: Verify proper error handling
  485. with pytest.raises(ValueError) as exc_info:
  486. WebsiteService.get_crawl_status_typed(api_request)
  487. assert "No valid credentials found for the provider" in str(exc_info.value)
  488. def test_get_crawl_status_missing_api_key(self, db_session_with_containers, mock_external_service_dependencies):
  489. """
  490. Test crawl status retrieval fails when API key is missing from config.
  491. This test verifies:
  492. - Missing API key raises ValueError
  493. - Proper error message is provided
  494. - Service handles configuration failures gracefully
  495. """
  496. # Arrange: Create test account and prepare request
  497. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  498. # Mock current_user for the test
  499. with patch("services.website_service.current_user") as mock_current_user:
  500. mock_current_user.current_tenant_id = account.current_tenant.id
  501. # Mock missing API key in config
  502. mock_external_service_dependencies["api_key_auth_service"].get_auth_credentials.return_value = {
  503. "config": {"base_url": "https://api.example.com"}
  504. }
  505. # Create API request
  506. api_request = WebsiteCrawlStatusApiRequest(provider="firecrawl", job_id="test_job_id_123")
  507. # Act & Assert: Verify proper error handling
  508. with pytest.raises(ValueError) as exc_info:
  509. WebsiteService.get_crawl_status_typed(api_request)
  510. assert "API key not found in configuration" in str(exc_info.value)
  511. def test_get_crawl_url_data_firecrawl_success(self, db_session_with_containers, mock_external_service_dependencies):
  512. """
  513. Test successful URL data retrieval with Firecrawl provider.
  514. This test verifies:
  515. - Firecrawl URL data is properly retrieved
  516. - API credentials are retrieved and decrypted
  517. - Data is returned for matching URL
  518. - Storage fallback works when needed
  519. """
  520. # Arrange: Create test account and prepare request
  521. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  522. # Mock storage to return existing data
  523. mock_external_service_dependencies["storage"].exists.return_value = True
  524. mock_external_service_dependencies["storage"].load_once.return_value = (
  525. b"["
  526. b'{"source_url": "https://example.com", "title": "Test Page", '
  527. b'"description": "Test Description", "markdown": "# Test Content"}'
  528. b"]"
  529. )
  530. # Act: Get URL data
  531. result = WebsiteService.get_crawl_url_data(
  532. job_id="test_job_id_123",
  533. provider="firecrawl",
  534. url="https://example.com",
  535. tenant_id=account.current_tenant.id,
  536. )
  537. # Assert: Verify successful operation
  538. assert result is not None
  539. assert result["source_url"] == "https://example.com"
  540. assert result["title"] == "Test Page"
  541. assert result["description"] == "Test Description"
  542. assert result["markdown"] == "# Test Content"
  543. # Verify external service interactions
  544. mock_external_service_dependencies["api_key_auth_service"].get_auth_credentials.assert_called_once_with(
  545. account.current_tenant.id, "website", "firecrawl"
  546. )
  547. mock_external_service_dependencies["encrypter"].decrypt_token.assert_called_once_with(
  548. tenant_id=account.current_tenant.id, token="encrypted_api_key"
  549. )
  550. # Verify storage was accessed
  551. mock_external_service_dependencies["storage"].exists.assert_called_once()
  552. mock_external_service_dependencies["storage"].load_once.assert_called_once()
  553. def test_get_crawl_url_data_watercrawl_success(
  554. self, db_session_with_containers, mock_external_service_dependencies
  555. ):
  556. """
  557. Test successful URL data retrieval with WaterCrawl provider.
  558. This test verifies:
  559. - WaterCrawl URL data is properly retrieved
  560. - API credentials are retrieved and decrypted
  561. - Provider returns expected data format
  562. - All required data fields are present
  563. """
  564. # Arrange: Create test account and prepare request
  565. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  566. # Act: Get URL data
  567. result = WebsiteService.get_crawl_url_data(
  568. job_id="watercrawl_job_123",
  569. provider="watercrawl",
  570. url="https://example.com",
  571. tenant_id=account.current_tenant.id,
  572. )
  573. # Assert: Verify successful operation
  574. assert result is not None
  575. assert result["title"] == "WaterCrawl Page"
  576. assert result["source_url"] == "https://example.com"
  577. assert result["description"] == "Test description"
  578. assert result["markdown"] == "# Test Content"
  579. # Verify external service interactions
  580. mock_external_service_dependencies["api_key_auth_service"].get_auth_credentials.assert_called_once_with(
  581. account.current_tenant.id, "website", "watercrawl"
  582. )
  583. mock_external_service_dependencies["encrypter"].decrypt_token.assert_called_once_with(
  584. tenant_id=account.current_tenant.id, token="encrypted_api_key"
  585. )
  586. def test_get_crawl_url_data_jinareader_success(
  587. self, db_session_with_containers, mock_external_service_dependencies
  588. ):
  589. """
  590. Test successful URL data retrieval with JinaReader provider.
  591. This test verifies:
  592. - JinaReader URL data is properly retrieved
  593. - API credentials are retrieved and decrypted
  594. - HTTP requests are made with proper parameters
  595. - Data is properly formatted and returned
  596. """
  597. # Arrange: Create test account and prepare request
  598. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  599. # Mock successful response for JinaReader
  600. mock_response = MagicMock()
  601. mock_response.json.return_value = {
  602. "code": 200,
  603. "data": {
  604. "title": "JinaReader Page",
  605. "url": "https://example.com",
  606. "description": "Test description",
  607. "content": "# Test Content",
  608. },
  609. }
  610. mock_external_service_dependencies["requests"].get.return_value = mock_response
  611. # Act: Get URL data without job_id (single page scraping)
  612. result = WebsiteService.get_crawl_url_data(
  613. job_id="", provider="jinareader", url="https://example.com", tenant_id=account.current_tenant.id
  614. )
  615. # Assert: Verify successful operation
  616. assert result is not None
  617. assert result["title"] == "JinaReader Page"
  618. assert result["url"] == "https://example.com"
  619. assert result["description"] == "Test description"
  620. assert result["content"] == "# Test Content"
  621. # Verify external service interactions
  622. mock_external_service_dependencies["api_key_auth_service"].get_auth_credentials.assert_called_once_with(
  623. account.current_tenant.id, "website", "jinareader"
  624. )
  625. mock_external_service_dependencies["encrypter"].decrypt_token.assert_called_once_with(
  626. tenant_id=account.current_tenant.id, token="encrypted_api_key"
  627. )
  628. # Verify HTTP request was made
  629. mock_external_service_dependencies["requests"].get.assert_called_once_with(
  630. "https://r.jina.ai/https://example.com",
  631. headers={"Accept": "application/json", "Authorization": "Bearer decrypted_api_key"},
  632. )
  633. def test_get_scrape_url_data_firecrawl_success(
  634. self, db_session_with_containers, mock_external_service_dependencies
  635. ):
  636. """
  637. Test successful URL scraping with Firecrawl provider.
  638. This test verifies:
  639. - Firecrawl scraping is properly executed
  640. - API credentials are retrieved and decrypted
  641. - Scraping parameters are correctly passed
  642. - Scraped data is returned in expected format
  643. """
  644. # Arrange: Create test account and prepare request
  645. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  646. # Mock FirecrawlApp scraping response
  647. mock_firecrawl_instance = MagicMock()
  648. mock_firecrawl_instance.scrape_url.return_value = {
  649. "title": "Scraped Page Title",
  650. "content": "This is the scraped content",
  651. "url": "https://example.com",
  652. "description": "Page description",
  653. }
  654. mock_external_service_dependencies["firecrawl_app"].return_value = mock_firecrawl_instance
  655. # Act: Scrape URL
  656. result = WebsiteService.get_scrape_url_data(
  657. provider="firecrawl", url="https://example.com", tenant_id=account.current_tenant.id, only_main_content=True
  658. )
  659. # Assert: Verify successful operation
  660. assert result is not None
  661. assert result["title"] == "Scraped Page Title"
  662. assert result["content"] == "This is the scraped content"
  663. assert result["url"] == "https://example.com"
  664. assert result["description"] == "Page description"
  665. # Verify external service interactions
  666. mock_external_service_dependencies["api_key_auth_service"].get_auth_credentials.assert_called_once_with(
  667. account.current_tenant.id, "website", "firecrawl"
  668. )
  669. mock_external_service_dependencies["encrypter"].decrypt_token.assert_called_once_with(
  670. tenant_id=account.current_tenant.id, token="encrypted_api_key"
  671. )
  672. # Verify FirecrawlApp was called with correct parameters
  673. mock_external_service_dependencies["firecrawl_app"].assert_called_once_with(
  674. api_key="decrypted_api_key", base_url="https://api.example.com"
  675. )
  676. mock_firecrawl_instance.scrape_url.assert_called_once_with(
  677. url="https://example.com", params={"onlyMainContent": True}
  678. )
  679. def test_get_scrape_url_data_watercrawl_success(
  680. self, db_session_with_containers, mock_external_service_dependencies
  681. ):
  682. """
  683. Test successful URL scraping with WaterCrawl provider.
  684. This test verifies:
  685. - WaterCrawl scraping is properly executed
  686. - API credentials are retrieved and decrypted
  687. - Provider returns expected scraping format
  688. - All required data fields are present
  689. """
  690. # Arrange: Create test account and prepare request
  691. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  692. # Act: Scrape URL
  693. result = WebsiteService.get_scrape_url_data(
  694. provider="watercrawl",
  695. url="https://example.com",
  696. tenant_id=account.current_tenant.id,
  697. only_main_content=False,
  698. )
  699. # Assert: Verify successful operation
  700. assert result is not None
  701. assert result["title"] == "Scraped Page"
  702. assert result["content"] == "Test content"
  703. assert result["url"] == "https://example.com"
  704. # Verify external service interactions
  705. mock_external_service_dependencies["api_key_auth_service"].get_auth_credentials.assert_called_once_with(
  706. account.current_tenant.id, "website", "watercrawl"
  707. )
  708. mock_external_service_dependencies["encrypter"].decrypt_token.assert_called_once_with(
  709. tenant_id=account.current_tenant.id, token="encrypted_api_key"
  710. )
  711. # Verify WaterCrawlProvider was called with correct parameters
  712. mock_external_service_dependencies["watercrawl_provider"].assert_called_once_with(
  713. api_key="decrypted_api_key", base_url="https://api.example.com"
  714. )
  715. def test_get_scrape_url_data_invalid_provider(self, db_session_with_containers, mock_external_service_dependencies):
  716. """
  717. Test URL scraping fails with invalid provider.
  718. This test verifies:
  719. - Invalid provider raises ValueError
  720. - Proper error message is provided
  721. - Service handles unsupported providers gracefully
  722. """
  723. # Arrange: Create test account and prepare request with invalid provider
  724. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  725. # Act & Assert: Verify proper error handling
  726. with pytest.raises(ValueError) as exc_info:
  727. WebsiteService.get_scrape_url_data(
  728. provider="invalid_provider",
  729. url="https://example.com",
  730. tenant_id=account.current_tenant.id,
  731. only_main_content=False,
  732. )
  733. assert "Invalid provider" in str(exc_info.value)
  734. def test_crawl_options_include_exclude_paths(self, db_session_with_containers, mock_external_service_dependencies):
  735. """
  736. Test CrawlOptions include and exclude path methods.
  737. This test verifies:
  738. - Include paths are properly parsed from comma-separated string
  739. - Exclude paths are properly parsed from comma-separated string
  740. - Empty or None values are handled correctly
  741. - Path lists are returned in expected format
  742. """
  743. # Arrange: Create CrawlOptions with various path configurations
  744. options_with_paths = CrawlOptions(includes="blog,news,articles", excludes="admin,private,test")
  745. options_without_paths = CrawlOptions(includes=None, excludes="")
  746. # Act: Get include and exclude paths
  747. include_paths = options_with_paths.get_include_paths()
  748. exclude_paths = options_with_paths.get_exclude_paths()
  749. empty_include_paths = options_without_paths.get_include_paths()
  750. empty_exclude_paths = options_without_paths.get_exclude_paths()
  751. # Assert: Verify path parsing
  752. assert include_paths == ["blog", "news", "articles"]
  753. assert exclude_paths == ["admin", "private", "test"]
  754. assert empty_include_paths == []
  755. assert empty_exclude_paths == []
  756. def test_website_crawl_api_request_conversion(self, db_session_with_containers, mock_external_service_dependencies):
  757. """
  758. Test WebsiteCrawlApiRequest conversion to CrawlRequest.
  759. This test verifies:
  760. - API request is properly converted to internal CrawlRequest
  761. - All options are correctly mapped
  762. - Default values are applied when options are missing
  763. - Conversion maintains data integrity
  764. """
  765. # Arrange: Create API request with various options
  766. api_request = WebsiteCrawlApiRequest(
  767. provider="firecrawl",
  768. url="https://example.com",
  769. options={
  770. "limit": 10,
  771. "crawl_sub_pages": True,
  772. "only_main_content": True,
  773. "includes": "blog,news",
  774. "excludes": "admin,private",
  775. "max_depth": 3,
  776. "use_sitemap": False,
  777. },
  778. )
  779. # Act: Convert to CrawlRequest
  780. crawl_request = api_request.to_crawl_request()
  781. # Assert: Verify conversion
  782. assert crawl_request.url == "https://example.com"
  783. assert crawl_request.provider == "firecrawl"
  784. assert crawl_request.options.limit == 10
  785. assert crawl_request.options.crawl_sub_pages is True
  786. assert crawl_request.options.only_main_content is True
  787. assert crawl_request.options.includes == "blog,news"
  788. assert crawl_request.options.excludes == "admin,private"
  789. assert crawl_request.options.max_depth == 3
  790. assert crawl_request.options.use_sitemap is False
  791. def test_website_crawl_api_request_from_args(self, db_session_with_containers, mock_external_service_dependencies):
  792. """
  793. Test WebsiteCrawlApiRequest creation from Flask arguments.
  794. This test verifies:
  795. - Request is properly created from parsed arguments
  796. - Required fields are validated
  797. - Optional fields are handled correctly
  798. - Validation errors are properly raised
  799. """
  800. # Arrange: Prepare valid arguments
  801. valid_args = {"provider": "watercrawl", "url": "https://example.com", "options": {"limit": 5}}
  802. # Act: Create request from args
  803. request = WebsiteCrawlApiRequest.from_args(valid_args)
  804. # Assert: Verify request creation
  805. assert request.provider == "watercrawl"
  806. assert request.url == "https://example.com"
  807. assert request.options == {"limit": 5}
  808. # Test missing provider
  809. invalid_args = {"url": "https://example.com", "options": {}}
  810. with pytest.raises(ValueError) as exc_info:
  811. WebsiteCrawlApiRequest.from_args(invalid_args)
  812. assert "Provider is required" in str(exc_info.value)
  813. # Test missing URL
  814. invalid_args = {"provider": "watercrawl", "options": {}}
  815. with pytest.raises(ValueError) as exc_info:
  816. WebsiteCrawlApiRequest.from_args(invalid_args)
  817. assert "URL is required" in str(exc_info.value)
  818. # Test missing options
  819. invalid_args = {"provider": "watercrawl", "url": "https://example.com"}
  820. with pytest.raises(ValueError) as exc_info:
  821. WebsiteCrawlApiRequest.from_args(invalid_args)
  822. assert "Options are required" in str(exc_info.value)
  823. def test_crawl_url_jinareader_sub_pages_success(
  824. self, db_session_with_containers, mock_external_service_dependencies
  825. ):
  826. """
  827. Test successful URL crawling with JinaReader provider for sub-pages.
  828. This test verifies:
  829. - JinaReader provider handles sub-page crawling correctly
  830. - HTTP POST request is made with proper parameters
  831. - Job ID is returned for multi-page crawling
  832. - All required parameters are passed correctly
  833. """
  834. # Arrange: Create test account and prepare request
  835. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  836. # Mock current_user for the test
  837. with patch("services.website_service.current_user") as mock_current_user:
  838. mock_current_user.current_tenant_id = account.current_tenant.id
  839. # Create API request for sub-page crawling
  840. api_request = WebsiteCrawlApiRequest(
  841. provider="jinareader",
  842. url="https://example.com",
  843. options={
  844. "limit": 5,
  845. "crawl_sub_pages": True,
  846. "only_main_content": False,
  847. "includes": None,
  848. "excludes": None,
  849. "max_depth": None,
  850. "use_sitemap": True,
  851. },
  852. )
  853. # Act: Execute crawl operation
  854. result = WebsiteService.crawl_url(api_request)
  855. # Assert: Verify successful operation
  856. assert result is not None
  857. assert result["status"] == "active"
  858. assert result["job_id"] == "jina_job_123"
  859. # Verify external service interactions
  860. mock_external_service_dependencies["api_key_auth_service"].get_auth_credentials.assert_called_once_with(
  861. account.current_tenant.id, "website", "jinareader"
  862. )
  863. mock_external_service_dependencies["encrypter"].decrypt_token.assert_called_once_with(
  864. tenant_id=account.current_tenant.id, token="encrypted_api_key"
  865. )
  866. # Verify HTTP POST request was made for sub-page crawling
  867. mock_external_service_dependencies["requests"].post.assert_called_once_with(
  868. "https://adaptivecrawl-kir3wx7b3a-uc.a.run.app",
  869. json={"url": "https://example.com", "maxPages": 5, "useSitemap": True},
  870. headers={"Content-Type": "application/json", "Authorization": "Bearer decrypted_api_key"},
  871. )
  872. def test_crawl_url_jinareader_failed_response(self, db_session_with_containers, mock_external_service_dependencies):
  873. """
  874. Test JinaReader crawling fails when API returns error.
  875. This test verifies:
  876. - Failed API response raises ValueError
  877. - Proper error message is provided
  878. - Service handles API failures gracefully
  879. """
  880. # Arrange: Create test account and prepare request
  881. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  882. # Mock failed response
  883. mock_failed_response = MagicMock()
  884. mock_failed_response.json.return_value = {"code": 500, "error": "Internal server error"}
  885. mock_external_service_dependencies["requests"].get.return_value = mock_failed_response
  886. # Mock current_user for the test
  887. with patch("services.website_service.current_user") as mock_current_user:
  888. mock_current_user.current_tenant_id = account.current_tenant.id
  889. # Create API request
  890. api_request = WebsiteCrawlApiRequest(
  891. provider="jinareader",
  892. url="https://example.com",
  893. options={"limit": 1, "crawl_sub_pages": False, "only_main_content": True},
  894. )
  895. # Act & Assert: Verify proper error handling
  896. with pytest.raises(ValueError) as exc_info:
  897. WebsiteService.crawl_url(api_request)
  898. assert "Failed to crawl" in str(exc_info.value)
  899. def test_get_crawl_status_firecrawl_active_job(
  900. self, db_session_with_containers, mock_external_service_dependencies
  901. ):
  902. """
  903. Test Firecrawl status retrieval for active (not completed) job.
  904. This test verifies:
  905. - Active job status is properly returned
  906. - Redis cache is not deleted for active jobs
  907. - Time consuming is not calculated for active jobs
  908. - All required status fields are present
  909. """
  910. # Arrange: Create test account and prepare request
  911. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  912. # Mock active job status
  913. mock_firecrawl_instance = MagicMock()
  914. mock_firecrawl_instance.check_crawl_status.return_value = {
  915. "status": "active",
  916. "total": 10,
  917. "current": 3,
  918. "data": [],
  919. }
  920. mock_external_service_dependencies["firecrawl_app"].return_value = mock_firecrawl_instance
  921. # Mock current_user for the test
  922. with patch("services.website_service.current_user") as mock_current_user:
  923. mock_current_user.current_tenant_id = account.current_tenant.id
  924. # Create API request
  925. api_request = WebsiteCrawlStatusApiRequest(provider="firecrawl", job_id="active_job_123")
  926. # Act: Get crawl status
  927. result = WebsiteService.get_crawl_status_typed(api_request)
  928. # Assert: Verify active job status
  929. assert result is not None
  930. assert result["status"] == "active"
  931. assert result["job_id"] == "active_job_123"
  932. assert result["total"] == 10
  933. assert result["current"] == 3
  934. assert "data" in result
  935. assert "time_consuming" not in result
  936. # Verify Redis cache was not accessed for active jobs
  937. mock_external_service_dependencies["redis_client"].get.assert_not_called()
  938. mock_external_service_dependencies["redis_client"].delete.assert_not_called()
  939. def test_get_crawl_url_data_firecrawl_storage_fallback(
  940. self, db_session_with_containers, mock_external_service_dependencies
  941. ):
  942. """
  943. Test Firecrawl URL data retrieval with storage fallback.
  944. This test verifies:
  945. - Storage fallback works when storage has data
  946. - API call is not made when storage has data
  947. - Data is properly parsed from storage
  948. - Correct URL data is returned
  949. """
  950. # Arrange: Create test account and prepare request
  951. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  952. # Mock storage to return existing data
  953. mock_external_service_dependencies["storage"].exists.return_value = True
  954. mock_external_service_dependencies["storage"].load_once.return_value = (
  955. b"["
  956. b'{"source_url": "https://example.com/page1", '
  957. b'"title": "Page 1", "description": "Description 1", "markdown": "# Page 1"}, '
  958. b'{"source_url": "https://example.com/page2", "title": "Page 2", '
  959. b'"description": "Description 2", "markdown": "# Page 2"}'
  960. b"]"
  961. )
  962. # Act: Get URL data for specific URL
  963. result = WebsiteService.get_crawl_url_data(
  964. job_id="test_job_id_123",
  965. provider="firecrawl",
  966. url="https://example.com/page1",
  967. tenant_id=account.current_tenant.id,
  968. )
  969. # Assert: Verify successful operation
  970. assert result is not None
  971. assert result["source_url"] == "https://example.com/page1"
  972. assert result["title"] == "Page 1"
  973. assert result["description"] == "Description 1"
  974. assert result["markdown"] == "# Page 1"
  975. # Verify storage was accessed
  976. mock_external_service_dependencies["storage"].exists.assert_called_once()
  977. mock_external_service_dependencies["storage"].load_once.assert_called_once()
  978. def test_get_crawl_url_data_firecrawl_api_fallback(
  979. self, db_session_with_containers, mock_external_service_dependencies
  980. ):
  981. """
  982. Test Firecrawl URL data retrieval with API fallback when storage is empty.
  983. This test verifies:
  984. - API fallback works when storage has no data
  985. - FirecrawlApp is called to get data
  986. - Completed job status is checked
  987. - Data is returned from API response
  988. """
  989. # Arrange: Create test account and prepare request
  990. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  991. # Mock storage to return no data
  992. mock_external_service_dependencies["storage"].exists.return_value = False
  993. # Mock FirecrawlApp for API fallback
  994. mock_firecrawl_instance = MagicMock()
  995. mock_firecrawl_instance.check_crawl_status.return_value = {
  996. "status": "completed",
  997. "data": [
  998. {
  999. "source_url": "https://example.com/api_page",
  1000. "title": "API Page",
  1001. "description": "API Description",
  1002. "markdown": "# API Content",
  1003. }
  1004. ],
  1005. }
  1006. mock_external_service_dependencies["firecrawl_app"].return_value = mock_firecrawl_instance
  1007. # Act: Get URL data
  1008. result = WebsiteService.get_crawl_url_data(
  1009. job_id="test_job_id_123",
  1010. provider="firecrawl",
  1011. url="https://example.com/api_page",
  1012. tenant_id=account.current_tenant.id,
  1013. )
  1014. # Assert: Verify successful operation
  1015. assert result is not None
  1016. assert result["source_url"] == "https://example.com/api_page"
  1017. assert result["title"] == "API Page"
  1018. assert result["description"] == "API Description"
  1019. assert result["markdown"] == "# API Content"
  1020. # Verify API was called
  1021. mock_external_service_dependencies["firecrawl_app"].assert_called_once()
  1022. def test_get_crawl_url_data_firecrawl_incomplete_job(
  1023. self, db_session_with_containers, mock_external_service_dependencies
  1024. ):
  1025. """
  1026. Test Firecrawl URL data retrieval fails for incomplete job.
  1027. This test verifies:
  1028. - Incomplete job raises ValueError
  1029. - Proper error message is provided
  1030. - Service handles incomplete jobs gracefully
  1031. """
  1032. # Arrange: Create test account and prepare request
  1033. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  1034. # Mock storage to return no data
  1035. mock_external_service_dependencies["storage"].exists.return_value = False
  1036. # Mock incomplete job status
  1037. mock_firecrawl_instance = MagicMock()
  1038. mock_firecrawl_instance.check_crawl_status.return_value = {"status": "active", "data": []}
  1039. mock_external_service_dependencies["firecrawl_app"].return_value = mock_firecrawl_instance
  1040. # Act & Assert: Verify proper error handling
  1041. with pytest.raises(ValueError) as exc_info:
  1042. WebsiteService.get_crawl_url_data(
  1043. job_id="test_job_id_123",
  1044. provider="firecrawl",
  1045. url="https://example.com/page",
  1046. tenant_id=account.current_tenant.id,
  1047. )
  1048. assert "Crawl job is not completed" in str(exc_info.value)
  1049. def test_get_crawl_url_data_jinareader_with_job_id(
  1050. self, db_session_with_containers, mock_external_service_dependencies
  1051. ):
  1052. """
  1053. Test JinaReader URL data retrieval with job ID for multi-page crawling.
  1054. This test verifies:
  1055. - JinaReader handles job ID-based data retrieval
  1056. - Status check is performed before data retrieval
  1057. - Processed data is properly formatted
  1058. - Correct URL data is returned
  1059. """
  1060. # Arrange: Create test account and prepare request
  1061. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  1062. # Mock successful status response
  1063. mock_status_response = MagicMock()
  1064. mock_status_response.json.return_value = {
  1065. "code": 200,
  1066. "data": {
  1067. "status": "completed",
  1068. "processed": {
  1069. "https://example.com/page1": {
  1070. "data": {
  1071. "title": "Page 1",
  1072. "url": "https://example.com/page1",
  1073. "description": "Description 1",
  1074. "content": "# Content 1",
  1075. }
  1076. }
  1077. },
  1078. },
  1079. }
  1080. mock_external_service_dependencies["requests"].post.return_value = mock_status_response
  1081. # Act: Get URL data with job ID
  1082. result = WebsiteService.get_crawl_url_data(
  1083. job_id="jina_job_123",
  1084. provider="jinareader",
  1085. url="https://example.com/page1",
  1086. tenant_id=account.current_tenant.id,
  1087. )
  1088. # Assert: Verify successful operation
  1089. assert result is not None
  1090. assert result["title"] == "Page 1"
  1091. assert result["url"] == "https://example.com/page1"
  1092. assert result["description"] == "Description 1"
  1093. assert result["content"] == "# Content 1"
  1094. # Verify HTTP requests were made
  1095. assert mock_external_service_dependencies["requests"].post.call_count == 2
  1096. def test_get_crawl_url_data_jinareader_incomplete_job(
  1097. self, db_session_with_containers, mock_external_service_dependencies
  1098. ):
  1099. """
  1100. Test JinaReader URL data retrieval fails for incomplete job.
  1101. This test verifies:
  1102. - Incomplete job raises ValueError
  1103. - Proper error message is provided
  1104. - Service handles incomplete jobs gracefully
  1105. """
  1106. # Arrange: Create test account and prepare request
  1107. account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
  1108. # Mock incomplete job status
  1109. mock_status_response = MagicMock()
  1110. mock_status_response.json.return_value = {"code": 200, "data": {"status": "active", "processed": {}}}
  1111. mock_external_service_dependencies["requests"].post.return_value = mock_status_response
  1112. # Act & Assert: Verify proper error handling
  1113. with pytest.raises(ValueError) as exc_info:
  1114. WebsiteService.get_crawl_url_data(
  1115. job_id="jina_job_123",
  1116. provider="jinareader",
  1117. url="https://example.com/page",
  1118. tenant_id=account.current_tenant.id,
  1119. )
  1120. assert "Crawl job is not completed" in str(exc_info.value)
  1121. def test_crawl_options_default_values(self, db_session_with_containers, mock_external_service_dependencies):
  1122. """
  1123. Test CrawlOptions default values and initialization.
  1124. This test verifies:
  1125. - Default values are properly set
  1126. - Optional fields can be None
  1127. - Boolean fields have correct defaults
  1128. - Integer fields have correct defaults
  1129. """
  1130. # Arrange: Create CrawlOptions with minimal parameters
  1131. options = CrawlOptions()
  1132. # Assert: Verify default values
  1133. assert options.limit == 1
  1134. assert options.crawl_sub_pages is False
  1135. assert options.only_main_content is False
  1136. assert options.includes is None
  1137. assert options.excludes is None
  1138. assert options.max_depth is None
  1139. assert options.use_sitemap is True
  1140. # Test with custom values
  1141. custom_options = CrawlOptions(
  1142. limit=10,
  1143. crawl_sub_pages=True,
  1144. only_main_content=True,
  1145. includes="blog,news",
  1146. excludes="admin",
  1147. max_depth=3,
  1148. use_sitemap=False,
  1149. )
  1150. assert custom_options.limit == 10
  1151. assert custom_options.crawl_sub_pages is True
  1152. assert custom_options.only_main_content is True
  1153. assert custom_options.includes == "blog,news"
  1154. assert custom_options.excludes == "admin"
  1155. assert custom_options.max_depth == 3
  1156. assert custom_options.use_sitemap is False
  1157. def test_website_crawl_status_api_request_from_args(
  1158. self, db_session_with_containers, mock_external_service_dependencies
  1159. ):
  1160. """
  1161. Test WebsiteCrawlStatusApiRequest creation from Flask arguments.
  1162. This test verifies:
  1163. - Request is properly created from parsed arguments
  1164. - Required fields are validated
  1165. - Job ID is properly handled
  1166. - Validation errors are properly raised
  1167. """
  1168. # Arrange: Prepare valid arguments
  1169. valid_args = {"provider": "firecrawl"}
  1170. job_id = "test_job_123"
  1171. # Act: Create request from args
  1172. request = WebsiteCrawlStatusApiRequest.from_args(valid_args, job_id)
  1173. # Assert: Verify request creation
  1174. assert request.provider == "firecrawl"
  1175. assert request.job_id == "test_job_123"
  1176. # Test missing provider
  1177. invalid_args = {}
  1178. with pytest.raises(ValueError) as exc_info:
  1179. WebsiteCrawlStatusApiRequest.from_args(invalid_args, job_id)
  1180. assert "Provider is required" in str(exc_info.value)
  1181. # Test missing job ID
  1182. with pytest.raises(ValueError) as exc_info:
  1183. WebsiteCrawlStatusApiRequest.from_args(valid_args, "")
  1184. assert "Job ID is required" in str(exc_info.value)
  1185. def test_scrape_request_initialization(self, db_session_with_containers, mock_external_service_dependencies):
  1186. """
  1187. Test ScrapeRequest dataclass initialization and properties.
  1188. This test verifies:
  1189. - ScrapeRequest is properly initialized
  1190. - All fields are correctly set
  1191. - Boolean field works correctly
  1192. - String fields are properly assigned
  1193. """
  1194. # Arrange: Create ScrapeRequest
  1195. request = ScrapeRequest(
  1196. provider="firecrawl", url="https://example.com", tenant_id="tenant_123", only_main_content=True
  1197. )
  1198. # Assert: Verify initialization
  1199. assert request.provider == "firecrawl"
  1200. assert request.url == "https://example.com"
  1201. assert request.tenant_id == "tenant_123"
  1202. assert request.only_main_content is True
  1203. # Test with different values
  1204. request2 = ScrapeRequest(
  1205. provider="watercrawl", url="https://test.com", tenant_id="tenant_456", only_main_content=False
  1206. )
  1207. assert request2.provider == "watercrawl"
  1208. assert request2.url == "https://test.com"
  1209. assert request2.tenant_id == "tenant_456"
  1210. assert request2.only_main_content is False