diff --git a/app/__init__.py b/app/__init__.py index b64f1e745..8e774cccb 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -156,6 +156,7 @@ 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 v2_template_blueprint as get_template + from app.v2.templates.get_templates import v2_templates_blueprint as get_templates from app.v2.template.post_template import v2_template_blueprint as post_template from app.authentication.auth import requires_auth @@ -165,6 +166,9 @@ def register_v2_blueprints(application): get_notifications.before_request(requires_auth) application.register_blueprint(get_notifications) + get_templates.before_request(requires_auth) + application.register_blueprint(get_templates) + get_template.before_request(requires_auth) application.register_blueprint(get_template) diff --git a/app/dao/templates_dao.py b/app/dao/templates_dao.py index 567e763f3..493c7c072 100644 --- a/app/dao/templates_dao.py +++ b/app/dao/templates_dao.py @@ -42,7 +42,16 @@ def dao_get_template_by_id(template_id, version=None): return Template.query.filter_by(id=template_id).one() -def dao_get_all_templates_for_service(service_id): +def dao_get_all_templates_for_service(service_id, template_type=None): + if template_type is not None: + return Template.query.filter_by( + service_id=service_id, + template_type=template_type, + archived=False + ).order_by( + desc(Template.created_at) + ).all() + return Template.query.filter_by( service_id=service_id, archived=False diff --git a/app/models.py b/app/models.py index 7b1b62cc2..f829ca429 100644 --- a/app/models.py +++ b/app/models.py @@ -321,9 +321,8 @@ class Template(db.Model): ) def serialize(self): - serialized = { - "id": self.id, + "id": str(self.id), "type": self.template_type, "created_at": self.created_at.strftime(DATETIME_FORMAT), "updated_at": self.updated_at.strftime(DATETIME_FORMAT) if self.updated_at else None, diff --git a/app/v2/template/get_template.py b/app/v2/template/get_template.py index 8bfbddc5a..fef0e1b2e 100644 --- a/app/v2/template/get_template.py +++ b/app/v2/template/get_template.py @@ -1,8 +1,5 @@ -import uuid - -from flask import jsonify, request +from flask import jsonify from jsonschema.exceptions import ValidationError -from werkzeug.exceptions import abort from app import api_user from app.dao import templates_dao diff --git a/app/v2/templates/__init__.py b/app/v2/templates/__init__.py new file mode 100644 index 000000000..6e0989dd4 --- /dev/null +++ b/app/v2/templates/__init__.py @@ -0,0 +1,7 @@ +from flask import Blueprint + +from app.v2.errors import register_errors + +v2_templates_blueprint = Blueprint("v2_templates", __name__, url_prefix='/v2/templates') + +register_errors(v2_templates_blueprint) diff --git a/app/v2/templates/get_templates.py b/app/v2/templates/get_templates.py new file mode 100644 index 000000000..101ace2f9 --- /dev/null +++ b/app/v2/templates/get_templates.py @@ -0,0 +1,19 @@ +from flask import jsonify, request +from jsonschema.exceptions import ValidationError + +from app import api_user +from app.dao import templates_dao +from app.schema_validation import validate +from app.v2.templates import v2_templates_blueprint +from app.v2.templates.templates_schemas import get_all_template_request + + +@v2_templates_blueprint.route("/", methods=['GET']) +def get_templates(): + data = validate(request.args.to_dict(), get_all_template_request) + + templates = templates_dao.dao_get_all_templates_for_service(api_user.service_id, data.get('type')) + + return jsonify( + templates=[template.serialize() for template in templates] + ), 200 diff --git a/app/v2/templates/templates_schemas.py b/app/v2/templates/templates_schemas.py new file mode 100644 index 000000000..d5b335874 --- /dev/null +++ b/app/v2/templates/templates_schemas.py @@ -0,0 +1,32 @@ +from app.models import TEMPLATE_TYPES +from app.v2.template.template_schemas import get_template_by_id_response as template + + +get_all_template_request = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "request schema for parameters allowed when getting all templates", + "type": "object", + "properties": { + "type": {"enum": TEMPLATE_TYPES} + }, + "additionalProperties": False, +} + +get_all_template_response = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "GET response schema when getting all templates", + "type": "object", + "properties": { + "templates": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/template" + } + } + }, + "required": ["templates"], + "definitions": { + "template": template + } +} diff --git a/tests/app/v2/template/test_get_template.py b/tests/app/v2/template/test_get_template.py index ef3c9f437..0f028a967 100644 --- a/tests/app/v2/template/test_get_template.py +++ b/tests/app/v2/template/test_get_template.py @@ -1,10 +1,9 @@ import pytest -import uuid from flask import json from app import DATETIME_FORMAT -from app.models import EMAIL_TYPE, SMS_TYPE, LETTER_TYPE, TEMPLATE_TYPES +from app.models import EMAIL_TYPE, TEMPLATE_TYPES from tests import create_authorization_header from tests.app.db import create_template @@ -13,7 +12,7 @@ valid_version_params = [None, 1] @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): +def test_get_template_by_id_returns_200(client, sample_service, tmp_type, version): template = create_template(sample_service, template_type=tmp_type) auth_header = create_authorization_header(service_id=sample_service.id) diff --git a/tests/app/v2/template/test_template_schemas.py b/tests/app/v2/template/test_template_schemas.py index c89802942..4517ad7e8 100644 --- a/tests/app/v2/template/test_template_schemas.py +++ b/tests/app/v2/template/test_template_schemas.py @@ -3,7 +3,7 @@ import uuid import pytest from flask import json -from app.models import EMAIL_TYPE, SMS_TYPE, LETTER_TYPE, TEMPLATE_TYPES +from app.models import EMAIL_TYPE, SMS_TYPE, TEMPLATE_TYPES from app.v2.template.template_schemas import ( get_template_by_id_response, get_template_by_id_request, @@ -55,9 +55,7 @@ invalid_json_post_args = [ ({"id": str(uuid.uuid4()), "personalisation": "invalid_personalisation"}, ["personalisation should contain key value pairs"]), ({"personalisation": "invalid_personalisation"}, - ["id is a required property", - "personalisation is a required property", - "personalisation should contain key value pairs"]) + ["id is a required property", "personalisation should contain key value pairs"]) ] valid_json_post_response = { @@ -111,16 +109,16 @@ 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): +@pytest.mark.parametrize("args,error_messages", invalid_json_post_args) +def test_post_template_preview_against_invalid_args_is_invalid(args, error_messages): with pytest.raises(ValidationError) as e: validate(args, post_template_preview_request) errors = json.loads(str(e.value)) assert errors['status_code'] == 400 - + assert len(errors['errors']) == len(error_messages) for error in errors['errors']: - assert error['message'] in error_message + assert error['message'] in error_messages @pytest.mark.parametrize("template_type", TEMPLATE_TYPES) diff --git a/tests/app/v2/templates/test_get_templates.py b/tests/app/v2/templates/test_get_templates.py new file mode 100644 index 000000000..180752271 --- /dev/null +++ b/tests/app/v2/templates/test_get_templates.py @@ -0,0 +1,115 @@ +import pytest + +from flask import json + +from app.models import TEMPLATE_TYPES, EMAIL_TYPE +from tests import create_authorization_header +from tests.app.db import create_template + + +def test_get_all_templates_returns_200(client, sample_service): + num_templates = 3 + templates = [] + for i in range(num_templates): + for tmp_type in TEMPLATE_TYPES: + subject = 'subject_{}'.format(i) if tmp_type == EMAIL_TYPE else '' + templates.append(create_template(sample_service, template_type=tmp_type, subject=subject)) + + auth_header = create_authorization_header(service_id=sample_service.id) + + response = client.get(path='/v2/templates/', + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 200 + assert response.headers['Content-type'] == 'application/json' + + json_response = json.loads(response.get_data(as_text=True)) + + assert len(json_response['templates']) == num_templates * len(TEMPLATE_TYPES) + + # need to reverse index as get all templates returns list sorted by descending date + for i in range(len(json_response['templates'])): + reverse_index = len(json_response['templates']) - 1 - i + assert json_response['templates'][reverse_index]['id'] == str(templates[i].id) + assert json_response['templates'][reverse_index]['body'] == templates[i].content + assert json_response['templates'][reverse_index]['type'] == templates[i].template_type + if templates[i].template_type == EMAIL_TYPE: + assert json_response['templates'][reverse_index]['subject'] == templates[i].subject + + +@pytest.mark.parametrize("tmp_type", TEMPLATE_TYPES) +def test_get_all_templates_for_valid_type_returns_200(client, sample_service, tmp_type): + num_templates = 3 + templates = [] + for i in range(num_templates): + subject = 'subject_{}'.format(i) if tmp_type == EMAIL_TYPE else '' + templates.append(create_template(sample_service, template_type=tmp_type, subject=subject)) + + auth_header = create_authorization_header(service_id=sample_service.id) + + response = client.get(path='/v2/templates/?type={}'.format(tmp_type), + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 200 + assert response.headers['Content-type'] == 'application/json' + + json_response = json.loads(response.get_data(as_text=True)) + + assert len(json_response['templates']) == num_templates + + # need to reverse index as get all templates returns list sorted by descending date + for i in range(len(json_response['templates'])): + reverse_index = len(json_response['templates']) - 1 - i + assert json_response['templates'][reverse_index]['id'] == str(templates[i].id) + assert json_response['templates'][reverse_index]['body'] == templates[i].content + assert json_response['templates'][reverse_index]['type'] == tmp_type + if templates[i].template_type == EMAIL_TYPE: + assert json_response['templates'][reverse_index]['subject'] == templates[i].subject + + +@pytest.mark.parametrize("tmp_type", TEMPLATE_TYPES) +def test_get_correct_num_templates_for_valid_type_returns_200(client, sample_service, tmp_type): + num_templates = 3 + + templates = [] + for i in range(num_templates): + templates.append(create_template(sample_service, template_type=tmp_type)) + + for other_type in TEMPLATE_TYPES: + if other_type != tmp_type: + templates.append(create_template(sample_service, template_type=other_type)) + + auth_header = create_authorization_header(service_id=sample_service.id) + + response = client.get(path='/v2/templates/?type={}'.format(tmp_type), + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 200 + + json_response = json.loads(response.get_data(as_text=True)) + + assert len(json_response['templates']) == num_templates + + +def test_get_all_templates_for_invalid_type_returns_400(client, sample_service): + auth_header = create_authorization_header(service_id=sample_service.id) + + invalid_type = 'coconut' + + response = client.get(path='/v2/templates/?type={}'.format(invalid_type), + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 400 + assert response.headers['Content-type'] == 'application/json' + + json_response = json.loads(response.get_data(as_text=True)) + + assert json_response == { + 'status_code': 400, + 'errors': [ + { + 'message': 'type coconut is not one of [sms, email, letter]', + 'error': 'ValidationError' + } + ] + } diff --git a/tests/app/v2/templates/test_templates_schemas.py b/tests/app/v2/templates/test_templates_schemas.py new file mode 100644 index 000000000..df798ce82 --- /dev/null +++ b/tests/app/v2/templates/test_templates_schemas.py @@ -0,0 +1,235 @@ +import uuid + +import pytest +from flask import json + +from app.models import EMAIL_TYPE, SMS_TYPE, TEMPLATE_TYPES +from app.v2.templates.templates_schemas import ( + get_all_template_request, + get_all_template_response +) +from app.schema_validation import validate +from jsonschema.exceptions import ValidationError + + +valid_json_get_all_response = [ + { + "templates": [ + { + 'id': str(uuid.uuid4()), + 'type': SMS_TYPE, + 'created_at': '2017-01-10T18:25:43.511Z', + 'updated_at': None, + 'version': 1, + 'created_by': 'someone@test.com', + 'body': 'some body' + }, + { + 'id': str(uuid.uuid4()), + 'type': EMAIL_TYPE, + 'created_at': '2017-02-10T18:25:43.511Z', + 'updated_at': None, + 'version': 2, + 'created_by': 'someone@test.com', + 'subject': 'test subject', + 'body': 'some body' + } + ] + }, + { + "templates": [ + { + 'id': str(uuid.uuid4()), + 'type': SMS_TYPE, + 'created_at': '2017-02-10T18:25:43.511Z', + 'updated_at': None, + 'version': 2, + 'created_by': 'someone@test.com', + 'body': 'some body' + } + ] + }, + { + "templates": [] + } +] + +invalid_json_get_all_response = [ + ({ + "templates": [ + { + 'id': 'invalid_id', + 'type': SMS_TYPE, + 'created_at': '2017-02-10T18:25:43.511Z', + 'updated_at': None, + 'version': 1, + 'created_by': 'someone@test.com', + 'body': 'some body' + } + ] + }, ['templates is not a valid UUID']), + ({ + "templates": [ + { + 'id': str(uuid.uuid4()), + 'type': SMS_TYPE, + 'created_at': '2017-02-10T18:25:43.511Z', + 'updated_at': None, + 'version': 'invalid_version', + 'created_by': 'someone@test.com', + 'body': 'some body' + } + ] + }, ['templates invalid_version is not of type integer']), + ({ + "templates": [ + { + 'id': str(uuid.uuid4()), + 'type': SMS_TYPE, + 'created_at': 'invalid_created_at', + 'updated_at': None, + 'version': 1, + 'created_by': 'someone@test.com', + 'body': 'some body' + } + ] + }, ['templates invalid_created_at is not a date-time']), + ({}, ['templates is a required property']), + ({ + "templates": [ + { + 'type': SMS_TYPE, + 'created_at': '2017-02-10T18:25:43.511Z', + 'updated_at': None, + 'version': 1, + 'created_by': 'someone@test.com', + 'body': 'some body' + } + ] + }, ['templates id is a required property']), + ({ + "templates": [ + { + 'id': str(uuid.uuid4()), + 'created_at': '2017-02-10T18:25:43.511Z', + 'updated_at': None, + 'version': 1, + 'created_by': 'someone@test.com', + 'body': 'some body' + } + ] + }, ['templates type is a required property']), + ({ + "templates": [ + { + 'id': str(uuid.uuid4()), + 'type': SMS_TYPE, + 'updated_at': None, + 'version': 1, + 'created_by': 'someone@test.com', + 'body': 'some body' + } + ] + }, ['templates created_at is a required property']), + ({ + "templates": [ + { + 'id': str(uuid.uuid4()), + 'type': SMS_TYPE, + 'created_at': '2017-02-10T18:25:43.511Z', + 'version': 1, + 'created_by': 'someone@test.com', + 'body': 'some body' + } + ] + }, ['templates updated_at is a required property']), + ({ + "templates": [ + { + 'id': str(uuid.uuid4()), + 'type': SMS_TYPE, + 'created_at': '2017-02-10T18:25:43.511Z', + 'updated_at': None, + 'created_by': 'someone@test.com', + 'body': 'some body' + } + ] + }, ['templates version is a required property']), + ({ + "templates": [ + { + 'id': str(uuid.uuid4()), + 'type': SMS_TYPE, + 'created_at': '2017-02-10T18:25:43.511Z', + 'updated_at': None, + 'version': 1, + 'body': 'some body' + } + ] + }, ['templates created_by is a required property']), + ({ + "templates": [ + { + 'id': str(uuid.uuid4()), + 'type': SMS_TYPE, + 'created_at': '2017-02-10T18:25:43.511Z', + 'updated_at': None, + 'version': 1, + 'created_by': 'someone@test.com' + } + ] + }, ['templates body is a required property']), + ({ + "templates": [ + { + 'type': SMS_TYPE, + 'created_at': '2017-02-10T18:25:43.511Z', + 'updated_at': None, + 'created_by': 'someone@test.com', + 'body': 'some body' + } + ] + }, ['templates id is a required property', 'templates version is a required property']), +] + + +@pytest.mark.parametrize("template_type", TEMPLATE_TYPES) +def test_get_all_template_request_schema_against_no_args_is_valid(template_type): + data = {} + assert validate(data, get_all_template_request) == data + + +@pytest.mark.parametrize("template_type", TEMPLATE_TYPES) +def test_get_all_template_request_schema_against_valid_args_is_valid(template_type): + data = {'type': template_type} + assert validate(data, get_all_template_request) == data + + +@pytest.mark.parametrize("template_type", TEMPLATE_TYPES) +def test_get_all_template_request_schema_against_invalid_args_is_invalid(template_type): + data = {'type': 'unknown'} + + with pytest.raises(ValidationError) as e: + validate(data, get_all_template_request) + errors = json.loads(str(e.value)) + + assert errors['status_code'] == 400 + assert len(errors['errors']) == 1 + assert errors['errors'][0]['message'] == 'type unknown is not one of [sms, email, letter]' + + +@pytest.mark.parametrize("response", valid_json_get_all_response) +def test_valid_get_all_templates_response_schema_is_valid(response): + assert validate(response, get_all_template_response) == response + + +@pytest.mark.parametrize("response,error_messages", invalid_json_get_all_response) +def test_invalid_get_all_templates_response_schema_is_invalid(response, error_messages): + with pytest.raises(ValidationError) as e: + validate(response, get_all_template_response) + errors = json.loads(str(e.value)) + + assert errors['status_code'] == 400 + assert len(errors['errors']) == len(error_messages) + for error in errors['errors']: + assert error['message'] in error_messages