Ilo/apps/accounts/tests.py
2026-05-02 20:01:30 +03:30

238 lines
8.2 KiB
Python

from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings
from rest_framework.test import APIClient
from apps.accounts.models import OTPCode, Role, TeacherProfile
from apps.accounts.services.otp import (
OTPCooldownError,
OTPVerificationError,
send_otp,
verify_otp,
)
from apps.accounts.validators import normalize_iran_mobile, validate_iran_mobile
User = get_user_model()
CONSOLE_SMS = "apps.accounts.services.sms.ConsoleSMSBackend"
class PhoneNormalizationTests(TestCase):
def test_normalizes_e164(self):
self.assertEqual(normalize_iran_mobile("+989121234567"), "09121234567")
def test_normalizes_0098_prefix(self):
self.assertEqual(normalize_iran_mobile("00989121234567"), "09121234567")
def test_normalizes_98_prefix(self):
self.assertEqual(normalize_iran_mobile("989121234567"), "09121234567")
def test_normalizes_no_leading_zero(self):
self.assertEqual(normalize_iran_mobile("9121234567"), "09121234567")
def test_keeps_canonical_form(self):
self.assertEqual(normalize_iran_mobile("09121234567"), "09121234567")
def test_strips_separators(self):
self.assertEqual(normalize_iran_mobile("0912 123 4567"), "09121234567")
def test_validates_canonical(self):
validate_iran_mobile("09121234567")
def test_rejects_too_short(self):
with self.assertRaises(ValidationError):
validate_iran_mobile("0912123456")
def test_rejects_non_iran(self):
with self.assertRaises(ValidationError):
validate_iran_mobile("+12025551234")
@override_settings(SMS_BACKEND=CONSOLE_SMS)
class OTPServiceTests(TestCase):
def setUp(self):
cache.clear()
def test_send_otp_creates_code(self):
otp = send_otp("09121234567")
self.assertEqual(otp.phone_number, "09121234567")
self.assertEqual(len(otp.code), 4)
self.assertFalse(otp.is_used)
def test_send_otp_within_cooldown_raises(self):
send_otp("09121234567")
with self.assertRaises(OTPCooldownError):
send_otp("09121234567")
def test_verify_correct_code_consumes(self):
otp = send_otp("09121234567")
verified = verify_otp("09121234567", otp.code)
self.assertEqual(verified.pk, otp.pk)
otp.refresh_from_db()
self.assertTrue(otp.is_used)
def test_verify_wrong_code_increments_attempts(self):
send_otp("09121234567")
with self.assertRaises(OTPVerificationError):
verify_otp("09121234567", "0000")
latest = OTPCode.objects.filter(phone_number="09121234567").first()
self.assertEqual(latest.attempts, 1)
self.assertFalse(latest.is_used)
def test_verify_with_no_active_code_raises(self):
with self.assertRaises(OTPVerificationError):
verify_otp("09121234567", "1234")
def test_issuing_new_otp_invalidates_previous(self):
first = send_otp("09121234567")
cache.clear()
second = send_otp("09121234567")
first.refresh_from_db()
self.assertTrue(first.is_used)
self.assertFalse(second.is_used)
self.assertNotEqual(first.pk, second.pk)
@override_settings(SMS_BACKEND=CONSOLE_SMS)
class AuthAPITests(TestCase):
def setUp(self):
cache.clear()
self.client = APIClient()
def _send(self, phone):
return self.client.post("/api/auth/otp/send/", {"phone_number": phone}, format="json")
def _latest_otp(self, phone):
return OTPCode.objects.get(phone_number=phone, is_used=False)
def test_signup_flow_creates_teacher_with_profile(self):
r = self._send("09121234567")
self.assertEqual(r.status_code, 200, r.content)
otp = self._latest_otp("09121234567")
r = self.client.post(
"/api/auth/otp/verify/",
{
"phone_number": "09121234567",
"code": otp.code,
"full_name": "New Teacher",
"role": "teacher",
},
format="json",
)
self.assertEqual(r.status_code, 200, r.content)
body = r.json()
self.assertTrue(body["created"])
self.assertEqual(body["user"]["role"], "teacher")
self.assertIn("access", body["tokens"])
self.assertIn("refresh", body["tokens"])
u = User.objects.get(phone_number="09121234567")
self.assertTrue(TeacherProfile.objects.filter(user=u).exists())
self.assertFalse(u.teacher_profile.is_approved)
def test_signup_flow_creates_student_no_profile(self):
self._send("09121234567")
otp = self._latest_otp("09121234567")
r = self.client.post(
"/api/auth/otp/verify/",
{
"phone_number": "09121234567",
"code": otp.code,
"full_name": "New Student",
"role": "student",
},
format="json",
)
self.assertEqual(r.status_code, 200, r.content)
u = User.objects.get(phone_number="09121234567")
self.assertEqual(u.role, Role.STUDENT)
self.assertFalse(TeacherProfile.objects.filter(user=u).exists())
def test_existing_user_login_does_not_require_signup_fields(self):
u = User.objects.create_user(
phone_number="09121234567", full_name="Existing", role=Role.STUDENT
)
self._send("09121234567")
otp = self._latest_otp("09121234567")
r = self.client.post(
"/api/auth/otp/verify/",
{"phone_number": "09121234567", "code": otp.code},
format="json",
)
self.assertEqual(r.status_code, 200, r.content)
body = r.json()
self.assertFalse(body["created"])
self.assertEqual(body["user"]["id"], u.id)
def test_new_user_without_signup_fields_rejected(self):
self._send("09121234567")
otp = self._latest_otp("09121234567")
r = self.client.post(
"/api/auth/otp/verify/",
{"phone_number": "09121234567", "code": otp.code},
format="json",
)
self.assertEqual(r.status_code, 400)
body = r.json()
self.assertIn("full_name", body)
self.assertIn("role", body)
def test_verify_wrong_code_400(self):
self._send("09121234567")
r = self.client.post(
"/api/auth/otp/verify/",
{
"phone_number": "09121234567",
"code": "0000",
"full_name": "X",
"role": "student",
},
format="json",
)
self.assertEqual(r.status_code, 400)
def test_phone_normalization_in_send(self):
r = self.client.post(
"/api/auth/otp/send/",
{"phone_number": "+989121234567"},
format="json",
)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()["phone_number"], "09121234567")
self.assertTrue(OTPCode.objects.filter(phone_number="09121234567").exists())
def test_invalid_phone_400(self):
r = self.client.post(
"/api/auth/otp/send/", {"phone_number": "12345"}, format="json"
)
self.assertEqual(r.status_code, 400)
def test_cooldown_429(self):
self._send("09121234567")
r = self._send("09121234567")
self.assertEqual(r.status_code, 429)
self.assertIn("retry_after", r.json())
def test_me_requires_auth(self):
r = self.client.get("/api/auth/me/")
self.assertEqual(r.status_code, 401)
def test_me_with_bearer_token(self):
User.objects.create_user(
phone_number="09121234567", full_name="X", role=Role.STUDENT
)
self._send("09121234567")
otp = self._latest_otp("09121234567")
r = self.client.post(
"/api/auth/otp/verify/",
{"phone_number": "09121234567", "code": otp.code},
format="json",
)
access = r.json()["tokens"]["access"]
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {access}")
r2 = self.client.get("/api/auth/me/")
self.assertEqual(r2.status_code, 200)
self.assertEqual(r2.json()["phone_number"], "09121234567")