170 lines
5.3 KiB
Python
170 lines
5.3 KiB
Python
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.channel == "sms"
|
|
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",
|
|
)
|
|
|
|
before_otp_count = PhoneOTP.objects.count()
|
|
|
|
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.count() == before_otp_count
|
|
|
|
|
|
@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
|