feat: DB constraints for phone auth
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 6.0.3 on 2026-03-13 20:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("accounts", "0003_preferred_language"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="phone_number",
|
||||
field=models.CharField(max_length=20, unique=True),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="user",
|
||||
constraint=models.CheckConstraint(
|
||||
condition=models.Q(("phone_number__regex", r"^\+[1-9][0-9]{7,14}$")),
|
||||
name="accounts_user_phone_e164_format",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -4,6 +4,7 @@ from datetime import timedelta
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
@@ -42,7 +43,7 @@ class UserManager(BaseUserManager):
|
||||
|
||||
class User(AbstractBaseUser, PermissionsMixin):
|
||||
email = models.EmailField(unique=True, null=True, blank=True)
|
||||
phone_number = models.CharField(max_length=20, unique=True, null=True, blank=True)
|
||||
phone_number = models.CharField(max_length=20, unique=True)
|
||||
role = models.CharField(max_length=20, choices=UserRole.choices, default=UserRole.CUSTOMER)
|
||||
first_name = models.CharField(max_length=150, blank=True)
|
||||
last_name = models.CharField(max_length=150, blank=True)
|
||||
@@ -62,6 +63,14 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
USERNAME_FIELD = "phone_number"
|
||||
REQUIRED_FIELDS = [] # email is optional; phone_number is the identifier
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
name="accounts_user_phone_e164_format",
|
||||
condition=Q(phone_number__regex=r"^\+[1-9][0-9]{7,14}$"),
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.email or self.phone_number or str(self.id)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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
|
||||
@@ -69,3 +70,24 @@ def test_otp_verify_rejects_auth_purpose_otp(client):
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user