From 312d2b4b8db9f7b7d7c1901f10a52df26e411de6 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Sun, 21 Jan 2024 14:02:16 +0400 Subject: [PATCH] Add `GetRatingQuery` --- .../common/constants/exceptions.py | 1 + .../common/interfaces/permissions_gateway.py | 3 + src/amdb/application/queries/get_rating.py | 15 ++ .../application/query_handlers/get_rating.py | 59 ++++++ .../persistence/redis/gateways/permissions.py | 3 + src/amdb/main/ioc.py | 17 ++ src/amdb/presentation/handler_factory.py | 8 + .../web_api/routers/ratings/get_rating.py | 23 +++ .../web_api/routers/ratings/router.py | 6 + .../query_handlers/test_get_rating.py | 183 ++++++++++++++++++ 10 files changed, 318 insertions(+) create mode 100644 src/amdb/application/queries/get_rating.py create mode 100644 src/amdb/application/query_handlers/get_rating.py create mode 100644 src/amdb/presentation/web_api/routers/ratings/get_rating.py create mode 100644 tests/unit/application/query_handlers/test_get_rating.py diff --git a/src/amdb/application/common/constants/exceptions.py b/src/amdb/application/common/constants/exceptions.py index 6051ccf..4eb4337 100644 --- a/src/amdb/application/common/constants/exceptions.py +++ b/src/amdb/application/common/constants/exceptions.py @@ -1,6 +1,7 @@ LOGIN_ACCESS_DENIED = "Access to login in denied" CREATE_MOVIE_ACCESS_DENIED = "Access to movie creation is denied" DELETE_MOVIE_ACCESS_DENIED = "Access to movie deletion is denied" +GET_RATING_ACCESS_DENIED = "Access to getting movie rating is denied" RATE_MOVIE_ACCESS_DENIED = "Access to movie rating is denied" UNRATE_MOVIE_ACCESS_DENIED = "Access to movie unrating is denied" diff --git a/src/amdb/application/common/interfaces/permissions_gateway.py b/src/amdb/application/common/interfaces/permissions_gateway.py index 8b55f33..25a9e1c 100644 --- a/src/amdb/application/common/interfaces/permissions_gateway.py +++ b/src/amdb/application/common/interfaces/permissions_gateway.py @@ -22,6 +22,9 @@ def for_create_movie(self) -> int: def for_delete_movie(self) -> int: raise NotImplementedError + def for_get_rating(self) -> int: + raise NotImplementedError + def for_rate_movie(self) -> int: raise NotImplementedError diff --git a/src/amdb/application/queries/get_rating.py b/src/amdb/application/queries/get_rating.py new file mode 100644 index 0000000..e1a56e4 --- /dev/null +++ b/src/amdb/application/queries/get_rating.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +from datetime import datetime + +from amdb.domain.entities.movie import MovieId + + +@dataclass(frozen=True, slots=True) +class GetRatingQuery: + movie_id: MovieId + + +@dataclass(frozen=True, slots=True) +class GetRatingResult: + value: float + created_at: datetime diff --git a/src/amdb/application/query_handlers/get_rating.py b/src/amdb/application/query_handlers/get_rating.py new file mode 100644 index 0000000..27c6f0c --- /dev/null +++ b/src/amdb/application/query_handlers/get_rating.py @@ -0,0 +1,59 @@ +from amdb.domain.services.access_concern import AccessConcern +from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.movie_gateway import MovieGateway +from amdb.application.common.interfaces.rating_gateway import RatingGateway +from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.queries.get_rating import GetRatingQuery, GetRatingResult +from amdb.application.common.constants.exceptions import ( + GET_RATING_ACCESS_DENIED, + MOVIE_DOES_NOT_EXIST, + MOVIE_NOT_RATED, +) +from amdb.application.common.exception import ApplicationError + + +class GetRatingHandler: + def __init__( + self, + *, + access_concern: AccessConcern, + permissions_gateway: PermissionsGateway, + movie_gateway: MovieGateway, + rating_gateway: RatingGateway, + identity_provider: IdentityProvider, + ) -> None: + self._access_concern = access_concern + self._permissions_gateway = permissions_gateway + self._movie_gateway = movie_gateway + self._rating_gateway = rating_gateway + self._identity_provider = identity_provider + + def execute(self, query: GetRatingQuery) -> GetRatingResult: + current_permissions = self._identity_provider.get_permissions() + required_permissions = self._permissions_gateway.for_get_rating() + access = self._access_concern.authorize( + current_permissions=current_permissions, + required_permissions=required_permissions, + ) + if not access: + raise ApplicationError(GET_RATING_ACCESS_DENIED) + + movie = self._movie_gateway.with_id(query.movie_id) + if not movie: + raise ApplicationError(MOVIE_DOES_NOT_EXIST) + + current_user_id = self._identity_provider.get_user_id() + + rating = self._rating_gateway.with_user_id_and_movie_id( + user_id=current_user_id, + movie_id=movie.id, + ) + if not rating: + raise ApplicationError(MOVIE_NOT_RATED) + + get_rating_result = GetRatingResult( + value=rating.value, + created_at=rating.created_at, + ) + + return get_rating_result diff --git a/src/amdb/infrastructure/persistence/redis/gateways/permissions.py b/src/amdb/infrastructure/persistence/redis/gateways/permissions.py index 39f673e..02aafde 100644 --- a/src/amdb/infrastructure/persistence/redis/gateways/permissions.py +++ b/src/amdb/infrastructure/persistence/redis/gateways/permissions.py @@ -33,6 +33,9 @@ def for_create_movie(self) -> int: def for_delete_movie(self) -> int: return 8 + def for_get_rating(self) -> int: + return 4 + def for_rate_movie(self) -> int: return 4 diff --git a/src/amdb/main/ioc.py b/src/amdb/main/ioc.py index 557a004..e3328eb 100644 --- a/src/amdb/main/ioc.py +++ b/src/amdb/main/ioc.py @@ -15,6 +15,7 @@ from amdb.application.command_handlers.rate_movie import RateMovieHandler from amdb.application.command_handlers.unrate_movie import UnrateMovieHandler from amdb.application.query_handlers.login import LoginHandler +from amdb.application.query_handlers.get_rating import GetRatingHandler from amdb.infrastructure.persistence.sqlalchemy.gateway_factory import ( build_sqlalchemy_gateway_factory, ) @@ -58,7 +59,9 @@ def login(self) -> Iterator[LoginHandler]: user_password_hash_gateway=gateway_factory.user_password_hash(), ) yield LoginHandler( + access_concern=AccessConcern(), user_gateway=gateway_factory.user(), + permissions_gateway=self._permissions_gateway, password_manager=hashing_password_manager, ) @@ -92,6 +95,20 @@ def delete_movie( identity_provider=identity_provider, ) + @contextmanager + def get_rating( + self, + identity_provider: IdentityProvider, + ) -> Iterator[GetRatingHandler]: + with build_sqlalchemy_gateway_factory(self._sessionmaker) as gateway_factory: + yield GetRatingHandler( + access_concern=AccessConcern(), + permissions_gateway=self._permissions_gateway, + movie_gateway=gateway_factory.movie(), + rating_gateway=gateway_factory.rating(), + identity_provider=identity_provider, + ) + @contextmanager def rate_movie( self, diff --git a/src/amdb/presentation/handler_factory.py b/src/amdb/presentation/handler_factory.py index 374255e..c7b5f1a 100644 --- a/src/amdb/presentation/handler_factory.py +++ b/src/amdb/presentation/handler_factory.py @@ -8,6 +8,7 @@ from amdb.application.command_handlers.rate_movie import RateMovieHandler from amdb.application.command_handlers.unrate_movie import UnrateMovieHandler from amdb.application.query_handlers.login import LoginHandler +from amdb.application.query_handlers.get_rating import GetRatingHandler class HandlerFactory(ABC): @@ -33,6 +34,13 @@ def delete_movie( ) -> ContextManager[DeleteMovieHandler]: raise NotImplementedError + @abstractmethod + def get_rating( + self, + identity_provider: IdentityProvider, + ) -> ContextManager[GetRatingHandler]: + raise NotImplementedError + @abstractmethod def rate_movie( self, diff --git a/src/amdb/presentation/web_api/routers/ratings/get_rating.py b/src/amdb/presentation/web_api/routers/ratings/get_rating.py new file mode 100644 index 0000000..39f3c43 --- /dev/null +++ b/src/amdb/presentation/web_api/routers/ratings/get_rating.py @@ -0,0 +1,23 @@ +from typing import Annotated + +from fastapi import Depends + +from amdb.domain.entities.movie import MovieId +from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.queries.get_rating import GetRatingQuery, GetRatingResult +from amdb.presentation.handler_factory import HandlerFactory +from amdb.presentation.web_api.dependencies.identity_provider import get_identity_provider + + +async def get_rating( + ioc: Annotated[HandlerFactory, Depends()], + identity_provider: Annotated[IdentityProvider, Depends(get_identity_provider)], + movie_id: MovieId, +) -> GetRatingResult: + with ioc.get_rating(identity_provider) as get_rating_handler: + get_rating_query = GetRatingQuery( + movie_id=movie_id, + ) + get_rating_result = get_rating_handler.execute(get_rating_query) + + return get_rating_result diff --git a/src/amdb/presentation/web_api/routers/ratings/router.py b/src/amdb/presentation/web_api/routers/ratings/router.py index 7c91e9b..bdd4503 100644 --- a/src/amdb/presentation/web_api/routers/ratings/router.py +++ b/src/amdb/presentation/web_api/routers/ratings/router.py @@ -1,5 +1,6 @@ from fastapi import APIRouter +from .get_rating import get_rating from .rate_movie import rate_movie from .unrate_movie import unrate_movie @@ -10,6 +11,11 @@ def create_ratings_router() -> APIRouter: tags=["ratings"], ) + router.add_api_route( + path="/{movie_id}", + endpoint=get_rating, + methods=["GET"], + ) router.add_api_route( path="/{movie_id}", endpoint=rate_movie, diff --git a/tests/unit/application/query_handlers/test_get_rating.py b/tests/unit/application/query_handlers/test_get_rating.py new file mode 100644 index 0000000..9ae56aa --- /dev/null +++ b/tests/unit/application/query_handlers/test_get_rating.py @@ -0,0 +1,183 @@ +from unittest.mock import Mock +from datetime import datetime, timezone + +import pytest +from uuid_extensions import uuid7 + +from amdb.domain.entities.user import UserId, User +from amdb.domain.entities.movie import MovieId, Movie +from amdb.domain.entities.rating import Rating +from amdb.domain.services.access_concern import AccessConcern +from amdb.application.common.interfaces.user_gateway import UserGateway +from amdb.application.common.interfaces.movie_gateway import MovieGateway +from amdb.application.common.interfaces.rating_gateway import RatingGateway +from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.unit_of_work import UnitOfWork +from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.queries.get_rating import GetRatingQuery, GetRatingResult +from amdb.application.query_handlers.get_rating import GetRatingHandler +from amdb.application.common.constants.exceptions import ( + GET_RATING_ACCESS_DENIED, + MOVIE_DOES_NOT_EXIST, + MOVIE_NOT_RATED, +) +from amdb.application.common.exception import ApplicationError + + +@pytest.fixture +def identity_provider_with_correct_permissions( + permissions_gateway: PermissionsGateway, +) -> IdentityProvider: + identity_provider = Mock() + + correct_permissions = permissions_gateway.for_get_rating() + identity_provider.get_permissions = Mock(return_value=correct_permissions) + + return identity_provider + + +def test_get_rating( + user_gateway: UserGateway, + movie_gateway: MovieGateway, + rating_gateway: RatingGateway, + permissions_gateway: PermissionsGateway, + unit_of_work: UnitOfWork, + identity_provider_with_correct_permissions: IdentityProvider, +): + user = User( + id=UserId(uuid7()), + name="John Doe", + ) + user_gateway.save(user) + + movie = Movie( + id=MovieId(uuid7()), + title="Matrix", + rating=10, + rating_count=1, + ) + movie_gateway.save(movie) + + rating = Rating( + movie_id=movie.id, + user_id=user.id, + value=10, + created_at=datetime.now(timezone.utc), + ) + rating_gateway.save(rating) + + unit_of_work.commit() + + identity_provider_with_correct_permissions.get_user_id = Mock( + return_value=user.id, + ) + + get_rating_query = GetRatingQuery( + movie_id=movie.id, + ) + get_rating_handler = GetRatingHandler( + access_concern=AccessConcern(), + permissions_gateway=permissions_gateway, + movie_gateway=movie_gateway, + rating_gateway=rating_gateway, + identity_provider=identity_provider_with_correct_permissions, + ) + + get_rating_result = get_rating_handler.execute(get_rating_query) + expected_get_rating_result = GetRatingResult( + value=rating.value, + created_at=rating.created_at, + ) + + assert get_rating_result == expected_get_rating_result + + +def test_get_rating_should_raise_error_when_access_is_denied( + movie_gateway: MovieGateway, + rating_gateway: RatingGateway, + permissions_gateway: PermissionsGateway, + identity_provider_with_incorrect_permissions: IdentityProvider, +): + get_rating_query = GetRatingQuery( + movie_id=MovieId(uuid7()), + ) + get_rating_handler = GetRatingHandler( + access_concern=AccessConcern(), + permissions_gateway=permissions_gateway, + movie_gateway=movie_gateway, + rating_gateway=rating_gateway, + identity_provider=identity_provider_with_incorrect_permissions, + ) + + with pytest.raises(ApplicationError) as error: + get_rating_handler.execute(get_rating_query) + + assert error.value.message == GET_RATING_ACCESS_DENIED + + +def test_get_rating_should_raise_error_when_movie_does_not_exist( + movie_gateway: MovieGateway, + rating_gateway: RatingGateway, + permissions_gateway: PermissionsGateway, + identity_provider_with_correct_permissions: IdentityProvider, +): + get_rating_query = GetRatingQuery( + movie_id=MovieId(uuid7()), + ) + get_rating_handler = GetRatingHandler( + access_concern=AccessConcern(), + permissions_gateway=permissions_gateway, + movie_gateway=movie_gateway, + rating_gateway=rating_gateway, + identity_provider=identity_provider_with_correct_permissions, + ) + + with pytest.raises(ApplicationError) as error: + get_rating_handler.execute(get_rating_query) + + assert error.value.message == MOVIE_DOES_NOT_EXIST + + +def test_get_rating_should_raise_error_when_movie_is_not_rated( + user_gateway: UserGateway, + movie_gateway: MovieGateway, + rating_gateway: RatingGateway, + permissions_gateway: PermissionsGateway, + unit_of_work: UnitOfWork, + identity_provider_with_correct_permissions: IdentityProvider, +): + user = User( + id=UserId(uuid7()), + name="John Doe", + ) + user_gateway.save(user) + + movie = Movie( + id=MovieId(uuid7()), + title="Matrix", + rating=0, + rating_count=0, + ) + movie_gateway.save(movie) + + unit_of_work.commit() + + identity_provider_with_correct_permissions.get_user_id = Mock( + return_value=user.id, + ) + + get_rating_query = GetRatingQuery( + movie_id=movie.id, + ) + get_rating_handler = GetRatingHandler( + access_concern=AccessConcern(), + permissions_gateway=permissions_gateway, + movie_gateway=movie_gateway, + rating_gateway=rating_gateway, + identity_provider=identity_provider_with_correct_permissions, + ) + + with pytest.raises(ApplicationError) as error: + get_rating_handler.execute(get_rating_query) + + assert error.value.message == MOVIE_NOT_RATED