|
@@ -0,0 +1,294 @@
|
|
|
|
|
+"""
|
|
|
|
|
+Tests para cubrir código específico del wrapper que no se ejecuta en tests de integración.
|
|
|
|
|
+Usamos mocks para simular el comportamiento de AsyncCamoufox.
|
|
|
|
|
+"""
|
|
|
|
|
+import pytest
|
|
|
|
|
+import asyncio
|
|
|
|
|
+from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
|
+from app.use_cases.camoufox.wrapper import CamoufoxWrapper
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
|
+class TestCamoufoxWrapperVisit:
|
|
|
|
|
+ """Tests del método visit() con mocks para cubrir todas las ramas."""
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.fixture
|
|
|
|
|
+ def mock_browser(self):
|
|
|
|
|
+ """Crea un mock de browser con page mockeado."""
|
|
|
|
|
+ mock_page = AsyncMock()
|
|
|
|
|
+ mock_page.content = AsyncMock(return_value="<html><body>Test</body></html>")
|
|
|
|
|
+ mock_page.screenshot = AsyncMock(return_value=b"screenshot_data")
|
|
|
|
|
+
|
|
|
|
|
+ mock_browser = AsyncMock()
|
|
|
|
|
+ mock_browser.new_page = AsyncMock(return_value=mock_page)
|
|
|
|
|
+
|
|
|
|
|
+ return mock_browser, mock_page
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.fixture
|
|
|
|
|
+ def wrapper(self):
|
|
|
|
|
+ return CamoufoxWrapper()
|
|
|
|
|
+
|
|
|
|
|
+ @patch("app.use_cases.camoufox.wrapper.AsyncCamoufox")
|
|
|
|
|
+ async def test_visit_with_proxy(self, mock_camoufox_class, wrapper, mock_browser):
|
|
|
|
|
+ """Test que cubre la rama de proxy en visit()."""
|
|
|
|
|
+ mock_browser_instance, mock_page = mock_browser
|
|
|
|
|
+ mock_camoufox_class.return_value.__aenter__.return_value = mock_browser_instance
|
|
|
|
|
+
|
|
|
|
|
+ result = await wrapper.visit(
|
|
|
|
|
+ "https://example.com",
|
|
|
|
|
+ {"proxy": "http://proxy:8080", "screenshot": False}
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Verificar que se llamó con proxy y geoip
|
|
|
|
|
+ call_kwargs = mock_camoufox_class.call_args[1]
|
|
|
|
|
+ assert call_kwargs["proxy"]["server"] == "http://proxy:8080"
|
|
|
|
|
+ assert call_kwargs["geoip"] is True
|
|
|
|
|
+ assert result["html"] == "<html><body>Test</body></html>"
|
|
|
|
|
+
|
|
|
|
|
+ @patch("app.use_cases.camoufox.wrapper.AsyncCamoufox")
|
|
|
|
|
+ async def test_visit_with_proxy_auth(self, mock_camoufox_class, wrapper, mock_browser):
|
|
|
|
|
+ """Test que cubre proxy con autenticación."""
|
|
|
|
|
+ mock_browser_instance, mock_page = mock_browser
|
|
|
|
|
+ mock_camoufox_class.return_value.__aenter__.return_value = mock_browser_instance
|
|
|
|
|
+
|
|
|
|
|
+ result = await wrapper.visit(
|
|
|
|
|
+ "https://example.com",
|
|
|
|
|
+ {
|
|
|
|
|
+ "proxy": "http://proxy:8080",
|
|
|
|
|
+ "proxyAuth": {"username": "user", "password": "pass"},
|
|
|
|
|
+ "screenshot": False,
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ call_kwargs = mock_camoufox_class.call_args[1]
|
|
|
|
|
+ assert call_kwargs["proxy"]["username"] == "user"
|
|
|
|
|
+ assert call_kwargs["proxy"]["password"] == "pass"
|
|
|
|
|
+
|
|
|
|
|
+ @patch("app.use_cases.camoufox.wrapper.AsyncCamoufox")
|
|
|
|
|
+ async def test_visit_with_headers(self, mock_camoufox_class, wrapper, mock_browser):
|
|
|
|
|
+ """Test que cubre la rama de headers (excluye host y content-length)."""
|
|
|
|
|
+ mock_browser_instance, mock_page = mock_browser
|
|
|
|
|
+ mock_camoufox_class.return_value.__aenter__.return_value = mock_browser_instance
|
|
|
|
|
+
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ "user-agent": "TestAgent",
|
|
|
|
|
+ "host": "example.com", # Debería ser eliminado
|
|
|
|
|
+ "content-length": "100", # Debería ser eliminado
|
|
|
|
|
+ "x-custom": "value",
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ result = await wrapper.visit(
|
|
|
|
|
+ "https://example.com",
|
|
|
|
|
+ {"headers": headers.copy(), "screenshot": False}
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Verificar que se llamó a set_extra_http_headers
|
|
|
|
|
+ mock_page.set_extra_http_headers.assert_called_once()
|
|
|
|
|
+ called_headers = mock_page.set_extra_http_headers.call_args[0][0]
|
|
|
|
|
+ assert "host" not in called_headers
|
|
|
|
|
+ assert "content-length" not in called_headers
|
|
|
|
|
+ assert "user-agent" in called_headers
|
|
|
|
|
+ assert "x-custom" in called_headers
|
|
|
|
|
+
|
|
|
|
|
+ @patch("app.use_cases.camoufox.wrapper.AsyncCamoufox")
|
|
|
|
|
+ async def test_visit_mercadolibre_triggers_hacks(self, mock_camoufox_class, wrapper, mock_browser):
|
|
|
|
|
+ """Test que verifica que URLs de MeLi llaman a _handle_mercadolibre."""
|
|
|
|
|
+ mock_browser_instance, mock_page = mock_browser
|
|
|
|
|
+
|
|
|
|
|
+ # Mockear _handle_mercadolibre para que retorne un HTML específico
|
|
|
|
|
+ with patch.object(wrapper, "_handle_mercadolibre", new_callable=AsyncMock) as mock_handle:
|
|
|
|
|
+ mock_handle.return_value = "<html><body>MeLi Hackeado</body></html>"
|
|
|
|
|
+ mock_camoufox_class.return_value.__aenter__.return_value = mock_browser_instance
|
|
|
|
|
+
|
|
|
|
|
+ result = await wrapper.visit(
|
|
|
|
|
+ "https://autos.mercadolibre.cl/auto",
|
|
|
|
|
+ {"screenshot": False}
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ mock_handle.assert_called_once()
|
|
|
|
|
+ assert result["html"] == "<html><body>MeLi Hackeado</body></html>"
|
|
|
|
|
+
|
|
|
|
|
+ @patch("app.use_cases.camoufox.wrapper.AsyncCamoufox")
|
|
|
|
|
+ async def test_visit_non_mercadolibre_skips_hacks(self, mock_camoufox_class, wrapper, mock_browser):
|
|
|
|
|
+ """Test que verifica que URLs normales NO llaman a _handle_mercadolibre."""
|
|
|
|
|
+ mock_browser_instance, mock_page = mock_browser
|
|
|
|
|
+ mock_camoufox_class.return_value.__aenter__.return_value = mock_browser_instance
|
|
|
|
|
+
|
|
|
|
|
+ # Mockear _handle_mercadolibre para verificar que NO se llama
|
|
|
|
|
+ with patch.object(wrapper, "_handle_mercadolibre", new_callable=AsyncMock) as mock_handle:
|
|
|
|
|
+ result = await wrapper.visit(
|
|
|
|
|
+ "https://example.com",
|
|
|
|
|
+ {"screenshot": False}
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ mock_handle.assert_not_called()
|
|
|
|
|
+ assert result["html"] == "<html><body>Test</body></html>"
|
|
|
|
|
+
|
|
|
|
|
+ @patch("app.use_cases.camoufox.wrapper.AsyncCamoufox")
|
|
|
|
|
+ async def test_visit_with_screenshot(self, mock_camoufox_class, wrapper, mock_browser):
|
|
|
|
|
+ """Test que cubre la rama de screenshot."""
|
|
|
|
|
+ mock_browser_instance, mock_page = mock_browser
|
|
|
|
|
+ mock_camoufox_class.return_value.__aenter__.return_value = mock_browser_instance
|
|
|
|
|
+
|
|
|
|
|
+ result = await wrapper.visit(
|
|
|
|
|
+ "https://example.com",
|
|
|
|
|
+ {"screenshot": True}
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ mock_page.screenshot.assert_called_once()
|
|
|
|
|
+ assert result["screenshot"] is not None
|
|
|
|
|
+
|
|
|
|
|
+ @patch("app.use_cases.camoufox.wrapper.AsyncCamoufox")
|
|
|
|
|
+ async def test_visit_without_screenshot(self, mock_camoufox_class, wrapper, mock_browser):
|
|
|
|
|
+ """Test que verifica que screenshot=False no llama a screenshot()."""
|
|
|
|
|
+ mock_browser_instance, mock_page = mock_browser
|
|
|
|
|
+ mock_camoufox_class.return_value.__aenter__.return_value = mock_browser_instance
|
|
|
|
|
+
|
|
|
|
|
+ result = await wrapper.visit(
|
|
|
|
|
+ "https://example.com",
|
|
|
|
|
+ {"screenshot": False}
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ mock_page.screenshot.assert_not_called()
|
|
|
|
|
+ assert result["screenshot"] is None
|
|
|
|
|
+
|
|
|
|
|
+ @patch("app.use_cases.camoufox.wrapper.AsyncCamoufox")
|
|
|
|
|
+ async def test_virtual_display_fallback(self, mock_camoufox_class, wrapper, mock_browser):
|
|
|
|
|
+ """Test que cubre el fallback de virtual display a headless=True."""
|
|
|
|
|
+ mock_browser_instance, mock_page = mock_browser
|
|
|
|
|
+
|
|
|
|
|
+ # Primera llamada falla, segunda tiene éxito
|
|
|
|
|
+ mock_camoufox_class.side_effect = [
|
|
|
|
|
+ Exception("Virtual display failed"),
|
|
|
|
|
+ MagicMock(__aenter__=AsyncMock(return_value=mock_browser_instance))
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ # Simular que headless era "virtual"
|
|
|
|
|
+ with patch("app.use_cases.camoufox.wrapper._resolve_headless_mode", return_value="virtual"):
|
|
|
|
|
+ result = await wrapper.visit(
|
|
|
|
|
+ "https://example.com",
|
|
|
|
|
+ {"screenshot": False}
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Verificar que se llamó dos veces (intento + fallback)
|
|
|
|
|
+ assert mock_camoufox_class.call_count == 2
|
|
|
|
|
+ # Segunda llamada debería tener headless=True
|
|
|
|
|
+ second_call_kwargs = mock_camoufox_class.call_args_list[1][1]
|
|
|
|
|
+ assert second_call_kwargs["headless"] is True
|
|
|
|
|
+
|
|
|
|
|
+ @patch("app.use_cases.camoufox.wrapper.AsyncCamoufox")
|
|
|
|
|
+ async def test_visit_with_waituntil(self, mock_camoufox_class, wrapper, mock_browser):
|
|
|
|
|
+ """Test que cubre diferentes valores de waitUntil."""
|
|
|
|
|
+ mock_browser_instance, mock_page = mock_browser
|
|
|
|
|
+ mock_camoufox_class.return_value.__aenter__.return_value = mock_browser_instance
|
|
|
|
|
+
|
|
|
|
|
+ for wait_until in ["load", "domcontentloaded", "networkidle", "commit"]:
|
|
|
|
|
+ await wrapper.visit(
|
|
|
|
|
+ "https://example.com",
|
|
|
|
|
+ {"waitUntil": wait_until, "screenshot": False}
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ mock_page.goto.assert_called()
|
|
|
|
|
+ call_kwargs = mock_page.goto.call_args[1]
|
|
|
|
|
+ assert call_kwargs["wait_until"] == wait_until
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
|
+class TestHandleMercadolibre:
|
|
|
|
|
+ """Tests específicos para el método _handle_mercadolibre."""
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.fixture
|
|
|
|
|
+ def wrapper(self):
|
|
|
|
|
+ return CamoufoxWrapper()
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.fixture
|
|
|
|
|
+ def mock_page(self):
|
|
|
|
|
+ page = AsyncMock()
|
|
|
|
|
+ page.content = AsyncMock(return_value="<html><body>Normal</body></html>")
|
|
|
|
|
+ page.wait_for_selector = AsyncMock()
|
|
|
|
|
+ page.evaluate = AsyncMock()
|
|
|
|
|
+ page.click = AsyncMock()
|
|
|
|
|
+ page.reload = AsyncMock()
|
|
|
|
|
+ return page
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.fixture
|
|
|
|
|
+ def mock_browser(self):
|
|
|
|
|
+ browser = AsyncMock()
|
|
|
|
|
+ browser.new_page = AsyncMock(return_value=AsyncMock())
|
|
|
|
|
+ return browser
|
|
|
|
|
+
|
|
|
|
|
+ async def test_handle_mercadolibre_captcha_detected(self, wrapper, mock_page, mock_browser):
|
|
|
|
|
+ """Test que cubre la rama de captcha 'Para continuar'."""
|
|
|
|
|
+ new_page_mock = AsyncMock()
|
|
|
|
|
+ new_page_mock.content = AsyncMock(return_value="<html><body>Después de recargar</body></html>")
|
|
|
|
|
+ mock_browser.new_page = AsyncMock(return_value=new_page_mock)
|
|
|
|
|
+
|
|
|
|
|
+ mock_page.content = AsyncMock(side_effect=[
|
|
|
|
|
+ "<html><body>Para continuar</body></html>",
|
|
|
|
|
+ "<html><body>Final</body></html>",
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ result = await wrapper._handle_mercadolibre(mock_page, "https://mercadolibre.cl", mock_browser)
|
|
|
|
|
+
|
|
|
|
|
+ # Verificar que se creó una nueva página
|
|
|
|
|
+ mock_browser.new_page.assert_called_once()
|
|
|
|
|
+ assert result is not None
|
|
|
|
|
+
|
|
|
|
|
+ async def test_handle_mercadolibre_rate_limit_detected(self, wrapper, mock_page, mock_browser):
|
|
|
|
|
+ """Test que cubre la rama de rate limit 429."""
|
|
|
|
|
+ mock_page.reload = AsyncMock()
|
|
|
|
|
+ mock_page.content = AsyncMock(side_effect=[
|
|
|
|
|
+ "<html><body>Normal</body></html>",
|
|
|
|
|
+ '<html><body>{"message":"local_rate_limited","status":429}</body></html>',
|
|
|
|
|
+ "<html><body>Después de reload</body></html>",
|
|
|
|
|
+ "<html><body>Final</body></html>",
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ result = await wrapper._handle_mercadolibre(mock_page, "https://mercadolibre.cl", mock_browser)
|
|
|
|
|
+
|
|
|
|
|
+ # Verificar que se llamó a reload
|
|
|
|
|
+ mock_page.reload.assert_called_once()
|
|
|
|
|
+ assert result is not None
|
|
|
|
|
+
|
|
|
|
|
+ async def test_handle_mercadolibre_cookie_banner_present(self, wrapper, mock_page, mock_browser):
|
|
|
|
|
+ """Test que cubre la rama de banner de cookies."""
|
|
|
|
|
+ mock_page.content = AsyncMock(return_value="<html><body>Normal</body></html>")
|
|
|
|
|
+ mock_page.wait_for_selector = AsyncMock()
|
|
|
|
|
+ mock_page.evaluate = AsyncMock()
|
|
|
|
|
+
|
|
|
|
|
+ result = await wrapper._handle_mercadolibre(mock_page, "https://mercadolibre.cl", mock_browser)
|
|
|
|
|
+
|
|
|
|
|
+ # Verificar que se llamó a wait_for_selector (al menos una vez)
|
|
|
|
|
+ assert mock_page.wait_for_selector.call_count >= 1
|
|
|
|
|
+ assert result is not None
|
|
|
|
|
+ mock_page.evaluate.assert_called()
|
|
|
|
|
+
|
|
|
|
|
+ async def test_handle_mercadolibre_cookie_banner_not_present(self, wrapper, mock_page, mock_browser):
|
|
|
|
|
+ """Test que cubre la rama donde el banner de cookies no existe."""
|
|
|
|
|
+ mock_page.content.return_value = "<html><body>Normal</body></html>"
|
|
|
|
|
+ mock_page.wait_for_selector.side_effect = Exception("Timeout")
|
|
|
|
|
+
|
|
|
|
|
+ # No debería fallar aunque el banner no exista
|
|
|
|
|
+ result = await wrapper._handle_mercadolibre(mock_page, "https://mercadolibre.cl", mock_browser)
|
|
|
|
|
+ assert result is not None
|
|
|
|
|
+
|
|
|
|
|
+ async def test_handle_mercadolibre_buttons_present(self, wrapper, mock_page, mock_browser):
|
|
|
|
|
+ """Test que cubre la rama de botones de características y descripción."""
|
|
|
|
|
+ mock_page.content.return_value = "<html><body>Normal</body></html>"
|
|
|
|
|
+ mock_page.wait_for_selector.return_value = None
|
|
|
|
|
+ mock_page.click.return_value = None
|
|
|
|
|
+
|
|
|
|
|
+ result = await wrapper._handle_mercadolibre(mock_page, "https://mercadolibre.cl", mock_browser)
|
|
|
|
|
+
|
|
|
|
|
+ # Verificar que se intentó hacer click en los botones
|
|
|
|
|
+ assert mock_page.wait_for_selector.call_count >= 2
|
|
|
|
|
+ assert mock_page.click.call_count >= 2
|
|
|
|
|
+
|
|
|
|
|
+ async def test_handle_mercadolibre_buttons_not_present(self, wrapper, mock_page, mock_browser):
|
|
|
|
|
+ """Test que cubre la rama donde los botones no existen."""
|
|
|
|
|
+ mock_page.content.return_value = "<html><body>Normal</body></html>"
|
|
|
|
|
+ mock_page.wait_for_selector.side_effect = Exception("Timeout")
|
|
|
|
|
+
|
|
|
|
|
+ # No debería fallar aunque los botones no existan
|
|
|
|
|
+ result = await wrapper._handle_mercadolibre(mock_page, "https://mercadolibre.cl", mock_browser)
|
|
|
|
|
+ assert result is not None
|