Handle files that can’t be interpreted as spreadsheets

There shouldn’t be a case where we see a `ValueError` on upload any
more. Our file handling should be robust enough to deal with whatever is
thrown at it.

This commit:
- adds test files with bad data (PNG files with their extensions changed to look
  like spreadsheets)
- catches whatever exceptions are raised by trying to parse these files
- returns a helpful flash message to the user

Anything else should raise a `500`, eg if the file can’t be uploaded to S3.
This commit is contained in:
Chris Hill-Scott
2016-05-13 21:25:59 +01:00
parent 1409ca36ca
commit 7bbc307a3e
8 changed files with 34 additions and 16 deletions

View File

@@ -4,6 +4,8 @@ import json
import uuid import uuid
import itertools import itertools
from contextlib import suppress from contextlib import suppress
from zipfile import BadZipFile
from xlrd.biffh import XLRDError
from flask import ( from flask import (
request, request,
@@ -123,10 +125,10 @@ def send_messages(service_id, template_id):
service_id=service_id, service_id=service_id,
upload_id=upload_id, upload_id=upload_id,
template_type=template.template_type)) template_type=template.template_type))
except ValueError as e: except (UnicodeDecodeError, BadZipFile, XLRDError):
flash('There was a problem uploading: {}'.format(form.file.data.filename)) flash('Couldnt read {}. Try using a different file format.'.format(
flash(str(e)) form.file.data.filename
return redirect(url_for('.send_messages', service_id=service_id, template_id=template_id)) ))
return render_template( return render_template(
'views/send.html', 'views/send.html',

View File

@@ -1,5 +1,6 @@
import pytest import pytest
import re import re
from itertools import repeat
from io import BytesIO from io import BytesIO
from os import path from os import path
from glob import glob from glob import glob
@@ -12,15 +13,26 @@ template_types = ['email', 'sms']
# The * ignores hidden files, eg .DS_Store # The * ignores hidden files, eg .DS_Store
test_spreadsheet_files = glob(path.join('tests', 'spreadsheet_files', '*')) test_spreadsheet_files = glob(path.join('tests', 'spreadsheet_files', '*'))
test_non_spreadsheet_files = glob(path.join('tests', 'non_spreadsheet_files', '*'))
def test_that_test_files_exist(): def test_that_test_files_exist():
assert len(test_spreadsheet_files) == 8 assert len(test_spreadsheet_files) == 8
assert len(test_non_spreadsheet_files) == 6
@pytest.mark.parametrize("filename", test_spreadsheet_files) @pytest.mark.parametrize(
"filename, acceptable_file",
list(zip(
test_spreadsheet_files, repeat(True)
)) +
list(zip(
test_non_spreadsheet_files, repeat(False)
))
)
def test_upload_files_in_different_formats( def test_upload_files_in_different_formats(
filename, filename,
acceptable_file,
app_, app_,
api_user_active, api_user_active,
mocker, mocker,
@@ -34,18 +46,24 @@ def test_upload_files_in_different_formats(
with app_.test_request_context(), app_.test_client() as client, open(filename, 'rb') as uploaded: with app_.test_request_context(), app_.test_client() as client, open(filename, 'rb') as uploaded:
client.login(api_user_active) client.login(api_user_active)
client.post( response = client.post(
url_for('main.send_messages', service_id=fake_uuid, template_id=fake_uuid), url_for('main.send_messages', service_id=fake_uuid, template_id=fake_uuid),
data={'file': (BytesIO(uploaded.read()), filename)}, data={'file': (BytesIO(uploaded.read()), filename)},
content_type='multipart/form-data' content_type='multipart/form-data'
) )
assert mock_s3_upload.call_args[0][2]['data'].strip() == ( if acceptable_file:
"phone number,name,favourite colour,fruit\r\n" assert mock_s3_upload.call_args[0][2]['data'].strip() == (
"07739 468 050,Pete,Coral,tomato\r\n" "phone number,name,favourite colour,fruit\r\n"
"07527 125 974,Not Pete,Magenta,Avacado\r\n" "07739 468 050,Pete,Coral,tomato\r\n"
"07512 058 823,Still Not Pete,Crimson,Pear" "07527 125 974,Not Pete,Magenta,Avacado\r\n"
) "07512 058 823,Still Not Pete,Crimson,Pear"
)
else:
assert not mock_s3_upload.called
assert (
'Couldnt read {}. Try using a different file format.'.format(filename)
) in response.get_data(as_text=True)
def test_upload_csvfile_with_errors_shows_check_page_with_errors( def test_upload_csvfile_with_errors_shows_check_page_with_errors(
@@ -100,20 +118,18 @@ def test_upload_csv_invalid_extension(app_,
mock_get_users_by_service, mock_get_users_by_service,
mock_get_service_statistics, mock_get_service_statistics,
fake_uuid): fake_uuid):
contents = u'phone number,name\n+44 123,test1\n+44 456,test2'
with app_.test_request_context(): with app_.test_request_context():
filename = 'invalid.txt'
with app_.test_client() as client: with app_.test_client() as client:
client.login(api_user_active) client.login(api_user_active)
resp = client.post( resp = client.post(
url_for('main.send_messages', service_id=fake_uuid, template_id=fake_uuid), url_for('main.send_messages', service_id=fake_uuid, template_id=fake_uuid),
data={'file': (BytesIO(contents.encode('utf-8')), filename)}, data={'file': (BytesIO('contents'.encode('utf-8')), 'invalid.txt')},
content_type='multipart/form-data', content_type='multipart/form-data',
follow_redirects=True follow_redirects=True
) )
assert resp.status_code == 200 assert resp.status_code == 200
assert "{} isnt a spreadsheet that Notify can read".format(filename) in resp.get_data(as_text=True) assert "invalid.txt isnt a spreadsheet that Notify can read" in resp.get_data(as_text=True)
def test_send_test_sms_message( def test_send_test_sms_message(

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB