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