Skip to content

Commit

Permalink
Add GetMyReviewsQuery
Browse files Browse the repository at this point in the history
  • Loading branch information
madnoberson committed Jan 29, 2024
1 parent 555dd7a commit 7d05899
Show file tree
Hide file tree
Showing 12 changed files with 278 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/amdb/application/common/constants/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
RATE_MOVIE_ACCESS_DENIED = "Access to movie rating is denied"
UNRATE_MOVIE_ACCESS_DENIED = "Access to movie unrating is denied"
GET_MOVIE_REVIEWS_ACCESS_DENIED = "Access to getting movie reviews is denied"
GET_MY_REVIEWS_ACCESS_DENIED = "Access to getting your reviews is denied"
GET_REVIEW_ACCESS_DENIED = "Access to getting review is denied"
REVIEW_MOVIE_ACCESS_DENIED = "Access to movie reviewing is denied"

Expand Down
3 changes: 3 additions & 0 deletions src/amdb/application/common/interfaces/permissions_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ def for_unrate_movie(self) -> int:
def for_get_movie_reviews(self) -> int:
raise NotImplementedError

def for_get_my_reviews(self) -> int:
raise NotImplementedError

def for_get_review(self) -> int:
raise NotImplementedError

Expand Down
8 changes: 8 additions & 0 deletions src/amdb/application/common/interfaces/review_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ def list_with_movie_id(
) -> list[Review]:
raise NotImplementedError

def list_with_user_id(
self,
user_id: UserId,
limit: int,
offset: int,
) -> list[Review]:
raise NotImplementedError

def save(self, review: Review) -> None:
raise NotImplementedError

Expand Down
29 changes: 29 additions & 0 deletions src/amdb/application/queries/get_my_reviews.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from dataclasses import dataclass
from datetime import datetime

from amdb.domain.entities.user import UserId
from amdb.domain.entities.movie import MovieId
from amdb.domain.entities.review import ReviewId, ReviewType


@dataclass(frozen=True, slots=True)
class GetMyReviewsQuery:
limit: int
offset: int


@dataclass(frozen=True, slots=True)
class Review:
id: ReviewId
user_id: UserId
movie_id: MovieId
title: str
content: str
type: ReviewType
created_at: datetime


@dataclass(frozen=True, slots=True)
class GetMyReviewsResult:
reviews: list[Review]
review_count: int
46 changes: 46 additions & 0 deletions src/amdb/application/query_handlers/get_my_reviews.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from amdb.domain.services.access_concern import AccessConcern
from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway
from amdb.application.common.interfaces.review_gateway import ReviewGateway
from amdb.application.common.interfaces.identity_provider import IdentityProvider
from amdb.application.common.constants.exceptions import GET_MY_REVIEWS_ACCESS_DENIED
from amdb.application.common.exception import ApplicationError
from amdb.application.queries.get_my_reviews import GetMyReviewsQuery, GetMyReviewsResult


class GetMyReviewsHandler:
def __init__(
self,
*,
access_concern: AccessConcern,
permissions_gateway: PermissionsGateway,
review_gateway: ReviewGateway,
identity_provider: IdentityProvider,
) -> None:
self._access_concern = access_concern
self._permissions_gateway = permissions_gateway
self._review_gateway = review_gateway
self._identity_provider = identity_provider

def execute(self, query: GetMyReviewsQuery) -> GetMyReviewsResult:
current_permissions = self._identity_provider.get_permissions()
required_permissions = self._permissions_gateway.for_get_my_reviews()
access = self._access_concern.authorize(
current_permissions=current_permissions,
required_permissions=required_permissions,
)
if not access:
raise ApplicationError(GET_MY_REVIEWS_ACCESS_DENIED)

current_user_id = self._identity_provider.get_user_id()

reviews = self._review_gateway.list_with_user_id(
user_id=current_user_id,
limit=query.limit,
offset=query.offset,
)
get_my_reviews_result = GetMyReviewsResult(
reviews=reviews, # type: ignore
review_count=len(reviews),
)

return get_my_reviews_result
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ def for_unrate_movie(self) -> int:
def for_get_movie_reviews(self) -> int:
return 4

def for_get_my_reviews(self) -> int:
return 4

def for_get_review(self) -> int:
return 4

Expand Down
18 changes: 18 additions & 0 deletions src/amdb/infrastructure/persistence/sqlalchemy/gateways/review.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,24 @@ def list_with_movie_id(

return review_entities

def list_with_user_id(
self,
user_id: UserId,
limit: int,
offset: int,
) -> list[ReviewEntity]:
statement = (
select(ReviewModel).where(ReviewModel.user_id == user_id).limit(limit).offset(offset)
)
review_models = self._session.scalars(statement)

review_entities = []
for review_model in review_models:
review_entity = self._mapper.to_entity(review_model)
review_entities.append(review_entity)

return review_entities

def save(self, review: ReviewEntity) -> None:
review_model = self._mapper.to_model(review)
self._session.add(review_model)
Expand Down
14 changes: 14 additions & 0 deletions src/amdb/main/ioc.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from amdb.application.query_handlers.get_my_ratings import GetMyRatingsHandler
from amdb.application.query_handlers.get_rating import GetRatingHandler
from amdb.application.query_handlers.get_movie_reviews import GetMovieReviewsHandler
from amdb.application.query_handlers.get_my_reviews import GetMyReviewsHandler
from amdb.application.query_handlers.get_review import GetReviewHandler
from amdb.infrastructure.persistence.sqlalchemy.gateway_factory import (
build_sqlalchemy_gateway_factory,
Expand Down Expand Up @@ -217,6 +218,19 @@ def get_movie_reviews(
identity_provider=identity_provider,
)

@contextmanager
def get_my_reviews(
self,
identity_provider: IdentityProvider,
) -> Iterator[GetMyReviewsHandler]:
with build_sqlalchemy_gateway_factory(self._sessionmaker) as gateway_factory:
yield GetMyReviewsHandler(
access_concern=AccessConcern(),
permissions_gateway=self._permissions_gateway,
review_gateway=gateway_factory.review(),
identity_provider=identity_provider,
)

@contextmanager
def get_review(
self,
Expand Down
7 changes: 7 additions & 0 deletions src/amdb/presentation/handler_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from amdb.application.query_handlers.get_my_ratings import GetMyRatingsHandler
from amdb.application.query_handlers.get_rating import GetRatingHandler
from amdb.application.query_handlers.get_movie_reviews import GetMovieReviewsHandler
from amdb.application.query_handlers.get_my_reviews import GetMyReviewsHandler
from amdb.application.query_handlers.get_review import GetReviewHandler


Expand Down Expand Up @@ -97,6 +98,12 @@ def get_movie_reviews(
) -> ContextManager[GetMovieReviewsHandler]:
raise NotImplementedError

def get_my_reviews(
self,
identity_provider: IdentityProvider,
) -> ContextManager[GetMyReviewsHandler]:
raise NotImplementedError

@abstractmethod
def get_review(
self,
Expand Down
24 changes: 24 additions & 0 deletions src/amdb/presentation/web_api/routers/reviews/get_my_reviews.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import Annotated

from fastapi import Depends

from amdb.application.common.interfaces.identity_provider import IdentityProvider
from amdb.application.queries.get_my_reviews import GetMyReviewsQuery, GetMyReviewsResult
from amdb.presentation.handler_factory import HandlerFactory
from amdb.presentation.web_api.dependencies.identity_provider import get_identity_provider


async def get_my_reviews(
ioc: Annotated[HandlerFactory, Depends()],
identity_provider: Annotated[IdentityProvider, Depends(get_identity_provider)],
limit: int = 100,
offset: int = 0,
) -> GetMyReviewsResult:
with ioc.get_my_reviews(identity_provider) as get_my_reviews_handler:
get_my_reviews_query = GetMyReviewsQuery(
limit=limit,
offset=offset,
)
get_my_reviews_result = get_my_reviews_handler.execute(get_my_reviews_query)

return get_my_reviews_result
7 changes: 7 additions & 0 deletions src/amdb/presentation/web_api/routers/reviews/router.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from fastapi import APIRouter

from .get_movie_reviews import get_movie_reviews
from .get_my_reviews import get_my_reviews
from .review_movie import review_movie
from .get_review import get_review

Expand Down Expand Up @@ -28,6 +29,12 @@ def create_reviews_router() -> APIRouter:
methods=["POST"],
tags=["me"],
)
router.add_api_route(
path="/me/reviews",
endpoint=get_my_reviews,
methods=["GET"],
tags=["me"],
)
router.add_api_route(
path="/me/reviews/{review_id}",
endpoint=get_review,
Expand Down
118 changes: 118 additions & 0 deletions tests/unit/application/query_handlers/test_get_my_reviews.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from unittest.mock import Mock
from datetime import date, 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.review import ReviewId, ReviewType, Review
from amdb.domain.services.access_concern import AccessConcern
from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway
from amdb.application.common.interfaces.user_gateway import UserGateway
from amdb.application.common.interfaces.movie_gateway import MovieGateway
from amdb.application.common.interfaces.review_gateway import ReviewGateway
from amdb.application.common.interfaces.unit_of_work import UnitOfWork
from amdb.application.common.interfaces.identity_provider import IdentityProvider
from amdb.application.queries.get_my_reviews import GetMyReviewsQuery, GetMyReviewsResult
from amdb.application.query_handlers.get_my_reviews import GetMyReviewsHandler
from amdb.application.common.constants.exceptions import GET_MY_REVIEWS_ACCESS_DENIED
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_my_reviews()
identity_provider.get_permissions = Mock(return_value=correct_permissions)

return identity_provider


def test_get_my_reviews(
permissions_gateway: PermissionsGateway,
user_gateway: UserGateway,
movie_gateway: MovieGateway,
review_gateway: ReviewGateway,
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="Gone girl",
release_date=date(2014, 10, 3),
rating=0,
rating_count=0,
)
movie_gateway.save(movie)

review = Review(
id=ReviewId(uuid7()),
user_id=user.id,
movie_id=movie.id,
title="Masterpice",
content="Extremely underrated",
type=ReviewType.POSITIVE,
created_at=datetime.now(timezone.utc),
)
review_gateway.save(review)

unit_of_work.commit()

identity_provider_with_correct_permissions.get_user_id = Mock(
return_value=user.id,
)

get_my_reviews_query = GetMyReviewsQuery(
limit=10,
offset=0,
)
get_my_reviews_handler = GetMyReviewsHandler(
access_concern=AccessConcern(),
permissions_gateway=permissions_gateway,
review_gateway=review_gateway,
identity_provider=identity_provider_with_correct_permissions,
)

get_my_reviews_result = get_my_reviews_handler.execute(get_my_reviews_query)
expected_get_my_reviews_result = GetMyReviewsResult(
reviews=[review],
review_count=1,
)

assert get_my_reviews_result == expected_get_my_reviews_result


def test_get_my_reviews_should_raise_error_when_access_is_denied(
permissions_gateway: PermissionsGateway,
review_gateway: ReviewGateway,
identity_provider_with_incorrect_permissions: IdentityProvider,
):
get_my_reviews_query = GetMyReviewsQuery(
limit=10,
offset=0,
)
get_my_reviews_handler = GetMyReviewsHandler(
access_concern=AccessConcern(),
permissions_gateway=permissions_gateway,
review_gateway=review_gateway,
identity_provider=identity_provider_with_incorrect_permissions,
)

identity_provider_with_incorrect_permissions.get_user_id = Mock(
return_value=UserId(uuid7()),
)

with pytest.raises(ApplicationError) as error:
get_my_reviews_handler.execute(get_my_reviews_query)

assert error.value.message == GET_MY_REVIEWS_ACCESS_DENIED

0 comments on commit 7d05899

Please sign in to comment.