Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crawl statement to feed the Experience Index #167

Merged
merged 8 commits into from
Jan 23, 2024
11 changes: 11 additions & 0 deletions src/api/core/warren/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from datetime import timedelta
from pathlib import Path
from typing import List, Optional, Union
from urllib.parse import urljoin

from pydantic import AnyHttpUrl, BaseModel, BaseSettings

Expand Down Expand Up @@ -66,6 +67,10 @@ class Settings(BaseSettings):
SENTRY_API_TRACES_SAMPLE_RATE: float = 1.0
SENTRY_CLI_TRACES_SAMPLE_RATE: float = 1.0

# Experience Index
MOODLE_BASE_URL: str = "https://moodle.preprod-fun.apps.openfun.fr"
MOODLE_WS_TOKEN: str = "yourWebServicesToken"

@property
def DATABASE_URL(self) -> str:
"""Get the database URL as required by SQLAlchemy."""
Expand All @@ -90,6 +95,12 @@ def SERVER_URL(self) -> str:
"""Get the full server URL."""
return f"{self.SERVER_PROTOCOL}://{self.SERVER_HOST}:{self.SERVER_PORT}"

# pylint: disable=invalid-name
@property
def XI_BASE_URL(self) -> str:
"""Get the Experience Index (XI) base URL."""
return urljoin(self.SERVER_URL, "/api/v1")

class Config:
"""Pydantic Configuration."""

Expand Down
2 changes: 1 addition & 1 deletion src/api/core/warren/tests/fixtures/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
@pytest.fixture(scope="session")
def db_engine():
"""Test database engine fixture."""
engine = create_engine(settings.TEST_DATABASE_URL, echo=True)
engine = create_engine(settings.TEST_DATABASE_URL, echo=False)

# Create database and tables
SQLModel.metadata.create_all(engine)
Expand Down
1 change: 1 addition & 0 deletions src/api/core/warren/tests/xi/client/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Experience Index client tests package."""
73 changes: 73 additions & 0 deletions src/api/core/warren/tests/xi/client/test_base_crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Tests for XI base client."""

from httpx import AsyncClient

from warren.xi.client import BaseCRUD


def test_base_crud_construct_url_with_trailing_slash(http_client: AsyncClient):
"""Test '_construct_url' with a base url which has a trailing slash."""

class Test(BaseCRUD):
@property
def _base_url(self) -> str:
"""Fake base url."""
return "test/"

async def create(self, **kwargs) -> None:
"""Placeholder method."""
raise NotImplementedError
jmaupetit marked this conversation as resolved.
Show resolved Hide resolved

async def delete(self, **kwargs) -> None:
"""Placeholder method."""
raise NotImplementedError

async def get(self, **kwargs) -> None:
"""Placeholder method."""
raise NotImplementedError

async def read(self, **kwargs) -> None:
"""Placeholder method."""
raise NotImplementedError

async def update(self, **kwargs) -> None:
"""Placeholder method."""
raise NotImplementedError

assert (
Test(client=http_client)._construct_url("uuid://123") == "test/uuid%3A%2F%2F123"
)


def test_base_crud_construct_url_without_trailing_slash(http_client: AsyncClient):
"""Test '_construct_url' with a base url which has no trailing slash."""

class Test(BaseCRUD):
@property
def _base_url(self) -> str:
"""Fake base url."""
return "test"

async def create(self, **kwargs) -> None:
"""Placeholder method."""
raise NotImplementedError

async def delete(self, **kwargs) -> None:
"""Placeholder method."""
raise NotImplementedError

async def get(self, **kwargs) -> None:
"""Placeholder method."""
raise NotImplementedError

async def read(self, **kwargs) -> None:
"""Placeholder method."""
raise NotImplementedError

async def update(self, **kwargs) -> None:
"""Placeholder method."""
raise NotImplementedError

assert (
Test(client=http_client)._construct_url("uuid://123") == "test/uuid%3A%2F%2F123"
)
105 changes: 105 additions & 0 deletions src/api/core/warren/tests/xi/client/test_crud_experience.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""Tests for XI experiences client."""

import uuid
from unittest.mock import AsyncMock

import pytest
from httpx import AsyncClient, HTTPError
from pydantic.main import BaseModel
from sqlmodel import Session

from warren.xi.client import CRUDExperience
from warren.xi.factories import ExperienceFactory
from warren.xi.models import ExperienceCreate, ExperienceRead


@pytest.mark.anyio
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)

class WrongData(BaseModel):
name: str

# Assert 'create' raises an HTTP error
with pytest.raises(HTTPError, match="422"):
await crud_instance.create(data=WrongData(name="foo"))

# Assert 'update' raises an HTTP error
with pytest.raises(HTTPError, match="404"):
await crud_instance.update(object_id=uuid.uuid4(), data=WrongData(name="foo"))

# Assert 'get' raises an HTTP error
with pytest.raises(HTTPError, match="422"):
await crud_instance.get(object_id="foo.")


@pytest.mark.anyio
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)

# 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, 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
data = ExperienceFactory.build_dict()

# Simulate a 'Not Found' experience by mocking the 'get' method
crud_instance.get = AsyncMock(return_value=None)

crud_instance.create = AsyncMock()
crud_instance.update = AsyncMock()

# Attempt creating an experience
await crud_instance.create_or_update(ExperienceCreate(**data))

crud_instance.create.assert_awaited_once()
crud_instance.update.assert_not_awaited()


@pytest.mark.anyio
async def test_crud_experience_create_or_update_existing(
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
data = ExperienceFactory.build_dict(exclude={})

# Simulate an existing experience by mocking the 'get' method
crud_instance.get = AsyncMock(return_value=ExperienceRead(**data))

crud_instance.create = AsyncMock()
crud_instance.update = AsyncMock()

# Attempt updating an experience
await crud_instance.create_or_update(ExperienceCreate(**data))

crud_instance.create.assert_not_awaited()
crud_instance.update.assert_awaited_once()
102 changes: 102 additions & 0 deletions src/api/core/warren/tests/xi/client/test_crud_relation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""Tests for XI relations client."""

from unittest.mock import AsyncMock, call
from uuid import uuid4

import pytest
from httpx import AsyncClient, HTTPError
from pydantic.main import BaseModel
from sqlmodel import Session

from warren.xi.client import CRUDRelation
from warren.xi.enums import RelationType
from warren.xi.models import RelationCreate


@pytest.mark.anyio
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)

class WrongData(BaseModel):
name: str

# Assert 'create' raises an HTTP error
with pytest.raises(HTTPError, match="422"):
await crud_instance.create(data=WrongData(name="foo"))

# Assert 'update' raises an HTTP error
with pytest.raises(HTTPError, match="404"):
await crud_instance.update(object_id=uuid4(), data=WrongData(name="foo"))

# Assert 'get' raises an HTTP error
with pytest.raises(HTTPError, match="422"):
await crud_instance.get(object_id="foo.")


@pytest.mark.anyio
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)

# 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, monkeypatch
):
"""Test creating bidirectional relations."""
monkeypatch.setattr(CRUDRelation, "_base_url", "/api/v1/relations")
crud_instance = CRUDRelation(client=http_client)

# Get two inverse relation types
relation_type = RelationType.HASPART
inverted_relation_type = RelationType.ISPARTOF

# Get two random UUIDs
source_id = uuid4()
target_id = uuid4()

# Mock 'create' method by returning a random UUID
crud_instance.create = AsyncMock(return_value=uuid4())

# Attempt creating bidirectional relations
_ = await crud_instance.create_bidirectional(
source_id=source_id,
target_id=target_id,
kinds=[relation_type, inverted_relation_type],
)

# Assert 'create' has been called twice, with inverted arguments
crud_instance.create.assert_has_awaits(
[
call(
RelationCreate(
source_id=source_id, target_id=target_id, kind=relation_type
)
),
call(
RelationCreate(
source_id=target_id,
target_id=source_id,
kind=inverted_relation_type,
)
),
]
)
1 change: 1 addition & 0 deletions src/api/core/warren/tests/xi/indexers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Experience Index indexers tests package."""
1 change: 1 addition & 0 deletions src/api/core/warren/tests/xi/indexers/moodle/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Experience Index Moodle tests package."""
Loading