Skip to content

Commit

Permalink
Merge pull request #968 from traderstechie/feat/delete-blog-dislike
Browse files Browse the repository at this point in the history
feat: delete blog dislike
  • Loading branch information
johnson-oragui authored Aug 24, 2024
2 parents c71f6dc + 59eb5c8 commit 5178826
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 4 deletions.
32 changes: 30 additions & 2 deletions api/v1/routes/blog.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from api.utils.pagination import paginated_response
from api.utils.success_response import success_response
from api.v1.models.user import User
from api.v1.models.blog import Blog, BlogDislike, BlogLike
from api.v1.models.blog import Blog
from api.v1.schemas.blog import (
BlogCreate,
BlogPostResponse,
Expand All @@ -20,7 +20,7 @@
CommentRequest,
CommentUpdateResponseModel
)
from api.v1.services.blog import BlogService
from api.v1.services.blog import BlogService, BlogDislikeService
from api.v1.services.user import user_service
from api.v1.schemas.comment import CommentCreate, CommentSuccessResponse
from api.v1.services.comment import comment_service
Expand Down Expand Up @@ -118,6 +118,7 @@ def like_blog_post(
current_user: User = Depends(user_service.get_current_user),
):
"""Endpoint to add `like` to a blog post.
Existing `dislike` by the `current_user` is automatically deleted.
args:
blog_id: `str` The ID of the blog post.
Expand All @@ -137,6 +138,9 @@ def like_blog_post(
# confirm current user has NOT liked before
blog_service.check_user_already_liked_blog(blog_p, current_user)

# check for BlogDislike by current user and delete it
blog_service.delete_opposite_blog_like_or_dislike(blog_p, current_user, "like")

# update likes
new_like = blog_service.create_blog_like(
db, blog_p.id, current_user.id, ip_address=get_ip_address(request))
Expand All @@ -160,6 +164,7 @@ def dislike_blog_post(
current_user: User = Depends(user_service.get_current_user),
):
"""Endpoint to add `dislike` to a blog post.
Existing `like` by the `current_user` is automatically deleted.
args:
blog_id: `str` The ID of the blog post.
Expand All @@ -179,6 +184,9 @@ def dislike_blog_post(
# confirm current user has NOT disliked before
blog_service.check_user_already_disliked_blog(blog_p, current_user)

# check for BlogLike by current user and delete it
blog_service.delete_opposite_blog_like_or_dislike(blog_p, current_user, "dislike")

# update disikes
new_dislike = blog_service.create_blog_dislike(
db, blog_p.id, current_user.id, ip_address=get_ip_address(request))
Expand Down Expand Up @@ -299,3 +307,23 @@ async def update_blog_comment(
status_code=200,
data=jsonable_encoder(updated_blog_comment)
)


@blog.delete("/dislikes/{blog_dislike_id}",
status_code=status.HTTP_204_NO_CONTENT)
def delete_blog_dislike(
blog_dislike_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(user_service.get_current_user),
):
"""Endpoint to delete `BlogDislike`
args:
blog_dislike_id: `str` The ID of the BlogDislike object.
request: `default` Request.
db: `default` Session.
"""
blog_dislike_service = BlogDislikeService(db)

# delete blog dislike
return blog_dislike_service.delete(blog_dislike_id, current_user.id)
56 changes: 54 additions & 2 deletions api/v1/services/blog.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,21 +118,48 @@ def fetch_blog_dislike(self, blog_id: str, user_id: str):
)
return blog_dislike

def check_user_already_liked_blog(self, blog: Blog, user: Blog):
def check_user_already_liked_blog(self, blog: Blog, user: User):
existing_like = self.fetch_blog_like(blog.id, user.id)
if isinstance(existing_like, BlogLike):
raise HTTPException(
detail="You have already liked this blog post",
status_code=status.HTTP_403_FORBIDDEN,
)

def check_user_already_disliked_blog(self, blog: Blog, user: Blog):
def check_user_already_disliked_blog(self, blog: Blog, user: User):
existing_dislike = self.fetch_blog_dislike(blog.id, user.id)
if isinstance(existing_dislike, BlogDislike):
raise HTTPException(
detail="You have already disliked this blog post",
status_code=status.HTTP_403_FORBIDDEN,
)

def delete_opposite_blog_like_or_dislike(self, blog: Blog, user: User, creating: str):
"""
This method checks if there's a BlogLike by `user` on `blog` when a BlogDislike
is being created and deletes the BlogLike. The same for BlogLike creation. \n
:param blog: `Blog` The blog being liked or disliked
:param user: `User` The user liking or disliking the blog
:param creating: `str` The operation being performed by the user. One of "like", "dislike"
"""
if creating == "like":
existing_dislike = self.fetch_blog_dislike(blog.id, user.id)
if existing_dislike:
# delete, but do not commit yet. Allow everything
# to be commited after the actual like is created
self.db.delete(existing_dislike)
elif creating == "dislike":
existing_like = self.fetch_blog_like(blog.id, user.id)
if existing_like:
# delete, but do not commit yet. Allow everything
# to be commited after the actual dislike is created
self.db.delete(existing_like)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid `creating` value for blog like/dislike"
)

def num_of_likes(self, blog_id: str) -> int:
"""Get the number of likes a blog post has"""
Expand Down Expand Up @@ -211,3 +238,28 @@ def update_blog_comment(
)

return comment


class BlogDislikeService:
"""BlogDislike service functionality"""

def __init__(self, db: Session):
self.db = db

def fetch(self, blog_dislike_id: str):
"""Fetch a blog dislike by its ID"""
return check_model_existence(self.db, BlogDislike, blog_dislike_id)

def delete(self, blog_dislike_id: str, user_id: str):
"""Delete blog dislike"""
blog_dislike = self.fetch(blog_dislike_id)

# check that current user owns the blog like
if blog_dislike.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Insufficient permission"
)

self.db.delete(blog_dislike)
self.db.commit()
142 changes: 142 additions & 0 deletions tests/v1/blog/test_delete_blog_dislike.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import pytest
from main import app
from uuid_extensions import uuid7
from sqlalchemy.orm import Session
from api.db.database import get_db
from datetime import datetime, timezone
from fastapi.testclient import TestClient
from unittest.mock import patch, MagicMock
from api.v1.models import User, BlogDislike
from api.v1.services.user import user_service

client = TestClient(app)

# Mock database
@pytest.fixture
def mock_db_session(mocker):
db_session_mock = mocker.MagicMock(spec=Session)
app.dependency_overrides[get_db] = lambda: db_session_mock
return db_session_mock


@pytest.fixture
def mock_user_service():
with patch("api.v1.services.user.user_service", autospec=True) as user_service_mock:
yield user_service_mock


@pytest.fixture
def mock_blog_service():
with patch("api.v1.services.blog.BlogService", autospec=True) as blog_service_mock:
yield blog_service_mock


# Test User
@pytest.fixture
def test_user():
return User(
id=str(uuid7()),
email="[email protected]",
password="hashedpassword",
first_name="test",
last_name="user",
is_active=True,
)


# Another User
@pytest.fixture
def another_user():
return User(
id=str(uuid7()),
email="[email protected]",
password="hashedpassword",
first_name="another",
last_name="user",
is_active=True,
)

@pytest.fixture
def test_blog_dislike(test_user):
return BlogDislike(
id=str(uuid7()),
user_id=test_user.id,
blog_id=str(uuid7()),
ip_address="192.168.1.0",
created_at=datetime.now(tz=timezone.utc)
)

@pytest.fixture
def access_token_user(test_user):
return user_service.create_access_token(user_id=test_user.id)

@pytest.fixture
def access_token_another(another_user):
return user_service.create_access_token(user_id=another_user.id)


def make_request(blog_dislike_id, token):
return client.delete(
f"/api/v1/blogs/dislikes/{blog_dislike_id}",
headers={"Authorization": f"Bearer {token}"}
)


# test for successful delete
@patch("api.v1.services.blog.BlogDislikeService.fetch")
def test_successful_delete_blog_dislike(
mock_fetch_blog_dislike,
mock_db_session,
test_user,
test_blog_dislike,
access_token_user
):
# mock current-user AND blog-like
mock_db_session.query().filter().first.return_value = test_user
mock_fetch_blog_dislike.return_value = test_blog_dislike

resp = make_request(test_blog_dislike.id, access_token_user)
assert resp.status_code == 204


# Test for wrong blog like id
def test_wrong_blog_dislike_id(
mock_db_session,
test_user,
access_token_user,
):
mock_db_session.query().filter().first.return_value = test_user
mock_db_session.get.return_value = None

### TEST REQUEST WITH WRONG blog_dislike_id ###
resp = make_request(str(uuid7()), access_token_user)
assert resp.status_code == 404
assert resp.json()['message'] == "BlogDislike does not exist"


# Test for unauthenticated user
def test_wrong_auth_token(
test_blog_dislike
):
mock_user_service.get_current_user = None

### TEST ATTEMPT WITH INVALID AUTH ###
resp = make_request(test_blog_dislike.id, None)
assert resp.status_code == 401
assert resp.json()['message'] == 'Could not validate credentials'


# Test for wrong owner request
def test_wrong_owner_request(
mock_db_session,
test_blog_dislike,
another_user,
access_token_another
):
mock_user_service.get_current_user = another_user
mock_db_session.get.return_value = test_blog_dislike

### TEST ATTEMPT BY NON OWNER ###
resp = make_request(test_blog_dislike.id, access_token_another)
assert resp.status_code == 401
assert resp.json()['message'] == 'Insufficient permission'

0 comments on commit 5178826

Please sign in to comment.