From b37eef974c7739b968012408300873456fae6ee6 Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Wed, 12 Oct 2016 13:06:39 +0100 Subject: [PATCH] Change email confirmation - New endpoint to send a user an email to verify the email address when they want to change it. --- app/user/rest.py | 50 +++++++-- config.py | 1 + tests/app/conftest.py | 207 +++++++++++++----------------------- tests/app/user/test_rest.py | 36 +++++++ 4 files changed, 154 insertions(+), 140 deletions(-) diff --git a/app/user/rest.py b/app/user/rest.py index 9af27bd99..d7714b6df 100644 --- a/app/user/rest.py +++ b/app/user/rest.py @@ -43,7 +43,6 @@ register_errors(user) def create_user(): user_to_create, errors = user_schema.load(request.get_json()) req_json = request.get_json() - # TODO password policy, what is valid password if not req_json.get('password', None): errors.update({'password': ['Missing data for required field.']}) raise InvalidRequest(errors, status_code=400) @@ -147,6 +146,35 @@ def send_user_sms_code(user_id): return jsonify({}), 204 +@user.route('//confirm-new-email', methods=['POST']) +def send_user_confirm_new_email(user_id): + user_to_send_to = get_model_users(user_id=user_id) + email, errors = email_data_request_schema.load(request.get_json()) + if errors: + raise InvalidRequest(message=errors, status_code=400) + + template = dao_get_template_by_id(current_app.config['CHANGE_EMAIL_CONFIRMATION_TEMPLATE_ID']) + message = { + 'template': str(template.id), + 'template_version': template.version, + 'to': user_to_send_to.email_address, + 'personalisation': { + 'name': user_to_send_to.name, + 'url': _create_confirmation_url(user=user_to_send_to, email_address=email), + 'feedback_url': current_app.config['ADMIN_BASE_URL'] + '/feedback' + } + } + + send_email.apply_async(( + current_app.config['NOTIFY_SERVICE_ID'], + str(uuid.uuid4()), + encryption.encrypt(message), + datetime.utcnow().strftime(DATETIME_FORMAT) + ), queue='notify') + + return jsonify({}), 204 + + @user.route('//email-verification', methods=['POST']) def send_user_email_verification(user_id): user_to_send_to = get_model_users(user_id=user_id) @@ -257,17 +285,25 @@ def send_user_reset_password(): return jsonify({}), 204 -def _create_reset_password_url(email): +def _create_url(data, base_url): from notifications_utils.url_safe_token import generate_token - data = json.dumps({'email': email, 'created_at': str(datetime.utcnow())}) token = generate_token(data, current_app.config['SECRET_KEY'], current_app.config['DANGEROUS_SALT']) + return base_url + token - return current_app.config['ADMIN_BASE_URL'] + '/new-password/' + token + +def _create_reset_password_url(email): + data = json.dumps({'email': email, 'created_at': str(datetime.utcnow())}) + base_url = current_app.config['ADMIN_BASE_URL'] + '/new-password/' + return _create_url(data=data, base_url=base_url) def _create_verification_url(user, secret_code): - from notifications_utils.url_safe_token import generate_token data = json.dumps({'user_id': str(user.id), 'email': user.email_address, 'secret_code': secret_code}) - token = generate_token(data, current_app.config['SECRET_KEY'], current_app.config['DANGEROUS_SALT']) + base_url = current_app.config['ADMIN_BASE_URL'] + '/verify-email/' + return _create_url(data=data, base_url=base_url) - return current_app.config['ADMIN_BASE_URL'] + '/verify-email/' + token + +def _create_confirmation_url(user, email_address): + data = json.dumps({'user_id': str(user.id), 'email': user.email_address}) + base_url = current_app.config['ADMIN_BASE_URL'] + '/confirm-new-email/' + return _create_url(data=data, base_url=base_url) diff --git a/config.py b/config.py index fa676c79c..da02d942c 100644 --- a/config.py +++ b/config.py @@ -64,6 +64,7 @@ class Config(object): EMAIL_VERIFY_CODE_TEMPLATE_ID = 'ece42649-22a8-4d06-b87f-d52d5d3f0a27' PASSWORD_RESET_TEMPLATE_ID = '474e9242-823b-4f99-813d-ed392e7f1201' ALREADY_REGISTERED_EMAIL_TEMPLATE_ID = '0880fbb1-a0c6-46f0-9a8e-36c986381ceb' + CHANGE_EMAIL_CONFIRMATION_TEMPLATE_ID = 'eb4d9930-87ab-4aef-9bce-786762687884' BROKER_URL = 'sqs://' BROKER_TRANSPORT_OPTIONS = { diff --git a/tests/app/conftest.py b/tests/app/conftest.py index 1dd226b7c..b64d0869f 100644 --- a/tests/app/conftest.py +++ b/tests/app/conftest.py @@ -712,144 +712,100 @@ def mock_firetext_client(mocker, statsd_client=None): @pytest.fixture(scope='function') def sms_code_template(notify_db, notify_db_session): - user = sample_user(notify_db, notify_db_session) - service = Service.query.get(current_app.config['NOTIFY_SERVICE_ID']) - if not service: - data = { - 'id': current_app.config['NOTIFY_SERVICE_ID'], - 'name': 'Notify Service', - 'message_limit': 1000, - 'active': True, - 'restricted': False, - 'email_from': 'notify.service', - 'created_by': user - } - service = Service(**data) - db.session.add(service) - - template = Template.query.get(current_app.config['SMS_CODE_TEMPLATE_ID']) - if not template: - data = { - 'id': current_app.config['SMS_CODE_TEMPLATE_ID'], - 'name': 'Sms code template', - 'template_type': 'sms', - 'content': '((verify_code))', - 'service': service, - 'created_by': user, - 'archived': False - } - template = Template(**data) - db.session.add(template) - return template + service, user = notify_service(notify_db, notify_db_session) + return create_notify_template(service=service, + user=user, + template_config_name='SMS_CODE_TEMPLATE_ID', + content='((verify_code))', + template_type='sms') @pytest.fixture(scope='function') def email_verification_template(notify_db, notify_db_session): - user = sample_user(notify_db, notify_db_session) - service = Service.query.get(current_app.config['NOTIFY_SERVICE_ID']) - if not service: - data = { - 'id': current_app.config['NOTIFY_SERVICE_ID'], - 'name': 'Notify Service', - 'message_limit': 1000, - 'active': True, - 'restricted': False, - 'email_from': 'notify.service', - 'created_by': user - } - service = Service(**data) - db.session.add(service) - - template = Template.query.get(current_app.config['EMAIL_VERIFY_CODE_TEMPLATE_ID']) - if not template: - data = { - 'id': current_app.config['EMAIL_VERIFY_CODE_TEMPLATE_ID'], - 'name': 'Email verification template', - 'template_type': 'email', - 'content': '((user_name)) use ((url)) to complete registration', - 'service': service, - 'created_by': user, - 'archived': False - } - template = Template(**data) - db.session.add(template) - return template + service, user = notify_service(notify_db, notify_db_session) + return create_notify_template(service=service, + user=user, + template_config_name='EMAIL_VERIFY_CODE_TEMPLATE_ID', + content='((user_name)) use ((url)) to complete registration', + template_type='email') @pytest.fixture(scope='function') def invitation_email_template(notify_db, notify_db_session): - user = sample_user(notify_db, notify_db_session) - service = Service.query.get(current_app.config['NOTIFY_SERVICE_ID']) - if not service: - data = { - 'id': current_app.config['NOTIFY_SERVICE_ID'], - 'name': 'Notify Service', - 'message_limit': 1000, - 'active': True, - 'restricted': False, - 'email_from': 'notify.service', - 'created_by': user - } - service = Service(**data) - db.session.add(service) - - template = Template.query.get(current_app.config['INVITATION_EMAIL_TEMPLATE_ID']) - if not template: - data = { - 'id': current_app.config['INVITATION_EMAIL_TEMPLATE_ID'], - 'name': 'Invitaion template', - 'template_type': 'email', - 'content': '((user_name)) is invited to Notify by ((service_name)) ((url)) to complete registration', - 'subject': 'Invitation to ((service_name))', - 'service': service, - 'created_by': user, - 'archived': False - } - template = Template(**data) - db.session.add(template) - return template + service, user = notify_service(notify_db, notify_db_session) + content = '((user_name)) is invited to Notify by ((service_name)) ((url)) to complete registration', + return create_notify_template(service=service, + user=user, + template_config_name='INVITATION_EMAIL_TEMPLATE_ID', + content=content, + subject='Invitation to ((service_name))', + template_type='email') @pytest.fixture(scope='function') def password_reset_email_template(notify_db, notify_db_session): - user = sample_user(notify_db, notify_db_session) - service = Service.query.get(current_app.config['NOTIFY_SERVICE_ID']) - if not service: - data = { - 'id': current_app.config['NOTIFY_SERVICE_ID'], - 'name': 'Notify Service', - 'message_limit': 1000, - 'active': True, - 'restricted': False, - 'email_from': 'notify.service', - 'created_by': user - } - service = Service(**data) - db.session.add(service) + service, user = notify_service(notify_db, notify_db_session) - template = Template.query.get(current_app.config['PASSWORD_RESET_TEMPLATE_ID']) - if not template: - data = { - 'id': current_app.config['PASSWORD_RESET_TEMPLATE_ID'], - 'name': 'Password reset template', - 'template_type': 'email', - 'content': '((user_name)) you can reset password by clicking ((url))', - 'subject': 'Reset your password', - 'service': service, - 'created_by': user, - 'archived': False - } - template = Template(**data) - db.session.add(template) - return template + return create_notify_template(service=service, + user=user, + template_config_name='PASSWORD_RESET_TEMPLATE_ID', + content='((user_name)) you can reset password by clicking ((url))', + subject='Reset your password', + template_type='email') @pytest.fixture(scope='function') def already_registered_template(notify_db, notify_db_session): + service, user = notify_service(notify_db, notify_db_session) + + content = """Sign in here: ((signin_url)) If you’ve forgotten your password, + you can reset it here: ((forgot_password_url)) feedback:((feedback_url))""" + return create_notify_template(service=service, user=user, + template_config_name='ALREADY_REGISTERED_EMAIL_TEMPLATE_ID', + content=content, + template_type='email') + + +@pytest.fixture(scope='function') +def change_email_confirmation_template(notify_db, + notify_db_session): + service, user = notify_service(notify_db, notify_db_session) + content = """Hi ((name)), + Click this link to confirm your new email address: + ((url)) + If you didn’t try to change the email address for your GOV.UK Notify account, let us know here: + ((feedback_url))""" + template = create_notify_template(service=service, + user=user, + template_config_name='CHANGE_EMAIL_CONFIRMATION_TEMPLATE_ID', + content=content, + template_type='email') + return template + + +def create_notify_template(service, user, template_config_name, content, template_type, subject=None): + template = Template.query.get(current_app.config[template_config_name]) + if not template: + data = { + 'id': current_app.config[template_config_name], + 'name': template_config_name, + 'template_type': template_type, + 'content': content, + 'service': service, + 'created_by': user, + 'subject': subject, + 'archived': False + } + template = Template(**data) + db.session.add(template) + return template + + +def notify_service(notify_db, notify_db_session): user = sample_user(notify_db, notify_db_session) service = Service.query.get(current_app.config['NOTIFY_SERVICE_ID']) if not service: @@ -864,22 +820,7 @@ def already_registered_template(notify_db, } service = Service(**data) db.session.add(service) - - template = Template.query.get(current_app.config['ALREADY_REGISTERED_EMAIL_TEMPLATE_ID']) - if not template: - data = { - 'id': current_app.config['ALREADY_REGISTERED_EMAIL_TEMPLATE_ID'], - 'name': 'ALREADY_REGISTERED_EMAIL_TEMPLATE_ID', - 'template_type': 'email', - 'content': """Sign in here: ((signin_url)) If you’ve forgotten your password, - you can reset it here: ((forgot_password_url)) feedback:((feedback_url))""", - 'service': service, - 'created_by': user, - 'archived': False - } - template = Template(**data) - db.session.add(template) - return template + return service, user @pytest.fixture(scope='function') diff --git a/tests/app/user/test_rest.py b/tests/app/user/test_rest.py index 9d1bf35c7..0ac5fceef 100644 --- a/tests/app/user/test_rest.py +++ b/tests/app/user/test_rest.py @@ -516,3 +516,39 @@ def test_send_already_registered_email_returns_400_when_data_is_missing(notify_a headers=[('Content-Type', 'application/json'), auth_header]) assert resp.status_code == 400 assert json.loads(resp.get_data(as_text=True))['message'] == {'email': ['Missing data for required field.']} + + +@freeze_time("2016-01-01T11:09:00.061258") +def test_send_user_confirm_new_email_returns_204(notify_api, sample_user, mocker, change_email_confirmation_template): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + mocker.patch("app.encryption.encrypt", return_value='encrypted_message') + mocked = mocker.patch('app.celery.tasks.send_email.apply_async') + mocker.patch('uuid.uuid4', return_value='some_uuid') # for the notification id + new_email = 'new_address@dig.gov.uk' + data = json.dumps({'email': new_email}) + auth_header = create_authorization_header() + + resp = client.post(url_for('user.send_user_confirm_new_email', user_id=str(sample_user.id)), + data=data, + headers=[('Content-Type', 'application/json'), auth_header]) + assert resp.status_code == 204 + mocked.assert_called_once_with(( + str(current_app.config['NOTIFY_SERVICE_ID']), + "some_uuid", + 'encrypted_message', + "2016-01-01T11:09:00.061258"), queue="notify") + + +def test_send_user_confirm_new_email_returns_400_when_email_missing(notify_api, sample_user, mocker): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + mocked = mocker.patch('app.celery.tasks.send_email.apply_async') + data = json.dumps({}) + auth_header = create_authorization_header() + resp = client.post(url_for('user.send_user_confirm_new_email', user_id=str(sample_user.id)), + data=data, + headers=[('Content-Type', 'application/json'), auth_header]) + assert resp.status_code == 400 + assert json.loads(resp.get_data(as_text=True))['message'] == {'email': ['Missing data for required field.']} + mocked.assert_not_called()