diff --git a/app/v2/errors.py b/app/v2/errors.py index 51262df4a..2b726e23d 100644 --- a/app/v2/errors.py +++ b/app/v2/errors.py @@ -1,7 +1,7 @@ import json from flask import jsonify, current_app, request -from jsonschema import ValidationError +from jsonschema import ValidationError as JsonSchemaValidationError from notifications_utils.recipients import InvalidEmailError from sqlalchemy.exc import DataError from sqlalchemy.orm.exc import NoResultFound @@ -57,6 +57,15 @@ class BadRequestError(InvalidRequest): self.message = message if message else self.message +class ValidationError(InvalidRequest): + message = "Your notification has failed validation" + + def __init__(self, fields=[], message=None, status_code=400): + self.status_code = status_code + self.fields = fields + self.message = message if message else self.message + + class PDFNotReadyError(BadRequestError): def __init__(self): super().__init__(message='PDF not available yet, try again later', status_code=400) @@ -77,7 +86,7 @@ def register_errors(blueprint): response = jsonify(error.to_dict_v2()), error.status_code return response - @blueprint.errorhandler(ValidationError) + @blueprint.errorhandler(JsonSchemaValidationError) def validation_error(error): current_app.logger.info(error) return jsonify(json.loads(error.message)), 400 diff --git a/app/v2/notifications/post_notifications.py b/app/v2/notifications/post_notifications.py index 32bb949d5..08595fc1f 100644 --- a/app/v2/notifications/post_notifications.py +++ b/app/v2/notifications/post_notifications.py @@ -5,7 +5,9 @@ from datetime import datetime from boto.exception import SQSError from flask import request, jsonify, current_app, abort -from notifications_utils.recipients import try_validate_and_format_phone_number +from notifications_utils.recipients import ( + format_postcode_for_printing, is_a_real_uk_postcode, try_validate_and_format_phone_number +) from app import ( api_user, @@ -56,7 +58,7 @@ from app.notifications.validators import ( validate_template, ) from app.schema_validation import validate -from app.v2.errors import BadRequestError +from app.v2.errors import BadRequestError, ValidationError from app.v2.notifications import v2_notification_blueprint from app.v2.notifications.create_response import ( create_post_sms_response_from_notification, @@ -344,12 +346,18 @@ def process_letter_notification(*, letter_data, api_key, template, reply_to_text template=template, reply_to_text=reply_to_text) + postcode = letter_data['personalisation']['postcode'] + if not is_a_real_uk_postcode(postcode): + raise ValidationError(message='Must be a real UK postcode') + test_key = api_key.key_type == KEY_TYPE_TEST # if we don't want to actually send the letter, then start it off in SENDING so we don't pick it up status = NOTIFICATION_CREATED if not test_key else NOTIFICATION_SENDING queue = QueueNames.CREATE_LETTERS_PDF if not test_key else QueueNames.RESEARCH_MODE + letter_data['personalisation']['postcode'] = format_postcode_for_printing(postcode) + notification = create_letter_notification(letter_data=letter_data, template=template, api_key=api_key, diff --git a/tests/app/v2/notifications/test_post_letter_notifications.py b/tests/app/v2/notifications/test_post_letter_notifications.py index 851d36647..305506d9f 100644 --- a/tests/app/v2/notifications/test_post_letter_notifications.py +++ b/tests/app/v2/notifications/test_post_letter_notifications.py @@ -118,6 +118,56 @@ def test_post_letter_notification_sets_postage( assert notification.postage == "first" +def test_post_letter_notification_formats_postcode( + client, notify_db_session, mocker +): + service = create_service(service_permissions=[LETTER_TYPE]) + template = create_template(service, template_type="letter") + mocker.patch('app.celery.tasks.letters_pdf_tasks.create_letters_pdf.apply_async') + data = { + 'template_id': str(template.id), + 'personalisation': { + 'address_line_1': 'Her Royal Highness Queen Elizabeth II', + 'address_line_2': 'Buckingham Palace', + 'address_line_3': 'London', + 'postcode': ' Sw1 1aa ', + 'name': 'Lizzie' + } + } + + resp_json = letter_request(client, data, service_id=service.id) + + assert validate(resp_json, post_letter_response) == resp_json + notification = Notification.query.one() + assert notification.personalisation["postcode"] == "SW1 1AA" + + +def test_post_letter_notification_throws_error_for_bad_postcode( + client, notify_db_session, mocker +): + service = create_service(service_permissions=[LETTER_TYPE]) + template = create_template(service, template_type="letter", postage="first") + mocker.patch('app.celery.tasks.letters_pdf_tasks.create_letters_pdf.apply_async') + data = { + 'template_id': str(template.id), + 'personalisation': { + 'address_line_1': 'Her Royal Highness Queen Elizabeth II', + 'address_line_2': 'Buckingham Palace', + 'address_line_3': 'London', + 'postcode': 'not a real postcode', + 'name': 'Lizzie' + } + } + + error_json = letter_request(client, data, service_id=service.id, _expected_status=400) + + assert error_json['status_code'] == 400 + assert error_json['errors'] == [{ + 'error': 'ValidationError', + 'message': 'Must be a real UK postcode' + }] + + @pytest.mark.parametrize('env', [ 'staging', 'live', @@ -366,7 +416,7 @@ def test_post_letter_notification_is_delivered_but_still_creates_pdf_if_in_trial data = { "template_id": sample_trial_letter_template.id, - "personalisation": {'address_line_1': 'Foo', 'address_line_2': 'Bar', 'postcode': 'Baz'} + "personalisation": {'address_line_1': 'Foo', 'address_line_2': 'Bar', 'postcode': 'BA5 5AB'} } letter_request(client, data=data, service_id=sample_trial_letter_template.service_id, key_type=KEY_TYPE_TEST) @@ -411,7 +461,7 @@ def test_post_letter_notification_persists_notification_reply_to_text( template = create_template(service=service, template_type='letter', reply_to=letter_contact.id) data = { "template_id": template.id, - "personalisation": {'address_line_1': 'Foo', 'address_line_2': 'Bar', 'postcode': 'Baz'} + "personalisation": {'address_line_1': 'Foo', 'address_line_2': 'Bar', 'postcode': 'BA5 5AB'} } letter_request(client, data=data, service_id=service.id, key_type=KEY_TYPE_NORMAL)