Skip to content

Commit 1be76ee

Browse files
Merge pull request #190 from UNLV-CS472-672/mute_block_report_tests
PR 7: Refactor Mute/Blocking Logic and Tests for User Moderation System
2 parents 583b091 + 6c71f63 commit 1be76ee

File tree

4 files changed

+264
-30
lines changed

4 files changed

+264
-30
lines changed

backend/apps/chat/consumers.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from asgiref.sync import sync_to_async
44
from .models import Message, Chat, BlockedUser, MutedUser
55
from django.contrib.auth.models import User
6+
from channels.db import database_sync_to_async
67

78
# https://medium.com/@farad.dev/how-to-build-a-real-time-chat-app-using-django-channels-2ba2621ea972
89
class ChatConsumer(AsyncWebsocketConsumer):
@@ -28,6 +29,10 @@ async def disconnect(self, close_code):
2829
self.channel_name
2930
)
3031

32+
@database_sync_to_async
33+
def user_blocked_or_muted(self, chat, sender):
34+
Chat.objects.user_blocked_or_muted(chat, sender)
35+
3136
# This function receive messages from WebSocket.
3237
async def receive(self, text_data):
3338
# Message is received
@@ -38,8 +43,7 @@ async def receive(self, text_data):
3843
try:
3944
chat = await sync_to_async(Chat.objects.get)(name=self.room_name)
4045
sender = await sync_to_async(User.objects.get)(id=self.user_id)
41-
receiver = await sync_to_async(Chat.objects.get_other_user)(chat, sender)
42-
await self.blocked_or_muted(sender, receiver)
46+
await self.user_blocked_or_muted(chat, sender)
4347

4448
# Try to save the message
4549
username, id, timestamp = await self.save_message(self.user_id, self.room_name, message)
@@ -146,17 +150,4 @@ def save_message(self, id, room_name, content):
146150
user = User.objects.get(id=id)
147151
chat = Chat.objects.get(name=room_name)
148152
message = Message.objects.create(content=content, chat=chat, sender=user) # Creates and saves message
149-
return user.username, message.id, message.timestamp
150-
151-
@staticmethod
152-
async def blocked_or_muted(sender, receiver):
153-
sender_profile = await sync_to_async(lambda: sender.profile)()
154-
receiver_profile = await sync_to_async(lambda: receiver.profile)()
155-
156-
blocked = await sync_to_async(BlockedUser.objects.filter(blocked_by=receiver_profile, blocked_user=sender_profile).exists)()
157-
if blocked:
158-
raise PermissionError("You are blocked by this user.")
159-
160-
muted = await sync_to_async(MutedUser.objects.filter(muted_by=receiver_profile, muted_user=sender_profile).exists)()
161-
if muted:
162-
raise PermissionError("You are muted by this user.")
153+
return user.username, message.id, message.timestamp

backend/apps/chat/managers.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ def get_or_create_chat(self, user1, user2):
1212

1313
def get_other_user(self, chat, user):
1414
return chat.user2 if chat.user1 == user else chat.user1
15+
16+
def user_blocked_or_muted(self, chat, sender):
17+
receiver = self.get_other_user(chat, sender)
18+
from .models import BlockedUser, MutedUser # avoid circular import
19+
20+
if BlockedUser.objects.filter(blocked_by=receiver.profile, blocked_user=sender.profile).exists():
21+
raise PermissionError("You are blocked by this user.")
22+
23+
if MutedUser.objects.filter(muted_by=receiver.profile, muted_user=sender.profile).exists():
24+
raise PermissionError("You are muted by this user.")
1525

1626
class MessageManager(models.Manager):
1727
def create_message(self, chat, sender, content):
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import pytest
2+
from django.contrib.auth.models import User
3+
from django.contrib.auth import get_user_model
4+
from .models import Chat, BlockedUser, MutedUser
5+
from apps.users.models import Profile
6+
7+
@pytest.mark.django_db
8+
def test_ensure_user_allowed_blocks_as_expected():
9+
User = get_user_model()
10+
u1 = User.objects.create_user("u1","p")
11+
u2 = User.objects.create_user("u2","p")
12+
Profile.objects.create(user=u1, role=1)
13+
Profile.objects.create(user=u2, role=1)
14+
chat, _ = Chat.objects.get_or_create_chat(u1, u2)
15+
16+
BlockedUser.objects.create(blocked_by=u2.profile, blocked_user=u1.profile)
17+
with pytest.raises(PermissionError, match="You are blocked by this user."):
18+
Chat.objects.user_blocked_or_muted(chat, u1)
19+
20+
@pytest.mark.django_db
21+
def test_user_blocked_or_muted_mutes_as_expected():
22+
u1 = User.objects.create_user("u1_muted", password="pass")
23+
u2 = User.objects.create_user("u2_muted", password="pass")
24+
Profile.objects.create(user=u1, role=1)
25+
Profile.objects.create(user=u2, role=1)
26+
chat, _ = Chat.objects.get_or_create_chat(u1, u2)
27+
28+
# mute u1 by u2
29+
MutedUser.objects.create(muted_by=u2.profile, muted_user=u1.profile)
30+
with pytest.raises(PermissionError, match="You are muted by this user."):
31+
Chat.objects.user_blocked_or_muted(chat, u1)
32+
33+
@pytest.mark.django_db
34+
def test_user_blocked_or_muted_allows_when_clear():
35+
u1 = User.objects.create_user("u1_clear", password="pass")
36+
u2 = User.objects.create_user("u2_clear", password="pass")
37+
Profile.objects.create(user=u1, role=1)
38+
Profile.objects.create(user=u2, role=1)
39+
chat, _ = Chat.objects.get_or_create_chat(u1, u2)
40+
Chat.objects.user_blocked_or_muted(chat, u1) # should not raise error
41+
42+
@pytest.mark.django_db
43+
def test_get_other_user_returns_correct_partner():
44+
u1 = User.objects.create_user("u1_other", password="pass")
45+
u2 = User.objects.create_user("u2_other", password="pass")
46+
# profiles must exist but not used here
47+
Profile.objects.create(user=u1, role=1)
48+
Profile.objects.create(user=u2, role=1)
49+
50+
chat, _ = Chat.objects.get_or_create_chat(u1, u2)
51+
# whoever you pass in, you get the other one back
52+
assert Chat.objects.get_other_user(chat, u1) == u2
53+
assert Chat.objects.get_other_user(chat, u2) == u1

backend/apps/chat/tests.py

Lines changed: 194 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
from django.contrib.auth.models import User
2+
from apps.users.models import Profile
23
from django.urls import reverse
34
from rest_framework import status
45
from rest_framework.test import APITestCase, APIClient
5-
from .models import Chat, Message
6+
from django.test import TestCase
7+
from apps.chat.models import Chat, Message, MutedUser, BlockedUser, ReportedUser
68
from apps.chat.consumers import ChatConsumer
9+
from django.utils.timezone import now
10+
import time
11+
from django.core.exceptions import ValidationError
712

813
def test_chat_consumer_init():
914
consumer = ChatConsumer()
1015
assert consumer is not None
1116

1217
class ChatAPITestCase(APITestCase):
13-
1418
def setUp(self):
1519
self.user1 = User.objects.create_user(username='alice', password='pass1234')
1620
self.user2 = User.objects.create_user(username='bob', password='pass1234')
@@ -63,21 +67,197 @@ def test_send_message(self):
6367
self.assertEqual(message.content, 'Hello Bob!')
6468
self.assertEqual(message.sender, self.user1)
6569

70+
def test_create_chat_missing_user2(self):
71+
url = reverse('chat-list')
72+
response = self.client.post(url, {}) # Missing "user2"
73+
self.assertEqual(response.status_code, 400)
74+
self.assertIn("user2", response.data)
6675

67-
def test_list_chat_messages(self):
68-
chat, created = Chat.objects.get_or_create_chat(self.user1, self.user2)
69-
Message.objects.create(chat=chat, sender=self.user1, content='Hi Bob!')
70-
Message.objects.create(chat=chat, sender=self.user2, content='Hey Alice!')
76+
def test_create_chat_user2_not_found(self):
77+
url = reverse('chat-list')
78+
response = self.client.post(url, {"user2": 9999}) # non-existent ID
79+
self.assertEqual(response.status_code, 400)
80+
self.assertIn("user2", response.data)
7181

72-
url = reverse('chat-messages', kwargs={'pk': chat.id})
82+
def test_create_chat_already_exists(self):
83+
Chat.objects.get_or_create_chat(self.user1, self.user2)
84+
url = reverse('chat-list')
85+
response = self.client.post(url, {"user2": self.user2.id})
86+
self.assertEqual(response.status_code, 400)
87+
self.assertIn("Chat already exists", str(response.data))
88+
89+
def test_get_messages_custom_action(self):
90+
chat, _ = Chat.objects.get_or_create_chat(self.user1, self.user2)
91+
Message.objects.create(chat=chat, sender=self.user1, content="Hello!")
92+
url = reverse('chat-messages', args=[chat.id]) # Must match router name
93+
response = self.client.get(url)
94+
self.assertEqual(response.status_code, 200)
95+
self.assertEqual(len(response.data), 1)
96+
self.assertEqual(response.data[0]['content'], "Hello!")
97+
98+
# https://chatgpt.com/share/681180bd-8930-800c-9f58-8f4de378d8e2
99+
class ChatModelTests(TestCase):
100+
def setUp(self):
101+
self.user1 = User.objects.create_user(username='alice', password='pass1234')
102+
self.user2 = User.objects.create_user(username='bob', password='pass1234')
103+
self.profile1 = Profile.objects.create(user=self.user1, role=1)
104+
self.profile2 = Profile.objects.create(user=self.user2, role=1)
105+
106+
def test_chat_str(self):
107+
chat = Chat.objects.create(user1=self.user1, user2=self.user2)
108+
expected_str = f"Chat between {self.user1} and {self.user2}"
109+
self.assertEqual(str(chat), expected_str)
110+
111+
def test_message_str(self):
112+
chat = Chat.objects.create(user1=self.user1, user2=self.user2)
113+
message = Message.objects.create(chat=chat, sender=self.user1, content="Hello!", timestamp=now())
114+
expected_str = f"From {self.user1} in Chat {chat.id} at {message.timestamp}"
115+
self.assertEqual(str(message), expected_str)
116+
117+
def test_muted_user_str(self):
118+
muted = MutedUser.objects.create(muted_user=self.profile2, muted_by=self.profile1)
119+
expected_str = f"{self.profile1} muted {self.profile2}"
120+
self.assertEqual(str(muted), expected_str)
121+
122+
def test_blocked_user_str(self):
123+
blocked = BlockedUser.objects.create(blocked_user=self.profile2, blocked_by=self.profile1)
124+
expected_str = f"{self.profile1} blocked {self.profile2}"
125+
self.assertEqual(str(blocked), expected_str)
126+
127+
def test_reported_user_str(self):
128+
reported = ReportedUser.objects.create(reported_user=self.profile2, reported_by=self.profile1, reason="Spam")
129+
expected_str = f"{self.profile1} reported {self.profile2}"
130+
self.assertEqual(str(reported), expected_str)
131+
132+
def test_muted_user_clean_valid(self):
133+
muted = MutedUser(muted_user=self.profile2, muted_by=self.profile1)
134+
muted.clean() # Should not raise
135+
136+
def test_muted_user_clean_invalid(self):
137+
muted = MutedUser(muted_user=self.profile1, muted_by=self.profile1)
138+
with self.assertRaises(ValidationError) as context:
139+
muted.clean()
140+
self.assertIn("You cannot mute yourself.", str(context.exception))
141+
142+
def test_blocked_user_clean_valid(self):
143+
blocked = BlockedUser(blocked_user=self.profile2, blocked_by=self.profile1)
144+
blocked.clean() # should not raise
145+
146+
def test_blocked_user_clean_invalid(self):
147+
blocked = BlockedUser(blocked_user=self.profile1, blocked_by=self.profile1)
148+
with self.assertRaises(ValidationError) as context:
149+
blocked.clean()
150+
self.assertIn("You cannot block yourself.", str(context.exception))
151+
152+
class ChatManagerTests(TestCase):
153+
def setUp(self):
154+
self.user1 = User.objects.create_user(username='alice', password='pass1234')
155+
self.user2 = User.objects.create_user(username='bob', password='pass1234')
156+
self.profile1 = Profile.objects.create(user=self.user1, role=1)
157+
self.profile2 = Profile.objects.create(user=self.user2, role=1)
158+
self.chat, _ = Chat.objects.get_or_create_chat(self.user1, self.user2)
159+
160+
def test_get_or_create_chat_orders_users(self):
161+
Chat.objects.all().delete()
162+
chat1, created1 = Chat.objects.get_or_create_chat(self.user2, self.user1)
163+
self.assertTrue(created1)
164+
self.assertEqual(chat1.user1, min(self.user1, self.user2, key=lambda u: u.id))
165+
self.assertEqual(chat1.user2, max(self.user1, self.user2, key=lambda u: u.id))
166+
167+
# should not create a new one
168+
chat2, created2 = Chat.objects.get_or_create_chat(self.user1, self.user2)
169+
self.assertFalse(created2)
170+
self.assertEqual(chat1.id, chat2.id)
171+
172+
def test_get_other_user(self):
173+
chat, _ = Chat.objects.get_or_create_chat(self.user1, self.user2)
174+
self.assertEqual(Chat.objects.get_other_user(chat, self.user1), self.user2)
175+
self.assertEqual(Chat.objects.get_other_user(chat, self.user2), self.user1)
176+
177+
def test_user_blocked_or_muted_blocked(self):
178+
chat, _ = Chat.objects.get_or_create_chat(self.user1, self.user2)
179+
BlockedUser.objects.create(blocked_by=self.profile2, blocked_user=self.profile1)
180+
181+
with self.assertRaises(PermissionError) as ctx:
182+
Chat.objects.user_blocked_or_muted(chat, self.user1)
183+
self.assertIn("blocked", str(ctx.exception))
184+
185+
def test_user_blocked_or_muted_muted(self):
186+
chat, _ = Chat.objects.get_or_create_chat(self.user1, self.user2)
187+
MutedUser.objects.create(muted_by=self.profile2, muted_user=self.profile1)
188+
189+
with self.assertRaises(PermissionError) as ctx:
190+
Chat.objects.user_blocked_or_muted(chat, self.user1)
191+
self.assertIn("muted", str(ctx.exception))
192+
193+
def test_create_message_and_update_timestamp(self):
194+
old_updated_at = self.chat.updatedAt
195+
time.sleep(0.01) # Make sure timestamp will differ
196+
msg = Message.objects.create_message(chat=self.chat, sender=self.user1, content="hello!")
197+
198+
self.chat.refresh_from_db()
199+
self.assertEqual(msg.content, "hello!")
200+
self.assertEqual(msg.chat, self.chat)
201+
self.assertGreater(self.chat.updatedAt, old_updated_at)
202+
203+
class MuteBlockReportAPITests(APITestCase):
204+
def setUp(self):
205+
self.user1 = User.objects.create_user(username='alice', password='pass1234')
206+
self.user2 = User.objects.create_user(username='bob', password='pass1234')
207+
self.profile1 = Profile.objects.create(user=self.user1, role=1)
208+
self.profile2 = Profile.objects.create(user=self.user2, role=1)
209+
210+
self.client = APIClient()
211+
self.client.force_authenticate(user=self.user1)
212+
213+
def test_mute_user(self):
214+
url = reverse('mute-list')
215+
payload = {"muted_user": self.profile2.id}
216+
response = self.client.post(url, payload)
217+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
218+
self.assertEqual(MutedUser.objects.count(), 1)
219+
220+
def test_duplicate_mute_user(self):
221+
MutedUser.objects.create(muted_user=self.profile2, muted_by=self.profile1)
222+
url = reverse('mute-list')
223+
payload = {"muted_user": self.profile2.id}
224+
response = self.client.post(url, payload)
225+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
226+
227+
def test_block_user(self):
228+
url = reverse('block-list')
229+
payload = {"blocked_user": self.profile2.id}
230+
response = self.client.post(url, payload)
231+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
232+
self.assertEqual(BlockedUser.objects.count(), 1)
233+
234+
def test_duplicate_block_user(self):
235+
BlockedUser.objects.create(blocked_user=self.profile2, blocked_by=self.profile1)
236+
url = reverse('block-list')
237+
payload = {"blocked_user": self.profile2.id}
238+
response = self.client.post(url, payload)
239+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
240+
241+
def test_report_user(self):
242+
url = reverse('report-list')
243+
payload = {
244+
"reported_user": self.profile2.id,
245+
"reason": "Spam"
246+
}
247+
response = self.client.post(url, payload)
248+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
249+
self.assertEqual(ReportedUser.objects.count(), 1)
250+
251+
def test_list_blocked_users(self):
252+
BlockedUser.objects.create(blocked_user=self.profile2, blocked_by=self.profile1)
253+
url = reverse('block-list')
73254
response = self.client.get(url)
74255
self.assertEqual(response.status_code, status.HTTP_200_OK)
75-
self.assertEqual(len(response.data), 2)
76-
self.assertEqual(response.data[0]['content'], 'Hi Bob!')
77-
self.assertEqual(response.data[1]['content'], 'Hey Alice!')
256+
self.assertEqual(len(response.data), 1)
78257

79-
def test_unauthenticated_access(self):
80-
self.client.logout()
81-
url = reverse('chat-list')
258+
def test_list_reported_users(self):
259+
ReportedUser.objects.create(reported_user=self.profile2, reported_by=self.profile1, reason="Spam")
260+
url = reverse('report-list')
82261
response = self.client.get(url)
83-
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
262+
self.assertEqual(response.status_code, status.HTTP_200_OK)
263+
self.assertEqual(len(response.data), 1)

0 commit comments

Comments
 (0)