diff --git a/app/api/api_v1/routers/__init__.py b/app/api/api_v1/routers/__init__.py index 94c9f748..b519f0a5 100644 --- a/app/api/api_v1/routers/__init__.py +++ b/app/api/api_v1/routers/__init__.py @@ -4,6 +4,7 @@ from app.api.api_v1.routers.collection import collections_router from app.api.api_v1.routers.config import config_router from app.api.api_v1.routers.corpus import corpora_router +from app.api.api_v1.routers.corpus_type import corpus_types_router from app.api.api_v1.routers.document import document_router from app.api.api_v1.routers.event import event_router from app.api.api_v1.routers.family import families_router @@ -12,6 +13,7 @@ "analytics_router", "auth_router", "corpora_router", + "corpus_types_router", "collections_router", "config_router", "document_router", diff --git a/app/api/api_v1/routers/corpus_type.py b/app/api/api_v1/routers/corpus_type.py new file mode 100644 index 00000000..786af28d --- /dev/null +++ b/app/api/api_v1/routers/corpus_type.py @@ -0,0 +1,58 @@ +import logging + +from fastapi import APIRouter, HTTPException, Request, status + +from app.errors import RepositoryError, ValidationError +from app.model.corpus_type import CorpusTypeReadDTO +from app.service import corpus_type as corpus_type_service + +corpus_types_router = APIRouter() + +_LOGGER = logging.getLogger(__name__) + + +@corpus_types_router.get( + "/corpus-types", + response_model=list[CorpusTypeReadDTO], +) +async def get_all_corpus_types(request: Request) -> list[CorpusTypeReadDTO]: + """Retrieve all corpus types. + + :param Request request: Request object. + :raises HTTPException: If the corpus type is not found. + :return CorpusTypeReadDTO: The requested corpus type. + """ + try: + return corpus_type_service.all(request.state.user) + except RepositoryError as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=e.message + ) + + +@corpus_types_router.get( + "/corpus-types/{corpus_type_name}", + response_model=CorpusTypeReadDTO, +) +async def get_corpus_type(corpus_type_name: str) -> CorpusTypeReadDTO: + """Retrieve a specific corpus type by its name. + + :param str corpus_type_name: The ID of the corpus type to retrieve. + :raises HTTPException: If the corpus type is not found. + :return CorpusTypeReadDTO: The requested corpus type. + """ + try: + corpus_type = corpus_type_service.get(corpus_type_name) + except ValidationError as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=e.message) + except RepositoryError as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=e.message + ) + + if corpus_type is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Corpus type not found: {corpus_type_name}", + ) + return corpus_type diff --git a/app/main.py b/app/main.py index 46bd5f1d..b46617e5 100644 --- a/app/main.py +++ b/app/main.py @@ -21,6 +21,7 @@ collections_router, config_router, corpora_router, + corpus_types_router, document_router, event_router, families_router, @@ -111,6 +112,14 @@ async def lifespan(app_: FastAPI): tags=["corpora"], dependencies=[Depends(check_user_auth)], ) + +app.include_router( + corpus_types_router, + prefix="/api/v1", + tags=["corpus-types"], + dependencies=[Depends(check_user_auth)], +) + # Add CORS middleware to allow cross origin requests from any port app.add_middleware( CORSMiddleware, diff --git a/app/model/authorisation.py b/app/model/authorisation.py index 5d542694..6d117c35 100644 --- a/app/model/authorisation.py +++ b/app/model/authorisation.py @@ -20,6 +20,7 @@ class AuthEndpoint(str, enum.Enum): EVENT = "EVENTS" BULK_IMPORT = "BULK-IMPORT" CORPUS = "CORPORA" + CORPUS_TYPE = "CORPUS-TYPES" AuthMap = Mapping[AuthEndpoint, Mapping[AuthOperation, AuthAccess]] @@ -72,4 +73,8 @@ class AuthEndpoint(str, enum.Enum): AuthOperation.READ: AuthAccess.SUPER, AuthOperation.UPDATE: AuthAccess.SUPER, }, + # Corpus Type + AuthEndpoint.CORPUS_TYPE: { + AuthOperation.READ: AuthAccess.SUPER, + }, } diff --git a/app/model/corpus_type.py b/app/model/corpus_type.py new file mode 100644 index 00000000..f199ae6f --- /dev/null +++ b/app/model/corpus_type.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel + +from app.model.general import Json + + +class CorpusTypeReadDTO(BaseModel): + """Representation of a Corpus Type.""" + + name: str + description: str + metadata: Json diff --git a/app/repository/__init__.py b/app/repository/__init__.py index 2dbf079c..d495bd03 100644 --- a/app/repository/__init__.py +++ b/app/repository/__init__.py @@ -3,6 +3,7 @@ import app.repository.collection as collection_repo import app.repository.config as config_repo import app.repository.corpus as corpus_repo +import app.repository.corpus_type as corpus_type_repo import app.repository.document as document_repo import app.repository.event as event_repo import app.repository.family as family_repo # type: ignore @@ -18,6 +19,7 @@ "collection_repo", "config_repo", "corpus_repo", + "corpus_type_repo", "document_repo", "event_repo", "family_repo", diff --git a/app/repository/corpus_type.py b/app/repository/corpus_type.py new file mode 100644 index 00000000..c64ab02c --- /dev/null +++ b/app/repository/corpus_type.py @@ -0,0 +1,66 @@ +import logging +from typing import Optional, cast + +from db_client.models.organisation import CorpusType, Organisation +from sqlalchemy import asc +from sqlalchemy.exc import MultipleResultsFound +from sqlalchemy.orm import Session + +from app.errors import RepositoryError +from app.model.corpus_type import CorpusTypeReadDTO + +_LOGGER = logging.getLogger(__name__) + + +def _corpus_type_to_dto(corpus_type: CorpusType) -> CorpusTypeReadDTO: + """Convert a CorpusType model to a CorpusTypeReadDTO. + + :param CorpusType corpus_type: The corpus type model. + :return CorpusTypeReadDTO: The corresponding DTO. + """ + return CorpusTypeReadDTO( + name=str(corpus_type.name), + description=str(corpus_type.description), + metadata=cast(dict, corpus_type.valid_metadata), + ) + + +def all(db: Session, org_id: Optional[int]) -> list[CorpusTypeReadDTO]: + """Get a list of all corpus types in the database. + + :param db Session: The database connection. + :param org_id int: the ID of the organisation the user belongs to + :return CorpusTypeReadDTO: The requested corpus type. + :raises RepositoryError: If the corpus type is not found. + """ + query = db.query(CorpusType) + if org_id is not None: + query = query.filter(Organisation.id == org_id) + + result = query.order_by(asc(CorpusType.name)).all() + + if not result: + return [] + + return [_corpus_type_to_dto(corpus_type) for corpus_type in result] + + +def get(db: Session, corpus_type_name: str) -> Optional[CorpusTypeReadDTO]: + """Get a corpus type from the database given a name. + + :param db Session: The database connection. + :param str corpus_type_name: The ID of the corpus type to retrieve. + :return CorpusTypeReadDTO: The requested corpus type. + :raises RepositoryError: If the corpus type is not found. + """ + try: + corpus_type = ( + db.query(CorpusType) + .filter(CorpusType.name == corpus_type_name) + .one_or_none() + ) + return _corpus_type_to_dto(corpus_type) if corpus_type is not None else None + + except MultipleResultsFound as e: + _LOGGER.error(e) + raise RepositoryError(e) diff --git a/app/service/authorisation.py b/app/service/authorisation.py index f8eef0b4..cffa3bdc 100644 --- a/app/service/authorisation.py +++ b/app/service/authorisation.py @@ -71,7 +71,7 @@ def is_authorised(user: UserContext, entity: AuthEndpoint, op: AuthOperation) -> return raise AuthorisationError( - f"User {user.email} is not authorised to {op} {_get_article(entity.value)} {entity}" + f"User {user.email} is not authorised to {op} {_get_article(entity.value)} {entity.name}" ) diff --git a/app/service/corpus_type.py b/app/service/corpus_type.py new file mode 100644 index 00000000..eed4ee1a --- /dev/null +++ b/app/service/corpus_type.py @@ -0,0 +1,42 @@ +import logging +from typing import Optional + +from pydantic import ConfigDict, validate_call +from sqlalchemy.orm import Session + +import app.clients.db.session as db_session +from app.model.corpus_type import CorpusTypeReadDTO +from app.model.user import UserContext +from app.repository import corpus_type as corpus_type_repo +from app.service import app_user + +_LOGGER = logging.getLogger(__name__) + + +@validate_call(config=ConfigDict(arbitrary_types_allowed=True)) +def all(user: UserContext) -> list[CorpusTypeReadDTO]: + """ + Gets the entire list of corpora from the repository. + + :param UserContext user: The current user context. + :return list[CorpusReadDTO]: The list of corpora. + """ + with db_session.get_db() as db: + org_id = app_user.restrict_entities_to_user_org(user) + return corpus_type_repo.all(db, org_id) + + +@validate_call(config=ConfigDict(arbitrary_types_allowed=True)) +def get( + corpus_type_name: str, db: Optional[Session] = None +) -> Optional[CorpusTypeReadDTO]: + """Retrieve a corpus type by ID. + + :param str corpus_type_name: The name of the corpus type to retrieve. + :return CorpusTypeReadDTO: The requested corpus type. + :raises RepositoryError: If there is an error during retrieval. + """ + if db is None: + db = db_session.get_db() + + return corpus_type_repo.get(db, corpus_type_name) diff --git a/pyproject.toml b/pyproject.toml index 8936f0e1..4eb8c18d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "admin_backend" -version = "2.17.24" +version = "2.17.25" description = "" authors = ["CPR-dev-team "] packages = [{ include = "app" }, { include = "tests" }] @@ -51,7 +51,7 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] -addopts = "-p no:cacheprovider" +addopts = "--import-mode=importlib" env_files = """ .env.test .env diff --git a/tests/integration_tests/bulk_import/test_bulk_import.py b/tests/integration_tests/bulk_import/test_bulk_import.py index c129001b..20589e0a 100644 --- a/tests/integration_tests/bulk_import/test_bulk_import.py +++ b/tests/integration_tests/bulk_import/test_bulk_import.py @@ -402,7 +402,7 @@ def test_bulk_import_admin_non_super( assert response.status_code == status.HTTP_403_FORBIDDEN data = response.json() assert ( - data["detail"] == "User admin@cpr.org is not authorised to CREATE a BULK-IMPORT" + data["detail"] == "User admin@cpr.org is not authorised to CREATE a BULK_IMPORT" ) @@ -416,5 +416,5 @@ def test_bulk_import_non_super_non_admin( assert response.status_code == status.HTTP_403_FORBIDDEN data = response.json() assert ( - data["detail"] == "User cclw@cpr.org is not authorised to CREATE a BULK-IMPORT" + data["detail"] == "User cclw@cpr.org is not authorised to CREATE a BULK_IMPORT" ) diff --git a/tests/integration_tests/bulk_import/test_bulk_import_template.py b/tests/integration_tests/bulk_import/test_bulk_import_template.py index f31d5092..4e41c751 100644 --- a/tests/integration_tests/bulk_import/test_bulk_import_template.py +++ b/tests/integration_tests/bulk_import/test_bulk_import_template.py @@ -237,7 +237,7 @@ def test_get_template_admin_non_super( assert response.status_code == status.HTTP_403_FORBIDDEN data = response.json() assert ( - data["detail"] == "User admin@cpr.org is not authorised to READ a BULK-IMPORT" + data["detail"] == "User admin@cpr.org is not authorised to READ a BULK_IMPORT" ) @@ -250,4 +250,4 @@ def test_get_template_non_admin_non_super( ) assert response.status_code == status.HTTP_403_FORBIDDEN data = response.json() - assert data["detail"] == "User cclw@cpr.org is not authorised to READ a BULK-IMPORT" + assert data["detail"] == "User cclw@cpr.org is not authorised to READ a BULK_IMPORT" diff --git a/tests/integration_tests/corpus/test_all.py b/tests/integration_tests/corpus/test_all.py index e86cfe8c..9ed1b250 100644 --- a/tests/integration_tests/corpus/test_all.py +++ b/tests/integration_tests/corpus/test_all.py @@ -48,7 +48,7 @@ def test_get_all_corpora_non_super( ) assert response.status_code == status.HTTP_403_FORBIDDEN data = response.json() - assert data["detail"] == "User cclw@cpr.org is not authorised to READ a CORPORA" + assert data["detail"] == "User cclw@cpr.org is not authorised to READ a CORPUS" def test_get_all_corpora_when_not_authenticated(client: TestClient, data_db: Session): diff --git a/tests/integration_tests/corpus/test_create.py b/tests/integration_tests/corpus/test_create.py index 92c0fc78..2d34b5ac 100644 --- a/tests/integration_tests/corpus/test_create.py +++ b/tests/integration_tests/corpus/test_create.py @@ -164,7 +164,7 @@ def test_create_corpus_non_admin_non_super(client: TestClient, user_header_token ) assert response.status_code == status.HTTP_403_FORBIDDEN data = response.json() - assert data["detail"] == "User cclw@cpr.org is not authorised to CREATE a CORPORA" + assert data["detail"] == "User cclw@cpr.org is not authorised to CREATE a CORPUS" def test_create_corpus_admin_non_super(client: TestClient, admin_user_header_token): @@ -174,7 +174,7 @@ def test_create_corpus_admin_non_super(client: TestClient, admin_user_header_tok ) assert response.status_code == status.HTTP_403_FORBIDDEN data = response.json() - assert data["detail"] == "User admin@cpr.org is not authorised to CREATE a CORPORA" + assert data["detail"] == "User admin@cpr.org is not authorised to CREATE a CORPUS" def test_create_corpus_rollback( diff --git a/tests/integration_tests/corpus/test_get.py b/tests/integration_tests/corpus/test_get.py index 50955bba..5732343b 100644 --- a/tests/integration_tests/corpus/test_get.py +++ b/tests/integration_tests/corpus/test_get.py @@ -43,7 +43,7 @@ def test_get_corpus_non_super(client: TestClient, data_db: Session, user_header_ ) assert response.status_code == status.HTTP_403_FORBIDDEN data = response.json() - assert data["detail"] == "User cclw@cpr.org is not authorised to READ a CORPORA" + assert data["detail"] == "User cclw@cpr.org is not authorised to READ a CORPUS" def test_get_corpus_when_not_authenticated(client: TestClient, data_db: Session): diff --git a/tests/integration_tests/corpus/test_search.py b/tests/integration_tests/corpus/test_search.py index 1f441c6b..c9a4d431 100644 --- a/tests/integration_tests/corpus/test_search.py +++ b/tests/integration_tests/corpus/test_search.py @@ -55,7 +55,7 @@ def test_search_corpus_non_super( ) assert response.status_code == status.HTTP_403_FORBIDDEN data = response.json() - assert data["detail"] == "User cclw@cpr.org is not authorised to READ a CORPORA" + assert data["detail"] == "User cclw@cpr.org is not authorised to READ a CORPUS" def test_search_corpus_when_not_authorised(client: TestClient, data_db: Session): diff --git a/tests/integration_tests/corpus/test_update.py b/tests/integration_tests/corpus/test_update.py index d16e885f..a94c23a5 100644 --- a/tests/integration_tests/corpus/test_update.py +++ b/tests/integration_tests/corpus/test_update.py @@ -165,7 +165,7 @@ def test_update_corpus_non_super_non_admin(client: TestClient, user_header_token ) assert response.status_code == status.HTTP_403_FORBIDDEN data = response.json() - assert data["detail"] == "User cclw@cpr.org is not authorised to UPDATE a CORPORA" + assert data["detail"] == "User cclw@cpr.org is not authorised to UPDATE a CORPUS" def test_update_corpus_non_super_admin(client: TestClient, admin_user_header_token): @@ -177,7 +177,7 @@ def test_update_corpus_non_super_admin(client: TestClient, admin_user_header_tok ) assert response.status_code == status.HTTP_403_FORBIDDEN data = response.json() - assert data["detail"] == "User admin@cpr.org is not authorised to UPDATE a CORPORA" + assert data["detail"] == "User admin@cpr.org is not authorised to UPDATE a CORPUS" def test_update_corpus_idempotent( diff --git a/tests/integration_tests/corpus_type/test_all.py b/tests/integration_tests/corpus_type/test_all.py new file mode 100644 index 00000000..05ea0777 --- /dev/null +++ b/tests/integration_tests/corpus_type/test_all.py @@ -0,0 +1,73 @@ +from fastapi import status +from fastapi.testclient import TestClient +from sqlalchemy.orm import Session + +from tests.integration_tests.setup_db import ( + EXPECTED_CCLW_CORPUS, + EXPECTED_NUM_CORPORA, + EXPECTED_UNFCCC_CORPUS, + setup_db, +) + + +def test_get_all_corpus_types( + client: TestClient, data_db: Session, superuser_header_token +): + setup_db(data_db) + response = client.get( + "/api/v1/corpus-types", + headers=superuser_header_token, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json() + assert isinstance(data, list) + assert len(data) > 0 # Ensure there are corpus types returned + + assert len(data) == EXPECTED_NUM_CORPORA + for item in data: + assert isinstance(item, dict) + assert all(key in ["name", "description", "metadata"] for key in item) + + # Check Laws and Policies content. + laws_and_policies_ct = data[1] + assert laws_and_policies_ct["name"] == EXPECTED_CCLW_CORPUS["corpus_type_name"] + assert ( + laws_and_policies_ct["description"] + == EXPECTED_CCLW_CORPUS["corpus_type_description"] + ) + assert laws_and_policies_ct["metadata"] is not None + assert isinstance(laws_and_policies_ct["metadata"], dict) + + # Check Intl. Agreements content. + int_agreements_ct = data[0] + assert int_agreements_ct["name"] == EXPECTED_UNFCCC_CORPUS["corpus_type_name"] + assert ( + int_agreements_ct["description"] + == EXPECTED_UNFCCC_CORPUS["corpus_type_description"] + ) + + assert int_agreements_ct["metadata"] is not None + assert isinstance(int_agreements_ct["metadata"], dict) + + +def test_get_all_corpus_types_non_super( + client: TestClient, data_db: Session, user_header_token +): + setup_db(data_db) + response = client.get( + "/api/v1/corpus-types", + headers=user_header_token, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + data = response.json() + assert data["detail"] == "User cclw@cpr.org is not authorised to READ a CORPUS_TYPE" + + +def test_get_all_corpus_types_when_not_authenticated( + client: TestClient, data_db: Session +): + setup_db(data_db) + response = client.get( + "/api/v1/corpus-types", + ) + assert response.status_code == status.HTTP_401_UNAUTHORIZED diff --git a/tests/integration_tests/corpus_type/test_get.py b/tests/integration_tests/corpus_type/test_get.py new file mode 100644 index 00000000..729b7399 --- /dev/null +++ b/tests/integration_tests/corpus_type/test_get.py @@ -0,0 +1,68 @@ +import pytest +from fastapi import status +from fastapi.testclient import TestClient +from sqlalchemy.orm import Session + +from tests.integration_tests.setup_db import ( + EXPECTED_CCLW_CORPUS, + EXPECTED_UNFCCC_CORPUS, + setup_db, +) + +EXPECTED_CORPUS_TYPE_KEYS = ["name", "description", "metadata"] + + +@pytest.mark.parametrize( + "expected_corpus_type", + [EXPECTED_CCLW_CORPUS, EXPECTED_UNFCCC_CORPUS], +) +def test_get_corpus_type( + client: TestClient, data_db: Session, superuser_header_token, expected_corpus_type +): + setup_db(data_db) + response = client.get( + f"/api/v1/corpus-types/{expected_corpus_type['corpus_type_name']}", + headers=superuser_header_token, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json() + + assert set(EXPECTED_CORPUS_TYPE_KEYS) == set(data.keys()) + + assert data["name"] == expected_corpus_type["corpus_type_name"] + assert data["description"] == expected_corpus_type["corpus_type_description"] + assert data["metadata"] is not None + assert isinstance(data["metadata"], dict) + + +def test_get_corpus_type_non_super( + client: TestClient, data_db: Session, user_header_token +): + setup_db(data_db) + response = client.get( + "/api/v1/corpus-types/Laws and Policies", headers=user_header_token + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + data = response.json() + assert data["detail"] == "User cclw@cpr.org is not authorised to READ a CORPUS_TYPE" + + +def test_get_corpus_type_when_not_authenticated(client: TestClient, data_db: Session): + setup_db(data_db) + response = client.get( + "/api/v1/corpus-types/Laws and Policies", + ) + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + +def test_get_corpus_type_when_not_found( + client: TestClient, data_db: Session, superuser_header_token +): + setup_db(data_db) + response = client.get( + "/api/v1/corpus-types/NonExistentType", + headers=superuser_header_token, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + data = response.json() + assert data["detail"] == "Corpus type not found: NonExistentType" diff --git a/tests/mocks/repos/corpus_type_repo.py b/tests/mocks/repos/corpus_type_repo.py new file mode 100644 index 00000000..6050508c --- /dev/null +++ b/tests/mocks/repos/corpus_type_repo.py @@ -0,0 +1,42 @@ +from typing import Optional + +from pytest import MonkeyPatch + +from app.errors import RepositoryError +from app.model.corpus_type import CorpusTypeReadDTO + + +def mock_corpus_type_repo(corpus_type_repo, monkeypatch: MonkeyPatch, mocker): + corpus_type_repo.valid = True + corpus_type_repo.return_empty = False + corpus_type_repo.throw_repository_error = False + + def maybe_throw(): + if corpus_type_repo.throw_repository_error: + raise RepositoryError("bad corpus type repo") + + def mock_all(_, org_id: Optional[int]) -> list[CorpusTypeReadDTO]: + maybe_throw() + if corpus_type_repo.return_empty: + return [] + return [ + CorpusTypeReadDTO( + name=f"test_name_{x}", description=f"test_description_{x}", metadata={} + ) + for x in range(2) + ] + + def mock_get(_, name: str) -> Optional[CorpusTypeReadDTO]: + maybe_throw() + + if not corpus_type_repo.return_empty: + return CorpusTypeReadDTO( + name="test_name", description="test_description", metadata={} + ) + return None + + monkeypatch.setattr(corpus_type_repo, "all", mock_all) + mocker.spy(corpus_type_repo, "all") + + monkeypatch.setattr(corpus_type_repo, "get", mock_get) + mocker.spy(corpus_type_repo, "get") diff --git a/tests/mocks/services/corpus_type_service.py b/tests/mocks/services/corpus_type_service.py new file mode 100644 index 00000000..15c06db0 --- /dev/null +++ b/tests/mocks/services/corpus_type_service.py @@ -0,0 +1,43 @@ +from typing import Optional + +from pytest import MonkeyPatch + +from app.errors import AuthorisationError, RepositoryError +from app.model.corpus_type import CorpusTypeReadDTO + + +def mock_corpus_type_service(corpus_type_service, monkeypatch: MonkeyPatch, mocker): + corpus_type_service.valid = True + corpus_type_service.missing = False + corpus_type_service.org_mismatch = False + corpus_type_service.throw_repository_error = False + + def maybe_throw(): + if corpus_type_service.throw_repository_error: + raise RepositoryError("bad repo") + + def mock_all(user_email: str) -> list[CorpusTypeReadDTO]: + maybe_throw() + return [ + CorpusTypeReadDTO( + name=f"test_name_{x}", description=f"test_description_{x}", metadata={} + ) + for x in range(2) + ] + + def mock_get(name: str) -> Optional[CorpusTypeReadDTO]: + maybe_throw() + if corpus_type_service.org_mismatch: + raise AuthorisationError("Org mismatch") + + if not corpus_type_service.missing: + return CorpusTypeReadDTO( + name="test_name", description="test_description", metadata={} + ) + return None + + monkeypatch.setattr(corpus_type_service, "all", mock_all) + mocker.spy(corpus_type_service, "all") + + monkeypatch.setattr(corpus_type_service, "get", mock_get) + mocker.spy(corpus_type_service, "get") diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index f7ac2c5e..ed99ebd5 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -22,6 +22,7 @@ import app.service.collection as collection_service import app.service.config as config_service import app.service.corpus as corpus_service +import app.service.corpus_type as corpus_type_service import app.service.document as document_service import app.service.event as event_service import app.service.family as family_service @@ -36,6 +37,7 @@ collection_repo, config_repo, corpus_repo, + corpus_type_repo, document_repo, event_repo, family_repo, @@ -47,6 +49,7 @@ from tests.mocks.repos.collection_repo import mock_collection_repo from tests.mocks.repos.config_repo import mock_config_repo from tests.mocks.repos.corpus_repo import mock_corpus_repo +from tests.mocks.repos.corpus_type_repo import mock_corpus_type_repo from tests.mocks.repos.db_client_corpus_helpers import mock_corpus_helpers_db_client from tests.mocks.repos.db_client_metadata import mock_metadata_db_client from tests.mocks.repos.document_repo import mock_document_repo @@ -59,6 +62,7 @@ from tests.mocks.services.collection_service import mock_collection_service from tests.mocks.services.config_service import mock_config_service from tests.mocks.services.corpus_service import mock_corpus_service +from tests.mocks.services.corpus_type_service import mock_corpus_type_service from tests.mocks.services.document_service import mock_document_service from tests.mocks.services.event_service import mock_event_service from tests.mocks.services.family_service import mock_family_service @@ -148,6 +152,13 @@ def corpus_repo_mock(monkeypatch, mocker): yield corpus_repo +@pytest.fixture +def corpus_type_repo_mock(monkeypatch, mocker): + """Mocks the repository for a single test.""" + mock_corpus_type_repo(corpus_type_repo, monkeypatch, mocker) + yield corpus_type_repo + + @pytest.fixture def db_client_metadata_mock(monkeypatch, mocker): """Mocks the repository for a single test.""" @@ -221,6 +232,13 @@ def corpus_service_mock(monkeypatch, mocker): yield corpus_service +@pytest.fixture +def corpus_type_service_mock(monkeypatch, mocker): + """Mocks the service for a single test.""" + mock_corpus_type_service(corpus_type_service, monkeypatch, mocker) + yield corpus_type_service + + @pytest.fixture def bulk_import_service_mock(monkeypatch, mocker): """Mocks the service for a single test.""" diff --git a/tests/unit_tests/routers/bulk_import/__init__.py b/tests/unit_tests/routers/bulk_import/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/unit_tests/routers/corpus_type/test_all.py b/tests/unit_tests/routers/corpus_type/test_all.py new file mode 100644 index 00000000..84cb3cec --- /dev/null +++ b/tests/unit_tests/routers/corpus_type/test_all.py @@ -0,0 +1,35 @@ +from fastapi import status +from fastapi.testclient import TestClient + + +def test_all_corpus_type_when_not_authenticated(client: TestClient): + response = client.get( + "/api/v1/corpus-types", + ) + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + +def test_all_corpus_type_when_non_admin_non_super( + client: TestClient, user_header_token +): + response = client.get("/api/v1/corpus-types", headers=user_header_token) + assert response.status_code == status.HTTP_403_FORBIDDEN + + +def test_all_corpus_type_when_admin_non_super( + client: TestClient, admin_user_header_token +): + response = client.get("/api/v1/corpus-types", headers=admin_user_header_token) + assert response.status_code == status.HTTP_403_FORBIDDEN + + +def test_all_when_ok( + client: TestClient, superuser_header_token, corpus_type_service_mock +): + response = client.get("/api/v1/corpus-types", headers=superuser_header_token) + assert response.status_code == status.HTTP_200_OK + data = response.json() + assert isinstance(data, list) + assert len(data) > 0 + assert data[0]["name"] == "test_name_0" + assert corpus_type_service_mock.all.call_count == 1 diff --git a/tests/unit_tests/routers/corpus_type/test_get.py b/tests/unit_tests/routers/corpus_type/test_get.py new file mode 100644 index 00000000..061cf601 --- /dev/null +++ b/tests/unit_tests/routers/corpus_type/test_get.py @@ -0,0 +1,52 @@ +from fastapi import status +from fastapi.testclient import TestClient + + +def test_get_corpus_type_when_not_authenticated(client: TestClient): + response = client.get( + "/api/v1/corpus-types/test_corpus_type", + ) + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + +def test_get_corpus_type_when_non_admin_non_super( + client: TestClient, user_header_token +): + response = client.get( + "/api/v1/corpus-types/test_corpus_type", headers=user_header_token + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + + +def test_get_corpus_type_when_admin_non_super( + client: TestClient, admin_user_header_token +): + response = client.get( + "/api/v1/corpus-types/test_corpus_type", headers=admin_user_header_token + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + + +def test_get_when_ok( + client: TestClient, corpus_type_service_mock, superuser_header_token +): + response = client.get( + "/api/v1/corpus-types/test_name", headers=superuser_header_token + ) + assert response.status_code == status.HTTP_200_OK + data = response.json() + assert data["name"] == "test_name" + assert corpus_type_service_mock.get.call_count == 1 + + +def test_get_when_not_found( + client: TestClient, corpus_type_service_mock, superuser_header_token +): + corpus_type_service_mock.missing = True + response = client.get( + "/api/v1/corpus-types/test_corpus_type", headers=superuser_header_token + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + data = response.json() + assert data["detail"] == "Corpus type not found: test_corpus_type" + assert corpus_type_service_mock.get.call_count == 1 diff --git a/tests/unit_tests/service/corpus_type/test_all_corpus_type_service.py b/tests/unit_tests/service/corpus_type/test_all_corpus_type_service.py new file mode 100644 index 00000000..32f7f318 --- /dev/null +++ b/tests/unit_tests/service/corpus_type/test_all_corpus_type_service.py @@ -0,0 +1,28 @@ +import pytest + +import app.service.corpus_type as corpus_type_service +from app.errors import RepositoryError + + +def test_all(corpus_type_repo_mock, admin_user_context): + result = corpus_type_service.all(admin_user_context) + assert result is not None + assert corpus_type_repo_mock.all.call_count == 1 + + +def test_all_returns_empty_list_if_no_results( + corpus_type_repo_mock, admin_user_context +): + corpus_type_repo_mock.return_empty = True + result = corpus_type_service.all(admin_user_context) + assert result == [] + assert corpus_type_repo_mock.all.call_count == 1 + + +def test_all_raises_db_error(corpus_type_repo_mock, bad_user_context): + corpus_type_repo_mock.throw_repository_error = True + with pytest.raises(RepositoryError) as e: + corpus_type_service.all(bad_user_context) + expected_msg = "bad corpus type repo" + assert e.value.message == expected_msg + assert corpus_type_repo_mock.all.call_count == 1 diff --git a/tests/unit_tests/service/corpus_type/test_get_corpus_type_service.py b/tests/unit_tests/service/corpus_type/test_get_corpus_type_service.py new file mode 100644 index 00000000..b9711de3 --- /dev/null +++ b/tests/unit_tests/service/corpus_type/test_get_corpus_type_service.py @@ -0,0 +1,15 @@ +import app.service.corpus_type as corpus_type_service + + +def test_get(corpus_type_repo_mock): + result = corpus_type_service.get("some_corpus_type_name") + assert result is not None + assert result.name == "test_name" + assert corpus_type_repo_mock.get.call_count == 1 + + +def test_get_returns_none(corpus_type_repo_mock): + corpus_type_repo_mock.return_empty = True + result = corpus_type_service.get("some_corpus_type_name") + assert result is None + assert corpus_type_repo_mock.get.call_count == 1