From 0973d08637e8eed826e9682a071aa413d39651d1 Mon Sep 17 00:00:00 2001 From: Haydar Ghasemi Date: Sat, 25 Jul 2026 17:16:18 +0330 Subject: [PATCH] FIX(account): fix duplicated invite code fix duplicated invite code --- .../0010_alter_inviterequest_mobile_number.py | 18 ++++++++ apps/users/models.py | 2 +- apps/users/serializers.py | 15 ++++++- apps/users/tests.py | 43 ++++++++++++++----- 4 files changed, 66 insertions(+), 12 deletions(-) create mode 100644 apps/users/migrations/0010_alter_inviterequest_mobile_number.py diff --git a/apps/users/migrations/0010_alter_inviterequest_mobile_number.py b/apps/users/migrations/0010_alter_inviterequest_mobile_number.py new file mode 100644 index 0000000..876bfa5 --- /dev/null +++ b/apps/users/migrations/0010_alter_inviterequest_mobile_number.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2026-07-25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0009_inviterequest'), + ] + + operations = [ + migrations.AlterField( + model_name='inviterequest', + name='mobile_number', + field=models.CharField(db_index=True, max_length=30, unique=True, verbose_name='mobile number'), + ), + ] diff --git a/apps/users/models.py b/apps/users/models.py index b5007d4..3aec958 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -278,7 +278,7 @@ class InviteRequest(BaseModel): related_name='invite_requests', verbose_name=_('inviter'), ) - mobile_number = models.CharField(_('mobile number'), max_length=30, db_index=True) + mobile_number = models.CharField(_('mobile number'), unique=True, max_length=30, db_index=True) application = models.ForeignKey( oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, diff --git a/apps/users/serializers.py b/apps/users/serializers.py index a43ec4a..2afd9b9 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -7,7 +7,7 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from unidecode import unidecode -from utils.exceptions import UnprocessableEntity +from utils.exceptions import UnprocessableEntity, Conflict phone_number_validator = RegexValidator(regex=r'^\+98[1-9]\d{9}$', message=_( "Phone number must be entered in the format: '+989999999999'." @@ -150,6 +150,19 @@ class InviteRequestSerializer(serializers.ModelSerializer): def validate_mobile_number(self, value): value = unidecode(value) phone_number_validator(value) + + if User.objects.filter(phone_number=value).exists(): + raise Conflict( + detail=_('This mobile number is already registered as a user.'), + code='user_already_exists', + ) + + if InviteRequest.objects.filter(mobile_number=value).exists(): + raise Conflict( + detail=_('An invite request for this mobile number already exists.'), + code='invite_request_already_exists', + ) + return value def create(self, validated_data): diff --git a/apps/users/tests.py b/apps/users/tests.py index 5a1d4a7..e3762ec 100644 --- a/apps/users/tests.py +++ b/apps/users/tests.py @@ -153,9 +153,11 @@ class InviteRequestTests(APITestCase): user_uuid = uuid.UUID('c14e8b86-8f4a-44d9-b29d-badceb47005f') inviter_uuid = uuid.UUID('d14e8b86-8f4a-44d9-b29d-badceb47005f') application_uuid = uuid.UUID('e14e8b86-8f4a-44d9-b29d-badceb47005f') + access_token = 'inviteAccessToken123456789012345678901' client_id = 'inviteClientId1234567890123456789012' client_secret = 'inviteClientSecret123456789012345678' mobile_number = '+989120000000' + expire_datetime = timezone.now() + timedelta(seconds=3600) client = APIClient() @@ -177,9 +179,13 @@ class InviteRequestTests(APITestCase): allowed_scope='accounts.invite:submit', ) - def basic_auth_header(self): - credentials = f'{self.client_id}:{self.client_secret}'.encode('utf-8') - return 'Basic ' + base64.b64encode(credentials).decode('utf-8') + AccessToken.objects.create( + token=self.access_token, + application_id=self.application_uuid, + scope='accounts.invite:submit', + expires=self.expire_datetime, + ) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {self.access_token}') def test_create_invite_request_success(self): response = self.client.post( @@ -189,7 +195,6 @@ class InviteRequestTests(APITestCase): 'mobile_number': self.mobile_number, }, format='json', - HTTP_AUTHORIZATION=self.basic_auth_header(), ) self.assertEqual(response.status_code, 201) @@ -208,13 +213,32 @@ class InviteRequestTests(APITestCase): 'mobile_number': self.mobile_number, }, format='json', - HTTP_AUTHORIZATION=self.basic_auth_header(), ) self.assertEqual(response.status_code, 400) self.assertEqual(InviteRequest.objects.count(), 0) - def test_create_invite_request_invalid_client_credentials(self): + def test_create_invite_request_existing_user(self): + response = self.client.post( + reverse('users:invite_request_api'), + data={ + 'invite_code': str(self.inviter_uuid), + 'mobile_number': self.inviter.phone_number, + }, + format='json', + ) + + self.assertEqual(response.status_code, 409) + self.assertEqual(response.json()['details']['error'], 'user_already_exists') + self.assertEqual(InviteRequest.objects.count(), 0) + + def test_create_invite_request_duplicate_mobile_number(self): + InviteRequest.objects.create( + inviter=self.inviter, + mobile_number=self.mobile_number, + application=self.application, + ) + response = self.client.post( reverse('users:invite_request_api'), data={ @@ -222,11 +246,11 @@ class InviteRequestTests(APITestCase): 'mobile_number': self.mobile_number, }, format='json', - HTTP_AUTHORIZATION='Basic ' + base64.b64encode(b'wrong:credentials').decode('utf-8'), ) - self.assertEqual(response.status_code, 401) - self.assertEqual(InviteRequest.objects.count(), 0) + self.assertEqual(response.status_code, 409) + self.assertEqual(response.json()['details']['error'], 'invite_request_already_exists') + self.assertEqual(InviteRequest.objects.count(), 1) def test_create_invite_request_missing_scope(self): self.application.allowed_scope = '' @@ -239,7 +263,6 @@ class InviteRequestTests(APITestCase): 'mobile_number': self.mobile_number, }, format='json', - HTTP_AUTHORIZATION=self.basic_auth_header(), ) self.assertEqual(response.status_code, 403)