mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-20 14:29:51 -04:00
Merge pull request #3923 from alphagov/refactor-email-verify
Split out utils code into separate modules
This commit is contained in:
@@ -1,113 +0,0 @@
|
||||
from collections import namedtuple
|
||||
|
||||
import pytest
|
||||
|
||||
from app.utils import get_errors_for_csv
|
||||
|
||||
MockRecipients = namedtuple(
|
||||
'RecipientCSV',
|
||||
[
|
||||
'rows_with_bad_recipients',
|
||||
'rows_with_missing_data',
|
||||
'rows_with_message_too_long',
|
||||
'rows_with_empty_message'
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"rows_with_bad_recipients, rows_with_missing_data, "
|
||||
"rows_with_message_too_long, rows_with_empty_message, template_type, expected_errors",
|
||||
[
|
||||
(
|
||||
[], [], [], [],
|
||||
'sms',
|
||||
[]
|
||||
),
|
||||
(
|
||||
{2}, [], [], [],
|
||||
'sms',
|
||||
['fix 1 phone number']
|
||||
),
|
||||
(
|
||||
{2, 4, 6}, [], [], [],
|
||||
'sms',
|
||||
['fix 3 phone numbers']
|
||||
),
|
||||
(
|
||||
{1}, [], [], [],
|
||||
'email',
|
||||
['fix 1 email address']
|
||||
),
|
||||
(
|
||||
{2, 4, 6}, [], [], [],
|
||||
'email',
|
||||
['fix 3 email addresses']
|
||||
),
|
||||
(
|
||||
{2}, [], [], [],
|
||||
'letter',
|
||||
['fix 1 address']
|
||||
),
|
||||
(
|
||||
{2, 4}, [], [], [],
|
||||
'letter',
|
||||
['fix 2 addresses']
|
||||
),
|
||||
(
|
||||
{2}, {3}, [], [],
|
||||
'sms',
|
||||
[
|
||||
'fix 1 phone number',
|
||||
'enter missing data in 1 row'
|
||||
]
|
||||
),
|
||||
(
|
||||
{2, 4, 6, 8}, {3, 6, 9, 12}, [], [],
|
||||
'sms',
|
||||
[
|
||||
'fix 4 phone numbers',
|
||||
'enter missing data in 4 rows'
|
||||
]
|
||||
),
|
||||
(
|
||||
{}, {}, {3}, [],
|
||||
'sms',
|
||||
[
|
||||
'shorten the message in 1 row'
|
||||
]
|
||||
),
|
||||
(
|
||||
{}, {}, {3, 12}, [],
|
||||
'sms',
|
||||
[
|
||||
'shorten the messages in 2 rows'
|
||||
]
|
||||
),
|
||||
(
|
||||
{}, {}, {}, {2},
|
||||
'sms',
|
||||
[
|
||||
'check you have content for the empty message in 1 row'
|
||||
]
|
||||
),
|
||||
(
|
||||
{}, {}, {}, {2, 4, 8},
|
||||
'sms',
|
||||
[
|
||||
'check you have content for the empty messages in 3 rows'
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
def test_get_errors_for_csv(
|
||||
rows_with_bad_recipients, rows_with_missing_data, rows_with_message_too_long, rows_with_empty_message,
|
||||
template_type,
|
||||
expected_errors
|
||||
):
|
||||
assert get_errors_for_csv(
|
||||
MockRecipients(
|
||||
rows_with_bad_recipients, rows_with_missing_data, rows_with_message_too_long, rows_with_empty_message
|
||||
),
|
||||
template_type
|
||||
) == expected_errors
|
||||
@@ -3,15 +3,12 @@ import inspect
|
||||
import re
|
||||
|
||||
import pytest
|
||||
from flask import current_app, request
|
||||
from werkzeug.exceptions import Forbidden, Unauthorized
|
||||
from flask import current_app
|
||||
|
||||
from app.main.views.index import index
|
||||
from app.models.roles_and_permissions import (
|
||||
translate_permissions_from_admin_roles_to_db,
|
||||
translate_permissions_from_db_to_admin_roles,
|
||||
)
|
||||
from app.utils import user_has_permissions
|
||||
from tests import service_json
|
||||
from tests.conftest import (
|
||||
ORGANISATION_ID,
|
||||
@@ -21,245 +18,6 @@ from tests.conftest import (
|
||||
)
|
||||
|
||||
|
||||
def _test_permissions(
|
||||
client,
|
||||
usr,
|
||||
permissions,
|
||||
will_succeed,
|
||||
kwargs=None,
|
||||
):
|
||||
request.view_args.update({'service_id': 'foo'})
|
||||
if usr:
|
||||
client.login(usr)
|
||||
|
||||
decorator = user_has_permissions(*permissions, **(kwargs or {}))
|
||||
decorated_index = decorator(index)
|
||||
|
||||
if will_succeed:
|
||||
decorated_index()
|
||||
else:
|
||||
try:
|
||||
if (
|
||||
decorated_index().location != '/sign-in?next=%2F' or
|
||||
decorated_index().status_code != 302
|
||||
):
|
||||
pytest.fail("Failed to throw a forbidden or unauthorised exception")
|
||||
except (Forbidden, Unauthorized):
|
||||
pass
|
||||
|
||||
|
||||
def test_user_has_permissions_on_endpoint_fail(
|
||||
client,
|
||||
mocker,
|
||||
mock_get_service,
|
||||
):
|
||||
user = _user_with_permissions()
|
||||
mocker.patch('app.user_api_client.get_user', return_value=user)
|
||||
_test_permissions(
|
||||
client,
|
||||
user,
|
||||
['send_messages'],
|
||||
will_succeed=False)
|
||||
|
||||
|
||||
def test_user_has_permissions_success(
|
||||
client,
|
||||
mocker,
|
||||
):
|
||||
user = _user_with_permissions()
|
||||
mocker.patch('app.user_api_client.get_user', return_value=user)
|
||||
_test_permissions(
|
||||
client,
|
||||
user,
|
||||
['manage_service'],
|
||||
will_succeed=True)
|
||||
|
||||
|
||||
def test_user_has_permissions_or(
|
||||
client,
|
||||
mocker,
|
||||
):
|
||||
user = _user_with_permissions()
|
||||
mocker.patch('app.user_api_client.get_user', return_value=user)
|
||||
_test_permissions(
|
||||
client,
|
||||
user,
|
||||
['send_messages', 'manage_service'],
|
||||
will_succeed=True)
|
||||
|
||||
|
||||
def test_user_has_permissions_multiple(
|
||||
client,
|
||||
mocker,
|
||||
):
|
||||
user = _user_with_permissions()
|
||||
mocker.patch('app.user_api_client.get_user', return_value=user)
|
||||
_test_permissions(
|
||||
client,
|
||||
user,
|
||||
['manage_templates', 'manage_service'],
|
||||
will_succeed=True)
|
||||
|
||||
|
||||
def test_exact_permissions(
|
||||
client,
|
||||
mocker,
|
||||
):
|
||||
user = _user_with_permissions()
|
||||
mocker.patch('app.user_api_client.get_user', return_value=user)
|
||||
_test_permissions(
|
||||
client,
|
||||
user,
|
||||
['manage_service', 'manage_templates'],
|
||||
will_succeed=True)
|
||||
|
||||
|
||||
def test_platform_admin_user_can_access_page_that_has_no_permissions(
|
||||
client,
|
||||
platform_admin_user,
|
||||
mocker,
|
||||
):
|
||||
mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
|
||||
_test_permissions(
|
||||
client,
|
||||
platform_admin_user,
|
||||
[],
|
||||
will_succeed=True)
|
||||
|
||||
|
||||
def test_platform_admin_user_can_not_access_page(
|
||||
client,
|
||||
platform_admin_user,
|
||||
mocker,
|
||||
mock_get_service,
|
||||
):
|
||||
mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
|
||||
_test_permissions(
|
||||
client,
|
||||
platform_admin_user,
|
||||
[],
|
||||
will_succeed=False,
|
||||
kwargs={'restrict_admin_usage': True})
|
||||
|
||||
|
||||
def test_no_user_returns_401_unauth(
|
||||
client
|
||||
):
|
||||
from flask_login import current_user
|
||||
assert not current_user.is_authenticated
|
||||
_test_permissions(
|
||||
client,
|
||||
None,
|
||||
[],
|
||||
will_succeed=False)
|
||||
|
||||
|
||||
def test_user_has_permissions_for_organisation(
|
||||
client,
|
||||
mocker,
|
||||
):
|
||||
user = _user_with_permissions()
|
||||
user['organisations'] = ['org_1', 'org_2']
|
||||
mocker.patch('app.user_api_client.get_user', return_value=user)
|
||||
client.login(user)
|
||||
|
||||
request.view_args = {'org_id': 'org_2'}
|
||||
|
||||
@user_has_permissions()
|
||||
def index():
|
||||
pass
|
||||
|
||||
index()
|
||||
|
||||
|
||||
def test_platform_admin_can_see_orgs_they_dont_have(
|
||||
client,
|
||||
platform_admin_user,
|
||||
mocker,
|
||||
):
|
||||
platform_admin_user['organisations'] = []
|
||||
mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
|
||||
client.login(platform_admin_user)
|
||||
|
||||
request.view_args = {'org_id': 'org_2'}
|
||||
|
||||
@user_has_permissions()
|
||||
def index():
|
||||
pass
|
||||
|
||||
index()
|
||||
|
||||
|
||||
def test_cant_use_decorator_without_view_args(
|
||||
client,
|
||||
platform_admin_user,
|
||||
mocker,
|
||||
):
|
||||
mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
|
||||
client.login(platform_admin_user)
|
||||
|
||||
request.view_args = {}
|
||||
|
||||
@user_has_permissions()
|
||||
def index():
|
||||
pass
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
index()
|
||||
|
||||
|
||||
def test_user_doesnt_have_permissions_for_organisation(
|
||||
client,
|
||||
mocker,
|
||||
):
|
||||
user = _user_with_permissions()
|
||||
user['organisations'] = ['org_1', 'org_2']
|
||||
mocker.patch('app.user_api_client.get_user', return_value=user)
|
||||
client.login(user)
|
||||
|
||||
request.view_args = {'org_id': 'org_3'}
|
||||
|
||||
@user_has_permissions()
|
||||
def index():
|
||||
pass
|
||||
|
||||
with pytest.raises(Forbidden):
|
||||
index()
|
||||
|
||||
|
||||
def test_user_with_no_permissions_to_service_goes_to_templates(
|
||||
client,
|
||||
mocker
|
||||
):
|
||||
user = _user_with_permissions()
|
||||
mocker.patch('app.user_api_client.get_user', return_value=user)
|
||||
client.login(user)
|
||||
request.view_args = {'service_id': 'bar'}
|
||||
|
||||
@user_has_permissions()
|
||||
def index():
|
||||
pass
|
||||
|
||||
index()
|
||||
|
||||
|
||||
def _user_with_permissions():
|
||||
user_data = {'id': 999,
|
||||
'name': 'Test User',
|
||||
'password': 'somepassword',
|
||||
'email_address': 'test@user.gov.uk',
|
||||
'mobile_number': '+4412341234',
|
||||
'state': 'active',
|
||||
'failed_login_count': 0,
|
||||
'permissions': {'foo': ['manage_users', 'manage_templates', 'manage_settings']},
|
||||
'platform_admin': False,
|
||||
'organisations': ['org_1', 'org_2'],
|
||||
'services': ['foo', 'bar'],
|
||||
'current_session_id': None,
|
||||
}
|
||||
return user_data
|
||||
|
||||
|
||||
def test_translate_permissions_from_db_to_admin_roles():
|
||||
db_perms = ['send_texts', 'send_emails', 'send_letters', 'manage_templates', 'some_unknown_permission']
|
||||
roles = translate_permissions_from_db_to_admin_roles(db_perms)
|
||||
|
||||
@@ -3,7 +3,7 @@ from flask import session, url_for
|
||||
from freezegun import freeze_time
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app.utils import is_gov_user
|
||||
from app.utils.user import is_gov_user
|
||||
from tests import organisation_json
|
||||
from tests.conftest import normalize_spaces
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import pytest
|
||||
from flask import url_for
|
||||
|
||||
import app
|
||||
from app.utils import is_gov_user
|
||||
from app.utils.user import is_gov_user
|
||||
from tests.conftest import (
|
||||
ORGANISATION_ID,
|
||||
ORGANISATION_TWO_ID,
|
||||
|
||||
@@ -1,97 +1,16 @@
|
||||
from collections import OrderedDict
|
||||
from csv import DictReader
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from bs4 import BeautifulSoup
|
||||
from flask import url_for
|
||||
from freezegun import freeze_time
|
||||
from notifications_utils.template import Template
|
||||
|
||||
from app import format_datetime_relative
|
||||
from app.formatters import email_safe, round_to_significant_figures
|
||||
from app.utils import (
|
||||
Spreadsheet,
|
||||
generate_next_dict,
|
||||
generate_notifications_csv,
|
||||
generate_previous_dict,
|
||||
get_current_financial_year,
|
||||
get_letter_printing_statement,
|
||||
get_letter_validation_error,
|
||||
get_logo_cdn_domain,
|
||||
get_sample_template,
|
||||
is_less_than_days_ago,
|
||||
merge_jsonlike,
|
||||
printing_today_or_tomorrow,
|
||||
)
|
||||
from tests.conftest import fake_uuid
|
||||
|
||||
|
||||
def _get_notifications_csv(
|
||||
row_number=1,
|
||||
recipient='foo@bar.com',
|
||||
template_name='foo',
|
||||
template_type='sms',
|
||||
job_name='bar.csv',
|
||||
status='Delivered',
|
||||
created_at='1943-04-19 12:00:00',
|
||||
rows=1,
|
||||
with_links=False,
|
||||
job_id=fake_uuid,
|
||||
created_by_name=None,
|
||||
created_by_email_address=None,
|
||||
):
|
||||
|
||||
def _get(
|
||||
service_id,
|
||||
page=1,
|
||||
job_id=None,
|
||||
template_type=template_type,
|
||||
):
|
||||
links = {}
|
||||
if with_links:
|
||||
links = {
|
||||
'prev': '/service/{}/notifications?page=0'.format(service_id),
|
||||
'next': '/service/{}/notifications?page=1'.format(service_id),
|
||||
'last': '/service/{}/notifications?page=2'.format(service_id)
|
||||
}
|
||||
|
||||
data = {
|
||||
'notifications': [{
|
||||
"row_number": row_number + i,
|
||||
"to": recipient,
|
||||
"recipient": recipient,
|
||||
"client_reference": 'ref 1234',
|
||||
"template_name": template_name,
|
||||
"template_type": template_type,
|
||||
"template": {"name": template_name, "template_type": template_type},
|
||||
"job_name": job_name,
|
||||
"status": status,
|
||||
"created_at": created_at,
|
||||
"updated_at": None,
|
||||
"created_by_name": created_by_name,
|
||||
"created_by_email_address": created_by_email_address,
|
||||
} for i in range(rows)],
|
||||
'total': rows,
|
||||
'page_size': 50,
|
||||
'links': links
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
return _get
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def _get_notifications_csv_mock(
|
||||
mocker,
|
||||
api_user_active,
|
||||
):
|
||||
return mocker.patch(
|
||||
'app.notification_api_client.get_notifications_for_service',
|
||||
side_effect=_get_notifications_csv()
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('service_name, safe_email', [
|
||||
@@ -129,179 +48,6 @@ def test_generate_previous_next_dict_adds_other_url_args(client):
|
||||
assert 'notifications/blah' in ret['url']
|
||||
|
||||
|
||||
def test_can_create_spreadsheet_from_large_excel_file():
|
||||
with open(str(Path.cwd() / 'tests' / 'spreadsheet_files' / 'excel 2007.xlsx'), 'rb') as xl:
|
||||
ret = Spreadsheet.from_file(xl, filename='xl.xlsx')
|
||||
assert ret.as_csv_data
|
||||
|
||||
|
||||
def test_can_create_spreadsheet_from_dict():
|
||||
assert Spreadsheet.from_dict(OrderedDict(
|
||||
foo='bar',
|
||||
name='Jane',
|
||||
)).as_csv_data == (
|
||||
"foo,name\r\n"
|
||||
"bar,Jane\r\n"
|
||||
)
|
||||
|
||||
|
||||
def test_can_create_spreadsheet_from_dict_with_filename():
|
||||
assert Spreadsheet.from_dict({}, filename='empty.csv').as_dict['file_name'] == "empty.csv"
|
||||
|
||||
|
||||
@pytest.mark.parametrize('args, kwargs', (
|
||||
(
|
||||
('hello', ['hello']),
|
||||
{},
|
||||
),
|
||||
(
|
||||
(),
|
||||
{'csv_data': 'hello', 'rows': ['hello']}
|
||||
),
|
||||
))
|
||||
def test_spreadsheet_checks_for_bad_arguments(args, kwargs):
|
||||
with pytest.raises(TypeError) as exception:
|
||||
Spreadsheet(*args, **kwargs)
|
||||
assert str(exception.value) == 'Spreadsheet must be created from either rows or CSV data'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('created_by_name, expected_content', [
|
||||
(
|
||||
None, [
|
||||
'Recipient,Reference,Template,Type,Sent by,Sent by email,Job,Status,Time\n',
|
||||
'foo@bar.com,ref 1234,foo,sms,,sender@email.gov.uk,,Delivered,1943-04-19 12:00:00\r\n',
|
||||
]
|
||||
),
|
||||
(
|
||||
'Anne Example', [
|
||||
'Recipient,Reference,Template,Type,Sent by,Sent by email,Job,Status,Time\n',
|
||||
'foo@bar.com,ref 1234,foo,sms,Anne Example,sender@email.gov.uk,,Delivered,1943-04-19 12:00:00\r\n',
|
||||
]
|
||||
),
|
||||
])
|
||||
def test_generate_notifications_csv_without_job(
|
||||
notify_admin,
|
||||
mocker,
|
||||
created_by_name,
|
||||
expected_content,
|
||||
):
|
||||
mocker.patch(
|
||||
'app.notification_api_client.get_notifications_for_service',
|
||||
side_effect=_get_notifications_csv(
|
||||
created_by_name=created_by_name,
|
||||
created_by_email_address="sender@email.gov.uk",
|
||||
job_id=None,
|
||||
job_name=None
|
||||
)
|
||||
)
|
||||
assert list(generate_notifications_csv(service_id=fake_uuid)) == expected_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize('original_file_contents, expected_column_headers, expected_1st_row', [
|
||||
(
|
||||
"""
|
||||
phone_number
|
||||
07700900123
|
||||
""",
|
||||
['Row number', 'phone_number', 'Template', 'Type', 'Job', 'Status', 'Time'],
|
||||
['1', '07700900123', 'foo', 'sms', 'bar.csv', 'Delivered', '1943-04-19 12:00:00'],
|
||||
),
|
||||
(
|
||||
"""
|
||||
phone_number, a, b, c
|
||||
07700900123, 🐜,🐝,🦀
|
||||
""",
|
||||
['Row number', 'phone_number', 'a', 'b', 'c', 'Template', 'Type', 'Job', 'Status', 'Time'],
|
||||
['1', '07700900123', '🐜', '🐝', '🦀', 'foo', 'sms', 'bar.csv', 'Delivered', '1943-04-19 12:00:00'],
|
||||
),
|
||||
(
|
||||
"""
|
||||
"phone_number", "a", "b", "c"
|
||||
"07700900123","🐜,🐜","🐝,🐝","🦀"
|
||||
""",
|
||||
['Row number', 'phone_number', 'a', 'b', 'c', 'Template', 'Type', 'Job', 'Status', 'Time'],
|
||||
['1', '07700900123', '🐜,🐜', '🐝,🐝', '🦀', 'foo', 'sms', 'bar.csv', 'Delivered', '1943-04-19 12:00:00'],
|
||||
),
|
||||
])
|
||||
def test_generate_notifications_csv_returns_correct_csv_file(
|
||||
notify_admin,
|
||||
mocker,
|
||||
_get_notifications_csv_mock,
|
||||
original_file_contents,
|
||||
expected_column_headers,
|
||||
expected_1st_row,
|
||||
):
|
||||
mocker.patch(
|
||||
'app.s3_client.s3_csv_client.s3download',
|
||||
return_value=original_file_contents,
|
||||
)
|
||||
csv_content = generate_notifications_csv(service_id='1234', job_id=fake_uuid, template_type='sms')
|
||||
csv_file = DictReader(StringIO('\n'.join(csv_content)))
|
||||
assert csv_file.fieldnames == expected_column_headers
|
||||
assert next(csv_file) == dict(zip(expected_column_headers, expected_1st_row))
|
||||
|
||||
|
||||
def test_generate_notifications_csv_only_calls_once_if_no_next_link(
|
||||
notify_admin,
|
||||
_get_notifications_csv_mock,
|
||||
):
|
||||
list(generate_notifications_csv(service_id='1234'))
|
||||
|
||||
assert _get_notifications_csv_mock.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("job_id", ["some", None])
|
||||
def test_generate_notifications_csv_calls_twice_if_next_link(
|
||||
notify_admin,
|
||||
mocker,
|
||||
job_id,
|
||||
):
|
||||
|
||||
mocker.patch(
|
||||
'app.s3_client.s3_csv_client.s3download',
|
||||
return_value="""
|
||||
phone_number
|
||||
07700900000
|
||||
07700900001
|
||||
07700900002
|
||||
07700900003
|
||||
07700900004
|
||||
07700900005
|
||||
07700900006
|
||||
07700900007
|
||||
07700900008
|
||||
07700900009
|
||||
"""
|
||||
)
|
||||
|
||||
service_id = '1234'
|
||||
response_with_links = _get_notifications_csv(rows=7, with_links=True)
|
||||
response_with_no_links = _get_notifications_csv(rows=3, row_number=8, with_links=False)
|
||||
|
||||
mock_get_notifications = mocker.patch(
|
||||
'app.notification_api_client.get_notifications_for_service',
|
||||
side_effect=[
|
||||
response_with_links(service_id),
|
||||
response_with_no_links(service_id),
|
||||
]
|
||||
)
|
||||
|
||||
csv_content = generate_notifications_csv(
|
||||
service_id=service_id,
|
||||
job_id=job_id or fake_uuid,
|
||||
template_type='sms',
|
||||
)
|
||||
csv = list(DictReader(StringIO('\n'.join(csv_content))))
|
||||
|
||||
assert len(csv) == 10
|
||||
assert csv[0]['phone_number'] == '07700900000'
|
||||
assert csv[9]['phone_number'] == '07700900009'
|
||||
assert mock_get_notifications.call_count == 2
|
||||
# mock_calls[0][2] is the kwargs from first call
|
||||
assert mock_get_notifications.mock_calls[0][2]['page'] == 1
|
||||
assert mock_get_notifications.mock_calls[1][2]['page'] == 2
|
||||
|
||||
|
||||
def test_get_cdn_domain_on_localhost(client, mocker):
|
||||
mocker.patch.dict('app.current_app.config', values={'ADMIN_BASE_URL': 'http://localhost:6012'})
|
||||
domain = get_logo_cdn_domain()
|
||||
@@ -354,246 +100,6 @@ def test_format_datetime_relative(time, human_readable_datetime):
|
||||
assert format_datetime_relative(time) == human_readable_datetime
|
||||
|
||||
|
||||
@pytest.mark.parametrize('utc_datetime', [
|
||||
'2018-08-01T23:00:00+00:00',
|
||||
'2018-08-01T16:29:00+00:00',
|
||||
'2018-11-01T00:00:00+00:00',
|
||||
'2018-11-01T10:00:00+00:00',
|
||||
'2018-11-01T17:29:00+00:00',
|
||||
])
|
||||
def test_printing_today_or_tomorrow_returns_today(utc_datetime):
|
||||
with freeze_time(utc_datetime):
|
||||
assert printing_today_or_tomorrow(utc_datetime) == 'today'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('utc_datetime', [
|
||||
'2018-08-01T22:59:00+00:00',
|
||||
'2018-08-01T16:30:00+00:00',
|
||||
'2018-11-01T17:30:00+00:00',
|
||||
'2018-11-01T21:00:00+00:00',
|
||||
'2018-11-01T23:59:00+00:00',
|
||||
])
|
||||
def test_printing_today_or_tomorrow_returns_tomorrow(utc_datetime):
|
||||
with freeze_time(utc_datetime):
|
||||
assert printing_today_or_tomorrow(utc_datetime) == 'tomorrow'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('created_at, current_datetime', [
|
||||
('2017-07-07T12:00:00+00:00', '2017-07-07 16:29:00'), # created today, summer
|
||||
('2017-07-06T23:30:00+00:00', '2017-07-07 16:29:00'), # created just after midnight, summer
|
||||
('2017-12-12T12:00:00+00:00', '2017-12-12 17:29:00'), # created today, winter
|
||||
('2017-12-12T21:30:00+00:00', '2017-12-13 17:29:00'), # created after 5:30 yesterday
|
||||
('2017-03-25T17:31:00+00:00', '2017-03-26 16:29:00'), # over clock change period on 2017-03-26
|
||||
])
|
||||
def test_get_letter_printing_statement_when_letter_prints_today(created_at, current_datetime):
|
||||
with freeze_time(current_datetime):
|
||||
statement = get_letter_printing_statement('created', created_at)
|
||||
|
||||
assert statement == 'Printing starts today at 5:30pm'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('created_at, current_datetime', [
|
||||
('2017-07-07T16:31:00+00:00', '2017-07-07 22:59:00'), # created today, summer
|
||||
('2017-12-12T17:31:00+00:00', '2017-12-12 23:59:00'), # created today, winter
|
||||
])
|
||||
def test_get_letter_printing_statement_when_letter_prints_tomorrow(created_at, current_datetime):
|
||||
with freeze_time(current_datetime):
|
||||
statement = get_letter_printing_statement('created', created_at)
|
||||
|
||||
assert statement == 'Printing starts tomorrow at 5:30pm'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('created_at, print_day', [
|
||||
('2017-07-06T16:29:00+00:00', 'yesterday'),
|
||||
('2017-12-01T00:00:00+00:00', 'on 1 December'),
|
||||
('2017-03-26T12:00:00+00:00', 'on 26 March'),
|
||||
])
|
||||
@freeze_time('2017-07-07 12:00:00')
|
||||
def test_get_letter_printing_statement_for_letter_that_has_been_sent(created_at, print_day):
|
||||
statement = get_letter_printing_statement('delivered', created_at)
|
||||
|
||||
assert statement == 'Printed {} at 5:30pm'.format(print_day)
|
||||
|
||||
|
||||
def test_get_letter_validation_error_for_unknown_error():
|
||||
assert get_letter_validation_error('Unknown error') == {
|
||||
'title': 'Validation failed'
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('error_message, invalid_pages, expected_title, expected_content, expected_summary', [
|
||||
(
|
||||
'letter-not-a4-portrait-oriented',
|
||||
[2],
|
||||
'Your letter is not A4 portrait size',
|
||||
(
|
||||
'You need to change the size or orientation of page 2. '
|
||||
'Files must meet our letter specification.'
|
||||
),
|
||||
(
|
||||
'Validation failed because page 2 is not A4 portrait size.'
|
||||
'Files must meet our letter specification.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'letter-not-a4-portrait-oriented',
|
||||
[2, 3, 4],
|
||||
'Your letter is not A4 portrait size',
|
||||
(
|
||||
'You need to change the size or orientation of pages 2, 3 and 4. '
|
||||
'Files must meet our letter specification.'
|
||||
),
|
||||
(
|
||||
'Validation failed because pages 2, 3 and 4 are not A4 portrait size.'
|
||||
'Files must meet our letter specification.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'content-outside-printable-area',
|
||||
[2],
|
||||
'Your content is outside the printable area',
|
||||
(
|
||||
'You need to edit page 2.'
|
||||
'Files must meet our letter specification.'
|
||||
),
|
||||
(
|
||||
'Validation failed because content is outside the printable area '
|
||||
'on page 2.'
|
||||
'Files must meet our letter specification.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'letter-too-long',
|
||||
None,
|
||||
'Your letter is too long',
|
||||
(
|
||||
'Letters must be 10 pages or less (5 double-sided sheets of paper). '
|
||||
'Your letter is 13 pages long.'
|
||||
),
|
||||
(
|
||||
'Validation failed because this letter is 13 pages long.'
|
||||
'Letters must be 10 pages or less (5 double-sided sheets of paper).'
|
||||
),
|
||||
),
|
||||
(
|
||||
'unable-to-read-the-file',
|
||||
None,
|
||||
'There’s a problem with your file',
|
||||
(
|
||||
'Notify cannot read this PDF.'
|
||||
'Save a new copy of your file and try again.'
|
||||
),
|
||||
(
|
||||
'Validation failed because Notify cannot read this PDF.'
|
||||
'Save a new copy of your file and try again.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'address-is-empty',
|
||||
None,
|
||||
'The address block is empty',
|
||||
(
|
||||
'You need to add a recipient address.'
|
||||
'Files must meet our letter specification.'
|
||||
),
|
||||
(
|
||||
'Validation failed because the address block is empty.'
|
||||
'Files must meet our letter specification.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'not-a-real-uk-postcode',
|
||||
None,
|
||||
'There’s a problem with the address for this letter',
|
||||
(
|
||||
'The last line of the address must be a real UK postcode.'
|
||||
),
|
||||
(
|
||||
'Validation failed because the last line of the address is not a real UK postcode.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'cant-send-international-letters',
|
||||
None,
|
||||
'There’s a problem with the address for this letter',
|
||||
(
|
||||
'You do not have permission to send letters to other countries.'
|
||||
),
|
||||
(
|
||||
'Validation failed because your service cannot send letters to other countries.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'not-a-real-uk-postcode-or-country',
|
||||
None,
|
||||
'There’s a problem with the address for this letter',
|
||||
(
|
||||
'The last line of the address must be a UK postcode or '
|
||||
'another country.'
|
||||
),
|
||||
(
|
||||
'Validation failed because the last line of the address is '
|
||||
'not a UK postcode or another country.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'not-enough-address-lines',
|
||||
None,
|
||||
'There’s a problem with the address for this letter',
|
||||
(
|
||||
'The address must be at least 3 lines long.'
|
||||
),
|
||||
(
|
||||
'Validation failed because the address must be at least 3 lines long.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'too-many-address-lines',
|
||||
None,
|
||||
'There’s a problem with the address for this letter',
|
||||
(
|
||||
'The address must be no more than 7 lines long.'
|
||||
),
|
||||
(
|
||||
'Validation failed because the address must be no more than 7 lines long.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'invalid-char-in-address',
|
||||
None,
|
||||
'There’s a problem with the address for this letter',
|
||||
(
|
||||
'Address lines must not start with any of the following characters: @ ( ) = [ ] ” \\ / , < > ~'
|
||||
),
|
||||
(
|
||||
'Validation failed because address lines must not start with any of the following '
|
||||
'characters: @ ( ) = [ ] ” \\ / , < > ~'
|
||||
),
|
||||
),
|
||||
])
|
||||
def test_get_letter_validation_error_for_known_errors(
|
||||
client_request,
|
||||
error_message,
|
||||
invalid_pages,
|
||||
expected_title,
|
||||
expected_content,
|
||||
expected_summary,
|
||||
):
|
||||
error = get_letter_validation_error(error_message, invalid_pages=invalid_pages, page_count=13)
|
||||
detail = BeautifulSoup(error['detail'], 'html.parser')
|
||||
summary = BeautifulSoup(error['summary'], 'html.parser')
|
||||
|
||||
assert error['title'] == expected_title
|
||||
|
||||
assert detail.text == expected_content
|
||||
if detail.select_one('a'):
|
||||
assert detail.select_one('a')['href'] == url_for('.letter_specification')
|
||||
|
||||
assert summary.text == expected_summary
|
||||
if summary.select_one('a'):
|
||||
assert summary.select_one('a')['href'] == url_for('.letter_specification')
|
||||
|
||||
|
||||
@pytest.mark.parametrize("date_from_db, expected_result", [
|
||||
('2019-11-17T11:35:21.726132Z', True),
|
||||
('2019-11-16T11:35:21.726132Z', False),
|
||||
@@ -604,12 +110,6 @@ 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("template_type", ["sms", "letter", "email"])
|
||||
def test_get_sample_template_returns_template(template_type):
|
||||
template = get_sample_template(template_type)
|
||||
assert isinstance(template, Template)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source_object, destination_object, expected_result", [
|
||||
# simple dicts:
|
||||
({"a": "b"}, {"c": "d"}, {"a": "b", "c": "d"}),
|
||||
|
||||
0
tests/app/utils/__init__.py
Normal file
0
tests/app/utils/__init__.py
Normal file
361
tests/app/utils/test_csv.py
Normal file
361
tests/app/utils/test_csv.py
Normal file
@@ -0,0 +1,361 @@
|
||||
from collections import OrderedDict, namedtuple
|
||||
from csv import DictReader
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.utils.csv import (
|
||||
Spreadsheet,
|
||||
generate_notifications_csv,
|
||||
get_errors_for_csv,
|
||||
)
|
||||
from tests.conftest import fake_uuid
|
||||
|
||||
|
||||
def _get_notifications_csv(
|
||||
row_number=1,
|
||||
recipient='foo@bar.com',
|
||||
template_name='foo',
|
||||
template_type='sms',
|
||||
job_name='bar.csv',
|
||||
status='Delivered',
|
||||
created_at='1943-04-19 12:00:00',
|
||||
rows=1,
|
||||
with_links=False,
|
||||
job_id=fake_uuid,
|
||||
created_by_name=None,
|
||||
created_by_email_address=None,
|
||||
):
|
||||
|
||||
def _get(
|
||||
service_id,
|
||||
page=1,
|
||||
job_id=None,
|
||||
template_type=template_type,
|
||||
):
|
||||
links = {}
|
||||
if with_links:
|
||||
links = {
|
||||
'prev': '/service/{}/notifications?page=0'.format(service_id),
|
||||
'next': '/service/{}/notifications?page=1'.format(service_id),
|
||||
'last': '/service/{}/notifications?page=2'.format(service_id)
|
||||
}
|
||||
|
||||
data = {
|
||||
'notifications': [{
|
||||
"row_number": row_number + i,
|
||||
"to": recipient,
|
||||
"recipient": recipient,
|
||||
"client_reference": 'ref 1234',
|
||||
"template_name": template_name,
|
||||
"template_type": template_type,
|
||||
"template": {"name": template_name, "template_type": template_type},
|
||||
"job_name": job_name,
|
||||
"status": status,
|
||||
"created_at": created_at,
|
||||
"updated_at": None,
|
||||
"created_by_name": created_by_name,
|
||||
"created_by_email_address": created_by_email_address,
|
||||
} for i in range(rows)],
|
||||
'total': rows,
|
||||
'page_size': 50,
|
||||
'links': links
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
return _get
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def _get_notifications_csv_mock(
|
||||
mocker,
|
||||
api_user_active,
|
||||
):
|
||||
return mocker.patch(
|
||||
'app.notification_api_client.get_notifications_for_service',
|
||||
side_effect=_get_notifications_csv()
|
||||
)
|
||||
|
||||
|
||||
def test_can_create_spreadsheet_from_large_excel_file():
|
||||
with open(str(Path.cwd() / 'tests' / 'spreadsheet_files' / 'excel 2007.xlsx'), 'rb') as xl:
|
||||
ret = Spreadsheet.from_file(xl, filename='xl.xlsx')
|
||||
assert ret.as_csv_data
|
||||
|
||||
|
||||
def test_can_create_spreadsheet_from_dict():
|
||||
assert Spreadsheet.from_dict(OrderedDict(
|
||||
foo='bar',
|
||||
name='Jane',
|
||||
)).as_csv_data == (
|
||||
"foo,name\r\n"
|
||||
"bar,Jane\r\n"
|
||||
)
|
||||
|
||||
|
||||
def test_can_create_spreadsheet_from_dict_with_filename():
|
||||
assert Spreadsheet.from_dict({}, filename='empty.csv').as_dict['file_name'] == "empty.csv"
|
||||
|
||||
|
||||
@pytest.mark.parametrize('args, kwargs', (
|
||||
(
|
||||
('hello', ['hello']),
|
||||
{},
|
||||
),
|
||||
(
|
||||
(),
|
||||
{'csv_data': 'hello', 'rows': ['hello']}
|
||||
),
|
||||
))
|
||||
def test_spreadsheet_checks_for_bad_arguments(args, kwargs):
|
||||
with pytest.raises(TypeError) as exception:
|
||||
Spreadsheet(*args, **kwargs)
|
||||
assert str(exception.value) == 'Spreadsheet must be created from either rows or CSV data'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('created_by_name, expected_content', [
|
||||
(
|
||||
None, [
|
||||
'Recipient,Reference,Template,Type,Sent by,Sent by email,Job,Status,Time\n',
|
||||
'foo@bar.com,ref 1234,foo,sms,,sender@email.gov.uk,,Delivered,1943-04-19 12:00:00\r\n',
|
||||
]
|
||||
),
|
||||
(
|
||||
'Anne Example', [
|
||||
'Recipient,Reference,Template,Type,Sent by,Sent by email,Job,Status,Time\n',
|
||||
'foo@bar.com,ref 1234,foo,sms,Anne Example,sender@email.gov.uk,,Delivered,1943-04-19 12:00:00\r\n',
|
||||
]
|
||||
),
|
||||
])
|
||||
def test_generate_notifications_csv_without_job(
|
||||
notify_admin,
|
||||
mocker,
|
||||
created_by_name,
|
||||
expected_content,
|
||||
):
|
||||
mocker.patch(
|
||||
'app.notification_api_client.get_notifications_for_service',
|
||||
side_effect=_get_notifications_csv(
|
||||
created_by_name=created_by_name,
|
||||
created_by_email_address="sender@email.gov.uk",
|
||||
job_id=None,
|
||||
job_name=None
|
||||
)
|
||||
)
|
||||
assert list(generate_notifications_csv(service_id=fake_uuid)) == expected_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize('original_file_contents, expected_column_headers, expected_1st_row', [
|
||||
(
|
||||
"""
|
||||
phone_number
|
||||
07700900123
|
||||
""",
|
||||
['Row number', 'phone_number', 'Template', 'Type', 'Job', 'Status', 'Time'],
|
||||
['1', '07700900123', 'foo', 'sms', 'bar.csv', 'Delivered', '1943-04-19 12:00:00'],
|
||||
),
|
||||
(
|
||||
"""
|
||||
phone_number, a, b, c
|
||||
07700900123, 🐜,🐝,🦀
|
||||
""",
|
||||
['Row number', 'phone_number', 'a', 'b', 'c', 'Template', 'Type', 'Job', 'Status', 'Time'],
|
||||
['1', '07700900123', '🐜', '🐝', '🦀', 'foo', 'sms', 'bar.csv', 'Delivered', '1943-04-19 12:00:00'],
|
||||
),
|
||||
(
|
||||
"""
|
||||
"phone_number", "a", "b", "c"
|
||||
"07700900123","🐜,🐜","🐝,🐝","🦀"
|
||||
""",
|
||||
['Row number', 'phone_number', 'a', 'b', 'c', 'Template', 'Type', 'Job', 'Status', 'Time'],
|
||||
['1', '07700900123', '🐜,🐜', '🐝,🐝', '🦀', 'foo', 'sms', 'bar.csv', 'Delivered', '1943-04-19 12:00:00'],
|
||||
),
|
||||
])
|
||||
def test_generate_notifications_csv_returns_correct_csv_file(
|
||||
notify_admin,
|
||||
mocker,
|
||||
_get_notifications_csv_mock,
|
||||
original_file_contents,
|
||||
expected_column_headers,
|
||||
expected_1st_row,
|
||||
):
|
||||
mocker.patch(
|
||||
'app.s3_client.s3_csv_client.s3download',
|
||||
return_value=original_file_contents,
|
||||
)
|
||||
csv_content = generate_notifications_csv(service_id='1234', job_id=fake_uuid, template_type='sms')
|
||||
csv_file = DictReader(StringIO('\n'.join(csv_content)))
|
||||
assert csv_file.fieldnames == expected_column_headers
|
||||
assert next(csv_file) == dict(zip(expected_column_headers, expected_1st_row))
|
||||
|
||||
|
||||
def test_generate_notifications_csv_only_calls_once_if_no_next_link(
|
||||
notify_admin,
|
||||
_get_notifications_csv_mock,
|
||||
):
|
||||
list(generate_notifications_csv(service_id='1234'))
|
||||
|
||||
assert _get_notifications_csv_mock.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("job_id", ["some", None])
|
||||
def test_generate_notifications_csv_calls_twice_if_next_link(
|
||||
notify_admin,
|
||||
mocker,
|
||||
job_id,
|
||||
):
|
||||
|
||||
mocker.patch(
|
||||
'app.s3_client.s3_csv_client.s3download',
|
||||
return_value="""
|
||||
phone_number
|
||||
07700900000
|
||||
07700900001
|
||||
07700900002
|
||||
07700900003
|
||||
07700900004
|
||||
07700900005
|
||||
07700900006
|
||||
07700900007
|
||||
07700900008
|
||||
07700900009
|
||||
"""
|
||||
)
|
||||
|
||||
service_id = '1234'
|
||||
response_with_links = _get_notifications_csv(rows=7, with_links=True)
|
||||
response_with_no_links = _get_notifications_csv(rows=3, row_number=8, with_links=False)
|
||||
|
||||
mock_get_notifications = mocker.patch(
|
||||
'app.notification_api_client.get_notifications_for_service',
|
||||
side_effect=[
|
||||
response_with_links(service_id),
|
||||
response_with_no_links(service_id),
|
||||
]
|
||||
)
|
||||
|
||||
csv_content = generate_notifications_csv(
|
||||
service_id=service_id,
|
||||
job_id=job_id or fake_uuid,
|
||||
template_type='sms',
|
||||
)
|
||||
csv = list(DictReader(StringIO('\n'.join(csv_content))))
|
||||
|
||||
assert len(csv) == 10
|
||||
assert csv[0]['phone_number'] == '07700900000'
|
||||
assert csv[9]['phone_number'] == '07700900009'
|
||||
assert mock_get_notifications.call_count == 2
|
||||
# mock_calls[0][2] is the kwargs from first call
|
||||
assert mock_get_notifications.mock_calls[0][2]['page'] == 1
|
||||
assert mock_get_notifications.mock_calls[1][2]['page'] == 2
|
||||
|
||||
|
||||
MockRecipients = namedtuple(
|
||||
'RecipientCSV',
|
||||
[
|
||||
'rows_with_bad_recipients',
|
||||
'rows_with_missing_data',
|
||||
'rows_with_message_too_long',
|
||||
'rows_with_empty_message'
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"rows_with_bad_recipients, rows_with_missing_data, "
|
||||
"rows_with_message_too_long, rows_with_empty_message, template_type, expected_errors",
|
||||
[
|
||||
(
|
||||
[], [], [], [],
|
||||
'sms',
|
||||
[]
|
||||
),
|
||||
(
|
||||
{2}, [], [], [],
|
||||
'sms',
|
||||
['fix 1 phone number']
|
||||
),
|
||||
(
|
||||
{2, 4, 6}, [], [], [],
|
||||
'sms',
|
||||
['fix 3 phone numbers']
|
||||
),
|
||||
(
|
||||
{1}, [], [], [],
|
||||
'email',
|
||||
['fix 1 email address']
|
||||
),
|
||||
(
|
||||
{2, 4, 6}, [], [], [],
|
||||
'email',
|
||||
['fix 3 email addresses']
|
||||
),
|
||||
(
|
||||
{2}, [], [], [],
|
||||
'letter',
|
||||
['fix 1 address']
|
||||
),
|
||||
(
|
||||
{2, 4}, [], [], [],
|
||||
'letter',
|
||||
['fix 2 addresses']
|
||||
),
|
||||
(
|
||||
{2}, {3}, [], [],
|
||||
'sms',
|
||||
[
|
||||
'fix 1 phone number',
|
||||
'enter missing data in 1 row'
|
||||
]
|
||||
),
|
||||
(
|
||||
{2, 4, 6, 8}, {3, 6, 9, 12}, [], [],
|
||||
'sms',
|
||||
[
|
||||
'fix 4 phone numbers',
|
||||
'enter missing data in 4 rows'
|
||||
]
|
||||
),
|
||||
(
|
||||
{}, {}, {3}, [],
|
||||
'sms',
|
||||
[
|
||||
'shorten the message in 1 row'
|
||||
]
|
||||
),
|
||||
(
|
||||
{}, {}, {3, 12}, [],
|
||||
'sms',
|
||||
[
|
||||
'shorten the messages in 2 rows'
|
||||
]
|
||||
),
|
||||
(
|
||||
{}, {}, {}, {2},
|
||||
'sms',
|
||||
[
|
||||
'check you have content for the empty message in 1 row'
|
||||
]
|
||||
),
|
||||
(
|
||||
{}, {}, {}, {2, 4, 8},
|
||||
'sms',
|
||||
[
|
||||
'check you have content for the empty messages in 3 rows'
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
def test_get_errors_for_csv(
|
||||
rows_with_bad_recipients, rows_with_missing_data, rows_with_message_too_long, rows_with_empty_message,
|
||||
template_type,
|
||||
expected_errors
|
||||
):
|
||||
assert get_errors_for_csv(
|
||||
MockRecipients(
|
||||
rows_with_bad_recipients, rows_with_missing_data, rows_with_message_too_long, rows_with_empty_message
|
||||
),
|
||||
template_type
|
||||
) == expected_errors
|
||||
250
tests/app/utils/test_letters.py
Normal file
250
tests/app/utils/test_letters.py
Normal file
@@ -0,0 +1,250 @@
|
||||
import pytest
|
||||
from bs4 import BeautifulSoup
|
||||
from flask import url_for
|
||||
from freezegun import freeze_time
|
||||
|
||||
from app.utils.letters import (
|
||||
get_letter_printing_statement,
|
||||
get_letter_validation_error,
|
||||
printing_today_or_tomorrow,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('utc_datetime', [
|
||||
'2018-08-01T23:00:00+00:00',
|
||||
'2018-08-01T16:29:00+00:00',
|
||||
'2018-11-01T00:00:00+00:00',
|
||||
'2018-11-01T10:00:00+00:00',
|
||||
'2018-11-01T17:29:00+00:00',
|
||||
])
|
||||
def test_printing_today_or_tomorrow_returns_today(utc_datetime):
|
||||
with freeze_time(utc_datetime):
|
||||
assert printing_today_or_tomorrow(utc_datetime) == 'today'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('utc_datetime', [
|
||||
'2018-08-01T22:59:00+00:00',
|
||||
'2018-08-01T16:30:00+00:00',
|
||||
'2018-11-01T17:30:00+00:00',
|
||||
'2018-11-01T21:00:00+00:00',
|
||||
'2018-11-01T23:59:00+00:00',
|
||||
])
|
||||
def test_printing_today_or_tomorrow_returns_tomorrow(utc_datetime):
|
||||
with freeze_time(utc_datetime):
|
||||
assert printing_today_or_tomorrow(utc_datetime) == 'tomorrow'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('created_at, current_datetime', [
|
||||
('2017-07-07T12:00:00+00:00', '2017-07-07 16:29:00'), # created today, summer
|
||||
('2017-07-06T23:30:00+00:00', '2017-07-07 16:29:00'), # created just after midnight, summer
|
||||
('2017-12-12T12:00:00+00:00', '2017-12-12 17:29:00'), # created today, winter
|
||||
('2017-12-12T21:30:00+00:00', '2017-12-13 17:29:00'), # created after 5:30 yesterday
|
||||
('2017-03-25T17:31:00+00:00', '2017-03-26 16:29:00'), # over clock change period on 2017-03-26
|
||||
])
|
||||
def test_get_letter_printing_statement_when_letter_prints_today(created_at, current_datetime):
|
||||
with freeze_time(current_datetime):
|
||||
statement = get_letter_printing_statement('created', created_at)
|
||||
|
||||
assert statement == 'Printing starts today at 5:30pm'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('created_at, current_datetime', [
|
||||
('2017-07-07T16:31:00+00:00', '2017-07-07 22:59:00'), # created today, summer
|
||||
('2017-12-12T17:31:00+00:00', '2017-12-12 23:59:00'), # created today, winter
|
||||
])
|
||||
def test_get_letter_printing_statement_when_letter_prints_tomorrow(created_at, current_datetime):
|
||||
with freeze_time(current_datetime):
|
||||
statement = get_letter_printing_statement('created', created_at)
|
||||
|
||||
assert statement == 'Printing starts tomorrow at 5:30pm'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('created_at, print_day', [
|
||||
('2017-07-06T16:29:00+00:00', 'yesterday'),
|
||||
('2017-12-01T00:00:00+00:00', 'on 1 December'),
|
||||
('2017-03-26T12:00:00+00:00', 'on 26 March'),
|
||||
])
|
||||
@freeze_time('2017-07-07 12:00:00')
|
||||
def test_get_letter_printing_statement_for_letter_that_has_been_sent(created_at, print_day):
|
||||
statement = get_letter_printing_statement('delivered', created_at)
|
||||
|
||||
assert statement == 'Printed {} at 5:30pm'.format(print_day)
|
||||
|
||||
|
||||
def test_get_letter_validation_error_for_unknown_error():
|
||||
assert get_letter_validation_error('Unknown error') == {
|
||||
'title': 'Validation failed'
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('error_message, invalid_pages, expected_title, expected_content, expected_summary', [
|
||||
(
|
||||
'letter-not-a4-portrait-oriented',
|
||||
[2],
|
||||
'Your letter is not A4 portrait size',
|
||||
(
|
||||
'You need to change the size or orientation of page 2. '
|
||||
'Files must meet our letter specification.'
|
||||
),
|
||||
(
|
||||
'Validation failed because page 2 is not A4 portrait size.'
|
||||
'Files must meet our letter specification.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'letter-not-a4-portrait-oriented',
|
||||
[2, 3, 4],
|
||||
'Your letter is not A4 portrait size',
|
||||
(
|
||||
'You need to change the size or orientation of pages 2, 3 and 4. '
|
||||
'Files must meet our letter specification.'
|
||||
),
|
||||
(
|
||||
'Validation failed because pages 2, 3 and 4 are not A4 portrait size.'
|
||||
'Files must meet our letter specification.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'content-outside-printable-area',
|
||||
[2],
|
||||
'Your content is outside the printable area',
|
||||
(
|
||||
'You need to edit page 2.'
|
||||
'Files must meet our letter specification.'
|
||||
),
|
||||
(
|
||||
'Validation failed because content is outside the printable area '
|
||||
'on page 2.'
|
||||
'Files must meet our letter specification.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'letter-too-long',
|
||||
None,
|
||||
'Your letter is too long',
|
||||
(
|
||||
'Letters must be 10 pages or less (5 double-sided sheets of paper). '
|
||||
'Your letter is 13 pages long.'
|
||||
),
|
||||
(
|
||||
'Validation failed because this letter is 13 pages long.'
|
||||
'Letters must be 10 pages or less (5 double-sided sheets of paper).'
|
||||
),
|
||||
),
|
||||
(
|
||||
'unable-to-read-the-file',
|
||||
None,
|
||||
'There’s a problem with your file',
|
||||
(
|
||||
'Notify cannot read this PDF.'
|
||||
'Save a new copy of your file and try again.'
|
||||
),
|
||||
(
|
||||
'Validation failed because Notify cannot read this PDF.'
|
||||
'Save a new copy of your file and try again.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'address-is-empty',
|
||||
None,
|
||||
'The address block is empty',
|
||||
(
|
||||
'You need to add a recipient address.'
|
||||
'Files must meet our letter specification.'
|
||||
),
|
||||
(
|
||||
'Validation failed because the address block is empty.'
|
||||
'Files must meet our letter specification.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'not-a-real-uk-postcode',
|
||||
None,
|
||||
'There’s a problem with the address for this letter',
|
||||
(
|
||||
'The last line of the address must be a real UK postcode.'
|
||||
),
|
||||
(
|
||||
'Validation failed because the last line of the address is not a real UK postcode.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'cant-send-international-letters',
|
||||
None,
|
||||
'There’s a problem with the address for this letter',
|
||||
(
|
||||
'You do not have permission to send letters to other countries.'
|
||||
),
|
||||
(
|
||||
'Validation failed because your service cannot send letters to other countries.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'not-a-real-uk-postcode-or-country',
|
||||
None,
|
||||
'There’s a problem with the address for this letter',
|
||||
(
|
||||
'The last line of the address must be a UK postcode or '
|
||||
'another country.'
|
||||
),
|
||||
(
|
||||
'Validation failed because the last line of the address is '
|
||||
'not a UK postcode or another country.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'not-enough-address-lines',
|
||||
None,
|
||||
'There’s a problem with the address for this letter',
|
||||
(
|
||||
'The address must be at least 3 lines long.'
|
||||
),
|
||||
(
|
||||
'Validation failed because the address must be at least 3 lines long.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'too-many-address-lines',
|
||||
None,
|
||||
'There’s a problem with the address for this letter',
|
||||
(
|
||||
'The address must be no more than 7 lines long.'
|
||||
),
|
||||
(
|
||||
'Validation failed because the address must be no more than 7 lines long.'
|
||||
),
|
||||
),
|
||||
(
|
||||
'invalid-char-in-address',
|
||||
None,
|
||||
'There’s a problem with the address for this letter',
|
||||
(
|
||||
'Address lines must not start with any of the following characters: @ ( ) = [ ] ” \\ / , < > ~'
|
||||
),
|
||||
(
|
||||
'Validation failed because address lines must not start with any of the following '
|
||||
'characters: @ ( ) = [ ] ” \\ / , < > ~'
|
||||
),
|
||||
),
|
||||
])
|
||||
def test_get_letter_validation_error_for_known_errors(
|
||||
client_request,
|
||||
error_message,
|
||||
invalid_pages,
|
||||
expected_title,
|
||||
expected_content,
|
||||
expected_summary,
|
||||
):
|
||||
error = get_letter_validation_error(error_message, invalid_pages=invalid_pages, page_count=13)
|
||||
detail = BeautifulSoup(error['detail'], 'html.parser')
|
||||
summary = BeautifulSoup(error['summary'], 'html.parser')
|
||||
|
||||
assert error['title'] == expected_title
|
||||
|
||||
assert detail.text == expected_content
|
||||
if detail.select_one('a'):
|
||||
assert detail.select_one('a')['href'] == url_for('.letter_specification')
|
||||
|
||||
assert summary.text == expected_summary
|
||||
if summary.select_one('a'):
|
||||
assert summary.select_one('a')['href'] == url_for('.letter_specification')
|
||||
10
tests/app/utils/test_templates.py
Normal file
10
tests/app/utils/test_templates.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import pytest
|
||||
from notifications_utils.template import Template
|
||||
|
||||
from app.utils.templates import get_sample_template
|
||||
|
||||
|
||||
@pytest.mark.parametrize("template_type", ["sms", "letter", "email"])
|
||||
def test_get_sample_template_returns_template(template_type):
|
||||
template = get_sample_template(template_type)
|
||||
assert isinstance(template, Template)
|
||||
245
tests/app/utils/test_user.py
Normal file
245
tests/app/utils/test_user.py
Normal file
@@ -0,0 +1,245 @@
|
||||
import pytest
|
||||
from flask import request
|
||||
from werkzeug.exceptions import Forbidden, Unauthorized
|
||||
|
||||
from app.main.views.index import index
|
||||
from app.utils.user import user_has_permissions
|
||||
|
||||
|
||||
def _test_permissions(
|
||||
client,
|
||||
usr,
|
||||
permissions,
|
||||
will_succeed,
|
||||
kwargs=None,
|
||||
):
|
||||
request.view_args.update({'service_id': 'foo'})
|
||||
if usr:
|
||||
client.login(usr)
|
||||
|
||||
decorator = user_has_permissions(*permissions, **(kwargs or {}))
|
||||
decorated_index = decorator(index)
|
||||
|
||||
if will_succeed:
|
||||
decorated_index()
|
||||
else:
|
||||
try:
|
||||
if (
|
||||
decorated_index().location != '/sign-in?next=%2F' or
|
||||
decorated_index().status_code != 302
|
||||
):
|
||||
pytest.fail("Failed to throw a forbidden or unauthorised exception")
|
||||
except (Forbidden, Unauthorized):
|
||||
pass
|
||||
|
||||
|
||||
def test_user_has_permissions_on_endpoint_fail(
|
||||
client,
|
||||
mocker,
|
||||
mock_get_service,
|
||||
):
|
||||
user = _user_with_permissions()
|
||||
mocker.patch('app.user_api_client.get_user', return_value=user)
|
||||
_test_permissions(
|
||||
client,
|
||||
user,
|
||||
['send_messages'],
|
||||
will_succeed=False)
|
||||
|
||||
|
||||
def test_user_has_permissions_success(
|
||||
client,
|
||||
mocker,
|
||||
):
|
||||
user = _user_with_permissions()
|
||||
mocker.patch('app.user_api_client.get_user', return_value=user)
|
||||
_test_permissions(
|
||||
client,
|
||||
user,
|
||||
['manage_service'],
|
||||
will_succeed=True)
|
||||
|
||||
|
||||
def test_user_has_permissions_or(
|
||||
client,
|
||||
mocker,
|
||||
):
|
||||
user = _user_with_permissions()
|
||||
mocker.patch('app.user_api_client.get_user', return_value=user)
|
||||
_test_permissions(
|
||||
client,
|
||||
user,
|
||||
['send_messages', 'manage_service'],
|
||||
will_succeed=True)
|
||||
|
||||
|
||||
def test_user_has_permissions_multiple(
|
||||
client,
|
||||
mocker,
|
||||
):
|
||||
user = _user_with_permissions()
|
||||
mocker.patch('app.user_api_client.get_user', return_value=user)
|
||||
_test_permissions(
|
||||
client,
|
||||
user,
|
||||
['manage_templates', 'manage_service'],
|
||||
will_succeed=True)
|
||||
|
||||
|
||||
def test_exact_permissions(
|
||||
client,
|
||||
mocker,
|
||||
):
|
||||
user = _user_with_permissions()
|
||||
mocker.patch('app.user_api_client.get_user', return_value=user)
|
||||
_test_permissions(
|
||||
client,
|
||||
user,
|
||||
['manage_service', 'manage_templates'],
|
||||
will_succeed=True)
|
||||
|
||||
|
||||
def test_platform_admin_user_can_access_page_that_has_no_permissions(
|
||||
client,
|
||||
platform_admin_user,
|
||||
mocker,
|
||||
):
|
||||
mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
|
||||
_test_permissions(
|
||||
client,
|
||||
platform_admin_user,
|
||||
[],
|
||||
will_succeed=True)
|
||||
|
||||
|
||||
def test_platform_admin_user_can_not_access_page(
|
||||
client,
|
||||
platform_admin_user,
|
||||
mocker,
|
||||
mock_get_service,
|
||||
):
|
||||
mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
|
||||
_test_permissions(
|
||||
client,
|
||||
platform_admin_user,
|
||||
[],
|
||||
will_succeed=False,
|
||||
kwargs={'restrict_admin_usage': True})
|
||||
|
||||
|
||||
def test_no_user_returns_401_unauth(
|
||||
client
|
||||
):
|
||||
from flask_login import current_user
|
||||
assert not current_user.is_authenticated
|
||||
_test_permissions(
|
||||
client,
|
||||
None,
|
||||
[],
|
||||
will_succeed=False)
|
||||
|
||||
|
||||
def test_user_has_permissions_for_organisation(
|
||||
client,
|
||||
mocker,
|
||||
):
|
||||
user = _user_with_permissions()
|
||||
user['organisations'] = ['org_1', 'org_2']
|
||||
mocker.patch('app.user_api_client.get_user', return_value=user)
|
||||
client.login(user)
|
||||
|
||||
request.view_args = {'org_id': 'org_2'}
|
||||
|
||||
@user_has_permissions()
|
||||
def index():
|
||||
pass
|
||||
|
||||
index()
|
||||
|
||||
|
||||
def test_platform_admin_can_see_orgs_they_dont_have(
|
||||
client,
|
||||
platform_admin_user,
|
||||
mocker,
|
||||
):
|
||||
platform_admin_user['organisations'] = []
|
||||
mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
|
||||
client.login(platform_admin_user)
|
||||
|
||||
request.view_args = {'org_id': 'org_2'}
|
||||
|
||||
@user_has_permissions()
|
||||
def index():
|
||||
pass
|
||||
|
||||
index()
|
||||
|
||||
|
||||
def test_cant_use_decorator_without_view_args(
|
||||
client,
|
||||
platform_admin_user,
|
||||
mocker,
|
||||
):
|
||||
mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
|
||||
client.login(platform_admin_user)
|
||||
|
||||
request.view_args = {}
|
||||
|
||||
@user_has_permissions()
|
||||
def index():
|
||||
pass
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
index()
|
||||
|
||||
|
||||
def test_user_doesnt_have_permissions_for_organisation(
|
||||
client,
|
||||
mocker,
|
||||
):
|
||||
user = _user_with_permissions()
|
||||
user['organisations'] = ['org_1', 'org_2']
|
||||
mocker.patch('app.user_api_client.get_user', return_value=user)
|
||||
client.login(user)
|
||||
|
||||
request.view_args = {'org_id': 'org_3'}
|
||||
|
||||
@user_has_permissions()
|
||||
def index():
|
||||
pass
|
||||
|
||||
with pytest.raises(Forbidden):
|
||||
index()
|
||||
|
||||
|
||||
def test_user_with_no_permissions_to_service_goes_to_templates(
|
||||
client,
|
||||
mocker
|
||||
):
|
||||
user = _user_with_permissions()
|
||||
mocker.patch('app.user_api_client.get_user', return_value=user)
|
||||
client.login(user)
|
||||
request.view_args = {'service_id': 'bar'}
|
||||
|
||||
@user_has_permissions()
|
||||
def index():
|
||||
pass
|
||||
|
||||
index()
|
||||
|
||||
|
||||
def _user_with_permissions():
|
||||
user_data = {'id': 999,
|
||||
'name': 'Test User',
|
||||
'password': 'somepassword',
|
||||
'email_address': 'test@user.gov.uk',
|
||||
'mobile_number': '+4412341234',
|
||||
'state': 'active',
|
||||
'failed_login_count': 0,
|
||||
'permissions': {'foo': ['manage_users', 'manage_templates', 'manage_settings']},
|
||||
'platform_admin': False,
|
||||
'organisations': ['org_1', 'org_2'],
|
||||
'services': ['foo', 'bar'],
|
||||
'current_session_id': None,
|
||||
}
|
||||
return user_data
|
||||
Reference in New Issue
Block a user