-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(be): implement forum upvote and downvote endpoints and tests (#641)
* feat(be): implement forum upvote modelviewset, serializer, model > > Co-authored-by: MucahitErdoganUnlu [email protected] * feat(be): implement forum downvote modelviewset, serializer, model, and unit tests > > Co-authored-by: MucahitErdoganUnlu [email protected]
- Loading branch information
1 parent
8a507b8
commit 88def9b
Showing
7 changed files
with
384 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# Generated by Django 5.1.2 on 2024-11-16 13:43 | ||
|
||
import django.db.models.deletion | ||
from django.conf import settings | ||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('core', '0001_initial'), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='ForumUpvote', | ||
fields=[ | ||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('created_at', models.DateTimeField(auto_now_add=True)), | ||
('forum_question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.forumquestion')), | ||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), | ||
], | ||
options={ | ||
'ordering': ['-created_at'], | ||
'unique_together': {('user', 'forum_question')}, | ||
}, | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# Generated by Django 5.1.2 on 2024-11-16 14:13 | ||
|
||
import django.db.models.deletion | ||
from django.conf import settings | ||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('core', '0002_forumupvote'), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='ForumDownvote', | ||
fields=[ | ||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('created_at', models.DateTimeField(auto_now_add=True)), | ||
('forum_question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.forumquestion')), | ||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), | ||
], | ||
options={ | ||
'ordering': ['-created_at'], | ||
'unique_together': {('user', 'forum_question')}, | ||
}, | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
from django.contrib.auth import get_user_model | ||
from faker import Faker | ||
from rest_framework import serializers | ||
|
||
from ..models import (ForumUpvote, ForumDownvote) | ||
|
||
User = get_user_model() | ||
queryset = User.objects.all() | ||
|
||
|
||
class ForumUpvoteSerializer(serializers.ModelSerializer): | ||
class Meta: | ||
model = ForumUpvote | ||
fields = ("id", "user", "forum_question", "created_at") | ||
read_only_fields = ("id", "user", "created_at") | ||
|
||
def validate(self, attrs): | ||
user = self.context["request"].user | ||
forum_question = attrs["forum_question"] | ||
|
||
if ForumUpvote.objects.filter(user=user, forum_question=forum_question).exists(): | ||
raise serializers.ValidationError("You have already upvoted this forum question.") | ||
|
||
return attrs | ||
|
||
|
||
class ForumDownvoteSerializer(serializers.ModelSerializer): | ||
class Meta: | ||
model = ForumDownvote | ||
fields = ("id", "user", "forum_question", "created_at") | ||
read_only_fields = ("id", "user", "created_at") | ||
|
||
def validate(self, attrs): | ||
user = self.context["request"].user | ||
forum_question = attrs["forum_question"] | ||
|
||
if ForumDownvote.objects.filter(user=user, forum_question=forum_question).exists(): | ||
raise serializers.ValidationError("You have already downvoted this forum question.") | ||
|
||
return attrs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
from rest_framework.test import APITestCase, APIClient | ||
from rest_framework import status | ||
from django.urls import reverse | ||
from rest_framework_simplejwt.tokens import RefreshToken | ||
from faker import Faker | ||
from core.models import ForumUpvote, ForumDownvote, ForumQuestion | ||
from django.contrib.auth import get_user_model | ||
|
||
User = get_user_model() | ||
fake = Faker() | ||
|
||
|
||
class ForumUpvoteAPITest(APITestCase): | ||
def setUp(self): | ||
# Create a test user | ||
self.user = User.objects.create_user( | ||
username=fake.user_name(), | ||
password="testpassword", | ||
email=fake.email(), | ||
full_name=fake.name() | ||
) | ||
|
||
# Authenticate the test client | ||
self.client = APIClient() | ||
refresh = RefreshToken.for_user(self.user) | ||
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}') | ||
|
||
# Create a ForumQuestion | ||
self.forum_question = ForumQuestion.objects.create( | ||
title="Test Forum Question", | ||
question="This is a test question for votes.", | ||
author=self.user | ||
) | ||
|
||
# Vote data | ||
self.data = {"forum_question": self.forum_question.id} | ||
|
||
def test_create_forum_upvote(self): | ||
"""Test creating a forum vote""" | ||
# Send POST request to create a new vote | ||
response = self.client.post(reverse('forum-upvote-list'), self.data, format='json') | ||
|
||
# Assertions | ||
self.assertEqual(response.status_code, status.HTTP_201_CREATED) | ||
self.assertTrue(ForumUpvote.objects.filter(user=self.user, forum_question=self.forum_question).exists()) | ||
self.assertIn("id", response.data) | ||
self.assertIn("user", response.data) | ||
self.assertIn("forum_question", response.data) | ||
|
||
def test_delete_forum_upvote(self): | ||
"""Test deleting a forum vote""" | ||
# Create a vote to delete | ||
self.client.post(reverse('forum-upvote-list'), self.data, format='json') | ||
forum_vote = ForumUpvote.objects.get(user=self.user, forum_question=self.forum_question) | ||
|
||
# Send DELETE request to remove the vote | ||
response = self.client.delete(reverse('forum-upvote-detail', args=[forum_vote.id])) | ||
|
||
# Assertions | ||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) | ||
self.assertFalse(ForumUpvote.objects.filter(id=forum_vote.id).exists()) | ||
|
||
def test_cannot_upvote_same_forum_question_twice(self): | ||
"""Test that the same forum question cannot be voted twice""" | ||
# Create the first vote | ||
self.client.post(reverse('forum-upvote-list'), self.data, format='json') | ||
|
||
# Attempt to create a duplicate vote | ||
response = self.client.post(reverse('forum-upvote-list'), self.data, format='json') | ||
|
||
# Assertions | ||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) | ||
self.assertEqual( | ||
ForumUpvote.objects.filter(user=self.user, forum_question=self.forum_question).count(), | ||
1 # Ensure only one vote exists | ||
) | ||
|
||
def test_cannot_delete_other_users_upvote(self): | ||
"""Test that a user cannot delete another user's vote""" | ||
# Create another user and their vote | ||
other_user = User.objects.create_user( | ||
username="otheruser", | ||
password="otherpassword", | ||
email="[email protected]", | ||
full_name="Other User" | ||
) | ||
other_vote = ForumUpvote.objects.create(user=other_user, forum_question=self.forum_question) | ||
|
||
# Attempt to delete the other user's vote | ||
response = self.client.delete(reverse('forum-upvote-detail', args=[other_vote.id])) | ||
|
||
# Assertions | ||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) | ||
self.assertTrue(ForumUpvote.objects.filter(id=other_vote.id).exists()) | ||
|
||
def test_get_list_upvote_pagination(self): | ||
"""Test that the upvotes are paginated""" | ||
# Create 15 upvotes | ||
# for _ in range(15): | ||
ForumUpvote.objects.create(user=self.user, forum_question=self.forum_question) | ||
|
||
# Send GET request to retrieve the upvotes | ||
response = self.client.get(reverse('forum-upvote-list')) | ||
|
||
# Assertions | ||
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
self.assertIn("count", response.data) | ||
self.assertIn("next", response.data) | ||
self.assertIn("previous", response.data) | ||
self.assertIn("results", response.data) | ||
self.assertIn("id", response.data['results'][0]) | ||
self.assertIn("user", response.data['results'][0]) | ||
self.assertIn("forum_question", response.data['results'][0]) | ||
self.assertIn("created_at", response.data['results'][0]) | ||
|
||
|
||
class ForumDownvoteAPITest(APITestCase): | ||
def setUp(self): | ||
# Create a test user | ||
self.user = User.objects.create_user( | ||
username=fake.user_name(), | ||
password="testpassword", | ||
email=fake.email(), | ||
full_name=fake.name() | ||
) | ||
|
||
# Authenticate the test client | ||
self.client = APIClient() | ||
refresh = RefreshToken.for_user(self.user) | ||
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}') | ||
|
||
# Create a ForumQuestion | ||
self.forum_question = ForumQuestion.objects.create( | ||
title="Test Forum Question", | ||
question="This is a test question for votes.", | ||
author=self.user | ||
) | ||
|
||
# Vote data | ||
self.data = {"forum_question": self.forum_question.id} | ||
|
||
def test_create_forum_downvote(self): | ||
"""Test creating a forum downvote""" | ||
# Send POST request to create a new downvote | ||
response = self.client.post(reverse('forum-downvote-list'), self.data, format='json') | ||
|
||
# Assertions | ||
self.assertEqual(response.status_code, status.HTTP_201_CREATED) | ||
self.assertTrue(ForumDownvote.objects.filter(user=self.user, forum_question=self.forum_question).exists()) | ||
self.assertIn("id", response.data) | ||
self.assertIn("user", response.data) | ||
self.assertIn("forum_question", response.data) | ||
|
||
def test_delete_forum_downvote(self): | ||
"""Test deleting a forum downvote""" | ||
# Create a downvote to delete | ||
self.client.post(reverse('forum-downvote-list'), self.data, format='json') | ||
forum_vote = ForumDownvote.objects.get(user=self.user, forum_question=self.forum_question) | ||
|
||
# Send DELETE request to remove the downvote | ||
response = self.client.delete(reverse('forum-downvote-detail', args=[forum_vote.id])) | ||
|
||
# Assertions | ||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) | ||
self.assertFalse(ForumDownvote.objects.filter(id=forum_vote.id).exists()) | ||
|
||
def test_cannot_downvote_same_forum_question_twice(self): | ||
"""Test that the same forum question cannot be downvoted twice""" | ||
# Create the first downvote | ||
self.client.post(reverse('forum-downvote-list'), self.data, format='json') | ||
|
||
# Attempt to create a duplicate downvote | ||
response = self.client.post(reverse('forum-downvote-list'), self.data, format='json') | ||
|
||
# Assertions | ||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) | ||
self.assertEqual( | ||
ForumDownvote.objects.filter(user=self.user, forum_question=self.forum_question).count(), | ||
1 # Ensure only one downvote exists | ||
) | ||
|
||
def test_cannot_delete_other_users_downvote(self): | ||
"""Test that a user cannot delete another user's vote""" | ||
# Create another user and their vote | ||
other_user = User.objects.create_user( | ||
username="otheruser", | ||
password="otherpassword", | ||
email="[email protected]", | ||
full_name="Other User" | ||
) | ||
other_vote = ForumDownvote.objects.create(user=other_user, forum_question=self.forum_question) | ||
|
||
# Attempt to delete the other user's vote | ||
response = self.client.delete(reverse('forum-downvote-detail', args=[other_vote.id])) | ||
|
||
# Assertions | ||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) | ||
self.assertTrue(ForumDownvote.objects.filter(id=other_vote.id).exists()) | ||
|
||
def test_get_list_downvote_pagination(self): | ||
"""Test that the upvotes are paginated""" | ||
# Create 15 upvotes | ||
# for _ in range(15): | ||
ForumDownvote.objects.create(user=self.user, forum_question=self.forum_question) | ||
|
||
# Send GET request to retrieve the upvotes | ||
response = self.client.get(reverse('forum-downvote-list')) | ||
|
||
# Assertions | ||
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
self.assertIn("count", response.data) | ||
self.assertIn("next", response.data) | ||
self.assertIn("previous", response.data) | ||
self.assertIn("results", response.data) | ||
self.assertIn("id", response.data['results'][0]) | ||
self.assertIn("user", response.data['results'][0]) | ||
self.assertIn("forum_question", response.data['results'][0]) | ||
self.assertIn("created_at", response.data['results'][0]) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.