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>
This commit is contained in:
2026-10-05 18:20:10 +03:30
parent 8835e01595
commit 663252a260
61 changed files with 357 additions and 1518 deletions

View File

@@ -14,7 +14,6 @@ from rest_framework.throttling import ScopedRateThrottle
from .authentication import issue_access_token
from .models import OTPCode, Profile, Session, User, hash_refresh_token
from .testing import guest_session
from .services import OTPDeliveryError, SMSDeliveryError, send_otp_sms
@@ -162,7 +161,6 @@ class SendOtpSmsTests(TestCase):
self.assertIn('provider_status=401', message)
class RequestOTPViewTests(TestCase):
"""
تست‌های سطح API برای RequestOTPView: مطمئن می‌شیم رفتارِ dev قبلی حفظ
@@ -286,12 +284,11 @@ class RefreshTokenReplayTests(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create(is_guest=True, device_id='device-refresh-test')
self.user = User.objects.create(phone='09121230201')
self.raw = 'test-refresh-token-abc123'
self.session = Session.objects.create(
user=self.user,
refresh_token=hash_refresh_token(self.raw),
device_id='device-refresh-test',
expires_at=timezone.now() + timezone.timedelta(days=30),
revoked=False,
)
@@ -760,14 +757,13 @@ class RefreshTokenThrottleTests(TestCase):
def setUp(self):
cache.clear()
self.client = APIClient()
self.user = User.objects.create(is_guest=True, device_id='device-refresh-throttle-test')
self.user = User.objects.create(phone='09121230202')
def _create_session(self, token_suffix):
raw = f'test-refresh-token-{token_suffix}'
session = Session.objects.create(
user=self.user,
refresh_token=hash_refresh_token(raw),
device_id='device-refresh-throttle-test',
expires_at=timezone.now() + timezone.timedelta(days=30),
revoked=False,
)
@@ -824,28 +820,6 @@ class RefreshTokenThrottleTests(TestCase):
self.assertFalse(blocked_session.revoked)
class GuestSessionThrottleTests(TestCase):
"""
🔒 اثبات محدودیتِ جدیدِ ScopedRateThrottle (scope='guest_session') روی
GuestSessionView: درخواست‌های تکراری از همون کلاینت (IP) باید بعد از
حدِ مجاز با ۴۲۹ رد بشن.
"""
def setUp(self):
self.client = APIClient()
cache.clear()
def test_guest_login_endpoint_is_gone(self):
# ورودِ مهمان حذف شده: هر کاربر باید با شماره ثبت‌نام کند. قدیمی‌ها باید ۴۰۴
# بگیرند، نه حسابِ مهمانِ ساخته‌شده.
before = User.objects.count()
response = self.client.post(
'/api/auth/session', {'type': 'guest', 'deviceId': 'device-x'}, format='json',
)
self.assertEqual(response.status_code, 404)
self.assertEqual(User.objects.count(), before)
class UserProfileViewTests(TestCase):
"""تست‌های GET /api/user/profile — پروفایل کاربر جاری."""
@@ -862,8 +836,8 @@ class UserProfileViewTests(TestCase):
response = self._get_profile()
self.assertEqual(response.status_code, 401)
def test_returns_profile_for_guest_without_existing_profile_row(self):
user = User.objects.create(device_id='device-profile-1', is_guest=True, name='')
def test_returns_profile_for_user_without_existing_profile_row(self):
user = User.objects.create(phone='09000000055', name='')
token = issue_access_token(user.id)
response = self._get_profile(token)
@@ -871,8 +845,7 @@ class UserProfileViewTests(TestCase):
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data['userId'], str(user.id))
self.assertEqual(data['isGuest'], True)
self.assertIsNone(data['phone'])
self.assertEqual(data['phone'], '09000000055')
self.assertIsNone(data['gradeId'])
self.assertIsNone(data['birthYear'])
self.assertIsNone(data['birthDate'])
@@ -882,7 +855,7 @@ class UserProfileViewTests(TestCase):
def test_returns_existing_profile_fields_for_registered_user(self):
user = User.objects.create(
phone='09120000001', is_guest=False, name='علی', avatar='avatar.png', grade_id=2,
phone='09120000001', name='علی', avatar='avatar.png', grade_id=2,
)
Profile.objects.create(user=user, birth_year=1384, bio='سلام')
token = issue_access_token(user.id)
@@ -894,15 +867,14 @@ class UserProfileViewTests(TestCase):
self.assertEqual(data['phone'], '09120000001')
self.assertEqual(data['name'], 'علی')
self.assertEqual(data['avatar'], 'avatar.png')
self.assertEqual(data['isGuest'], False)
self.assertEqual(data['gradeId'], 2)
self.assertEqual(data['birthYear'], 1384)
self.assertEqual(data['bio'], 'سلام')
def test_does_not_leak_other_users_profile(self):
other = User.objects.create(device_id='device-other', is_guest=True, name='')
other = User.objects.create(phone='09000000056', name='')
Profile.objects.create(user=other, bio='محرمانه')
user = User.objects.create(device_id='device-profile-2', is_guest=True, name='')
user = User.objects.create(phone='09000000057', name='')
token = issue_access_token(user.id)
response = self._get_profile(token)
@@ -932,13 +904,13 @@ class SendSmsAdminActionTests(TestCase):
# inactive/never-active موردنظرِ خودشون رو صریحاً می‌سازن.
now = timezone.now()
self.user1 = User.objects.create(
name='u1', is_guest=False, phone='0910000001', last_activity_at=now,
name='u1', phone='0910000001', last_activity_at=now,
)
self.user2 = User.objects.create(
name='u2', is_guest=False, phone='0910000002', last_activity_at=now,
name='u2', phone='0910000002', last_activity_at=now,
)
self.guest_no_phone = User.objects.create(
name='guest', is_guest=True, phone=None, last_activity_at=now,
self.no_phone_user = User.objects.create(
name='no-phone', phone=None, last_activity_at=now,
)
self.changelist_url = reverse('admin:accounts_user_changelist')
@@ -963,10 +935,10 @@ class SendSmsAdminActionTests(TestCase):
mock_send_sms.assert_not_called()
@patch('accounts.admin.send_sms')
def test_apply_sends_sms_to_users_with_phone_and_skips_guest(self, mock_send_sms):
def test_apply_sends_sms_to_users_with_phone_and_skips_users_without(self, mock_send_sms):
response = self.client.post(self.changelist_url, {
'action': 'send_sms_to_selected',
'_selected_action': [self.user1.pk, self.user2.pk, self.guest_no_phone.pk],
'_selected_action': [self.user1.pk, self.user2.pk, self.no_phone_user.pk],
'apply': 'ارسال پیامک',
'message': 'سلام از طرف ادمین',
})
@@ -1030,14 +1002,14 @@ class SendSmsAdminActionTests(TestCase):
"""
now = timezone.now()
active_user = User.objects.create(
name='active', is_guest=False, phone='0910000009', last_activity_at=now,
name='active', phone='0910000009', last_activity_at=now,
)
inactive_user = User.objects.create(
name='inactive', is_guest=False, phone='0910000010',
name='inactive', phone='0910000010',
last_activity_at=now - timezone.timedelta(days=90),
)
never_active_user = User.objects.create(
name='never-active', is_guest=False, phone='0910000011', last_activity_at=None,
name='never-active', phone='0910000011', last_activity_at=None,
)
cutoff_date = (now - timezone.timedelta(days=30)).date()
@@ -1085,7 +1057,7 @@ class SendSmsAdminActionTests(TestCase):
'inactive' هم مثلِ 'selected' حفظ بشه.
"""
User.objects.create(
name='inactive-no-phone', is_guest=True, phone=None,
name='inactive-no-phone', phone=None,
last_activity_at=timezone.now() - timezone.timedelta(days=90),
)
cutoff_date = (timezone.now() - timezone.timedelta(days=30)).date()
@@ -1115,7 +1087,7 @@ class SendSmsAdminActionTests(TestCase):
"""
response = self.client.post(self.changelist_url, {
'action': 'send_sms_to_selected',
'_selected_action': [self.user1.pk, self.user2.pk, self.guest_no_phone.pk],
'_selected_action': [self.user1.pk, self.user2.pk, self.no_phone_user.pk],
'apply': 'ارسال پیامک',
'message': 'متن',
'target': 'selected',
@@ -1137,11 +1109,11 @@ class SendSmsAdminActionTests(TestCase):
"""
now = timezone.now()
User.objects.create(
name='inactive-1', is_guest=False, phone='0910000021',
name='inactive-1', phone='0910000021',
last_activity_at=now - timezone.timedelta(days=90),
)
User.objects.create(
name='inactive-2', is_guest=False, phone='0910000022',
name='inactive-2', phone='0910000022',
last_activity_at=now - timezone.timedelta(days=90),
)
cutoff_date = (now - timezone.timedelta(days=30)).date()
@@ -1185,7 +1157,7 @@ class SendSmsAdminActionTests(TestCase):
"""
response = self.client.post(self.changelist_url, {
'action': 'send_sms_to_selected',
'_selected_action': [self.user1.pk, self.guest_no_phone.pk],
'_selected_action': [self.user1.pk, self.no_phone_user.pk],
'apply': 'ارسال پیامک',
'message': 'متن',
'target': 'selected',
@@ -1226,7 +1198,7 @@ class SupervisorGroupAccessTests(TestCase):
is_staff=True,
)
self.target_user = User.objects.create(name='target', is_guest=False, phone='0910000099')
self.target_user = User.objects.create(name='target', phone='0910000099')
self.plan = Plan.objects.create(title='یک‌ماهه', price=10000)
self.order = Order.objects.create(
user_id=self.target_user.id, amount=10000, idempotency_key='sup-order-1',
@@ -1333,7 +1305,7 @@ class SupervisorGroupAccessTests(TestCase):
original_name = self.target_user.name
response = self.client.post(
reverse('admin:accounts_user_change', args=[self.target_user.pk]),
{'name': 'دستکاری‌شده', 'is_guest': 'on'},
{'name': 'دستکاری‌شده'},
)
self.assertEqual(response.status_code, 403)
self.target_user.refresh_from_db()
@@ -1422,7 +1394,7 @@ class AuthUserRowCacheTests(TestCase):
def setUp(self):
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='dev-authcache', is_guest=True, name='n')
self.user = User.objects.create(phone='09000000058', name='n')
self.token = issue_access_token(self.user.id)
# هر endpointِ authenticated؛ /api/user/hearts سبک است.
self.url = '/api/user/hearts'
@@ -1492,7 +1464,7 @@ class UpdateUserProfileTests(TestCase):
def setUp(self):
self.client = APIClient()
cache.clear()
self.user = User.objects.create(device_id='device-patch-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000059', name='')
self.token = issue_access_token(self.user.id)
def tearDown(self):
@@ -1536,13 +1508,12 @@ class UpdateUserProfileTests(TestCase):
def test_empty_body_is_rejected(self):
self.assertEqual(self._patch({}).status_code, 400)
def test_phone_and_guest_flag_cannot_be_changed(self):
response = self._patch({'name': 'رضا', 'phone': '09120000000', 'isGuest': False})
def test_phone_cannot_be_changed(self):
response = self._patch({'name': 'رضا', 'phone': '09120000000'})
self.assertEqual(response.status_code, 200)
self.user.refresh_from_db()
self.assertIsNone(self.user.phone)
self.assertTrue(self.user.is_guest)
self.assertNotEqual(self.user.phone, '09120000000')
def test_out_of_range_birth_year_is_rejected(self):
self.assertEqual(self._patch({'birthYear': 2026}).status_code, 400)
@@ -1660,8 +1631,11 @@ class RefreshTokenStoredHashedTests(TestCase):
def test_issued_tokens_are_stored_only_as_hash(self):
client = APIClient()
body = guest_session('hash-dev')
raw = body['refreshToken']
user = User.objects.create(phone='09121230203')
raw = 'raw-token-for-hash-test'
Session.objects.create(user=user, refresh_token=hash_refresh_token(raw),
expires_at=timezone.now() + timezone.timedelta(days=1))
self.assertFalse(Session.objects.filter(refresh_token=raw).exists())
self.assertFalse(Session.objects.filter(refresh_token=raw).exists())
self.assertTrue(Session.objects.filter(refresh_token=hash_refresh_token(raw)).exists())
@@ -1674,7 +1648,7 @@ class RefreshTokenStoredHashedTests(TestCase):
from django.apps import apps as global_apps
user = User.objects.create(is_guest=True, device_id='hash-mig')
user = User.objects.create(phone='09121230204')
Session.objects.create(user=user, refresh_token='old-raw-token',
expires_at=timezone.now() + timezone.timedelta(days=1))
import_module('accounts.migrations.0007_hash_refresh_tokens').hash_existing(global_apps, None)