diff --git a/requirements_for_test.txt b/requirements_for_test.txt index 1ea6f7a8d..e7ded4dff 100644 --- a/requirements_for_test.txt +++ b/requirements_for_test.txt @@ -1,11 +1,12 @@ -r requirements.txt -pep8==1.5.7 -pytest==2.8.3 -pytest-mock==0.8.1 -pytest-cov==2.2.0 +pep8==1.7.0 +pytest==3.0.1 +pytest-mock==1.2 +pytest-cov==2.3.1 coveralls==1.1 -mock==1.0.1 -moto==0.4.19 -flex==5.7.0 -freezegun==0.3.6 -requests-mock==0.7.0 +moto==0.4.25 +flex==5.8.0 +freezegun==0.3.7 +requests-mock==1.0.0 +jsonschema==2.5.1 +strict-rfc3339==0.7 diff --git a/tests/app/celery/test_tasks.py b/tests/app/celery/test_tasks.py index 798d04b35..3b2159c38 100644 --- a/tests/app/celery/test_tasks.py +++ b/tests/app/celery/test_tasks.py @@ -3,7 +3,7 @@ from datetime import datetime import pytest from freezegun import freeze_time -from mock import ANY +from unittest.mock import ANY from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm.exc import NoResultFound diff --git a/tests/app/conftest.py b/tests/app/conftest.py index 76aba362a..40ad43b91 100644 --- a/tests/app/conftest.py +++ b/tests/app/conftest.py @@ -1,8 +1,8 @@ -import requests_mock -import pytest import uuid from datetime import (datetime, date) +import requests_mock +import pytest from flask import current_app from app import db @@ -28,7 +28,6 @@ from app.dao.jobs_dao import dao_create_job from app.dao.notifications_dao import dao_create_notification from app.dao.invited_user_dao import save_invited_user from app.clients.sms.firetext import FiretextClient -from app.clients.sms.mmg import MMGClient @pytest.yield_fixture @@ -276,9 +275,9 @@ def sample_job(notify_db, @pytest.fixture(scope='function') def sample_job_with_placeholdered_template( - notify_db, - notify_db_session, - service=None + notify_db, + notify_db_session, + service=None ): return sample_job( notify_db, @@ -374,6 +373,41 @@ def sample_notification(notify_db, return notification +@pytest.fixture(scope='function') +def sample_email_notification(notify_db, notify_db_session): + created_at = datetime.utcnow() + service = sample_service(notify_db, notify_db_session) + template = sample_email_template(notify_db, notify_db_session, service=service) + job = sample_job(notify_db, notify_db_session, service=service, template=template) + + notification_id = uuid.uuid4() + + to = 'foo@bar.com' + + data = { + 'id': notification_id, + 'to': to, + 'job_id': job.id, + 'job': job, + 'service_id': service.id, + 'service': service, + 'template': template, + 'template_version': template.version, + 'status': 'created', + 'reference': None, + 'created_at': created_at, + 'billable_units': 0, + 'personalisation': None, + 'notification_type': template.template_type, + 'api_key_id': None, + 'key_type': KEY_TYPE_NORMAL, + 'job_row_number': 1 + } + notification = Notification(**data) + dao_create_notification(notification) + return notification + + @pytest.fixture(scope='function') def mock_statsd_inc(mocker): return mocker.patch('app.statsd_client.incr') diff --git a/tests/app/notifications/test_rest.py b/tests/app/notifications/test_rest.py index efdd6152c..f4a8db289 100644 --- a/tests/app/notifications/test_rest.py +++ b/tests/app/notifications/test_rest.py @@ -624,44 +624,6 @@ def test_get_notifications_for_service_returns_merged_template_content(notify_ap } -def test_get_notification_public_api_format_is_not_changed(notify_api, sample_notification): - with notify_api.test_request_context(), notify_api.test_client() as client: - auth_header = create_authorization_header(service_id=sample_notification.service_id) - - response = client.get( - '/notifications/{}'.format(sample_notification.id), - headers=[auth_header]) - - assert response.status_code == 200 - notification = json.loads(response.get_data(as_text=True))['data']['notification'] - # you should never remove things from this list! - assert set(notification.keys()) == { - # straight from db - 'id', - 'to', - 'job_row_number', - 'template_version', - 'billable_units', - 'notification_type', - 'created_at', - 'sent_at', - 'sent_by', - 'updated_at', - 'status', - 'reference', - - # relationships - 'template', - 'service', - 'job', - 'api_key', - - # other - 'body', - 'content_char_count' - } - - def test_get_notification_selects_correct_template_for_personalisation(notify_api, notify_db, notify_db_session, diff --git a/tests/app/public_contracts/__init__.py b/tests/app/public_contracts/__init__.py new file mode 100644 index 000000000..c3276dd83 --- /dev/null +++ b/tests/app/public_contracts/__init__.py @@ -0,0 +1,16 @@ +import os + +from flask import json +import jsonschema + + +def validate(json_string, schema_filename): + schema_dir = os.path.join(os.path.dirname(__file__), 'schemas') + resolver = jsonschema.RefResolver('file://' + schema_dir + '/', None) + with open(os.path.join(schema_dir, schema_filename)) as schema: + jsonschema.validate( + json.loads(json_string), + json.load(schema), + format_checker=jsonschema.FormatChecker(), + resolver=resolver + ) diff --git a/tests/app/public_contracts/schemas/GET_notification_return_email.json b/tests/app/public_contracts/schemas/GET_notification_return_email.json new file mode 100644 index 000000000..e6fb6b19f --- /dev/null +++ b/tests/app/public_contracts/schemas/GET_notification_return_email.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "GET notification return schema - for email notifications", + "type" : "object", + "properties": { + "data": { + "type": "object", + "properties": { + "notification": {"$ref": "email_notification.json"} + }, + "additionalProperties": false, + "required": ["notification"] + } + }, + "additionalProperties": false, + "required": ["data"] +} diff --git a/tests/app/public_contracts/schemas/GET_notification_return_sms.json b/tests/app/public_contracts/schemas/GET_notification_return_sms.json new file mode 100644 index 000000000..2127ffd46 --- /dev/null +++ b/tests/app/public_contracts/schemas/GET_notification_return_sms.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "GET notification return schema - for sms notifications", + "type" : "object", + "properties": { + "data": { + "type": "object", + "properties": { + "notification": {"$ref": "sms_notification.json"} + }, + "additionalProperties": false, + "required": ["notification"] + } + }, + "additionalProperties": false, + "required": ["data"] +} diff --git a/tests/app/public_contracts/schemas/GET_notifications_return.json b/tests/app/public_contracts/schemas/GET_notifications_return.json new file mode 100644 index 000000000..7939dd8f1 --- /dev/null +++ b/tests/app/public_contracts/schemas/GET_notifications_return.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "GET notification return schema - for sms notifications", + "type" : "object", + "properties": { + "notifications": { + "type": "array", + "items": { + "oneOf": [ + {"$ref": "sms_notification.json"}, + {"$ref": "email_notification.json"} + ] + } + }, + "links": { + "type": "object", + "additionalProperties": false + }, + "page_size": {"type": "number"}, + "total": {"type": "number"} + }, + "additionalProperties": false, + "required": [ + "notifications", "links", "page_size", "total" + ] +} diff --git a/tests/app/public_contracts/schemas/POST_notification_return_email.json b/tests/app/public_contracts/schemas/POST_notification_return_email.json new file mode 100644 index 000000000..bd84f798a --- /dev/null +++ b/tests/app/public_contracts/schemas/POST_notification_return_email.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "POST notification return schema - for email notifications", + "type" : "object", + "properties": { + "data": { + "type": "object", + "properties": { + "notification": { + "type": "object", + "properties": { + "id": {"$ref": "definitions.json#/uuid"} + }, + "additionalProperties": false, + "required": ["id"] + }, + "body": {"type": "string"}, + "template_version": {"type": "number"}, + "subject": {"type": "string"} + }, + "additionalProperties": false, + "required": ["notification", "body", "template_version", "subject"] + } + }, + "additionalProperties": false, + "required": ["data"] +} diff --git a/tests/app/public_contracts/schemas/POST_notification_return_sms.json b/tests/app/public_contracts/schemas/POST_notification_return_sms.json new file mode 100644 index 000000000..3119c6d73 --- /dev/null +++ b/tests/app/public_contracts/schemas/POST_notification_return_sms.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "POST notification return schema - for sms notifications", + "type" : "object", + "properties": { + "data": { + "type": "object", + "properties": { + "notification": { + "type": "object", + "properties": { + "id": {"$ref": "definitions.json#/uuid"} + }, + "additionalProperties": false, + "required": ["id"] + }, + "body": {"type": "string"}, + "template_version": {"type": "number"} + }, + "additionalProperties": false, + "required": ["notification", "body", "template_version"] + } + }, + "additionalProperties": false, + "required": ["data"] +} diff --git a/tests/app/public_contracts/schemas/definitions.json b/tests/app/public_contracts/schemas/definitions.json new file mode 100644 index 000000000..5b12591dc --- /dev/null +++ b/tests/app/public_contracts/schemas/definitions.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Common definitions - usage example: {'$ref': 'definitions.json#/uuid'} (swap quotes for double quotes)", + "uuid": { + "type": "string", + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + "datetime": { + "type": "string", + "format": "date-time" + } +} diff --git a/tests/app/public_contracts/schemas/email_notification.json b/tests/app/public_contracts/schemas/email_notification.json new file mode 100644 index 000000000..013fb49ff --- /dev/null +++ b/tests/app/public_contracts/schemas/email_notification.json @@ -0,0 +1,106 @@ +{ + "description": "Single email notification schema - as returned by GET /notification and GET /notification/{}", + "type": "object", + "properties": { + "id": {"$ref": "definitions.json#/uuid"}, + "to": {"type": "string", "format": "email"}, + "job_row_number": {"oneOf":[ + {"type": "number"}, + {"type": "null"} + ]}, + "template_version": {"type": "number"}, + "billable_units": {"type": "number"}, + "notification_type": { + "type": "string", + "enum": ["email"] + }, + "created_at": {"$ref": "definitions.json#/datetime"}, + "sent_at": {"oneOf":[ + {"$ref": "definitions.json#/datetime"}, + {"type": "null"} + ]}, + "sent_by": {"oneOf":[ + {"type": "string"}, + {"type": "null"} + ]}, + "updated_at": {"oneOf":[ + {"$ref": "definitions.json#/datetime"}, + {"type": "null"} + ]}, + "status": { + "type": "string", + "enum": [ + "created", + "sending", + "delivered", + "pending", + "failed", + "technical-failure", + "temporary-failure", + "permanent-failure" + ] + }, + "reference": {"oneOf":[ + {"type": "string"}, + {"type": "null"} + ]}, + "template": { + "type": "object", + "properties": { + "id": {"$ref": "definitions.json#/uuid"}, + "name": {"type": "string"}, + "template_type": { + "type": "string", + "enum": ["email"] + }, + "version": {"type": "number"} + }, + "additionalProperties": false, + "required": ["id", "name", "template_type", "version"] + }, + "service": {"$ref": "definitions.json#/uuid"}, + "job": { + "oneOf": [ + { + "type": "object", + "properties": { + "id": {"$ref": "definitions.json#/uuid"}, + "original_file_name": {"type": "string"} + }, + "additionalProperties": false, + "required": ["id", "original_file_name"] + }, + {"type": "null"} + ] + }, + "api_key": {"oneOf":[ + {"$ref": "definitions.json#/uuid"}, + {"type": "null"} + ]}, + "body": {"type": "string"}, + "content_char_count": {"type": "null"}, + "subject": {"type": "string"} + }, + "additionalProperties": false, + "required": [ + "id", + "to", + "job_row_number", + "template_version", + "billable_units", + "notification_type", + "created_at", + "sent_at", + "sent_by", + "updated_at", + "status", + "reference", + "template", + "service", + "job", + "api_key", + "body", + "content_char_count", + "subject" + ] +} diff --git a/tests/app/public_contracts/schemas/sms_notification.json b/tests/app/public_contracts/schemas/sms_notification.json new file mode 100644 index 000000000..d81def6ea --- /dev/null +++ b/tests/app/public_contracts/schemas/sms_notification.json @@ -0,0 +1,104 @@ +{ + "description": "Single sms notification schema - as returned by GET /notification and GET /notification/{}", + "type": "object", + "properties": { + "id": {"$ref": "definitions.json#/uuid"}, + "to": {"type": "string"}, + "job_row_number": {"oneOf":[ + {"type": "number"}, + {"type": "null"} + ]}, + "template_version": {"type": "number"}, + "billable_units": {"type": "number"}, + "notification_type": { + "type": "string", + "enum": ["sms"] + }, + "created_at": {"$ref": "definitions.json#/datetime"}, + "sent_at": {"oneOf":[ + {"$ref": "definitions.json#/datetime"}, + {"type": "null"} + ]}, + "sent_by": {"oneOf":[ + {"type": "string"}, + {"type": "null"} + ]}, + "updated_at": {"oneOf":[ + {"$ref": "definitions.json#/datetime"}, + {"type": "null"} + ]}, + "status": { + "type": "string", + "enum": [ + "created", + "sending", + "delivered", + "pending", + "failed", + "technical-failure", + "temporary-failure", + "permanent-failure" + ] + }, + "reference": {"oneOf":[ + {"type": "string"}, + {"type": "null"} + ]}, + "template": { + "type": "object", + "properties": { + "id": {"$ref": "definitions.json#/uuid"}, + "name": {"type": "string"}, + "template_type": { + "type": "string", + "enum": ["sms"] + }, + "version": {"type": "number"} + }, + "additionalProperties": false, + "required": ["id", "name", "template_type", "version"] + }, + "service": {"$ref": "definitions.json#/uuid"}, + "job": { + "oneOf": [ + { + "type": "object", + "properties": { + "id": {"$ref": "definitions.json#/uuid"}, + "original_file_name": {"type": "string"} + }, + "additionalProperties": false, + "required": ["id", "original_file_name"] + }, + {"type": "null"} + ] + }, + "api_key": {"oneOf":[ + {"$ref": "definitions.json#/uuid"}, + {"type": "null"} + ]}, + "body": {"type": "string"}, + "content_char_count": {"type": "number"} + }, + "additionalProperties": false, + "required": [ + "id", + "to", + "job_row_number", + "template_version", + "billable_units", + "notification_type", + "created_at", + "sent_at", + "sent_by", + "updated_at", + "status", + "reference", + "template", + "service", + "job", + "api_key", + "body", + "content_char_count" + ] +} diff --git a/tests/app/public_contracts/test_GET_notification.py b/tests/app/public_contracts/test_GET_notification.py new file mode 100644 index 000000000..fdc13e078 --- /dev/null +++ b/tests/app/public_contracts/test_GET_notification.py @@ -0,0 +1,59 @@ +from . import validate +from app.models import ApiKey, KEY_TYPE_NORMAL +from app.dao.notifications_dao import dao_update_notification +from app.dao.api_key_dao import save_model_api_key +from tests import create_authorization_header + + +def test_get_api_sms_contract(client, sample_notification): + api_key = ApiKey(service=sample_notification.service, + name='api_key', + created_by=sample_notification.service.created_by, + key_type=KEY_TYPE_NORMAL) + save_model_api_key(api_key) + sample_notification.job = None + sample_notification.api_key = api_key + sample_notification.key_type = KEY_TYPE_NORMAL + dao_update_notification(sample_notification) + auth_header = create_authorization_header(service_id=sample_notification.service_id) + response = client.get('/notifications/{}'.format(sample_notification.id), headers=[auth_header]) + + validate(response.get_data(as_text=True), 'GET_notification_return_sms.json') + + +def test_get_api_email_contract(client, sample_email_notification): + api_key = ApiKey(service=sample_email_notification.service, + name='api_key', + created_by=sample_email_notification.service.created_by, + key_type=KEY_TYPE_NORMAL) + save_model_api_key(api_key) + sample_email_notification.job = None + sample_email_notification.api_key = api_key + sample_email_notification.key_type = KEY_TYPE_NORMAL + dao_update_notification(sample_email_notification) + + auth_header = create_authorization_header(service_id=sample_email_notification.service_id) + response = client.get('/notifications/{}'.format(sample_email_notification.id), headers=[auth_header]) + + validate(response.get_data(as_text=True), 'GET_notification_return_email.json') + + +def test_get_job_sms_contract(client, sample_notification): + auth_header = create_authorization_header(service_id=sample_notification.service_id) + response = client.get('/notifications/{}'.format(sample_notification.id), headers=[auth_header]) + + validate(response.get_data(as_text=True), 'GET_notification_return_sms.json') + + +def test_get_job_email_contract(client, sample_email_notification): + auth_header = create_authorization_header(service_id=sample_email_notification.service_id) + response = client.get('/notifications/{}'.format(sample_email_notification.id), headers=[auth_header]) + + validate(response.get_data(as_text=True), 'GET_notification_return_email.json') + + +def test_get_notifications_contract(client, sample_notification, sample_email_notification): + auth_header = create_authorization_header(service_id=sample_notification.service_id) + response = client.get('/notifications', headers=[auth_header]) + + validate(response.get_data(as_text=True), 'GET_notifications_return.json') diff --git a/tests/app/public_contracts/test_POST_notification.py b/tests/app/public_contracts/test_POST_notification.py new file mode 100644 index 000000000..ffb91210c --- /dev/null +++ b/tests/app/public_contracts/test_POST_notification.py @@ -0,0 +1,44 @@ +from flask import json + +from . import validate +from tests import create_authorization_header + + +def test_post_sms_contract(client, mocker, sample_template): + mocker.patch('app.celery.tasks.send_sms.apply_async') + mocker.patch('app.encryption.encrypt', return_value="something_encrypted") + + data = { + 'to': '07700 900 855', + 'template': str(sample_template.id) + } + + auth_header = create_authorization_header(service_id=sample_template.service_id) + + response = client.post( + path='/notifications/sms', + data=json.dumps(data), + headers=[('Content-Type', 'application/json'), auth_header] + ) + + validate(response.get_data(as_text=True), 'POST_notification_return_sms.json') + + +def test_post_email_contract(client, mocker, sample_email_template): + mocker.patch('app.celery.tasks.send_sms.apply_async') + mocker.patch('app.encryption.encrypt', return_value="something_encrypted") + + data = { + 'to': 'foo@bar.com', + 'template': str(sample_email_template.id) + } + + auth_header = create_authorization_header(service_id=sample_email_template.service_id) + + response = client.post( + path='/notifications/email', + data=json.dumps(data), + headers=[('Content-Type', 'application/json'), auth_header] + ) + + validate(response.get_data(as_text=True), 'POST_notification_return_email.json') diff --git a/tests/app/test_statsd_decorators.py b/tests/app/test_statsd_decorators.py index 3470f9038..dc904f2e1 100644 --- a/tests/app/test_statsd_decorators.py +++ b/tests/app/test_statsd_decorators.py @@ -1,4 +1,4 @@ -from mock import ANY +from unittest.mock import ANY from app.statsd_decorators import statsd import app diff --git a/tests/conftest.py b/tests/conftest.py index ea9fa15ad..3da2dead3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ import os import boto3 -import mock +from unittest import mock import pytest from alembic.command import upgrade from alembic.config import Config @@ -24,6 +24,12 @@ def notify_api(request): return app +@pytest.fixture(scope='function') +def client(notify_api): + with notify_api.test_request_context(), notify_api.test_client() as client: + yield client + + @pytest.fixture(scope='session') def notify_db(notify_api, request): Migrate(notify_api, db)