Skip to content

Commit 0ba6183

Browse files
feat:follower notification added
1 parent 8198aa3 commit 0ba6183

File tree

4 files changed

+253
-0
lines changed

4 files changed

+253
-0
lines changed

thenewboston/general/enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ class NotificationType(Enum):
1515
POST_COIN_TRANSFER = 'POST_COIN_TRANSFER'
1616
POST_COMMENT = 'POST_COMMENT'
1717
POST_LIKE = 'POST_LIKE'
18+
PROFILE_FOLLOW = 'PROFILE_FOLLOW'

thenewboston/social/serializers/follower.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ def create(self, validated_data):
4646
def validate(self, data):
4747
request = self.context.get('request')
4848

49+
if request.user == data['following']:
50+
raise serializers.ValidationError('You cannot follow yourself.')
51+
4952
if Follower.objects.filter(follower=request.user, following=data['following']).exists():
5053
raise serializers.ValidationError('This relationship already exists.')
5154

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import pytest
2+
from django.contrib.auth import get_user_model
3+
from model_bakery import baker
4+
from rest_framework import status
5+
6+
from thenewboston.notifications.models import Notification
7+
from thenewboston.social.models import Follower
8+
9+
User = get_user_model()
10+
11+
12+
@pytest.mark.django_db
13+
class TestFollowerViewSet:
14+
def test_unauthenticated_user_cannot_create_follower(self, api_client):
15+
"""Test that unauthenticated users cannot follow others."""
16+
user_to_follow = baker.make(User)
17+
18+
response = api_client.post('/api/followers', {'following': user_to_follow.id})
19+
assert response.status_code == status.HTTP_401_UNAUTHORIZED
20+
21+
def test_authenticated_user_can_follow_another_user(self, authenticated_api_client):
22+
"""Test successful follow and notification creation."""
23+
user_to_follow = baker.make(User)
24+
25+
initial_notification_count = Notification.objects.count()
26+
27+
response = authenticated_api_client.post('/api/followers', {'following': user_to_follow.id})
28+
29+
assert response.status_code == status.HTTP_201_CREATED
30+
assert response.data['follower']['id'] == authenticated_api_client.forced_user.id
31+
assert response.data['following']['id'] == user_to_follow.id
32+
33+
# Verify follower relationship was created
34+
assert Follower.objects.filter(follower=authenticated_api_client.forced_user, following=user_to_follow).exists()
35+
36+
# Verify notification was created for the followed user
37+
assert Notification.objects.count() == initial_notification_count + 1
38+
notification = Notification.objects.latest('created_date')
39+
assert notification.owner == user_to_follow
40+
assert notification.payload['notification_type'] == 'PROFILE_FOLLOW'
41+
assert notification.payload['follower']['id'] == authenticated_api_client.forced_user.id
42+
43+
def test_user_cannot_follow_themselves(self, authenticated_api_client):
44+
"""Test that users cannot follow themselves."""
45+
user = authenticated_api_client.forced_user
46+
47+
response = authenticated_api_client.post('/api/followers', {'following': user.id})
48+
49+
assert response.status_code == status.HTTP_400_BAD_REQUEST
50+
assert 'You cannot follow yourself' in str(response.data)
51+
52+
# Verify no follower relationship was created
53+
assert not Follower.objects.filter(follower=user, following=user).exists()
54+
55+
# Verify no notification was created
56+
assert Notification.objects.filter(owner=user).count() == 0
57+
58+
def test_user_cannot_follow_same_user_twice(self, authenticated_api_client):
59+
"""Test that duplicate follow relationships are prevented."""
60+
user_to_follow = baker.make(User)
61+
62+
# First follow - should succeed
63+
response1 = authenticated_api_client.post('/api/followers', {'following': user_to_follow.id})
64+
assert response1.status_code == status.HTTP_201_CREATED
65+
66+
# Second follow attempt - should fail
67+
response2 = authenticated_api_client.post('/api/followers', {'following': user_to_follow.id})
68+
assert response2.status_code == status.HTTP_400_BAD_REQUEST
69+
assert 'This relationship already exists' in str(response2.data)
70+
71+
# Verify only one relationship exists
72+
assert (
73+
Follower.objects.filter(follower=authenticated_api_client.forced_user, following=user_to_follow).count()
74+
== 1
75+
)
76+
77+
# Verify only one notification was created
78+
assert Notification.objects.filter(owner=user_to_follow).count() == 1
79+
80+
def test_user_can_unfollow(self, authenticated_api_client):
81+
"""Test that users can unfollow others."""
82+
user_to_follow = baker.make(User)
83+
84+
# Create follow relationship
85+
Follower.objects.create(follower=authenticated_api_client.forced_user, following=user_to_follow)
86+
87+
# Unfollow
88+
response = authenticated_api_client.delete(f'/api/followers/{user_to_follow.id}')
89+
90+
assert response.status_code == status.HTTP_204_NO_CONTENT
91+
assert not Follower.objects.filter(
92+
follower=authenticated_api_client.forced_user, following=user_to_follow
93+
).exists()
94+
95+
def test_user_can_follow_multiple_users(self, authenticated_api_client):
96+
"""Test that a user can follow multiple different users."""
97+
user1 = baker.make(User)
98+
user2 = baker.make(User)
99+
100+
# Follow user1
101+
response1 = authenticated_api_client.post('/api/followers', {'following': user1.id})
102+
assert response1.status_code == status.HTTP_201_CREATED
103+
104+
# Follow user2
105+
response2 = authenticated_api_client.post('/api/followers', {'following': user2.id})
106+
assert response2.status_code == status.HTTP_201_CREATED
107+
108+
# Verify both relationships exist
109+
assert Follower.objects.filter(follower=authenticated_api_client.forced_user).count() == 2
110+
assert Follower.objects.filter(follower=authenticated_api_client.forced_user, following=user1).exists()
111+
assert Follower.objects.filter(follower=authenticated_api_client.forced_user, following=user2).exists()
112+
113+
# Verify notifications were created for both users
114+
assert Notification.objects.filter(owner=user1).count() == 1
115+
assert Notification.objects.filter(owner=user2).count() == 1
116+
117+
def test_multiple_users_can_follow_same_user(self, api_client):
118+
"""Test that multiple users can follow the same user."""
119+
user1 = baker.make(User)
120+
user2 = baker.make(User)
121+
target_user = baker.make(User)
122+
123+
# User1 follows target_user
124+
api_client.force_authenticate(user=user1)
125+
response1 = api_client.post('/api/followers', {'following': target_user.id})
126+
assert response1.status_code == status.HTTP_201_CREATED
127+
128+
# User2 follows target_user
129+
api_client.force_authenticate(user=user2)
130+
response2 = api_client.post('/api/followers', {'following': target_user.id})
131+
assert response2.status_code == status.HTTP_201_CREATED
132+
133+
# Verify both relationships exist
134+
assert Follower.objects.filter(following=target_user).count() == 2
135+
136+
# Verify target_user received notifications from both followers
137+
notifications = Notification.objects.filter(owner=target_user).order_by('created_date')
138+
assert notifications.count() == 2
139+
assert notifications[0].payload['follower']['id'] == user1.id
140+
assert notifications[1].payload['follower']['id'] == user2.id
141+
142+
def test_list_followers(self, api_client):
143+
"""Test listing followers with self_following field."""
144+
user1 = baker.make(User)
145+
user2 = baker.make(User)
146+
user3 = baker.make(User)
147+
148+
# user2 and user3 follow user1
149+
Follower.objects.create(follower=user2, following=user1)
150+
Follower.objects.create(follower=user3, following=user1)
151+
152+
# user2 also follows user3
153+
Follower.objects.create(follower=user2, following=user3)
154+
155+
# Request as user2
156+
api_client.force_authenticate(user=user2)
157+
response = api_client.get(f'/api/followers?following={user1.id}')
158+
159+
assert response.status_code == status.HTTP_200_OK
160+
results = response.data['results']
161+
assert len(results) == 2
162+
163+
# Check self_following field
164+
for result in results:
165+
if result['follower']['id'] == user3.id:
166+
# user2 follows user3
167+
assert result['self_following'] is True
168+
elif result['follower']['id'] == user2.id:
169+
# user2 looking at themselves
170+
assert result['self_following'] is False
171+
172+
def test_follower_notification_payload_structure(self, authenticated_api_client):
173+
"""Test that notification payload has correct structure."""
174+
user_to_follow = baker.make(User)
175+
176+
response = authenticated_api_client.post('/api/followers', {'following': user_to_follow.id})
177+
assert response.status_code == status.HTTP_201_CREATED
178+
179+
notification = Notification.objects.get(owner=user_to_follow)
180+
181+
# Verify payload structure
182+
assert 'notification_type' in notification.payload
183+
assert 'follower' in notification.payload
184+
assert notification.payload['notification_type'] == 'PROFILE_FOLLOW'
185+
186+
# Verify follower data includes essential fields
187+
follower_data = notification.payload['follower']
188+
assert 'id' in follower_data
189+
assert 'username' in follower_data
190+
assert follower_data['id'] == authenticated_api_client.forced_user.id
191+
assert follower_data['username'] == authenticated_api_client.forced_user.username
192+
193+
def test_follow_nonexistent_user(self, authenticated_api_client):
194+
"""Test that following a non-existent user fails gracefully."""
195+
response = authenticated_api_client.post('/api/followers', {'following': 99999})
196+
197+
assert response.status_code == status.HTTP_400_BAD_REQUEST
198+
199+
def test_notification_not_created_for_invalid_follow(self, authenticated_api_client):
200+
"""Test that notifications are not created when follow validation fails."""
201+
user = authenticated_api_client.forced_user
202+
initial_notification_count = Notification.objects.count()
203+
204+
# Try to follow self (should fail)
205+
response = authenticated_api_client.post('/api/followers', {'following': user.id})
206+
assert response.status_code == status.HTTP_400_BAD_REQUEST
207+
208+
# Verify no notification was created
209+
assert Notification.objects.count() == initial_notification_count
210+
211+
def test_follower_and_following_user_details_in_response(self, authenticated_api_client):
212+
"""Test that the API response includes full user details for follower and following."""
213+
user_to_follow = baker.make(User, username='target_user')
214+
215+
response = authenticated_api_client.post('/api/followers', {'following': user_to_follow.id})
216+
217+
assert response.status_code == status.HTTP_201_CREATED
218+
219+
# Check follower details in response
220+
assert 'follower' in response.data
221+
assert response.data['follower']['id'] == authenticated_api_client.forced_user.id
222+
assert response.data['follower']['username'] == authenticated_api_client.forced_user.username
223+
224+
# Check following details in response
225+
assert 'following' in response.data
226+
assert response.data['following']['id'] == user_to_follow.id
227+
assert response.data['following']['username'] == 'target_user'

thenewboston/social/views/follower.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@
33
from rest_framework.permissions import IsAuthenticated
44
from rest_framework.response import Response
55

6+
from thenewboston.general.enums import MessageType, NotificationType
67
from thenewboston.general.pagination import CustomPageNumberPagination
78
from thenewboston.general.permissions import IsObjectFollowerOrReadOnly
9+
from thenewboston.notifications.consumers import NotificationConsumer
10+
from thenewboston.notifications.models import Notification
11+
from thenewboston.notifications.serializers.notification import NotificationReadSerializer
12+
from thenewboston.users.serializers.user import UserReadSerializer
813

914
from ..filters.follower import FollowerFilter
1015
from ..models import Follower
@@ -18,10 +23,27 @@ class FollowerViewSet(viewsets.ModelViewSet):
1823
permission_classes = [IsAuthenticated, IsObjectFollowerOrReadOnly]
1924
queryset = Follower.objects.all()
2025

26+
@staticmethod
27+
def notify_profile_owner(follower, request):
28+
notification = Notification.objects.create(
29+
owner=follower.following,
30+
payload={
31+
'follower': UserReadSerializer(follower.follower, context={'request': request}).data,
32+
'notification_type': NotificationType.PROFILE_FOLLOW.value,
33+
},
34+
)
35+
36+
notification_data = NotificationReadSerializer(notification, context={'request': request}).data
37+
38+
NotificationConsumer.stream_notification(
39+
message_type=MessageType.CREATE_NOTIFICATION, notification_data=notification_data
40+
)
41+
2142
def create(self, request, *args, **kwargs):
2243
serializer = self.get_serializer(data=request.data, context={'request': request})
2344
serializer.is_valid(raise_exception=True)
2445
follower = serializer.save()
46+
self.notify_profile_owner(follower, request)
2547
read_serializer = FollowerReadSerializer(follower, context={'request': request})
2648

2749
return Response(read_serializer.data, status=status.HTTP_201_CREATED)

0 commit comments

Comments
 (0)