Merge pull request #50 from alphagov/forgot-password

Forgot password and Reset password
This commit is contained in:
Adam Shimali
2016-01-11 12:34:50 +00:00
25 changed files with 411 additions and 135 deletions

View File

@@ -20,6 +20,7 @@
@import '../govuk_elements/public/sass/elements/details'; @import '../govuk_elements/public/sass/elements/details';
@import '../govuk_elements/public/sass/elements/elements-typography'; @import '../govuk_elements/public/sass/elements/elements-typography';
@import '../govuk_elements/public/sass/elements/forms'; @import '../govuk_elements/public/sass/elements/forms';
@import '../govuk_elements/public/sass/elements/forms/form-validation';
@import '../govuk_elements/public/sass/elements/forms/form-block-labels'; @import '../govuk_elements/public/sass/elements/forms/form-block-labels';
@import '../govuk_elements/public/sass/elements/icons'; @import '../govuk_elements/public/sass/elements/icons';
@import '../govuk_elements/public/sass/elements/layout'; @import '../govuk_elements/public/sass/elements/layout';

View File

@@ -2,8 +2,7 @@ from flask import Blueprint
main = Blueprint('main', __name__) main = Blueprint('main', __name__)
from app.main.views import ( from app.main.views import (
index, sign_in, sign_out, register, two_factor, verify, sms, add_service, index, sign_in, sign_out, register, two_factor, verify, sms, add_service,
code_not_received, jobs, dashboard, templates, service_settings code_not_received, jobs, dashboard, templates, service_settings, forgot_password, new_password
) )

View File

@@ -1,3 +1,7 @@
from datetime import datetime
from sqlalchemy.orm import load_only
from app import db, login_manager from app import db, login_manager
from app.models import User from app.models import User
from app.main.encryption import hashpw from app.main.encryption import hashpw
@@ -53,3 +57,18 @@ def update_mobile_number(id, mobile_number):
user.mobile_number = mobile_number user.mobile_number = mobile_number
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
def update_password(user, password):
user.password = hashpw(password)
user.password_changed_at = datetime.now()
user.state = 'active'
db.session.add(user)
db.session.commit()
def request_password_reset(email):
user = get_user_by_email(email)
user.state = 'request_password_reset'
db.session.add(user)
db.session.commit()

View File

@@ -1,5 +0,0 @@
class AdminApiClientException(Exception):
def __init__(self, message):
self.value = message

View File

@@ -1,67 +1,79 @@
from datetime import datetime
from flask_wtf import Form from flask_wtf import Form
from wtforms import StringField, PasswordField, ValidationError from wtforms import StringField, PasswordField, ValidationError
from wtforms.validators import DataRequired, Email, Length, Regexp from wtforms.validators import DataRequired, Email, Length, Regexp
from app.main.dao import verify_codes_dao
from app.main.encryption import check_hash
from app.main.validators import Blacklist, ValidateUserCodes from app.main.validators import Blacklist, ValidateUserCodes
def email_address():
gov_uk_email \
= "(^[^@^\\s]+@[^@^\\.^\\s]+(\\.[^@^\\.^\\s]*)*.gov.uk)"
return StringField('Email address', validators=[
Length(min=5, max=255),
DataRequired(message='Email cannot be empty'),
Email(message='Enter a valid email address'),
Regexp(regex=gov_uk_email, message='Enter a gov.uk email address')])
def mobile_number():
mobile_number_regex = "^\\+44[\\d]{10}$"
return StringField('Mobile phone number',
validators=[DataRequired(message='Mobile number can not be empty'),
Regexp(regex=mobile_number_regex, message='Enter a +44 mobile number')])
def password():
return PasswordField('Create a password',
validators=[DataRequired(message='Password can not be empty'),
Length(10, 255, message='Password must be at least 10 characters'),
Blacklist(message='That password is blacklisted, too common')])
def sms_code():
verify_code = '^\d{5}$'
return StringField('Text message confirmation code',
validators=[DataRequired(message='Text message confirmation code can not be empty'),
Regexp(regex=verify_code,
message='Text message confirmation code must be 5 digits'),
ValidateUserCodes(code_type='sms')])
def email_code():
verify_code = '^\d{5}$'
return StringField("Email confirmation code",
validators=[DataRequired(message='Email confirmation code can not be empty'),
Regexp(regex=verify_code, message='Email confirmation code must be 5 digits'),
ValidateUserCodes(code_type='email')])
class LoginForm(Form): class LoginForm(Form):
email_address = StringField('Email address', validators=[ email_address = StringField('Email address', validators=[
Length(min=5, max=255), Length(min=5, max=255),
DataRequired(message='Email cannot be empty'), DataRequired(message='Email cannot be empty'),
Email(message='Please enter a valid email address') Email(message='Enter a valid email address')
]) ])
password = PasswordField('Password', validators=[ password = PasswordField('Password', validators=[
DataRequired(message='Please enter your password') DataRequired(message='Enter your password')
]) ])
gov_uk_email = "(^[^@^\\s]+@[^@^\\.^\\s]+(\\.[^@^\\.^\\s]*)*.gov.uk)"
mobile_number = "^\\+44[\\d]{10}$"
verify_code = '^\d{5}$'
class RegisterUserForm(Form): class RegisterUserForm(Form):
def __init__(self, existing_email_addresses, *args, **kwargs):
def __init__(self, existing_email_addresses, existing_mobile_numbers, *args, **kwargs):
self.existing_emails = existing_email_addresses self.existing_emails = existing_email_addresses
self.existing_mobiles = existing_mobile_numbers
super(RegisterUserForm, self).__init__(*args, **kwargs) super(RegisterUserForm, self).__init__(*args, **kwargs)
name = StringField('Full name', name = StringField('Full name',
validators=[DataRequired(message='Name can not be empty')]) validators=[DataRequired(message='Name can not be empty')])
email_address = StringField('Email address', validators=[ email_address = email_address()
Length(min=5, max=255), mobile_number = mobile_number()
DataRequired(message='Email cannot be empty'), password = password()
Email(message='Please enter a valid email address'),
Regexp(regex=gov_uk_email, message='Please enter a gov.uk email address')
])
mobile_number = StringField('Mobile phone number',
validators=[DataRequired(message='Please enter your mobile number'),
Regexp(regex=mobile_number, message='Please enter a +44 mobile number')])
password = PasswordField('Create a password',
validators=[DataRequired(message='Please enter your password'),
Length(10, 255, message='Password must be at least 10 characters'),
Blacklist(message='That password is blacklisted, too common')])
def validate_email_address(self, field): def validate_email_address(self, field):
# Validate email address is unique. # Validate email address is unique.
if field.data in self.existing_emails: if self.existing_emails(field.data):
raise ValidationError('Email address already exists') raise ValidationError('Email address already exists')
def validate_mobile_number(self, field):
# Validate mobile number is unique
# Code to re-added later
# if field.data in self.existing_mobiles:
# raise ValidationError('Mobile number already exists')
pass
class TwoFactorForm(Form): class TwoFactorForm(Form):
def __init__(self, user_codes, *args, **kwargs): def __init__(self, user_codes, *args, **kwargs):
''' '''
Keyword arguments: Keyword arguments:
@@ -71,13 +83,10 @@ class TwoFactorForm(Form):
self.user_codes = user_codes self.user_codes = user_codes
super(TwoFactorForm, self).__init__(*args, **kwargs) super(TwoFactorForm, self).__init__(*args, **kwargs)
sms_code = StringField('sms code', validators=[DataRequired(message='Enter verification code'), sms_code = sms_code()
Regexp(regex=verify_code, message='Code must be 5 digits'),
ValidateUserCodes(code_type='sms')])
class VerifyForm(Form): class VerifyForm(Form):
def __init__(self, user_codes, *args, **kwargs): def __init__(self, user_codes, *args, **kwargs):
''' '''
Keyword arguments: Keyword arguments:
@@ -87,29 +96,16 @@ class VerifyForm(Form):
self.user_codes = user_codes self.user_codes = user_codes
super(VerifyForm, self).__init__(*args, **kwargs) super(VerifyForm, self).__init__(*args, **kwargs)
sms_code = StringField("Text message confirmation code", sms_code = sms_code()
validators=[DataRequired(message='SMS code can not be empty'), email_code = email_code()
Regexp(regex=verify_code, message='Code must be 5 digits'),
ValidateUserCodes(code_type='sms')])
email_code = StringField("Email confirmation code",
validators=[DataRequired(message='Email code can not be empty'),
Regexp(regex=verify_code, message='Code must be 5 digits'),
ValidateUserCodes(code_type='email')])
class EmailNotReceivedForm(Form): class EmailNotReceivedForm(Form):
email_address = StringField('Email address', validators=[ email_address = email_address()
Length(min=5, max=255),
DataRequired(message='Email cannot be empty'),
Email(message='Please enter a valid email address'),
Regexp(regex=gov_uk_email, message='Please enter a gov.uk email address')
])
class TextNotReceivedForm(Form): class TextNotReceivedForm(Form):
mobile_number = StringField('Mobile phone number', validators=[ mobile_number = mobile_number()
DataRequired(message='Please enter your mobile number'),
Regexp(regex=mobile_number, message='Please enter a +44 mobile number')])
class AddServiceForm(Form): class AddServiceForm(Form):
@@ -118,8 +114,16 @@ class AddServiceForm(Form):
super(AddServiceForm, self).__init__(*args, **kwargs) super(AddServiceForm, self).__init__(*args, **kwargs)
service_name = StringField(validators=[ service_name = StringField(validators=[
DataRequired(message='Please enter your service name')]) DataRequired(message='Service name can not be empty')])
def validate_service_name(self, a): def validate_service_name(self, a):
if self.service_name.data in self.service_names: if self.service_name.data in self.service_names:
raise ValidationError('Service name already exists') raise ValidationError('Service name already exists')
class ForgotPasswordForm(Form):
email_address = email_address()
class NewPasswordForm(Form):
new_password = password()

View File

@@ -1,6 +1,8 @@
from random import randint from random import randint
from flask import url_for, current_app
from app import admin_api_client from app import admin_api_client
from app.main.exceptions import AdminApiClientException
from app.main.dao import verify_codes_dao from app.main.dao import verify_codes_dao
@@ -10,24 +12,43 @@ def create_verify_code():
def send_sms_code(user_id, mobile_number): def send_sms_code(user_id, mobile_number):
sms_code = create_verify_code() sms_code = create_verify_code()
try: verify_codes_dao.add_code(user_id=user_id, code=sms_code, code_type='sms')
verify_codes_dao.add_code(user_id=user_id, code=sms_code, code_type='sms') admin_api_client.send_sms(mobile_number=mobile_number, message=sms_code, token=admin_api_client.auth_token)
admin_api_client.send_sms(mobile_number=mobile_number, message=sms_code, token=admin_api_client.auth_token)
except:
raise AdminApiClientException('Exception when sending sms.')
return sms_code return sms_code
def send_email_code(user_id, email): def send_email_code(user_id, email):
email_code = create_verify_code() email_code = create_verify_code()
try: verify_codes_dao.add_code(user_id=user_id, code=email_code, code_type='email')
verify_codes_dao.add_code(user_id=user_id, code=email_code, code_type='email') admin_api_client.send_email(email_address=email,
admin_api_client.send_email(email_address=email, from_str='notify@digital.cabinet-office.gov.uk',
from_str='notify@digital.cabinet-office.gov.uk', message=email_code,
message=email_code, subject='Verification code',
subject='Verification code', token=admin_api_client.auth_token)
token=admin_api_client.auth_token)
except:
raise AdminApiClientException('Exception when sending email.')
return email_code return email_code
def send_change_password_email(email):
link_to_change_password = url_for('.new_password', token=generate_token(email), _external=True)
admin_api_client.send_email(email_address=email,
from_str='notify@digital.cabinet-office.gov.uk',
message=link_to_change_password,
subject='Reset password for GOV.UK Notify',
token=admin_api_client.auth_token)
def generate_token(email):
from itsdangerous import TimestampSigner
signer = TimestampSigner(current_app.config['SECRET_KEY'])
return signer.sign(email).decode('utf8')
def check_token(token):
from itsdangerous import TimestampSigner, SignatureExpired
signer = TimestampSigner(current_app.config['SECRET_KEY'])
try:
email = signer.unsign(token, max_age=current_app.config['TOKEN_MAX_AGE_SECONDS'])
return email
except SignatureExpired as e:
current_app.logger.info('token expired %s' % e)

View File

@@ -0,0 +1,19 @@
from flask import render_template, flash, current_app
from app.main import main
from app.main.dao import users_dao
from app.main.forms import ForgotPasswordForm
from app.main.views import send_change_password_email
@main.route('/forgot-password', methods=['GET', 'POST'])
def forgot_password():
form = ForgotPasswordForm()
if form.validate_on_submit():
if users_dao.get_user_by_email(form.email_address.data):
users_dao.request_password_reset(form.email_address.data)
send_change_password_email(form.email_address.data)
return render_template('views/password-reset-sent.html')
else:
current_app.logger.info('The email address used does not exist.')
else:
return render_template('views/forgot-password.html', form=form)

View File

@@ -1,6 +1,4 @@
from flask import render_template from flask import render_template
from flask_login import login_required
from app.main import main from app.main import main
@@ -34,16 +32,6 @@ def checkemail():
return render_template('views/check-email.html') return render_template('views/check-email.html')
@main.route("/forgot-password")
def forgotpassword():
return render_template('views/forgot-password.html')
@main.route("/new-password")
def newpassword():
return render_template('views/new-password.html')
@main.route("/user-profile") @main.route("/user-profile")
def userprofile(): def userprofile():
return render_template('views/user-profile.html') return render_template('views/user-profile.html')
@@ -59,6 +47,11 @@ def apikeys():
return render_template('views/api-keys.html') return render_template('views/api-keys.html')
@main.route("/verification-not-received") @main.route("/manage-templates")
def verificationnotreceived(): def managetemplates():
return render_template('views/verification-not-received.html') return render_template('views/manage-templates.html')
@main.route("/edit-template")
def edittemplate():
return render_template('views/edit-template.html')

View File

@@ -0,0 +1,28 @@
from flask import (render_template, url_for, redirect, flash)
from app.main import main
from app.main.dao import users_dao
from app.main.forms import NewPasswordForm
from app.main.views import send_sms_code, check_token
@main.route('/new-password/<path:token>', methods=['GET', 'POST'])
def new_password(token):
email_address = check_token(token)
if not email_address:
flash('The link in the email we sent you has expired. Enter your email address to resend.')
return redirect(url_for('.forgot_password'))
user = users_dao.get_user_by_email(email_address=email_address.decode('utf-8'))
if user and user.state != 'request_password_reset':
flash('The link in the email we sent you has already been used.')
return redirect(url_for('.index'))
form = NewPasswordForm()
if form.validate_on_submit():
users_dao.update_password(user, form.new_password.data)
send_sms_code(user.id, user.mobile_number)
return redirect(url_for('main.two_factor'))
else:
return render_template('views/new-password.html', token=token, form=form, user=user)

View File

@@ -1,12 +1,9 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from flask import render_template, redirect, jsonify, session from flask import render_template, redirect, session
from sqlalchemy.exc import SQLAlchemyError
from app.main import main from app.main import main
from app.main.dao import users_dao from app.main.dao import users_dao
from app.main.encryption import hashpw
from app.main.exceptions import AdminApiClientException
from app.main.forms import RegisterUserForm from app.main.forms import RegisterUserForm
from app.main.views import send_sms_code, send_email_code from app.main.views import send_sms_code, send_email_code
from app.models import User from app.models import User
@@ -15,16 +12,8 @@ from app.models import User
# TODO how do we handle duplicate unverifed email addresses? # TODO how do we handle duplicate unverifed email addresses?
# malicious or otherwise. # malicious or otherwise.
@main.route('/register', methods=['GET', 'POST']) @main.route('/register', methods=['GET', 'POST'])
def process_register(): def register():
try: form = RegisterUserForm(users_dao.get_user_by_email)
existing_emails, existing_mobiles = zip(
*[(x.email_address, x.mobile_number) for x in
users_dao.get_all_users()])
except ValueError:
# Value error is raised if the db is empty.
existing_emails, existing_mobiles = [], []
form = RegisterUserForm(existing_emails, existing_mobiles)
if form.validate_on_submit(): if form.validate_on_submit():
user = User(name=form.name.data, user = User(name=form.name.data,

View File

@@ -9,4 +9,4 @@ from app.main import main
@login_required @login_required
def sign_out(): def sign_out():
logout_user() logout_user()
return redirect(url_for('main.sign_in')) return redirect(url_for('main.index'))

View File

@@ -43,6 +43,7 @@
</div> </div>
{% endblock %} {% endblock %}
{% set global_header_text = "GOV.UK Notify" %} {% set global_header_text = "GOV.UK Notify" %}
@@ -53,9 +54,20 @@
{% endif %} {% endif %}
{% block content %} {% block content %}
<main id="content" role="main" class="page-container"> <main id="content" role="main" class="page-container">
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="error-summary">
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endwith %}
{% block fullwidth_content %}{% endblock %} {% block fullwidth_content %}{% endblock %}
</main> </main>
{% endblock %} {% endblock %}
{% block body_end %} {% block body_end %}

View File

@@ -2,7 +2,7 @@
<dt>{{ field.label }} <dt>{{ field.label }}
<dd>{{ field(**kwargs)|safe }} <dd>{{ field(**kwargs)|safe }}
{% if field.errors %} {% if field.errors %}
<ul class=error-summary> <ul class=error>
{% for error in field.errors %} {% for error in field.errors %}
<li>{{ error }}</li> <li>{{ error }}</li>
{% endfor %} {% endfor %}

View File

@@ -12,15 +12,13 @@ GOV.UK Notify
<p>If you have forgotten your password, we can send you an email to create a new password.</p> <p>If you have forgotten your password, we can send you an email to create a new password.</p>
<form autocomplete="off" action="" method="post">
{{ form.hidden_tag() }}
{{ render_field(form.email_address, class='form-control-2-3') }}
<p> <p>
<label class="form-label" for="email">Email address</label> <button class="button" role="button">Send email</button>
<input class="form-control" id="email" type="text" value="">
</p>
<p>
<a class="button" href="sign-in" role="button">Send email</a>
</p> </p>
</form>
</div> </div>
</div> </div>

View File

@@ -8,19 +8,26 @@ GOV.UK Notify
<div class="grid-row"> <div class="grid-row">
<div class="column-two-thirds"> <div class="column-two-thirds">
{% if user %}
<h1 class="heading-xlarge">Create a new password</h1> <h1 class="heading-xlarge">Create a new password</h1>
<p> You can now create a new password for your account.</p> <p> You can now create a new password for your account.</p>
<p> <form action="" autocomplete="off" method="post">
<label class="form-label" for="password">Create a password</label> {{ form.hidden_tag() }}
<input class="form-control-1-4" id="password" type="password"> <br> <p>
{{ render_field(form.new_password, class="form-control-1-4", type="password") }}
<span class="font-xsmall">Your password must have at least 10 characters</span> <span class="font-xsmall">Your password must have at least 10 characters</span>
</p> </p>
<p> <p>
<a class="button" href="two-factor" role="button">Continue</a> <button class="button" role="button">Continue</button>
</p> </p>
</form>
{% else %}
Message about email address does not exist. Some one needs to figure out the words here.
{% endif %}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,18 @@
{% extends "admin_template.html" %}
{% block page_title %}
GOV.UK Notify |
{% endblock %}
{% block fullwidth_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-xlarge">GOV.UK Notify</h1>
<p>You have been sent an email containing a link to reset your password.</p>
</div>
</div>
{% endblock %}

View File

@@ -18,7 +18,7 @@ Sign in
{{ render_field(form.email_address, class='form-control-2-3') }} {{ render_field(form.email_address, class='form-control-2-3') }}
{{ render_field(form.password, class='form-control-2-3') }} {{ render_field(form.password, class='form-control-2-3') }}
<p> <p>
<span class="font-xsmall"><a href="">Forgotten password?</a></span> <span class="font-xsmall"><a href="{{url_for('main.forgot_password')}}">Forgotten password?</a></span>
</p> </p>
{{ page_footer("Continue") }} {{ page_footer("Continue") }}
</form> </form>

View File

@@ -29,6 +29,7 @@ class Config(object):
SECRET_KEY = 'secret-key' SECRET_KEY = 'secret-key'
HTTP_PROTOCOL = 'http' HTTP_PROTOCOL = 'http'
DANGEROUS_SALT = 'itsdangeroussalt' DANGEROUS_SALT = 'itsdangeroussalt'
TOKEN_MAX_AGE_SECONDS = 120000
class Development(Config): class Development(Config):

View File

@@ -1,8 +1,7 @@
from datetime import datetime from datetime import datetime
import pytest import pytest
import sqlalchemy import sqlalchemy
from app.main.encryption import check_hash
from app.models import User from app.models import User
from app.main.dao import users_dao from app.main.dao import users_dao
@@ -161,3 +160,61 @@ def test_should_update_email_address(notifications_admin, notifications_admin_db
users_dao.update_email_address(user.id, 'new_email@testit.gov.uk') users_dao.update_email_address(user.id, 'new_email@testit.gov.uk')
updated = users_dao.get_user_by_id(user.id) updated = users_dao.get_user_by_id(user.id)
assert updated.email_address == 'new_email@testit.gov.uk' assert updated.email_address == 'new_email@testit.gov.uk'
def test_should_update_password(notifications_admin, notifications_admin_db, notify_db_session):
user = User(name='Update Email',
password='somepassword',
email_address='test@it.gov.uk',
mobile_number='+441234123412',
created_at=datetime.now(),
role_id=1,
state='active')
start = datetime.now()
users_dao.insert_user(user)
saved = users_dao.get_user_by_id(user.id)
assert check_hash('somepassword', saved.password)
assert saved.password_changed_at is None
users_dao.update_password(saved, 'newpassword')
updated = users_dao.get_user_by_id(user.id)
assert check_hash('newpassword', updated.password)
assert updated.password_changed_at < datetime.now()
assert updated.password_changed_at > start
def test_should_return_list_of_all_email_addresses(notifications_admin, notifications_admin_db, notify_db_session):
first = User(name='First Person',
password='somepassword',
email_address='first@it.gov.uk',
mobile_number='+441234123412',
created_at=datetime.now(),
role_id=1,
state='active')
second = User(name='Second Person',
password='somepassword',
email_address='second@it.gov.uk',
mobile_number='+441234123412',
created_at=datetime.now(),
role_id=1,
state='active')
users_dao.insert_user(first)
users_dao.insert_user(second)
email_addresses = users_dao.get_all_users()
expected = [first.email_address, second.email_address]
assert expected == [x.email_address for x in email_addresses]
def test_should_update_state_to_request_password_reset(notifications_admin, notifications_admin_db, notify_db_session):
user = User(name='Requesting Password Resest',
password='somepassword',
email_address='request@new_password.gov.uk',
mobile_number='+441234123412',
created_at=datetime.now(),
role_id=1,
state='active')
users_dao.insert_user(user)
users_dao.request_password_reset(user.email_address)
saved = users_dao.get_user_by_email(user.email_address)
assert saved.state == 'request_password_reset'

View File

@@ -1,10 +1,9 @@
from pytest import fail from app.main.dao import users_dao
from app.main.forms import RegisterUserForm from app.main.forms import RegisterUserForm
def test_should_raise_validation_error_for_password(notifications_admin): def test_should_raise_validation_error_for_password(notifications_admin):
form = RegisterUserForm([], []) form = RegisterUserForm(users_dao.get_user_by_email)
form.name.data = 'test' form.name.data = 'test'
form.email_address.data = 'teset@example.gov.uk' form.email_address.data = 'teset@example.gov.uk'
form.mobile_number.data = '+441231231231' form.mobile_number.data = '+441231231231'

View File

@@ -39,4 +39,4 @@ def test_should_return_form_errors_when_service_name_is_empty(notifications_admi
client.post('/two-factor', data={'sms_code': '12345'}) client.post('/two-factor', data={'sms_code': '12345'})
response = client.post('/add-service', data={}) response = client.post('/add-service', data={})
assert response.status_code == 200 assert response.status_code == 200
assert 'Please enter your service name' in response.get_data(as_text=True) assert 'Service name can not be empty' in response.get_data(as_text=True)

View File

@@ -0,0 +1,41 @@
from flask import url_for
from app.main.dao import users_dao
from app.main.views import generate_token
from tests.app.main import create_test_user
def test_should_render_forgot_password(notifications_admin, notifications_admin_db, notify_db_session):
response = notifications_admin.test_client().get('/forgot-password')
assert response.status_code == 200
assert 'If you have forgotten your password, we can send you an email to create a new password.' \
in response.get_data(as_text=True)
def test_should_redirect_to_password_reset_sent_and_state_updated(notifications_admin,
notifications_admin_db,
mocker,
notify_db_session):
mocker.patch("app.admin_api_client.send_email")
user = create_test_user('active')
response = notifications_admin.test_client().post('/forgot-password',
data={'email_address': user.email_address})
assert response.status_code == 200
assert 'You have been sent an email containing a link to reset your password.' in response.get_data(
as_text=True)
assert users_dao.get_user_by_id(user.id).state == 'request_password_reset'
def test_should_redirect_to_forgot_password_with_flash_message_when_token_is_expired(notifications_admin,
notifications_admin_db,
notify_db_session):
with notifications_admin.test_request_context():
with notifications_admin.test_client() as client:
notifications_admin.config['TOKEN_MAX_AGE_SECONDS'] = -1000
user = create_test_user('active')
token = generate_token(user.email_address)
response = client.post('/new-password/{}'.format(token),
data={'new_password': 'a-new_password'})
assert response.status_code == 302
assert response.location == url_for('.forgot_password', _external=True)
notifications_admin.config['TOKEN_MAX_AGE_SECONDS'] = 86400

View File

@@ -0,0 +1,75 @@
from flask import url_for
from app.main.dao import users_dao
from app.main.encryption import check_hash
from app.main.views import generate_token
from tests.app.main import create_test_user
def test_should_render_new_password_template(notifications_admin, notifications_admin_db, notify_db_session):
with notifications_admin.test_request_context():
with notifications_admin.test_client() as client:
user = create_test_user('request_password_reset')
token = generate_token(user.email_address)
response = client.get(url_for('.new_password', token=token))
assert response.status_code == 200
assert ' You can now create a new password for your account.' in response.get_data(as_text=True)
def test_should_render_new_password_template_with_message_of_bad_token(notifications_admin, notifications_admin_db,
notify_db_session):
with notifications_admin.test_request_context():
with notifications_admin.test_client() as client:
create_test_user('request_password_reset')
token = generate_token('no_user@d.gov.uk')
response = client.get(url_for('.new_password', token=token))
assert response.status_code == 200
assert 'Message about email address does not exist. Some one needs to figure out the words here.' in \
response.get_data(as_text=True)
def test_should_redirect_to_two_factor_when_password_reset_is_successful(notifications_admin,
notifications_admin_db,
notify_db_session,
mocker):
_set_up_mocker(mocker)
with notifications_admin.test_request_context():
with notifications_admin.test_client() as client:
user = create_test_user('request_password_reset')
token = generate_token(user.email_address)
response = client.post(url_for('.new_password', token=token), data={'new_password': 'a-new_password'})
assert response.status_code == 302
assert response.location == url_for('.two_factor', _external=True)
saved_user = users_dao.get_user_by_id(user.id)
assert check_hash('a-new_password', saved_user.password)
assert saved_user.state == 'active'
def test_should_redirect_to_forgot_password_with_flash_message_when_token_is_expired(notifications_admin,
notifications_admin_db,
notify_db_session):
with notifications_admin.test_request_context():
with notifications_admin.test_client() as client:
notifications_admin.config['TOKEN_MAX_AGE_SECONDS'] = -1000
user = create_test_user('request_password_reset')
token = generate_token(user.email_address)
response = client.post(url_for('.new_password', token=token), data={'new_password': 'a-new_password'})
assert response.status_code == 302
assert response.location == url_for('.forgot_password', _external=True)
notifications_admin.config['TOKEN_MAX_AGE_SECONDS'] = 86400
def test_should_redirect_to_forgot_password_when_user_is_active_should_be_request_password_reset(notifications_admin,
notifications_admin_db,
notify_db_session):
with notifications_admin.test_request_context():
with notifications_admin.test_client() as client:
user = create_test_user('active')
token = generate_token(user.email_address)
response = client.post(url_for('.new_password', token=token), data={'new_password': 'a-new_password'})
assert response.status_code == 302
assert response.location == url_for('.index', _external=True)
def _set_up_mocker(mocker):
mocker.patch("app.admin_api_client.send_sms")

View File

@@ -31,7 +31,7 @@ def test_process_register_returns_400_when_mobile_number_is_invalid(notification
'password': 'validPassword!'}) 'password': 'validPassword!'})
assert response.status_code == 200 assert response.status_code == 200
assert 'Please enter a +44 mobile number' in response.get_data(as_text=True) assert 'Enter a +44 mobile number' in response.get_data(as_text=True)
def test_should_return_400_when_email_is_not_gov_uk(notifications_admin, def test_should_return_400_when_email_is_not_gov_uk(notifications_admin,
@@ -46,7 +46,7 @@ def test_should_return_400_when_email_is_not_gov_uk(notifications_admin,
'password': 'validPassword!'}) 'password': 'validPassword!'})
assert response.status_code == 200 assert response.status_code == 200
assert 'Please enter a gov.uk email address' in response.get_data(as_text=True) assert 'Enter a gov.uk email address' in response.get_data(as_text=True)
def test_should_add_verify_codes_on_session(notifications_admin, notifications_admin_db, mocker, notify_db_session): def test_should_add_verify_codes_on_session(notifications_admin, notifications_admin_db, mocker, notify_db_session):

View File

@@ -40,4 +40,4 @@ def test_sign_out_user(notifications_admin,
response = client.get(url_for('main.sign_out')) response = client.get(url_for('main.sign_out'))
assert response.status_code == 302 assert response.status_code == 302
assert response.location == url_for( assert response.location == url_for(
'main.sign_in', _external=True) 'main.index', _external=True)