from functools import partial from unittest.mock import ANY, PropertyMock import pytest from flask import url_for from freezegun import freeze_time from notifications_utils.clients.zendesk.zendesk_client import NotifySupportTicket from app.main.views.feedback import in_business_hours from app.models.feedback import ( GENERAL_TICKET_TYPE, PROBLEM_TICKET_TYPE, QUESTION_TICKET_TYPE, ) from tests.conftest import SERVICE_ONE_ID, normalize_spaces def no_redirect(): return lambda: None @pytest.mark.skip(reason="Not currently using Zendesk") def test_get_support_index_page( client_request, ): page = client_request.get(".support") assert page.select_one("form")["method"] == "post" assert "action" not in page.select_one("form") assert normalize_spaces(page.select_one("h1").text) == "Support" assert ( normalize_spaces(page.select_one("form label[for=support_type-0]").text) == "Report a problem" ) assert page.select_one("form input#support_type-0")["value"] == "report-problem" assert ( normalize_spaces(page.select_one("form label[for=support_type-1]").text) == "Ask a question or give feedback" ) assert ( page.select_one("form input#support_type-1")["value"] == "ask-question-give-feedback" ) assert ( normalize_spaces(page.select_one("form button[type=submit]").text) == "Continue" ) @pytest.mark.skip(reason="Not currently using Zendesk") def test_get_support_index_page_when_signed_out( client_request, ): client_request.logout() page = client_request.get(".support") assert page.select_one("form")["method"] == "post" assert "action" not in page.select_one("form") assert normalize_spaces(page.select_one("form label[for=who-0]").text) == ( "I work in the public sector and need to send emails or text messages" ) assert page.select_one("form input#who-0")["value"] == "public-sector" assert normalize_spaces(page.select_one("form label[for=who-1]").text) == ( "I’m a member of the public with a question for the government" ) assert page.select_one("form input#who-1")["value"] == "public" assert ( normalize_spaces(page.select_one("form button[type=submit]").text) == "Continue" ) @freeze_time("2016-12-12 12:00:00.000000") @pytest.mark.parametrize( ("support_type", "expected_h1"), [ (PROBLEM_TICKET_TYPE, "Report a problem"), (QUESTION_TICKET_TYPE, "Ask a question or give feedback"), ], ) def test_choose_support_type( client_request, mock_get_non_empty_organizations_and_services_for_user, support_type, expected_h1, ): page = client_request.post( "main.support", _data={"support_type": support_type}, _follow_redirects=True, ) assert page.h1.string.strip() == expected_h1 assert not page.select_one("input[name=name]") assert not page.select_one("input[name=email_address]") assert page.find("form").find("p").text.strip() == ( "We’ll reply to test@user.gsa.gov" ) @freeze_time("2016-12-12 12:00:00.000000") def test_get_support_as_someone_in_the_public_sector( client_request, ): client_request.logout() page = client_request.post( "main.support", _data={"who": "public-sector"}, _follow_redirects=True, ) assert normalize_spaces(page.select("h1")) == ("Contact Notify.gov support") assert page.select_one("form textarea[name=feedback]") assert page.select_one("form input[name=name]") assert page.select_one("form input[name=email_address]") assert page.select_one("form button[type=submit]") def test_get_support_as_member_of_public( client_request, ): client_request.logout() page = client_request.post( "main.support", _data={"who": "public"}, _follow_redirects=True, ) assert normalize_spaces(page.select("h1")) == ( "The Notify.gov service is for people who work in the government" ) assert len(page.select("h2 a")) == 3 assert not page.select("form") assert not page.select("input") assert not page.select("form [type=submit]") @freeze_time("2016-12-12 12:00:00.000000") @pytest.mark.parametrize( ("ticket_type", "expected_status_code"), [(PROBLEM_TICKET_TYPE, 200), (QUESTION_TICKET_TYPE, 200), ("gripe", 404)], ) def test_get_feedback_page(client_request, ticket_type, expected_status_code): client_request.logout() client_request.get( "main.feedback", ticket_type=ticket_type, _expected_status=expected_status_code, ) @freeze_time("2016-12-12 12:00:00.000000") @pytest.mark.parametrize( ("ticket_type", "zendesk_ticket_type"), [ (PROBLEM_TICKET_TYPE, "incident"), (QUESTION_TICKET_TYPE, "question"), (GENERAL_TICKET_TYPE, "question"), ], ) def test_passed_non_logged_in_user_details_through_flow( client_request, mocker, ticket_type, zendesk_ticket_type ): client_request.logout() mock_create_ticket = mocker.spy(NotifySupportTicket, "__init__") mock_send_ticket_to_zendesk = mocker.patch( "app.main.views.feedback.zendesk_client.send_ticket_to_zendesk", autospec=True, ) data = { "feedback": "blah", "name": "Anne Example", "email_address": "anne@example.com", } client_request.post( "main.feedback", ticket_type=ticket_type, _data=data, _expected_redirect=url_for( "main.thanks", out_of_hours_emergency=False, email_address_provided=True, ), ) mock_create_ticket.assert_called_once_with( ANY, subject="Notify feedback", message="blah\n", ticket_type=zendesk_ticket_type, p1=False, user_name="Anne Example", user_email="anne@example.com", org_id=None, org_type=None, service_id=None, ) mock_send_ticket_to_zendesk.assert_called_once() @freeze_time("2016-12-12 12:00:00.000000") @pytest.mark.parametrize( "data", [ {"feedback": "blah"}, {"feedback": "blah", "name": "Ignored", "email_address": "ignored@email.com"}, ], ) @pytest.mark.parametrize( ("ticket_type", "zendesk_ticket_type"), [ (PROBLEM_TICKET_TYPE, "incident"), (QUESTION_TICKET_TYPE, "question"), (GENERAL_TICKET_TYPE, "question"), ], ) def test_passes_user_details_through_flow( client_request, mock_get_non_empty_organizations_and_services_for_user, mocker, ticket_type, zendesk_ticket_type, data, ): mock_create_ticket = mocker.spy(NotifySupportTicket, "__init__") mock_send_ticket_to_zendesk = mocker.patch( "app.main.views.feedback.zendesk_client.send_ticket_to_zendesk", autospec=True, ) client_request.post( "main.feedback", ticket_type=ticket_type, _data=data, _expected_status=302, _expected_redirect=url_for( "main.thanks", email_address_provided=True, out_of_hours_emergency=False, ), ) mock_create_ticket.assert_called_once_with( ANY, subject="Notify feedback", message=ANY, ticket_type=zendesk_ticket_type, p1=False, user_name="Test User", user_email="test@user.gsa.gov", org_id=None, org_type="federal", service_id=SERVICE_ONE_ID, ) assert mock_create_ticket.call_args[1]["message"] == "\n".join( [ "blah", 'Service: "service one"', url_for( "main.service_dashboard", service_id=SERVICE_ONE_ID, _external=True, ), "", ] ) mock_send_ticket_to_zendesk.assert_called_once() @freeze_time("2016-12-12 12:00:00.000000") @pytest.mark.parametrize( "data", [ {"feedback": "blah", "name": "Fred"}, {"feedback": "blah"}, ], ) @pytest.mark.parametrize( "ticket_type", [ PROBLEM_TICKET_TYPE, QUESTION_TICKET_TYPE, ], ) def test_email_address_required_for_problems_and_questions( client_request, mocker, data, ticket_type, ): mocker.patch("app.main.views.feedback.zendesk_client") client_request.logout() page = client_request.post( "main.feedback", ticket_type=ticket_type, _data=data, _expected_status=200 ) assert normalize_spaces(page.select_one(".usa-error-message").text) == ( "Error: Cannot be empty" ) @freeze_time("2016-12-12 12:00:00.000000") @pytest.mark.parametrize("ticket_type", [PROBLEM_TICKET_TYPE, QUESTION_TICKET_TYPE]) def test_email_address_must_be_valid_if_provided_to_support_form( client_request, mocker, ticket_type, ): client_request.logout() page = client_request.post( "main.feedback", ticket_type=ticket_type, _data={ "feedback": "blah", "email_address": "not valid", }, _expected_status=200, ) assert normalize_spaces(page.select_one("span.usa-error-message").text) == ( "Error: Enter a valid email address" ) @pytest.mark.parametrize( ("ticket_type", "severe", "is_in_business_hours", "is_out_of_hours_emergency"), [ # business hours, never an emergency (PROBLEM_TICKET_TYPE, "yes", True, False), (QUESTION_TICKET_TYPE, "yes", True, False), (PROBLEM_TICKET_TYPE, "no", True, False), (QUESTION_TICKET_TYPE, "no", True, False), # out of hours, if the user says it’s not an emergency (PROBLEM_TICKET_TYPE, "no", False, False), (QUESTION_TICKET_TYPE, "no", False, False), # out of hours, only problems can be emergencies (PROBLEM_TICKET_TYPE, "yes", False, True), (QUESTION_TICKET_TYPE, "yes", False, False), ], ) def test_urgency( client_request, mock_get_non_empty_organizations_and_services_for_user, mocker, ticket_type, severe, is_in_business_hours, is_out_of_hours_emergency, ): mocker.patch( "app.main.views.feedback.in_business_hours", return_value=is_in_business_hours ) mock_ticket = mocker.patch("app.main.views.feedback.NotifySupportTicket") mocker.patch( "app.main.views.feedback.zendesk_client.send_ticket_to_zendesk", autospec=True, ) client_request.post( "main.feedback", ticket_type=ticket_type, severe=severe, _data={"feedback": "blah", "email_address": "test@example.com"}, _expected_status=302, _expected_redirect=url_for( "main.thanks", out_of_hours_emergency=is_out_of_hours_emergency, email_address_provided=True, ), ) assert mock_ticket.call_args[1]["p1"] == is_out_of_hours_emergency ids, params = zip( *[ ( "non-logged in users always have to triage", ( GENERAL_TICKET_TYPE, False, False, True, 302, partial(url_for, "main.triage", ticket_type=GENERAL_TICKET_TYPE), ), ), ( "trial services are never high priority", (PROBLEM_TICKET_TYPE, False, True, False, 200, no_redirect()), ), ( "we can triage in hours", (PROBLEM_TICKET_TYPE, True, True, True, 200, no_redirect()), ), ( "only problems are high priority", (QUESTION_TICKET_TYPE, False, True, True, 200, no_redirect()), ), ( "should triage out of hours", ( PROBLEM_TICKET_TYPE, False, True, True, 302, partial(url_for, "main.triage", ticket_type=PROBLEM_TICKET_TYPE), ), ), ] ) @pytest.mark.parametrize( ( "ticket_type", "is_in_business_hours", "logged_in", "has_live_services", "expected_status", "expected_redirect", ), params, ids=ids, ) def test_redirects_to_triage( client_request, api_user_active, mocker, mock_get_user, ticket_type, is_in_business_hours, logged_in, has_live_services, expected_status, expected_redirect, ): mocker.patch( "app.models.user.User.live_services", new_callable=PropertyMock, return_value=[{}, {}] if has_live_services else [], ) mocker.patch( "app.main.views.feedback.in_business_hours", return_value=is_in_business_hours ) if not logged_in: client_request.logout() client_request.get( "main.feedback", ticket_type=ticket_type, _expected_status=expected_status, _expected_redirect=expected_redirect(), ) @pytest.mark.parametrize( ("ticket_type", "expected_h1"), [ (PROBLEM_TICKET_TYPE, "Report a problem"), (GENERAL_TICKET_TYPE, "Contact Notify.gov support"), ], ) def test_options_on_triage_page( client_request, ticket_type, expected_h1, ): page = client_request.get("main.triage", ticket_type=ticket_type) assert normalize_spaces(page.select_one("h1").text) == expected_h1 assert page.select("form input[type=radio]")[0]["value"] == "yes" assert page.select("form input[type=radio]")[1]["value"] == "no" def test_doesnt_lose_message_if_post_across_closing( client_request, mocker, ): mocker.patch("app.models.user.User.live_services", return_value=True) mocker.patch("app.main.views.feedback.in_business_hours", return_value=False) page = client_request.post( "main.feedback", ticket_type=PROBLEM_TICKET_TYPE, _data={"feedback": "foo"}, _expected_status=302, _expected_redirect=url_for(".triage", ticket_type=PROBLEM_TICKET_TYPE), ) with client_request.session_transaction() as session: assert session["feedback_message"] == "foo" page = client_request.get( "main.feedback", ticket_type=PROBLEM_TICKET_TYPE, severe="yes", ) with client_request.session_transaction() as session: assert page.find("textarea", {"name": "feedback"}).text == "\r\nfoo" assert "feedback_message" not in session @pytest.mark.parametrize( ("when", "is_in_business_hours"), [ ("2016-06-06 09:29:59+0100", False), # opening time, summer and winter ("2016-12-12 09:29:59+0000", False), ("2016-06-06 09:30:00+0100", True), ("2016-12-12 09:30:00+0000", True), ("2016-12-12 12:00:00+0000", True), # middle of the day ("2016-12-12 17:29:59+0000", True), # closing time ("2016-12-12 17:30:00+0000", False), ("2016-12-10 12:00:00+0000", False), # Saturday ("2016-12-11 12:00:00+0000", False), # Sunday ("2016-01-01 12:00:00+0000", False), # Bank holiday ], ) def test_in_business_hours(when, is_in_business_hours): with freeze_time(when): assert in_business_hours() == is_in_business_hours @pytest.mark.parametrize( "ticket_type", [ GENERAL_TICKET_TYPE, PROBLEM_TICKET_TYPE, ], ) @pytest.mark.parametrize( ("choice", "expected_redirect_param"), [ ("yes", "yes"), ("no", "no"), ], ) def test_triage_redirects_to_correct_url( client_request, ticket_type, choice, expected_redirect_param, ): client_request.post( "main.triage", ticket_type=ticket_type, _data={"severe": choice}, _expected_status=302, _expected_redirect=url_for( "main.feedback", ticket_type=ticket_type, severe=expected_redirect_param, ), ) @pytest.mark.parametrize( ("extra_args", "expected_back_link"), [ ( {"severe": "yes"}, partial(url_for, "main.triage", ticket_type=PROBLEM_TICKET_TYPE), ), ( {"severe": "no"}, partial(url_for, "main.triage", ticket_type=PROBLEM_TICKET_TYPE), ), ({"severe": "foo"}, partial(url_for, "main.support")), # hacking the URL ({}, partial(url_for, "main.support")), ], ) @freeze_time("2012-12-12 12:12") def test_back_link_from_form( client_request, mock_get_non_empty_organizations_and_services_for_user, extra_args, expected_back_link, ): page = client_request.get( "main.feedback", ticket_type=PROBLEM_TICKET_TYPE, **extra_args ) assert page.select_one(".usa-back-link")["href"] == expected_back_link() assert normalize_spaces(page.select_one("h1").text) == "Report a problem" @pytest.mark.parametrize( ( "is_in_business_hours", "severe", "expected_status_code", "expected_redirect", "expected_status_code_when_logged_in", "expected_redirect_when_logged_in", ), [ (True, "yes", 200, no_redirect(), 200, no_redirect()), (True, "no", 200, no_redirect(), 200, no_redirect()), ( False, "no", 200, no_redirect(), 200, no_redirect(), ), # Treat empty query param as mangled URL – ask question again ( False, "", 302, partial(url_for, "main.triage", ticket_type=PROBLEM_TICKET_TYPE), 302, partial(url_for, "main.triage", ticket_type=PROBLEM_TICKET_TYPE), ), # User hasn’t answered the triage question ( False, None, 302, partial(url_for, "main.triage", ticket_type=PROBLEM_TICKET_TYPE), 302, partial(url_for, "main.triage", ticket_type=PROBLEM_TICKET_TYPE), ), # Escalation is needed for non-logged-in users ( False, "yes", 302, partial(url_for, "main.bat_phone"), 200, no_redirect(), ), ], ) def test_should_be_shown_the_bat_email( client_request, active_user_with_permissions, mocker, service_one, mock_get_non_empty_organizations_and_services_for_user, is_in_business_hours, severe, expected_status_code, expected_redirect, expected_status_code_when_logged_in, expected_redirect_when_logged_in, ): mocker.patch( "app.main.views.feedback.in_business_hours", return_value=is_in_business_hours ) feedback_page = url_for( "main.feedback", ticket_type=PROBLEM_TICKET_TYPE, severe=severe ) client_request.logout() client_request.get_url( feedback_page, _expected_status=expected_status_code, _expected_redirect=expected_redirect(), ) # logged in users should never be redirected to the bat email page client_request.login(active_user_with_permissions) client_request.get_url( feedback_page, _expected_status=expected_status_code_when_logged_in, _expected_redirect=expected_redirect_when_logged_in(), ) @pytest.mark.parametrize( ( "severe", "expected_status_code", "expected_redirect", "expected_status_code_when_logged_in", "expected_redirect_when_logged_in", ), [ # User hasn’t answered the triage question ( None, 302, partial(url_for, "main.triage", ticket_type=GENERAL_TICKET_TYPE), 302, partial(url_for, "main.triage", ticket_type=GENERAL_TICKET_TYPE), ), # Escalation is needed for non-logged-in users ( "yes", 302, partial(url_for, "main.bat_phone"), 200, no_redirect(), ), ], ) def test_should_be_shown_the_bat_email_for_general_questions( client_request, active_user_with_permissions, mocker, service_one, mock_get_non_empty_organizations_and_services_for_user, severe, expected_status_code, expected_redirect, expected_status_code_when_logged_in, expected_redirect_when_logged_in, ): mocker.patch("app.main.views.feedback.in_business_hours", return_value=False) feedback_page = url_for( "main.feedback", ticket_type=GENERAL_TICKET_TYPE, severe=severe ) client_request.logout() client_request.get_url( feedback_page, _expected_status=expected_status_code, _expected_redirect=expected_redirect(), ) # logged in users should never be redirected to the bat email page client_request.login(active_user_with_permissions) client_request.get_url( feedback_page, _expected_status=expected_status_code_when_logged_in, _expected_redirect=expected_redirect_when_logged_in(), ) def test_bat_email_page( client_request, active_user_with_permissions, mocker, service_one, ): bat_phone_page = "main.bat_phone" client_request.logout() page = client_request.get(bat_phone_page) assert page.select_one(".usa-back-link").text == "Back" assert page.select_one(".usa-back-link")["href"] == url_for("main.support") assert page.select("main a")[1].text == "Fill in this form" assert page.select("main a")[1]["href"] == url_for( "main.feedback", ticket_type=PROBLEM_TICKET_TYPE, severe="no" ) next_page = client_request.get_url(page.select("main a")[1]["href"]) assert next_page.h1.text.strip() == "Report a problem" client_request.login(active_user_with_permissions) client_request.get( bat_phone_page, _expected_redirect=url_for("main.feedback", ticket_type=PROBLEM_TICKET_TYPE), ) @pytest.mark.parametrize( ("out_of_hours_emergency", "email_address_provided", "out_of_hours", "message"), [ # Out of hours emergencies trump everything else ( True, True, True, "We’ll reply in the next 30 minutes.", ), ( True, False, False, # Not a real scenario "We’ll reply in the next 30 minutes.", ), # Anonymous tickets don’t promise a reply ( False, False, False, "We’ll aim to read your message in the next 30 minutes.", ), ( False, False, True, "We’ll read your message when we’re back in the office.", ), # When we look at your ticket depends on whether we’re in normal # business hours ( False, True, False, "We’ll aim to read your message in the next 30 minutes and we’ll reply within one working day.", ), (False, True, True, "We’ll reply within one working day."), ], ) def test_thanks( client_request, mocker, api_user_active, mock_get_user, out_of_hours_emergency, email_address_provided, out_of_hours, message, ): mocker.patch( "app.main.views.feedback.in_business_hours", return_value=(not out_of_hours) ) page = client_request.get( "main.thanks", out_of_hours_emergency=out_of_hours_emergency, email_address_provided=email_address_provided, ) assert normalize_spaces(page.find("main").find("p").text) == message