test_api_controller.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. """
  2. Tests unitarios del endpoint /visit con mocks para no depender de Camoufox.
  3. Importamos app.main dentro del contexto del patch para asegurar que todos
  4. los módulos se carguen con AsyncCamoufox mockeado.
  5. """
  6. from contextlib import contextmanager
  7. from unittest.mock import AsyncMock, patch
  8. @contextmanager
  9. def mocked_client(mock_response=None):
  10. """Context manager que crea un TestClient con AsyncCamoufox mockeado.
  11. Importa app.main dentro del contexto del patch."""
  12. if mock_response is None:
  13. mock_response = {"html": "<html>Example</html>", "screenshot": None}
  14. with patch("app.use_cases.camoufox.wrapper.AsyncCamoufox") as MockClass:
  15. mock_browser = AsyncMock()
  16. mock_page = AsyncMock()
  17. mock_page.content = AsyncMock(return_value=mock_response["html"])
  18. mock_page.screenshot = AsyncMock(
  19. return_value=b"fake_screenshot_bytes" if mock_response.get("screenshot") else None
  20. )
  21. mock_page.set_extra_http_headers = AsyncMock()
  22. mock_page.goto = AsyncMock()
  23. mock_page.wait_for_load_state = AsyncMock()
  24. mock_page.set_default_navigation_timeout = AsyncMock()
  25. mock_page.wait_for_selector = AsyncMock()
  26. mock_page.query_selector = AsyncMock(return_value=None)
  27. mock_page.click = AsyncMock()
  28. mock_browser.new_page = AsyncMock(return_value=mock_page)
  29. MockClass.return_value.__aenter__ = AsyncMock(return_value=mock_browser)
  30. MockClass.return_value.__aexit__ = AsyncMock(return_value=False)
  31. # Importar app DENTRO del patch para que se cargue con AsyncCamoufox mockeado
  32. from app.main import app
  33. from fastapi.testclient import TestClient
  34. yield TestClient(app)
  35. def test_visit_missing_url():
  36. from app.main import app
  37. from fastapi.testclient import TestClient
  38. client = TestClient(app)
  39. response = client.post("/api/v1/visit", json={})
  40. assert response.status_code == 422
  41. class TestVisitEndpoint:
  42. """Tests que mockean AsyncCamoufox para evitar llamadas reales al browser."""
  43. def test_visit_invalid_url(self):
  44. with mocked_client() as client:
  45. response = client.post("/api/v1/visit", json={"url": "not-a-url"})
  46. assert response.status_code == 422
  47. def test_visit_valid_url(self):
  48. with mocked_client() as client:
  49. response = client.post(
  50. "/api/v1/visit",
  51. json={"url": "https://example.com", "screenshot": False},
  52. )
  53. assert response.status_code == 200
  54. body = response.json()
  55. assert body["status"] == "OK"
  56. assert "html" in body["data"]
  57. def test_visit_with_screenshot(self):
  58. with mocked_client(
  59. {"html": "<html>Example</html>", "screenshot": "base64data"}
  60. ) as client:
  61. response = client.post(
  62. "/api/v1/visit",
  63. json={"url": "https://example.com", "screenshot": True},
  64. )
  65. assert response.status_code == 200
  66. body = response.json()
  67. assert body["status"] == "OK"
  68. assert "screenshot" in body["data"]
  69. def test_visit_with_screenshot_selector(self):
  70. with mocked_client() as client:
  71. response = client.post(
  72. "/api/v1/visit",
  73. json={
  74. "url": "https://example.com",
  75. "screenshotSelector": "#product",
  76. "selectorTimeout": 2000,
  77. },
  78. )
  79. assert response.status_code == 200
  80. def test_visit_with_click_selector(self):
  81. with mocked_client() as client:
  82. response = client.post(
  83. "/api/v1/visit",
  84. json={
  85. "url": "https://example.com",
  86. "clickSelector": "button#load-more",
  87. "screenshotDelay": 500,
  88. },
  89. )
  90. assert response.status_code == 200
  91. def test_visit_with_invalid_selector_timeout(self):
  92. with mocked_client() as client:
  93. response = client.post(
  94. "/api/v1/visit",
  95. json={"url": "https://example.com", "selectorTimeout": 50},
  96. )
  97. assert response.status_code == 422
  98. def test_visit_with_invalid_waituntil(self):
  99. with mocked_client() as client:
  100. response = client.post(
  101. "/api/v1/visit",
  102. json={"url": "https://example.com", "waitUntil": "invalid"},
  103. )
  104. assert response.status_code == 422
  105. def test_visit_with_valid_waituntil(self):
  106. with mocked_client() as client:
  107. for wait_until in ["load", "domcontentloaded", "networkidle", "commit"]:
  108. response = client.post(
  109. "/api/v1/visit",
  110. json={"url": "https://example.com", "waitUntil": wait_until},
  111. )
  112. assert response.status_code == 200
  113. def test_visit_response_format(self):
  114. with mocked_client() as client:
  115. response = client.post(
  116. "/api/v1/visit",
  117. json={"url": "https://example.com"},
  118. )
  119. body = response.json()
  120. assert "status" in body
  121. assert "message" in body
  122. assert "data" in body
  123. assert "code" in body
  124. assert body["code"] == "UNKNOWN_CODE"
  125. def test_visit_with_wait_time_valid(self):
  126. with mocked_client() as client:
  127. response = client.post(
  128. "/api/v1/visit",
  129. json={"url": "https://example.com", "waitTime": 10},
  130. )
  131. assert response.status_code == 200
  132. def test_visit_with_wait_time_invalid_negative(self):
  133. with mocked_client() as client:
  134. response = client.post(
  135. "/api/v1/visit",
  136. json={"url": "https://example.com", "waitTime": -5},
  137. )
  138. assert response.status_code == 422
  139. def test_visit_with_wait_time_invalid_above_max(self):
  140. with mocked_client() as client:
  141. response = client.post(
  142. "/api/v1/visit",
  143. json={"url": "https://example.com", "waitTime": 120},
  144. )
  145. assert response.status_code == 422
  146. class TestVisitEndpointController:
  147. """Tests que verifican el controller directamente con VisitUseCase mockeado."""
  148. def test_controller_visit_uses_visit_use_case(self):
  149. """Test que cubre la llamada a use_case.execute."""
  150. from app.controllers.api_controller import ApiController
  151. from app.models.schemas import VisitRequest
  152. from fastapi import Request
  153. from unittest.mock import MagicMock
  154. controller = ApiController()
  155. mock_request = MagicMock(spec=Request)
  156. mock_request.scope = {"raw_headers_dict": {"user-agent": "test"}}
  157. with patch.object(controller, "visit_use_case") as mock_use_case:
  158. async def async_execute(*args, **kwargs):
  159. return {"html": "<html>Controller</html>", "screenshot": None}
  160. mock_use_case.execute = async_execute
  161. import asyncio
  162. result = asyncio.run(
  163. controller.visit(mock_request, VisitRequest(url="https://example.com"))
  164. )
  165. assert result["status"] == "OK"
  166. assert result["data"]["html"] == "<html>Controller</html>"
  167. def test_controller_visit_error_handling(self):
  168. """Test que cubre el manejo de errores."""
  169. from app.controllers.api_controller import ApiController
  170. from app.models.schemas import VisitRequest
  171. from fastapi import Request
  172. from unittest.mock import MagicMock
  173. controller = ApiController()
  174. mock_request = MagicMock(spec=Request)
  175. mock_request.scope = {"raw_headers_dict": {"user-agent": "test"}}
  176. with patch.object(controller, "visit_use_case") as mock_use_case:
  177. async def async_execute(*args, **kwargs):
  178. raise Exception("Test error")
  179. mock_use_case.execute = async_execute
  180. import asyncio
  181. result = asyncio.run(
  182. controller.visit(mock_request, VisitRequest(url="https://example.com"))
  183. )
  184. assert result["status"] == "NO_OK"
  185. assert "Test error" in result["message"]