From 6af616eb83ce650ff0fb5f10e3194be7d116bcbf Mon Sep 17 00:00:00 2001 From: Leo Hemsted Date: Fri, 3 Nov 2017 16:00:22 +0000 Subject: [PATCH] add name to personalisation and urlencode next param also add tests --- app/user/rest.py | 14 +++--- app/user/users_schema.py | 11 +++-- tests/app/conftest.py | 18 +++++++ tests/app/user/test_rest_verify.py | 76 +++++++++++++++++++++++++++++- 4 files changed, 108 insertions(+), 11 deletions(-) diff --git a/app/user/rest.py b/app/user/rest.py index 68ca8d943..ac220a1d1 100644 --- a/app/user/rest.py +++ b/app/user/rest.py @@ -1,6 +1,7 @@ import json import uuid from datetime import datetime +from urllib.parse import urlencode from flask import (jsonify, request, Blueprint, current_app) @@ -157,7 +158,7 @@ def send_user_sms_code(user_id): mobile = data.get('to') or user_to_send_to.mobile_number template = dao_get_template_by_id(current_app.config['SMS_CODE_TEMPLATE_ID']) - personalisation = {'verify_code': secret_code}, + personalisation = {'verify_code': secret_code} create_2fa_code(template, mobile, personalisation) return jsonify({}), 204 @@ -165,14 +166,15 @@ def send_user_sms_code(user_id): @user_blueprint.route('//email-code', methods=['POST']) def send_user_email_code(user_id): - user_to_send_to = validate_2fa_call(user_id, request.get_json(), post_send_user_email_code_schema) + data = request.get_json() + user_to_send_to = validate_2fa_call(user_id, data, post_send_user_email_code_schema) if not user_to_send_to: return jsonify({}), 204 - create_user_code(user_to_send_to, uuid.uuid4(), EMAIL_TYPE) + create_user_code(user_to_send_to, str(uuid.uuid4()), EMAIL_TYPE) template = dao_get_template_by_id(current_app.config['EMAIL_2FA_TEMPLATE_ID']) - personalisation = {'url': _create_2fa_url(user_to_send_to)}, + personalisation = {'name': user_to_send_to.name, 'url': _create_2fa_url(user_to_send_to, data.get('next'))} create_2fa_code(template, user_to_send_to.email_address, personalisation) @@ -384,10 +386,10 @@ def _create_confirmation_url(user, email_address): return url_with_token(data, url, current_app.config) -def _create_2fa_url(user, next_redir=None): +def _create_2fa_url(user, next_redir): data = json.dumps({'user_id': str(user.id), 'email': user.email_address}) url = '/email-auth/' ret = url_with_token(data, url, current_app.config) if next_redir: - ret += '?next={}'.format(next_redir) + ret += '?{}'.format(urlencode({'next': next_redir})) return ret diff --git a/app/user/users_schema.py b/app/user/users_schema.py index f8b1bdc1f..357ab9f4a 100644 --- a/app/user/users_schema.py +++ b/app/user/users_schema.py @@ -6,7 +6,8 @@ post_verify_code_schema = { 'code': {'type': 'string'}, 'code_type': {'type': 'string'}, }, - 'required': ['code', 'code_type'] + 'required': ['code', 'code_type'], + 'additionalProperties': False } @@ -15,11 +16,13 @@ post_send_user_email_code_schema = { 'description': 'POST schema for generating a 2fa email', 'type': 'object', 'properties': { - # doesn't need 'to' as we'll just grab user.email_address + # doesn't need 'to' as we'll just grab user.email_address. but lets keep it + # as allowed to keep admin code cleaner, but only as null to prevent confusion + 'to': {'type': 'null'}, 'next': {'type': ['string', 'null']}, }, 'required': [], - 'additionalProperties': [] + 'additionalProperties': False } @@ -31,5 +34,5 @@ post_send_user_sms_code_schema = { 'to': {'type': ['string', 'null']}, }, 'required': [], - 'additionalProperties': [] + 'additionalProperties': False } diff --git a/tests/app/conftest.py b/tests/app/conftest.py index 999e319c0..6e89c662d 100644 --- a/tests/app/conftest.py +++ b/tests/app/conftest.py @@ -864,6 +864,24 @@ def sms_code_template(notify_db, ) +@pytest.fixture(scope='function') +def email_2fa_code_template(notify_db, notify_db_session): + service, user = notify_service(notify_db, notify_db_session) + return create_custom_template( + service=service, + user=user, + template_config_name='EMAIL_2FA_TEMPLATE_ID', + content=( + 'Hi ((name)),' + '' + 'To sign in to GOV.​UK Notify please open this link:' + '((url))' + ), + subject='Sign in to GOV.UK Notify', + template_type='email' + ) + + @pytest.fixture(scope='function') def email_verification_template(notify_db, notify_db_session): diff --git a/tests/app/user/test_rest_verify.py b/tests/app/user/test_rest_verify.py index 674f4cb4b..55ca3c543 100644 --- a/tests/app/user/test_rest_verify.py +++ b/tests/app/user/test_rest_verify.py @@ -9,11 +9,13 @@ import pytest from flask import url_for, current_app from freezegun import freeze_time +from app.dao.users_dao import create_user_code from app.dao.services_dao import dao_update_service, dao_fetch_service_by_id from app.models import ( VerifyCode, User, - Notification + Notification, + EMAIL_TYPE ) from app import db import app.celery.tasks @@ -334,3 +336,75 @@ def test_reset_failed_login_count_returns_404_when_user_does_not_exist(client): data={}, headers=[('Content-Type', 'application/json'), create_authorization_header()]) assert resp.status_code == 404 + + +def test_send_user_email_code(admin_request, mocker, sample_user, email_2fa_code_template): + deliver_email = mocker.patch('app.celery.provider_tasks.deliver_email.apply_async') + + data = { + 'to': None + } + admin_request.post( + 'user.send_user_email_code', + user_id=sample_user.id, + _data=data, + _expected_status=204 + ) + noti = Notification.query.one() + assert noti.to == sample_user.email_address + assert str(noti.template_id) == current_app.config['EMAIL_2FA_TEMPLATE_ID'] + assert noti.personalisation['name'] == 'Test User' + deliver_email.assert_called_once_with( + [str(noti.id)], + queue='notify-internal-tasks' + ) + + +def test_send_user_email_code_with_urlencoded_next_param(admin_request, mocker, sample_user, email_2fa_code_template): + mocker.patch('app.celery.provider_tasks.deliver_email.apply_async') + + data = { + 'to': None, + 'next': '/services' + } + admin_request.post( + 'user.send_user_email_code', + user_id=sample_user.id, + _data=data, + _expected_status=204 + ) + noti = Notification.query.one() + code = VerifyCode.query.one() + assert noti.personalisation['url'].endswith('?next=%2Fservices') + + +def test_send_email_code_returns_404_for_bad_input_data(admin_request): + resp = admin_request.post( + 'user.send_user_email_code', + user_id=uuid.uuid4(), + _data={}, + _expected_status=404 + ) + assert resp['message'] == 'No result found' + + +@freeze_time('2016-01-01T12:00:00') +def test_user_verify_email_code(admin_request, sample_user): + magic_code = str(uuid.uuid4()) + verify_code = create_user_code(sample_user, magic_code, EMAIL_TYPE) + + data = { + 'code_type': 'email', + 'code': magic_code + } + + admin_request.post( + 'user.verify_user_code', + user_id=sample_user.id, + _data=data, + _expected_status=204 + ) + + assert verify_code.code_used + assert sample_user.logged_in_at == datetime.utcnow() + assert sample_user.current_session_id is not None