Merge pull request #3931 from alphagov/refactor-email-verify-webauthn

Refactor email revalidation check
This commit is contained in:
Ben Thorner
2021-06-16 16:24:41 +01:00
committed by GitHub
21 changed files with 294 additions and 272 deletions

View File

@@ -2,10 +2,14 @@ from functools import partial
import pytest
from flask import url_for
from freezegun import freeze_time
from app import (
from app.formatters import (
email_safe,
format_datetime_relative,
format_notification_status_as_url,
format_number_in_pounds_as_currency,
round_to_significant_figures,
)
@@ -52,3 +56,74 @@ def test_format_notification_status_as_url(
])
def test_format_number_in_pounds_as_currency(input_number, formatted_number):
assert format_number_in_pounds_as_currency(input_number) == formatted_number
@pytest.mark.parametrize('time, human_readable_datetime', [
('2018-03-14 09:00', '14 March at 9:00am'),
('2018-03-14 15:00', '14 March at 3:00pm'),
('2018-03-15 09:00', '15 March at 9:00am'),
('2018-03-15 15:00', '15 March at 3:00pm'),
('2018-03-19 09:00', '19 March at 9:00am'),
('2018-03-19 15:00', '19 March at 3:00pm'),
('2018-03-19 23:59', '19 March at 11:59pm'),
('2018-03-20 00:00', '19 March at midnight'), # we specifically refer to 00:00 as belonging to the day before.
('2018-03-20 00:01', 'yesterday at 12:01am'),
('2018-03-20 09:00', 'yesterday at 9:00am'),
('2018-03-20 15:00', 'yesterday at 3:00pm'),
('2018-03-20 23:59', 'yesterday at 11:59pm'),
('2018-03-21 00:00', 'yesterday at midnight'), # we specifically refer to 00:00 as belonging to the day before.
('2018-03-21 00:01', 'today at 12:01am'),
('2018-03-21 09:00', 'today at 9:00am'),
('2018-03-21 12:00', 'today at midday'),
('2018-03-21 15:00', 'today at 3:00pm'),
('2018-03-21 23:59', 'today at 11:59pm'),
('2018-03-22 00:00', 'today at midnight'), # we specifically refer to 00:00 as belonging to the day before.
('2018-03-22 00:01', 'tomorrow at 12:01am'),
('2018-03-22 09:00', 'tomorrow at 9:00am'),
('2018-03-22 15:00', 'tomorrow at 3:00pm'),
('2018-03-22 23:59', 'tomorrow at 11:59pm'),
('2018-03-23 00:01', '23 March at 12:01am'),
('2018-03-23 09:00', '23 March at 9:00am'),
('2018-03-23 15:00', '23 March at 3:00pm'),
])
def test_format_datetime_relative(time, human_readable_datetime):
with freeze_time('2018-03-21 12:00'):
assert format_datetime_relative(time) == human_readable_datetime
@pytest.mark.parametrize('value, significant_figures, expected_result', (
(0, 1, 0),
(0, 2, 0),
(12_345, 1, 10_000),
(12_345, 2, 12_000),
(12_345, 3, 12_300),
(12_345, 9, 12_345),
(12_345.6789, 1, 10_000),
(12_345.6789, 9, 12_345),
(-12_345, 1, -10_000),
))
def test_round_to_significant_figures(value, significant_figures, expected_result):
assert round_to_significant_figures(value, significant_figures) == expected_result
@pytest.mark.parametrize('service_name, safe_email', [
('name with spaces', 'name.with.spaces'),
('singleword', 'singleword'),
('UPPER CASE', 'upper.case'),
('Service - with dash', 'service.with.dash'),
('lots of spaces', 'lots.of.spaces'),
('name.with.dots', 'name.with.dots'),
('name-with-other-delimiters', 'namewithotherdelimiters'),
('.leading', 'leading'),
('trailing.', 'trailing'),
('üńïçödë wördś', 'unicode.words'),
])
def test_email_safe_return_dot_separated_email_domain(service_name, safe_email):
assert email_safe(service_name) == safe_email

View File

@@ -1,7 +1,6 @@
import pytest
from bs4 import BeautifulSoup
from flask import url_for
from freezegun import freeze_time
from tests.conftest import (
SERVICE_ONE_ID,
@@ -11,6 +10,11 @@ from tests.conftest import (
)
@pytest.fixture
def mock_email_validated_recently(mocker):
return mocker.patch('app.main.views.two_factor.email_needs_revalidating', return_value=False)
@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', [
@@ -71,7 +75,6 @@ def test_should_render_two_factor_page(
)['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(
client,
api_user_active,
@@ -79,12 +82,12 @@ def test_should_login_user_and_should_redirect_to_next_url(
mock_get_user_by_email,
mock_check_verify_code,
mock_create_event,
mock_email_validated_recently,
):
with client.session_transaction() as session:
session['user_details'] = {
'id': api_user_active['id'],
'email': api_user_active['email_address']}
api_user_active['email_access_validated_at'] = '2020-01-23T11:35:21.726132Z'
response = client.post(url_for('main.two_factor_sms', next='/services/{}'.format(SERVICE_ONE_ID)),
data={'sms_code': '12345'})
@@ -96,7 +99,6 @@ def test_should_login_user_and_should_redirect_to_next_url(
)
@freeze_time('2020-01-27T12:00:00')
def test_should_send_email_and_redirect_to_info_page_if_user_needs_to_revalidate_email(
client,
api_user_active,
@@ -107,7 +109,7 @@ def test_should_send_email_and_redirect_to_info_page_if_user_needs_to_revalidate
mocker
):
mocker.patch('app.user_api_client.get_user', return_value=api_user_active)
api_user_active['email_access_validated_at'] = '2019-03-23T11:35:21.726132Z'
mocker.patch('app.main.views.two_factor.email_needs_revalidating', return_value=True)
with client.session_transaction() as session:
session['user_details'] = {
'id': api_user_active['id'],
@@ -124,7 +126,6 @@ def test_should_send_email_and_redirect_to_info_page_if_user_needs_to_revalidate
mock_send_verify_code.assert_called_with(api_user_active['id'], 'email', None, mocker.ANY)
@freeze_time('2020-01-27T12:00:00')
def test_should_login_user_and_not_redirect_to_external_url(
client,
api_user_active,
@@ -133,12 +134,12 @@ def test_should_login_user_and_not_redirect_to_external_url(
mock_check_verify_code,
mock_get_services_with_one_service,
mock_create_event,
mock_email_validated_recently,
):
with client.session_transaction() as session:
session['user_details'] = {
'id': api_user_active['id'],
'email': api_user_active['email_address']}
api_user_active['email_access_validated_at'] = '2020-01-23T11:35:21.726132Z'
response = client.post(url_for('main.two_factor_sms', next='http://www.google.com'),
data={'sms_code': '12345'})
@@ -149,7 +150,6 @@ def test_should_login_user_and_not_redirect_to_external_url(
@pytest.mark.parametrize('platform_admin', (
True, False,
))
@freeze_time('2020-01-27T12:00:00')
def test_should_login_user_and_redirect_to_show_accounts(
client,
api_user_active,
@@ -157,13 +157,13 @@ def test_should_login_user_and_redirect_to_show_accounts(
mock_get_user_by_email,
mock_check_verify_code,
mock_create_event,
mock_email_validated_recently,
platform_admin,
):
with client.session_transaction() as session:
session['user_details'] = {
'id': api_user_active['id'],
'email': api_user_active['email_address']}
api_user_active['email_access_validated_at'] = '2020-01-23T11:35:21.726132Z'
api_user_active['platform_admin'] = platform_admin
response = client.post(url_for('main.two_factor_sms'),
@@ -192,7 +192,6 @@ def test_should_return_200_with_sms_code_error_when_sms_code_is_wrong(
assert 'Code not found' in response.get_data(as_text=True)
@freeze_time('2020-01-27T12:00:00')
def test_should_login_user_when_multiple_valid_codes_exist(
client,
api_user_active,
@@ -201,19 +200,18 @@ def test_should_login_user_when_multiple_valid_codes_exist(
mock_check_verify_code,
mock_get_services_with_one_service,
mock_create_event,
mock_email_validated_recently,
):
with client.session_transaction() as session:
session['user_details'] = {
'id': api_user_active['id'],
'email': api_user_active['email_address']}
api_user_active['email_access_validated_at'] = '2020-01-23T11:35:21.726132Z'
response = client.post(url_for('main.two_factor_sms'),
data={'sms_code': '23456'})
assert response.status_code == 302
@freeze_time('2020-01-27T12:00:00')
def test_two_factor_should_set_password_when_new_password_exists_in_session(
client,
api_user_active,
@@ -222,13 +220,13 @@ def test_two_factor_should_set_password_when_new_password_exists_in_session(
mock_get_services_with_one_service,
mock_update_user_password,
mock_create_event,
mock_email_validated_recently,
):
with client.session_transaction() as session:
session['user_details'] = {
'id': api_user_active['id'],
'email': api_user_active['email_address'],
'password': 'changedpassword'}
api_user_active['email_access_validated_at'] = '2020-01-23T11:35:21.726132Z'
response = client.post(url_for('main.two_factor_sms'),
data={'sms_code': '12345'})
@@ -279,7 +277,6 @@ def test_two_factor_get_should_redirect_to_sign_in_if_user_not_in_session(
)
@freeze_time('2020-01-27T12:00:00')
def test_two_factor_should_activate_pending_user(
client,
mocker,
@@ -287,10 +284,10 @@ def test_two_factor_should_activate_pending_user(
mock_check_verify_code,
mock_create_event,
mock_activate_user,
mock_email_validated_recently,
):
mocker.patch('app.user_api_client.get_user', return_value=api_user_pending)
mocker.patch('app.service_api_client.get_services', return_value={'data': []})
api_user_pending['email_access_validated_at'] = '2020-01-23T11:35:21.726132Z'
with client.session_transaction() as session:
session['user_details'] = {
'id': api_user_pending['id'],

View File

@@ -4,7 +4,6 @@ from unittest.mock import ANY, Mock
import pytest
from fido2 import cbor
from flask import url_for
from freezegun.api import freeze_time
from app.models.webauthn_credential import RegistrationError, WebAuthnCredential
@@ -336,7 +335,6 @@ def test_complete_authentication_clears_session(
assert 'webauthn_authentication_state' not in session
@freeze_time('2020-01-30')
@pytest.mark.parametrize('url_kwargs, expected_redirect', [
({}, '/accounts-or-dashboard'),
({'next': '/bar'}, '/bar'),
@@ -350,7 +348,6 @@ def test_verify_webauthn_login_signs_user_in(
expected_redirect,
):
platform_admin_user['auth_type'] = 'webauthn_auth'
platform_admin_user['email_access_validated_at'] = '2020-01-25T00:00:00.000000Z'
with client.session_transaction() as session:
session['user_details'] = {
@@ -360,6 +357,7 @@ def test_verify_webauthn_login_signs_user_in(
mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
mocker.patch('app.main.views.webauthn_credentials._verify_webauthn_authentication')
mocker.patch('app.user_api_client.complete_webauthn_login_attempt', return_value=(True, None))
mocker.patch('app.main.views.webauthn_credentials.email_needs_revalidating', return_value=False)
resp = client.post(url_for('main.webauthn_complete_authentication', **url_kwargs))
@@ -397,7 +395,6 @@ def test_verify_webauthn_login_signs_user_in_doesnt_sign_user_in_if_api_rejects(
assert resp.status_code == 403
@freeze_time('2020-04-30')
def test_verify_webauthn_login_signs_user_in_sends_revalidation_email_if_needed(
client,
mocker,
@@ -405,7 +402,6 @@ def test_verify_webauthn_login_signs_user_in_sends_revalidation_email_if_needed(
platform_admin_user,
):
platform_admin_user['auth_type'] = 'webauthn_auth'
platform_admin_user['email_access_validated_at'] = '2020-01-25T00:00:00.000000Z'
user_details = {
'id': platform_admin_user['id'],
'email': platform_admin_user['email_address']
@@ -417,6 +413,7 @@ def test_verify_webauthn_login_signs_user_in_sends_revalidation_email_if_needed(
mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
mocker.patch('app.main.views.webauthn_credentials._verify_webauthn_authentication')
mocker.patch('app.user_api_client.complete_webauthn_login_attempt', return_value=(True, None))
mocker.patch('app.main.views.webauthn_credentials.email_needs_revalidating', return_value=True)
resp = client.post(url_for('main.webauthn_complete_authentication'))

View File

@@ -1,51 +1,6 @@
import pytest
from freezegun import freeze_time
from app import format_datetime_relative
from app.formatters import email_safe, round_to_significant_figures
from app.utils import (
generate_next_dict,
generate_previous_dict,
get_current_financial_year,
get_logo_cdn_domain,
is_less_than_days_ago,
merge_jsonlike,
)
@pytest.mark.parametrize('service_name, safe_email', [
('name with spaces', 'name.with.spaces'),
('singleword', 'singleword'),
('UPPER CASE', 'upper.case'),
('Service - with dash', 'service.with.dash'),
('lots of spaces', 'lots.of.spaces'),
('name.with.dots', 'name.with.dots'),
('name-with-other-delimiters', 'namewithotherdelimiters'),
('.leading', 'leading'),
('trailing.', 'trailing'),
('üńïçödë wördś', 'unicode.words'),
])
def test_email_safe_return_dot_separated_email_domain(service_name, safe_email):
assert email_safe(service_name) == safe_email
def test_generate_previous_dict(client):
ret = generate_previous_dict('main.view_jobs', 'foo', 2, {})
assert 'page=1' in ret['url']
assert ret['title'] == 'Previous page'
assert ret['label'] == 'page 1'
def test_generate_next_dict(client):
ret = generate_next_dict('main.view_jobs', 'foo', 2, {})
assert 'page=3' in ret['url']
assert ret['title'] == 'Next page'
assert ret['label'] == 'page 3'
def test_generate_previous_next_dict_adds_other_url_args(client):
ret = generate_next_dict('main.view_notifications', 'foo', 2, {'message_type': 'blah'})
assert 'notifications/blah' in ret['url']
from app.utils import get_logo_cdn_domain, merge_jsonlike
def test_get_cdn_domain_on_localhost(client, mocker):
@@ -60,56 +15,6 @@ def test_get_cdn_domain_on_non_localhost(client, mocker):
assert domain == 'static-logos.admintest.com'
@pytest.mark.parametrize('time, human_readable_datetime', [
('2018-03-14 09:00', '14 March at 9:00am'),
('2018-03-14 15:00', '14 March at 3:00pm'),
('2018-03-15 09:00', '15 March at 9:00am'),
('2018-03-15 15:00', '15 March at 3:00pm'),
('2018-03-19 09:00', '19 March at 9:00am'),
('2018-03-19 15:00', '19 March at 3:00pm'),
('2018-03-19 23:59', '19 March at 11:59pm'),
('2018-03-20 00:00', '19 March at midnight'), # we specifically refer to 00:00 as belonging to the day before.
('2018-03-20 00:01', 'yesterday at 12:01am'),
('2018-03-20 09:00', 'yesterday at 9:00am'),
('2018-03-20 15:00', 'yesterday at 3:00pm'),
('2018-03-20 23:59', 'yesterday at 11:59pm'),
('2018-03-21 00:00', 'yesterday at midnight'), # we specifically refer to 00:00 as belonging to the day before.
('2018-03-21 00:01', 'today at 12:01am'),
('2018-03-21 09:00', 'today at 9:00am'),
('2018-03-21 12:00', 'today at midday'),
('2018-03-21 15:00', 'today at 3:00pm'),
('2018-03-21 23:59', 'today at 11:59pm'),
('2018-03-22 00:00', 'today at midnight'), # we specifically refer to 00:00 as belonging to the day before.
('2018-03-22 00:01', 'tomorrow at 12:01am'),
('2018-03-22 09:00', 'tomorrow at 9:00am'),
('2018-03-22 15:00', 'tomorrow at 3:00pm'),
('2018-03-22 23:59', 'tomorrow at 11:59pm'),
('2018-03-23 00:01', '23 March at 12:01am'),
('2018-03-23 09:00', '23 March at 9:00am'),
('2018-03-23 15:00', '23 March at 3:00pm'),
])
def test_format_datetime_relative(time, human_readable_datetime):
with freeze_time('2018-03-21 12:00'):
assert format_datetime_relative(time) == human_readable_datetime
@pytest.mark.parametrize("date_from_db, expected_result", [
('2019-11-17T11:35:21.726132Z', True),
('2019-11-16T11:35:21.726132Z', False),
('2019-11-16T11:35:21+0000', False),
])
@freeze_time('2020-02-14T12:00:00')
def test_is_less_than_days_ago(date_from_db, expected_result):
assert is_less_than_days_ago(date_from_db, 90) == expected_result
@pytest.mark.parametrize("source_object, destination_object, expected_result", [
# simple dicts:
({"a": "b"}, {"c": "d"}, {"a": "b", "c": "d"}),
@@ -143,29 +48,3 @@ def test_is_less_than_days_ago(date_from_db, expected_result):
def test_merge_jsonlike_merges_jsonlike_objects_correctly(source_object, destination_object, expected_result):
merge_jsonlike(source_object, destination_object)
assert source_object == expected_result
@pytest.mark.parametrize('value, significant_figures, expected_result', (
(0, 1, 0),
(0, 2, 0),
(12_345, 1, 10_000),
(12_345, 2, 12_000),
(12_345, 3, 12_300),
(12_345, 9, 12_345),
(12_345.6789, 1, 10_000),
(12_345.6789, 9, 12_345),
(-12_345, 1, -10_000),
))
def test_round_to_significant_figures(value, significant_figures, expected_result):
assert round_to_significant_figures(value, significant_figures) == expected_result
@pytest.mark.parametrize('datetime_string, financial_year', (
('2021-01-01T00:00:00+00:00', 2020), # Start of 2021
('2021-03-31T22:59:59+00:00', 2020), # One minute before midnight (BST)
('2021-03-31T23:00:00+00:00', 2021), # Midnight (BST)
('2021-12-12T12:12:12+01:00', 2021), # Later in the year
))
def test_get_financial_year(datetime_string, financial_year):
with freeze_time(datetime_string):
assert get_current_financial_year() == financial_year

View File

@@ -0,0 +1,19 @@
import pytest
from freezegun import freeze_time
from app.models.user import User
from app.utils.login import email_needs_revalidating
@freeze_time('2020-11-27T12:00:00')
@pytest.mark.parametrize(('email_access_validated_at', 'expected_result'), (
('2020-10-01T11:35:21.726132Z', False),
('2020-07-23T11:35:21.726132Z', True),
))
def test_email_needs_revalidating(
api_user_active,
email_access_validated_at,
expected_result,
):
api_user_active['email_access_validated_at'] = email_access_validated_at
assert email_needs_revalidating(User(api_user_active)) == expected_result

View File

@@ -0,0 +1,20 @@
from app.utils.pagination import generate_next_dict, generate_previous_dict
def test_generate_previous_dict(client):
result = generate_previous_dict('main.view_jobs', 'foo', 2, {})
assert 'page=1' in result['url']
assert result['title'] == 'Previous page'
assert result['label'] == 'page 1'
def test_generate_next_dict(client):
result = generate_next_dict('main.view_jobs', 'foo', 2, {})
assert 'page=3' in result['url']
assert result['title'] == 'Next page'
assert result['label'] == 'page 3'
def test_generate_previous_next_dict_adds_other_url_args(client):
result = generate_next_dict('main.view_notifications', 'foo', 2, {'message_type': 'blah'})
assert 'notifications/blah' in result['url']

View File

@@ -0,0 +1,25 @@
import pytest
from freezegun import freeze_time
from app.utils.time import get_current_financial_year, is_less_than_days_ago
@pytest.mark.parametrize("date_from_db, expected_result", [
('2019-11-17T11:35:21.726132Z', True),
('2019-11-16T11:35:21.726132Z', False),
('2019-11-16T11:35:21+0000', False),
])
@freeze_time('2020-02-14T12:00:00')
def test_is_less_than_days_ago(date_from_db, expected_result):
assert is_less_than_days_ago(date_from_db, 90) == expected_result
@pytest.mark.parametrize('datetime_string, financial_year', (
('2021-01-01T00:00:00+00:00', 2020), # Start of 2021
('2021-03-31T22:59:59+00:00', 2020), # One minute before midnight (BST)
('2021-03-31T23:00:00+00:00', 2021), # Midnight (BST)
('2021-12-12T12:12:12+01:00', 2021), # Later in the year
))
def test_get_financial_year(datetime_string, financial_year):
with freeze_time(datetime_string):
assert get_current_financial_year() == financial_year