Files
MathApp/notifications/tests.py
Zareian 663252a260 Remove the guest system entirely: no is_guest/device_id, no guest throttles, phone login is the only way in
accounts.0008 drops the columns and refuses to run while guest rows exist.
Docs describe the current state; tests that only proved guest behaviour are gone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 18:20:10 +03:30

175 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from unittest.mock import patch
from django.test import TestCase
from accounts.services import SMSDeliveryError
from .models import Notification
from .services import notify
class NotifyInAppChannelTests(TestCase):
"""
channel='in_app' (پیش‌فرض) باید دقیقاً همون رفتارِ قبلیِ notify() رو
داشته باشه: صرفاً یک Notification.objects.create ساده، بدونِ
فراخوانیِ send_sms.
"""
@patch('notifications.services.send_sms')
def test_default_channel_creates_notification_and_does_not_send_sms(self, mock_send_sms):
result = notify(user_id=1, type='order_paid', title='t', body='b', related_id=5)
self.assertIsInstance(result, Notification)
self.assertEqual(Notification.objects.count(), 1)
notification = Notification.objects.get()
self.assertEqual(notification.user_id, 1)
self.assertEqual(notification.type, 'order_paid')
self.assertEqual(notification.title, 't')
self.assertEqual(notification.body, 'b')
self.assertEqual(notification.related_id, 5)
mock_send_sms.assert_not_called()
@patch('notifications.services.send_sms')
def test_explicit_in_app_channel_same_as_default(self, mock_send_sms):
result = notify(user_id=2, type='xp_earned', title='t', channel='in_app')
self.assertIsInstance(result, Notification)
self.assertEqual(Notification.objects.count(), 1)
mock_send_sms.assert_not_called()
class NotifySmsChannelTests(TestCase):
"""
channel='sms' نباید هیچ Notificationی بسازه؛ فقط باید
accounts.services.send_sms رو صدا بزنه (بدونِ تکرارِ منطقِ provider).
"""
@patch('notifications.services.send_sms')
def test_sms_channel_calls_send_sms_with_body_and_creates_no_notification(self, mock_send_sms):
result = notify(
user_id=3, type='order_paid', title='t', body='پیام پیامکی',
channel='sms', phone='0910000000',
)
mock_send_sms.assert_called_once_with(phone='0910000000', message='پیام پیامکی')
self.assertIsNone(result)
self.assertEqual(Notification.objects.count(), 0)
@patch('notifications.services.send_sms')
def test_sms_channel_falls_back_to_title_when_body_empty(self, mock_send_sms):
notify(user_id=3, type='order_paid', title='fallback-title', channel='sms', phone='0910000000')
mock_send_sms.assert_called_once_with(phone='0910000000', message='fallback-title')
def test_sms_channel_without_phone_raises_value_error(self):
with self.assertRaises(ValueError):
notify(user_id=3, type='order_paid', title='t', channel='sms')
self.assertEqual(Notification.objects.count(), 0)
@patch('notifications.services.send_sms')
def test_sms_delivery_error_propagates_and_creates_no_notification(self, mock_send_sms):
mock_send_sms.side_effect = SMSDeliveryError('boom')
with self.assertRaises(SMSDeliveryError):
notify(user_id=3, type='order_paid', title='t', channel='sms', phone='0910000000')
self.assertEqual(Notification.objects.count(), 0)
class NotifyInvalidChannelTests(TestCase):
def test_unsupported_channel_raises_value_error_and_creates_nothing(self):
with self.assertRaises(ValueError):
notify(user_id=3, type='order_paid', title='t', channel='email')
self.assertEqual(Notification.objects.count(), 0)
from django.core.cache import cache
from rest_framework.test import APIClient
from accounts.authentication import issue_access_token
from accounts.models import User
class NotificationListCapTests(TestCase):
def setUp(self):
cache.clear()
self.client = APIClient()
self.user = User.objects.create(phone='09000000065', name='n')
self.token = issue_access_token(self.user.id)
def test_returns_at_most_100_newest_with_correct_unread_count(self):
Notification.objects.bulk_create([
Notification(user_id=self.user.id, type='xp_earned', title=f't{i}', is_read=(i % 2 == 0))
for i in range(150)
])
r = self.client.get('/api/notifications', HTTP_AUTHORIZATION=f'Bearer {self.token}')
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data['items']), 100)
self.assertEqual(r.data['unreadCount'], 75) # از کلِ ۱۵۰، نه از ۱۰۰ برگشتی
# جدیدترین اول (Meta.ordering = -created_at؛ اینجا هم‌زمان → -id غالب)
ids = [it['id'] if isinstance(it, dict) else it.id for it in r.data['items']]
self.assertEqual(ids, sorted(ids, reverse=True))
class NotificationPayloadShapeTests(TestCase):
"""
body و createdAt پیش‌تر روی مدل بودند ولی در پاسخ نبودند، یعنی فرانت نه
متنِ اعلان را می‌دید نه زمانش. این تست همان شکاف را قفل می‌کند تا دوباره
از قرارداد نیفتد.
"""
def setUp(self):
cache.clear()
self.client = APIClient()
self.user = User.objects.create(phone='09000000066', name='n')
self.token = issue_access_token(self.user.id)
def test_list_item_carries_body_and_created_at(self):
Notification.objects.create(
user_id=self.user.id, type='order_paid',
title='پرداخت موفق', body='اشتراک ماهانه فعال شد.', is_read=False,
)
r = self.client.get('/api/notifications', HTTP_AUTHORIZATION=f'Bearer {self.token}')
self.assertEqual(r.status_code, 200)
item = r.data['items'][0]
self.assertEqual(item['body'], 'اشتراک ماهانه فعال شد.')
self.assertIsNotNone(item['createdAt'])
# فیلدهای قبلی هنوز سرِ جایشان‌اند (تغییر فقط افزایشی بوده).
self.assertEqual(
set(item.keys()), {'id', 'type', 'title', 'body', 'isRead', 'createdAt'},
)
def test_empty_body_is_returned_as_empty_string_not_omitted(self):
# body روی مدل blank=True است؛ نبودِ متن نباید فیلد را از پاسخ حذف کند،
# وگرنه کلاینت باید برای دو شکلِ متفاوتِ پاسخ کد بنویسد.
Notification.objects.create(user_id=self.user.id, type='xp_earned', title='t')
r = self.client.get('/api/notifications', HTTP_AUTHORIZATION=f'Bearer {self.token}')
self.assertEqual(r.data['items'][0]['body'], '')
class MarkAllReadTests(TestCase):
"""دکمه‌ی «همه خوانده شد» با یک درخواست، نه یکی برای هر اعلان."""
def setUp(self):
from django.core.cache import cache
cache.clear()
from accounts.authentication import issue_access_token
from accounts.models import User
self.me = User.objects.create(phone='09120000021')
self.other = User.objects.create(phone='09120000022')
self.auth = {'HTTP_AUTHORIZATION': f'Bearer {issue_access_token(self.me.id)}'}
for user in (self.me, self.me, self.other):
Notification.objects.create(user_id=user.id, type='xp_earned', title='x')
def test_marks_only_my_unread_notifications(self):
response = self.client.patch('/api/notifications/read-all', **self.auth)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {'success': True, 'updated': 2})
self.assertFalse(Notification.objects.filter(user_id=self.me.id, is_read=False).exists())
self.assertTrue(Notification.objects.filter(user_id=self.other.id, is_read=False).exists())
def test_requires_sign_in(self):
self.assertEqual(self.client.patch('/api/notifications/read-all').status_code, 401)