import json from unittest.mock import ANY, Mock import pytest from bs4 import BeautifulSoup from flask import url_for from freezegun import freeze_time from notifications_python_client.errors import HTTPError from tests import template_json, validate_route_permission from tests.app.main.views.test_template_folders import ( CHILD_FOLDER_ID, FOLDER_TWO_ID, PARENT_FOLDER_ID, _folder, _template, ) from tests.conftest import ( SERVICE_ONE_ID, SERVICE_TWO_ID, TEMPLATE_ONE_ID, ElementNotFound, create_active_caseworking_user, create_active_user_view_permissions, create_template, normalize_spaces, ) @pytest.mark.parametrize('permissions, expected_message', ( (['email'], ( 'Every message starts with a template. You can change it later. ' 'You need a template before you can send messages.' )), (['sms'], ( 'Every message starts with a template. You can change it later. ' 'You need a template before you can send messages.' )), (['email', 'sms'], ( 'Every message starts with a template. You can change it later. ' 'You need a template before you can send messages.' )), )) def test_should_show_empty_page_when_no_templates( client_request, service_one, mock_get_service_templates_when_no_templates_exist, mock_get_template_folders, mock_get_no_api_keys, permissions, expected_message, ): service_one['permissions'] = permissions page = client_request.get( 'main.choose_template', service_id=service_one['id'], ) assert normalize_spaces(page.select_one('h1').text) == ( 'Templates' ) assert normalize_spaces(page.select_one('main p').text) == ( expected_message ) assert page.select_one('#add_new_folder_form') assert page.select_one('#add_new_template_form') def test_should_show_add_template_form_if_service_has_folder_permission( client_request, service_one, mock_get_service_templates_when_no_templates_exist, mock_get_template_folders, mock_get_no_api_keys, ): page = client_request.get( 'main.choose_template', service_id=service_one['id'], ) assert normalize_spaces(page.select_one('h1').text) == ( 'Templates' ) assert normalize_spaces(page.select_one('main p').text) == ( 'Every message starts with a template. You can change it later. ' 'You need a template before you can send messages.' ) assert [ (item['name'], item['value']) for item in page.select('[type=radio]') ] == [ # ('add_template_by_template_type', 'email'), ('add_template_by_template_type', 'sms'), ] assert not page.select('main a') @pytest.mark.parametrize( 'user, expected_page_title, extra_args, expected_nav_links, expected_templates', [ ( create_active_user_view_permissions(), 'Templates', {}, ['Email', 'Text message'], [ 'sms_template_one', 'sms_template_two', 'email_template_one', 'email_template_two', ] ), ( create_active_user_view_permissions(), 'Templates', {'template_type': 'sms'}, ['All', 'Email'], ['sms_template_one', 'sms_template_two'], ), ( create_active_user_view_permissions(), 'Templates', {'template_type': 'email'}, ['All', 'Text message'], ['email_template_one', 'email_template_two'], ), ( create_active_caseworking_user(), 'Templates', {}, ['Email', 'Text message'], [ 'sms_template_one', 'sms_template_two', 'email_template_one', 'email_template_two', ], ), ( create_active_caseworking_user(), 'Templates', {'template_type': 'email'}, ['All', 'Text message'], ['email_template_one', 'email_template_two'], ), ] ) def test_should_show_page_for_choosing_a_template( client_request, mock_get_service_templates, mock_get_template_folders, mock_has_no_jobs, mock_get_no_api_keys, extra_args, expected_nav_links, expected_templates, service_one, mocker, user, expected_page_title, ): client_request.login(user) page = client_request.get( 'main.choose_template', service_id=service_one['id'], **extra_args ) assert normalize_spaces(page.select_one('h1').text) == expected_page_title links_in_page = page.select('.pill a:not(.pill-item--selected)') assert len(links_in_page) == len(expected_nav_links) for index, expected_link in enumerate(expected_nav_links): assert links_in_page[index].text.strip() == expected_link template_links = page.select('#template-list .govuk-label a, .template-list-item a') assert len(template_links) == len(expected_templates) for index, expected_template in enumerate(expected_templates): assert template_links[index].text.strip() == expected_template mock_get_service_templates.assert_called_once_with(SERVICE_ONE_ID) mock_get_template_folders.assert_called_once_with(SERVICE_ONE_ID) def test_choose_template_can_pass_through_an_initial_state_to_templates_and_folders_selection_form( client_request, mock_get_template_folders, mock_get_service_templates, mock_get_no_api_keys, ): page = client_request.get( 'main.choose_template', service_id=SERVICE_ONE_ID, initial_state='add-new-template' ) templates_and_folders_form = page.find('form') assert templates_and_folders_form['data-prev-state'] == 'add-new-template' def test_should_not_show_template_nav_if_only_one_type_of_template( client_request, mock_get_template_folders, mock_get_service_templates_with_only_one_template, mock_get_no_api_keys, ): page = client_request.get( 'main.choose_template', service_id=SERVICE_ONE_ID, ) assert not page.select('.pill') def test_should_not_show_live_search_if_list_of_templates_fits_onscreen( client_request, mock_get_template_folders, mock_get_service_templates, mock_get_no_api_keys, ): page = client_request.get( 'main.choose_template', service_id=SERVICE_ONE_ID, ) assert not page.select('.live-search') def test_should_show_live_search_if_list_of_templates_taller_than_screen( client_request, mock_get_template_folders, mock_get_more_service_templates_than_can_fit_onscreen, mock_get_no_api_keys, ): page = client_request.get( 'main.choose_template', service_id=SERVICE_ONE_ID, ) search = page.select_one('.live-search') assert search['data-module'] == 'live-search' assert search['data-targets'] == '#template-list .template-list-item' assert normalize_spaces(search.select_one('label').text) == ( 'Search by name' ) assert len(page.select(search['data-targets'])) == len(page.select('#template-list .usa-label')) == 20 def test_should_label_search_by_id_for_services_with_api_keys( client_request, mock_get_template_folders, mock_get_more_service_templates_than_can_fit_onscreen, mock_get_api_keys, ): page = client_request.get( 'main.choose_template', service_id=SERVICE_ONE_ID, ) assert normalize_spaces(page.select_one('.live-search label').text) == ( 'Search by name or ID' ) def test_should_show_live_search_if_service_has_lots_of_folders( client_request, mock_get_template_folders, mock_get_service_templates, # returns 4 templates mock_get_no_api_keys, ): mock_get_template_folders.return_value = [ _folder('one', PARENT_FOLDER_ID), _folder('two', None, parent=PARENT_FOLDER_ID), _folder('three', None, parent=PARENT_FOLDER_ID), _folder('four', None, parent=PARENT_FOLDER_ID), ] page = client_request.get( 'main.choose_template', service_id=SERVICE_ONE_ID, ) count_of_templates_and_folders = len(page.select('#template-list .usa-label')) count_of_folders = len(page.select('.template-list-folder:first-of-type')) count_of_templates = count_of_templates_and_folders - count_of_folders assert len(page.select('.live-search')) == 1 assert count_of_folders == 4 assert count_of_templates == 4 @pytest.mark.parametrize('service_permissions, expected_values, expected_labels', ( pytest.param(['email', 'sms'], [ # 'email', 'sms', 'copy-existing', ], [ # 'Email', 'Text message', 'Copy an existing template', ]), pytest.param(['email', 'sms'], [ # 'email', 'sms', 'copy-existing', ], [ # 'Email', 'Text message', 'Copy an existing template', ]), )) def test_should_show_new_template_choices_if_service_has_folder_permission( client_request, service_one, mock_get_service_templates, mock_get_template_folders, mock_get_no_api_keys, service_permissions, expected_values, expected_labels, ): service_one['permissions'] = service_permissions page = client_request.get( 'main.choose_template', service_id=SERVICE_ONE_ID, ) if not page.select('#add_new_template_form'): raise ElementNotFound() assert normalize_spaces(page.select_one('#add_new_template_form fieldset legend').text) == ( 'New template' ) assert [ choice['value'] for choice in page.select('#add_new_template_form input[type=radio]') ] == expected_values assert [ normalize_spaces(choice.text) for choice in page.select('#add_new_template_form label') ] == expected_labels @pytest.mark.parametrize("permissions,are_data_attrs_added", [ (['sms'], True), (['email'], True), (['sms', 'email'], False), ]) def test_should_add_data_attributes_for_services_that_only_allow_one_type_of_notifications( client_request, service_one, mock_get_service_templates, mock_get_template_folders, mock_get_no_api_keys, permissions, are_data_attrs_added ): service_one['permissions'] = permissions page = client_request.get( 'main.choose_template', service_id=SERVICE_ONE_ID, ) if not page.select('#add_new_template_form'): raise ElementNotFound() if are_data_attrs_added: assert page.find(id='add_new_template_form').attrs['data-channel'] == permissions[0] assert page.find(id='add_new_template_form').attrs['data-service'] == SERVICE_ONE_ID else: assert page.find(id='add_new_template_form').attrs.get('data-channel') is None assert page.find(id='add_new_template_form').attrs.get('data-service') is None def test_should_show_page_for_one_template( client_request, mock_get_service_template, fake_uuid, ): template_id = fake_uuid page = client_request.get( '.edit_service_template', service_id=SERVICE_ONE_ID, template_id=template_id, ) assert page.select_one('input[type=text]')['value'] == "Two week reminder" assert "Template <em>content</em> with & entity" in str( page.select_one('textarea') ) assert page.select_one('textarea')['data-module'] == 'enhanced-textbox' assert page.select_one('textarea')['data-highlight-placeholders'] == 'true' assert "priority" not in str(page.select_one('main')) assert ( page.select_one('[data-module=update-status]')['data-target'] ) == ( page.select_one('textarea')['id'] ) == ( 'template_content' ) assert ( page.select_one('[data-module=update-status]')['data-updates-url'] ) == url_for( '.count_content_length', service_id=SERVICE_ONE_ID, template_type='sms', ) assert ( page.select_one('[data-module=update-status]')['aria-live'] ) == ( 'polite' ) mock_get_service_template.assert_called_with(SERVICE_ONE_ID, template_id, None) def test_caseworker_redirected_to_set_sender_for_one_off( client_request, mock_get_service_templates, mock_get_service_template, mocker, fake_uuid, active_caseworking_user, ): client_request.login(active_caseworking_user) client_request.get( 'main.view_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _expected_status=302, _expected_redirect=url_for( 'main.set_sender', service_id=SERVICE_ONE_ID, template_id=fake_uuid, ), ) @freeze_time('2020-01-01 15:00') def test_caseworker_sees_template_page_if_template_is_deleted( client_request, mock_get_deleted_template, fake_uuid, mocker, active_caseworking_user, ): mocker.patch('app.user_api_client.get_user', return_value=active_caseworking_user) template_id = fake_uuid page = client_request.get( '.view_template', service_id=SERVICE_ONE_ID, template_id=template_id, _test_page_title=False, ) content = str(page) assert url_for("main.send_one_off", service_id=SERVICE_ONE_ID, template_id=fake_uuid) not in content assert page.select('p.hint')[0].text.strip() == 'This template was deleted today at 15:00 UTC.' mock_get_deleted_template.assert_called_with(SERVICE_ONE_ID, template_id, None) def test_user_with_only_send_and_view_redirected_to_set_sender_for_one_off( client_request, mock_get_service_templates, mock_get_service_template, active_user_with_permissions, mocker, fake_uuid, ): active_user_with_permissions['permissions'][SERVICE_ONE_ID] = [ 'send_messages', 'view_activity', ] client_request.login(active_user_with_permissions) client_request.get( 'main.view_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _expected_status=302, _expected_redirect=url_for( 'main.set_sender', service_id=SERVICE_ONE_ID, template_id=fake_uuid, ), ) @pytest.mark.parametrize('permissions, links_to_be_shown, permissions_warning_to_be_shown', [ ( ['view_activity'], [], 'If you need to send this text message or edit this template, contact your manager.' ), ( ['manage_api_keys'], [], None, ), ( ['manage_templates'], [ ('.edit_service_template', 'Edit'), ], None, ), ( ['send_messages', 'manage_templates'], [ ('.set_sender', 'Prepare to send a message using this template'), ('.edit_service_template', 'Edit'), ], None, ), ]) def test_should_be_able_to_view_a_template_with_links( client_request, mock_get_service_template, mock_get_template_folders, active_user_with_permissions, fake_uuid, permissions, links_to_be_shown, permissions_warning_to_be_shown, ): active_user_with_permissions['permissions'][SERVICE_ONE_ID] = permissions + ['view_activity'] client_request.login(active_user_with_permissions) page = client_request.get( '.view_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _test_page_title=False, ) assert normalize_spaces(page.select_one('h1').text) == ( 'Review your message' ) assert normalize_spaces(page.select_one('title').text) == ( 'Two week reminder – Templates – service one – U.S. Notify' ) assert [ (link['href'], normalize_spaces(link.text)) for link in page.select('.usa-pill-separate-item') ] == [ (url_for( endpoint, service_id=SERVICE_ONE_ID, template_id=fake_uuid, ), text) for endpoint, text in links_to_be_shown ] assert normalize_spaces(page.select_one('main p').text) == ( 'To: phone number' or permissions_warning_to_be_shown ) @pytest.mark.skip(reason="Hiding the copy-to-clipboard widget until API access is opened up") def test_should_show_template_id_on_template_page( client_request, mock_get_service_template, mock_get_template_folders, fake_uuid, ): page = client_request.get( '.view_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _test_page_title=False, ) assert fake_uuid in page.select('.copy-to-clipboard__value')[0].text def test_should_show_sms_template_with_downgraded_unicode_characters( client_request, mocker, service_one, mock_get_template_folders, fake_uuid, ): msg = 'here:\tare some “fancy quotes” and zero\u200Bwidth\u200Bspaces' rendered_msg = 'here: are some "fancy quotes" and zerowidthspaces' mocker.patch( 'app.service_api_client.get_service_template', return_value={'data': template_json(service_one['id'], fake_uuid, type_='sms', content=msg)} ) page = client_request.get( '.view_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _test_page_title=False, ) assert rendered_msg in page.text @pytest.mark.parametrize('prefix_sms', [ True, pytest.param(False, marks=pytest.mark.xfail()) ]) def test_should_show_message_with_prefix_hint_if_enabled_for_service( client_request, mocker, mock_get_service_template, mock_get_users_by_service, service_one, fake_uuid, prefix_sms ): service_one['prefix_sms'] = prefix_sms page = client_request.get( '.edit_service_template', service_id=service_one['id'], template_id=fake_uuid, ) assert 'Your service name will be added to the start of your message' in page.text def test_should_show_page_template_with_priority_select_if_platform_admin( client_request, platform_admin_user, mocker, mock_get_service_template, service_one, fake_uuid, ): mocker.patch('app.user_api_client.get_users_for_service', return_value=[platform_admin_user]) template_id = fake_uuid client_request.login(platform_admin_user) page = client_request.get( '.edit_service_template', service_id=service_one['id'], template_id=template_id, ) assert page.select_one('input[name=name]')['value'] == "Two week reminder" assert "Template <em>content</em> with & entity" in str(page.select_one('textarea')) assert "Use priority queue?" not in page.text mock_get_service_template.assert_called_with(service_one['id'], template_id, None) def test_choosing_to_copy_redirects( client_request, service_one, mock_get_service_templates, mock_get_template_folders, ): client_request.post( 'main.choose_template', service_id=SERVICE_ONE_ID, _data={ 'operation': 'add-new-template', 'add_template_by_template_type': 'copy-existing' }, _expected_status=302, _expected_redirect=url_for( 'main.choose_template_to_copy', service_id=SERVICE_ONE_ID, ), ) def test_choose_a_template_to_copy( client_request, mock_get_service_templates, mock_get_template_folders, mock_get_no_api_keys, mock_get_just_services_for_user, ): page = client_request.get( 'main.choose_template_to_copy', service_id=SERVICE_ONE_ID, ) assert page.select('.folder-heading') == [] expected = [ ( 'Service 1 ' '4 templates' ), ( 'Service 1 sms_template_one ' 'Text message template' ), ( 'Service 1 sms_template_two ' 'Text message template' ), ( 'Service 1 email_template_one ' 'Email template' ), ( 'Service 1 email_template_two ' 'Email template' ), ( 'Service 2 ' '4 templates' ), ( 'Service 2 sms_template_one ' 'Text message template' ), ( 'Service 2 sms_template_two ' 'Text message template' ), ( 'Service 2 email_template_one ' 'Email template' ), ( 'Service 2 email_template_two ' 'Email template' ), ] actual = page.select('.template-list-item') assert len(actual) == len(expected) for actual, expected in zip(actual, expected): # noqa: B020 assert normalize_spaces(actual.text) == expected links = page.select('main nav a') assert links[0]['href'] == url_for( 'main.choose_template_to_copy', service_id=SERVICE_ONE_ID, from_service=SERVICE_TWO_ID, ) assert links[1]['href'] == url_for( 'main.choose_template_to_copy', service_id=SERVICE_ONE_ID, from_service=SERVICE_TWO_ID, ) assert links[2]['href'] == url_for( 'main.copy_template', service_id=SERVICE_ONE_ID, template_id=TEMPLATE_ONE_ID, from_service=SERVICE_TWO_ID, ) def test_choose_a_template_to_copy_when_user_has_one_service( client_request, mock_get_service_templates, mock_get_template_folders, mock_get_no_api_keys, mock_get_empty_organizations_and_one_service_for_user, ): page = client_request.get( 'main.choose_template_to_copy', service_id=SERVICE_ONE_ID, ) assert page.select('.folder-heading') == [] expected = [ ( 'sms_template_one ' 'Text message template' ), ( 'sms_template_two ' 'Text message template' ), ( 'email_template_one ' 'Email template' ), ( 'email_template_two ' 'Email template' ), ] actual = page.select('.template-list-item') assert len(actual) == len(expected) for actual, expected in zip(actual, expected): # noqa: B020 assert normalize_spaces(actual.text) == expected assert page.select('main nav a')[0]['href'] == url_for( 'main.copy_template', service_id=SERVICE_ONE_ID, template_id=TEMPLATE_ONE_ID, from_service=SERVICE_TWO_ID, ) def test_choose_a_template_to_copy_from_folder_within_service( mocker, client_request, mock_get_template_folders, mock_get_non_empty_organizations_and_services_for_user, mock_get_no_api_keys, ): mock_get_template_folders.return_value = [ _folder('Parent folder', PARENT_FOLDER_ID), _folder('Child folder empty', CHILD_FOLDER_ID, parent=PARENT_FOLDER_ID), _folder('Child folder non-empty', FOLDER_TWO_ID, parent=PARENT_FOLDER_ID), ] mocker.patch( 'app.service_api_client.get_service_templates', return_value={'data': [ _template( 'sms', 'Should not appear in list (at service root)', ), _template( 'sms', 'Should appear in list (at same level)', parent=PARENT_FOLDER_ID, ), _template( 'sms', 'Should appear in list (nested)', parent=FOLDER_TWO_ID, template_id=TEMPLATE_ONE_ID, ), ]} ) page = client_request.get( 'main.choose_template_to_copy', service_id=SERVICE_ONE_ID, from_service=SERVICE_ONE_ID, from_folder=PARENT_FOLDER_ID, ) assert normalize_spaces(page.select_one('.folder-heading').text) == ( 'service one Parent folder' ) breadcrumb_links = page.select('.folder-heading a') assert len(breadcrumb_links) == 1 assert breadcrumb_links[0]['href'] == url_for( 'main.choose_template_to_copy', service_id=SERVICE_ONE_ID, from_service=SERVICE_ONE_ID, ) expected = [ ( 'Child folder empty ' 'Empty' ), ( 'Child folder non-empty ' '1 template' ), ( 'Child folder non-empty Should appear in list (nested) ' 'Text message template' ), ( 'Should appear in list (at same level) ' 'Text message template' ), ] actual = page.select('.template-list-item') assert len(actual) == len(expected) for actual, expected in zip(actual, expected): # noqa: B020 assert normalize_spaces(actual.text) == expected links = page.select('main nav a') assert links[0]['href'] == url_for( 'main.choose_template_to_copy', service_id=SERVICE_ONE_ID, from_service=SERVICE_ONE_ID, from_folder=CHILD_FOLDER_ID, ) assert links[1]['href'] == url_for( 'main.choose_template_to_copy', service_id=SERVICE_ONE_ID, from_service=SERVICE_ONE_ID, from_folder=FOLDER_TWO_ID, ) assert links[2]['href'] == url_for( 'main.choose_template_to_copy', service_id=SERVICE_ONE_ID, from_folder=FOLDER_TWO_ID, ) assert links[3]['href'] == url_for( 'main.copy_template', service_id=SERVICE_ONE_ID, template_id=TEMPLATE_ONE_ID, from_service=SERVICE_ONE_ID, ) @pytest.mark.parametrize('existing_template_names, expected_name', ( ( ['Two week reminder'], 'Two week reminder (copy)' ), ( ['Two week reminder (copy)'], 'Two week reminder (copy 2)' ), ( ['Two week reminder', 'Two week reminder (copy)'], 'Two week reminder (copy 2)' ), ( ['Two week reminder (copy 8)', 'Two week reminder (copy 9)'], 'Two week reminder (copy 10)' ), ( ['Two week reminder (copy)', 'Two week reminder (copy 9)'], 'Two week reminder (copy 10)' ), ( ['Two week reminder (copy)', 'Two week reminder (copy 10)'], 'Two week reminder (copy 2)' ), )) def test_load_edit_template_with_copy_of_template( client_request, active_user_with_permission_to_two_services, mock_get_service_templates, mock_get_service_email_template, mock_get_non_empty_organizations_and_services_for_user, existing_template_names, expected_name, ): mock_get_service_templates.side_effect = lambda service_id: {'data': [ {'name': existing_template_name, 'template_type': 'sms'} for existing_template_name in existing_template_names ]} client_request.login(active_user_with_permission_to_two_services) page = client_request.get( 'main.copy_template', service_id=SERVICE_ONE_ID, template_id=TEMPLATE_ONE_ID, from_service=SERVICE_TWO_ID, ) assert page.select_one('form')['method'] == 'post' assert page.select_one('input')['value'] == ( expected_name ) assert page.select_one('textarea').text == ( '\r\nYour ((thing)) is due soon' ) mock_get_service_email_template.assert_called_once_with( SERVICE_TWO_ID, TEMPLATE_ONE_ID, ) def test_copy_template_loads_template_from_within_subfolder( client_request, active_user_with_permission_to_two_services, mock_get_service_templates, mock_get_non_empty_organizations_and_services_for_user, mocker ): template = template_json( SERVICE_TWO_ID, TEMPLATE_ONE_ID, name='foo', folder=PARENT_FOLDER_ID ) mock_get_service_template = mocker.patch( 'app.service_api_client.get_service_template', return_value={'data': template} ) mock_get_template_folder = mocker.patch( 'app.template_folder_api_client.get_template_folder', return_value=_folder('Parent folder', PARENT_FOLDER_ID), ) client_request.login(active_user_with_permission_to_two_services) page = client_request.get( 'main.copy_template', service_id=SERVICE_ONE_ID, template_id=TEMPLATE_ONE_ID, from_service=SERVICE_TWO_ID, ) assert page.select_one('input')['value'] == 'foo (copy)' mock_get_service_template.assert_called_once_with(SERVICE_TWO_ID, TEMPLATE_ONE_ID) mock_get_template_folder.assert_called_once_with(SERVICE_TWO_ID, PARENT_FOLDER_ID) def test_cant_copy_template_from_non_member_service( client_request, mock_get_service_email_template, mock_get_organizations_and_services_for_user, ): client_request.get( 'main.copy_template', service_id=SERVICE_ONE_ID, template_id=TEMPLATE_ONE_ID, from_service=SERVICE_TWO_ID, _expected_status=403, ) assert mock_get_service_email_template.call_args_list == [] @pytest.mark.parametrize('service_permissions, data, expected_error', ( ( ['email'], { 'operation': 'add-new-template', 'add_template_by_template_type': 'sms', }, "Sending text messages has been disabled for your service." ), )) def test_should_not_allow_creation_of_template_through_form_without_correct_permission( client_request, service_one, mock_get_service_templates, mock_get_template_folders, service_permissions, data, expected_error, fake_uuid, ): service_one['permissions'] = service_permissions page = client_request.post( 'main.choose_template', service_id=SERVICE_ONE_ID, _data=data, _follow_redirects=True, _expected_status=403, ) assert normalize_spaces(page.select('main p')[0].text) == expected_error assert page.select(".usa-back-link")[0].text == "Back" assert page.select(".usa-back-link")[0]['href'] == url_for( '.choose_template', service_id=SERVICE_ONE_ID, ) @pytest.mark.parametrize('method', ('get', 'post')) @pytest.mark.parametrize('type_of_template, expected_error', [ ('email', 'Sending emails has been disabled for your service.'), ('sms', 'Sending text messages has been disabled for your service.'), ]) def test_should_not_allow_creation_of_a_template_without_correct_permission( client_request, service_one, mocker, method, type_of_template, expected_error, ): service_one['permissions'] = [] page = getattr(client_request, method)( '.add_service_template', service_id=SERVICE_ONE_ID, template_type=type_of_template, _follow_redirects=True, _expected_status=403, ) assert page.select('main p')[0].text.strip() == expected_error assert page.select(".usa-back-link")[0].text == "Back" assert page.select(".usa-back-link")[0]['href'] == url_for( '.choose_template', service_id=service_one['id'], ) def test_should_redirect_when_saving_a_template( client_request, mock_get_service_template, mock_get_api_keys, mock_update_service_template, fake_uuid, ): name = "new name" content = "template content with & entity" client_request.post( '.edit_service_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _data={ 'id': fake_uuid, 'name': name, 'template_content': content, 'template_type': 'sms', 'service': SERVICE_ONE_ID, }, _expected_status=302, _expected_redirect=url_for( '.view_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, ), ) mock_update_service_template.assert_called_with( fake_uuid, name, 'sms', content, SERVICE_ONE_ID, None ) def test_should_edit_content_when_process_type_is_priority_not_platform_admin( client_request, mock_get_service_template_with_priority, mock_update_service_template, fake_uuid, ): client_request.post( '.edit_service_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _data={ 'id': fake_uuid, 'name': "new name", 'template_content': "new template content with & entity", 'template_type': 'sms', 'service': SERVICE_ONE_ID, }, _expected_status=302, _expected_redirect=url_for( '.view_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, ) ) mock_update_service_template.assert_called_with( fake_uuid, "new name", 'sms', "new template content with & entity", SERVICE_ONE_ID, None ) def test_should_not_allow_template_edits_without_correct_permission( client_request, mock_get_service_template, service_one, fake_uuid, ): service_one['permissions'] = ['email'] page = client_request.get( '.edit_service_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _follow_redirects=True, _expected_status=403, ) assert page.select('main p')[0].text.strip() == "Sending text messages has been disabled for your service." assert page.select(".usa-back-link")[0].text == "Back" assert page.select(".usa-back-link")[0]['href'] == url_for( '.view_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, ) def test_should_403_when_edit_template_with_process_type_of_priority_for_non_platform_admin( client_request, active_user_with_permissions, mocker, mock_get_service_template, mock_update_service_template, fake_uuid, service_one, ): service_one['users'] = [active_user_with_permissions] client_request.login(active_user_with_permissions) mocker.patch('app.user_api_client.get_users_for_service', return_value=[active_user_with_permissions]) template_id = fake_uuid data = { 'id': template_id, 'name': "new name", 'template_content': "template content with & entity", 'template_type': 'sms', 'service': service_one['id'], 'process_type': 'priority' } client_request.post( '.edit_service_template', service_id=service_one['id'], template_id=template_id, _data=data, _expected_status=403, ) assert mock_update_service_template.called is False def test_should_403_when_create_template_with_process_type_of_priority_for_non_platform_admin( client_request, active_user_with_permissions, mocker, mock_get_service_template, mock_update_service_template, fake_uuid, service_one, ): service_one['users'] = [active_user_with_permissions] client_request.login(active_user_with_permissions, service_one) mocker.patch('app.user_api_client.get_users_for_service', return_value=[active_user_with_permissions]) template_id = fake_uuid data = { 'id': template_id, 'name': "new name", 'template_content': "template content with & entity", 'template_type': 'sms', 'service': service_one['id'], 'process_type': 'priority' } client_request.post( '.add_service_template', service_id=service_one['id'], template_type='sms', _data=data, _expected_status=403, ) assert mock_update_service_template.called is False @pytest.mark.parametrize('old_content, new_content, expected_paragraphs', [ ( "my favorite color is blue", "my favorite color is ((color))", [ 'You added ((color))', 'Before you send any messages, make sure your API calls include color.', ] ), ( "hello ((name))", "hello ((first name)) ((middle name)) ((last name))", [ 'You removed ((name))', 'You added ((first name)) ((middle name)) and ((last name))', 'Before you send any messages, make sure your API calls include first name, middle name and last name.', ] ), ]) def test_should_show_interstitial_when_making_breaking_change( client_request, mock_update_service_template, mock_get_user_by_email, mock_get_api_keys, fake_uuid, mocker, new_content, old_content, expected_paragraphs, ): email_template = create_template( template_id=fake_uuid, template_type='email', subject="Your ((thing)) is due soon", content=old_content ) mocker.patch('app.service_api_client.get_service_template', return_value={'data': email_template}) data = { 'id': fake_uuid, 'name': "new name", 'template_content': new_content, 'template_type': 'email', 'subject': 'reminder \'" & ((thing))', 'service': SERVICE_ONE_ID, 'process_type': 'normal' } page = client_request.post( '.edit_service_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _data=data, _expected_status=200, ) assert page.h1.string.strip() == "Confirm changes" assert page.find('a', {'class': 'usa-back-link'})['href'] == url_for( ".edit_service_template", service_id=SERVICE_ONE_ID, template_id=fake_uuid, ) assert [ normalize_spaces(paragraph.text) for paragraph in page.select('main p') ] == expected_paragraphs for key, value in { 'name': 'new name', 'subject': 'reminder \'" & ((thing))', 'template_content': new_content, 'confirm': 'true' }.items(): assert page.find('input', {'name': key})['value'] == value # BeautifulSoup returns the value attribute as unencoded, let’s make # sure that it is properly encoded in the HTML assert str(page.find('input', {'name': 'subject'})) == ( """""" ) def test_removing_placeholders_is_not_a_breaking_change( client_request, mock_get_service_email_template, mock_update_service_template, fake_uuid, ): existing_template = mock_get_service_email_template(0, 0)['data'] client_request.post( '.edit_service_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _data={ 'name': existing_template['name'], 'template_content': "no placeholders", 'subject': existing_template['subject'], }, _expected_status=302, _expected_redirect=url_for( 'main.view_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, ), ) assert mock_update_service_template.called is True def test_should_not_create_too_big_template( client_request, mock_get_service_template, mock_create_service_template_content_too_big, fake_uuid, ): page = client_request.post( '.add_service_template', service_id=SERVICE_ONE_ID, template_type='sms', _data={ 'name': "new name", 'template_content': "template content", 'template_type': 'sms', 'service': SERVICE_ONE_ID, 'process_type': 'normal' }, _expected_status=200, ) assert "Content has a character count greater than the limit of 459" in page.text def test_should_not_update_too_big_template( client_request, mock_get_service_template, mock_update_service_template_400_content_too_big, fake_uuid, ): page = client_request.post( '.edit_service_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _data={ 'id': fake_uuid, 'name': "new name", 'template_content': "template content", 'service': SERVICE_ONE_ID, 'template_type': 'sms', 'process_type': 'normal', }, _expected_status=200, ) assert "Content has a character count greater than the limit of 459" in page.text def test_should_redirect_when_saving_a_template_email( client_request, mock_get_service_email_template, mock_update_service_template, mock_get_user_by_email, fake_uuid, ): name = "new name" content = "template content with & entity ((thing)) ((date))" subject = "subject & entity" client_request.post( '.edit_service_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _data={ 'id': fake_uuid, 'name': name, 'template_content': content, 'template_type': 'email', 'service': SERVICE_ONE_ID, 'subject': subject, }, _expected_status=302, _expected_redirect=url_for( '.view_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, ), ) mock_update_service_template.assert_called_with( fake_uuid, name, 'email', content, SERVICE_ONE_ID, subject ) def test_should_show_delete_template_page_with_time_block( client_request, mock_get_service_template, mock_get_template_folders, mocker, fake_uuid ): mocker.patch('app.template_statistics_client.get_last_used_date_for_template', return_value='2012-01-01 12:00:00') with freeze_time('2012-01-01 12:10:00'): page = client_request.get( '.delete_service_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _test_page_title=False, ) assert "Are you sure you want to delete ‘Two week reminder’?" in page.select('.banner-dangerous')[0].text assert normalize_spaces(page.select('.banner-dangerous p')[0].text) == ( 'This template was last used 10 minutes ago.' ) assert normalize_spaces(page.select('.sms-message-wrapper')[0].text) == ( 'service one: Template content with & entity' ) mock_get_service_template.assert_called_with(SERVICE_ONE_ID, fake_uuid, None) def test_should_show_delete_template_page_with_time_block_for_empty_notification( client_request, mock_get_service_template, mock_get_template_folders, mocker, fake_uuid ): mocker.patch('app.template_statistics_client.get_last_used_date_for_template', return_value=None) with freeze_time('2012-01-01 11:00:00'): page = client_request.get( '.delete_service_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _test_page_title=False, ) assert "Are you sure you want to delete ‘Two week reminder’?" in page.select('.banner-dangerous')[0].text assert normalize_spaces(page.select('.banner-dangerous p')[0].text) == ( 'This template has never been used.' ) assert normalize_spaces(page.select('.sms-message-wrapper')[0].text) == ( 'service one: Template content with & entity' ) mock_get_service_template.assert_called_with(SERVICE_ONE_ID, fake_uuid, None) def test_should_show_delete_template_page_with_never_used_block( client_request, mock_get_service_template, mock_get_template_folders, fake_uuid, mocker, ): mocker.patch( 'app.template_statistics_client.get_last_used_date_for_template', side_effect=HTTPError(response=Mock(status_code=404), message="Default message") ) page = client_request.get( '.delete_service_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _test_page_title=False, ) assert "Are you sure you want to delete ‘Two week reminder’?" in page.select('.banner-dangerous')[0].text assert not page.select('.banner-dangerous p') assert normalize_spaces(page.select('.sms-message-wrapper')[0].text) == ( 'service one: Template content with & entity' ) mock_get_service_template.assert_called_with(SERVICE_ONE_ID, fake_uuid, None) @pytest.mark.parametrize('parent', ( PARENT_FOLDER_ID, None )) def test_should_redirect_when_deleting_a_template( mocker, client_request, mock_delete_service_template, mock_get_template_folders, parent, ): mock_get_template_folders.return_value = [ {'id': PARENT_FOLDER_ID, 'name': 'Folder', 'parent': None, 'users_with_permission': [ANY]} ] mock_get_service_template = mocker.patch( 'app.service_api_client.get_service_template', return_value={'data': _template( 'sms', 'Hello', parent=parent, )}, ) client_request.post( '.delete_service_template', service_id=SERVICE_ONE_ID, template_id=TEMPLATE_ONE_ID, _expected_status=302, _expected_redirect=url_for( '.choose_template', service_id=SERVICE_ONE_ID, template_folder_id=parent, ) ) mock_get_service_template.assert_called_with( SERVICE_ONE_ID, TEMPLATE_ONE_ID, None ) mock_delete_service_template.assert_called_with( SERVICE_ONE_ID, TEMPLATE_ONE_ID ) @freeze_time('2016-01-01T15:00') def test_should_show_page_for_a_deleted_template( client_request, mock_get_template_folders, mock_get_deleted_template, mock_get_user, mock_get_user_by_email, mock_has_permissions, fake_uuid, ): template_id = fake_uuid page = client_request.get( '.view_template', service_id=SERVICE_ONE_ID, template_id=template_id, _test_page_title=False, ) content = str(page) assert url_for("main.edit_service_template", service_id=SERVICE_ONE_ID, template_id=fake_uuid) not in content assert url_for("main.send_one_off", service_id=SERVICE_ONE_ID, template_id=fake_uuid) not in content assert page.select('p.hint')[0].text.strip() == 'This template was deleted today at 15:00 UTC.' assert 'Delete this template' not in page.select_one('main').text mock_get_deleted_template.assert_called_with(SERVICE_ONE_ID, template_id, None) @pytest.mark.parametrize('route', [ 'main.add_service_template', 'main.edit_service_template', 'main.delete_service_template' ]) def test_route_permissions( route, mocker, notify_admin, client_request, api_user_active, service_one, mock_get_service_template, mock_get_template_folders, fake_uuid, ): mocker.patch('app.template_statistics_client.get_last_used_date_for_template', return_value='2012-01-01 12:00:00') validate_route_permission( mocker, notify_admin, "GET", 200, url_for( route, service_id=service_one['id'], template_type='sms', template_id=fake_uuid), ['manage_templates'], api_user_active, service_one) def test_route_permissions_for_choose_template( mocker, notify_admin, client_request, api_user_active, mock_get_template_folders, service_one, mock_get_service_templates, mock_get_no_api_keys, ): mocker.patch('app.job_api_client.get_job') validate_route_permission( mocker, notify_admin, "GET", 200, url_for( 'main.choose_template', service_id=service_one['id'], ), [], api_user_active, service_one) @pytest.mark.parametrize('route', [ 'main.add_service_template', 'main.edit_service_template', 'main.delete_service_template' ]) def test_route_invalid_permissions( route, mocker, notify_admin, client_request, api_user_active, service_one, mock_get_service_template, fake_uuid, ): validate_route_permission( mocker, notify_admin, "GET", 403, url_for( route, service_id=service_one['id'], template_type='sms', template_id=fake_uuid), ['view_activity'], api_user_active, service_one) @pytest.mark.parametrize('template_type, expected', ( ('email', 'New email template'), ('sms', 'New text message template'), )) def test_add_template_page_title( client_request, service_one, template_type, expected, ): service_one['permissions'] += [template_type] page = client_request.get( '.add_service_template', service_id=SERVICE_ONE_ID, template_type=template_type, ) assert normalize_spaces(page.select_one('h1').text) == expected def test_can_create_email_template_with_emoji( client_request, mock_create_service_template ): client_request.post( '.add_service_template', service_id=SERVICE_ONE_ID, template_type='email', _data={ 'name': "new name", 'subject': "Food incoming!", 'template_content': "here's a burrito 🌯", 'template_type': 'email', 'service': SERVICE_ONE_ID, 'process_type': 'normal' }, _expected_status=302, ) assert mock_create_service_template.called is True @pytest.mark.parametrize('template_type, expected_error', ( ('sms', ( 'You cannot use 🍜 in text messages.' )), )) def test_should_not_create_sms_template_with_emoji( client_request, service_one, mock_create_service_template, template_type, expected_error, ): service_one['permissions'] += [template_type] page = client_request.post( '.add_service_template', service_id=SERVICE_ONE_ID, template_type=template_type, _data={ 'name': "new name", 'template_content': "here are some noodles 🍜", 'template_type': 'sms', 'service': SERVICE_ONE_ID, 'process_type': 'normal', }, _expected_status=200, ) assert expected_error in page.text assert mock_create_service_template.called is False @pytest.mark.parametrize('template_type, expected_error', ( ('sms', ( 'You cannot use 🍔 in text messages.' )), )) def test_should_not_update_sms_template_with_emoji( mocker, client_request, service_one, mock_get_service_template, mock_update_service_template, fake_uuid, template_type, expected_error, ): service_one['permissions'] += [template_type] return mocker.patch( 'app.service_api_client.get_service_template', return_value=template_json( SERVICE_ONE_ID, fake_uuid, type_=template_type, ), ) page = client_request.post( '.edit_service_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _data={ 'id': fake_uuid, 'name': "new name", 'template_content': "here's a burger 🍔", 'service': SERVICE_ONE_ID, 'template_type': template_type, 'process_type': 'normal' }, _expected_status=200, ) assert expected_error in page.text assert mock_update_service_template.called is False @pytest.mark.parametrize('template_type', ( 'sms', )) def test_should_create_sms_template_without_downgrading_unicode_characters( client_request, service_one, mock_create_service_template, template_type, ): service_one['permissions'] += [template_type] msg = 'here:\tare some “fancy quotes” and non\u200Bbreaking\u200Bspaces' client_request.post( '.add_service_template', service_id=SERVICE_ONE_ID, template_type='sms', _data={ 'name': "new name", 'template_content': msg, 'template_type': template_type, 'service': SERVICE_ONE_ID, }, expected_status=302, ) mock_create_service_template.assert_called_with( ANY, # name ANY, # type msg, # content ANY, # service_id ANY, # subject ANY # parent_folder_id ) def test_should_show_message_before_redacting_template( client_request, mock_get_service_template, service_one, fake_uuid, ): page = client_request.get( 'main.redact_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _test_page_title=False, ) assert ( 'Are you sure you want to hide personalization after sending?' ) in page.select('.banner-dangerous')[0].text form = page.select('.banner-dangerous form')[0] assert 'action' not in form assert form['method'] == 'post' def test_should_show_redact_template( client_request, mock_get_service_template, mock_get_template_folders, mock_redact_template, service_one, fake_uuid, ): page = client_request.post( 'main.redact_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _follow_redirects=True, ) assert normalize_spaces(page.select('.banner-default-with-tick')[0].text) == ( 'Personalized content will be hidden for messages sent with this template' ) mock_redact_template.assert_called_once_with(SERVICE_ONE_ID, fake_uuid) def test_should_show_hint_once_template_redacted( client_request, mocker, service_one, mock_get_template_folders, fake_uuid, ): template = create_template(template_type='email', content='hi ((name))', redact_personalisation=True) mocker.patch('app.service_api_client.get_service_template', return_value={'data': template}) page = client_request.get( 'main.view_template', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _test_page_title=False, ) assert page.select('.hint')[0].text == 'Personalization is hidden after sending' def test_set_template_sender( client_request, fake_uuid, mock_update_service_template_sender, mock_get_service_template, single_sms_sender ): data = { 'sender': '1234', } client_request.post( 'main.set_template_sender', service_id=SERVICE_ONE_ID, template_id=fake_uuid, _data=data, ) mock_update_service_template_sender.assert_called_once_with( SERVICE_ONE_ID, fake_uuid, '1234', ) @pytest.mark.parametrize( 'template_type, prefix_sms, content, expected_message, expected_class', ( ( 'sms', False, '', 'Will be charged as 1 text message', None, ), ( 'sms', False, 'a' * 160, 'Will be charged as 1 text message', None, ), ( 'sms', False, 'a' * 161, 'Will be charged as 2 text messages', None, ), ( # service name takes 13 characters, 147 + 13 = 160 'sms', True, 'a' * 147, 'Will be charged as 1 text message', None, ), ( # service name takes 13 characters, 148 + 13 = 161 'sms', True, 'a' * 148, 'Will be charged as 2 text messages', None, ), ( 'sms', False, 'a' * 918, 'Will be charged as 6 text messages', None, ), ( # Service name increases fragment count but doesn’t count # against total character limit 'sms', True, 'a' * 918, 'Will be charged as 7 text messages', None, ), ( # Can’t make a 7 fragment text template from content alone 'sms', False, 'a' * 919, 'You have 1 character too many', 'usa-error-message', ), ( # Service name increases content count but character count # is based on content alone 'sms', True, 'a' * 919, 'You have 1 character too many', 'usa-error-message', ), ( # Service name increases content count but character count # is based on content alone 'sms', True, 'a' * 920, 'You have 2 characters too many', 'usa-error-message', ), ( 'sms', False, 'Ẅ' * 70, 'Will be charged as 1 text message', None, ), ( 'sms', False, 'Ẅ' * 71, 'Will be charged as 2 text messages', None, ), ( 'sms', False, 'Ẅ' * 918, 'Will be charged as 14 text messages', None, ), ( 'sms', False, 'Ẅ' * 919, 'You have 1 character too many', 'usa-error-message', ), ( 'sms', False, 'Hello ((name))', 'Will be charged as 1 text message (not including personalization)', None, ), ( # Length of placeholder body doesn’t count towards fragment count 'sms', False, f'Hello (( {"a" * 999} ))', 'Will be charged as 1 text message (not including personalization)', None, ), ), ) def test_content_count_json_endpoint( client_request, service_one, template_type, prefix_sms, content, expected_message, expected_class, ): service_one['prefix_sms'] = prefix_sms response = client_request.post_response( 'main.count_content_length', service_id=SERVICE_ONE_ID, template_type=template_type, _data={ 'template_content': content, }, _expected_status=200, ) html = json.loads(response.get_data(as_text=True))['html'] snippet = BeautifulSoup(html, 'html.parser').select_one('span') assert normalize_spaces(snippet.text) == expected_message if snippet.has_attr('class'): assert snippet['class'] == [expected_class] else: assert expected_class is None @pytest.mark.parametrize('template_type', ( 'email', 'banana', )) def test_content_count_json_endpoint_for_unsupported_template_types( client_request, template_type, ): client_request.post( 'main.count_content_length', service_id=SERVICE_ONE_ID, template_type=template_type, content='foo', _expected_status=404, )