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:
@@ -14,7 +14,6 @@ from django.utils import timezone
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from accounts.authentication import issue_access_token
|
||||
from accounts.testing import guest_session
|
||||
from accounts.models import OTPCode, User
|
||||
from config.log_formatters import JSONLogFormatter
|
||||
|
||||
@@ -30,33 +29,6 @@ def _otp_row(phone, code='482913'):
|
||||
return code
|
||||
|
||||
|
||||
def _guest(device):
|
||||
return guest_session(device)
|
||||
|
||||
|
||||
class GuestUpgradeRevokesEarlierSessionsTests(TestCase):
|
||||
"""کسی که پیش از ثبتِ شماره با شناسهی دستگاه نشست گرفته بود، بعد از آن به حسابِ شمارهدار نمیرسید."""
|
||||
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
|
||||
def test_pre_upgrade_tokens_stop_working(self):
|
||||
attacker = _guest('shared-device')
|
||||
victim = _guest('shared-device')
|
||||
login = _bearer(victim['token']).post('/api/auth/login',
|
||||
{'phone': '09121230001', 'otp': _otp_row('09121230001')},
|
||||
format='json')
|
||||
self.assertEqual(login.status_code, 200)
|
||||
self.assertEqual(login.json()['userId'], victim['userId'])
|
||||
|
||||
refresh = APIClient().post('/api/auth/refresh', {'refreshToken': attacker['refreshToken']}, format='json')
|
||||
self.assertEqual(refresh.status_code, 401)
|
||||
# توکنِ دسترسیِ پیش از ثبت هم (اگر همان ثانیه صادر نشده باشد) رد میشود؛ نشستِ تازه کار میکند.
|
||||
self.assertEqual(_bearer(login.json()['token']).get('/api/user/profile').status_code, 200)
|
||||
fresh = APIClient().post('/api/auth/refresh', {'refreshToken': login.json()['refreshToken']}, format='json')
|
||||
self.assertEqual(fresh.status_code, 200)
|
||||
|
||||
|
||||
class AccountDeletionKeepsLimitsTests(TestCase):
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
@@ -66,23 +38,25 @@ class AccountDeletionKeepsLimitsTests(TestCase):
|
||||
question_credits=20, hearts_days=7, duration_days=0)
|
||||
DiscountCode.objects.create(code='FREE1', type='percent', value=100, max_uses=1)
|
||||
|
||||
def _guest_redeems(self, device):
|
||||
s = _guest(device)
|
||||
c = _bearer(s['token'])
|
||||
def _user_redeems(self, phone):
|
||||
user = User.objects.create(phone=phone)
|
||||
c = _bearer(issue_access_token(user.id))
|
||||
p = c.post('/api/subscription/purchase', {'planId': str(self.plan.id)}, format='json')
|
||||
return c, c.post('/api/discount/redeem', {'code': 'FREE1', 'orderId': p.json()['orderId']}, format='json')
|
||||
|
||||
def test_single_use_code_stays_used_after_delete(self):
|
||||
c1, first = self._guest_redeems('g-1')
|
||||
c1, first = self._user_redeems('09121230101')
|
||||
self.assertEqual(first.status_code, 200)
|
||||
self.assertEqual(c1.post('/api/user/delete', {'confirm': 'حذف'}, format='json').status_code, 200)
|
||||
_, again = self._guest_redeems('g-2')
|
||||
OTPCode.objects.create(phone='09121230101', code='777777',
|
||||
expires_at=timezone.now() + timezone.timedelta(minutes=5))
|
||||
self.assertEqual(c1.post('/api/user/delete', {'otp': '777777'}, format='json').status_code, 200)
|
||||
_, again = self._user_redeems('09121230102')
|
||||
self.assertEqual(again.status_code, 400)
|
||||
|
||||
@override_settings(DEBUG=False, OTP_REQUEST_PHONE_COOLDOWN_SECONDS=0)
|
||||
def test_per_phone_sms_cap_survives_delete(self):
|
||||
phone = '09121230009'
|
||||
user = User.objects.create(phone=phone, is_guest=False)
|
||||
user = User.objects.create(phone=phone)
|
||||
for i in range(10):
|
||||
OTPCode.objects.create(phone=phone, code=f'{i:06d}', is_used=True,
|
||||
expires_at=timezone.now() - timezone.timedelta(minutes=i + 1))
|
||||
@@ -95,14 +69,6 @@ class AccountDeletionKeepsLimitsTests(TestCase):
|
||||
r = APIClient().post('/api/auth/otp/request', {'phone': phone}, format='json')
|
||||
self.assertEqual(r.status_code, 429)
|
||||
|
||||
def test_public_demo_guest_cannot_be_deleted(self):
|
||||
demo = User.objects.create(device_id='frontend-test-device', is_guest=True, name='demo')
|
||||
s = _guest('frontend-test-device')
|
||||
self.assertEqual(int(s['userId']), demo.id)
|
||||
r = _bearer(s['token']).post('/api/user/delete', {'confirm': 'حذف'}, format='json')
|
||||
self.assertEqual(r.status_code, 403)
|
||||
self.assertTrue(User.objects.filter(pk=demo.id).exists())
|
||||
|
||||
|
||||
class DisplayNameContactFilterTests(TestCase):
|
||||
def test_contacts_are_removed_and_names_kept(self):
|
||||
@@ -151,35 +117,6 @@ class KavenegarKeyNotLoggedTests(TestCase):
|
||||
self.assertNotIn(FAKE_KEY, out)
|
||||
|
||||
|
||||
class SupervisorDoesNotSeeDeviceIdsTests(TestCase):
|
||||
"""شناسهی دستگاه برای مهمان رمزِ ورود است؛ کارمندِ فقطخواندنی آن را نمیبیند."""
|
||||
|
||||
def setUp(self):
|
||||
call_command('setup_supervisor_group', verbosity=0)
|
||||
self.guest = User.objects.create(device_id='secret-device-123', is_guest=True)
|
||||
from accounts.models import Session
|
||||
Session.objects.create(user=self.guest, refresh_token='h' * 64, device_id='secret-device-123',
|
||||
expires_at=timezone.now() + timezone.timedelta(days=1))
|
||||
staff = get_user_model().objects.create_user('sup', 'sup@example.com', 'pw-for-tests-only', is_staff=True)
|
||||
staff.groups.add(Group.objects.get(name='Supervisor'))
|
||||
# Supervisor نشستها را نمیبیند؛ اینجا اجازهی دیدن داده میشود تا ستونِ آن صفحه هم سنجیده شود.
|
||||
from django.contrib.auth.models import Permission
|
||||
staff.user_permissions.add(Permission.objects.get(codename='view_session', content_type__app_label='accounts'))
|
||||
self.client.force_login(staff)
|
||||
|
||||
def test_hidden_in_list_search_and_form(self):
|
||||
listing = self.client.get('/admin/accounts/user/')
|
||||
self.assertEqual(listing.status_code, 200)
|
||||
self.assertNotContains(listing, 'secret-device-123')
|
||||
# متنِ جستجو در خودِ صفحه تکرار میشود؛ نتیجه نباید همان مهمان باشد.
|
||||
search = self.client.get('/admin/accounts/user/', {'q': 'secret-device-123'})
|
||||
self.assertNotContains(search, f'/admin/accounts/user/{self.guest.pk}/change/')
|
||||
form = self.client.get(f'/admin/accounts/user/{self.guest.pk}/change/')
|
||||
self.assertNotContains(form, 'secret-device-123')
|
||||
sessions = self.client.get('/admin/accounts/session/')
|
||||
self.assertNotContains(sessions, 'secret-device-123')
|
||||
|
||||
|
||||
def _staff_with(*perms, username='staff1'):
|
||||
from django.contrib.auth.models import Permission
|
||||
staff = get_user_model().objects.create_user(username, f'{username}@example.com', 'pw-for-tests-only', is_staff=True)
|
||||
@@ -188,53 +125,24 @@ def _staff_with(*perms, username='staff1'):
|
||||
return staff
|
||||
|
||||
|
||||
class DeviceIdNotGuessableByAdminFilterTests(TestCase):
|
||||
"""فیلترِ querystringِ changelist (?device_id__startswith=…) شناسهی دستگاه را برای غیرِ ابرکاربر لو نمیدهد."""
|
||||
|
||||
def setUp(self):
|
||||
call_command('setup_supervisor_group', verbosity=0)
|
||||
self.guest = User.objects.create(device_id='abc-secret-999', is_guest=True)
|
||||
from accounts.models import Session
|
||||
Session.objects.create(user=self.guest, refresh_token='h' * 64, device_id='abc-secret-999',
|
||||
expires_at=timezone.now() + timezone.timedelta(days=1))
|
||||
staff = _staff_with(('accounts', 'view_session'))
|
||||
staff.groups.add(Group.objects.get(name='Supervisor'))
|
||||
self.client.force_login(staff)
|
||||
|
||||
def test_filter_on_device_id_is_refused(self):
|
||||
for lookup in ('device_id__startswith', 'device_id__regex', 'device_id'):
|
||||
r = self.client.get('/admin/accounts/user/', {lookup: 'abc'})
|
||||
self.assertEqual(r.status_code, 400, lookup)
|
||||
self.assertNotContains(r, f'/admin/accounts/user/{self.guest.pk}/change/', status_code=400)
|
||||
r = self.client.get('/admin/accounts/session/', {'device_id__startswith': 'abc'})
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
def test_superuser_still_filters(self):
|
||||
root = get_user_model().objects.create_superuser('root', 'root@example.com', 'pw-for-tests-only')
|
||||
self.client.force_login(root)
|
||||
r = self.client.get('/admin/accounts/user/', {'device_id__startswith': 'abc'})
|
||||
self.assertContains(r, f'/admin/accounts/user/{self.guest.pk}/change/')
|
||||
|
||||
|
||||
class StaffCannotSwapLoginPhoneTests(TestCase):
|
||||
"""کارمندی با change_user شمارهی کاربر را عوض نمیکند؛ وگرنه با شمارهی خودش کد میگیرد و واردِ آن حساب میشود."""
|
||||
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.victim = User.objects.create(phone='09120000077', is_guest=False, name='victim')
|
||||
self.victim = User.objects.create(phone='09120000077', name='victim')
|
||||
self.client.force_login(_staff_with(('accounts', 'change_user'), ('accounts', 'view_user')))
|
||||
|
||||
def tearDown(self):
|
||||
cache.clear()
|
||||
|
||||
def test_phone_and_is_guest_are_read_only(self):
|
||||
def test_phone_is_read_only(self):
|
||||
r = self.client.post(f'/admin/accounts/user/{self.victim.pk}/change/', {
|
||||
'phone': '09129999999', 'name': 'renamed', 'avatar': '', 'grade_id': '', 'is_guest': 'on'})
|
||||
'phone': '09129999999', 'name': 'renamed', 'avatar': '', 'grade_id': ''})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
self.victim.refresh_from_db()
|
||||
self.assertEqual(self.victim.name, 'renamed')
|
||||
self.assertEqual(self.victim.phone, '09120000077')
|
||||
self.assertFalse(self.victim.is_guest)
|
||||
login = APIClient().post('/api/auth/login', {'phone': '09129999999', 'otp': _otp_row('09129999999')},
|
||||
format='json')
|
||||
self.assertNotEqual(login.json().get('userId'), str(self.victim.pk))
|
||||
@@ -270,7 +178,7 @@ class StaffAccountsAdminSuperuserOnlyTests(TestCase):
|
||||
|
||||
|
||||
class UnicodeInputsAreNormalisedOrRejectedTests(TestCase):
|
||||
"""رقمهای تمامعرض/ریاضی، کدِ غیرِ ASCII، پایهی ناموجود و deviceIdِ null: ۴۰۰ یا نرمالشده، نه ۵۰۰ و نه حسابِ جدا."""
|
||||
"""رقمهای تمامعرض/ریاضی، کدِ غیرِ ASCIIو پایهی ناموجود: ۴۰۰ یا نرمالشده، نه ۵۰۰ و نه حسابِ جدا."""
|
||||
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
@@ -290,20 +198,15 @@ class UnicodeInputsAreNormalisedOrRejectedTests(TestCase):
|
||||
r = APIClient().post('/api/auth/login', {'phone': phone, 'otp': code}, format='json')
|
||||
self.assertEqual(r.status_code, 400, code)
|
||||
|
||||
def test_login_device_id_null_is_400(self):
|
||||
phone = '09120000098'
|
||||
code = _otp_row(phone)
|
||||
r = APIClient().post('/api/auth/login', {'phone': phone, 'otp': code, 'deviceId': None}, format='json')
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
def test_unknown_or_huge_grade_is_400(self):
|
||||
user = User.objects.create(phone='09120000097', is_guest=False)
|
||||
user = User.objects.create(phone='09120000097')
|
||||
app = _bearer(issue_access_token(user.id))
|
||||
for gid in (99999, 10 ** 30):
|
||||
self.assertEqual(app.post('/api/user/grade', {'gradeId': gid}, format='json').status_code, 400, gid)
|
||||
|
||||
def test_birth_date_with_superscript_digit_is_400(self):
|
||||
user = User.objects.create(phone='09120000096', is_guest=False)
|
||||
user = User.objects.create(phone='09120000096')
|
||||
r = _bearer(issue_access_token(user.id)).patch('/api/user/profile', {'birthDate': '13²5-04-12'}, format='json')
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
@@ -315,19 +218,3 @@ class UnicodeInputsAreNormalisedOrRejectedTests(TestCase):
|
||||
self.assertEqual(clean('علی رضایی'), 'علی رضایی')
|
||||
|
||||
|
||||
class DemoGuestNotUpgradableTests(TestCase):
|
||||
"""مهمانِ نمایشیِ مشترک با ورودِ یک شماره به آن شماره تبدیل نمیشود؛ شمارهی تازه حسابِ تازه میگیرد."""
|
||||
|
||||
def test_demo_guest_keeps_its_identity(self):
|
||||
cache.clear()
|
||||
self.addCleanup(cache.clear)
|
||||
s = _guest('frontend-test-device')
|
||||
demo_id = int(s['userId'])
|
||||
phone = '09120000095'
|
||||
code = _otp_row(phone)
|
||||
r = _bearer(s['token']).post('/api/auth/login', {'phone': phone, 'otp': code}, format='json')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertNotEqual(int(r.json()['userId']), demo_id)
|
||||
demo = User.objects.get(pk=demo_id)
|
||||
self.assertIsNone(demo.phone)
|
||||
self.assertTrue(demo.is_guest)
|
||||
|
||||
Reference in New Issue
Block a user