misc: make minimum password strength configurable in ous (#68745)
This commit is contained in:
parent
bc20207e2b
commit
c393adcdd5
|
@ -0,0 +1,31 @@
|
|||
# Generated by Django 2.2.26 on 2022-09-20 14:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('a2_rbac', '0029_use_unique_constraints'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='organizationalunit',
|
||||
name='min_password_strength',
|
||||
field=models.IntegerField(
|
||||
blank=True,
|
||||
choices=[
|
||||
(None, 'System default'),
|
||||
(0, 'Very Weak'),
|
||||
(1, 'Weak'),
|
||||
(2, 'Fair'),
|
||||
(3, 'Good'),
|
||||
(4, 'Strong'),
|
||||
],
|
||||
default=None,
|
||||
null=True,
|
||||
verbose_name='Minimum password strength',
|
||||
),
|
||||
),
|
||||
]
|
|
@ -106,6 +106,15 @@ class OrganizationalUnit(AbstractBase):
|
|||
MANUAL_PASSWORD_POLICY: PolicyValue(False, False, True, False),
|
||||
}
|
||||
|
||||
MIN_PASSWORD_STRENGTH_CHOICES = (
|
||||
(None, _("System default")),
|
||||
(0, _("Very Weak")),
|
||||
(1, _("Weak")),
|
||||
(2, _("Fair")),
|
||||
(3, _("Good")),
|
||||
(4, _("Strong")),
|
||||
)
|
||||
|
||||
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'))
|
||||
default = fields.UniqueBooleanField(verbose_name=_('Default organizational unit'))
|
||||
|
@ -128,6 +137,14 @@ class OrganizationalUnit(AbstractBase):
|
|||
verbose_name=_('User creation password policy'), choices=USER_ADD_PASSWD_POLICY_CHOICES, default=0
|
||||
)
|
||||
|
||||
min_password_strength = models.IntegerField(
|
||||
verbose_name=_('Minimum password strength'),
|
||||
choices=MIN_PASSWORD_STRENGTH_CHOICES,
|
||||
default=None,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
clean_unused_accounts_alert = models.PositiveIntegerField(
|
||||
verbose_name=_('Days after which the user receives an account deletion alert'),
|
||||
validators=[
|
||||
|
|
|
@ -35,7 +35,7 @@ from authentic2.forms.widgets import (
|
|||
ProfileImageInput,
|
||||
)
|
||||
from authentic2.manager.utils import label_from_role
|
||||
from authentic2.passwords import validate_password
|
||||
from authentic2.passwords import get_password_checker, get_password_strength
|
||||
from authentic2.validators import email_validator
|
||||
|
||||
|
||||
|
@ -45,7 +45,38 @@ class PasswordField(CharField):
|
|||
|
||||
class NewPasswordField(CharField):
|
||||
widget = NewPasswordInput
|
||||
default_validators = [validate_password]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.min_strength = None
|
||||
|
||||
def _get_min_strength(self):
|
||||
return self._min_strength
|
||||
|
||||
def _set_min_strength(self, value):
|
||||
self._min_strength = value
|
||||
self.widget.min_strength = value
|
||||
|
||||
min_strength = property(_get_min_strength, _set_min_strength)
|
||||
|
||||
def validate(self, value):
|
||||
super().validate(value)
|
||||
if value == '':
|
||||
return
|
||||
|
||||
min_strength = self.min_strength
|
||||
if min_strength is not None:
|
||||
if get_password_strength(value).strength < min_strength:
|
||||
raise ValidationError(_('This password is not strong enough.'))
|
||||
|
||||
min_length = app_settings.A2_PASSWORD_POLICY_MIN_LENGTH
|
||||
if min_length > len(value):
|
||||
raise ValidationError(_('Password must be at least %s characters.') % min_length)
|
||||
else:
|
||||
password_checker = get_password_checker()
|
||||
errors = [not check.result for check in password_checker(value)]
|
||||
if any(errors):
|
||||
raise ValidationError(_('This password is not accepted.'))
|
||||
|
||||
|
||||
class CheckPasswordField(CharField):
|
||||
|
|
|
@ -26,6 +26,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
from authentic2.backends.ldap_backend import LDAPUser
|
||||
from authentic2.journal import journal
|
||||
from authentic2.passwords import get_min_password_strength
|
||||
|
||||
from .. import app_settings, hooks, models, validators
|
||||
from ..backends import get_user_queryset
|
||||
|
@ -176,10 +177,15 @@ class SetPasswordForm(NotifyOfPasswordChange, PasswordResetMixin, auth_forms.Set
|
|||
new_password1 = NewPasswordField(label=_("New password"))
|
||||
new_password2 = CheckPasswordField(label=_("New password confirmation"))
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
super().__init__(user, *args, **kwargs)
|
||||
self.fields['new_password1'].min_strength = get_min_password_strength(user)
|
||||
|
||||
def clean_new_password1(self):
|
||||
new_password1 = self.cleaned_data.get('new_password1')
|
||||
if new_password1 and self.user.check_password(new_password1):
|
||||
raise ValidationError(_('New password must differ from old password'))
|
||||
|
||||
return new_password1
|
||||
|
||||
|
||||
|
@ -192,11 +198,16 @@ class PasswordChangeForm(
|
|||
|
||||
old_password.widget.attrs.update({'autocomplete': 'current-password'})
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
super().__init__(user, *args, **kwargs)
|
||||
self.fields['new_password1'].min_strength = get_min_password_strength(user)
|
||||
|
||||
def clean_new_password1(self):
|
||||
new_password1 = self.cleaned_data.get('new_password1')
|
||||
old_password = self.cleaned_data.get('old_password')
|
||||
if new_password1 and new_password1 == old_password:
|
||||
raise ValidationError(_('New password must differ from old password'))
|
||||
|
||||
return new_password1
|
||||
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
from authentic2.a2_rbac.models import OrganizationalUnit
|
||||
from authentic2.forms.fields import CheckPasswordField, NewPasswordField
|
||||
from authentic2.passwords import get_min_password_strength
|
||||
|
||||
from .. import app_settings, models
|
||||
from . import profile as profile_forms
|
||||
|
@ -154,6 +155,10 @@ class RegistrationCompletionForm(RegistrationCompletionFormNoPassword):
|
|||
password1 = NewPasswordField(label=_('Password'))
|
||||
password2 = CheckPasswordField(label=_("Password (again)"))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['password1'].min_strength = get_min_password_strength(self.instance)
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Verifiy that the values entered into the two password fields
|
||||
|
|
|
@ -274,12 +274,7 @@ class PasswordInput(BasePasswordInput):
|
|||
|
||||
class NewPasswordInput(PasswordInput):
|
||||
template_name = 'authentic2/widgets/new_password.html'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
min_strength = app_settings.A2_PASSWORD_POLICY_MIN_STRENGTH
|
||||
if min_strength:
|
||||
self.attrs['data-min-strength'] = min_strength
|
||||
min_strength = None
|
||||
|
||||
def get_context(self, *args, **kwargs):
|
||||
context = super().get_context(*args, **kwargs)
|
||||
|
@ -292,6 +287,10 @@ class NewPasswordInput(PasswordInput):
|
|||
if attrs is None:
|
||||
attrs = {}
|
||||
attrs['autocomplete'] = 'new-password'
|
||||
|
||||
if self.min_strength is not None:
|
||||
attrs['data-min-strength'] = self.min_strength
|
||||
|
||||
output = super().render(name, value, attrs=attrs, renderer=renderer)
|
||||
if attrs:
|
||||
_id = attrs.get('id')
|
||||
|
|
|
@ -41,7 +41,7 @@ from authentic2.forms.fields import (
|
|||
from authentic2.forms.mixins import SlugMixin
|
||||
from authentic2.forms.profile import BaseUserForm
|
||||
from authentic2.models import APIClient, PasswordReset, Service
|
||||
from authentic2.passwords import generate_password
|
||||
from authentic2.passwords import generate_password, get_min_password_strength
|
||||
from authentic2.utils.misc import send_email_change_email, send_password_reset_mail, send_templated_mail
|
||||
from authentic2.validators import EmailValidator
|
||||
from django_rbac.backends import DjangoRBACBackend
|
||||
|
@ -254,6 +254,10 @@ class UserChangePasswordForm(CssClass, forms.ModelForm):
|
|||
password2 = CheckPasswordField(label=_("Confirmation"), required=False)
|
||||
send_mail = forms.BooleanField(initial=False, label=_('Send informations to user'), required=False)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.fields['password1'].min_strength = get_min_password_strength(self.instance)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ()
|
||||
|
@ -669,6 +673,7 @@ class OUEditForm(SlugMixin, CssClass, forms.ModelForm):
|
|||
'check_required_on_login_attributes',
|
||||
'user_can_reset_password',
|
||||
'user_add_password_policy',
|
||||
'min_password_strength',
|
||||
'clean_unused_accounts_alert',
|
||||
'clean_unused_accounts_deletion',
|
||||
'home_url',
|
||||
|
|
|
@ -19,7 +19,6 @@ import random
|
|||
import re
|
||||
import string
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.module_loading import import_string
|
||||
from django.utils.translation import ugettext as _
|
||||
from zxcvbn import zxcvbn
|
||||
|
@ -110,20 +109,10 @@ def get_password_checker(*args, **kwargs):
|
|||
return import_string(app_settings.A2_PASSWORD_POLICY_CLASS)(*args, **kwargs)
|
||||
|
||||
|
||||
def validate_password(password):
|
||||
min_strength = app_settings.A2_PASSWORD_POLICY_MIN_STRENGTH
|
||||
if min_strength is not None:
|
||||
if get_password_strength(password).strength < min_strength:
|
||||
raise ValidationError(_('This password is not strong enough.'))
|
||||
|
||||
min_length = app_settings.A2_PASSWORD_POLICY_MIN_LENGTH
|
||||
if min_length > len(password):
|
||||
raise ValidationError(_('Password must be at least %s characters.') % min_length)
|
||||
else:
|
||||
password_checker = get_password_checker()
|
||||
errors = [not check.result for check in password_checker(password)]
|
||||
if any(errors):
|
||||
raise ValidationError(_('This password is not accepted.'))
|
||||
def get_min_password_strength(user):
|
||||
if user.ou and user.ou.min_password_strength is not None:
|
||||
return user.ou.min_password_strength
|
||||
return app_settings.A2_PASSWORD_POLICY_MIN_STRENGTH
|
||||
|
||||
|
||||
class StrengthReport:
|
||||
|
|
|
@ -28,9 +28,6 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from . import app_settings
|
||||
|
||||
# keep those symbols here for retrocompatibility
|
||||
from .passwords import validate_password # pylint: disable=unused-import
|
||||
|
||||
|
||||
# copied from http://www.djangotips.com/real-email-validation
|
||||
class EmailValidator:
|
||||
|
|
|
@ -1760,7 +1760,7 @@ def test_validate_password_regex(app, settings):
|
|||
@pytest.mark.parametrize(
|
||||
'min_length, password,strength,label',
|
||||
[
|
||||
(0, '?', 0, 'Very Weak'),
|
||||
(0, '', 0, 'Very Weak'),
|
||||
(0, '?', 0, 'Very Weak'),
|
||||
(0, '?JR!', 1, 'Weak'),
|
||||
(0, '?JR!p4A', 2, 'Fair'),
|
||||
|
|
|
@ -19,6 +19,7 @@ import pytest
|
|||
from django.core.exceptions import ValidationError
|
||||
|
||||
from authentic2.attribute_kinds import PhoneNumberField
|
||||
from authentic2.forms.passwords import NewPasswordField
|
||||
|
||||
|
||||
def test_phonenumber_field():
|
||||
|
@ -32,3 +33,54 @@ def test_phonenumber_field():
|
|||
for value in ['01a01']:
|
||||
with pytest.raises(ValidationError):
|
||||
field.clean(value)
|
||||
|
||||
|
||||
def test_validate_password(settings):
|
||||
field = NewPasswordField()
|
||||
with pytest.raises(ValidationError):
|
||||
field.validate('aaaaaZZZZZZ')
|
||||
with pytest.raises(ValidationError):
|
||||
field.validate('00000aaaaaa')
|
||||
with pytest.raises(ValidationError):
|
||||
field.validate('00000ZZZZZZ')
|
||||
field.validate('000aaaaZZZZ')
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'password,min_strength',
|
||||
[
|
||||
('?', 0),
|
||||
('?JR!', 1),
|
||||
('?JR!p4A', 2),
|
||||
('?JR!p4A2i', 3),
|
||||
('?JR!p4A2i:#', 4),
|
||||
],
|
||||
)
|
||||
def test_validate_password_strength(settings, password, min_strength):
|
||||
settings.A2_PASSWORD_POLICY_MIN_LENGTH = len(password)
|
||||
field = NewPasswordField()
|
||||
|
||||
field.min_strength = min_strength
|
||||
field.validate(password)
|
||||
|
||||
settings.A2_PASSWORD_POLICY_MIN_LENGTH = len(password) + 1
|
||||
with pytest.raises(ValidationError):
|
||||
field.validate(password)
|
||||
|
||||
if min_strength < 4:
|
||||
settings.A2_PASSWORD_POLICY_MIN_LENGTH = len(password)
|
||||
field.min_strength = min_strength + 1
|
||||
with pytest.raises(ValidationError):
|
||||
field.validate(password)
|
||||
|
||||
|
||||
def test_digits_password_policy(settings):
|
||||
settings.A2_PASSWORD_POLICY_REGEX = '^[0-9]{8}$'
|
||||
settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG = 'pasbon'
|
||||
settings.A2_PASSWORD_POLICY_MIN_LENGTH = 0
|
||||
settings.A2_PASSWORD_POLICY_MIN_CLASSES = 0
|
||||
field = NewPasswordField()
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
field.validate('aaa')
|
||||
field.validate('12345678')
|
||||
|
|
|
@ -198,6 +198,24 @@ def test_manager_user_password_reset(app, superuser, simple_user):
|
|||
assert str(app.session['_auth_user_id']) == str(simple_user.pk)
|
||||
|
||||
|
||||
def test_manager_user_change_password_form(app, simple_user):
|
||||
from authentic2.manager.forms import UserChangePasswordForm
|
||||
|
||||
data = {
|
||||
'password1': 'Password0',
|
||||
'password2': 'Password0',
|
||||
}
|
||||
|
||||
form = UserChangePasswordForm(instance=simple_user, data=data)
|
||||
assert form.fields['password1'].widget.min_strength is None
|
||||
assert 'password1' not in form.errors
|
||||
|
||||
simple_user.ou.min_password_strength = 3
|
||||
form = UserChangePasswordForm(instance=simple_user, data=data)
|
||||
assert form.fields['password1'].widget.min_strength == 3
|
||||
assert form.errors['password1'] == ['This password is not strong enough.']
|
||||
|
||||
|
||||
def test_manager_user_detail_by_uuid(app, superuser, simple_user, simple_role):
|
||||
simple_user.roles.add(simple_role)
|
||||
url = reverse('a2-manager-user-by-uuid-detail', kwargs={'slug': simple_user.uuid})
|
||||
|
|
|
@ -23,7 +23,10 @@ from django.urls import reverse
|
|||
from django.utils.http import urlquote
|
||||
|
||||
from authentic2 import models
|
||||
from authentic2.a2_rbac.utils import get_default_ou
|
||||
from authentic2.apps.journal.models import Event
|
||||
from authentic2.forms.profile import modelform_factory
|
||||
from authentic2.forms.registration import RegistrationCompletionForm
|
||||
from authentic2.utils import misc as utils_misc
|
||||
from authentic2.validators import EmailValidator
|
||||
|
||||
|
@ -907,3 +910,44 @@ def test_registration_service_integration(app, service, settings):
|
|||
assert response.context['home_ou'] == service.ou
|
||||
assert response.context['home_service'] == service
|
||||
assert response.context['home_url'] == 'https://portail.example.net/page/'
|
||||
|
||||
|
||||
def test_registration_completion_form(db, simple_user):
|
||||
form_class = modelform_factory(get_user_model(), form=RegistrationCompletionForm)
|
||||
data = {
|
||||
'email': 'jonh.doe@yopmail.com',
|
||||
'password': 'blah',
|
||||
'password1': 'Password0',
|
||||
'password2': 'Password0',
|
||||
'date_joined': '2022-02-07',
|
||||
'ou': simple_user.ou.pk,
|
||||
}
|
||||
|
||||
form = form_class(instance=simple_user, data=data)
|
||||
assert form.fields['password1'].widget.min_strength is None
|
||||
assert 'password1' not in form.errors
|
||||
|
||||
simple_user.ou.min_password_strength = 3
|
||||
form = form_class(instance=simple_user, data=data)
|
||||
assert form.fields['password1'].widget.min_strength == 3
|
||||
assert form.errors['password1'] == ['This password is not strong enough.']
|
||||
|
||||
|
||||
def test_registration_completion(db, app, mailoutbox):
|
||||
default_ou = get_default_ou()
|
||||
default_ou.min_password_strength = 3
|
||||
default_ou.save()
|
||||
|
||||
resp = app.get(reverse('registration_register'))
|
||||
resp.form.set('email', 'testbot@entrouvert.com')
|
||||
resp = resp.form.submit().follow()
|
||||
link = get_link_from_mail(mailoutbox[0])
|
||||
resp = app.get(link)
|
||||
|
||||
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()
|
||||
|
||||
assert 'This password is not strong enough' in resp.text
|
||||
|
|
|
@ -21,44 +21,7 @@ from unittest import mock
|
|||
import pytest
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from authentic2.validators import EmailValidator, HexaColourValidator, validate_password
|
||||
|
||||
|
||||
def test_validate_password(settings):
|
||||
with pytest.raises(ValidationError):
|
||||
validate_password('aaaaaZZZZZZ')
|
||||
with pytest.raises(ValidationError):
|
||||
validate_password('00000aaaaaa')
|
||||
with pytest.raises(ValidationError):
|
||||
validate_password('00000ZZZZZZ')
|
||||
validate_password('000aaaaZZZZ')
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'password,min_strength',
|
||||
[
|
||||
('', 0),
|
||||
('?', 0),
|
||||
('?JR!', 1),
|
||||
('?JR!p4A', 2),
|
||||
('?JR!p4A2i', 3),
|
||||
('?JR!p4A2i:#', 4),
|
||||
],
|
||||
)
|
||||
def test_validate_password_strength(settings, password, min_strength):
|
||||
settings.A2_PASSWORD_POLICY_MIN_LENGTH = len(password)
|
||||
settings.A2_PASSWORD_POLICY_MIN_STRENGTH = min_strength
|
||||
validate_password(password)
|
||||
|
||||
settings.A2_PASSWORD_POLICY_MIN_LENGTH = len(password) + 1
|
||||
with pytest.raises(ValidationError):
|
||||
validate_password(password)
|
||||
|
||||
if min_strength < 4:
|
||||
settings.A2_PASSWORD_POLICY_MIN_LENGTH = len(password)
|
||||
settings.A2_PASSWORD_POLICY_MIN_STRENGTH = min_strength + 1
|
||||
with pytest.raises(ValidationError):
|
||||
validate_password(password)
|
||||
from authentic2.validators import EmailValidator, HexaColourValidator
|
||||
|
||||
|
||||
def test_validate_colour():
|
||||
|
@ -72,17 +35,6 @@ def test_validate_colour():
|
|||
validator('#ff00ff')
|
||||
|
||||
|
||||
def test_digits_password_policy(settings):
|
||||
settings.A2_PASSWORD_POLICY_REGEX = '^[0-9]{8}$'
|
||||
settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG = 'pasbon'
|
||||
settings.A2_PASSWORD_POLICY_MIN_LENGTH = 0
|
||||
settings.A2_PASSWORD_POLICY_MIN_CLASSES = 0
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
validate_password('aaa')
|
||||
validate_password('12345678')
|
||||
|
||||
|
||||
class TestEmailValidator:
|
||||
@pytest.mark.parametrize(
|
||||
'bad_email',
|
||||
|
|
|
@ -24,6 +24,7 @@ from django.urls import reverse
|
|||
from django.utils.html import escape
|
||||
|
||||
from authentic2.custom_user.models import DeletedUser, User
|
||||
from authentic2.forms.passwords import PasswordChangeForm, SetPasswordForm
|
||||
|
||||
from .utils import assert_event, get_link_from_mail, login, logout
|
||||
|
||||
|
@ -83,6 +84,38 @@ def test_password_change_error(
|
|||
assert 'boum!' in resp
|
||||
|
||||
|
||||
def test_password_change_form(simple_user):
|
||||
data = {
|
||||
'new_password1': 'Password0',
|
||||
'new_password2': 'Password0',
|
||||
}
|
||||
|
||||
form = PasswordChangeForm(user=simple_user, data=data)
|
||||
assert form.fields['new_password1'].widget.min_strength is None
|
||||
assert 'new_password1' not in form.errors
|
||||
|
||||
simple_user.ou.min_password_strength = 3
|
||||
form = PasswordChangeForm(user=simple_user, data=data)
|
||||
assert form.fields['new_password1'].widget.min_strength == 3
|
||||
assert form.errors['new_password1'] == ['This password is not strong enough.']
|
||||
|
||||
|
||||
def test_set_password_form(simple_user):
|
||||
data = {
|
||||
'new_password1': 'Password0',
|
||||
'new_password2': 'Password0',
|
||||
}
|
||||
|
||||
form = SetPasswordForm(user=simple_user, data=data)
|
||||
assert form.fields['new_password1'].widget.min_strength is None
|
||||
assert 'new_password1' not in form.errors
|
||||
|
||||
simple_user.ou.min_password_strength = 3
|
||||
form = SetPasswordForm(user=simple_user, data=data)
|
||||
assert form.fields['new_password1'].widget.min_strength == 3
|
||||
assert form.errors['new_password1'] == ['This password is not strong enough.']
|
||||
|
||||
|
||||
def test_well_known_password_change(app):
|
||||
resp = app.get('/.well-known/change-password')
|
||||
assert resp.location == '/accounts/password/change/'
|
||||
|
|
|
@ -52,7 +52,7 @@ def test_datalisttextinput_init_and_render():
|
|||
assert not data
|
||||
|
||||
|
||||
def test_new_password_input(settings):
|
||||
def test_new_password_input():
|
||||
widget = NewPasswordInput()
|
||||
html = widget.render('foo', 'bar')
|
||||
query = PyQuery(html)
|
||||
|
@ -60,8 +60,8 @@ def test_new_password_input(settings):
|
|||
textinput = query.find('input')
|
||||
assert textinput.attr('data-min-strength') is None
|
||||
|
||||
settings.A2_PASSWORD_POLICY_MIN_STRENGTH = 3
|
||||
widget = NewPasswordInput()
|
||||
widget.min_strength = 3
|
||||
html = widget.render('foo', 'bar')
|
||||
query = PyQuery(html)
|
||||
|
||||
|
|
Loading…
Reference in New Issue