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) == ( "Select or create a template" ) 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) == ( "Select or create a template" ) 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(), "Select or create a template", {}, ["Email", "Text message"], [ "sms_template_one", "sms_template_two", "email_template_one", "email_template_two", ], ), ( create_active_user_view_permissions(), "Select or create a template", {"template_type": "sms"}, ["All", "Email"], ["sms_template_one", "sms_template_two"], ), ( create_active_user_view_permissions(), "Select or create a template", {"template_type": "email"}, ["All", "Text message"], ["email_template_one", "email_template_two"], ), ( create_active_caseworking_user(), "Select or create a template", {}, ["Email", "Text message"], [ "sms_template_one", "sms_template_two", "email_template_one", "email_template_two", ], ), ( create_active_caseworking_user(), "Select or create a template", {"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 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-checkbox")) == 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-checkbox")) 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', "Start with a blank template", "Copy an existing template", ], ), # TODO This is a duplicate of above. Why? # pytest.param( # ["email", "sms"], # [ # # 'email', # "sms", # "copy-existing", # ], # [ # # 'Email', # "Start with a blank template", # "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 20: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 America/New_York." ) 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 this template"), ], None, ), ( ["send_messages", "manage_templates"], [ (".set_sender", "Use this template"), (".edit_service_template", "Edit this template"), ], 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) == ("Confirm your template") assert normalize_spaces(page.select_one("title").text) == ( "Confirm your template – service one – Notify.gov" ) 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 sms_template_one"), ("Service 1 sms_template_two"), ("Service 1 email_template_one"), ("Service 1 email_template_two"), (""), ("Service 2 sms_template_one"), ("Service 2 sms_template_two"), ("Service 2 email_template_one"), ("Service 2 email_template_two"), ] actual = page.select(".template-list-item") assert len(actual) == len(expected) zipobject = zip(actual, expected) for actual, expected in zipobject: 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, ) 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"), ("sms_template_two"), ("email_template_one"), ("email_template_two"), ] actual = page.select(".template-list-item") assert len(actual) == len(expected) zipobject = zip(actual, expected) for actual, expected in zipobject: 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_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 non-empty Should appear in list (nested)"), ("Should appear in list (at same level)"), ] actual = page.select(".template-list-item") assert len(actual) == len(expected) zipobject = zip(actual, expected) for actual, expected in zipobject: 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_folder=FOLDER_TWO_ID, ) # assert links[1]["href"] == url_for( # "main.choose_template_to_copy", # service_id=SERVICE_ONE_ID, # from_service=SERVICE_ONE_ID, # from_folder=PARENT_FOLDER_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.strip() == ("Your ((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-01T20: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 America/New_York." ) 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", # ( # "Please remove the unaccepted character 🍜 in your message, then save again" # ), # ), # ], # ) # 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, # ) # # print(page.main.prettify()) # assert expected_error in normalize_spaces( # page.select_one("#template_content-error").text # ) # assert mock_create_service_template.called is False # @pytest.mark.parametrize( # ("template_type", "expected_error"), # [ # ( # "sms", # ( # "Please remove the unaccepted character 🍔 in your message, then save again" # ), # ), # ], # ) # 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 all personalized and conditional" " content after sending for increased privacy protection?" ) 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, "a" * 160, "Will be charged as 1 text message", None, ), ( "sms", False, "a" * 161, "Will be charged as 2 text messages", None, ), ( "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.", # noqa E501 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 expected_message in normalize_spaces(snippet.text) 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, )