from datetime import timedelta from django.test import TestCase from django.utils import timezone from rest_framework.test import APIClient from accounts.authentication import issue_access_token from accounts.models import User from .models import HEART_REFILL_INTERVAL, Character, UserHearts, UserStats class UserHeartsRegenTests(TestCase): """رگرسیون: قبلاً next_refill_at هیچ‌وقت واقعاً چک نمی‌شد و هارتِ ازدست‌رفته برای همیشه صفر می‌موند.""" def setUp(self): self.client = APIClient() self.user = User.objects.create(device_id='dev-hearts-regen', is_guest=True, name='') self.token = issue_access_token(self.user.id) def _get_hearts(self): return self.client.get('/api/user/hearts', HTTP_AUTHORIZATION=f'Bearer {self.token}') def test_no_refill_before_interval_elapses(self): hearts = UserHearts.objects.create(user_id=self.user.id, current_hearts=3, max_hearts=5) hearts.lose_heart() # شبیه‌سازیِ از دست دادنِ یک هارتِ دیگه؛ تایمر رو ست می‌کنه hearts.save() response = self._get_hearts() self.assertEqual(response.data['currentHearts'], 2) self.assertIsNotNone(response.data['nextRefillAt']) def test_refill_after_interval_elapses(self): hearts = UserHearts.objects.create( user_id=self.user.id, current_hearts=3, max_hearts=5, next_refill_at=timezone.now() - timedelta(seconds=1), ) response = self._get_hearts() self.assertEqual(response.data['currentHearts'], 4) hearts.refresh_from_db() self.assertEqual(hearts.current_hearts, 4) self.assertIsNotNone(hearts.next_refill_at) # هنوز به max نرسیده، تایمرِ بعدی ست شده def test_refill_catches_up_multiple_missed_intervals(self): UserHearts.objects.create( user_id=self.user.id, current_hearts=1, max_hearts=5, next_refill_at=timezone.now() - 3 * HEART_REFILL_INTERVAL, ) response = self._get_hearts() self.assertEqual(response.data['currentHearts'], 5) self.assertIsNone(response.data['nextRefillAt']) # به max رسیده، تایمر خاموش می‌شه def test_full_hearts_have_no_refill_timer(self): UserHearts.objects.create(user_id=self.user.id, current_hearts=5, max_hearts=5) response = self._get_hearts() self.assertEqual(response.data['currentHearts'], 5) self.assertIsNone(response.data['nextRefillAt']) class CharacterGamificationTests(TestCase): """کاتالوگِ کاراکترها (مثلِ ماسکات‌های Duolingo): باز/بسته‌بودن تابعی از XP کاربره، و فقط کاراکترِ باز‌شده قابلِ انتخاب‌شدنه.""" def setUp(self): self.client = APIClient() self.user = User.objects.create(device_id='device-characters-1', is_guest=True, name='') self.token = issue_access_token(self.user.id) self.owl = Character.objects.create(slug='owl', name='جغد', icon='owl', unlock_xp=0, order=1) self.fox = Character.objects.create(slug='fox', name='روباه', icon='fox', unlock_xp=100, order=2) def _list(self): return self.client.get('/api/characters', HTTP_AUTHORIZATION=f'Bearer {self.token}') def _select(self, character_id): return self.client.post( '/api/characters/select', {'characterId': str(character_id)}, format='json', HTTP_AUTHORIZATION=f'Bearer {self.token}', ) def test_list_reports_locked_and_unlocked_by_xp(self): UserStats.objects.create(user_id=self.user.id, xp=50) response = self._list() self.assertEqual(response.status_code, 200) by_slug = {c['slug']: c for c in response.data['characters']} self.assertTrue(by_slug['owl']['unlocked']) self.assertFalse(by_slug['fox']['unlocked']) def test_list_with_no_stats_yet_only_zero_xp_characters_unlocked(self): response = self._list() self.assertEqual(response.status_code, 200) by_slug = {c['slug']: c for c in response.data['characters']} self.assertTrue(by_slug['owl']['unlocked']) self.assertFalse(by_slug['fox']['unlocked']) def test_can_select_unlocked_character(self): UserStats.objects.create(user_id=self.user.id, xp=10) response = self._select(self.owl.id) self.assertEqual(response.status_code, 200) self.assertTrue(response.data['selected']) stats = UserStats.objects.get(user_id=self.user.id) self.assertEqual(stats.selected_character_id, self.owl.id) def test_cannot_select_locked_character(self): UserStats.objects.create(user_id=self.user.id, xp=10) response = self._select(self.fox.id) self.assertEqual(response.status_code, 400) stats = UserStats.objects.get(user_id=self.user.id) self.assertIsNone(stats.selected_character_id) def test_selection_reflected_in_list(self): UserStats.objects.create(user_id=self.user.id, xp=10) self._select(self.owl.id) response = self._list() by_slug = {c['slug']: c for c in response.data['characters']} self.assertTrue(by_slug['owl']['selected']) self.assertFalse(by_slug['fox']['selected'])