diff --git a/app/main/views/code_not_received.py b/app/main/views/code_not_received.py index ac51b3be1..25e2f060a 100644 --- a/app/main/views/code_not_received.py +++ b/app/main/views/code_not_received.py @@ -1,4 +1,4 @@ -from flask import redirect, render_template, session, url_for +from flask import redirect, render_template, request, session, url_for from app import user_api_client from app.main import main @@ -19,16 +19,17 @@ def resend_email_verification(): @redirect_to_sign_in def check_and_resend_text_code(): user = User.from_email_address(session['user_details']['email']) + redirect_url = request.args.get('next') if user.state == 'active': # this is a verified user and therefore redirect to page to request resend without edit mobile - return render_template('views/verification-not-received.html') + return render_template('views/verification-not-received.html', redirect_url=redirect_url) form = TextNotReceivedForm(mobile_number=user.mobile_number) if form.validate_on_submit(): user.send_verify_code(to=form.mobile_number.data) user.update(mobile_number=form.mobile_number.data) - return redirect(url_for('.verify')) + return redirect(url_for('.verify', next=redirect_url)) return render_template('views/text-not-received.html', form=form) @@ -38,16 +39,18 @@ def check_and_resend_text_code(): def check_and_resend_verification_code(): user = User.from_email_address(session['user_details']['email']) user.send_verify_code() + redirect_url = request.args.get('next') if user.state == 'pending': - return redirect(url_for('main.verify')) + return redirect(url_for('main.verify', next=redirect_url)) else: - return redirect(url_for('main.two_factor')) + return redirect(url_for('main.two_factor', next=redirect_url)) @main.route('/email-not-received', methods=['GET']) @redirect_to_sign_in def email_not_received(): - return render_template('views/email-not-received.html') + redirect_url = request.args.get('next') + return render_template('views/email-not-received.html', redirect_url=redirect_url) @main.route('/send-new-email-token', methods=['GET']) diff --git a/app/main/views/forgot_password.py b/app/main/views/forgot_password.py index a16c8a78a..359ad40c9 100644 --- a/app/main/views/forgot_password.py +++ b/app/main/views/forgot_password.py @@ -1,4 +1,4 @@ -from flask import render_template +from flask import render_template, request from notifications_python_client.errors import HTTPError from app import user_api_client @@ -11,7 +11,7 @@ def forgot_password(): form = ForgotPasswordForm() if form.validate_on_submit(): try: - user_api_client.send_reset_password_url(form.email_address.data) + user_api_client.send_reset_password_url(form.email_address.data, next_string=request.args.get('next')) except HTTPError as e: if e.status_code == 404: return render_template('views/password-reset-sent.html') diff --git a/app/main/views/new_password.py b/app/main/views/new_password.py index 7b3c701f9..2f35d9ddf 100644 --- a/app/main/views/new_password.py +++ b/app/main/views/new_password.py @@ -5,6 +5,7 @@ from flask import ( flash, redirect, render_template, + request, session, url_for, ) @@ -46,6 +47,6 @@ def new_password(token): else: # send user a 2fa sms code user.send_verify_code() - return redirect(url_for('main.two_factor')) + return redirect(url_for('main.two_factor', next=request.args.get('next'))) else: return render_template('views/new-password.html', token=token, form=form, user=user) diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index ae15fa751..7ce9225b4 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -24,6 +24,8 @@ def sign_in(): return redirect(url_for('main.show_accounts_or_dashboard')) form = LoginForm() + password_reset_url = url_for('.forgot_password', next=request.args.get('next')) + redirect_url = request.args.get('next') if form.validate_on_submit(): @@ -32,7 +34,7 @@ def sign_in(): ) if user and user.state == 'pending': - return redirect(url_for('main.resend_email_verification')) + return redirect(url_for('main.resend_email_verification', next=redirect_url)) if user and session.get('invited_user'): invited_user = InvitedUser.from_session() @@ -44,24 +46,25 @@ def sign_in(): invited_user.accept_invite() if user and user.sign_in(): if user.sms_auth: - return redirect(url_for('.two_factor', next=request.args.get('next'))) + return redirect(url_for('.two_factor', next=redirect_url)) if user.email_auth: - return redirect(url_for('.two_factor_email_sent')) + return redirect(url_for('.two_factor_email_sent', next=redirect_url)) # Vague error message for login in case of user not known, locked, inactive or password not verified flash(Markup( ( - "The email address or password you entered is incorrect." - " Forgotten your password?" - ).format(password_reset=url_for('.forgot_password')) + f"The email address or password you entered is incorrect." + f" Forgotten your password?" + ) )) other_device = current_user.logged_in_elsewhere() return render_template( 'views/signin.html', form=form, - again=bool(request.args.get('next')), - other_device=other_device + again=bool(redirect_url), + other_device=other_device, + password_reset_url=password_reset_url ) diff --git a/app/main/views/two_factor.py b/app/main/views/two_factor.py index 1d64f1b1b..52f106dc4 100644 --- a/app/main/views/two_factor.py +++ b/app/main/views/two_factor.py @@ -24,7 +24,8 @@ def two_factor_email_sent(): title = 'Email resent' if request.args.get('email_resent') else 'Check your email' return render_template( 'views/two-factor-email.html', - title=title + title=title, + redirect_url=request.args.get('next') ) @@ -35,6 +36,7 @@ def two_factor_email_interstitial(token): @main.route('/email-auth/', methods=['POST']) def two_factor_email(token): + redirect_url = request.args.get('next') if current_user.is_authenticated: return redirect_when_logged_in(platform_admin=current_user.platform_admin) @@ -47,14 +49,14 @@ def two_factor_email(token): current_app.config['EMAIL_2FA_EXPIRY_SECONDS'] )) except SignatureExpired: - return render_template('views/email-link-invalid.html') + return render_template('views/email-link-invalid.html', redirect_url=redirect_url) user_id = token_data['user_id'] # checks if code was already used logged_in, msg = user_api_client.check_verify_code(user_id, token_data['secret_code'], "email") if not logged_in: - return render_template('views/email-link-invalid.html') + return render_template('views/email-link-invalid.html', redirect_url=redirect_url) return log_in_user(user_id) @@ -68,21 +70,23 @@ def two_factor(): return user_api_client.check_verify_code(user_id, code, "sms") form = TwoFactorForm(_check_code) + redirect_url = request.args.get('next') if form.validate_on_submit(): if is_less_than_days_ago(user.email_access_validated_at, 90): return log_in_user(user_id) else: - user_api_client.send_verify_code(user.id, 'email', None, request.args.get('next')) - return redirect(url_for('.revalidate_email_sent')) + user_api_client.send_verify_code(user.id, 'email', None, redirect_url) + return redirect(url_for('.revalidate_email_sent', next=redirect_url)) - return render_template('views/two-factor.html', form=form) + return render_template('views/two-factor.html', form=form, redirect_url=redirect_url) @main.route('/re-validate-email', methods=['GET']) def revalidate_email_sent(): title = 'Email resent' if request.args.get('email_resent') else 'Check your email' - return render_template('views/re-validate-email-sent.html', title=title) + redirect_url = request.args.get('next') + return render_template('views/re-validate-email-sent.html', title=title, redirect_url=redirect_url) # see http://flask.pocoo.org/snippets/62/ diff --git a/app/notify_client/user_api_client.py b/app/notify_client/user_api_client.py index c97b241be..bafd22284 100644 --- a/app/notify_client/user_api_client.py +++ b/app/notify_client/user_api_client.py @@ -165,9 +165,11 @@ class UserApiClient(NotifyAdminAPIClient): endpoint = '/user/{}/service/{}/permission'.format(user_id, service_id) self.post(endpoint, data=data) - def send_reset_password_url(self, email_address): + def send_reset_password_url(self, email_address, next_string=None): endpoint = '/user/reset-password' data = {'email': email_address} + if next_string: + data['next'] = next_string self.post(endpoint, data=data) def find_users_by_full_or_partial_email(self, email_address): diff --git a/app/templates/views/email-link-invalid.html b/app/templates/views/email-link-invalid.html index 613173d8a..8d45c46a0 100644 --- a/app/templates/views/email-link-invalid.html +++ b/app/templates/views/email-link-invalid.html @@ -11,7 +11,11 @@

The link has expired

-

Sign in again to get a new link.

+

+ + Sign in again + to get a new link. +

diff --git a/app/templates/views/email-not-received.html b/app/templates/views/email-not-received.html index f7eb1dd96..a6ddc8881 100644 --- a/app/templates/views/email-not-received.html +++ b/app/templates/views/email-not-received.html @@ -16,7 +16,7 @@ {{ govukButton({ "element": "a", "text": "Resend email link", - "href": url_for('main.resend_email_link') + "href": url_for('main.resend_email_link', next=redirect_url) }) }}

diff --git a/app/templates/views/re-validate-email-sent.html b/app/templates/views/re-validate-email-sent.html index af74486d0..d14a4c621 100644 --- a/app/templates/views/re-validate-email-sent.html +++ b/app/templates/views/re-validate-email-sent.html @@ -14,7 +14,7 @@

We’ve sent you a link to sign in to Notify. The link will open in a new browser window, so you can close this one.

{{ page_footer( - secondary_link=url_for('main.email_not_received'), + secondary_link=url_for('main.email_not_received', next=redirect_url), secondary_link_text='Not received an email?' ) }} diff --git a/app/templates/views/signin.html b/app/templates/views/signin.html index 374e59f89..c4dd5c609 100644 --- a/app/templates/views/signin.html +++ b/app/templates/views/signin.html @@ -33,7 +33,7 @@ {% call form_wrapper(autocomplete=True) %} {{ form.email_address(param_extensions={"autocomplete": "email"}) }} {{ form.password(param_extensions={"autocomplete": "current-password"}) }} - {{ page_footer("Continue", secondary_link=url_for('.forgot_password'), secondary_link_text="Forgotten your password?") }} + {{ page_footer("Continue", secondary_link=password_reset_url, secondary_link_text="Forgotten your password?") }} {% endcall %} diff --git a/app/templates/views/two-factor-email.html b/app/templates/views/two-factor-email.html index 82e2c749e..81315863e 100644 --- a/app/templates/views/two-factor-email.html +++ b/app/templates/views/two-factor-email.html @@ -13,7 +13,7 @@

We’ve emailed you a link to sign in to Notify.

Clicking the link will open Notify in a new browser window, so you can close this one.

{{ page_footer( - secondary_link=url_for('main.email_not_received'), + secondary_link=url_for('main.email_not_received', next=redirect_url), secondary_link_text='Not received an email?' ) }} diff --git a/app/templates/views/two-factor.html b/app/templates/views/two-factor.html index 8a7cfa2f7..d0a252f14 100644 --- a/app/templates/views/two-factor.html +++ b/app/templates/views/two-factor.html @@ -22,7 +22,7 @@ }) }} {{ page_footer( "Continue", - secondary_link=url_for('main.check_and_resend_text_code'), + secondary_link=url_for('main.check_and_resend_text_code', next=redirect_url), secondary_link_text='Not received a text message?' ) }} {% endcall %} diff --git a/app/templates/views/verification-not-received.html b/app/templates/views/verification-not-received.html index 5b15d35dc..085b9c210 100644 --- a/app/templates/views/verification-not-received.html +++ b/app/templates/views/verification-not-received.html @@ -16,7 +16,7 @@ {{ govukButton({ "element": "a", "text": "Resend security code", - "href": url_for('main.check_and_resend_verification_code') + "href": url_for('main.check_and_resend_verification_code', next=redirect_url) }) }}

diff --git a/tests/app/main/views/test_code_not_received.py b/tests/app/main/views/test_code_not_received.py index 23712125e..11405790f 100644 --- a/tests/app/main/views/test_code_not_received.py +++ b/tests/app/main/views/test_code_not_received.py @@ -2,6 +2,8 @@ import pytest from bs4 import BeautifulSoup from flask import url_for +from tests.conftest import SERVICE_ONE_ID + def test_should_render_email_verification_resend_show_email_address_and_resend_verify_email( client, @@ -27,23 +29,32 @@ def test_should_render_email_verification_resend_show_email_address_and_resend_v mock_send_verify_email.assert_called_with(api_user_active['id'], api_user_active['email_address']) +@pytest.mark.parametrize('redirect_url', [ + None, + f'/services/{SERVICE_ONE_ID}/templates', +]) def test_should_render_correct_resend_template_for_active_user( client, api_user_active, mock_get_user_by_email, mock_send_verify_code, + redirect_url ): with client.session_transaction() as session: session['user_details'] = { 'id': api_user_active['id'], 'email': api_user_active['email_address']} - response = client.get(url_for('main.check_and_resend_text_code')) + response = client.get(url_for('main.check_and_resend_text_code', next=redirect_url)) assert response.status_code == 200 page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') assert page.h1.string == 'Resend security code' # there shouldn't be a form for updating mobile number assert page.find('form') is None + assert page.find('a', class_="govuk-button")['href'] == url_for( + 'main.check_and_resend_verification_code', + next=redirect_url + ) def test_should_render_correct_resend_template_for_pending_user( @@ -71,6 +82,10 @@ def test_should_render_correct_resend_template_for_pending_user( assert page.find('form').input['value'] == api_user_pending['mobile_number'] +@pytest.mark.parametrize('redirect_url', [ + None, + f'/services/{SERVICE_ONE_ID}/templates', +]) @pytest.mark.parametrize('phone_number_to_register_with', [ '+447700900460', '+1800-555-555', @@ -82,6 +97,7 @@ def test_should_resend_verify_code_and_update_mobile_for_pending_user( mock_update_user_attribute, mock_send_verify_code, phone_number_to_register_with, + redirect_url ): mocker.patch('app.user_api_client.get_user_by_email', return_value=api_user_pending) @@ -89,10 +105,10 @@ def test_should_resend_verify_code_and_update_mobile_for_pending_user( session['user_details'] = { 'id': api_user_pending['id'], 'email': api_user_pending['email_address']} - response = client.post(url_for('main.check_and_resend_text_code'), + response = client.post(url_for('main.check_and_resend_text_code', next=redirect_url), data={'mobile_number': phone_number_to_register_with}) assert response.status_code == 302 - assert response.location == url_for('main.verify', _external=True) + assert response.location == url_for('main.verify', _external=True, next=redirect_url) mock_update_user_attribute.assert_called_once_with( api_user_pending['id'], @@ -105,27 +121,37 @@ def test_should_resend_verify_code_and_update_mobile_for_pending_user( ) +@pytest.mark.parametrize('redirect_url', [ + None, + f'/services/{SERVICE_ONE_ID}/templates', +]) def test_check_and_redirect_to_two_factor_if_user_active( client, api_user_active, mock_get_user_by_email, mock_send_verify_code, + redirect_url ): with client.session_transaction() as session: session['user_details'] = { 'id': api_user_active['id'], 'email': api_user_active['email_address']} - response = client.get(url_for('main.check_and_resend_verification_code')) + response = client.get(url_for('main.check_and_resend_verification_code', next=redirect_url)) assert response.status_code == 302 - assert response.location == url_for('main.two_factor', _external=True) + assert response.location == url_for('main.two_factor', _external=True, next=redirect_url) +@pytest.mark.parametrize('redirect_url', [ + None, + f'/services/{SERVICE_ONE_ID}/templates', +]) def test_check_and_redirect_to_verify_if_user_pending( client, mocker, api_user_pending, mock_get_user_pending, mock_send_verify_code, + redirect_url ): mocker.patch('app.user_api_client.get_user_by_email', return_value=api_user_pending) @@ -134,9 +160,9 @@ def test_check_and_redirect_to_verify_if_user_pending( session['user_details'] = { 'id': api_user_pending['id'], 'email': api_user_pending['email_address']} - response = client.get(url_for('main.check_and_resend_verification_code')) + response = client.get(url_for('main.check_and_resend_verification_code', next=redirect_url)) assert response.status_code == 302 - assert response.location == url_for('main.verify', _external=True) + assert response.location == url_for('main.verify', _external=True, next=redirect_url) @pytest.mark.parametrize('endpoint', [ @@ -152,3 +178,28 @@ def test_redirect_to_sign_in_if_not_logged_in( assert response.location == url_for('main.sign_in', _external=True) assert response.status_code == 302 + + +@pytest.mark.parametrize('redirect_url', [ + None, + f'/services/{SERVICE_ONE_ID}/templates', +]) +def test_should_render_correct_email_not_received_template_for_active_user( + client, + api_user_active, + mock_get_user_by_email, + mock_send_verify_code, + redirect_url +): + with client.session_transaction() as session: + session['user_details'] = { + 'id': api_user_active['id'], + 'email': api_user_active['email_address']} + response = client.get(url_for('main.email_not_received', next=redirect_url)) + assert response.status_code == 200 + + page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') + assert page.h1.string == 'Resend email link' + # there shouldn't be a form for updating mobile number + assert page.find('form') is None + assert page.find('a', class_="govuk-button")['href'] == url_for('main.resend_email_link', next=redirect_url) diff --git a/tests/app/main/views/test_forgot_password.py b/tests/app/main/views/test_forgot_password.py index c75f83210..f3a106da6 100644 --- a/tests/app/main/views/test_forgot_password.py +++ b/tests/app/main/views/test_forgot_password.py @@ -4,6 +4,7 @@ from notifications_python_client.errors import HTTPError import app from tests import user_json +from tests.conftest import SERVICE_ONE_ID def test_should_render_forgot_password(client): @@ -31,7 +32,23 @@ def test_should_redirect_to_password_reset_sent_for_valid_email( assert response.status_code == 200 assert 'Click the link in the email to reset your password.' \ in response.get_data(as_text=True) - app.user_api_client.send_reset_password_url.assert_called_once_with(sample_user['email_address']) + app.user_api_client.send_reset_password_url.assert_called_once_with(sample_user['email_address'], next_string=None) + + +def test_forgot_password_sends_next_link_with_reset_password_email_request( + client, + fake_uuid, + mocker, +): + sample_user = user_json(email_address='test@user.gov.uk') + mocker.patch('app.user_api_client.send_reset_password_url', return_value=None) + response = client.post( + url_for('.forgot_password') + f"?next=/services/{SERVICE_ONE_ID}/templates", + data={'email_address': sample_user['email_address']}) + assert response.status_code == 200 + app.user_api_client.send_reset_password_url.assert_called_once_with( + sample_user['email_address'], next_string=f'/services/{SERVICE_ONE_ID}/templates' + ) def test_should_redirect_to_password_reset_sent_for_missing_email( @@ -48,4 +65,6 @@ def test_should_redirect_to_password_reset_sent_for_missing_email( assert response.status_code == 200 assert 'Click the link in the email to reset your password.' \ in response.get_data(as_text=True) - app.user_api_client.send_reset_password_url.assert_called_once_with(api_user_active['email_address']) + app.user_api_client.send_reset_password_url.assert_called_once_with( + api_user_active['email_address'], next_string=None + ) diff --git a/tests/app/main/views/test_new_password.py b/tests/app/main/views/test_new_password.py index 80359bcd7..867c25a6b 100644 --- a/tests/app/main/views/test_new_password.py +++ b/tests/app/main/views/test_new_password.py @@ -1,11 +1,12 @@ import json from datetime import datetime +import pytest from flask import url_for from itsdangerous import SignatureExpired from notifications_utils.url_safe_token import generate_token -from tests.conftest import url_for_endpoint_with_token +from tests.conftest import SERVICE_ONE_ID, url_for_endpoint_with_token def test_should_render_new_password_template( @@ -36,21 +37,26 @@ def test_should_return_404_when_email_address_does_not_exist( assert response.status_code == 404 +@pytest.mark.parametrize('redirect_url', [ + None, + f'/services/{SERVICE_ONE_ID}/templates', +]) def test_should_redirect_to_two_factor_when_password_reset_is_successful( app_, client, mock_get_user_by_email_request_password_reset, mock_login, mock_send_verify_code, - mock_reset_failed_login_count + mock_reset_failed_login_count, + redirect_url ): user = mock_get_user_by_email_request_password_reset.return_value data = json.dumps({'email': user['email_address'], 'created_at': str(datetime.utcnow())}) token = generate_token(data, app_.config['SECRET_KEY'], app_.config['DANGEROUS_SALT']) - response = client.post(url_for_endpoint_with_token('.new_password', token=token), + response = client.post(url_for_endpoint_with_token('.new_password', token=token, next=redirect_url), data={'new_password': 'a-new_password'}) assert response.status_code == 302 - assert response.location == url_for('.two_factor', _external=True) + assert response.location == url_for('.two_factor', _external=True, next=redirect_url) mock_get_user_by_email_request_password_reset.assert_called_once_with(user['email_address']) diff --git a/tests/app/main/views/test_sign_in.py b/tests/app/main/views/test_sign_in.py index 671305998..29a7d4d05 100644 --- a/tests/app/main/views/test_sign_in.py +++ b/tests/app/main/views/test_sign_in.py @@ -5,7 +5,7 @@ from bs4 import BeautifulSoup from flask import url_for from app.models.user import User -from tests.conftest import normalize_spaces +from tests.conftest import SERVICE_ONE_ID, normalize_spaces def test_render_sign_in_template_for_new_user( @@ -27,6 +27,20 @@ def test_render_sign_in_template_for_new_user( assert 'Sign in again' not in normalize_spaces(page.text) +def test_render_sign_in_template_with_next_link_for_password_reset( + client_request +): + client_request.logout() + page = client_request.get( + 'main.sign_in', + _optional_args=f"?next=/services/{SERVICE_ONE_ID}/templates", + _test_page_title=False + ) + forgot_password_link = page.find('a', class_="govuk-link govuk-link--no-visited-state page-footer-secondary-link") + assert forgot_password_link.text == 'Forgotten your password?' + assert forgot_password_link['href'] == url_for('main.forgot_password', next=f'/services/{SERVICE_ONE_ID}/templates') + + def test_sign_in_explains_session_timeout(client): response = client.get(url_for('main.sign_in', next='/foo')) assert response.status_code == 200 @@ -92,6 +106,10 @@ def test_logged_in_user_redirects_to_account( ) +@pytest.mark.parametrize('redirect_url', [ + None, + f'/services/{SERVICE_ONE_ID}/templates', +]) @pytest.mark.parametrize('email_address, password', [ ('valid@example.gov.uk', 'val1dPassw0rd!'), (' valid@example.gov.uk ', ' val1dPassw0rd! '), @@ -105,34 +123,40 @@ def test_process_sms_auth_sign_in_return_2fa_template( mock_verify_password, email_address, password, + redirect_url ): response = client.post( - url_for('main.sign_in'), data={ + url_for('main.sign_in', next=redirect_url), data={ 'email_address': email_address, 'password': password}) assert response.status_code == 302 - assert response.location == url_for('.two_factor', _external=True) + assert response.location == url_for('.two_factor', next=redirect_url, _external=True) mock_verify_password.assert_called_with(api_user_active['id'], password) mock_get_user_by_email.assert_called_with('valid@example.gov.uk') +@pytest.mark.parametrize('redirect_url', [ + None, + f'/services/{SERVICE_ONE_ID}/templates', +]) def test_process_email_auth_sign_in_return_2fa_template( client, api_user_active_email_auth, mock_send_verify_code, mock_verify_password, - mocker + mocker, + redirect_url ): mocker.patch('app.user_api_client.get_user', return_value=api_user_active_email_auth) mocker.patch('app.user_api_client.get_user_by_email', return_value=api_user_active_email_auth) response = client.post( - url_for('main.sign_in'), data={ + url_for('main.sign_in', next=redirect_url), data={ 'email_address': 'valid@example.gov.uk', 'password': 'val1dPassw0rd!'}) assert response.status_code == 302 - assert response.location == url_for('.two_factor_email_sent', _external=True) - mock_send_verify_code.assert_called_with(api_user_active_email_auth['id'], 'email', None, None) + assert response.location == url_for('.two_factor_email_sent', _external=True, next=redirect_url) + mock_send_verify_code.assert_called_with(api_user_active_email_auth['id'], 'email', None, redirect_url) mock_verify_password.assert_called_with(api_user_active_email_auth['id'], 'val1dPassw0rd!') @@ -175,16 +199,21 @@ def test_should_return_redirect_when_user_is_pending( assert response.status_code == 200 +@pytest.mark.parametrize('redirect_url', [ + None, + f'/services/{SERVICE_ONE_ID}/templates', +]) def test_should_attempt_redirect_when_user_is_pending( client, mock_get_user_by_email_pending, mock_verify_password, + redirect_url ): response = client.post( - url_for('main.sign_in'), data={ + url_for('main.sign_in', next=redirect_url), data={ 'email_address': 'pending_user@example.gov.uk', 'password': 'val1dPassw0rd!'}) - assert response.location == url_for('main.resend_email_verification', _external=True) + assert response.location == url_for('main.resend_email_verification', _external=True, next=redirect_url) assert response.status_code == 302 diff --git a/tests/app/main/views/test_two_factor.py b/tests/app/main/views/test_two_factor.py index 2c2c9dd2e..64204a870 100644 --- a/tests/app/main/views/test_two_factor.py +++ b/tests/app/main/views/test_two_factor.py @@ -11,11 +11,41 @@ from tests.conftest import ( ) +@pytest.mark.parametrize('request_url', ['two_factor_email_sent', 'revalidate_email_sent']) +@pytest.mark.parametrize('redirect_url', [None, f'/services/{SERVICE_ONE_ID}/templates']) +@pytest.mark.parametrize('email_resent, page_title', [ + (None, 'Check your email'), + (True, 'Email resent') +]) +def test_two_factor_email_sent_page( + client, + email_resent, + page_title, + redirect_url, + request_url +): + response = client.get(url_for(f'main.{request_url}', next=redirect_url, email_resent=email_resent)) + assert response.status_code == 200 + + page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') + assert page.h1.string == page_title + # there shouldn't be a form for updating mobile number + assert page.find('form') is None + resend_email_link = page.find('a', class_="govuk-link govuk-link--no-visited-state page-footer-secondary-link") + assert resend_email_link.text == 'Not received an email?' + assert resend_email_link['href'] == url_for('main.email_not_received', next=redirect_url) + + +@pytest.mark.parametrize('redirect_url', [ + None, + f'/services/{SERVICE_ONE_ID}/templates', +]) def test_should_render_two_factor_page( client, api_user_active, mock_get_user_by_email, - mocker + mocker, + redirect_url ): # TODO this lives here until we work out how to # reassign the session after it is lost mid register process @@ -24,7 +54,7 @@ def test_should_render_two_factor_page( 'id': api_user_active['id'], 'email': api_user_active['email_address']} mocker.patch('app.user_api_client.get_user', return_value=api_user_active) - response = client.get(url_for('main.two_factor')) + response = client.get(url_for('main.two_factor', next=redirect_url)) assert response.status_code == 200 page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') assert page.select_one('main p').text.strip() == ( @@ -36,6 +66,10 @@ def test_should_render_two_factor_page( assert page.select_one('input')['type'] == 'tel' assert page.select_one('input')['pattern'] == '[0-9]*' + assert page.select_one( + 'a:contains("Not received a text message?")' + )['href'] == url_for('main.check_and_resend_text_code', next=redirect_url) + @freeze_time('2020-01-27T12:00:00') def test_should_login_user_and_should_redirect_to_next_url( @@ -78,11 +112,15 @@ def test_should_send_email_and_redirect_to_info_page_if_user_needs_to_revalidate session['user_details'] = { 'id': api_user_active['id'], 'email': api_user_active['email_address']} - response = client.post(url_for('main.two_factor', next='/services/{}'.format(SERVICE_ONE_ID)), + response = client.post(url_for('main.two_factor', next=f'/services/{SERVICE_ONE_ID}'), data={'sms_code': '12345'}) assert response.status_code == 302 - assert response.location == url_for('main.revalidate_email_sent', _external=True) + assert response.location == url_for( + 'main.revalidate_email_sent', + _external=True, + next=f'/services/{SERVICE_ONE_ID}' + ) mock_send_verify_code.assert_called_with(api_user_active['id'], 'email', None, mocker.ANY) @@ -313,17 +351,22 @@ def test_valid_two_factor_email_link_logs_in_user( assert response.location == url_for('main.show_accounts_or_dashboard', _external=True) +@pytest.mark.parametrize('redirect_url', [ + None, + f'/services/{SERVICE_ONE_ID}/templates', +]) def test_two_factor_email_link_has_expired( app_, valid_token, client, mock_send_verify_code, - fake_uuid + fake_uuid, + redirect_url ): with set_config(app_, 'EMAIL_2FA_EXPIRY_SECONDS', -1): response = client.post( - url_for_endpoint_with_token('main.two_factor_email', token=valid_token), + url_for_endpoint_with_token('main.two_factor_email', token=valid_token, next=redirect_url), follow_redirects=True, ) @@ -331,6 +374,8 @@ def test_two_factor_email_link_has_expired( page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') assert page.h1.text.strip() == 'The link has expired' + assert page.select_one('a:contains("Sign in again")')['href'] == url_for('main.sign_in', next=redirect_url) + assert mock_send_verify_code.called is False @@ -346,20 +391,26 @@ def test_two_factor_email_link_is_invalid( assert normalize_spaces( page.select_one('.banner-dangerous').text ) == "There’s something wrong with the link you’ve used." + assert response.status_code == 404 +@pytest.mark.parametrize('redirect_url', [ + None, + f'/services/{SERVICE_ONE_ID}/templates', +]) def test_two_factor_email_link_is_already_used( client, valid_token, mocker, - mock_send_verify_code + mock_send_verify_code, + redirect_url ): mocker.patch('app.user_api_client.check_verify_code', return_value=(False, 'Code has expired')) response = client.post( - url_for_endpoint_with_token('main.two_factor_email', token=valid_token), + url_for_endpoint_with_token('main.two_factor_email', token=valid_token, next=redirect_url), follow_redirects=True ) @@ -367,6 +418,8 @@ def test_two_factor_email_link_is_already_used( assert response.status_code == 200 assert page.h1.text.strip() == 'The link has expired' + assert page.select_one('a:contains("Sign in again")')['href'] == url_for('main.sign_in', next=redirect_url) + assert mock_send_verify_code.called is False diff --git a/tests/conftest.py b/tests/conftest.py index 51691e067..56cd6fb33 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3655,9 +3655,9 @@ def mock_create_event(mocker): return mocker.patch('app.events_api_client.create_event', side_effect=_add_event) -def url_for_endpoint_with_token(endpoint, token): +def url_for_endpoint_with_token(endpoint, token, next=None): token = token.replace('%2E', '.') - return url_for(endpoint, token=token) + return url_for(endpoint, token=token, next=next) @pytest.fixture