Files
MathApp/ai_tutor/tests_credit_concurrency.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

74 lines
3.1 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.
"""
اعتبارِ بسته زیرِ درخواستِ هم‌زمان (بازبینیِ ۲۰۲۶-۱۰-۰۲).
پیش از این فقط ادعا بود که قفلِ ردیفِ کاربر و `transaction_mode: IMMEDIATE`ِ SQLite جلوی
خرجِ دوباره‌ی یک اعتبار را می‌گیرند. این‌جا دو thread واقعی، با دو اتصالِ جدا به دیتابیسِ فایلیِ
تست، هم‌زمان می‌پرسند. جوابِ دستیار ساختگی است و ۰٫۴ ثانیه طول می‌کشد تا دو درخواست واقعاً روی
هم بیفتند. فقط روی SQLite سنجیده شده؛ بعد از مهاجرت به Postgres دوباره باید سبز شود.
"""
import threading
import time
from unittest.mock import patch
from django.core.cache import cache
from django.db import connection
from django.test import TransactionTestCase, override_settings
from rest_framework.test import APIClient
from accounts.authentication import issue_access_token
from accounts.models import User
from subscription.models import QuestionCredit
from subscription.services import credit_balance
from .models import Message
def _slow_answer(*args, **kwargs):
time.sleep(0.4)
return 'جواب', ['بیشتر توضیح بده']
@override_settings(AI_DAILY_QUOTA_FREE=0)
class ConcurrentCreditTests(TransactionTestCase):
def setUp(self):
cache.clear()
self.user = User.objects.create(phone='09120007201')
def tearDown(self):
cache.clear()
def race(self, *headers):
codes = []
barrier = threading.Barrier(len(headers))
def ask(extra):
try:
client = APIClient()
client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
barrier.wait()
codes.append(client.post('/api/ai/ask', {'userQuestion': '۲ + ۲؟'}, format='json', **extra).status_code)
finally:
connection.close()
with patch('rag.services.config.rag_is_configured', return_value=False), \
patch('ai_tutor.views.generate_tutor_answer', side_effect=_slow_answer):
threads = [threading.Thread(target=ask, args=(h,)) for h in headers]
for t in threads:
t.start()
for t in threads:
t.join()
return sorted(codes)
def test_two_questions_at_once_with_one_credit_get_one_answer(self):
QuestionCredit.objects.create(user_id=self.user.id, delta=1, reason='grant')
self.assertEqual(self.race({}, {}), [200, 429])
self.assertEqual(credit_balance(self.user.id), 0)
self.assertEqual(Message.objects.filter(role='assistant').count(), 1)
def test_the_same_idempotency_key_twice_at_once_costs_one_credit(self):
QuestionCredit.objects.create(user_id=self.user.id, delta=2, reason='grant')
key = {'HTTP_IDEMPOTENCY_KEY': 'same-key'}
self.assertEqual(self.race(key, key), [200, 200])
self.assertEqual(credit_balance(self.user.id), 1)
self.assertEqual(Message.objects.filter(role='user').count(), 1)