diff --git a/api/v1/routes/blog.py b/api/v1/routes/blog.py index d07ffceb..ca475a9b 100644 --- a/api/v1/routes/blog.py +++ b/api/v1/routes/blog.py @@ -20,7 +20,7 @@ CommentRequest, CommentUpdateResponseModel ) -from api.v1.services.blog import BlogService, BlogLikeService +from api.v1.services.blog import BlogService, BlogDislikeService, BlogLikeService from api.v1.services.user import user_service from api.v1.schemas.comment import CommentCreate, CommentSuccessResponse from api.v1.services.comment import comment_service @@ -327,3 +327,23 @@ async def delete_blog_like( # delete blog like return blog_like_service.delete(blog_like_id, current_user.id) + + +@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) diff --git a/api/v1/services/blog.py b/api/v1/services/blog.py index 6e6eb842..46c02b14 100644 --- a/api/v1/services/blog.py +++ b/api/v1/services/blog.py @@ -263,3 +263,28 @@ def delete(self, blog_like_id: str, user_id: str): self.db.delete(blog_like) self.db.commit() + + +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() diff --git a/tests/v1/blog/test_delete_blog_dislike.py b/tests/v1/blog/test_delete_blog_dislike.py new file mode 100644 index 00000000..946d8a2e --- /dev/null +++ b/tests/v1/blog/test_delete_blog_dislike.py @@ -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="testuser@gmail.com", + password="hashedpassword", + first_name="test", + last_name="user", + is_active=True, + ) + + +# Another User +@pytest.fixture +def another_user(): + return User( + id=str(uuid7()), + email="anotheruser@gmail.com", + 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' \ No newline at end of file