Files
MathApp/accounts/tests_audit_fixes.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

122 lines
5.3 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.
"""
حساب، ورود و چند موردِ دیگر از بازبینیِ ۲۰۲۶-۰۹-۲۵.
"""
import time
from unittest.mock import patch
from django.core.cache import cache
from django.test import TestCase, override_settings
from rest_framework.test import APIClient
from accounts.authentication import issue_access_token
from accounts.models import OTPCode, User
from content.models import Choice, Grade, Lesson, Node, Question, Unit
from progress.models import UserHearts, UserStats
def _client(user):
client = APIClient()
client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}')
return client
def _login(phone):
"""ورودِ واقعی با پیامکِ (ماک‌شده) و کد؛ پاسخِ login را برمی‌گرداند."""
with patch('accounts.views.secrets.randbelow', return_value=123456), \
patch('accounts.views.send_otp_sms'):
APIClient().post('/api/auth/otp/request', {'phone': phone}, format='json')
return APIClient().post('/api/auth/login', {'phone': phone, 'otp': '123456'}, format='json').json()
@override_settings(OTP_REQUEST_PHONE_COOLDOWN_SECONDS=0)
class LogoutTests(TestCase):
def setUp(self):
cache.clear()
def test_logout_kills_the_current_token_and_its_refresh(self):
session = _login('09121234901')
client = APIClient()
client.credentials(HTTP_AUTHORIZATION=f'Bearer {session["token"]}')
self.assertEqual(client.get('/api/user/profile').status_code, 200)
time.sleep(1.1) # زمانِ توکن ثانیه‌ای است؛ توکن باید پیش از خروج صادر شده باشد
response = client.post('/api/auth/logout', {'refreshToken': session['refreshToken']}, format='json')
self.assertEqual(response.status_code, 200)
self.assertEqual(client.get('/api/user/profile').status_code, 401)
refreshed = APIClient().post('/api/auth/refresh', {'refreshToken': session['refreshToken']}, format='json')
self.assertEqual(refreshed.status_code, 401)
# ورودِ دوباره کار می‌کند
again = _login('09121234901')
fresh = APIClient()
fresh.credentials(HTTP_AUTHORIZATION=f'Bearer {again["token"]}')
self.assertEqual(fresh.get('/api/user/profile').status_code, 200)
@override_settings(DEBUG=True, OTP_REQUEST_PHONE_COOLDOWN_SECONDS=0)
class OtpTests(TestCase):
def setUp(self):
cache.clear()
def _request(self, phone, code='123456'):
with patch('accounts.views.secrets.randbelow', return_value=int(code)), \
patch('accounts.views.send_otp_sms'):
APIClient().post('/api/auth/otp/request', {'phone': phone}, format='json')
def test_a_code_dies_after_five_wrong_guesses(self):
phone = '09121234001'
self._request(phone)
for guess in ('000001', '000002', '000003', '000004', '000005'):
APIClient().post('/api/auth/login', {'phone': phone, 'otp': guess}, format='json')
self.assertTrue(OTPCode.objects.get(phone=phone).is_used)
right = APIClient().post('/api/auth/login', {'phone': phone, 'otp': '123456'}, format='json')
self.assertEqual(right.status_code, 400)
class DisplayNameTests(TestCase):
def test_contact_details_are_stripped_from_the_name(self):
user = User.objects.create(phone='09121234004')
body = _client(user).patch('/api/user/profile', {'name': 'سارا 0912 345 6789 @sara'},
format='json').json()
self.assertEqual(body['name'], 'سارا')
class HeartsTests(TestCase):
def setUp(self):
cache.clear()
grade = Grade.objects.create(title='پایه نهم', icon='', color='')
node = Node.objects.create(unit=Unit.objects.create(grade=grade, title='ف', order=1), title='ن', order=1)
self.question = Question.objects.create(lesson=Lesson.objects.create(node=node, title='د'),
type='multiple_choice', prompt='؟')
self.right = Choice.objects.create(question=self.question, value='۱', is_correct=True)
self.user = User.objects.create(phone='09121234005')
UserHearts.objects.create(user_id=self.user.id, current_hearts=0)
def _answer(self):
return _client(self.user).post(f'/api/questions/{self.question.id}/answer',
{'selectedOptionId': self.right.id}, format='json')
# از ۲۰۲۶-۱۰-۰۲ پیش‌فرض روشن است (قلبِ هدیه‌ی بسته‌ها بدونِ آن اثری نداشت)؛ با .env خاموش می‌شود.
@override_settings(HEARTS_ENFORCED=False)
def test_can_be_switched_off(self):
self.assertEqual(self._answer().status_code, 200)
@override_settings(HEARTS_ENFORCED=True)
def test_when_enforced_no_hearts_means_no_answer(self):
response = self._answer()
self.assertEqual(response.status_code, 403)
self.assertEqual(response.json()['code'], 'no_hearts')
class AdminLoginThrottleTests(TestCase):
def setUp(self):
cache.clear()
@override_settings(ADMIN_LOGIN_MAX_ATTEMPTS=3)
def test_password_guessing_is_capped(self):
codes = [self.client.post('/admin/login/', {'username': 'a', 'password': 'b'}).status_code
for _ in range(4)]
self.assertEqual(codes[-1], 429)
self.assertNotEqual(codes[0], 429)