from unittest.mock import patch import pytest from django.db import IntegrityError from django.urls import reverse from apps.accounts.models import OtpPurpose, PhoneOTP, User @pytest.mark.django_db def test_create_user_rejects_email_only_identity(): # Phone-first invariant: do not allow creating users without phone_number. with pytest.raises(ValueError): User.objects.create_user(email="email-only@example.com") @pytest.mark.django_db def test_register_requires_phone_number(client): # Public registration must keep phone as required identifier. response = client.post( reverse("register"), {"email": "new@example.com", "password": "StrongPass123"}, content_type="application/json", ) assert response.status_code == 400 assert "phone_number" in response.json() @pytest.mark.django_db def test_phone_auth_verify_rejects_verify_purpose_otp(client): user = User.objects.create_user(phone_number="+966512345678") otp = PhoneOTP.objects.create( phone_number=user.phone_number, channel="sms", purpose=OtpPurpose.VERIFY, provider="console", code_hash="not-used", expires_at=PhoneOTP.expiry_at(), ) # Purpose boundary: /phone/verify must only accept auth OTP requests. with patch("apps.accounts.views.verify_otp", return_value=True): response = client.post( reverse("phone_auth_verify"), {"request_id": str(otp.id), "code": "123456"}, content_type="application/json", ) assert response.status_code == 400 @pytest.mark.django_db def test_otp_verify_rejects_auth_purpose_otp(client): otp = PhoneOTP.objects.create( phone_number="+966512345678", channel="sms", purpose=OtpPurpose.AUTH, provider="console", code_hash="not-used", expires_at=PhoneOTP.expiry_at(), ) # Purpose boundary: /otp/verify must only accept verify OTP requests. with patch("apps.accounts.views.verify_otp", return_value=True): response = client.post( reverse("otp_verify"), {"request_id": str(otp.id), "code": "123456"}, content_type="application/json", ) assert response.status_code == 400 @pytest.mark.django_db def test_db_rejects_null_phone_number(): # DB invariant: phone_number is mandatory. with pytest.raises(IntegrityError): User.objects.create(email="null-phone@example.com") @pytest.mark.django_db def test_db_rejects_non_e164_phone_number(): # DB invariant: store normalized E.164 only. with pytest.raises(IntegrityError): User.objects.create_user(phone_number="0512345678") @pytest.mark.django_db def test_db_rejects_duplicate_phone_number(): User.objects.create_user(phone_number="+966512345678") with pytest.raises(IntegrityError): User.objects.create_user(phone_number="+966512345678") @pytest.mark.django_db def test_password_token_endpoint_is_disabled(client): User.objects.create_user(phone_number="+966512345678", password="StrongPass123") response = client.post( reverse("token_obtain_pair"), {"phone_number": "+966512345678", "password": "StrongPass123"}, content_type="application/json", ) assert response.status_code == 410 assert "detail" in response.json()