from unittest.mock import patch import pytest from django.test import override_settings from django.urls import reverse from apps.accounts.models import OtpPurpose, PhoneOTP, User @pytest.mark.django_db @override_settings(OTP_PROVIDER="console") def test_phone_auth_request_creates_customer_for_new_phone(client): with patch("apps.accounts.services.otp.generate_code", return_value="123456"): response = client.post( reverse("phone_auth_request"), { "phone_number": "0512345678", "channel": "sms", "first_name": "Sara", "last_name": "Ali", "email": "sara@example.com", }, content_type="application/json", ) assert response.status_code == 201 data = response.json() assert "request_id" in data assert "expires_at" in data user = User.objects.get(phone_number="+966512345678") assert user.role == "customer" assert user.is_phone_verified is False otp = PhoneOTP.objects.get(id=data["request_id"]) assert otp.phone_number == "+966512345678" assert otp.purpose == OtpPurpose.AUTH @pytest.mark.django_db @override_settings(OTP_PROVIDER="console") def test_phone_auth_request_existing_phone_no_duplicate_user(client): User.objects.create_user( phone_number="+966512345678", email="existing@example.com", first_name="Existing", ) before_count = User.objects.filter(phone_number="+966512345678").count() with patch("apps.accounts.services.otp.generate_code", return_value="123456"): response = client.post( reverse("phone_auth_request"), {"phone_number": "0512345678", "channel": "sms"}, content_type="application/json", ) assert response.status_code == 201 assert User.objects.filter(phone_number="+966512345678").count() == before_count assert PhoneOTP.objects.filter(phone_number="+966512345678").count() == 1 @pytest.mark.django_db @override_settings(OTP_PROVIDER="console") def test_phone_auth_request_rejects_email_already_used(client): User.objects.create_user( phone_number="+966500000001", email="taken@example.com", ) response = client.post( reverse("phone_auth_request"), { "phone_number": "0512345678", "channel": "sms", "email": "taken@example.com", }, content_type="application/json", ) assert response.status_code == 400 assert "detail" in response.json() assert User.objects.filter(phone_number="+966512345678").count() == 0 assert PhoneOTP.objects.filter(phone_number="+966512345678").count() == 0 @pytest.mark.django_db def test_phone_auth_request_invalid_phone_localized_en(client): response = client.post( reverse("phone_auth_request"), {"phone_number": "123", "channel": "sms"}, content_type="application/json", HTTP_ACCEPT_LANGUAGE="en", ) assert response.status_code == 400 assert response.json()["phone_number"][0] == "Phone number must be in E.164 format or a valid Saudi mobile" @pytest.mark.django_db def test_phone_auth_request_invalid_phone_localized_ar(client): response = client.post( reverse("phone_auth_request"), {"phone_number": "123", "channel": "sms"}, content_type="application/json", HTTP_ACCEPT_LANGUAGE="ar-sa", ) assert response.status_code == 400 assert response.json()["phone_number"][0] == "يجب أن يكون رقم الهاتف بصيغة E.164 أو رقم جوال سعودي صالح" @pytest.mark.django_db @override_settings( OTP_PROVIDER="console", OTP_MAX_PER_WINDOW=5, OTP_WINDOW_MINUTES=15, OTP_RESEND_COOLDOWN_SECONDS=60, ) def test_phone_auth_request_cooldown_returns_retry_after(client): with patch("apps.accounts.services.otp.generate_code", return_value="123456"): first = client.post( reverse("phone_auth_request"), {"phone_number": "0512345678", "channel": "sms"}, content_type="application/json", ) assert first.status_code == 201 second = client.post( reverse("phone_auth_request"), {"phone_number": "0512345678", "channel": "sms"}, content_type="application/json", ) assert second.status_code == 429 data = second.json() assert "detail" in data assert data["retry_after_seconds"] > 0 @pytest.mark.django_db @override_settings( OTP_PROVIDER="console", OTP_MAX_PER_WINDOW=1, OTP_WINDOW_MINUTES=15, OTP_RESEND_COOLDOWN_SECONDS=0, ) def test_phone_auth_request_rate_limit_returns_retry_after(client): with patch("apps.accounts.services.otp.generate_code", return_value="123456"): first = client.post( reverse("phone_auth_request"), {"phone_number": "0512345678", "channel": "sms"}, content_type="application/json", ) assert first.status_code == 201 second = client.post( reverse("phone_auth_request"), {"phone_number": "0512345678", "channel": "sms"}, content_type="application/json", ) assert second.status_code == 429 data = second.json() assert "detail" in data assert data["retry_after_seconds"] > 0