diff --git a/app/main/views/uploads.py b/app/main/views/uploads.py index e1093f708..8da369bf5 100644 --- a/app/main/views/uploads.py +++ b/app/main/views/uploads.py @@ -7,6 +7,7 @@ from functools import partial from io import BytesIO from zipfile import BadZipFile +from botocore.exceptions import ClientError from flask import ( abort, current_app, @@ -257,7 +258,20 @@ def _get_error_from_upload_form(form_errors): def uploaded_letter_preview(service_id, file_id): re_upload_form = PDFUploadForm() - metadata = get_letter_metadata(service_id, file_id) + try: + metadata = get_letter_metadata(service_id, file_id) + except ClientError as e: + # if the file's not there, it's probably because we've already created the notification and the letter has been + # moved to the normal letters-pdf bucket. So lets just bounce out to the notification page + if e.response['Error']['Code'] == 'NoSuchKey': + return redirect(url_for( + '.view_notification', + service_id=service_id, + notification_id=file_id, + )) + else: + raise + original_filename = metadata.get('filename') page_count = metadata.get('page_count') status = metadata.get('status') diff --git a/tests/app/main/views/test_uploads.py b/tests/app/main/views/test_uploads.py deleted file mode 100644 index 941321500..000000000 --- a/tests/app/main/views/test_uploads.py +++ /dev/null @@ -1,1583 +0,0 @@ -import re -import uuid -from io import BytesIO -from unittest.mock import ANY, Mock - -import pytest -from flask import make_response, url_for -from freezegun import freeze_time -from requests import RequestException - -from app.s3_client.s3_letter_upload_client import LetterMetadata -from app.utils import normalize_spaces -from tests.conftest import ( - SERVICE_ONE_ID, - create_active_caseworking_user, - create_active_user_with_permissions, - create_platform_admin_user, -) - - -@pytest.mark.parametrize('extra_permissions', ( - pytest.param( - [], - marks=pytest.mark.xfail(raises=AssertionError), - ), - pytest.param( - ['upload_letters'], - marks=pytest.mark.xfail(raises=AssertionError), - ), - ['letter'], - ['letter', 'upload_letters'], -)) -def test_upload_letters_button_only_with_letters_permission( - client_request, - service_one, - mock_get_uploads, - mock_get_jobs, - mock_get_no_contact_lists, - extra_permissions, -): - service_one['permissions'] += extra_permissions - page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID) - assert page.find('a', text=re.compile('Upload a letter')) - - -@pytest.mark.parametrize('user', ( - create_platform_admin_user(), - create_active_user_with_permissions(), -)) -def test_all_users_have_upload_contact_list( - client_request, - mock_get_uploads, - mock_get_jobs, - mock_get_no_contact_lists, - user, -): - client_request.login(user) - page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID) - button = page.find('a', text=re.compile('Upload an emergency contact list')) - assert button - assert button['href'] == url_for( - 'main.upload_contact_list', service_id=SERVICE_ONE_ID, - ) - - -@pytest.mark.parametrize('extra_permissions, expected_empty_message', ( - ([], ( - 'You have not uploaded any files recently.' - )), - (['letter'], ( - 'You have not uploaded any files recently. ' - 'Upload a letter and Notify will print, pack and post it for you.' - )), -)) -def test_get_upload_hub_with_no_uploads( - mocker, - client_request, - service_one, - mock_get_no_uploads, - mock_get_no_contact_lists, - extra_permissions, - expected_empty_message, -): - mocker.patch('app.job_api_client.get_jobs', return_value={'data': []}) - service_one['permissions'] += extra_permissions - page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID) - assert normalize_spaces(' '.join( - paragraph.text for paragraph in page.select('main p') - )) == expected_empty_message - assert not page.select('.file-list-filename') - - -@freeze_time('2017-10-10 10:10:10') -def test_get_upload_hub_page( - mocker, - client_request, - service_one, - mock_get_uploads, - mock_get_no_contact_lists, -): - mocker.patch('app.job_api_client.get_jobs', return_value={'data': []}) - service_one['permissions'] += ['letter', 'upload_letters'] - page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID) - assert page.find('h1').text == 'Uploads' - assert page.find('a', text=re.compile('Upload a letter')).attrs['href'] == url_for( - 'main.upload_letter', service_id=SERVICE_ONE_ID - ) - - uploads = page.select('tbody tr') - - assert len(uploads) == 3 - - assert normalize_spaces(uploads[0].text.strip()) == ( - 'Uploaded letters ' - 'Printing today at 5:30pm ' - '33 letters' - ) - assert uploads[0].select_one('a.file-list-filename-large')['href'] == url_for( - 'main.uploaded_letters', - service_id=SERVICE_ONE_ID, - letter_print_day='2017-10-10', - ) - - assert normalize_spaces(uploads[1].text.strip()) == ( - 'some.csv ' - 'Sent 1 January 2016 at 11:09am ' - '0 sending 8 delivered 2 failed' - ) - assert uploads[1].select_one('a.file-list-filename-large')['href'] == ( - '/services/{}/jobs/job_id_1'.format(SERVICE_ONE_ID) - ) - - assert normalize_spaces(uploads[2].text.strip()) == ( - 'some.pdf ' - 'Sent 1 January 2016 at 11:09am ' - 'Firstname Lastname ' - '123 Example Street' - ) - assert normalize_spaces(str(uploads[2].select_one('.govuk-body'))) == ( - '

' - 'Firstname Lastname
' - '123 Example Street
' - '

' - ) - assert uploads[2].select_one('a.file-list-filename-large')['href'] == ( - '/services/{}/notification/letter_id_1'.format(SERVICE_ONE_ID) - ) - - -@freeze_time('2020-02-02 14:00') -def test_get_uploaded_letters( - mocker, - client_request, - service_one, - mock_get_uploaded_letters, -): - page = client_request.get( - 'main.uploaded_letters', - service_id=SERVICE_ONE_ID, - letter_print_day='2020-02-02' - ) - assert page.select_one('.govuk-back-link')['href'] == url_for( - 'main.uploads', - service_id=SERVICE_ONE_ID, - ) - assert normalize_spaces( - page.select_one('h1').text - ) == ( - 'Uploaded letters' - ) - assert normalize_spaces( - page.select('main p')[0].text - ) == ( - '1,234 letters' - ) - assert normalize_spaces( - page.select('main p')[1].text - ) == ( - 'Printing starts today at 5:30pm' - ) - - assert [ - normalize_spaces(row.text) - for row in page.select('tbody tr') - ] == [ - ( - 'Homer-Simpson.pdf ' - '742 Evergreen Terrace ' - '2 February at 1:59pm' - ), - ( - 'Kevin-McCallister.pdf ' - '671 Lincoln Avenue, Winnetka ' - '2 February at 12:59pm' - ), - ] - - assert [ - link['href'] for link in page.select('tbody tr a') - ] == [ - url_for( - 'main.view_notification', - service_id=SERVICE_ONE_ID, - notification_id='03e34025-be54-4d43-8e6a-fb1ea0fd1f29', - from_uploaded_letters='2020-02-02', - ), - url_for( - 'main.view_notification', - service_id=SERVICE_ONE_ID, - notification_id='fc090d91-e761-4464-9041-9c4594c96a35', - from_uploaded_letters='2020-02-02', - ), - ] - - next_page_link = page.select_one('a[rel=next]') - prev_page_link = page.select_one('a[rel=previous]') - assert next_page_link['href'] == url_for( - 'main.uploaded_letters', service_id=SERVICE_ONE_ID, letter_print_day='2020-02-02', page=2 - ) - assert normalize_spaces(next_page_link.text) == ( - 'Next page ' - 'page 2' - ) - assert prev_page_link['href'] == url_for( - 'main.uploaded_letters', service_id=SERVICE_ONE_ID, letter_print_day='2020-02-02', page=0 - ) - assert normalize_spaces(prev_page_link.text) == ( - 'Previous page ' - 'page 0' - ) - - mock_get_uploaded_letters.assert_called_once_with( - SERVICE_ONE_ID, - letter_print_day='2020-02-02', - page=1, - ) - - -@freeze_time('2020-02-02 14:00') -def test_get_empty_uploaded_letters_page( - mocker, - client_request, - service_one, - mock_get_no_uploaded_letters, -): - page = client_request.get( - 'main.uploaded_letters', - service_id=SERVICE_ONE_ID, - letter_print_day='2020-02-02' - ) - page.select_one('main table') - - assert not page.select('tbody tr') - assert not page.select_one('a[rel=next]') - assert not page.select_one('a[rel=previous]') - - -@freeze_time('2020-02-02') -def test_get_uploaded_letters_passes_through_page_argument( - mocker, - client_request, - service_one, - mock_get_uploaded_letters, -): - client_request.get( - 'main.uploaded_letters', - service_id=SERVICE_ONE_ID, - letter_print_day='2020-02-02', - page=99, - ) - mock_get_uploaded_letters.assert_called_once_with( - SERVICE_ONE_ID, - letter_print_day='2020-02-02', - page=99, - ) - - -def test_get_uploaded_letters_404s_for_bad_page_arguments( - mocker, - client_request, -): - client_request.get( - 'main.uploaded_letters', - service_id=SERVICE_ONE_ID, - letter_print_day='2020-02-02', - page='one', - _expected_status=404, - ) - - -def test_get_uploaded_letters_404s_for_invalid_date( - mocker, - client_request, -): - client_request.get( - 'main.uploaded_letters', - service_id=SERVICE_ONE_ID, - letter_print_day='1234-56-78', - _expected_status=404, - ) - - -def test_get_upload_letter(client_request): - page = client_request.get('main.upload_letter', service_id=SERVICE_ONE_ID) - - assert page.find('h1').text == 'Upload a letter' - assert page.find('input', class_='file-upload-field') - assert page.select('main button[type=submit]') - assert normalize_spaces(page.find('label', class_='file-upload-button').text) == 'Choose file' - - -@pytest.mark.parametrize('extra_permissions, expected_allow_international', ( - ([], False), - (['international_letters'], True), -)) -def test_post_upload_letter_redirects_for_valid_file( - mocker, - active_user_with_permissions, - service_one, - client_request, - fake_uuid, - extra_permissions, - expected_allow_international, -): - mocker.patch('uuid.uuid4', return_value=fake_uuid) - antivirus_mock = mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True) - mock_sanitise = mocker.patch( - 'app.main.views.uploads.sanitise_letter', - return_value=Mock( - content='The sanitised content', - json=lambda: {'file': 'VGhlIHNhbml0aXNlZCBjb250ZW50', 'recipient_address': 'The Queen'} - ) - ) - mock_s3 = mocker.patch('app.main.views.uploads.upload_letter_to_s3') - mocker.patch('app.main.views.uploads.get_letter_metadata', return_value=LetterMetadata({ - 'filename': 'tests/test_pdf_files/one_page_pdf.pdf', - 'page_count': '1', - 'status': 'valid', - 'recipient': 'The Queen' - })) - mocker.patch('app.main.views.uploads.service_api_client.get_precompiled_template') - - service_one['restricted'] = False - service_one['permissions'] += extra_permissions - client_request.login(active_user_with_permissions, service=service_one) - - with open('tests/test_pdf_files/one_page_pdf.pdf', 'rb') as file: - page = client_request.post( - 'main.upload_letter', - service_id=SERVICE_ONE_ID, - _data={'file': file}, - _follow_redirects=True, - ) - assert antivirus_mock.called - - mock_s3.assert_called_once_with( - b'The sanitised content', - file_location='service-{}/{}.pdf'.format(SERVICE_ONE_ID, fake_uuid), - status='valid', - page_count=1, - filename='tests/test_pdf_files/one_page_pdf.pdf', - recipient='The Queen', - ) - mock_sanitise.assert_called_once_with( - ANY, - allow_international_letters=expected_allow_international, - ) - - assert 'The Queen' in page.find('div', class_='js-stick-at-bottom-when-scrolling').text - assert page.find('h1').text == 'tests/test_pdf_files/one_page_pdf.pdf' - assert not page.find(id='validation-error-message') - - assert not page.find('input', {'name': 'file_id'}) - assert normalize_spaces(page.select('main button[type=submit]')[0].text) == 'Send 1 letter' - - -def test_post_upload_letter_shows_letter_preview_for_valid_file( - mocker, - active_user_with_permissions, - service_one, - client_request, - fake_uuid, -): - letter_template = {'template_type': 'letter', - 'reply_to_text': '', - 'postage': 'second', - 'subject': 'hi', - 'content': 'my letter'} - - mocker.patch('uuid.uuid4', return_value=fake_uuid) - mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True) - mocker.patch( - 'app.main.views.uploads.sanitise_letter', - return_value=Mock( - content='The sanitised content', - json=lambda: {'file': 'VGhlIHNhbml0aXNlZCBjb250ZW50', 'recipient_address': 'The Queen'} - ) - ) - mocker.patch('app.main.views.uploads.upload_letter_to_s3') - mocker.patch('app.main.views.uploads.pdf_page_count', return_value=3) - mocker.patch('app.main.views.uploads.get_letter_metadata', return_value=LetterMetadata({ - 'filename': 'tests/test_pdf_files/one_page_pdf.pdf', - 'page_count': '3', - 'status': 'valid', - 'recipient': 'The Queen' - })) - mocker.patch('app.main.views.uploads.service_api_client.get_precompiled_template', return_value=letter_template) - - service_one['restricted'] = False - client_request.login(active_user_with_permissions, service=service_one) - - with open('tests/test_pdf_files/one_page_pdf.pdf', 'rb') as file: - page = client_request.post( - 'main.upload_letter', - service_id=SERVICE_ONE_ID, - _data={'file': file}, - _follow_redirects=True, - ) - - assert page.find('h1').text == 'tests/test_pdf_files/one_page_pdf.pdf' - assert len(page.select('.letter-postage')) == 0 - # Check postage radios exists and second class is checked by default - assert page.find('input', id="postage-0", value="first") - assert page.find('input', id="postage-1", value="second").has_attr('checked') - - letter_images = page.select('main img') - assert len(letter_images) == 3 - - for page_no, img in enumerate(letter_images, start=1): - assert img['src'] == url_for( - '.view_letter_upload_as_preview', - service_id=SERVICE_ONE_ID, - file_id=fake_uuid, - page=page_no) - - -def test_upload_international_letter_shows_preview_with_no_choice_of_postage( - mocker, - active_user_with_permissions, - service_one, - client_request, - fake_uuid, -): - letter_template = { - 'template_type': 'letter', - 'reply_to_text': '', - 'postage': 'second', - 'subject': 'hi', - 'content': 'my letter', - } - - mocker.patch('uuid.uuid4', return_value=fake_uuid) - mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True) - mocker.patch('app.main.views.uploads.sanitise_letter', return_value=Mock( - content='The sanitised content', - json=lambda: {'file': 'VGhlIHNhbml0aXNlZCBjb250ZW50', 'recipient_address': 'The Queen'} - )) - mocker.patch('app.main.views.uploads.upload_letter_to_s3') - mocker.patch('app.main.views.uploads.pdf_page_count', return_value=3) - mocker.patch('app.main.views.uploads.get_letter_metadata', return_value=LetterMetadata({ - 'filename': 'tests/test_pdf_files/one_page_pdf.pdf', - 'page_count': '3', - 'status': 'valid', - 'recipient': ( - '123 Example Street\n' - 'Andorra la Vella\n' - 'Andorra' - ), - })) - mocker.patch('app.main.views.uploads.service_api_client.get_precompiled_template', return_value=letter_template) - - service_one['restricted'] = False - client_request.login(active_user_with_permissions, service=service_one) - - with open('tests/test_pdf_files/one_page_pdf.pdf', 'rb') as file: - page = client_request.post( - 'main.upload_letter', - service_id=SERVICE_ONE_ID, - _data={'file': file}, - _follow_redirects=True, - ) - - assert page.find('h1').text == 'tests/test_pdf_files/one_page_pdf.pdf' - assert not page.select('.letter-postage') - assert not page.select('input[type=radio]') - assert normalize_spaces( - page.select_one('.js-stick-at-bottom-when-scrolling').text - ) == ( - 'Recipient: 123 Example Street, Andorra la Vella, Andorra ' - 'Postage: international ' - 'Send 1 letter' - ) - - -def test_post_upload_letter_shows_error_when_file_is_not_a_pdf(client_request): - with open('tests/non_spreadsheet_files/actually_a_png.csv', 'rb') as file: - page = client_request.post( - 'main.upload_letter', - service_id=SERVICE_ONE_ID, - _data={'file': file}, - _expected_status=200 - ) - assert page.find('h1').text == 'Wrong file type' - assert page.find('div', class_='banner-dangerous').find('p').text == 'Save your letter as a PDF and try again.' - assert normalize_spaces(page.find('label', class_='file-upload-button').text) == 'Upload your file again' - - -def test_post_upload_letter_shows_error_when_no_file_uploaded(client_request): - page = client_request.post( - 'main.upload_letter', - service_id=SERVICE_ONE_ID, - _data={'file': ''}, - _expected_status=200 - ) - assert page.find('div', class_='banner-dangerous').find('h1').text == 'You need to choose a file to upload' - assert normalize_spaces(page.find('label', class_='file-upload-button').text) == 'Upload your file again' - - -def test_post_upload_letter_shows_error_when_file_contains_virus(mocker, client_request): - mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=False) - - with open('tests/test_pdf_files/one_page_pdf.pdf', 'rb') as file: - page = client_request.post( - 'main.upload_letter', - service_id=SERVICE_ONE_ID, - _data={'file': file}, - _expected_status=400 - ) - assert page.find('div', class_='banner-dangerous').find('h1').text == 'Your file contains a virus' - assert normalize_spaces(page.find('label', class_='file-upload-button').text) == 'Upload your file again' - - -def test_post_choose_upload_file_when_file_is_too_big(mocker, client_request): - mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True) - - with open('tests/test_pdf_files/big.pdf', 'rb') as file: - page = client_request.post( - 'main.upload_letter', - service_id=SERVICE_ONE_ID, - _data={'file': file}, - _expected_status=400 - ) - assert page.find('div', class_='banner-dangerous').find('h1').text == 'Your file is too big' - assert page.find('div', class_='banner-dangerous').find('p').text == 'Files must be smaller than 2MB.' - assert normalize_spaces(page.find('label', class_='file-upload-button').text) == 'Upload your file again' - - -def test_post_choose_upload_file_when_file_is_malformed(mocker, client_request): - mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True) - - with open('tests/test_pdf_files/no_eof_marker.pdf', 'rb') as file: - page = client_request.post( - 'main.upload_letter', - service_id=SERVICE_ONE_ID, - _data={'file': file}, - _expected_status=400 - ) - assert page.find('div', class_='banner-dangerous').find('h1').text == "There’s a problem with your file" - assert page.find( - 'div', class_='banner-dangerous' - ).find('p').text == 'Notify cannot read this PDF.Save a new copy of your file and try again.' - assert normalize_spaces(page.find('label', class_='file-upload-button').text) == 'Upload your file again' - - -def test_post_upload_letter_with_invalid_file(mocker, client_request, fake_uuid): - mocker.patch('uuid.uuid4', return_value=fake_uuid) - mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True) - mock_s3 = mocker.patch('app.main.views.uploads.upload_letter_to_s3') - - mock_sanitise_response = Mock() - mock_sanitise_response.raise_for_status.side_effect = RequestException(response=Mock(status_code=400)) - mock_sanitise_response.json = lambda: { - "message": "content-outside-printable-area", - "invalid_pages": [1] - } - mocker.patch('app.main.views.uploads.sanitise_letter', return_value=mock_sanitise_response) - mocker.patch('app.main.views.uploads.service_api_client.get_precompiled_template') - mocker.patch('app.main.views.uploads.get_letter_metadata', return_value=LetterMetadata({ - 'filename': 'tests/test_pdf_files/one_page_pdf.pdf', 'page_count': '1', 'status': 'invalid', - 'message': 'content-outside-printable-area', 'invalid_pages': '[1]'})) - - with open('tests/test_pdf_files/one_page_pdf.pdf', 'rb') as file: - file_contents = file.read() - file.seek(0) - - page = client_request.post( - 'main.upload_letter', - service_id=SERVICE_ONE_ID, - _data={'file': file}, - _follow_redirects=True - ) - - mock_s3.assert_called_once_with( - file_contents, - file_location='service-{}/{}.pdf'.format(SERVICE_ONE_ID, fake_uuid), - status='invalid', - page_count=1, - filename='tests/test_pdf_files/one_page_pdf.pdf', - invalid_pages=[1], - message='content-outside-printable-area' - ) - - assert page.find('div', class_='banner-dangerous').find('h1', {"data-error-type": 'content-outside-printable-area'}) - assert not page.find('button', {'class': 'page-footer__button', 'type': 'submit'}) - - -def test_post_upload_letter_shows_letter_preview_for_invalid_file(mocker, client_request, fake_uuid): - letter_template = {'template_type': 'letter', - 'reply_to_text': '', - 'postage': 'first', - 'subject': 'hi', - 'content': 'my letter'} - - mocker.patch('uuid.uuid4', return_value=fake_uuid) - mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True) - mocker.patch('app.main.views.uploads.upload_letter_to_s3') - mock_sanitise_response = Mock() - mock_sanitise_response.raise_for_status.side_effect = RequestException(response=Mock(status_code=400)) - mock_sanitise_response.json = lambda: {"message": "template preview error", "recipient_address": "The Queen"} - mocker.patch('app.main.views.uploads.sanitise_letter', return_value=mock_sanitise_response) - mocker.patch('app.main.views.uploads.service_api_client.get_precompiled_template', return_value=letter_template) - mocker.patch('app.main.views.uploads.get_letter_metadata', return_value=LetterMetadata({ - 'filename': 'tests/test_pdf_files/one_page_pdf.pdf', 'page_count': '1', 'status': 'invalid', - 'message': 'template-preview-error'})) - - with open('tests/test_pdf_files/one_page_pdf.pdf', 'rb') as file: - page = client_request.post( - 'main.upload_letter', - service_id=SERVICE_ONE_ID, - _data={'file': file}, - _follow_redirects=True, - ) - - assert 'The Queen' not in page.text - assert len(page.select('.letter-postage')) == 0 - - assert page.find("a", {"class": "govuk-back-link"})["href"] == "/services/{}/upload-letter".format(SERVICE_ONE_ID) - assert page.find("label", {"class": "file-upload-button"}) - - letter_images = page.select('main img') - assert len(letter_images) == 1 - assert letter_images[0]['src'] == url_for( - '.view_letter_upload_as_preview', - service_id=SERVICE_ONE_ID, - file_id=fake_uuid, - page=1 - ) - - -def test_post_upload_letter_does_not_upload_to_s3_if_template_preview_raises_unknown_error( - mocker, - client_request, - fake_uuid, -): - mocker.patch('uuid.uuid4', return_value=fake_uuid) - mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True) - mock_s3 = mocker.patch('app.main.views.uploads.upload_letter_to_s3') - - mocker.patch('app.main.views.uploads.sanitise_letter', side_effect=RequestException()) - - with pytest.raises(RequestException): - with open('tests/test_pdf_files/one_page_pdf.pdf', 'rb') as file: - client_request.post( - 'main.upload_letter', - service_id=SERVICE_ONE_ID, - _data={'file': file}, - _follow_redirects=True - ) - - assert not mock_s3.called - - -def test_uploaded_letter_preview( - mocker, - active_user_with_permissions, - service_one, - client_request, - fake_uuid, -): - mocker.patch('app.main.views.uploads.service_api_client') - mocker.patch('app.main.views.uploads.get_letter_metadata', return_value=LetterMetadata({ - 'filename': 'my_encoded_filename%C2%A3.pdf', - 'page_count': '1', - 'status': 'valid', - 'recipient': 'Bugs Bunny%0A123 Big Hole%0DLooney Town' # 'Bugs Bunny%0A123 Big Hole\rLooney Town' url encoded - })) - - service_one['restricted'] = False - client_request.login(active_user_with_permissions, service=service_one) - - page = client_request.get( - 'main.uploaded_letter_preview', - service_id=SERVICE_ONE_ID, - file_id=fake_uuid, - ) - - assert page.find('h1').text == 'my_encoded_filename£.pdf' - assert page.find('div', class_='letter-sent') - assert not page.find("label", {"class": "file-upload-button"}) - assert page.find('button', {'class': 'page-footer__button', 'type': 'submit'}) - - -def test_uploaded_letter_preview_does_not_show_send_button_if_service_in_trial_mode( - mocker, - client_request, - fake_uuid, -): - mocker.patch('app.main.views.uploads.service_api_client') - mocker.patch('app.main.views.uploads.get_letter_metadata', return_value=LetterMetadata({ - 'filename': 'my_letter.pdf', 'page_count': '1', 'status': 'valid', 'recipient': 'The Queen'})) - - # client_request uses service_one, which is in trial mode - page = client_request.get( - 'main.uploaded_letter_preview', - service_id=SERVICE_ONE_ID, - file_id=fake_uuid, - original_filename='my_letter.pdf', - page_count=1, - status='valid', - error={}, - _test_page_title=False, - ) - - assert normalize_spaces(page.find('h1').text) == 'You cannot send this letter' - assert page.find('div', class_='letter-sent') - assert normalize_spaces( - page.select_one('.js-stick-at-bottom-when-scrolling p').text - ) == ( - 'Recipient: The Queen' - ) - assert not page.find('form') - assert len(page.select('main button[type=submit]')) == 0 - - -@pytest.mark.parametrize('invalid_pages, page_requested, overlay_expected', ( - ('[1, 2]', 1, True), - ('[1, 2]', 2, True), - ('[1, 2]', 3, False), - ('[]', 1, False), -)) -def test_uploaded_letter_preview_image_shows_overlay_when_content_outside_printable_area_on_a_page( - mocker, - logged_in_client, - mock_get_service, - fake_uuid, - invalid_pages, - page_requested, - overlay_expected, -): - mocker.patch( - 'app.main.views.uploads.get_letter_pdf_and_metadata', - return_value=('pdf_file', { - 'message': 'content-outside-printable-area', - 'invalid_pages': invalid_pages, - }) - ) - template_preview_mock_valid = mocker.patch( - 'app.main.views.uploads.TemplatePreview.from_valid_pdf_file', - return_value=make_response('page.html', 200) - ) - template_preview_mock_invalid = mocker.patch( - 'app.main.views.uploads.TemplatePreview.from_invalid_pdf_file', - return_value=make_response('page.html', 200) - ) - - logged_in_client.get( - url_for( - 'main.view_letter_upload_as_preview', - file_id=fake_uuid, - service_id=SERVICE_ONE_ID, - page=page_requested, - ) - ) - - if overlay_expected: - template_preview_mock_invalid.assert_called_once_with('pdf_file', page_requested) - assert template_preview_mock_valid.called is False - else: - template_preview_mock_valid.assert_called_once_with('pdf_file', page_requested) - assert template_preview_mock_invalid.called is False - - -@pytest.mark.parametrize( - 'metadata', [ - {'message': 'letter-not-a4-portrait-oriented'}, - {'message': 'letter-too-long'}, - {}, - ] -) -def test_uploaded_letter_preview_image_does_not_show_overlay_if_no_content_outside_printable_area( - mocker, - logged_in_client, - mock_get_service, - metadata, - fake_uuid, -): - mocker.patch( - 'app.main.views.uploads.get_letter_pdf_and_metadata', - return_value=('pdf_file', metadata) - ) - template_preview_mock = mocker.patch( - 'app.main.views.uploads.TemplatePreview.from_valid_pdf_file', - return_value=make_response('page.html', 200)) - - logged_in_client.get( - url_for('main.view_letter_upload_as_preview', file_id=fake_uuid, service_id=SERVICE_ONE_ID, page=1) - ) - - template_preview_mock.assert_called_once_with('pdf_file', 1) - - -def test_uploaded_letter_preview_image_400s_for_bad_page_type( - client_request, - fake_uuid, -): - client_request.get( - 'main.view_letter_upload_as_preview', - file_id=fake_uuid, - service_id=SERVICE_ONE_ID, - page='foo', - _test_page_title=False, - _expected_status=400, - ) - - -@pytest.mark.parametrize('address, post_data, expected_postage', ( - ( - 'address', - {'filename': 'my_file.pdf', 'postage': 'first'}, - 'first', - ), - ( - 'address', - {'filename': 'my_file.pdf'}, - 'second', - ), - ( - '123 Example Street\nLiechtenstein', - {'filename': 'my_file.pdf', 'postage': 'first'}, - 'europe', - ), - ( - '123 Example Street\nLiechtenstein', - {'filename': 'my_file.pdf'}, - 'europe', - ), - ( - '123 Example Street\nLesotho', - {'filename': 'my_file.pdf'}, - 'rest-of-world', - ), -)) -def test_send_uploaded_letter_sends_letter_and_redirects_to_notification_page( - mocker, - service_one, - client_request, - fake_uuid, - address, - post_data, - expected_postage, -): - metadata = LetterMetadata({ - 'filename': 'my_file.pdf', - 'page_count': '1', - 'status': 'valid', - 'recipient': address, - }) - - mocker.patch('app.main.views.uploads.get_letter_pdf_and_metadata', return_value=('file', metadata)) - mock_send = mocker.patch('app.main.views.uploads.notification_api_client.send_precompiled_letter') - mocker.patch('app.main.views.uploads.get_letter_metadata', return_value=metadata) - - service_one['permissions'] = ['letter', 'upload_letters'] - - client_request.post( - 'main.send_uploaded_letter', - service_id=SERVICE_ONE_ID, - file_id=fake_uuid, - _data=post_data, - _expected_redirect=url_for( - 'main.view_notification', - service_id=SERVICE_ONE_ID, - notification_id=fake_uuid, - _external=True - ) - ) - mock_send.assert_called_once_with( - SERVICE_ONE_ID, - 'my_file.pdf', - fake_uuid, - expected_postage, - address, - ) - - -@pytest.mark.parametrize('permissions', [ - ['email'], - ['letter'], - ['upload_letters'], -]) -def test_send_uploaded_letter_when_service_does_not_have_correct_permissions( - mocker, - service_one, - client_request, - permissions, - fake_uuid, -): - mocker.patch('app.main.views.uploads.get_letter_pdf_and_metadata', return_value=('file', {'status': 'valid'})) - mock_send = mocker.patch('app.main.views.uploads.notification_api_client.send_precompiled_letter') - - service_one['permissions'] = permissions - - client_request.post( - 'main.send_uploaded_letter', - service_id=SERVICE_ONE_ID, - file_id=fake_uuid, - _data={'filename': 'my_file.pdf', 'postage': 'first'}, - _expected_status=403 - ) - assert not mock_send.called - - -def test_send_uploaded_letter_when_metadata_states_pdf_is_invalid( - mocker, service_one, client_request, fake_uuid, -): - mock_send = mocker.patch('app.main.views.uploads.notification_api_client.send_precompiled_letter') - mocker.patch( - 'app.main.views.uploads.get_letter_metadata', - return_value=LetterMetadata( - { - 'filename': 'my_file.pdf', 'page_count': '3', 'status': 'invalid', - 'message': 'error', 'invalid_pages': '[1]' - } - ) - ) - - service_one['permissions'] = ['letter', 'upload_letters'] - - client_request.post( - 'main.send_uploaded_letter', - service_id=SERVICE_ONE_ID, - file_id=fake_uuid, - _data={'filename': 'my_file.pdf'}, - _expected_status=403 - ) - assert not mock_send.called - - -@pytest.mark.parametrize('user', ( - create_active_caseworking_user(), - create_active_user_with_permissions(), -)) -@freeze_time("2012-12-12 12:12") -def test_uploads_page_shows_scheduled_jobs( - mocker, - client_request, - mock_get_no_uploads, - mock_get_jobs, - mock_get_no_contact_lists, - user, -): - client_request.login(user) - page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID) - - assert [ - normalize_spaces(row.text) for row in page.select('tr') - ] == [ - ( - 'File Status' - ), - ( - 'even_later.csv ' - 'Sending 1 January 2016 at 11:09pm ' - '1 text message waiting to send' - ), - ( - 'send_me_later.csv ' - 'Sending 1 January 2016 at 11:09am ' - '1 text message waiting to send' - ), - ] - assert not page.select('.table-empty-message') - - -@freeze_time('2020-03-15') -def test_uploads_page_shows_contact_lists_first( - mocker, - client_request, - mock_get_no_uploads, - mock_get_jobs, - mock_get_contact_lists, -): - page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID) - - assert [ - normalize_spaces(row.text) for row in page.select('tr') - ] == [ - ( - 'File Status' - ), - ( - 'phone number list.csv ' - 'Uploaded 13 March at 1:00pm ' - '123 saved phone numbers' - ), - ( - 'EmergencyContactList.xls ' - 'Uploaded 13 March at 10:59am ' - '100 saved email addresses' - ), - ( - 'even_later.csv ' - 'Sending 1 January 2016 at 11:09pm ' - '1 text message waiting to send' - ), - ( - 'send_me_later.csv ' - 'Sending 1 January 2016 at 11:09am ' - '1 text message waiting to send' - ), - ] - assert page.select_one('.file-list-filename-large')['href'] == url_for( - 'main.contact_list', - service_id=SERVICE_ONE_ID, - contact_list_id='d7b0bd1a-d1c7-4621-be5c-3c1b4278a2ad', - ) - - -def test_get_uploads_shows_pagination( - client_request, - active_user_with_permissions, - mock_get_jobs, - mock_get_uploads, - mock_get_no_contact_lists, -): - page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID) - - assert normalize_spaces(page.select_one('.next-page').text) == ( - 'Next page ' - 'page 2' - ) - assert normalize_spaces(page.select_one('.previous-page').text) == ( - 'Previous page ' - 'page 0' - ) - - -def test_upload_contact_list_page(client_request): - page = client_request.get( - 'main.upload_contact_list', - service_id=SERVICE_ONE_ID, - ) - assert 'action' not in page.select_one('form') - assert page.select_one('form input')['name'] == 'file' - assert page.select_one('form input')['type'] == 'file' - - assert normalize_spaces(page.select('.spreadsheet')[0].text) == ( - 'Example A ' - '1 email address ' - '2 test@example.gov.uk' - ) - assert normalize_spaces(page.select('.spreadsheet')[1].text) == ( - 'Example A ' - '1 phone number ' - '2 07700 900123' - ) - - -@pytest.mark.parametrize('file_contents, expected_error, expected_thead, expected_tbody,', [ - ( - """ - telephone,name - +447700900986 - """, - ( - 'Your file has too many columns ' - 'It needs to have 1 column, called ‘email address’ or ‘phone number’. ' - 'Right now it has 2 columns called ‘telephone’ and ‘name’. ' - 'Skip to file contents' - ), - 'Row in file 1 telephone name', - '2 +447700900986', - ), - ( - """ - phone number, email address - +447700900986, test@example.com - """, - ( - 'Your file has too many columns ' - 'It needs to have 1 column, called ‘email address’ or ‘phone number’. ' - 'Right now it has 2 columns called ‘phone number’ and ‘email address’. ' - 'Skip to file contents' - ), - 'Row in file 1 phone number email address', - '2 +447700900986 test@example.com', - ), - ( - """ - email address - +447700900986 - """, - ( - 'There’s a problem with invalid.csv ' - 'You need to fix 1 email address. ' - 'Skip to file contents' - ), - 'Row in file 1 email address', - '2 Not a valid email address +447700900986', - ), - ( - """ - phone number - test@example.com - """, - ( - 'There’s a problem with invalid.csv ' - 'You need to fix 1 phone number. ' - 'Skip to file contents' - ), - 'Row in file 1 phone number', - '2 Must not contain letters or symbols test@example.com', - ), - ( - """ - phone number, phone number, PHONE_NUMBER - +447700900111,+447700900222,+447700900333, - """, - ( - 'Your file has too many columns ' - 'It needs to have 1 column, called ‘email address’ or ‘phone number’. ' - 'Right now it has 3 columns called ‘phone number’, ‘phone number’ and ‘PHONE_NUMBER’. ' - 'Skip to file contents' - ), - 'Row in file 1 phone number phone number PHONE_NUMBER', - '2 +447700900333 +447700900333 +447700900333', - ), - ( - """ - phone number - """, - ( - 'Your file is missing some rows ' - 'It needs at least one row of data. ' - 'Skip to file contents' - ), - 'Row in file 1 phone number', - '', - ), - ( - "+447700900986", - ( - 'Your file is missing some rows ' - 'It needs at least one row of data, in a column called ' - '‘email address’ or ‘phone number’. ' - 'Skip to file contents' - ), - 'Row in file 1 +447700900986', - '', - ), - ( - "", - ( - 'Your file is missing some rows ' - 'It needs at least one row of data, in a column called ' - '‘email address’ or ‘phone number’. ' - 'Skip to file contents' - ), - 'Row in file 1', - '', - ), - ( - """ - phone number - +447700900986 - - +447700900986 - """, - ( - 'There’s a problem with invalid.csv ' - 'You need to enter missing data in 1 row. ' - 'Skip to file contents' - ), - 'Row in file 1 phone number', - ( - '3 Missing' - ) - ), - ( - """ - phone number - +447700900 - """, - ( - 'There’s a problem with invalid.csv ' - 'You need to fix 1 phone number. ' - 'Skip to file contents' - ), - 'Row in file 1 phone number', - '2 Not enough digits +447700900', - ), - ( - """ - email address - ok@example.com - bad@example1 - bad@example2 - """, - ( - 'There’s a problem with invalid.csv ' - 'You need to fix 2 email addresses. ' - 'Skip to file contents' - ), - 'Row in file 1 email address', - ( - '3 Not a valid email address bad@example1 ' - '4 Not a valid email address bad@example2' - ), - ), -]) -def test_upload_csv_file_shows_error_banner( - client_request, - mocker, - mock_s3_upload, - mock_get_job_doesnt_exist, - mock_get_users_by_service, - fake_uuid, - file_contents, - expected_error, - expected_thead, - expected_tbody, -): - mock_upload = mocker.patch( - 'app.models.contact_list.s3upload', - return_value=fake_uuid, - ) - mock_download = mocker.patch( - 'app.models.contact_list.s3download', - return_value=file_contents, - ) - - page = client_request.post( - 'main.upload_contact_list', - service_id=SERVICE_ONE_ID, - _data={'file': (BytesIO(''.encode('utf-8')), 'invalid.csv')}, - _follow_redirects=True, - ) - mock_upload.assert_called_once_with( - SERVICE_ONE_ID, - {'data': '', 'file_name': 'invalid.csv'}, - ANY, - bucket='test-contact-list', - ) - mock_download.assert_called_once_with( - SERVICE_ONE_ID, - fake_uuid, - bucket='test-contact-list', - ) - - assert normalize_spaces(page.select_one('.banner-dangerous').text) == expected_error - - assert page.select_one('form')['action'] == url_for( - 'main.upload_contact_list', - service_id=SERVICE_ONE_ID, - ) - assert page.select_one('form input')['type'] == 'file' - - assert normalize_spaces(page.select_one('thead').text) == expected_thead - assert normalize_spaces(page.select_one('tbody').text) == expected_tbody - - -def test_upload_csv_file_shows_error_banner_for_too_many_rows( - client_request, - mocker, - mock_s3_upload, - mock_get_job_doesnt_exist, - mock_get_users_by_service, - fake_uuid, -): - mocker.patch('app.models.contact_list.s3upload', return_value=fake_uuid) - mocker.patch('app.models.contact_list.s3download', return_value='\n'.join( - ['phone number'] + (['07700900986'] * 50001) - )) - - page = client_request.post( - 'main.upload_contact_list', - service_id=SERVICE_ONE_ID, - _data={'file': (BytesIO(''.encode('utf-8')), 'invalid.csv')}, - _follow_redirects=True, - ) - - assert normalize_spaces(page.select_one('.banner-dangerous').text) == ( - 'Your file has too many rows ' - 'Notify can store files up to 50,000 rows in size. ' - 'Your file has 50,001 rows. ' - 'Skip to file contents' - ) - assert len(page.select('tbody tr')) == 50 - assert normalize_spaces(page.select_one('.table-show-more-link').text) == ( - 'Only showing the first 50 rows' - ) - - -def test_upload_csv_shows_trial_mode_error( - client_request, - mock_get_users_by_service, - mock_get_job_doesnt_exist, - fake_uuid, - mocker -): - mocker.patch('app.models.contact_list.s3upload', return_value=fake_uuid) - mocker.patch('app.models.contact_list.s3download', return_value=( - 'phone number\n' - '07900900321' # Not in team - )) - - page = client_request.get( - 'main.check_contact_list', - service_id=SERVICE_ONE_ID, - upload_id=fake_uuid, - _test_page_title=False, - ) - - assert normalize_spaces(page.select_one('.banner-dangerous').text) == ( - 'You cannot save this phone number ' - 'In trial mode you can only send to yourself and members of your team ' - 'Skip to file contents' - ) - assert page.select_one('.banner-dangerous a')['href'] == url_for( - 'main.trial_mode_new' - ) - - -def test_upload_csv_shows_ok_page( - client_request, - mock_get_live_service, - mock_get_users_by_service, - mock_get_job_doesnt_exist, - fake_uuid, - mocker -): - mocker.patch('app.models.contact_list.s3download', return_value='\n'.join( - ['email address'] + ['test@example.com'] * 51 - )) - mock_metadata_set = mocker.patch('app.models.contact_list.set_metadata_on_csv_upload') - - page = client_request.get( - 'main.check_contact_list', - service_id=SERVICE_ONE_ID, - upload_id=fake_uuid, - original_file_name='good times.xlsx', - _test_page_title=False, - ) - - mock_metadata_set.assert_called_once_with( - SERVICE_ONE_ID, - fake_uuid, - bucket='test-contact-list', - row_count=51, - original_file_name='good times.xlsx', - template_type='email', - valid=True, - ) - - assert normalize_spaces(page.select_one('h1').text) == ( - 'good times.xlsx' - ) - assert normalize_spaces(page.select_one('main p').text) == ( - '51 email addresses found' - ) - assert page.select_one('form')['action'] == url_for( - 'main.save_contact_list', - service_id=SERVICE_ONE_ID, - upload_id=fake_uuid, - ) - - assert normalize_spaces(page.select_one('form [type=submit]').text) == ( - 'Save contact list' - ) - assert normalize_spaces(page.select_one('thead').text) == ( - 'Row in file 1 email address' - ) - assert len(page.select('tbody tr')) == 50 - assert normalize_spaces(page.select_one('tbody tr').text) == ( - '2 test@example.com' - ) - assert normalize_spaces(page.select_one('.table-show-more-link').text) == ( - 'Only showing the first 50 rows' - ) - - -def test_save_contact_list( - mocker, - client_request, - fake_uuid, - mock_create_contact_list, -): - mock_get_metadata = mocker.patch('app.models.contact_list.get_csv_metadata', return_value={ - 'row_count': 999, - 'valid': True, - 'original_file_name': 'example.csv', - 'template_type': 'email' - }) - client_request.post( - 'main.save_contact_list', - service_id=SERVICE_ONE_ID, - upload_id=fake_uuid, - _expected_status=302, - _expected_redirect=url_for( - 'main.uploads', - service_id=SERVICE_ONE_ID, - _external=True, - ) - ) - mock_get_metadata.assert_called_once_with( - SERVICE_ONE_ID, - fake_uuid, - bucket='test-contact-list', - ) - mock_create_contact_list.assert_called_once_with( - service_id=SERVICE_ONE_ID, - upload_id=fake_uuid, - original_file_name='example.csv', - row_count=999, - template_type='email', - ) - - -def test_cant_save_bad_contact_list( - mocker, - client_request, - fake_uuid, - mock_create_contact_list, -): - mocker.patch('app.models.contact_list.get_csv_metadata', return_value={ - 'row_count': 999, - 'valid': False, - 'original_file_name': 'example.csv', - 'template_type': 'email' - }) - client_request.post( - 'main.save_contact_list', - service_id=SERVICE_ONE_ID, - upload_id=fake_uuid, - _expected_status=403, - ) - assert mock_create_contact_list.called is False - - -@freeze_time('2020-03-13 16:51:56') -def test_view_contact_list( - mocker, - client_request, - mock_get_contact_list, - fake_uuid, -): - mocker.patch('app.models.contact_list.s3download', return_value='\n'.join( - ['email address'] + ['test@example.com'] * 51 - )) - page = client_request.get( - 'main.contact_list', - service_id=SERVICE_ONE_ID, - contact_list_id=fake_uuid, - ) - assert normalize_spaces(page.select_one('h1').text) == ( - 'EmergencyContactList.xls' - ) - assert normalize_spaces(page.select('main p')[0].text) == ( - 'Uploaded by Test User today at 10:59am' - ) - assert normalize_spaces(page.select('main p')[1].text) == ( - 'Download this list 51 email addresses' - ) - assert page.select_one('a[download]')['href'] == url_for( - 'main.download_contact_list', - service_id=SERVICE_ONE_ID, - contact_list_id=fake_uuid, - ) - assert normalize_spaces(page.select_one('table').text).startswith( - 'Email addresses ' - '1 email address ' - '2 test@example.com ' - '3 test@example.com ' - ) - assert normalize_spaces(page.select_one('table').text).endswith( - '50 test@example.com ' - '51 test@example.com' - ) - assert normalize_spaces(page.select_one('.table-show-more-link').text) == ( - 'Only showing the first 50 rows' - ) - - -def test_view_contact_list_404s_for_non_existing_list( - client_request, - mock_get_no_contact_list, - fake_uuid, -): - client_request.get( - 'main.contact_list', - service_id=SERVICE_ONE_ID, - contact_list_id=uuid.uuid4(), - _expected_status=404, - ) - - -def test_download_contact_list( - mocker, - logged_in_client, - fake_uuid, - mock_get_contact_list, -): - mocker.patch( - 'app.models.contact_list.s3download', - return_value='phone number\n07900900321' - ) - response = logged_in_client.get(url_for( - 'main.download_contact_list', - service_id=SERVICE_ONE_ID, - contact_list_id=fake_uuid, - )) - assert response.status_code == 200 - assert response.headers['Content-Type'] == ( - 'text/csv; ' - 'charset=utf-8' - ) - assert response.headers['Content-Disposition'] == ( - 'attachment; ' - 'filename=EmergencyContactList.csv' - ) - assert response.get_data(as_text=True) == ( - 'phone number\n' - '07900900321' - ) - - -def test_confirm_delete_contact_list( - mocker, - client_request, - fake_uuid, - mock_get_contact_list, -): - mocker.patch( - 'app.models.contact_list.s3download', - return_value='phone number\n07900900321' - ) - page = client_request.get( - 'main.delete_contact_list', - service_id=SERVICE_ONE_ID, - contact_list_id=fake_uuid, - ) - assert normalize_spaces(page.select_one('.banner-dangerous').text) == ( - 'Are you sure you want to delete ‘EmergencyContactList.xls’? ' - 'Yes, delete' - ) - assert 'action' not in page.select_one('form') - assert page.select_one('form')['method'] == 'post' - assert page.select_one('form button')['type'] == 'submit' - - -def test_delete_contact_list( - mocker, - client_request, - fake_uuid, - mock_get_contact_list, -): - mock_delete = mocker.patch( - 'app.models.contact_list.contact_list_api_client.delete_contact_list' - ) - client_request.post( - 'main.delete_contact_list', - service_id=SERVICE_ONE_ID, - contact_list_id=fake_uuid, - _expected_redirect=url_for( - 'main.uploads', - service_id=SERVICE_ONE_ID, - _external=True, - ) - ) - mock_delete.assert_called_once_with( - service_id=SERVICE_ONE_ID, - contact_list_id=fake_uuid, - ) diff --git a/tests/app/main/views/uploads/test_upload_contact_list.py b/tests/app/main/views/uploads/test_upload_contact_list.py new file mode 100644 index 000000000..af2daf85d --- /dev/null +++ b/tests/app/main/views/uploads/test_upload_contact_list.py @@ -0,0 +1,548 @@ +import uuid +from io import BytesIO +from unittest.mock import ANY + +import pytest +from flask import url_for +from freezegun import freeze_time + +from app.utils import normalize_spaces +from tests.conftest import SERVICE_ONE_ID + + +def test_upload_contact_list_page(client_request): + page = client_request.get( + 'main.upload_contact_list', + service_id=SERVICE_ONE_ID, + ) + assert 'action' not in page.select_one('form') + assert page.select_one('form input')['name'] == 'file' + assert page.select_one('form input')['type'] == 'file' + + assert normalize_spaces(page.select('.spreadsheet')[0].text) == ( + 'Example A ' + '1 email address ' + '2 test@example.gov.uk' + ) + assert normalize_spaces(page.select('.spreadsheet')[1].text) == ( + 'Example A ' + '1 phone number ' + '2 07700 900123' + ) + + +@pytest.mark.parametrize('file_contents, expected_error, expected_thead, expected_tbody,', [ + ( + """ + telephone,name + +447700900986 + """, + ( + 'Your file has too many columns ' + 'It needs to have 1 column, called ‘email address’ or ‘phone number’. ' + 'Right now it has 2 columns called ‘telephone’ and ‘name’. ' + 'Skip to file contents' + ), + 'Row in file 1 telephone name', + '2 +447700900986', + ), + ( + """ + phone number, email address + +447700900986, test@example.com + """, + ( + 'Your file has too many columns ' + 'It needs to have 1 column, called ‘email address’ or ‘phone number’. ' + 'Right now it has 2 columns called ‘phone number’ and ‘email address’. ' + 'Skip to file contents' + ), + 'Row in file 1 phone number email address', + '2 +447700900986 test@example.com', + ), + ( + """ + email address + +447700900986 + """, + ( + 'There’s a problem with invalid.csv ' + 'You need to fix 1 email address. ' + 'Skip to file contents' + ), + 'Row in file 1 email address', + '2 Not a valid email address +447700900986', + ), + ( + """ + phone number + test@example.com + """, + ( + 'There’s a problem with invalid.csv ' + 'You need to fix 1 phone number. ' + 'Skip to file contents' + ), + 'Row in file 1 phone number', + '2 Must not contain letters or symbols test@example.com', + ), + ( + """ + phone number, phone number, PHONE_NUMBER + +447700900111,+447700900222,+447700900333, + """, + ( + 'Your file has too many columns ' + 'It needs to have 1 column, called ‘email address’ or ‘phone number’. ' + 'Right now it has 3 columns called ‘phone number’, ‘phone number’ and ‘PHONE_NUMBER’. ' + 'Skip to file contents' + ), + 'Row in file 1 phone number phone number PHONE_NUMBER', + '2 +447700900333 +447700900333 +447700900333', + ), + ( + """ + phone number + """, + ( + 'Your file is missing some rows ' + 'It needs at least one row of data. ' + 'Skip to file contents' + ), + 'Row in file 1 phone number', + '', + ), + ( + "+447700900986", + ( + 'Your file is missing some rows ' + 'It needs at least one row of data, in a column called ' + '‘email address’ or ‘phone number’. ' + 'Skip to file contents' + ), + 'Row in file 1 +447700900986', + '', + ), + ( + "", + ( + 'Your file is missing some rows ' + 'It needs at least one row of data, in a column called ' + '‘email address’ or ‘phone number’. ' + 'Skip to file contents' + ), + 'Row in file 1', + '', + ), + ( + """ + phone number + +447700900986 + + +447700900986 + """, + ( + 'There’s a problem with invalid.csv ' + 'You need to enter missing data in 1 row. ' + 'Skip to file contents' + ), + 'Row in file 1 phone number', + ( + '3 Missing' + ) + ), + ( + """ + phone number + +447700900 + """, + ( + 'There’s a problem with invalid.csv ' + 'You need to fix 1 phone number. ' + 'Skip to file contents' + ), + 'Row in file 1 phone number', + '2 Not enough digits +447700900', + ), + ( + """ + email address + ok@example.com + bad@example1 + bad@example2 + """, + ( + 'There’s a problem with invalid.csv ' + 'You need to fix 2 email addresses. ' + 'Skip to file contents' + ), + 'Row in file 1 email address', + ( + '3 Not a valid email address bad@example1 ' + '4 Not a valid email address bad@example2' + ), + ), +]) +def test_upload_csv_file_shows_error_banner( + client_request, + mocker, + mock_s3_upload, + mock_get_job_doesnt_exist, + mock_get_users_by_service, + fake_uuid, + file_contents, + expected_error, + expected_thead, + expected_tbody, +): + mock_upload = mocker.patch( + 'app.models.contact_list.s3upload', + return_value=fake_uuid, + ) + mock_download = mocker.patch( + 'app.models.contact_list.s3download', + return_value=file_contents, + ) + + page = client_request.post( + 'main.upload_contact_list', + service_id=SERVICE_ONE_ID, + _data={'file': (BytesIO(''.encode('utf-8')), 'invalid.csv')}, + _follow_redirects=True, + ) + mock_upload.assert_called_once_with( + SERVICE_ONE_ID, + {'data': '', 'file_name': 'invalid.csv'}, + ANY, + bucket='test-contact-list', + ) + mock_download.assert_called_once_with( + SERVICE_ONE_ID, + fake_uuid, + bucket='test-contact-list', + ) + + assert normalize_spaces(page.select_one('.banner-dangerous').text) == expected_error + + assert page.select_one('form')['action'] == url_for( + 'main.upload_contact_list', + service_id=SERVICE_ONE_ID, + ) + assert page.select_one('form input')['type'] == 'file' + + assert normalize_spaces(page.select_one('thead').text) == expected_thead + assert normalize_spaces(page.select_one('tbody').text) == expected_tbody + + +def test_upload_csv_file_shows_error_banner_for_too_many_rows( + client_request, + mocker, + mock_s3_upload, + mock_get_job_doesnt_exist, + mock_get_users_by_service, + fake_uuid, +): + mocker.patch('app.models.contact_list.s3upload', return_value=fake_uuid) + mocker.patch('app.models.contact_list.s3download', return_value='\n'.join( + ['phone number'] + (['07700900986'] * 50001) + )) + + page = client_request.post( + 'main.upload_contact_list', + service_id=SERVICE_ONE_ID, + _data={'file': (BytesIO(''.encode('utf-8')), 'invalid.csv')}, + _follow_redirects=True, + ) + + assert normalize_spaces(page.select_one('.banner-dangerous').text) == ( + 'Your file has too many rows ' + 'Notify can store files up to 50,000 rows in size. ' + 'Your file has 50,001 rows. ' + 'Skip to file contents' + ) + assert len(page.select('tbody tr')) == 50 + assert normalize_spaces(page.select_one('.table-show-more-link').text) == ( + 'Only showing the first 50 rows' + ) + + +def test_upload_csv_shows_trial_mode_error( + client_request, + mock_get_users_by_service, + mock_get_job_doesnt_exist, + fake_uuid, + mocker +): + mocker.patch('app.models.contact_list.s3upload', return_value=fake_uuid) + mocker.patch('app.models.contact_list.s3download', return_value=( + 'phone number\n' + '07900900321' # Not in team + )) + + page = client_request.get( + 'main.check_contact_list', + service_id=SERVICE_ONE_ID, + upload_id=fake_uuid, + _test_page_title=False, + ) + + assert normalize_spaces(page.select_one('.banner-dangerous').text) == ( + 'You cannot save this phone number ' + 'In trial mode you can only send to yourself and members of your team ' + 'Skip to file contents' + ) + assert page.select_one('.banner-dangerous a')['href'] == url_for( + 'main.trial_mode_new' + ) + + +def test_upload_csv_shows_ok_page( + client_request, + mock_get_live_service, + mock_get_users_by_service, + mock_get_job_doesnt_exist, + fake_uuid, + mocker +): + mocker.patch('app.models.contact_list.s3download', return_value='\n'.join( + ['email address'] + ['test@example.com'] * 51 + )) + mock_metadata_set = mocker.patch('app.models.contact_list.set_metadata_on_csv_upload') + + page = client_request.get( + 'main.check_contact_list', + service_id=SERVICE_ONE_ID, + upload_id=fake_uuid, + original_file_name='good times.xlsx', + _test_page_title=False, + ) + + mock_metadata_set.assert_called_once_with( + SERVICE_ONE_ID, + fake_uuid, + bucket='test-contact-list', + row_count=51, + original_file_name='good times.xlsx', + template_type='email', + valid=True, + ) + + assert normalize_spaces(page.select_one('h1').text) == ( + 'good times.xlsx' + ) + assert normalize_spaces(page.select_one('main p').text) == ( + '51 email addresses found' + ) + assert page.select_one('form')['action'] == url_for( + 'main.save_contact_list', + service_id=SERVICE_ONE_ID, + upload_id=fake_uuid, + ) + + assert normalize_spaces(page.select_one('form [type=submit]').text) == ( + 'Save contact list' + ) + assert normalize_spaces(page.select_one('thead').text) == ( + 'Row in file 1 email address' + ) + assert len(page.select('tbody tr')) == 50 + assert normalize_spaces(page.select_one('tbody tr').text) == ( + '2 test@example.com' + ) + assert normalize_spaces(page.select_one('.table-show-more-link').text) == ( + 'Only showing the first 50 rows' + ) + + +def test_save_contact_list( + mocker, + client_request, + fake_uuid, + mock_create_contact_list, +): + mock_get_metadata = mocker.patch('app.models.contact_list.get_csv_metadata', return_value={ + 'row_count': 999, + 'valid': True, + 'original_file_name': 'example.csv', + 'template_type': 'email' + }) + client_request.post( + 'main.save_contact_list', + service_id=SERVICE_ONE_ID, + upload_id=fake_uuid, + _expected_status=302, + _expected_redirect=url_for( + 'main.uploads', + service_id=SERVICE_ONE_ID, + _external=True, + ) + ) + mock_get_metadata.assert_called_once_with( + SERVICE_ONE_ID, + fake_uuid, + bucket='test-contact-list', + ) + mock_create_contact_list.assert_called_once_with( + service_id=SERVICE_ONE_ID, + upload_id=fake_uuid, + original_file_name='example.csv', + row_count=999, + template_type='email', + ) + + +def test_cant_save_bad_contact_list( + mocker, + client_request, + fake_uuid, + mock_create_contact_list, +): + mocker.patch('app.models.contact_list.get_csv_metadata', return_value={ + 'row_count': 999, + 'valid': False, + 'original_file_name': 'example.csv', + 'template_type': 'email' + }) + client_request.post( + 'main.save_contact_list', + service_id=SERVICE_ONE_ID, + upload_id=fake_uuid, + _expected_status=403, + ) + assert mock_create_contact_list.called is False + + +@freeze_time('2020-03-13 16:51:56') +def test_view_contact_list( + mocker, + client_request, + mock_get_contact_list, + fake_uuid, +): + mocker.patch('app.models.contact_list.s3download', return_value='\n'.join( + ['email address'] + ['test@example.com'] * 51 + )) + page = client_request.get( + 'main.contact_list', + service_id=SERVICE_ONE_ID, + contact_list_id=fake_uuid, + ) + assert normalize_spaces(page.select_one('h1').text) == ( + 'EmergencyContactList.xls' + ) + assert normalize_spaces(page.select('main p')[0].text) == ( + 'Uploaded by Test User today at 10:59am' + ) + assert normalize_spaces(page.select('main p')[1].text) == ( + 'Download this list 51 email addresses' + ) + assert page.select_one('a[download]')['href'] == url_for( + 'main.download_contact_list', + service_id=SERVICE_ONE_ID, + contact_list_id=fake_uuid, + ) + assert normalize_spaces(page.select_one('table').text).startswith( + 'Email addresses ' + '1 email address ' + '2 test@example.com ' + '3 test@example.com ' + ) + assert normalize_spaces(page.select_one('table').text).endswith( + '50 test@example.com ' + '51 test@example.com' + ) + assert normalize_spaces(page.select_one('.table-show-more-link').text) == ( + 'Only showing the first 50 rows' + ) + + +def test_view_contact_list_404s_for_non_existing_list( + client_request, + mock_get_no_contact_list, + fake_uuid, +): + client_request.get( + 'main.contact_list', + service_id=SERVICE_ONE_ID, + contact_list_id=uuid.uuid4(), + _expected_status=404, + ) + + +def test_download_contact_list( + mocker, + logged_in_client, + fake_uuid, + mock_get_contact_list, +): + mocker.patch( + 'app.models.contact_list.s3download', + return_value='phone number\n07900900321' + ) + response = logged_in_client.get(url_for( + 'main.download_contact_list', + service_id=SERVICE_ONE_ID, + contact_list_id=fake_uuid, + )) + assert response.status_code == 200 + assert response.headers['Content-Type'] == ( + 'text/csv; ' + 'charset=utf-8' + ) + assert response.headers['Content-Disposition'] == ( + 'attachment; ' + 'filename=EmergencyContactList.csv' + ) + assert response.get_data(as_text=True) == ( + 'phone number\n' + '07900900321' + ) + + +def test_confirm_delete_contact_list( + mocker, + client_request, + fake_uuid, + mock_get_contact_list, +): + mocker.patch( + 'app.models.contact_list.s3download', + return_value='phone number\n07900900321' + ) + page = client_request.get( + 'main.delete_contact_list', + service_id=SERVICE_ONE_ID, + contact_list_id=fake_uuid, + ) + assert normalize_spaces(page.select_one('.banner-dangerous').text) == ( + 'Are you sure you want to delete ‘EmergencyContactList.xls’? ' + 'Yes, delete' + ) + assert 'action' not in page.select_one('form') + assert page.select_one('form')['method'] == 'post' + assert page.select_one('form button')['type'] == 'submit' + + +def test_delete_contact_list( + mocker, + client_request, + fake_uuid, + mock_get_contact_list, +): + mock_delete = mocker.patch( + 'app.models.contact_list.contact_list_api_client.delete_contact_list' + ) + client_request.post( + 'main.delete_contact_list', + service_id=SERVICE_ONE_ID, + contact_list_id=fake_uuid, + _expected_redirect=url_for( + 'main.uploads', + service_id=SERVICE_ONE_ID, + _external=True, + ) + ) + mock_delete.assert_called_once_with( + service_id=SERVICE_ONE_ID, + contact_list_id=fake_uuid, + ) diff --git a/tests/app/main/views/uploads/test_upload_hub.py b/tests/app/main/views/uploads/test_upload_hub.py new file mode 100644 index 000000000..fae8de5d8 --- /dev/null +++ b/tests/app/main/views/uploads/test_upload_hub.py @@ -0,0 +1,394 @@ +import re + +import pytest +from flask import url_for +from freezegun import freeze_time + +from app.utils import normalize_spaces +from tests.conftest import ( + SERVICE_ONE_ID, + create_active_caseworking_user, + create_active_user_with_permissions, + create_platform_admin_user, +) + + +@pytest.mark.parametrize('extra_permissions', ( + pytest.param( + [], + marks=pytest.mark.xfail(raises=AssertionError), + ), + pytest.param( + ['upload_letters'], + marks=pytest.mark.xfail(raises=AssertionError), + ), + ['letter'], + ['letter', 'upload_letters'], +)) +def test_upload_letters_button_only_with_letters_permission( + client_request, + service_one, + mock_get_uploads, + mock_get_jobs, + mock_get_no_contact_lists, + extra_permissions, +): + service_one['permissions'] += extra_permissions + page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID) + assert page.find('a', text=re.compile('Upload a letter')) + + +@pytest.mark.parametrize('user', ( + create_platform_admin_user(), + create_active_user_with_permissions(), +)) +def test_all_users_have_upload_contact_list( + client_request, + mock_get_uploads, + mock_get_jobs, + mock_get_no_contact_lists, + user, +): + client_request.login(user) + page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID) + button = page.find('a', text=re.compile('Upload an emergency contact list')) + assert button + assert button['href'] == url_for( + 'main.upload_contact_list', service_id=SERVICE_ONE_ID, + ) + + +@pytest.mark.parametrize('extra_permissions, expected_empty_message', ( + ([], ( + 'You have not uploaded any files recently.' + )), + (['letter'], ( + 'You have not uploaded any files recently. ' + 'Upload a letter and Notify will print, pack and post it for you.' + )), +)) +def test_get_upload_hub_with_no_uploads( + mocker, + client_request, + service_one, + mock_get_no_uploads, + mock_get_no_contact_lists, + extra_permissions, + expected_empty_message, +): + mocker.patch('app.job_api_client.get_jobs', return_value={'data': []}) + service_one['permissions'] += extra_permissions + page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID) + assert normalize_spaces(' '.join( + paragraph.text for paragraph in page.select('main p') + )) == expected_empty_message + assert not page.select('.file-list-filename') + + +@freeze_time('2017-10-10 10:10:10') +def test_get_upload_hub_page( + mocker, + client_request, + service_one, + mock_get_uploads, + mock_get_no_contact_lists, +): + mocker.patch('app.job_api_client.get_jobs', return_value={'data': []}) + service_one['permissions'] += ['letter', 'upload_letters'] + page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID) + assert page.find('h1').text == 'Uploads' + assert page.find('a', text=re.compile('Upload a letter')).attrs['href'] == url_for( + 'main.upload_letter', service_id=SERVICE_ONE_ID + ) + + uploads = page.select('tbody tr') + + assert len(uploads) == 3 + + assert normalize_spaces(uploads[0].text.strip()) == ( + 'Uploaded letters ' + 'Printing today at 5:30pm ' + '33 letters' + ) + assert uploads[0].select_one('a.file-list-filename-large')['href'] == url_for( + 'main.uploaded_letters', + service_id=SERVICE_ONE_ID, + letter_print_day='2017-10-10', + ) + + assert normalize_spaces(uploads[1].text.strip()) == ( + 'some.csv ' + 'Sent 1 January 2016 at 11:09am ' + '0 sending 8 delivered 2 failed' + ) + assert uploads[1].select_one('a.file-list-filename-large')['href'] == ( + '/services/{}/jobs/job_id_1'.format(SERVICE_ONE_ID) + ) + + assert normalize_spaces(uploads[2].text.strip()) == ( + 'some.pdf ' + 'Sent 1 January 2016 at 11:09am ' + 'Firstname Lastname ' + '123 Example Street' + ) + assert normalize_spaces(str(uploads[2].select_one('.govuk-body'))) == ( + '

' + 'Firstname Lastname
' + '123 Example Street
' + '

' + ) + assert uploads[2].select_one('a.file-list-filename-large')['href'] == ( + '/services/{}/notification/letter_id_1'.format(SERVICE_ONE_ID) + ) + + +@freeze_time('2020-02-02 14:00') +def test_get_uploaded_letters( + mocker, + client_request, + service_one, + mock_get_uploaded_letters, +): + page = client_request.get( + 'main.uploaded_letters', + service_id=SERVICE_ONE_ID, + letter_print_day='2020-02-02' + ) + assert page.select_one('.govuk-back-link')['href'] == url_for( + 'main.uploads', + service_id=SERVICE_ONE_ID, + ) + assert normalize_spaces( + page.select_one('h1').text + ) == ( + 'Uploaded letters' + ) + assert normalize_spaces( + page.select('main p')[0].text + ) == ( + '1,234 letters' + ) + assert normalize_spaces( + page.select('main p')[1].text + ) == ( + 'Printing starts today at 5:30pm' + ) + + assert [ + normalize_spaces(row.text) + for row in page.select('tbody tr') + ] == [ + ( + 'Homer-Simpson.pdf ' + '742 Evergreen Terrace ' + '2 February at 1:59pm' + ), + ( + 'Kevin-McCallister.pdf ' + '671 Lincoln Avenue, Winnetka ' + '2 February at 12:59pm' + ), + ] + + assert [ + link['href'] for link in page.select('tbody tr a') + ] == [ + url_for( + 'main.view_notification', + service_id=SERVICE_ONE_ID, + notification_id='03e34025-be54-4d43-8e6a-fb1ea0fd1f29', + from_uploaded_letters='2020-02-02', + ), + url_for( + 'main.view_notification', + service_id=SERVICE_ONE_ID, + notification_id='fc090d91-e761-4464-9041-9c4594c96a35', + from_uploaded_letters='2020-02-02', + ), + ] + + next_page_link = page.select_one('a[rel=next]') + prev_page_link = page.select_one('a[rel=previous]') + assert next_page_link['href'] == url_for( + 'main.uploaded_letters', service_id=SERVICE_ONE_ID, letter_print_day='2020-02-02', page=2 + ) + assert normalize_spaces(next_page_link.text) == ( + 'Next page ' + 'page 2' + ) + assert prev_page_link['href'] == url_for( + 'main.uploaded_letters', service_id=SERVICE_ONE_ID, letter_print_day='2020-02-02', page=0 + ) + assert normalize_spaces(prev_page_link.text) == ( + 'Previous page ' + 'page 0' + ) + + mock_get_uploaded_letters.assert_called_once_with( + SERVICE_ONE_ID, + letter_print_day='2020-02-02', + page=1, + ) + + +@freeze_time('2020-02-02 14:00') +def test_get_empty_uploaded_letters_page( + mocker, + client_request, + service_one, + mock_get_no_uploaded_letters, +): + page = client_request.get( + 'main.uploaded_letters', + service_id=SERVICE_ONE_ID, + letter_print_day='2020-02-02' + ) + page.select_one('main table') + + assert not page.select('tbody tr') + assert not page.select_one('a[rel=next]') + assert not page.select_one('a[rel=previous]') + + +@freeze_time('2020-02-02') +def test_get_uploaded_letters_passes_through_page_argument( + mocker, + client_request, + service_one, + mock_get_uploaded_letters, +): + client_request.get( + 'main.uploaded_letters', + service_id=SERVICE_ONE_ID, + letter_print_day='2020-02-02', + page=99, + ) + mock_get_uploaded_letters.assert_called_once_with( + SERVICE_ONE_ID, + letter_print_day='2020-02-02', + page=99, + ) + + +def test_get_uploaded_letters_404s_for_bad_page_arguments( + mocker, + client_request, +): + client_request.get( + 'main.uploaded_letters', + service_id=SERVICE_ONE_ID, + letter_print_day='2020-02-02', + page='one', + _expected_status=404, + ) + + +def test_get_uploaded_letters_404s_for_invalid_date( + mocker, + client_request, +): + client_request.get( + 'main.uploaded_letters', + service_id=SERVICE_ONE_ID, + letter_print_day='1234-56-78', + _expected_status=404, + ) + + +@pytest.mark.parametrize('user', ( + create_active_caseworking_user(), + create_active_user_with_permissions(), +)) +@freeze_time("2012-12-12 12:12") +def test_uploads_page_shows_scheduled_jobs( + mocker, + client_request, + mock_get_no_uploads, + mock_get_jobs, + mock_get_no_contact_lists, + user, +): + client_request.login(user) + page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID) + + assert [ + normalize_spaces(row.text) for row in page.select('tr') + ] == [ + ( + 'File Status' + ), + ( + 'even_later.csv ' + 'Sending 1 January 2016 at 11:09pm ' + '1 text message waiting to send' + ), + ( + 'send_me_later.csv ' + 'Sending 1 January 2016 at 11:09am ' + '1 text message waiting to send' + ), + ] + assert not page.select('.table-empty-message') + + +@freeze_time('2020-03-15') +def test_uploads_page_shows_contact_lists_first( + mocker, + client_request, + mock_get_no_uploads, + mock_get_jobs, + mock_get_contact_lists, +): + page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID) + + assert [ + normalize_spaces(row.text) for row in page.select('tr') + ] == [ + ( + 'File Status' + ), + ( + 'phone number list.csv ' + 'Uploaded 13 March at 1:00pm ' + '123 saved phone numbers' + ), + ( + 'EmergencyContactList.xls ' + 'Uploaded 13 March at 10:59am ' + '100 saved email addresses' + ), + ( + 'even_later.csv ' + 'Sending 1 January 2016 at 11:09pm ' + '1 text message waiting to send' + ), + ( + 'send_me_later.csv ' + 'Sending 1 January 2016 at 11:09am ' + '1 text message waiting to send' + ), + ] + assert page.select_one('.file-list-filename-large')['href'] == url_for( + 'main.contact_list', + service_id=SERVICE_ONE_ID, + contact_list_id='d7b0bd1a-d1c7-4621-be5c-3c1b4278a2ad', + ) + + +def test_get_uploads_shows_pagination( + client_request, + active_user_with_permissions, + mock_get_jobs, + mock_get_uploads, + mock_get_no_contact_lists, +): + page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID) + + assert normalize_spaces(page.select_one('.next-page').text) == ( + 'Next page ' + 'page 2' + ) + assert normalize_spaces(page.select_one('.previous-page').text) == ( + 'Previous page ' + 'page 0' + ) diff --git a/tests/app/main/views/uploads/test_upload_letter.py b/tests/app/main/views/uploads/test_upload_letter.py new file mode 100644 index 000000000..5478f975b --- /dev/null +++ b/tests/app/main/views/uploads/test_upload_letter.py @@ -0,0 +1,680 @@ +from unittest.mock import ANY, Mock + +import pytest +from botocore.exceptions import ClientError +from flask import make_response, url_for +from requests import RequestException + +from app.s3_client.s3_letter_upload_client import LetterMetadata +from app.utils import normalize_spaces +from tests.conftest import SERVICE_ONE_ID + + +def test_get_upload_letter(client_request): + page = client_request.get('main.upload_letter', service_id=SERVICE_ONE_ID) + + assert page.find('h1').text == 'Upload a letter' + assert page.find('input', class_='file-upload-field') + assert page.select('main button[type=submit]') + assert normalize_spaces(page.find('label', class_='file-upload-button').text) == 'Choose file' + + +@pytest.mark.parametrize('extra_permissions, expected_allow_international', ( + ([], False), + (['international_letters'], True), +)) +def test_post_upload_letter_redirects_for_valid_file( + mocker, + active_user_with_permissions, + service_one, + client_request, + fake_uuid, + extra_permissions, + expected_allow_international, +): + mocker.patch('uuid.uuid4', return_value=fake_uuid) + antivirus_mock = mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True) + mock_sanitise = mocker.patch( + 'app.main.views.uploads.sanitise_letter', + return_value=Mock( + content='The sanitised content', + json=lambda: {'file': 'VGhlIHNhbml0aXNlZCBjb250ZW50', 'recipient_address': 'The Queen'} + ) + ) + mock_s3 = mocker.patch('app.main.views.uploads.upload_letter_to_s3') + mocker.patch('app.main.views.uploads.get_letter_metadata', return_value=LetterMetadata({ + 'filename': 'tests/test_pdf_files/one_page_pdf.pdf', + 'page_count': '1', + 'status': 'valid', + 'recipient': 'The Queen' + })) + mocker.patch('app.main.views.uploads.service_api_client.get_precompiled_template') + + service_one['restricted'] = False + service_one['permissions'] += extra_permissions + client_request.login(active_user_with_permissions, service=service_one) + + with open('tests/test_pdf_files/one_page_pdf.pdf', 'rb') as file: + page = client_request.post( + 'main.upload_letter', + service_id=SERVICE_ONE_ID, + _data={'file': file}, + _follow_redirects=True, + ) + assert antivirus_mock.called + + mock_s3.assert_called_once_with( + b'The sanitised content', + file_location='service-{}/{}.pdf'.format(SERVICE_ONE_ID, fake_uuid), + status='valid', + page_count=1, + filename='tests/test_pdf_files/one_page_pdf.pdf', + recipient='The Queen', + ) + mock_sanitise.assert_called_once_with( + ANY, + allow_international_letters=expected_allow_international, + ) + + assert 'The Queen' in page.find('div', class_='js-stick-at-bottom-when-scrolling').text + assert page.find('h1').text == 'tests/test_pdf_files/one_page_pdf.pdf' + assert not page.find(id='validation-error-message') + + assert not page.find('input', {'name': 'file_id'}) + assert normalize_spaces(page.select('main button[type=submit]')[0].text) == 'Send 1 letter' + + +def test_post_upload_letter_shows_letter_preview_for_valid_file( + mocker, + active_user_with_permissions, + service_one, + client_request, + fake_uuid, +): + letter_template = {'template_type': 'letter', + 'reply_to_text': '', + 'postage': 'second', + 'subject': 'hi', + 'content': 'my letter'} + + mocker.patch('uuid.uuid4', return_value=fake_uuid) + mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True) + mocker.patch( + 'app.main.views.uploads.sanitise_letter', + return_value=Mock( + content='The sanitised content', + json=lambda: {'file': 'VGhlIHNhbml0aXNlZCBjb250ZW50', 'recipient_address': 'The Queen'} + ) + ) + mocker.patch('app.main.views.uploads.upload_letter_to_s3') + mocker.patch('app.main.views.uploads.pdf_page_count', return_value=3) + mocker.patch('app.main.views.uploads.get_letter_metadata', return_value=LetterMetadata({ + 'filename': 'tests/test_pdf_files/one_page_pdf.pdf', + 'page_count': '3', + 'status': 'valid', + 'recipient': 'The Queen' + })) + mocker.patch('app.main.views.uploads.service_api_client.get_precompiled_template', return_value=letter_template) + + service_one['restricted'] = False + client_request.login(active_user_with_permissions, service=service_one) + + with open('tests/test_pdf_files/one_page_pdf.pdf', 'rb') as file: + page = client_request.post( + 'main.upload_letter', + service_id=SERVICE_ONE_ID, + _data={'file': file}, + _follow_redirects=True, + ) + + assert page.find('h1').text == 'tests/test_pdf_files/one_page_pdf.pdf' + assert len(page.select('.letter-postage')) == 0 + # Check postage radios exists and second class is checked by default + assert page.find('input', id="postage-0", value="first") + assert page.find('input', id="postage-1", value="second").has_attr('checked') + + letter_images = page.select('main img') + assert len(letter_images) == 3 + + for page_no, img in enumerate(letter_images, start=1): + assert img['src'] == url_for( + '.view_letter_upload_as_preview', + service_id=SERVICE_ONE_ID, + file_id=fake_uuid, + page=page_no) + + +def test_upload_international_letter_shows_preview_with_no_choice_of_postage( + mocker, + active_user_with_permissions, + service_one, + client_request, + fake_uuid, +): + letter_template = { + 'template_type': 'letter', + 'reply_to_text': '', + 'postage': 'second', + 'subject': 'hi', + 'content': 'my letter', + } + + mocker.patch('uuid.uuid4', return_value=fake_uuid) + mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True) + mocker.patch('app.main.views.uploads.sanitise_letter', return_value=Mock( + content='The sanitised content', + json=lambda: {'file': 'VGhlIHNhbml0aXNlZCBjb250ZW50', 'recipient_address': 'The Queen'} + )) + mocker.patch('app.main.views.uploads.upload_letter_to_s3') + mocker.patch('app.main.views.uploads.pdf_page_count', return_value=3) + mocker.patch('app.main.views.uploads.get_letter_metadata', return_value=LetterMetadata({ + 'filename': 'tests/test_pdf_files/one_page_pdf.pdf', + 'page_count': '3', + 'status': 'valid', + 'recipient': ( + '123 Example Street\n' + 'Andorra la Vella\n' + 'Andorra' + ), + })) + mocker.patch('app.main.views.uploads.service_api_client.get_precompiled_template', return_value=letter_template) + + service_one['restricted'] = False + client_request.login(active_user_with_permissions, service=service_one) + + with open('tests/test_pdf_files/one_page_pdf.pdf', 'rb') as file: + page = client_request.post( + 'main.upload_letter', + service_id=SERVICE_ONE_ID, + _data={'file': file}, + _follow_redirects=True, + ) + + assert page.find('h1').text == 'tests/test_pdf_files/one_page_pdf.pdf' + assert not page.select('.letter-postage') + assert not page.select('input[type=radio]') + assert normalize_spaces( + page.select_one('.js-stick-at-bottom-when-scrolling').text + ) == ( + 'Recipient: 123 Example Street, Andorra la Vella, Andorra ' + 'Postage: international ' + 'Send 1 letter' + ) + + +def test_post_upload_letter_shows_error_when_file_is_not_a_pdf(client_request): + with open('tests/non_spreadsheet_files/actually_a_png.csv', 'rb') as file: + page = client_request.post( + 'main.upload_letter', + service_id=SERVICE_ONE_ID, + _data={'file': file}, + _expected_status=200 + ) + assert page.find('h1').text == 'Wrong file type' + assert page.find('div', class_='banner-dangerous').find('p').text == 'Save your letter as a PDF and try again.' + assert normalize_spaces(page.find('label', class_='file-upload-button').text) == 'Upload your file again' + + +def test_post_upload_letter_shows_error_when_no_file_uploaded(client_request): + page = client_request.post( + 'main.upload_letter', + service_id=SERVICE_ONE_ID, + _data={'file': ''}, + _expected_status=200 + ) + assert page.find('div', class_='banner-dangerous').find('h1').text == 'You need to choose a file to upload' + assert normalize_spaces(page.find('label', class_='file-upload-button').text) == 'Upload your file again' + + +def test_post_upload_letter_shows_error_when_file_contains_virus(mocker, client_request): + mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=False) + + with open('tests/test_pdf_files/one_page_pdf.pdf', 'rb') as file: + page = client_request.post( + 'main.upload_letter', + service_id=SERVICE_ONE_ID, + _data={'file': file}, + _expected_status=400 + ) + assert page.find('div', class_='banner-dangerous').find('h1').text == 'Your file contains a virus' + assert normalize_spaces(page.find('label', class_='file-upload-button').text) == 'Upload your file again' + + +def test_post_choose_upload_file_when_file_is_too_big(mocker, client_request): + mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True) + + with open('tests/test_pdf_files/big.pdf', 'rb') as file: + page = client_request.post( + 'main.upload_letter', + service_id=SERVICE_ONE_ID, + _data={'file': file}, + _expected_status=400 + ) + assert page.find('div', class_='banner-dangerous').find('h1').text == 'Your file is too big' + assert page.find('div', class_='banner-dangerous').find('p').text == 'Files must be smaller than 2MB.' + assert normalize_spaces(page.find('label', class_='file-upload-button').text) == 'Upload your file again' + + +def test_post_choose_upload_file_when_file_is_malformed(mocker, client_request): + mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True) + + with open('tests/test_pdf_files/no_eof_marker.pdf', 'rb') as file: + page = client_request.post( + 'main.upload_letter', + service_id=SERVICE_ONE_ID, + _data={'file': file}, + _expected_status=400 + ) + assert page.find('div', class_='banner-dangerous').find('h1').text == "There’s a problem with your file" + assert page.find( + 'div', class_='banner-dangerous' + ).find('p').text == 'Notify cannot read this PDF.Save a new copy of your file and try again.' + assert normalize_spaces(page.find('label', class_='file-upload-button').text) == 'Upload your file again' + + +def test_post_upload_letter_with_invalid_file(mocker, client_request, fake_uuid): + mocker.patch('uuid.uuid4', return_value=fake_uuid) + mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True) + mock_s3 = mocker.patch('app.main.views.uploads.upload_letter_to_s3') + + mock_sanitise_response = Mock() + mock_sanitise_response.raise_for_status.side_effect = RequestException(response=Mock(status_code=400)) + mock_sanitise_response.json = lambda: { + "message": "content-outside-printable-area", + "invalid_pages": [1] + } + mocker.patch('app.main.views.uploads.sanitise_letter', return_value=mock_sanitise_response) + mocker.patch('app.main.views.uploads.service_api_client.get_precompiled_template') + mocker.patch('app.main.views.uploads.get_letter_metadata', return_value=LetterMetadata({ + 'filename': 'tests/test_pdf_files/one_page_pdf.pdf', 'page_count': '1', 'status': 'invalid', + 'message': 'content-outside-printable-area', 'invalid_pages': '[1]'})) + + with open('tests/test_pdf_files/one_page_pdf.pdf', 'rb') as file: + file_contents = file.read() + file.seek(0) + + page = client_request.post( + 'main.upload_letter', + service_id=SERVICE_ONE_ID, + _data={'file': file}, + _follow_redirects=True + ) + + mock_s3.assert_called_once_with( + file_contents, + file_location='service-{}/{}.pdf'.format(SERVICE_ONE_ID, fake_uuid), + status='invalid', + page_count=1, + filename='tests/test_pdf_files/one_page_pdf.pdf', + invalid_pages=[1], + message='content-outside-printable-area' + ) + + assert page.find('div', class_='banner-dangerous').find('h1', {"data-error-type": 'content-outside-printable-area'}) + assert not page.find('button', {'class': 'page-footer__button', 'type': 'submit'}) + + +def test_post_upload_letter_shows_letter_preview_for_invalid_file(mocker, client_request, fake_uuid): + letter_template = {'template_type': 'letter', + 'reply_to_text': '', + 'postage': 'first', + 'subject': 'hi', + 'content': 'my letter'} + + mocker.patch('uuid.uuid4', return_value=fake_uuid) + mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True) + mocker.patch('app.main.views.uploads.upload_letter_to_s3') + mock_sanitise_response = Mock() + mock_sanitise_response.raise_for_status.side_effect = RequestException(response=Mock(status_code=400)) + mock_sanitise_response.json = lambda: {"message": "template preview error", "recipient_address": "The Queen"} + mocker.patch('app.main.views.uploads.sanitise_letter', return_value=mock_sanitise_response) + mocker.patch('app.main.views.uploads.service_api_client.get_precompiled_template', return_value=letter_template) + mocker.patch('app.main.views.uploads.get_letter_metadata', return_value=LetterMetadata({ + 'filename': 'tests/test_pdf_files/one_page_pdf.pdf', 'page_count': '1', 'status': 'invalid', + 'message': 'template-preview-error'})) + + with open('tests/test_pdf_files/one_page_pdf.pdf', 'rb') as file: + page = client_request.post( + 'main.upload_letter', + service_id=SERVICE_ONE_ID, + _data={'file': file}, + _follow_redirects=True, + ) + + assert 'The Queen' not in page.text + assert len(page.select('.letter-postage')) == 0 + + assert page.find("a", {"class": "govuk-back-link"})["href"] == "/services/{}/upload-letter".format(SERVICE_ONE_ID) + assert page.find("label", {"class": "file-upload-button"}) + + letter_images = page.select('main img') + assert len(letter_images) == 1 + assert letter_images[0]['src'] == url_for( + '.view_letter_upload_as_preview', + service_id=SERVICE_ONE_ID, + file_id=fake_uuid, + page=1 + ) + + +def test_post_upload_letter_does_not_upload_to_s3_if_template_preview_raises_unknown_error( + mocker, + client_request, + fake_uuid, +): + mocker.patch('uuid.uuid4', return_value=fake_uuid) + mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True) + mock_s3 = mocker.patch('app.main.views.uploads.upload_letter_to_s3') + + mocker.patch('app.main.views.uploads.sanitise_letter', side_effect=RequestException()) + + with pytest.raises(RequestException): + with open('tests/test_pdf_files/one_page_pdf.pdf', 'rb') as file: + client_request.post( + 'main.upload_letter', + service_id=SERVICE_ONE_ID, + _data={'file': file}, + _follow_redirects=True + ) + + assert not mock_s3.called + + +def test_uploaded_letter_preview( + mocker, + active_user_with_permissions, + service_one, + client_request, + fake_uuid, +): + mocker.patch('app.main.views.uploads.service_api_client') + mocker.patch('app.main.views.uploads.get_letter_metadata', return_value=LetterMetadata({ + 'filename': 'my_encoded_filename%C2%A3.pdf', + 'page_count': '1', + 'status': 'valid', + 'recipient': 'Bugs Bunny%0A123 Big Hole%0DLooney Town' # 'Bugs Bunny%0A123 Big Hole\rLooney Town' url encoded + })) + + service_one['restricted'] = False + client_request.login(active_user_with_permissions, service=service_one) + + page = client_request.get( + 'main.uploaded_letter_preview', + service_id=SERVICE_ONE_ID, + file_id=fake_uuid, + ) + + assert page.find('h1').text == 'my_encoded_filename£.pdf' + assert page.find('div', class_='letter-sent') + assert not page.find("label", {"class": "file-upload-button"}) + assert page.find('button', {'class': 'page-footer__button', 'type': 'submit'}) + + +def test_uploaded_letter_preview_does_not_show_send_button_if_service_in_trial_mode( + mocker, + client_request, + fake_uuid, +): + mocker.patch('app.main.views.uploads.service_api_client') + mocker.patch('app.main.views.uploads.get_letter_metadata', return_value=LetterMetadata({ + 'filename': 'my_letter.pdf', 'page_count': '1', 'status': 'valid', 'recipient': 'The Queen'})) + + # client_request uses service_one, which is in trial mode + page = client_request.get( + 'main.uploaded_letter_preview', + service_id=SERVICE_ONE_ID, + file_id=fake_uuid, + original_filename='my_letter.pdf', + page_count=1, + status='valid', + error={}, + _test_page_title=False, + ) + + assert normalize_spaces(page.find('h1').text) == 'You cannot send this letter' + assert page.find('div', class_='letter-sent') + assert normalize_spaces( + page.select_one('.js-stick-at-bottom-when-scrolling p').text + ) == ( + 'Recipient: The Queen' + ) + assert not page.find('form') + assert len(page.select('main button[type=submit]')) == 0 + + +def test_uploaded_letter_preview_redirects_if_file_not_in_s3( + mocker, + client_request, + fake_uuid +): + boto_error_json = {'Error': {'Code': 'NoSuchKey', 'Message': 'The specified key does not exist.'}} + mocker.patch( + 'app.main.views.uploads.get_letter_metadata', + side_effect=ClientError(boto_error_json, 'operation_name') + ) + client_request.get( + 'main.uploaded_letter_preview', + service_id=SERVICE_ONE_ID, + file_id=fake_uuid, + _expected_status=302, + _expected_redirect=url_for( + 'main.view_notification', + service_id=SERVICE_ONE_ID, + notification_id=fake_uuid, + _external=True + ) + ) + + +@pytest.mark.parametrize('invalid_pages, page_requested, overlay_expected', ( + ('[1, 2]', 1, True), + ('[1, 2]', 2, True), + ('[1, 2]', 3, False), + ('[]', 1, False), +)) +def test_uploaded_letter_preview_image_shows_overlay_when_content_outside_printable_area_on_a_page( + mocker, + logged_in_client, + mock_get_service, + fake_uuid, + invalid_pages, + page_requested, + overlay_expected, +): + mocker.patch( + 'app.main.views.uploads.get_letter_pdf_and_metadata', + return_value=('pdf_file', { + 'message': 'content-outside-printable-area', + 'invalid_pages': invalid_pages, + }) + ) + template_preview_mock_valid = mocker.patch( + 'app.main.views.uploads.TemplatePreview.from_valid_pdf_file', + return_value=make_response('page.html', 200) + ) + template_preview_mock_invalid = mocker.patch( + 'app.main.views.uploads.TemplatePreview.from_invalid_pdf_file', + return_value=make_response('page.html', 200) + ) + + logged_in_client.get( + url_for( + 'main.view_letter_upload_as_preview', + file_id=fake_uuid, + service_id=SERVICE_ONE_ID, + page=page_requested, + ) + ) + + if overlay_expected: + template_preview_mock_invalid.assert_called_once_with('pdf_file', page_requested) + assert template_preview_mock_valid.called is False + else: + template_preview_mock_valid.assert_called_once_with('pdf_file', page_requested) + assert template_preview_mock_invalid.called is False + + +@pytest.mark.parametrize( + 'metadata', [ + {'message': 'letter-not-a4-portrait-oriented'}, + {'message': 'letter-too-long'}, + {}, + ] +) +def test_uploaded_letter_preview_image_does_not_show_overlay_if_no_content_outside_printable_area( + mocker, + logged_in_client, + mock_get_service, + metadata, + fake_uuid, +): + mocker.patch( + 'app.main.views.uploads.get_letter_pdf_and_metadata', + return_value=('pdf_file', metadata) + ) + template_preview_mock = mocker.patch( + 'app.main.views.uploads.TemplatePreview.from_valid_pdf_file', + return_value=make_response('page.html', 200)) + + logged_in_client.get( + url_for('main.view_letter_upload_as_preview', file_id=fake_uuid, service_id=SERVICE_ONE_ID, page=1) + ) + + template_preview_mock.assert_called_once_with('pdf_file', 1) + + +def test_uploaded_letter_preview_image_400s_for_bad_page_type( + client_request, + fake_uuid, +): + client_request.get( + 'main.view_letter_upload_as_preview', + file_id=fake_uuid, + service_id=SERVICE_ONE_ID, + page='foo', + _test_page_title=False, + _expected_status=400, + ) + + +@pytest.mark.parametrize('address, post_data, expected_postage', ( + ( + 'address', + {'filename': 'my_file.pdf', 'postage': 'first'}, + 'first', + ), + ( + 'address', + {'filename': 'my_file.pdf'}, + 'second', + ), + ( + '123 Example Street\nLiechtenstein', + {'filename': 'my_file.pdf', 'postage': 'first'}, + 'europe', + ), + ( + '123 Example Street\nLiechtenstein', + {'filename': 'my_file.pdf'}, + 'europe', + ), + ( + '123 Example Street\nLesotho', + {'filename': 'my_file.pdf'}, + 'rest-of-world', + ), +)) +def test_send_uploaded_letter_sends_letter_and_redirects_to_notification_page( + mocker, + service_one, + client_request, + fake_uuid, + address, + post_data, + expected_postage, +): + metadata = LetterMetadata({ + 'filename': 'my_file.pdf', + 'page_count': '1', + 'status': 'valid', + 'recipient': address, + }) + + mocker.patch('app.main.views.uploads.get_letter_pdf_and_metadata', return_value=('file', metadata)) + mock_send = mocker.patch('app.main.views.uploads.notification_api_client.send_precompiled_letter') + mocker.patch('app.main.views.uploads.get_letter_metadata', return_value=metadata) + + service_one['permissions'] = ['letter', 'upload_letters'] + + client_request.post( + 'main.send_uploaded_letter', + service_id=SERVICE_ONE_ID, + file_id=fake_uuid, + _data=post_data, + _expected_redirect=url_for( + 'main.view_notification', + service_id=SERVICE_ONE_ID, + notification_id=fake_uuid, + _external=True + ) + ) + mock_send.assert_called_once_with( + SERVICE_ONE_ID, + 'my_file.pdf', + fake_uuid, + expected_postage, + address, + ) + + +@pytest.mark.parametrize('permissions', [ + ['email'], + ['letter'], + ['upload_letters'], +]) +def test_send_uploaded_letter_when_service_does_not_have_correct_permissions( + mocker, + service_one, + client_request, + permissions, + fake_uuid, +): + mocker.patch('app.main.views.uploads.get_letter_pdf_and_metadata', return_value=('file', {'status': 'valid'})) + mock_send = mocker.patch('app.main.views.uploads.notification_api_client.send_precompiled_letter') + + service_one['permissions'] = permissions + + client_request.post( + 'main.send_uploaded_letter', + service_id=SERVICE_ONE_ID, + file_id=fake_uuid, + _data={'filename': 'my_file.pdf', 'postage': 'first'}, + _expected_status=403 + ) + assert not mock_send.called + + +def test_send_uploaded_letter_when_metadata_states_pdf_is_invalid( + mocker, service_one, client_request, fake_uuid, +): + mock_send = mocker.patch('app.main.views.uploads.notification_api_client.send_precompiled_letter') + mocker.patch( + 'app.main.views.uploads.get_letter_metadata', + return_value=LetterMetadata( + { + 'filename': 'my_file.pdf', 'page_count': '3', 'status': 'invalid', + 'message': 'error', 'invalid_pages': '[1]' + } + ) + ) + + service_one['permissions'] = ['letter', 'upload_letters'] + + client_request.post( + 'main.send_uploaded_letter', + service_id=SERVICE_ONE_ID, + file_id=fake_uuid, + _data={'filename': 'my_file.pdf'}, + _expected_status=403 + ) + assert not mock_send.called