Permettre une création de compte par numéro de téléphone via l’API d’enregistrement (#83190) #167

Closed
pmarillonnet wants to merge 4 commits from wip/83190-phone-registration-api-endpoint into wip/82737-authn-tel-post-registration-account-selection-buggy-form
12 changed files with 496 additions and 30 deletions

View File

@ -145,6 +145,7 @@ def extract_settings_from_environ():
'A2_CAN_RESET_PASSWORD',
'A2_REGISTRATION_CAN_DELETE_ACCOUNT',
'A2_REGISTRATION_EMAIL_IS_UNIQUE',
'A2_REGISTRATION_PHONE_IS_UNIQUE',
'REGISTRATION_OPEN',
'A2_AUTH_PASSWORD_ENABLE',
'SSLAUTH_ENABLE',

View File

@ -68,6 +68,7 @@ class OrganizationalUnitAdmin(admin.ModelAdmin):
'description',
'username_is_unique',
'email_is_unique',
'phone_is_unique',
'default',
'validate_emails',
'user_can_reset_password',

View File

@ -0,0 +1,17 @@
# Generated by Django 3.2.18 on 2023-11-06 15:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('a2_rbac', '0037_remove_organizationalunit_min_password_strength'),
]
operations = [
migrations.AddField(
model_name='organizationalunit',
name='phone_is_unique',
field=models.BooleanField(blank=True, default=False, verbose_name='Phone is unique'),
),
]

View File

@ -104,6 +104,7 @@ class OrganizationalUnit(AbstractBase):
username_is_unique = models.BooleanField(blank=True, default=False, verbose_name=_('Username is unique'))
email_is_unique = models.BooleanField(blank=True, default=False, verbose_name=_('Email is unique'))
phone_is_unique = models.BooleanField(blank=True, default=False, verbose_name=_('Phone is unique'))
default = fields.UniqueBooleanField(verbose_name=_('Default organizational unit'))
validate_emails = models.BooleanField(blank=True, default=False, verbose_name=_('Validate emails'))
@ -234,6 +235,7 @@ class OrganizationalUnit(AbstractBase):
'description': self.description,
'default': self.default,
'email_is_unique': self.email_is_unique,
'phone_is_unique': self.phone_is_unique,
'username_is_unique': self.username_is_unique,
'validate_emails': self.validate_emails,
}

View File

@ -27,6 +27,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import MultipleObjectsReturned
from django.db import models, transaction
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.dateparse import parse_datetime
from django.utils.encoding import force_str
from django.utils.functional import cached_property
@ -64,10 +65,11 @@ from .a2_rbac.utils import get_default_ou
from .apps.journal.models import Event
from .custom_user.models import Profile, ProfileType, User
from .journal_event_types import UserLogin, UserRegistration
from .models import APIClient, Attribute, PasswordReset, Service
from .models import APIClient, Attribute, AttributeValue, PasswordReset, Service
from .passwords import get_password_checker, get_password_strength
from .utils import hooks
from .utils import misc as utils_misc
from .utils import sms as utils_sms
from .utils.api import DjangoRBACPermission, NaturalKeyRelatedField
from .utils.lookups import Unaccent
@ -132,10 +134,12 @@ class RegistrationSerializer(serializers.Serializer):
allow_null=True,
)
username = serializers.CharField(required=False, allow_blank=True)
phone = serializers.CharField(required=False, allow_blank=True)
first_name = serializers.CharField(required=False, allow_blank=True, default='')
last_name = serializers.CharField(required=False, allow_blank=True, default='')
password = serializers.CharField(required=False, allow_null=True)
no_email_validation = serializers.BooleanField(required=False)
no_phone_validation = serializers.BooleanField(required=False)
return_url = serializers.URLField(required=False, allow_blank=True)
def validate(self, attrs):
@ -155,25 +159,60 @@ class RegistrationSerializer(serializers.Serializer):
if 'email' not in attrs:
raise serializers.ValidationError(_('Email is required'))
if User.objects.filter(email__iexact=attrs['email']).exists():
raise serializers.ValidationError(_('Account already exists'))
raise serializers.ValidationError(_('Account already exists when looking up by email'))
if ou.email_is_unique:
if 'email' not in attrs:
raise serializers.ValidationError(_('Email is required in this ou'))
if User.objects.filter(ou=ou, email__iexact=attrs['email']).exists():
raise serializers.ValidationError(_('Account already exists in this ou'))
raise serializers.ValidationError(
_('Account already exists in this ou when looking up by email')
)
if app_settings.A2_USERNAME_IS_UNIQUE or app_settings.A2_REGISTRATION_USERNAME_IS_UNIQUE:
if 'username' not in attrs:
raise serializers.ValidationError(_('Username is required'))
if User.objects.filter(username=attrs['username']).exists():
raise serializers.ValidationError(_('Account already exists'))
raise serializers.ValidationError(_('Account already exists when looking up by username'))
if ou.username_is_unique:
if 'username' not in attrs:
raise serializers.ValidationError(_('Username is required in this ou'))
if User.objects.filter(ou=ou, username=attrs['username']).exists():
raise serializers.ValidationError(_('Account already exists in this ou'))
raise serializers.ValidationError(
_('Account already exists in this ou when looking up by username')
)
user_ct = ContentType.objects.get_for_model(User)
phone_field = utils_misc.get_password_authenticator().phone_identifier_field
if phone_field and (
app_settings.A2_PHONE_IS_UNIQUE or app_settings.A2_REGISTRATION_PHONE_IS_UNIQUE
):
if 'phone' not in attrs:
raise serializers.ValidationError(_('Phone is required'))
if AttributeValue.objects.filter(
content=attrs['phone'],
attribute=phone_field,
content_type=user_ct,
).exists():
raise serializers.ValidationError(
_('Account already exists when looking up by phone number')
)
if ou.phone_is_unique:
if 'phone' not in attrs:
raise serializers.ValidationError(_('Phone is required'))
known_owner_ids = AttributeValue.objects.filter(
content=attrs['phone'],
attribute=phone_field,
content_type=user_ct,
).values_list('object_id', flat=True)
if ou.id in set(User.objects.filter(id__in=known_owner_ids).values_list('ou', flat=True)):
raise serializers.ValidationError(
_('Account already exists in this ou when looking up by phone number')
)
return attrs
@ -216,7 +255,6 @@ class Register(BaseRpcView):
raise PermissionDenied(
'You do not have permission to create users in ou %s' % validated_data['ou'].slug
)
email = validated_data.get('email')
registration_data = {}
for field in ('first_name', 'last_name', 'password', 'username'):
if field in validated_data:
@ -228,6 +266,8 @@ class Register(BaseRpcView):
'registration_data': registration_data,
}
email = validated_data.get('email')
phone = validated_data.get('phone')
token = None
final_return_url = None
if validated_data.get('return_url'):
@ -262,18 +302,40 @@ class Register(BaseRpcView):
if token:
response['token'] = token
response_status = status.HTTP_202_ACCEPTED
elif phone and not validated_data.get('no_phone_validation'):
try:
# XXX custom code lifetime for registration API?
code = utils_sms.send_registration_sms(
phone,
validated_data['ou'],
)
except utils_sms.SMSError as e:
response = {
'result': 0,
'errors': {'__all__': ['SMS sending failed']},
'exception': force_str(e),
}
response_status = status.HTTP_503_SERVICE_UNAVAILABLE
else:
response = {
'result': 1,
}
if token:
response['token'] = token
response['validation_url'] = reverse('input_sms_code', kwargs={'token': code.url_token})
response_status = status.HTTP_202_ACCEPTED
else:
username = validated_data.get('username')
first_name = validated_data.get('first_name')
last_name = validated_data.get('last_name')
password = validated_data.get('password')
ou = validated_data.get('ou')
if not email and not username and not (first_name and last_name):
if not email and not phone and not username and not (first_name and last_name):
response = {
'result': 0,
'errors': {
'__all__': [
'You must set at least a username, an email or a first name and a last name'
'You must set at least a username, an email, a phone number, or first and last names'
]
},
}
@ -285,6 +347,13 @@ class Register(BaseRpcView):
if password:
new_user.set_password(password)
new_user.save()
if phone:
AttributeValue.objects.create(
attribute=utils_misc.get_password_authenticator().phone_identifier_field,
content=phone,
object_id=new_user.id,
content_type=ContentType.objects.get_for_model(new_user),
)
validated_data['uuid'] = new_user.uuid
response = {
'result': 1,

View File

@ -138,9 +138,13 @@ default_settings = dict(
A2_USER_CAN_RESET_PASSWORD_BY_USERNAME=Setting(
default=False, definition='Allow password reset request by username'
),
A2_EMAIL_IS_UNIQUE=Setting(default=False, definition='Email of users must be unique'),
A2_EMAIL_IS_UNIQUE=Setting(default=False, definition="Users' email address must be unique"),
A2_REGISTRATION_EMAIL_IS_UNIQUE=Setting(
default=False, definition='Email of registered accounts must be unique'
default=False, definition='Email address declared at registration time must be unique'
),
A2_PHONE_IS_UNIQUE=Setting(default=False, definition="Users' phone number must be unique"),
A2_REGISTRATION_PHONE_IS_UNIQUE=Setting(
default=False, definition='Phone number declared at registration time must be unique'
),
A2_REGISTRATION_FORM_USERNAME_REGEX=Setting(
default=r'^[\w.@+-]+$', definition='Regex to validate usernames'

View File

@ -7,8 +7,17 @@
{% block content %}
<h2>{% trans "Login" %}</h2>
{% if email %}
<p>
{% blocktrans trimmed count accounts_number=accounts|length %}An account already exists for this email address.{% plural %}Existing accounts are associated with this email address.{% endblocktrans %}
</p>
{% elif phone %}
<p>
{% blocktrans trimmed count accounts_number=accounts|length %}An account already exists for this phone number.{% plural %}Existing accounts are associated with this phone number.{% endblocktrans %}
</p>
{% endif %}
<p>
{% blocktrans trimmed count accounts_number=accounts|length %}An account already exists for this email. Please click on the account name to log in with.{% plural %}More accounts are associated to this email. Please choose the account you want to log in with:{% endblocktrans %}
{% blocktrans trimmed count accounts_number=accounts|length %} Please click on the account name to log in with.{% plural %}Please choose the account you want to log in with.{% endblocktrans %}
</p>
<ul>

View File

@ -1776,9 +1776,19 @@ class RegistrationCompletionView(CreateView):
self.users = User.objects.filter(**qs_filter).order_by('date_joined')
if self.ou:
self.users = self.users.filter(ou=self.ou)
self.email_is_unique = app_settings.A2_EMAIL_IS_UNIQUE or app_settings.A2_REGISTRATION_EMAIL_IS_UNIQUE
if self.ou:
self.email_is_unique |= self.ou.email_is_unique
self.email_is_unique = self.phone_is_unique = False
if self.token.get('email', None):
self.email_is_unique = (
app_settings.A2_EMAIL_IS_UNIQUE or app_settings.A2_REGISTRATION_EMAIL_IS_UNIQUE
)
if self.ou:
self.email_is_unique |= self.ou.email_is_unique
elif self.token.get('phone', None) and self.authenticator.is_phone_authn_active:
self.phone_is_unique = (
app_settings.A2_PHONE_IS_UNIQUE or app_settings.A2_REGISTRATION_PHONE_IS_UNIQUE
)
if self.ou:
self.phone_is_unique |= self.ou.phone_is_unique
self.init_fields_labels_and_help_texts()
set_home_url(request, self.get_success_url())
return super().dispatch(request, *args, **kwargs)
@ -1901,12 +1911,18 @@ class RegistrationCompletionView(CreateView):
if hasattr(self, 'phone'):
ctx['phone'] = self.phone
ctx['email_is_unique'] = self.email_is_unique
ctx['phone_is_unique'] = self.phone_is_unique
ctx['create'] = 'create' in self.request.GET
return ctx
def get(self, request, *args, **kwargs):
if len(self.users) == 1 and self.email_is_unique:
# Found one user, EMAIL is unique, log her in
if len(self.users) == 1 and (
self.email_is_unique
and self.token.get('email', None)
or self.phone_is_unique
and self.token.get('phone', None)
):
# Found one user whose identifier is unique, log her in
utils_misc.simulate_authentication(request, self.users[0], method=self.authentication_method)
return utils_misc.redirect(request, self.get_success_url())
confirm_data = self.token.get('confirm_data', False)
@ -1936,8 +1952,13 @@ class RegistrationCompletionView(CreateView):
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
if self.users and self.email_is_unique:
# email is unique, users already exist, creating a new one is forbidden !
if self.users and (
self.email_is_unique
and self.token.get('email', None)
or self.phone_is_unique
and self.token.get('phone', None)
):
# identifier is unique, users already exist, creating a new one is forbidden !
return utils_misc.redirect(
request, request.resolver_match.view_name, args=self.args, kwargs=self.kwargs
)

View File

@ -32,6 +32,7 @@ from django.urls import reverse
from django.utils.encoding import force_str
from django.utils.text import slugify
from django.utils.timezone import now
from httmock import HTTMock, remember_called, urlmatch
from requests.models import Response
from authentic2.a2_rbac.models import SEARCH_OP
@ -41,7 +42,15 @@ from authentic2.a2_rbac.utils import get_default_ou
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
from authentic2.apps.journal.models import Event, EventType
from authentic2.custom_user.models import Profile, ProfileType
from authentic2.models import APIClient, Attribute, AttributeValue, AuthorizedRole, PasswordReset, Service
from authentic2.models import (
APIClient,
Attribute,
AttributeValue,
AuthorizedRole,
PasswordReset,
Service,
SMSCode,
)
from authentic2.utils.misc import good_next_url
from authentic2_idp_cas.models import Service as CASService
@ -85,6 +94,18 @@ USER_ATTRIBUTES_SET = {
}
@urlmatch(netloc='foo.whatever.none')
@remember_called
def sms_service_mock(url, request):
return {
'content': {},
'headers': {
'content-type': 'application/json',
},
'status_code': 200,
}
def test_api_user_simple(logged_app):
resp = logged_app.get('/api/user/')
assert isinstance(resp.json, dict)
@ -1101,7 +1122,7 @@ def test_register_no_email_validation(settings, app, admin, django_user_model):
assert 'errors' in response.json
assert response.json['result'] == 0
assert response.json['errors'] == {
'__all__': ['You must set at least a username, an email or a first name and a last name'],
'__all__': ['You must set at least a username, an email, a phone number, or first and last names'],
}
# valid payload
@ -1159,7 +1180,7 @@ def test_register_ou_no_email_validation(settings, app, admin, django_user_model
assert 'errors' in response.json
assert response.json['result'] == 0
assert response.json['errors'] == {
'__all__': ['You must set at least a username, an email or a first name and a last name'],
'__all__': ['You must set at least a username, an email, a phone number, or first and last names'],
}
# valid payload
@ -2658,7 +2679,7 @@ def test_api_users_create_user_delete(app, settings, admin):
assert resp.json['errors'] == {'email': ['email already used']}
def test_api_register_user_delete(app, settings, admin):
def test_api_register_user_delete_with_email_identifier(app, settings, admin):
settings.A2_EMAIL_IS_UNIQUE = True
user = User.objects.create(username='foo', email='john.doe@example.com', ou=get_default_ou())
@ -2675,12 +2696,91 @@ def test_api_register_user_delete(app, settings, admin):
'ou': 'default',
}
response = app.post_json(reverse('a2-api-register'), params=payload, headers=headers, status=400)
assert response.json['errors'] == {'__all__': ['Account already exists']}
assert response.json['errors'] == {'__all__': ['Account already exists when looking up by email']}
user.delete()
response = app.post_json(reverse('a2-api-register'), params=payload, headers=headers, status=201)
def test_api_register_user_delete_with_phone_no_identifier_validation(
app, settings, admin, phone_activated_authn
):
settings.A2_PHONE_IS_UNIQUE = True
user = User.objects.create(username='foo', ou=get_default_ou())
user.attributes.phone = '+33111221133'
user.save()
headers = basic_authorization_header(admin)
payload = {
'username': 'john.doe',
'phone': '+33111221133',
'email': 'john.doe@example.com',
'first_name': 'John',
'last_name': 'Doe',
'password': '12XYab',
'no_phone_validation': True,
'no_email_validation': True,
'return_url': 'http://sp.example.com/validate/',
'ou': 'default',
}
response = app.post_json(reverse('a2-api-register'), params=payload, headers=headers, status=400)
assert response.json['errors'] == {'__all__': ['Account already exists when looking up by phone number']}
user.delete()
response = app.post_json(reverse('a2-api-register'), params=payload, headers=headers, status=201)
assert User.objects.get(email='john.doe@example.com').attributes.phone == '+33111221133'
def test_api_register_user_delete_with_phone_validation(app, settings, admin, phone_activated_authn):
settings.A2_PHONE_IS_UNIQUE = True
user = User.objects.create(username='foo', ou=get_default_ou())
user.attributes.phone = '+33111221133'
user.save()
headers = basic_authorization_header(admin)
payload = {
'username': 'john.doe',
'phone': '+33111221133',
'first_name': 'John',
'last_name': 'Doe',
'password': '12XYab',
'return_url': 'http://sp.example.com/validate/',
'ou': 'default',
}
response = app.post_json(reverse('a2-api-register'), params=payload, headers=headers, status=400)
assert response.json['errors'] == {'__all__': ['Account already exists when looking up by phone number']}
user.delete()
response = app.post_json(reverse('a2-api-register'), params=payload, headers=headers, status=503)
assert response.json['errors'] == {'__all__': ['SMS sending failed']}
assert SMSCode.objects.count() == 0
settings.SMS_URL = 'https://foo.whatever.none/'
with HTTMock(sms_service_mock):
response = app.post_json(reverse('a2-api-register'), params=payload, headers=headers, status=202)
assert SMSCode.objects.count() == 1
response = app.get(response.json['validation_url'])
form = response.form
form.set('sms_code', SMSCode.objects.get().value)
response = form.submit().follow()
# XXX even when validation happens, fields should be pre-filled
# store registration_data in SMSCode then in Token (?)
form = response.form
form.set('first_name', 'Didier')
form.set('last_name', 'Toto')
form.set('password1', 'Bépo7390$$!')
form.set('password2', 'Bépo7390$$!')
response = form.submit()
assert response.location == '/'
response = response.follow()
assert response.pyquery('ul.messages li').text() == 'You have just created an account.'
def test_api_password_change_user_delete(app, settings, admin, ou1):
app.authorization = ('Basic', (admin.username, admin.username))
user1 = User.objects.create(username='john.doe', email='john.doe@example.com', ou=ou1)

View File

@ -277,6 +277,7 @@ def test_ou_export_json(db):
description='basic ou description',
username_is_unique=True,
email_is_unique=True,
phone_is_unique=True,
default=False,
validate_emails=True,
)
@ -287,6 +288,7 @@ def test_ou_export_json(db):
assert ou_dict['description'] == ou.description
assert ou_dict['username_is_unique'] == ou.username_is_unique
assert ou_dict['email_is_unique'] == ou.email_is_unique
assert ou_dict['phone_is_unique'] == ou.phone_is_unique
assert ou_dict['default'] == ou.default
assert ou_dict['validate_emails'] == ou.validate_emails

View File

@ -35,6 +35,7 @@ from rest_framework import status, test
from authentic2 import attribute_kinds, models
from authentic2.a2_rbac.models import OrganizationalUnit, Role
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
from authentic2.utils import misc as utils_misc
from authentic2.utils.misc import continue_to_next_url, login_require, make_url, redirect, redirect_to_login
@ -394,7 +395,7 @@ class APITest(TestCase):
ct_user = ContentType.objects.get_for_model(User)
self.ou = OrganizationalUnit.objects.create(
slug='ou', name='OU', email_is_unique=True, username_is_unique=True
slug='ou', name='OU', email_is_unique=True, username_is_unique=True, phone_is_unique=False
)
self.reguser1 = User.objects.create(username='reguser1')
self.reguser1.set_password('password')
@ -493,7 +494,10 @@ class APITest(TestCase):
response = client.post(
reverse('a2-api-register'), content_type='application/json', data=json.dumps(payload)
)
self.assertEqual(response.data['errors']['__all__'], [_('Account already exists in this ou')])
self.assertEqual(
response.data['errors']['__all__'],
[_('Account already exists in this ou when looking up by email')],
)
# Username is required
payload = {
'email': '1' + email,
@ -516,7 +520,9 @@ class APITest(TestCase):
response = client.post(
reverse('a2-api-register'), content_type='application/json', data=json.dumps(payload)
)
self.assertEqual(response.data['errors']['__all__'], [_('Account already exists')])
self.assertEqual(
response.data['errors']['__all__'], [_('Account already exists when looking up by username')]
)
def test_register_reguser2_wrong_ou(self):
client = test.APIClient()
@ -539,10 +545,21 @@ class APITest(TestCase):
self.assertIn('errors', response.data)
@override_settings(A2_REQUIRED_FIELDS=['username'])
def test_email_is_unique_double_registration(self):
def test_identifier_is_unique_double_registration(self):
# disable existing attributes
models.Attribute.objects.update(disabled=True)
phone, dummy = models.Attribute.objects.get_or_create(
name='phone',
kind='phone_number',
user_editable=True,
defaults={'label': 'Phone'},
)
LoginPasswordAuthenticator.objects.update(
accept_phone_authentication=True,
phone_identifier_field=phone,
)
cred = self.reguser3_cred
User = get_user_model()
user_count = User.objects.count()
@ -550,10 +567,12 @@ class APITest(TestCase):
password = '12=XY=ab'
username = 'john.doe'
email = 'john.doe@example.com'
phone = '+3311221133'
return_url = 'http://sp.org/register/'
payload = {
'email': email,
'username': username,
'phone': phone,
'ou': self.ou.slug,
'password': password,
'return_url': return_url,
@ -599,7 +618,10 @@ class APITest(TestCase):
response = client.post(
reverse('a2-api-register'), content_type='application/json', data=json.dumps(payload)
)
self.assertEqual(response.data['errors']['__all__'], [_('Account already exists in this ou')])
self.assertEqual(
response.data['errors']['__all__'],
[_('Account already exists in this ou when looking up by email')],
)
# Username is required
payload = {
'email': '1' + email,
@ -622,7 +644,42 @@ class APITest(TestCase):
response = client.post(
reverse('a2-api-register'), content_type='application/json', data=json.dumps(payload)
)
self.assertEqual(response.data['errors']['__all__'], [_('Account already exists')])
self.assertEqual(
response.data['errors']['__all__'], [_('Account already exists when looking up by username')]
)
last_user.attributes.phone = phone
last_user.save()
self.ou.phone_is_unique = True
self.ou.save()
# Phone is required
payload = {
'email': '1' + email,
'username': '1' + username,
'ou': self.ou.slug,
'password': password,
'return_url': return_url,
}
response = client.post(
reverse('a2-api-register'), content_type='application/json', data=json.dumps(payload)
)
self.assertEqual(response.data['errors']['__all__'], [_('Phone is required')])
# Test phone is unique
payload = {
'email': '1' + email,
'username': '1' + username,
'phone': phone,
'ou': self.ou.slug,
'password': password,
'return_url': return_url,
}
response = client.post(
reverse('a2-api-register'), content_type='application/json', data=json.dumps(payload)
)
self.assertEqual(
response.data['errors']['__all__'],
[_('Account already exists in this ou when looking up by phone number')],
)
@override_settings(A2_REQUIRED_FIELDS=['username'])
def test_email_username_is_unique_double_registration(self):

View File

@ -25,11 +25,12 @@ from django.urls import reverse
from httmock import HTTMock, remember_called, urlmatch
from authentic2 import models
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
from authentic2.apps.journal.models import Event
from authentic2.forms.profile import modelform_factory
from authentic2.forms.registration import RegistrationCompletionForm
from authentic2.models import Attribute, SMSCode, Token
from authentic2.models import Attribute, AttributeValue, SMSCode, Token
from authentic2.utils import misc as utils_misc
from authentic2.validators import EmailValidator
@ -1098,6 +1099,188 @@ def test_phone_registration_connection_error(app, db, settings, freezer, caplog,
)
def test_phone_registration_number_already_existing_create(app, db, settings, phone_activated_authn):
settings.SMS_URL = 'https://foo.whatever.none/'
# create duplicates
for i in range(3):
user = User.objects.create(
first_name='John',
last_name='Doe',
email=f'john-{i}@example.com',
username=f'john-{i}',
ou=get_default_ou(),
)
user.attributes.phone = '+33612345678'
user.save()
resp = app.get(reverse('registration_register'))
resp.form.set('phone_1', '612345678')
with HTTMock(sms_service_mock):
resp = resp.form.submit().follow()
code = SMSCode.objects.get()
resp.form.set('sms_code', code.value)
resp = resp.form.submit()
location = resp.location.split('?')[0]
resp = resp.follow()
assert Token.objects.count() == 1
assert 'Existing accounts are associated with this phone number.' in resp.text
# three existing accounts
assert len(resp.pyquery('form')) == 3
# the possibility to create a new one
assert resp.pyquery('p a').text() == 'create a new account'
resp = app.get(f'{location}?create')
resp.form.set('password1', 'Password0')
resp.form.set('password2', 'Password0')
resp.form.set('first_name', 'John')
resp.form.set('last_name', 'Doe')
resp = resp.form.submit().follow()
assert 'You have just created an account' in resp.text
assert AttributeValue.objects.filter(content='+33612345678').count() == 4
assert User.objects.filter(first_name='John', last_name='Doe').count() == 4
def test_phone_registration_number_already_existing_select(app, db, settings, phone_activated_authn):
settings.SMS_URL = 'https://foo.whatever.none/'
user_ids = []
# create duplicates
for i in range(3):
user = User.objects.create(
first_name='John',
last_name='Doe',
email=f'john-{i}@example.com',
username=f'john-{i}',
ou=get_default_ou(),
)
user.attributes.phone = '+33612345678'
user.save()
user_ids.append(user.id)
resp = app.get(reverse('registration_register'))
resp.form.set('phone_1', '612345678')
with HTTMock(sms_service_mock):
resp = resp.form.submit().follow()
code = SMSCode.objects.get()
resp.form.set('sms_code', code.value)
resp = resp.form.submit().follow()
assert Token.objects.count() == 1
assert 'Existing accounts are associated with this phone number.' in resp.text
# three existing accounts
assert len(resp.pyquery('form')) == 3
# the possibility to create a new one
assert resp.pyquery('p a').text() == 'create a new account'
resp.forms[1].submit().follow()
assert app.session['_auth_user_id'] == str(user_ids[1])
def test_phone_registration_number_already_existing_phone_is_unique(app, db, settings, phone_activated_authn):
settings.SMS_URL = 'https://foo.whatever.none/'
settings.A2_PHONE_IS_UNIQUE = True
settings.A2_REGISTRATION_PHONE_IS_UNIQUE = True
# create duplicate
user = User.objects.create(
first_name='John',
last_name='Doe',
email='john@example.com',
username='john',
ou=get_default_ou(),
)
user.attributes.phone = '+33612345678'
user.save()
resp = app.get(reverse('registration_register'))
resp.form.set('phone_1', '612345678')
with HTTMock(sms_service_mock):
resp = resp.form.submit().follow()
code = SMSCode.objects.get()
resp.form.set('sms_code', code.value)
resp = resp.form.submit().follow()
assert app.session['_auth_user_id'] == str(user.id)
# logged in user is redirected to their homepage
assert resp.location == '/'
def test_phone_registration_number_already_existing_registration_phone_is_unique(
app, db, settings, phone_activated_authn
):
settings.SMS_URL = 'https://foo.whatever.none/'
settings.A2_PHONE_IS_UNIQUE = False
settings.A2_REGISTRATION_PHONE_IS_UNIQUE = True
user_ids = []
# create duplicate
user = User.objects.create(
first_name='John',
last_name='Doe',
email='john@example.com',
username='john',
ou=get_default_ou(),
)
user.attributes.phone = '+33612345678'
user.save()
user_ids.append(user.id)
resp = app.get(reverse('registration_register'))
resp.form.set('phone_1', '612345678')
with HTTMock(sms_service_mock):
resp = resp.form.submit().follow()
code = SMSCode.objects.get()
resp.form.set('sms_code', code.value)
resp = resp.form.submit().follow()
assert Token.objects.count() == 1
assert app.session['_auth_user_id'] == str(user.id)
# logged in user is redirected to their homepage
assert resp.location == '/'
def test_phone_registration_number_already_existing_ou_phone_is_unique(
app, db, settings, phone_activated_authn
):
settings.SMS_URL = 'https://foo.whatever.none/'
settings.A2_PHONE_IS_UNIQUE = False
settings.A2_REGISTRATION_PHONE_IS_UNIQUE = False
ou = get_default_ou()
ou.phone_is_unique = True
ou.save()
user_ids = []
# create duplicate
user = User.objects.create(
first_name='John',
last_name='Doe',
email='john@example.com',
username='john',
ou=ou,
)
user.attributes.phone = '+33612345678'
user.save()
user_ids.append(user.id)
resp = app.get(reverse('registration_register'))
resp.form.set('phone_1', '612345678')
with HTTMock(sms_service_mock):
resp = resp.form.submit().follow()
code = SMSCode.objects.get()
resp.form.set('sms_code', code.value)
resp = resp.form.submit().follow()
assert Token.objects.count() == 1
assert app.session['_auth_user_id'] == str(user.id)
# logged in user is redirected to their homepage
assert resp.location == '/'
def test_phone_registration(app, db, settings, phone_activated_authn):
settings.SMS_URL = 'https://foo.whatever.none/'
code_length = settings.SMS_CODE_LENGTH