| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- """
- 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
|