""" 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="Test") 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"] == "Test" @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 = "MeLi Hackeado" 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"] == "MeLi Hackeado" @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"] == "Test" @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="Normal") 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="Después de recargar") mock_browser.new_page = AsyncMock(return_value=new_page_mock) mock_page.content = AsyncMock(side_effect=[ "Para continuar", "Final", ]) 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=[ "Normal", '{"message":"local_rate_limited","status":429}', "Después de reload", "Final", ]) 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="Normal") 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 = "Normal" 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 = "Normal" 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 = "Normal" 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