test_wrapper_async.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. """
  2. Tests para cubrir código específico del wrapper que no se ejecuta en tests de integración.
  3. Usamos mocks para simular el comportamiento de AsyncCamoufox.
  4. """
  5. import pytest
  6. import asyncio
  7. from unittest.mock import AsyncMock, MagicMock, patch
  8. from app.use_cases.camoufox.wrapper import CamoufoxWrapper
  9. @pytest.mark.asyncio
  10. class TestCamoufoxWrapperVisit:
  11. """Tests del método visit() con mocks para cubrir todas las ramas."""
  12. @pytest.fixture
  13. def mock_browser(self):
  14. """Crea un mock de browser con page mockeado."""
  15. mock_page = AsyncMock()
  16. mock_page.content = AsyncMock(return_value="<html><body>Test</body></html>")
  17. mock_page.screenshot = AsyncMock(return_value=b"screenshot_data")
  18. mock_browser = AsyncMock()
  19. mock_browser.new_page = AsyncMock(return_value=mock_page)
  20. return mock_browser, mock_page
  21. @pytest.fixture
  22. def wrapper(self):
  23. return CamoufoxWrapper()
  24. @patch("app.use_cases.camoufox.wrapper.AsyncCamoufox")
  25. async def test_visit_with_proxy(self, mock_camoufox_class, wrapper, mock_browser):
  26. """Test que cubre la rama de proxy en visit()."""
  27. mock_browser_instance, mock_page = mock_browser
  28. mock_camoufox_class.return_value.__aenter__.return_value = mock_browser_instance
  29. result = await wrapper.visit(
  30. "https://example.com",
  31. {"proxy": "http://proxy:8080", "screenshot": False}
  32. )
  33. # Verificar que se llamó con proxy y geoip
  34. call_kwargs = mock_camoufox_class.call_args[1]
  35. assert call_kwargs["proxy"]["server"] == "http://proxy:8080"
  36. assert call_kwargs["geoip"] is True
  37. assert result["html"] == "<html><body>Test</body></html>"
  38. @patch("app.use_cases.camoufox.wrapper.AsyncCamoufox")
  39. async def test_visit_with_proxy_auth(self, mock_camoufox_class, wrapper, mock_browser):
  40. """Test que cubre proxy con autenticación."""
  41. mock_browser_instance, mock_page = mock_browser
  42. mock_camoufox_class.return_value.__aenter__.return_value = mock_browser_instance
  43. result = await wrapper.visit(
  44. "https://example.com",
  45. {
  46. "proxy": "http://proxy:8080",
  47. "proxyAuth": {"username": "user", "password": "pass"},
  48. "screenshot": False,
  49. }
  50. )
  51. call_kwargs = mock_camoufox_class.call_args[1]
  52. assert call_kwargs["proxy"]["username"] == "user"
  53. assert call_kwargs["proxy"]["password"] == "pass"
  54. @patch("app.use_cases.camoufox.wrapper.AsyncCamoufox")
  55. async def test_visit_with_headers(self, mock_camoufox_class, wrapper, mock_browser):
  56. """Test que cubre la rama de headers (excluye host y content-length)."""
  57. mock_browser_instance, mock_page = mock_browser
  58. mock_camoufox_class.return_value.__aenter__.return_value = mock_browser_instance
  59. headers = {
  60. "user-agent": "TestAgent",
  61. "host": "example.com", # Debería ser eliminado
  62. "content-length": "100", # Debería ser eliminado
  63. "x-custom": "value",
  64. }
  65. result = await wrapper.visit(
  66. "https://example.com",
  67. {"headers": headers.copy(), "screenshot": False}
  68. )
  69. # Verificar que se llamó a set_extra_http_headers
  70. mock_page.set_extra_http_headers.assert_called_once()
  71. called_headers = mock_page.set_extra_http_headers.call_args[0][0]
  72. assert "host" not in called_headers
  73. assert "content-length" not in called_headers
  74. assert "user-agent" in called_headers
  75. assert "x-custom" in called_headers
  76. @patch("app.use_cases.camoufox.wrapper.AsyncCamoufox")
  77. async def test_visit_mercadolibre_triggers_hacks(self, mock_camoufox_class, wrapper, mock_browser):
  78. """Test que verifica que URLs de MeLi llaman a _handle_mercadolibre."""
  79. mock_browser_instance, mock_page = mock_browser
  80. # Mockear _handle_mercadolibre para que retorne un HTML específico
  81. with patch.object(wrapper, "_handle_mercadolibre", new_callable=AsyncMock) as mock_handle:
  82. mock_handle.return_value = "<html><body>MeLi Hackeado</body></html>"
  83. mock_camoufox_class.return_value.__aenter__.return_value = mock_browser_instance
  84. result = await wrapper.visit(
  85. "https://autos.mercadolibre.cl/auto",
  86. {"screenshot": False}
  87. )
  88. mock_handle.assert_called_once()
  89. assert result["html"] == "<html><body>MeLi Hackeado</body></html>"
  90. @patch("app.use_cases.camoufox.wrapper.AsyncCamoufox")
  91. async def test_visit_non_mercadolibre_skips_hacks(self, mock_camoufox_class, wrapper, mock_browser):
  92. """Test que verifica que URLs normales NO llaman a _handle_mercadolibre."""
  93. mock_browser_instance, mock_page = mock_browser
  94. mock_camoufox_class.return_value.__aenter__.return_value = mock_browser_instance
  95. # Mockear _handle_mercadolibre para verificar que NO se llama
  96. with patch.object(wrapper, "_handle_mercadolibre", new_callable=AsyncMock) as mock_handle:
  97. result = await wrapper.visit(
  98. "https://example.com",
  99. {"screenshot": False}
  100. )
  101. mock_handle.assert_not_called()
  102. assert result["html"] == "<html><body>Test</body></html>"
  103. @patch("app.use_cases.camoufox.wrapper.AsyncCamoufox")
  104. async def test_visit_with_screenshot(self, mock_camoufox_class, wrapper, mock_browser):
  105. """Test que cubre la rama de screenshot."""
  106. mock_browser_instance, mock_page = mock_browser
  107. mock_camoufox_class.return_value.__aenter__.return_value = mock_browser_instance
  108. result = await wrapper.visit(
  109. "https://example.com",
  110. {"screenshot": True}
  111. )
  112. mock_page.screenshot.assert_called_once()
  113. assert result["screenshot"] is not None
  114. @patch("app.use_cases.camoufox.wrapper.AsyncCamoufox")
  115. async def test_visit_without_screenshot(self, mock_camoufox_class, wrapper, mock_browser):
  116. """Test que verifica que screenshot=False no llama a screenshot()."""
  117. mock_browser_instance, mock_page = mock_browser
  118. mock_camoufox_class.return_value.__aenter__.return_value = mock_browser_instance
  119. result = await wrapper.visit(
  120. "https://example.com",
  121. {"screenshot": False}
  122. )
  123. mock_page.screenshot.assert_not_called()
  124. assert result["screenshot"] is None
  125. @patch("app.use_cases.camoufox.wrapper.AsyncCamoufox")
  126. async def test_virtual_display_fallback(self, mock_camoufox_class, wrapper, mock_browser):
  127. """Test que cubre el fallback de virtual display a headless=True."""
  128. mock_browser_instance, mock_page = mock_browser
  129. # Primera llamada falla, segunda tiene éxito
  130. mock_camoufox_class.side_effect = [
  131. Exception("Virtual display failed"),
  132. MagicMock(__aenter__=AsyncMock(return_value=mock_browser_instance))
  133. ]
  134. # Simular que headless era "virtual"
  135. with patch("app.use_cases.camoufox.wrapper._resolve_headless_mode", return_value="virtual"):
  136. result = await wrapper.visit(
  137. "https://example.com",
  138. {"screenshot": False}
  139. )
  140. # Verificar que se llamó dos veces (intento + fallback)
  141. assert mock_camoufox_class.call_count == 2
  142. # Segunda llamada debería tener headless=True
  143. second_call_kwargs = mock_camoufox_class.call_args_list[1][1]
  144. assert second_call_kwargs["headless"] is True
  145. @patch("app.use_cases.camoufox.wrapper.AsyncCamoufox")
  146. async def test_visit_with_waituntil(self, mock_camoufox_class, wrapper, mock_browser):
  147. """Test que cubre diferentes valores de waitUntil."""
  148. mock_browser_instance, mock_page = mock_browser
  149. mock_camoufox_class.return_value.__aenter__.return_value = mock_browser_instance
  150. for wait_until in ["load", "domcontentloaded", "networkidle", "commit"]:
  151. await wrapper.visit(
  152. "https://example.com",
  153. {"waitUntil": wait_until, "screenshot": False}
  154. )
  155. mock_page.goto.assert_called()
  156. call_kwargs = mock_page.goto.call_args[1]
  157. assert call_kwargs["wait_until"] == wait_until
  158. @pytest.mark.asyncio
  159. class TestHandleMercadolibre:
  160. """Tests específicos para el método _handle_mercadolibre."""
  161. @pytest.fixture
  162. def wrapper(self):
  163. return CamoufoxWrapper()
  164. @pytest.fixture
  165. def mock_page(self):
  166. page = AsyncMock()
  167. page.content = AsyncMock(return_value="<html><body>Normal</body></html>")
  168. page.wait_for_selector = AsyncMock()
  169. page.evaluate = AsyncMock()
  170. page.click = AsyncMock()
  171. page.reload = AsyncMock()
  172. return page
  173. @pytest.fixture
  174. def mock_browser(self):
  175. browser = AsyncMock()
  176. browser.new_page = AsyncMock(return_value=AsyncMock())
  177. return browser
  178. async def test_handle_mercadolibre_captcha_detected(self, wrapper, mock_page, mock_browser):
  179. """Test que cubre la rama de captcha 'Para continuar'."""
  180. new_page_mock = AsyncMock()
  181. new_page_mock.content = AsyncMock(return_value="<html><body>Después de recargar</body></html>")
  182. mock_browser.new_page = AsyncMock(return_value=new_page_mock)
  183. mock_page.content = AsyncMock(side_effect=[
  184. "<html><body>Para continuar</body></html>",
  185. "<html><body>Final</body></html>",
  186. ])
  187. result = await wrapper._handle_mercadolibre(mock_page, "https://mercadolibre.cl", mock_browser)
  188. # Verificar que se creó una nueva página
  189. mock_browser.new_page.assert_called_once()
  190. assert result is not None
  191. async def test_handle_mercadolibre_rate_limit_detected(self, wrapper, mock_page, mock_browser):
  192. """Test que cubre la rama de rate limit 429."""
  193. mock_page.reload = AsyncMock()
  194. mock_page.content = AsyncMock(side_effect=[
  195. "<html><body>Normal</body></html>",
  196. '<html><body>{"message":"local_rate_limited","status":429}</body></html>',
  197. "<html><body>Después de reload</body></html>",
  198. "<html><body>Final</body></html>",
  199. ])
  200. result = await wrapper._handle_mercadolibre(mock_page, "https://mercadolibre.cl", mock_browser)
  201. # Verificar que se llamó a reload
  202. mock_page.reload.assert_called_once()
  203. assert result is not None
  204. async def test_handle_mercadolibre_cookie_banner_present(self, wrapper, mock_page, mock_browser):
  205. """Test que cubre la rama de banner de cookies."""
  206. mock_page.content = AsyncMock(return_value="<html><body>Normal</body></html>")
  207. mock_page.wait_for_selector = AsyncMock()
  208. mock_page.evaluate = AsyncMock()
  209. result = await wrapper._handle_mercadolibre(mock_page, "https://mercadolibre.cl", mock_browser)
  210. # Verificar que se llamó a wait_for_selector (al menos una vez)
  211. assert mock_page.wait_for_selector.call_count >= 1
  212. assert result is not None
  213. mock_page.evaluate.assert_called()
  214. async def test_handle_mercadolibre_cookie_banner_not_present(self, wrapper, mock_page, mock_browser):
  215. """Test que cubre la rama donde el banner de cookies no existe."""
  216. mock_page.content.return_value = "<html><body>Normal</body></html>"
  217. mock_page.wait_for_selector.side_effect = Exception("Timeout")
  218. # No debería fallar aunque el banner no exista
  219. result = await wrapper._handle_mercadolibre(mock_page, "https://mercadolibre.cl", mock_browser)
  220. assert result is not None
  221. async def test_handle_mercadolibre_buttons_present(self, wrapper, mock_page, mock_browser):
  222. """Test que cubre la rama de botones de características y descripción."""
  223. mock_page.content.return_value = "<html><body>Normal</body></html>"
  224. mock_page.wait_for_selector.return_value = None
  225. mock_page.click.return_value = None
  226. result = await wrapper._handle_mercadolibre(mock_page, "https://mercadolibre.cl", mock_browser)
  227. # Verificar que se intentó hacer click en los botones
  228. assert mock_page.wait_for_selector.call_count >= 2
  229. assert mock_page.click.call_count >= 2
  230. async def test_handle_mercadolibre_buttons_not_present(self, wrapper, mock_page, mock_browser):
  231. """Test que cubre la rama donde los botones no existen."""
  232. mock_page.content.return_value = "<html><body>Normal</body></html>"
  233. mock_page.wait_for_selector.side_effect = Exception("Timeout")
  234. # No debería fallar aunque los botones no existan
  235. result = await wrapper._handle_mercadolibre(mock_page, "https://mercadolibre.cl", mock_browser)
  236. assert result is not None