From a439dd1e3830da3f43e16a83147b4a4837fe2580 Mon Sep 17 00:00:00 2001 From: Julien Maupetit Date: Thu, 18 Jan 2024 22:15:22 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=85(api)=20fix=20httpx=5Fmock=20usage=20i?= =?UTF-8?q?n=20xi=20tools?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit httpx_mock does not mock httpx requests to localhost (due to the non_mocked_hosts) fixture. We need to adapt to it and change the base server URL for xi endpoints we need to mock. --- .../tests/xi/client/test_crud_experience.py | 47 +++++++++---------- .../tests/xi/client/test_crud_relation.py | 44 ++++++++--------- .../tests/xi/indexers/moodle/test_etl.py | 22 +++++++-- src/api/core/warren/xi/client.py | 16 ++----- 4 files changed, 64 insertions(+), 65 deletions(-) diff --git a/src/api/core/warren/tests/xi/client/test_crud_experience.py b/src/api/core/warren/tests/xi/client/test_crud_experience.py index 2c8e5e45..2642072a 100644 --- a/src/api/core/warren/tests/xi/client/test_crud_experience.py +++ b/src/api/core/warren/tests/xi/client/test_crud_experience.py @@ -1,13 +1,11 @@ """Tests for XI experiences client.""" -import re import uuid from unittest.mock import AsyncMock import pytest from httpx import AsyncClient, HTTPError from pydantic.main import BaseModel -from pytest_httpx import HTTPXMock from sqlmodel import Session from warren.xi.client import CRUDExperience @@ -16,57 +14,55 @@ @pytest.mark.anyio -async def test_crud_experience_raise_status( - httpx_mock: HTTPXMock, http_client: AsyncClient -): +async def test_crud_experience_raise_status(http_client: AsyncClient, monkeypatch): """Test that each operation raises an HTTP error in case of failure.""" + monkeypatch.setattr(CRUDExperience, "_base_url", "/api/v1/experiences") crud_instance = CRUDExperience(client=http_client) - # Mock each request to the XI by returning a 422 status - httpx_mock.add_response(url=re.compile(r".*experiences.*"), status_code=422) - class WrongData(BaseModel): name: str # Assert 'create' raises an HTTP error - with pytest.raises(HTTPError): + with pytest.raises(HTTPError, match="422"): await crud_instance.create(data=WrongData(name="foo")) # Assert 'update' raises an HTTP error - with pytest.raises(HTTPError): + with pytest.raises(HTTPError, match="404"): await crud_instance.update(object_id=uuid.uuid4(), data=WrongData(name="foo")) - # Assert 'read' raises an HTTP error - with pytest.raises(HTTPError): - await crud_instance.read() - # Assert 'get' raises an HTTP error - with pytest.raises(HTTPError): + with pytest.raises(HTTPError, match="422"): await crud_instance.get(object_id="foo.") @pytest.mark.anyio -async def test_crud_experience_get_not_found( - httpx_mock: HTTPXMock, http_client: AsyncClient -): +async def test_crud_experience_get_not_found(http_client: AsyncClient, monkeypatch): """Test getting an unknown experience.""" + monkeypatch.setattr(CRUDExperience, "_base_url", "/api/v1/experiences") crud_instance = CRUDExperience(client=http_client) - # Mock GET request to the XI by returning a 404 status - httpx_mock.add_response( - method="GET", url=re.compile(r".*experiences.*"), status_code=404 - ) - # Assert 'get' return 'None' without raising any HTTP errors response = await crud_instance.get(object_id=uuid.uuid4()) assert response is None +@pytest.mark.anyio +async def test_crud_experience_read_empty(http_client: AsyncClient, monkeypatch): + """Test reading experiences when no experience has been saved.""" + monkeypatch.setattr(CRUDExperience, "_base_url", "/api/v1/experiences") + crud_instance = CRUDExperience(client=http_client) + + # Assert 'get' return 'None' without raising any HTTP errors + experiences = await crud_instance.read() + assert experiences == [] + + @pytest.mark.anyio async def test_crud_experience_create_or_update_new( - http_client: AsyncClient, db_session: Session + http_client: AsyncClient, db_session: Session, monkeypatch ): """Test creating an experience using 'create_or_update'.""" + monkeypatch.setattr(CRUDExperience, "_base_url", "/api/v1/experiences") crud_instance = CRUDExperience(client=http_client) # Get random experience data @@ -87,9 +83,10 @@ async def test_crud_experience_create_or_update_new( @pytest.mark.anyio async def test_crud_experience_create_or_update_existing( - http_client: AsyncClient, db_session: Session + http_client: AsyncClient, db_session: Session, monkeypatch ): """Test updating an experience using 'create_or_update'.""" + monkeypatch.setattr(CRUDExperience, "_base_url", "/api/v1/experiences") crud_instance = CRUDExperience(client=http_client) # Get random experience data diff --git a/src/api/core/warren/tests/xi/client/test_crud_relation.py b/src/api/core/warren/tests/xi/client/test_crud_relation.py index ec3d8883..9d361527 100644 --- a/src/api/core/warren/tests/xi/client/test_crud_relation.py +++ b/src/api/core/warren/tests/xi/client/test_crud_relation.py @@ -1,13 +1,11 @@ """Tests for XI relations client.""" -import re from unittest.mock import AsyncMock, call from uuid import uuid4 import pytest from httpx import AsyncClient, HTTPError from pydantic.main import BaseModel -from pytest_httpx import HTTPXMock from sqlmodel import Session from warren.xi.client import CRUDRelation @@ -16,57 +14,55 @@ @pytest.mark.anyio -async def test_crud_relation_raise_status( - httpx_mock: HTTPXMock, http_client: AsyncClient -): +async def test_crud_relation_raise_status(http_client: AsyncClient, monkeypatch): """Test that each operation raises an HTTP error in case of failure.""" + monkeypatch.setattr(CRUDRelation, "_base_url", "/api/v1/relations") crud_instance = CRUDRelation(client=http_client) - # Mock each request to the XI by returning a 422 status - httpx_mock.add_response(url=re.compile(r".*relations.*"), status_code=422) - class WrongData(BaseModel): name: str # Assert 'create' raises an HTTP error - with pytest.raises(HTTPError): + with pytest.raises(HTTPError, match="422"): await crud_instance.create(data=WrongData(name="foo")) # Assert 'update' raises an HTTP error - with pytest.raises(HTTPError): + with pytest.raises(HTTPError, match="404"): await crud_instance.update(object_id=uuid4(), data=WrongData(name="foo")) - # Assert 'read' raises an HTTP error - with pytest.raises(HTTPError): - await crud_instance.read() - # Assert 'get' raises an HTTP error - with pytest.raises(HTTPError): + with pytest.raises(HTTPError, match="422"): await crud_instance.get(object_id="foo.") @pytest.mark.anyio -async def test_crud_relation_get_not_found( - httpx_mock: HTTPXMock, http_client: AsyncClient -): +async def test_crud_relation_get_not_found(http_client: AsyncClient, monkeypatch): """Test getting an unknown relation.""" + monkeypatch.setattr(CRUDRelation, "_base_url", "/api/v1/relations") crud_instance = CRUDRelation(client=http_client) - # Mock GET request to the XI by returning a 404 status - httpx_mock.add_response( - method="GET", url=re.compile(r".*relations.*"), status_code=404 - ) - # Assert 'get' return 'None' without raising any HTTP errors response = await crud_instance.get(object_id=uuid4()) assert response is None +@pytest.mark.anyio +async def test_crud_relation_read_empty(http_client: AsyncClient, monkeypatch): + """Test reading relations when no relation has been saved.""" + monkeypatch.setattr(CRUDRelation, "_base_url", "/api/v1/relations") + crud_instance = CRUDRelation(client=http_client) + + # Assert 'get' return 'None' without raising any HTTP errors + relations = await crud_instance.read() + assert relations == [] + + @pytest.mark.anyio async def test_crud_relation_create_bidirectional( - http_client: AsyncClient, db_session: Session + http_client: AsyncClient, db_session: Session, monkeypatch ): """Test creating bidirectional relations.""" + monkeypatch.setattr(CRUDRelation, "_base_url", "/api/v1/relations") crud_instance = CRUDRelation(client=http_client) # Get two inverse relation types diff --git a/src/api/core/warren/tests/xi/indexers/moodle/test_etl.py b/src/api/core/warren/tests/xi/indexers/moodle/test_etl.py index 9a9badad..67992739 100644 --- a/src/api/core/warren/tests/xi/indexers/moodle/test_etl.py +++ b/src/api/core/warren/tests/xi/indexers/moodle/test_etl.py @@ -32,8 +32,14 @@ async def test_courses_factory(http_client: AsyncClient): @pytest.mark.anyio -async def test_course_content_factory(httpx_mock: HTTPXMock, db_session: Session): +async def test_course_content_factory( + httpx_mock: HTTPXMock, db_session: Session, monkeypatch +): """Test 'CourseContent' factory class method in multiple scenario.""" + # Mock the experience index as localhost cannot be mocked due + # to the non_mock_hosts fixture defined in the video plugin + monkeypatch.setattr(settings, "SERVER_HOST", "mocked-xi") + # Simulate experience not found by mocking the XI response wrong_iri = f"uuid://{uuid4().hex}" httpx_mock.add_response( @@ -255,8 +261,14 @@ def test_course_content_transform(http_client: AsyncClient): @pytest.mark.anyio -async def test_courses_load_errors(httpx_mock: HTTPXMock, http_client: AsyncClient): +async def test_courses_load_errors( + httpx_mock: HTTPXMock, http_client: AsyncClient, monkeypatch +): """Test '_load' method from 'Courses' when encountering errors.""" + # Mock the experience index as localhost cannot be mocked due + # to the non_mock_hosts fixture defined in the video plugin + monkeypatch.setattr(settings, "SERVER_HOST", "mocked-xi") + # Instantiate the 'Courses' indexer indexer = Courses( lms=Moodle(), @@ -286,9 +298,13 @@ async def test_courses_load_errors(httpx_mock: HTTPXMock, http_client: AsyncClie @pytest.mark.anyio async def test_course_content_load_errors( - httpx_mock: HTTPXMock, http_client: AsyncClient + httpx_mock: HTTPXMock, http_client: AsyncClient, monkeypatch ): """Test '_load' method from 'CourseContent' when encountering errors.""" + # Mock the experience index as localhost cannot be mocked due + # to the non_mock_hosts fixture defined in the video plugin + monkeypatch.setattr(settings, "SERVER_HOST", "mocked-xi") + # Generate a random experience experience = ExperienceRead( **ExperienceFactory.build_dict( diff --git a/src/api/core/warren/xi/client.py b/src/api/core/warren/xi/client.py index 6823f504..2f6aca87 100644 --- a/src/api/core/warren/xi/client.py +++ b/src/api/core/warren/xi/client.py @@ -49,6 +49,7 @@ class BaseCRUD(ABC): """ _client: AsyncClient + _base_url: str def __init__(self, client: AsyncClient): """Initialize the base class with an AsyncClient instance.""" @@ -66,11 +67,6 @@ def _construct_url(self, path: Union[str, UUID, IRI]) -> str: """ return join(self._base_url, quote_plus(str(path))) - @property - @abstractmethod - def _base_url(self): - """Abstract method to get the URL for specific entity.""" - @abstractmethod async def create(self, data): """Abstract method for creating an entity.""" @@ -95,10 +91,7 @@ async def delete(self, **kwargs): class CRUDExperience(BaseCRUD): """Handle asynchronous CRUD operations on experiences.""" - @property - def _base_url(self) -> str: - """Provide the base URL for experiences' endpoints.""" - return "experiences" + _base_url: str = "experiences" async def create(self, data: ExperienceCreate) -> UUID: """Create an experience.""" @@ -151,10 +144,7 @@ async def create_or_update(self, data: ExperienceCreate) -> Union[UUID, Experien class CRUDRelation(BaseCRUD): """Handle asynchronous CRUD operations on relations.""" - @property - def _base_url(self) -> str: - """Provide the base URL for relations' endpoints.""" - return "relations" + _base_url: str = "relations" async def create(self, data: RelationCreate) -> UUID: """Create a relation."""