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>
122 lines
5.3 KiB
Python
122 lines
5.3 KiB
Python
"""
|
||
حساب، ورود و چند موردِ دیگر از بازبینیِ ۲۰۲۶-۰۹-۲۵.
|
||
"""
|
||
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)
|