public user IDs, plans priced by tutor cost, age-based tutor tone, self-service account deletion
- profile publicId ("User482917", accounts/public_id.py); admin shows and searches it
- plans API reports aiDailyQuota per plan and freeAiDailyQuota; migration 0007 sets the
temporary prices (monthly 99,000 / yearly 990,000 toman, 5 tutor questions a day) unless
an admin already edited the yearly plan
- tutor tone follows the learner's age (18+ gets an adult tone at any grade); notation
still follows the grade's book; answer cache keyed by tone (prompt tutor-v12-age-tone)
- POST /api/user/delete: phone users confirm with a fresh SMS code (same checks as login,
now shared in verify_phone_otp), guests type "حذف"; payment records are kept, demo
accounts refused
- delete_users --skip-paid
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
66
accounts/tests_delete_account.py
Normal file
66
accounts/tests_delete_account.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
POST /api/user/delete — حذفِ حسابِ خود (۲۰۲۶-۱۰-۰۲). راههای شکستن: حذف با توکنِ تنها
|
||||
(بیکد)، حدسِ کد، حذفِ حسابِ دمو، و پاک شدنِ سندِ مالی.
|
||||
"""
|
||||
from django.core.cache import cache
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils import timezone
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from accounts.authentication import issue_access_token
|
||||
from accounts.models import OTPCode, User
|
||||
from payment.models import Order
|
||||
from progress.models import UserStats
|
||||
|
||||
|
||||
def _client(user):
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}')
|
||||
return client
|
||||
|
||||
|
||||
class DeleteAccountTests(TestCase):
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.user = User.objects.create(phone='09121115001', is_guest=False)
|
||||
UserStats.objects.create(user_id=self.user.id, xp=50)
|
||||
self.client = _client(self.user)
|
||||
|
||||
def code(self, value='482913'):
|
||||
OTPCode.objects.create(phone=self.user.phone, code=value,
|
||||
expires_at=timezone.now() + timezone.timedelta(minutes=5))
|
||||
return value
|
||||
|
||||
def delete(self, **body):
|
||||
return self.client.post('/api/user/delete', body, format='json')
|
||||
|
||||
def test_a_token_alone_is_not_enough(self):
|
||||
self.assertEqual(self.delete().status_code, 400)
|
||||
self.assertTrue(User.objects.filter(pk=self.user.pk).exists())
|
||||
|
||||
def test_a_wrong_code_deletes_nothing(self):
|
||||
self.code()
|
||||
self.assertEqual(self.delete(otp='111111').status_code, 400)
|
||||
self.assertTrue(User.objects.filter(pk=self.user.pk).exists())
|
||||
|
||||
def test_the_right_code_deletes_everything_but_payment_records(self):
|
||||
Order.objects.create(user_id=self.user.id, amount=99000, status='paid', idempotency_key='k')
|
||||
response = self.delete(otp=self.code())
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
self.assertFalse(User.objects.filter(pk=self.user.pk).exists())
|
||||
self.assertFalse(UserStats.objects.filter(user_id=self.user.pk).exists())
|
||||
self.assertTrue(Order.objects.filter(user_id=self.user.pk).exists())
|
||||
# توکنِ قبلی دیگر به هیچ کاربری نمیرسد.
|
||||
self.assertIn(self.client.get('/api/user/profile').status_code, (401, 403))
|
||||
|
||||
def test_guest_confirms_by_typing_the_word(self):
|
||||
guest = User.objects.create(device_id='guest-del', is_guest=True)
|
||||
client = _client(guest)
|
||||
self.assertEqual(client.post('/api/user/delete', {'confirm': 'باشه'}, format='json').status_code, 400)
|
||||
self.assertEqual(client.post('/api/user/delete', {'confirm': 'حذف'}, format='json').status_code, 200)
|
||||
self.assertFalse(User.objects.filter(pk=guest.pk).exists())
|
||||
|
||||
@override_settings(TEST_LOGIN_PHONES={'09121115001': '4321'})
|
||||
def test_demo_accounts_cannot_be_deleted(self):
|
||||
self.assertEqual(self.delete(otp='4321').status_code, 403)
|
||||
self.assertTrue(User.objects.filter(pk=self.user.pk).exists())
|
||||
Reference in New Issue
Block a user