""" حساب، ورود و چند موردِ دیگر از بازبینیِ ۲۰۲۶-۰۹-۲۵. """ 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.testing import guest_session 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 class LogoutTests(TestCase): def setUp(self): cache.clear() def test_logout_kills_the_current_token_and_its_refresh(self): guest = guest_session('dev-out') client = APIClient() client.credentials(HTTP_AUTHORIZATION=f'Bearer {guest["token"]}') self.assertEqual(client.get('/api/user/profile').status_code, 200) time.sleep(1.1) # زمانِ توکن ثانیه‌ای است؛ توکن باید پیش از خروج صادر شده باشد response = client.post('/api/auth/logout', {'refreshToken': guest['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': guest['refreshToken']}, format='json') self.assertEqual(refreshed.status_code, 401) # ورودِ دوباره کار می‌کند again = guest_session('dev-out') 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) @override_settings(DEBUG=True, OTP_REQUEST_PHONE_COOLDOWN_SECONDS=0, TEST_LOGIN_PHONES={'09120000101': '4321'}) class GuestSignupKeepsProgressTests(TestCase): def setUp(self): cache.clear() guest = guest_session('dev-up') self.guest_id = int(guest['userId']) UserStats.objects.create(user_id=self.guest_id, xp=120) self.client = APIClient() self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {guest["token"]}') def _login(self, phone, otp): return self.client.post('/api/auth/login', {'phone': phone, 'otp': otp}, format='json') def test_the_guest_becomes_the_registered_user(self): with patch('accounts.views.secrets.randbelow', return_value=654321), \ patch('accounts.views.send_otp_sms'): self.client.post('/api/auth/otp/request', {'phone': '09121234002'}, format='json') body = self._login('09121234002', '654321').json() self.assertEqual(int(body['userId']), self.guest_id) user = User.objects.get(pk=self.guest_id) self.assertEqual((user.phone, user.is_guest), ('09121234002', False)) self.assertEqual(UserStats.objects.get(user_id=self.guest_id).xp, 120) def test_an_existing_account_is_not_merged(self): existing = User.objects.create(phone='09121234003', is_guest=False) with patch('accounts.views.secrets.randbelow', return_value=111222), \ patch('accounts.views.send_otp_sms'): self.client.post('/api/auth/otp/request', {'phone': '09121234003'}, format='json') body = self._login('09121234003', '111222').json() self.assertEqual(int(body['userId']), existing.id) self.assertTrue(User.objects.get(pk=self.guest_id).is_guest) def test_demo_test_phones_never_absorb_a_guest(self): body = self._login('09120000101', '4321').json() self.assertNotEqual(int(body['userId']), self.guest_id) self.assertTrue(User.objects.get(pk=self.guest_id).is_guest) class DisplayNameTests(TestCase): def test_contact_details_are_stripped_from_the_name(self): user = User.objects.create(phone='09121234004', is_guest=False) 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', is_guest=False) 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)