Skip to content

Commit

Permalink
✅(api) fix httpx_mock usage in xi tools
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jmaupetit committed Jan 23, 2024
1 parent 0fc739f commit a439dd1
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 65 deletions.
47 changes: 22 additions & 25 deletions src/api/core/warren/tests/xi/client/test_crud_experience.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
44 changes: 20 additions & 24 deletions src/api/core/warren/tests/xi/client/test_crud_relation.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
22 changes: 19 additions & 3 deletions src/api/core/warren/tests/xi/indexers/moodle/test_etl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(
Expand Down
16 changes: 3 additions & 13 deletions src/api/core/warren/xi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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."""
Expand All @@ -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."""
Expand Down Expand Up @@ -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."""
Expand Down

0 comments on commit a439dd1

Please sign in to comment.