diff --git a/app/__init__.py b/app/__init__.py index 17bd96179..f860a0c21 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -149,7 +149,8 @@ def register_blueprint(application): def register_v2_blueprints(application): from app.v2.notifications.post_notifications import v2_notification_blueprint as post_notifications from app.v2.notifications.get_notifications import v2_notification_blueprint as get_notifications - from app.v2.template.get_template import template_blueprint + from app.v2.template.get_template import v2_template_blueprint as get_template + from app.v2.template.post_template import v2_template_blueprint as post_template from app.authentication.auth import requires_auth post_notifications.before_request(requires_auth) @@ -158,8 +159,11 @@ def register_v2_blueprints(application): get_notifications.before_request(requires_auth) application.register_blueprint(get_notifications) - template_blueprint.before_request(requires_auth) - application.register_blueprint(template_blueprint) + get_template.before_request(requires_auth) + application.register_blueprint(get_template) + + post_template.before_request(requires_auth) + application.register_blueprint(post_template) def init_app(app): diff --git a/app/utils.py b/app/utils.py index 15d38e42d..a939f849d 100644 --- a/app/utils.py +++ b/app/utils.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta import pytz from flask import url_for from sqlalchemy import func -from notifications_utils.template import SMSMessageTemplate, PlainTextEmailTemplate +from notifications_utils.template import SMSMessageTemplate, PlainTextEmailTemplate, LetterPreviewTemplate def pagination_links(pagination, endpoint, **kwargs): @@ -26,9 +26,9 @@ def url_with_token(data, url, config): def get_template_instance(template, values): - from app.models import SMS_TYPE, EMAIL_TYPE + from app.models import SMS_TYPE, EMAIL_TYPE, LETTER_TYPE return { - SMS_TYPE: SMSMessageTemplate, EMAIL_TYPE: PlainTextEmailTemplate + SMS_TYPE: SMSMessageTemplate, EMAIL_TYPE: PlainTextEmailTemplate, LETTER_TYPE: LetterPreviewTemplate }[template['template_type']](template, values) diff --git a/app/v2/template/__init__.py b/app/v2/template/__init__.py index ad051cfca..32a21d564 100644 --- a/app/v2/template/__init__.py +++ b/app/v2/template/__init__.py @@ -2,6 +2,6 @@ from flask import Blueprint from app.v2.errors import register_errors -template_blueprint = Blueprint("v2_template", __name__, url_prefix='/v2/template') +v2_template_blueprint = Blueprint("v2_template", __name__, url_prefix='/v2/template') -register_errors(template_blueprint) +register_errors(v2_template_blueprint) diff --git a/app/v2/template/get_template.py b/app/v2/template/get_template.py index 794cba614..8bfbddc5a 100644 --- a/app/v2/template/get_template.py +++ b/app/v2/template/get_template.py @@ -7,22 +7,18 @@ from werkzeug.exceptions import abort from app import api_user from app.dao import templates_dao from app.schema_validation import validate -from app.v2.template import template_blueprint +from app.v2.template import v2_template_blueprint from app.v2.template.template_schemas import get_template_by_id_request -@template_blueprint.route("/", methods=['GET']) -@template_blueprint.route("//version/", methods=['GET']) +@v2_template_blueprint.route("/", methods=['GET']) +@v2_template_blueprint.route("//version/", methods=['GET']) def get_template_by_id(template_id, version=None): - try: - _data = {} - _data['id'] = template_id - if version: - _data['version'] = version + _data = {'id': template_id} + if version: + _data['version'] = version - data = validate(_data, get_template_by_id_request) - except ValueError or AttributeError: - abort(404) + data = validate(_data, get_template_by_id_request) template = templates_dao.dao_get_template_by_id_and_service_id( template_id, api_user.service_id, data.get('version')) diff --git a/app/v2/template/post_template.py b/app/v2/template/post_template.py new file mode 100644 index 000000000..531a9683c --- /dev/null +++ b/app/v2/template/post_template.py @@ -0,0 +1,41 @@ +import uuid + +from flask import jsonify, request +from jsonschema.exceptions import ValidationError +from werkzeug.exceptions import abort + +from app import api_user +from app.dao import templates_dao +from app.schema_validation import validate +from app.utils import get_template_instance +from app.v2.errors import BadRequestError +from app.v2.template import v2_template_blueprint +from app.v2.template.template_schemas import post_template_preview_request, create_post_template_preview_response + + +@v2_template_blueprint.route("//preview", methods=['POST']) +def post_template_preview(template_id): + _data = request.get_json() + _data['id'] = template_id + + data = validate(_data, post_template_preview_request) + + template = templates_dao.dao_get_template_by_id_and_service_id( + template_id, api_user.service_id) + + template_object = get_template_instance( + template.__dict__, values=data.get('personalisation')) + + check_placeholders(template_object) + + resp = create_post_template_preview_response(template=template, + body=str(template_object), + url_root=request.url_root) + + return jsonify(resp), 200 + + +def check_placeholders(template_object): + if template_object.missing_data: + message = 'Missing personalisation: {}'.format(", ".join(template_object.missing_data)) + raise BadRequestError(message=message, fields=[{'template': message}]) diff --git a/app/v2/template/template_schemas.py b/app/v2/template/template_schemas.py index fc727302b..bc405129c 100644 --- a/app/v2/template/template_schemas.py +++ b/app/v2/template/template_schemas.py @@ -1,5 +1,5 @@ from app.models import TEMPLATE_TYPES -from app.schema_validation.definitions import uuid +from app.schema_validation.definitions import uuid, personalisation get_template_by_id_request = { @@ -39,3 +39,41 @@ get_template_by_id_response = { }, "required": ["id", "type", "created_at", "updated_at", "version", "created_by", "body"] } + +post_template_preview_request = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "POST template schema", + "type": "object", + "title": "POST v2/template/{id}/preview", + "properties": { + "id": uuid, + "personalisation": personalisation + }, + "required": ["id", "personalisation"] +} + +post_template_preview_response = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "POST template preview schema response", + "type": "object", + "title": "reponse v2/template/{id}/preview", + "properties": { + "id": uuid, + "type": {"enum": TEMPLATE_TYPES}, + "version": {"type": "integer"}, + "body": {"type": "string"}, + "subject": {"type": ["string", "null"]} + }, + "required": ["id", "type", "version", "body"] +} + + +def create_post_template_preview_response(template, body, url_root): + return { + "id": template.id, + "type": template.template_type, + "version": template.version, + "body": body, + "subject": template.subject, + "uri": "{}v2/template/{}/preview".format(url_root, template.id) + } diff --git a/tests/app/v2/template/test_get_template.py b/tests/app/v2/template/test_get_template.py index f883dec56..ef3c9f437 100644 --- a/tests/app/v2/template/test_get_template.py +++ b/tests/app/v2/template/test_get_template.py @@ -4,15 +4,14 @@ import uuid from flask import json from app import DATETIME_FORMAT -from app.models import EMAIL_TYPE, SMS_TYPE, LETTER_TYPE +from app.models import EMAIL_TYPE, SMS_TYPE, LETTER_TYPE, TEMPLATE_TYPES from tests import create_authorization_header from tests.app.db import create_template -template_types = [EMAIL_TYPE, SMS_TYPE, LETTER_TYPE] valid_version_params = [None, 1] -@pytest.mark.parametrize("tmp_type", template_types) +@pytest.mark.parametrize("tmp_type", TEMPLATE_TYPES) @pytest.mark.parametrize("version", valid_version_params) def test_get_email_template_by_id_returns_200(client, sample_service, tmp_type, version): template = create_template(sample_service, template_type=tmp_type) @@ -42,12 +41,10 @@ def test_get_email_template_by_id_returns_200(client, sample_service, tmp_type, assert json_response == expected_response -def test_get_template_with_non_existent_template_id_returns_404(client, sample_service): +def test_get_template_with_non_existent_template_id_returns_404(client, fake_uuid, sample_service): auth_header = create_authorization_header(service_id=sample_service.id) - random_template_id = str(uuid.uuid4()) - - response = client.get(path='/v2/template/{}'.format(random_template_id), + response = client.get(path='/v2/template/{}'.format(fake_uuid), headers=[('Content-Type', 'application/json'), auth_header]) assert response.status_code == 404 @@ -66,7 +63,7 @@ def test_get_template_with_non_existent_template_id_returns_404(client, sample_s } -@pytest.mark.parametrize("tmp_type", template_types) +@pytest.mark.parametrize("tmp_type", TEMPLATE_TYPES) def test_get_template_with_non_existent_version_returns_404(client, sample_service, tmp_type): template = create_template(sample_service, template_type=tmp_type) diff --git a/tests/app/v2/template/test_post_template.py b/tests/app/v2/template/test_post_template.py new file mode 100644 index 000000000..f99b3ee86 --- /dev/null +++ b/tests/app/v2/template/test_post_template.py @@ -0,0 +1,81 @@ +import pytest +import uuid + +from flask import json + +from app.models import TEMPLATE_TYPES +from tests import create_authorization_header +from tests.app.db import create_template + +valid_data = { + 'personalisation': {'Name': 'Jo'} +} + + +@pytest.mark.parametrize("tmp_type", TEMPLATE_TYPES) +def test_valid_post_template_returns_200(client, sample_service, tmp_type): + template = create_template( + sample_service, + template_type=tmp_type, + content='Dear ((Name)), Hello. Yours Truly, The Government.') + + auth_header = create_authorization_header(service_id=sample_service.id) + + response = client.post( + path='/v2/template/{}/preview'.format(template.id), + data=json.dumps(valid_data), + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 200 + + resp_json = json.loads(response.get_data(as_text=True)) + + assert resp_json['id'] == str(template.id) + assert 'v2/template/{}/preview'.format(template.id) in resp_json['uri'] + assert 'Dear {}'.format(valid_data['personalisation']['Name']) in resp_json['body'] + + +@pytest.mark.parametrize("tmp_type", TEMPLATE_TYPES) +def test_invalid_post_template_returns_400(client, sample_service, tmp_type): + template = create_template( + sample_service, + template_type=tmp_type, + content='Dear ((Name)), Hello ((Missing)). Yours Truly, The Government.') + + auth_header = create_authorization_header(service_id=sample_service.id) + + response = client.post( + path='/v2/template/{}/preview'.format(template.id), + data=json.dumps(valid_data), + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 400 + + resp_json = json.loads(response.get_data(as_text=True)) + + assert resp_json['errors'][0]['error'] == 'BadRequestError' + assert 'Missing personalisation: Missing' in resp_json['errors'][0]['message'] + + +def test_post_template_with_non_existent_template_id_returns_404(client, fake_uuid, sample_service): + auth_header = create_authorization_header(service_id=sample_service.id) + + response = client.post( + path='/v2/template/{}/preview'.format(fake_uuid), + data=json.dumps(valid_data), + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 404 + assert response.headers['Content-type'] == 'application/json' + + json_response = json.loads(response.get_data(as_text=True)) + + assert json_response == { + "errors": [ + { + "error": "NoResultFound", + "message": "No result found" + } + ], + "status_code": 404 + } diff --git a/tests/app/v2/template/test_template_schemas.py b/tests/app/v2/template/test_template_schemas.py index 3a89cf5e2..97fca1ca4 100644 --- a/tests/app/v2/template/test_template_schemas.py +++ b/tests/app/v2/template/test_template_schemas.py @@ -3,37 +3,39 @@ import uuid import pytest from flask import json +from app.models import EMAIL_TYPE, SMS_TYPE, LETTER_TYPE, TEMPLATE_TYPES from app.v2.template.template_schemas import ( get_template_by_id_response, - get_template_by_id_request + get_template_by_id_request, + post_template_preview_request, + post_template_preview_response ) from app.schema_validation import validate from jsonschema.exceptions import ValidationError -valid_json = { +valid_json_get_response = { 'id': str(uuid.uuid4()), - 'type': 'email', + 'type': SMS_TYPE, 'created_at': '2017-01-10T18:25:43.511Z', 'updated_at': None, 'version': 1, 'created_by': 'someone@test.com', - 'body': "some body" + 'body': 'some body' } -valid_json_with_optionals = { +valid_json_get_response_with_optionals = { 'id': str(uuid.uuid4()), - 'type': 'email', + 'type': EMAIL_TYPE, 'created_at': '2017-01-10T18:25:43.511Z', 'updated_at': None, 'version': 1, 'created_by': 'someone', - 'body': "some body", + 'body': 'some body', 'subject': "some subject" } -valid_request_args = [ - {"id": str(uuid.uuid4()), "version": 1}, {"id": str(uuid.uuid4())}] +valid_request_args = [{"id": str(uuid.uuid4()), "version": 1}, {"id": str(uuid.uuid4())}] invalid_request_args = [ ({"id": str(uuid.uuid4()), "version": "test"}, ["version test is not of type integer, null"]), @@ -43,6 +45,39 @@ invalid_request_args = [ ({"id": "invalid_uuid", "version": 0}, ["version 0 is less than the minimum of 1", "id is not a valid UUID"]) ] +valid_json_post_args = { + "id": str(uuid.uuid4()), + "personalisation": {"key": "value"} +} + +invalid_json_post_args = [ + ({"id": "invalid_uuid", "personalisation": {"key": "value"}}, ["id is not a valid UUID"]), + ({"id": str(uuid.uuid4()), "personalisation": "invalid_personalisation"}, + ["personalisation should contain key value pairs"]), + ({"id": str(uuid.uuid4())}, ["personalisation is a required property"]), + ({"personalisation": "invalid_personalisation"}, + ["id is a required property", + "personalisation is a required property", + "personalisation should contain key value pairs"]) +] + +valid_json_post_response = { + 'id': str(uuid.uuid4()), + 'type': 'email', + 'version': 1, + 'created_by': 'someone@test.com', + 'body': 'some body' +} + +valid_json_post_response_with_optionals = { + 'id': str(uuid.uuid4()), + 'type': 'email', + 'version': 1, + 'created_by': 'someone@test.com', + 'body': "some body", + 'subject': 'some subject' +} + @pytest.mark.parametrize("args", valid_request_args) def test_get_template_request_schema_against_valid_args_is_valid(args): @@ -61,10 +96,37 @@ def test_get_template_request_schema_against_invalid_args_is_invalid(args, error assert error['message'] in error_message -@pytest.mark.parametrize("response", [valid_json, valid_json_with_optionals]) +@pytest.mark.parametrize("template_type", TEMPLATE_TYPES) +@pytest.mark.parametrize("response", [valid_json_get_response, valid_json_get_response_with_optionals]) @pytest.mark.parametrize("updated_datetime", [None, '2017-01-11T18:25:43.511Z']) -def test_get_template_response_schema_is_valid(response, updated_datetime): +def test_get_template_response_schema_is_valid(response, template_type, updated_datetime): if updated_datetime: response['updated_at'] = updated_datetime + response['type'] = template_type + assert validate(response, get_template_by_id_response) == response + + +def test_post_template_preview_against_valid_args_is_valid(): + assert validate(valid_json_post_args, post_template_preview_request) == valid_json_post_args + + +@pytest.mark.parametrize("args,error_message", invalid_json_post_args) +def test_post_template_preview_against_invalid_args_is_invalid(args, error_message): + with pytest.raises(ValidationError) as e: + validate(args, post_template_preview_request) + errors = json.loads(str(e.value)) + + assert errors['status_code'] == 400 + + for error in errors['errors']: + assert error['message'] in error_message + + +@pytest.mark.parametrize("template_type", TEMPLATE_TYPES) +@pytest.mark.parametrize("response", [valid_json_post_response, valid_json_post_response_with_optionals]) +def test_post_template_preview_response_schema_is_valid(response, template_type): + response['type'] = template_type + + assert validate(response, post_template_preview_response) == response