import json import uuid from functools import partial from urllib.parse import parse_qs, quote, urlparse import pytest from bs4 import BeautifulSoup from flask import url_for from freezegun import freeze_time from app.main.views.jobs import get_status_filters, get_time_left from app.models.service import Service from tests.conftest import ( SERVICE_ONE_ID, active_caseworking_user, active_user_view_permissions, active_user_with_permissions, mock_get_notifications, normalize_spaces, ) @pytest.mark.parametrize( "user,extra_args,expected_update_endpoint,expected_limit_days,page_title", [ ( active_user_view_permissions, {'message_type': 'email'}, '/email.json', 7, 'Emails', ), ( active_user_view_permissions, {'message_type': 'sms'}, '/sms.json', 7, 'Text messages', ), ( active_caseworking_user, {}, '.json', None, 'Sent messages', ), ] ) @pytest.mark.parametrize( "status_argument, expected_api_call", [ ( '', [ 'created', 'pending', 'sending', 'pending-virus-check', 'delivered', 'sent', 'returned-letter', 'failed', 'temporary-failure', 'permanent-failure', 'technical-failure', 'virus-scan-failed', 'validation-failed' ] ), ( 'sending', ['sending', 'created', 'pending', 'pending-virus-check'] ), ( 'delivered', ['delivered', 'sent', 'returned-letter'] ), ( 'failed', [ 'failed', 'temporary-failure', 'permanent-failure', 'technical-failure', 'virus-scan-failed', 'validation-failed' ] ) ] ) @pytest.mark.parametrize( "page_argument, expected_page_argument", [ (1, 1), (22, 22), (None, 1) ] ) @pytest.mark.parametrize( "to_argument, expected_to_argument", [ ('', ''), ('+447900900123', '+447900900123'), ('test@example.com', 'test@example.com'), ] ) def test_can_show_notifications( client_request, logged_in_client, service_one, mock_get_notifications, mock_get_service_statistics, mock_get_service_data_retention, mock_has_no_jobs, user, extra_args, expected_update_endpoint, expected_limit_days, page_title, status_argument, expected_api_call, page_argument, expected_page_argument, to_argument, expected_to_argument, mocker, fake_uuid, ): client_request.login(user(fake_uuid)) if expected_to_argument: page = client_request.post( 'main.view_notifications', service_id=service_one['id'], status=status_argument, page=page_argument, _data={ 'to': to_argument }, _expected_status=200, **extra_args ) else: page = client_request.get( 'main.view_notifications', service_id=service_one['id'], status=status_argument, page=page_argument, **extra_args ) text_of_first_row = page.select('tbody tr')[0].text assert '07123456789' in text_of_first_row assert ( 'template content' in text_of_first_row or 'template subject' in text_of_first_row ) assert 'Delivered' in text_of_first_row assert page_title in page.h1.text.strip() path_to_json = page.find("div", {'data-key': 'notifications'})['data-resource'] url = urlparse(path_to_json) assert url.path == '/services/{}/notifications{}'.format( service_one['id'], expected_update_endpoint, ) query_dict = parse_qs(url.query) if status_argument: assert query_dict['status'] == [status_argument] if expected_page_argument: assert query_dict['page'] == [str(expected_page_argument)] assert 'to' not in query_dict mock_get_notifications.assert_called_with( limit_days=expected_limit_days, page=expected_page_argument, service_id=service_one['id'], status=expected_api_call, template_type=list(extra_args.values()), to=expected_to_argument, ) json_response = logged_in_client.get(url_for( 'main.get_notifications_as_json', service_id=service_one['id'], status=status_argument, **extra_args )) json_content = json.loads(json_response.get_data(as_text=True)) assert json_content.keys() == {'counts', 'notifications', 'service_data_retention_days'} def test_can_show_notifications_if_data_retention_not_available( client_request, mock_get_notifications, mock_get_service_statistics, mock_has_no_jobs, ): page = client_request.get( 'main.view_notifications', service_id=SERVICE_ONE_ID, status='sending,delivered,failed', ) assert page.h1.text.strip() == 'Messages' @pytest.mark.parametrize('user, query_parameters, expected_download_link', [ ( active_user_with_permissions, {}, partial( url_for, '.download_notifications_csv', message_type=None, ), ), ( active_user_with_permissions, {'status': 'failed'}, partial( url_for, '.download_notifications_csv', status='failed' ), ), ( active_user_with_permissions, {'message_type': 'sms'}, partial( url_for, '.download_notifications_csv', message_type='sms', ), ), ( active_user_view_permissions, {}, partial( url_for, '.download_notifications_csv', ), ), ( active_caseworking_user, {}, lambda service_id: None, ), ]) def test_link_to_download_notifications( client_request, fake_uuid, mock_get_notifications, mock_get_service_statistics, mock_get_service_data_retention, mock_has_no_jobs, user, query_parameters, expected_download_link, ): client_request.login(user(fake_uuid)) page = client_request.get( 'main.view_notifications', service_id=SERVICE_ONE_ID, **query_parameters ) download_link = page.select_one('a[download=download]') assert ( download_link['href'] if download_link else None ) == expected_download_link(service_id=SERVICE_ONE_ID) def test_download_not_available_to_users_without_dashboard( client_request, active_caseworking_user, ): client_request.login(active_caseworking_user) client_request.get( 'main.download_notifications_csv', service_id=SERVICE_ONE_ID, _expected_status=403, ) def test_letters_with_status_virus_scan_failed_shows_a_failure_description( mocker, active_user_with_permissions, logged_in_client, service_one, mock_get_service_statistics, mock_get_service_data_retention, ): mock_get_notifications( mocker, active_user_with_permissions, is_precompiled_letter=True, noti_status='virus-scan-failed' ) response = logged_in_client.get(url_for( 'main.view_notifications', service_id=service_one['id'], message_type='letter', status='', )) assert response.status_code == 200 page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') error_description = page.find('div', attrs={'class': 'table-field-status-error'}).text.strip() assert 'Virus detected\n' in error_description @pytest.mark.parametrize('letter_status', [ 'pending-virus-check', 'virus-scan-failed' ]) def test_should_not_show_preview_link_for_precompiled_letters_in_virus_states( mocker, active_user_with_permissions, logged_in_client, service_one, mock_get_service_statistics, mock_get_service_data_retention, letter_status, ): mock_get_notifications( mocker, active_user_with_permissions, is_precompiled_letter=True, noti_status=letter_status ) response = logged_in_client.get(url_for( 'main.view_notifications', service_id=service_one['id'], message_type='letter', status='', )) page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') assert not page.find('a', attrs={'class': 'file-list-filename'}) def test_shows_message_when_no_notifications( client_request, mock_get_service_statistics, mock_get_service_data_retention, mock_get_notifications_with_no_notifications, ): page = client_request.get( 'main.view_notifications', service_id=SERVICE_ONE_ID, message_type='sms', ) assert normalize_spaces(page.select('tbody tr')[0].text) == ( 'No messages found (messages are kept for 7 days)' ) @pytest.mark.parametrize(( 'initial_query_arguments,' 'form_post_data,' 'expected_search_box_label,' 'expected_search_box_contents' ), [ ( {}, {}, 'Search by phone number or email address', '', ), ( { 'message_type': 'sms', }, {}, 'Search by phone number', '', ), ( { 'message_type': 'sms', }, { 'to': '+33(0)5-12-34-56-78', }, 'Search by phone number', '+33(0)5-12-34-56-78', ), ( { 'status': 'failed', 'message_type': 'email', 'page': '99', }, { 'to': 'test@example.com', }, 'Search by email address', 'test@example.com', ), ]) def test_search_recipient_form( logged_in_client, mock_get_notifications, mock_get_service_statistics, mock_get_service_data_retention, initial_query_arguments, form_post_data, expected_search_box_label, expected_search_box_contents, ): response = logged_in_client.post( url_for( 'main.view_notifications', service_id=SERVICE_ONE_ID, **initial_query_arguments ), data=form_post_data ) assert response.status_code == 200 page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') assert page.find("form")['method'] == 'post' action_url = page.find("form")['action'] url = urlparse(action_url) assert url.path == '/services/{}/notifications/{}'.format( SERVICE_ONE_ID, initial_query_arguments.get('message_type', '') ).rstrip('/') query_dict = parse_qs(url.query) assert query_dict == {} assert page.select_one('label[for=to]').text.strip() == expected_search_box_label recipient_inputs = page.select("input[name=to]") assert(len(recipient_inputs) == 2) for field in recipient_inputs: assert field['value'] == expected_search_box_contents def test_should_show_notifications_for_a_service_with_next_previous( logged_in_client, service_one, active_user_with_permissions, mock_get_notifications_with_previous_next, mock_get_service_statistics, mock_get_service_data_retention, mocker, ): response = logged_in_client.get(url_for( 'main.view_notifications', service_id=service_one['id'], message_type='sms', page=2 )) assert response.status_code == 200 page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') next_page_link = page.find('a', {'rel': 'next'}) prev_page_link = page.find('a', {'rel': 'previous'}) assert ( url_for('main.view_notifications', service_id=service_one['id'], message_type='sms', page=3) in next_page_link['href'] ) assert 'Next page' in next_page_link.text.strip() assert 'page 3' in next_page_link.text.strip() assert ( url_for('main.view_notifications', service_id=service_one['id'], message_type='sms', page=1) in prev_page_link['href'] ) assert 'Previous page' in prev_page_link.text.strip() assert 'page 1' in prev_page_link.text.strip() @pytest.mark.parametrize( "job_created_at, expected_message", [ ("2016-01-10 11:09:00.000000+00:00", "Data available for 7 days"), ("2016-01-04 11:09:00.000000+00:00", "Data available for 1 day"), ("2016-01-03 11:09:00.000000+00:00", "Data available for 12 hours"), ("2016-01-02 23:59:59.000000+00:00", "Data no longer available") ] ) @freeze_time("2016-01-10 12:00:00.000000") def test_time_left(job_created_at, expected_message): assert get_time_left(job_created_at) == expected_message STATISTICS = { 'sms': { 'requested': 6, 'failed': 2, 'delivered': 1 } } def test_get_status_filters_calculates_stats(client): ret = get_status_filters(Service({'id': 'foo'}), 'sms', STATISTICS) assert {label: count for label, _option, _link, count in ret} == { 'total': 6, 'sending': 3, 'failed': 2, 'delivered': 1 } def test_get_status_filters_in_right_order(client): ret = get_status_filters(Service({'id': 'foo'}), 'sms', STATISTICS) assert [label for label, _option, _link, _count in ret] == [ 'total', 'sending', 'delivered', 'failed' ] def test_get_status_filters_constructs_links(client): ret = get_status_filters(Service({'id': 'foo'}), 'sms', STATISTICS) link = ret[0][2] assert link == '/services/foo/notifications/sms?status={}'.format(quote('sending,delivered,failed')) def test_html_contains_notification_id( logged_in_client, service_one, active_user_with_permissions, mock_get_notifications, mock_get_service_statistics, mock_get_service_data_retention, mocker, ): response = logged_in_client.get(url_for( 'main.view_notifications', service_id=service_one['id'], message_type='sms', status='') ) assert response.status_code == 200 page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') notifications = page.tbody.find_all('tr') for tr in notifications: assert uuid.UUID(tr.attrs['id']) def test_html_contains_links_for_failed_notifications( client_request, active_user_with_permissions, mock_get_service_statistics, mock_get_service_data_retention, mocker, ): mock_get_notifications(mocker, active_user_with_permissions, noti_status='technical-failure') response = client_request.get( 'main.view_notifications', service_id=SERVICE_ONE_ID, message_type='sms', status='sending%2Cdelivered%2Cfailed' ) notifications = response.tbody.find_all('tr') for tr in notifications: link_text = tr.find('div', class_='table-field-status-error').find('a').text assert normalize_spaces(link_text) == 'Technical failure' def test_redacts_templates_that_should_be_redacted( client_request, mocker, active_user_with_permissions, mock_get_service_statistics, mock_get_service_data_retention, ): mock_get_notifications( mocker, active_user_with_permissions, template_content="hello ((name))", personalisation={'name': 'Jo'}, redact_personalisation=True, ) page = client_request.get( 'main.view_notifications', service_id=SERVICE_ONE_ID, message_type='sms', ) assert normalize_spaces(page.select('tbody tr th')[0].text) == ( '07123456789 hello hidden' ) @pytest.mark.parametrize( "message_type, tablist_visible, search_bar_visible", [ ('email', True, True), ('sms', True, True), ('letter', False, False) ] ) def test_big_numbers_and_search_dont_show_for_letters( client_request, service_one, mock_get_notifications, active_user_with_permissions, mock_get_service_statistics, mock_get_service_data_retention, message_type, tablist_visible, search_bar_visible ): page = client_request.get( 'main.view_notifications', service_id=service_one['id'], message_type=message_type, status='', page=1, ) assert (len(page.select("[role=tablist]")) > 0) == tablist_visible assert (len(page.select("[type=search]")) > 0) == search_bar_visible @freeze_time("2017-09-27 16:30:00.000000") @pytest.mark.parametrize( "message_type, status, expected_hint_status, single_line", [ ('email', 'created', 'Sending since 27 September at 5:30pm', True), ('email', 'sending', 'Sending since 27 September at 5:30pm', True), ('email', 'temporary-failure', 'Inbox not accepting messages right now 27 September at 5:31pm', False), ('email', 'permanent-failure', 'Email address doesn’t exist 27 September at 5:31pm', False), ('email', 'delivered', 'Delivered 27 September at 5:31pm', True), ('sms', 'created', 'Sending since 27 September at 5:30pm', True), ('sms', 'sending', 'Sending since 27 September at 5:30pm', True), ('sms', 'temporary-failure', 'Phone not accepting messages right now 27 September at 5:31pm', False), ('sms', 'permanent-failure', 'Phone number doesn’t exist 27 September at 5:31pm', False), ('sms', 'delivered', 'Delivered 27 September at 5:31pm', True), ('letter', 'created', '27 September at 5:30pm', True), ('letter', 'pending-virus-check', '27 September at 5:30pm', True), ('letter', 'sending', '27 September at 5:30pm', True), ('letter', 'delivered', '27 September at 5:30pm', True), ('letter', 'received', '27 September at 5:30pm', True), ('letter', 'accepted', '27 September at 5:30pm', True), ('letter', 'cancelled', '27 September at 5:30pm', False), # The API won’t return cancelled letters ('letter', 'permanent-failure', '27 September at 5:31pm', False), # Deprecated for ‘cancelled’ ('letter', 'temporary-failure', '27 September at 5:30pm', False), # Not currently a real letter status ('letter', 'virus-scan-failed', 'Virus detected 27 September at 5:30pm', False), ('letter', 'validation-failed', 'Validation failed 27 September at 5:30pm', False), ('letter', 'technical-failure', 'Technical failure 27 September at 5:30pm', False), ] ) def test_sending_status_hint_displays_correctly_on_notifications_page( client_request, service_one, active_user_with_permissions, mock_get_service_statistics, mock_get_service_data_retention, message_type, status, expected_hint_status, single_line, mocker ): mock_get_notifications(mocker, True, diff_template_type=message_type, noti_status=status) page = client_request.get( 'main.view_notifications', service_id=service_one['id'], message_type=message_type ) assert normalize_spaces(page.select(".table-field-right-aligned")[0].text) == expected_hint_status assert bool(page.select('.align-with-message-body')) is single_line @pytest.mark.parametrize("is_precompiled_letter,expected_hint", [ (True, "Provided as PDF"), (False, "template subject") ]) def test_should_expected_hint_for_letters( logged_in_client, service_one, active_user_with_permissions, mock_get_service_statistics, mock_get_service_data_retention, mocker, fake_uuid, is_precompiled_letter, expected_hint ): mock_get_notifications( mocker, active_user_with_permissions, is_precompiled_letter=is_precompiled_letter) response = logged_in_client.get(url_for( 'main.view_notifications', service_id=SERVICE_ONE_ID, message_type='letter' )) page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') assert page.find('p', {'class': 'file-list-hint'}).text.strip() == expected_hint