diff --git a/README.md b/README.md index a6917c43c..5c6143b4d 100644 --- a/README.md +++ b/README.md @@ -20,31 +20,19 @@ Create a local environment.sh file containing the following: ``` echo " -export NOTIFY_ENVIRONMENT='development' -export ADMIN_BASE_URL='http://localhost:6012' -export ADMIN_CLIENT_USER_NAME='dev-notify-admin' -export ADMIN_CLIENT_SECRET='dev-notify-secret-key' -export API_HOST_NAME='http://localhost:6011' - -export AWS_REGION='eu-west-1' -export AWS_ACCESS_KEY_ID=[MY ACCESS KEY] -export AWS_SECRET_ACCESS_KEY=[MY SECRET] - -export DANGEROUS_SALT='dev-notify-salt' -export FIRETEXT_API_KEY=[contact team member for api key] -export FROM_NUMBER='40605' -export INVITATION_EMAIL_FROM='invites' -export INVITATION_EXPIRATION_DAYS=2 -export MMG_API_KEY=mmg=secret-key -export MMG_URL="https://api.mmg.co.uk/json/api.php" -export NOTIFICATION_QUEUE_PREFIX='[unique-to-environment]' # -export NOTIFY_EMAIL_DOMAIN='notify.tools' -export SECRET_KEY='dev-notify-secret-key' export SQLALCHEMY_DATABASE_URI='postgresql://localhost/notification_api' -export STATSD_ENABLED=True -export STATSD_HOST="localhost" -export STATSD_PORT=1000 -export STATSD_PREFIX="stats-prefix" +export SECRET_KEY='secret-key' +export DANGEROUS_SALT='dangerous-salt' +export NOTIFY_ENVIRONMENT="development" +export ADMIN_CLIENT_SECRET='notify-secret-key' +export ADMIN_BASE_URL='http://localhost:6012' +export FROM_NUMBER='development' +export MMG_URL="https://api.mmg.co.uk/json/api.php" +export MMG_API_KEY='MMG_API_KEY' +export LOADTESTING_API_KEY="FIRETEXT_SIMULATION_KEY" +export FIRETEXT_API_KEY="FIRETEXT_ACTUAL_KEY" +export STATSD_PREFIX="FAKE_PREFIX" +export NOTIFICATION_QUEUE_PREFIX="PREFIX-TO-IDENTIFY-SQS-QUEUE" "> environment.sh ``` @@ -52,7 +40,11 @@ NOTE: The SECRET_KEY and DANGEROUS_SALT should match those in the [notifications NOTE: Also note the unique prefix for the queue names. This prevents clashing with others queues in shared amazon environment and using a prefix enables filtering by queue name in the SQS interface. +Install Postgresql +```shell + brew install postgres +``` ## To run the application @@ -73,7 +65,6 @@ scripts/run_celery_beat.sh ``` - ## To test the application First, ensure that `scripts/boostrap.sh` has been run, as it creates the test database. diff --git a/app/__init__.py b/app/__init__.py index 464157f60..cf47632f9 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -41,7 +41,6 @@ def create_app(app_name=None): from config import configs application.config.from_object(configs[os.environ['NOTIFY_ENVIRONMENT']]) - if app_name: application.config['NOTIFY_APP_NAME'] = app_name diff --git a/app/celery/tasks.py b/app/celery/tasks.py index f60ae45c9..eee951dfd 100644 --- a/app/celery/tasks.py +++ b/app/celery/tasks.py @@ -125,8 +125,11 @@ def send_sms(self, return try: - _save_notification(created_at, notification, notification_id, service_id, SMS_TYPE, api_key_id, key_type) - + dao_create_notification( + Notification.from_api_request( + created_at, notification, notification_id, service.id, SMS_TYPE, api_key_id, key_type + ) + ) send_sms_to_provider.apply_async((service_id, notification_id), queue='send-sms') current_app.logger.info( @@ -160,7 +163,11 @@ def send_email(self, service_id, return try: - _save_notification(created_at, notification, notification_id, service_id, EMAIL_TYPE, api_key_id, key_type) + dao_create_notification( + Notification.from_api_request( + created_at, notification, notification_id, service.id, EMAIL_TYPE, api_key_id, key_type + ) + ) send_email_to_provider.apply_async((service_id, notification_id), queue='send-email') @@ -176,25 +183,6 @@ def send_email(self, service_id, ) -def _save_notification(created_at, notification, notification_id, service_id, notification_type, api_key_id, key_type): - notification_db_object = Notification( - id=notification_id, - template_id=notification['template'], - template_version=notification['template_version'], - to=notification['to'], - service_id=service_id, - job_id=notification.get('job', None), - job_row_number=notification.get('row_number', None), - status='created', - created_at=datetime.strptime(created_at, DATETIME_FORMAT), - personalisation=notification.get('personalisation'), - notification_type=notification_type, - api_key_id=api_key_id, - key_type=key_type - ) - dao_create_notification(notification_db_object) - - def service_allowed_to_send_to(recipient, service, key_type): if not service.restricted or key_type == KEY_TYPE_TEST: return True diff --git a/app/dao/notifications_dao.py b/app/dao/notifications_dao.py index ef1a9f60e..aaab8f5e2 100644 --- a/app/dao/notifications_dao.py +++ b/app/dao/notifications_dao.py @@ -106,7 +106,7 @@ def dao_get_template_usage(service_id, limit_days=None): @statsd(namespace="dao") def dao_get_last_template_usage(template_id): - return NotificationHistory.query.filter(NotificationHistory.template_id == template_id)\ + return NotificationHistory.query.filter(NotificationHistory.template_id == template_id) \ .join(Template) \ .order_by(desc(NotificationHistory.created_at)) \ .first() @@ -275,3 +275,14 @@ def delete_notifications_created_more_than_a_week_ago(status): ).delete(synchronize_session='fetch') db.session.commit() return deleted + + +@statsd(namespace="dao") +@transactional +def dao_delete_notifications_and_history_by_id(notification_id): + db.session.query(Notification).filter( + Notification.id == notification_id + ).delete(synchronize_session='fetch') + db.session.query(NotificationHistory).filter( + NotificationHistory.id == notification_id + ).delete(synchronize_session='fetch') diff --git a/app/models.py b/app/models.py index c4facc46c..91294c15d 100644 --- a/app/models.py +++ b/app/models.py @@ -1,6 +1,5 @@ import uuid import datetime - from sqlalchemy.dialects.postgresql import ( UUID, JSON @@ -15,8 +14,8 @@ from app.encryption import ( from app.authentication.utils import get_secret from app import ( db, - encryption -) + encryption, + DATETIME_FORMAT) from app.history_meta import Versioned @@ -74,7 +73,6 @@ user_to_service = db.Table( UniqueConstraint('user_id', 'service_id', name='uix_user_to_service') ) - BRANDING_GOVUK = 'govuk' BRANDING_ORG = 'org' BRANDING_BOTH = 'both' @@ -395,6 +393,7 @@ class VerifyCode(db.Model): def check_code(self, cde): return check_hash(cde, self._code) + NOTIFICATION_CREATED = 'created' NOTIFICATION_SENDING = 'sending' NOTIFICATION_DELIVERED = 'delivered' @@ -427,7 +426,6 @@ NOTIFICATION_STATUS_TYPES_ENUM = db.Enum(*NOTIFICATION_STATUS_TYPES, name='notif class Notification(db.Model): - __tablename__ = 'notifications' id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) @@ -482,6 +480,32 @@ class Notification(db.Model): if personalisation: self._personalisation = encryption.encrypt(personalisation) + @classmethod + def from_api_request( + cls, + created_at, + notification, + notification_id, + service_id, + notification_type, + api_key_id, + key_type): + return cls( + id=notification_id, + template_id=notification['template'], + template_version=notification['template_version'], + to=notification['to'], + service_id=service_id, + job_id=notification.get('job', None), + job_row_number=notification.get('row_number', None), + status='created', + created_at=datetime.datetime.strptime(created_at, DATETIME_FORMAT), + personalisation=notification.get('personalisation'), + notification_type=notification_type, + api_key_id=api_key_id, + key_type=key_type + ) + class NotificationHistory(db.Model): __tablename__ = 'notification_history' @@ -522,7 +546,6 @@ INVITED_USER_STATUS_TYPES = ['pending', 'accepted', 'cancelled'] class InvitedUser(db.Model): - __tablename__ = 'invited_users' id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) @@ -598,7 +621,6 @@ class Permission(db.Model): class TemplateStatistics(db.Model): - id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), index=True, unique=False, nullable=False) service = db.relationship('Service', backref=db.backref('template_statistics', lazy='dynamic')) @@ -615,7 +637,6 @@ class TemplateStatistics(db.Model): class Event(db.Model): - __tablename__ = 'events' id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) diff --git a/app/notifications/rest.py b/app/notifications/rest.py index 86b423e5c..35e55f2ed 100644 --- a/app/notifications/rest.py +++ b/app/notifications/rest.py @@ -13,9 +13,10 @@ from notifications_utils.recipients import allowed_to_send_to, first_column_head from notifications_utils.template import Template from notifications_utils.renderers import PassThrough from app.clients.email.aws_ses import get_aws_responses -from app import api_user, encryption, create_uuid, DATETIME_FORMAT, statsd_client +from app import api_user, create_uuid, DATETIME_FORMAT, statsd_client +from app.dao.notifications_dao import dao_create_notification, dao_delete_notifications_and_history_by_id from app.dao.services_dao import dao_fetch_todays_stats_for_service -from app.models import KEY_TYPE_TEAM, KEY_TYPE_TEST +from app.models import KEY_TYPE_TEAM, KEY_TYPE_TEST, Notification, KEY_TYPE_NORMAL, EMAIL_TYPE from app.dao import ( templates_dao, services_dao, @@ -35,7 +36,6 @@ from app.schemas import ( day_schema, unarchived_template_schema ) -from app.celery.tasks import send_sms, send_email from app.utils import pagination_links notifications = Blueprint('notifications', __name__) @@ -46,6 +46,7 @@ from app.errors import ( ) register_errors(notifications) +from app.celery.provider_tasks import send_sms_to_provider, send_email_to_provider @notifications.route('/notifications/email/ses', methods=['POST']) @@ -192,19 +193,16 @@ def send_notification(notification_type): service_id = str(api_user.service_id) service = services_dao.dao_fetch_service_by_id(service_id) - service_stats = sum(row.count for row in dao_fetch_todays_stats_for_service(service.id)) - - if all(( - api_user.key_type != KEY_TYPE_TEST, - service_stats >= service.message_limit - )): - error = 'Exceeded send limits ({}) for today'.format(service.message_limit) - raise InvalidRequest(error, status_code=429) - notification, errors = ( sms_template_notification_schema if notification_type == SMS_TYPE else email_notification_schema ).load(request.get_json()) + if all((api_user.key_type != KEY_TYPE_TEST, service.restricted)): + service_stats = sum(row.count for row in dao_fetch_todays_stats_for_service(service.id)) + if service_stats >= service.message_limit: + error = 'Exceeded send limits ({}) for today'.format(service.message_limit) + raise InvalidRequest(error, status_code=429) + if errors: raise InvalidRequest(errors, status_code=400) @@ -233,8 +231,8 @@ def send_notification(notification_type): raise InvalidRequest(errors, status_code=400) if ( - template_object.template_type == SMS_TYPE and - template_object.replaced_content_count > current_app.config.get('SMS_CHAR_COUNT_LIMIT') + template_object.template_type == SMS_TYPE and + template_object.replaced_content_count > current_app.config.get('SMS_CHAR_COUNT_LIMIT') ): char_count = current_app.config.get('SMS_CHAR_COUNT_LIMIT') message = 'Content has a character count greater than the limit of {}'.format(char_count) @@ -242,14 +240,14 @@ def send_notification(notification_type): raise InvalidRequest(errors, status_code=400) if all(( - api_user.key_type != KEY_TYPE_TEST, - service.restricted or api_user.key_type == KEY_TYPE_TEAM, - not allowed_to_send_to( - notification['to'], - itertools.chain.from_iterable( - [user.mobile_number, user.email_address] for user in service.users + api_user.key_type != KEY_TYPE_TEST, + service.restricted or api_user.key_type == KEY_TYPE_TEAM, + not allowed_to_send_to( + notification['to'], + itertools.chain.from_iterable( + [user.mobile_number, user.email_address] for user in service.users + ) ) - ) )): if (api_user.key_type == KEY_TYPE_TEAM): message = 'Can’t send to this recipient using a team-only API key' @@ -265,34 +263,15 @@ def send_notification(notification_type): notification_id = create_uuid() notification.update({"template_version": template.version}) - if notification_type == SMS_TYPE: - send_sms.apply_async( - ( - service_id, - notification_id, - encryption.encrypt(notification), - datetime.utcnow().strftime(DATETIME_FORMAT) - ), - kwargs={ - 'api_key_id': str(api_user.id), - 'key_type': api_user.key_type - }, - queue='db-sms' - ) - else: - send_email.apply_async( - ( - service_id, - notification_id, - encryption.encrypt(notification), - datetime.utcnow().strftime(DATETIME_FORMAT) - ), - kwargs={ - 'api_key_id': str(api_user.id), - 'key_type': api_user.key_type - }, - queue='db-email' - ) + persist_notification( + service, + notification_id, + notification, + datetime.utcnow().strftime(DATETIME_FORMAT), + notification_type, + str(api_user.id), + api_user.key_type + ) return jsonify( data=get_notification_return_data( @@ -323,3 +302,33 @@ def get_notification_statistics_for_day(): ) data, errors = notifications_statistics_schema.dump(statistics, many=True) return jsonify(data=data), 200 + + +def persist_notification( + service, + notification_id, + notification, + created_at, + notification_type, + api_key_id=None, + key_type=KEY_TYPE_NORMAL, +): + dao_create_notification( + Notification.from_api_request( + created_at, notification, notification_id, service.id, notification_type, api_key_id, key_type + ) + ) + + try: + if notification_type == SMS_TYPE: + send_sms_to_provider.apply_async((str(service.id), str(notification_id)), queue='send-sms') + if notification_type == EMAIL_TYPE: + send_email_to_provider.apply_async((str(service.id), str(notification_id)), queue='send-email') + except Exception as e: + current_app.logger.exception("Failed to send to SQS exception", e) + dao_delete_notifications_and_history_by_id(notification_id) + raise InvalidRequest(message="Internal server error", status_code=500) + + current_app.logger.info( + "{} {} created at {}".format(notification_type, notification_id, created_at) + ) diff --git a/config.py b/config.py index 1df96acf4..6b2be3721 100644 --- a/config.py +++ b/config.py @@ -5,27 +5,57 @@ import os class Config(object): - DEBUG = False + ######################################## + # Secrets that are held in credstash ### + ######################################## + + # URL of admin app ADMIN_BASE_URL = os.environ['ADMIN_BASE_URL'] - ADMIN_CLIENT_USER_NAME = os.environ['ADMIN_CLIENT_USER_NAME'] + + # admin app api key ADMIN_CLIENT_SECRET = os.environ['ADMIN_CLIENT_SECRET'] - AWS_REGION = os.environ['AWS_REGION'] + + # encyption secret/salt + SECRET_KEY = os.environ['SECRET_KEY'] DANGEROUS_SALT = os.environ['DANGEROUS_SALT'] - INVITATION_EXPIRATION_DAYS = int(os.environ['INVITATION_EXPIRATION_DAYS']) - INVITATION_EMAIL_FROM = os.environ['INVITATION_EMAIL_FROM'] + + # DB conection string + SQLALCHEMY_DATABASE_URI = os.environ['SQLALCHEMY_DATABASE_URI'] + + # MMG API Url + MMG_URL = os.environ['MMG_URL'] + + # MMG API Key + MMG_API_KEY = os.environ['MMG_API_KEY'] + + # Firetext API Key + FIRETEXT_API_KEY = os.getenv("FIRETEXT_API_KEY") + + # Firetext simluation key + LOADTESTING_API_KEY = os.getenv("LOADTESTING_API_KEY") + + # Hosted graphite statsd prefix + STATSD_PREFIX = os.getenv('STATSD_PREFIX') + + # Prefix to identify queues in SQS + NOTIFICATION_QUEUE_PREFIX = os.getenv('NOTIFICATION_QUEUE_PREFIX') + + ########################### + # Default config values ### + ########################### + + DEBUG = False + NOTIFY_ENVIRONMENT = 'development' + ADMIN_CLIENT_USER_NAME = 'notify-admin' + AWS_REGION = 'eu-west-1' + INVITATION_EXPIRATION_DAYS = 2 NOTIFY_APP_NAME = 'api' NOTIFY_LOG_PATH = '/var/log/notify/application.log' - # Notification Queue names are a combination of a prefix plus a name - NOTIFICATION_QUEUE_PREFIX = os.environ['NOTIFICATION_QUEUE_PREFIX'] - SECRET_KEY = os.environ['SECRET_KEY'] SQLALCHEMY_COMMIT_ON_TEARDOWN = False - SQLALCHEMY_DATABASE_URI = os.environ['SQLALCHEMY_DATABASE_URI'] SQLALCHEMY_RECORD_QUERIES = True SQLALCHEMY_TRACK_MODIFICATIONS = True - NOTIFY_EMAIL_DOMAIN = os.environ['NOTIFY_EMAIL_DOMAIN'] PAGE_SIZE = 50 SMS_CHAR_COUNT_LIMIT = 495 - MMG_URL = os.environ['MMG_URL'] BRANDING_PATH = '/static/images/email-template/crests/' NOTIFY_SERVICE_ID = 'd6aa2c68-a2d9-4437-ab19-3ae8eb202553' @@ -37,10 +67,10 @@ class Config(object): BROKER_URL = 'sqs://' BROKER_TRANSPORT_OPTIONS = { - 'region': 'eu-west-1', + 'region': AWS_REGION, 'polling_interval': 1, # 1 second 'visibility_timeout': 14410, # 4 hours 10 seconds. 10 seconds longer than max retry - 'queue_name_prefix': os.environ['NOTIFICATION_QUEUE_PREFIX'] + '-' + 'queue_name_prefix': NOTIFICATION_QUEUE_PREFIX } CELERY_ENABLE_UTC = True, CELERY_TIMEZONE = 'Europe/London' @@ -101,26 +131,27 @@ class Config(object): Queue('retry', Exchange('default'), routing_key='retry'), Queue('email-already-registered', Exchange('default'), routing_key='email-already-registered') ] - API_HOST_NAME = os.environ['API_HOST_NAME'] - MMG_API_KEY = os.environ['MMG_API_KEY'] - FIRETEXT_API_KEY = os.getenv("FIRETEXT_API_KEY") - LOADTESTING_NUMBER = os.getenv('LOADTESTING_NUMBER') - LOADTESTING_API_KEY = os.getenv("LOADTESTING_API_KEY") - CSV_UPLOAD_BUCKET_NAME = os.getenv("CSV_UPLOAD_BUCKET_NAME") + API_HOST_NAME = "http://localhost:6011" + NOTIFICATIONS_ALERT = 5 # five mins - FROM_NUMBER = os.getenv('FROM_NUMBER') + FROM_NUMBER = 'development' STATSD_ENABLED = False STATSD_HOST = "statsd.hostedgraphite.com" STATSD_PORT = 8125 - STATSD_PREFIX = None SENDING_NOTIFICATIONS_TIMEOUT_PERIOD = 259200 +###################### +# Config overrides ### +###################### + class Development(Config): - NOTIFY_ENVIRONMENT = 'development' + NOTIFY_EMAIL_DOMAIN = 'notify.tools' CSV_UPLOAD_BUCKET_NAME = 'development-notifications-csv-upload' + NOTIFY_ENVIRONMENT = 'development' + NOTIFICATION_QUEUE_PREFIX = 'development' DEBUG = True SQLALCHEMY_ECHO = False CELERY_QUEUES = Config.CELERY_QUEUES + [ @@ -132,10 +163,14 @@ class Development(Config): class Test(Config): + NOTIFY_EMAIL_DOMAIN = 'test.notify.com' + FROM_NUMBER = 'testing' NOTIFY_ENVIRONMENT = 'test' DEBUG = True CSV_UPLOAD_BUCKET_NAME = 'test-notifications-csv-upload' - STATSD_PREFIX = "test" + STATSD_ENABLED = True + STATSD_HOST = "localhost" + STATSD_PORT = 1000 CELERY_QUEUES = Config.CELERY_QUEUES + [ Queue('db-sms', Exchange('default'), routing_key='db-sms'), Queue('send-sms', Exchange('default'), routing_key='send-sms'), @@ -145,23 +180,29 @@ class Test(Config): class Preview(Config): + NOTIFY_EMAIL_DOMAIN = 'notify.works' NOTIFY_ENVIRONMENT = 'preview' CSV_UPLOAD_BUCKET_NAME = 'preview-notifications-csv-upload' - STATSD_PREFIX = "preview" + API_HOST_NAME = 'http://admin-api.internal' + FROM_NUMBER = 'preview' class Staging(Config): + NOTIFY_EMAIL_DOMAIN = 'staging-notify.works' NOTIFY_ENVIRONMENT = 'staging' CSV_UPLOAD_BUCKET_NAME = 'staging-notify-csv-upload' - STATSD_PREFIX = os.getenv('STATSD_PREFIX') STATSD_ENABLED = True + API_HOST_NAME = 'http://admin-api.internal' + FROM_NUMBER = 'stage' class Live(Config): + NOTIFY_EMAIL_DOMAIN = 'notifications.service.gov.uk' NOTIFY_ENVIRONMENT = 'live' CSV_UPLOAD_BUCKET_NAME = 'live-notifications-csv-upload' - STATSD_PREFIX = os.getenv('STATSD_PREFIX') STATSD_ENABLED = True + API_HOST_NAME = 'http://admin-api.internal' + FROM_NUMBER = '40604' configs = { diff --git a/environment_test.sh b/environment_test.sh index d0c536072..a74a81a86 100644 --- a/environment_test.sh +++ b/environment_test.sh @@ -1,24 +1,13 @@ -#!/bin/bash -export NOTIFY_ENVIRONMENT='test' -export ADMIN_BASE_URL='http://localhost:6012' -export ADMIN_CLIENT_USER_NAME='dev-notify-admin' -export ADMIN_CLIENT_SECRET='dev-notify-secret-key' -export AWS_REGION='eu-west-1' -export DANGEROUS_SALT='dangerous-salt' -export INVITATION_EMAIL_FROM='invites' -export INVITATION_EXPIRATION_DAYS=2 -export NOTIFICATION_QUEUE_PREFIX='test-env-not-used' -export SECRET_KEY='secret-key' export SQLALCHEMY_DATABASE_URI=${TEST_DATABASE:='postgresql://localhost/test_notification_api'} -export FIRETEXT_API_KEY="Firetext" -export NOTIFY_EMAIL_DOMAIN="test.notify.com" -export MMG_API_KEY='mmg-secret-key' -export LOADTESTING_API_KEY="loadtesting" -export LOADTESTING_NUMBER="loadtesting" -export STATSD_ENABLED=True -export STATSD_HOST="localhost" -export STATSD_PORT=1000 -export STATSD_PREFIX="stats-prefix" -export API_HOST_NAME="http://localhost:6011" +export SECRET_KEY='secret-key' +export DANGEROUS_SALT='dangerous-salt' +export NOTIFY_ENVIRONMENT='test' +export ADMIN_CLIENT_SECRET='dev-notify-secret-key' +export ADMIN_BASE_URL='http://localhost:6012' export FROM_NUMBER='from_number' export MMG_URL="https://api.mmg.co.uk/json/api.php" +export MMG_API_KEY='mmg-secret-key' +export LOADTESTING_API_KEY="loadtesting" +export FIRETEXT_API_KEY="Firetext" +export STATSD_PREFIX="stats-prefix" +export NOTIFICATION_QUEUE_PREFIX='testing' diff --git a/requirements.txt b/requirements.txt index 8e879e822..2369584bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ celery==3.1.23 monotonic==1.2 statsd==3.2.1 -git+https://github.com/alphagov/notifications-python-client.git@1.2.0#egg=notifications-python-client==1.2.0 +git+https://github.com/alphagov/notifications-python-client.git@1.3.0#egg=notifications-python-client==1.3.0 git+https://github.com/alphagov/notifications-utils.git@9.0.1#egg=notifications-utils==9.0.1 diff --git a/tests/app/clients/test_mmg.py b/tests/app/clients/test_mmg.py index bc4428193..649764d89 100644 --- a/tests/app/clients/test_mmg.py +++ b/tests/app/clients/test_mmg.py @@ -60,7 +60,7 @@ def test_send_sms_calls_mmg_correctly(notify_api, mocker): assert request_args['reqType'] == 'BULK' assert request_args['MSISDN'] == to assert request_args['msg'] == content - assert request_args['sender'] == 'from_number' + assert request_args['sender'] == 'testing' assert request_args['cid'] == reference assert request_args['multi'] is True diff --git a/tests/app/dao/test_notification_dao.py b/tests/app/dao/test_notification_dao.py index 44cd88079..3ef686713 100644 --- a/tests/app/dao/test_notification_dao.py +++ b/tests/app/dao/test_notification_dao.py @@ -33,7 +33,8 @@ from app.dao.notifications_dao import ( get_notifications_for_job, get_notifications_for_service, update_notification_status_by_id, - update_notification_status_by_reference + update_notification_status_by_reference, + dao_delete_notifications_and_history_by_id ) from notifications_utils.template import get_sms_fragment_count @@ -55,6 +56,7 @@ def test_should_have_decorated_notifications_dao_functions(): assert get_notifications_for_service.__wrapped__.__name__ == 'get_notifications_for_service' # noqa assert get_notification_by_id.__wrapped__.__name__ == 'get_notification_by_id' # noqa assert delete_notifications_created_more_than_a_week_ago.__wrapped__.__name__ == 'delete_notifications_created_more_than_a_week_ago' # noqa + assert dao_delete_notifications_and_history_by_id.__wrapped__.__name__ == 'dao_delete_notifications_and_history_by_id' # noqa def test_should_be_able_to_get_template_usage_history(notify_db, notify_db_session, sample_service): @@ -73,7 +75,6 @@ def test_should_be_able_to_get_all_template_usage_history_order_by_notification_ notify_db, notify_db_session, sample_service): - sms = sample_template(notify_db, notify_db_session) sample_notification(notify_db, notify_db_session, service=sample_service, template=sms) @@ -88,7 +89,6 @@ def test_should_be_able_to_get_all_template_usage_history_order_by_notification_ def test_should_be_able_to_get_no_template_usage_history_if_no_notifications_using_template( notify_db, notify_db_session): - sms = sample_template(notify_db, notify_db_session) results = dao_get_last_template_usage(sms.id) @@ -729,6 +729,67 @@ def test_updating_notification_updates_notification_history(sample_notification) assert hist.status == 'sending' +def test_should_delete_notification_and_notification_history_for_id(notify_db, notify_db_session, sample_template): + data = _notification_json(sample_template) + notification = Notification(**data) + + dao_create_notification(notification) + + assert Notification.query.count() == 1 + assert NotificationHistory.query.count() == 1 + + dao_delete_notifications_and_history_by_id(notification.id) + + assert Notification.query.count() == 0 + assert NotificationHistory.query.count() == 0 + + +def test_should_delete_only_notification_and_notification_history_with_id(notify_db, notify_db_session, + sample_template): + id_1 = uuid.uuid4() + id_2 = uuid.uuid4() + data_1 = _notification_json(sample_template, id=id_1) + data_2 = _notification_json(sample_template, id=id_2) + + notification_1 = Notification(**data_1) + notification_2 = Notification(**data_2) + + dao_create_notification(notification_1) + dao_create_notification(notification_2) + + assert Notification.query.count() == 2 + assert NotificationHistory.query.count() == 2 + + dao_delete_notifications_and_history_by_id(notification_1.id) + + assert Notification.query.count() == 1 + assert NotificationHistory.query.count() == 1 + assert Notification.query.first().id == notification_2.id + assert NotificationHistory.query.first().id == notification_2.id + + +def test_should_delete_no_notifications_or_notification_historys_if_no_matching_ids( + notify_db, + notify_db_session, + sample_template +): + id_1 = uuid.uuid4() + id_2 = uuid.uuid4() + data_1 = _notification_json(sample_template, id=id_1) + + notification_1 = Notification(**data_1) + + dao_create_notification(notification_1) + + assert Notification.query.count() == 1 + assert NotificationHistory.query.count() == 1 + + dao_delete_notifications_and_history_by_id(id_2) + + assert Notification.query.count() == 1 + assert NotificationHistory.query.count() == 1 + + def _notification_json(sample_template, job_id=None, id=None, status=None): data = { 'to': '+44709123456', diff --git a/tests/app/notifications/rest/test_send_notification.py b/tests/app/notifications/rest/test_send_notification.py index b0492e8bb..9dd29e1b5 100644 --- a/tests/app/notifications/rest/test_send_notification.py +++ b/tests/app/notifications/rest/test_send_notification.py @@ -1,3 +1,4 @@ +import uuid from datetime import datetime import random import string @@ -10,7 +11,8 @@ from notifications_python_client.authentication import create_jwt_token import app from app import encryption -from app.models import ApiKey, KEY_TYPE_TEAM, KEY_TYPE_TEST +from app.dao import notifications_dao +from app.models import ApiKey, KEY_TYPE_TEAM, KEY_TYPE_TEST, Notification, NotificationHistory from app.dao.templates_dao import dao_get_all_templates_for_service, dao_update_template from app.dao.services_dao import dao_update_service from app.dao.api_key_dao import save_model_api_key @@ -26,7 +28,7 @@ from tests.app.conftest import ( def test_create_sms_should_reject_if_missing_required_fields(notify_api, sample_api_key, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_sms.apply_async') + mocker.patch('app.celery.provider_tasks.send_sms_to_provider.apply_async') data = {} auth_header = create_authorization_header(service_id=sample_api_key.service_id) @@ -46,7 +48,7 @@ def test_create_sms_should_reject_if_missing_required_fields(notify_api, sample_ def test_should_reject_bad_phone_numbers(notify_api, sample_template, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_sms.apply_async') + mocker.patch('app.celery.provider_tasks.send_sms_to_provider.apply_async') data = { 'to': 'invalid', @@ -60,7 +62,7 @@ def test_should_reject_bad_phone_numbers(notify_api, sample_template, mocker): headers=[('Content-Type', 'application/json'), auth_header]) json_resp = json.loads(response.get_data(as_text=True)) - app.celery.tasks.send_sms.apply_async.assert_not_called() + app.celery.provider_tasks.send_sms_to_provider.apply_async.assert_not_called() assert json_resp['result'] == 'error' assert len(json_resp['message'].keys()) == 1 assert 'Invalid phone number: Must not contain letters or symbols' in json_resp['message']['to'] @@ -70,7 +72,7 @@ def test_should_reject_bad_phone_numbers(notify_api, sample_template, mocker): def test_send_notification_invalid_template_id(notify_api, sample_template, mocker, fake_uuid): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_sms.apply_async') + mocker.patch('app.celery.provider_tasks.send_sms_to_provider.apply_async') data = { 'to': '+447700900855', @@ -84,7 +86,7 @@ def test_send_notification_invalid_template_id(notify_api, sample_template, mock headers=[('Content-Type', 'application/json'), auth_header]) json_resp = json.loads(response.get_data(as_text=True)) - app.celery.tasks.send_sms.apply_async.assert_not_called() + app.celery.provider_tasks.send_sms_to_provider.apply_async.assert_not_called() assert response.status_code == 404 test_string = 'No result found' @@ -95,7 +97,7 @@ def test_send_notification_invalid_template_id(notify_api, sample_template, mock def test_send_notification_with_placeholders_replaced(notify_api, sample_email_template_with_placeholders, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_email.apply_async') + mocker.patch('app.celery.tasks.send_email_to_provider.apply_async') data = { 'to': 'ok@ok.com', @@ -115,16 +117,12 @@ def test_send_notification_with_placeholders_replaced(notify_api, sample_email_t notification_id = response_data['notification']['id'] data.update({"template_version": sample_email_template_with_placeholders.version}) - app.celery.tasks.send_email.apply_async.assert_called_once_with( + app.celery.provider_tasks.send_email_to_provider.apply_async.assert_called_once_with( (str(sample_email_template_with_placeholders.service.id), - notification_id, - ANY, - "2016-01-01T11:09:00.061258"), - kwargs=ANY, - queue="db-email" + str(notification_id)), + queue="send-email" ) assert response.status_code == 201 - assert encryption.decrypt(app.celery.tasks.send_email.apply_async.call_args[0][0][2]) == data assert response_data['body'] == 'Hello Jo\nThis is an email from GOV.UK' assert response_data['subject'] == 'Jo' @@ -152,7 +150,7 @@ def test_should_not_send_notification_for_archived_template(notify_api, sample_t def test_send_notification_with_missing_personalisation(notify_api, sample_template_with_placeholders, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_sms.apply_async') + mocker.patch('app.celery.provider_tasks.send_sms_to_provider.apply_async') data = { 'to': '+447700900855', @@ -169,7 +167,7 @@ def test_send_notification_with_missing_personalisation(notify_api, sample_templ headers=[('Content-Type', 'application/json'), auth_header]) json_resp = json.loads(response.get_data(as_text=True)) - app.celery.tasks.send_sms.apply_async.assert_not_called() + app.celery.provider_tasks.send_sms_to_provider.apply_async.assert_not_called() assert response.status_code == 400 assert 'Missing personalisation: name' in json_resp['message']['template'] @@ -180,7 +178,7 @@ def test_send_notification_with_too_much_personalisation_data( ): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_sms.apply_async') + mocker.patch('app.celery.provider_tasks.send_sms_to_provider.apply_async') data = { 'to': '+447700900855', @@ -197,7 +195,7 @@ def test_send_notification_with_too_much_personalisation_data( headers=[('Content-Type', 'application/json'), auth_header]) json_resp = json.loads(response.get_data(as_text=True)) - app.celery.tasks.send_sms.apply_async.assert_not_called() + app.celery.provider_tasks.send_sms_to_provider.apply_async.assert_not_called() assert response.status_code == 400 assert 'Personalisation not needed for template: foo' in json_resp['message']['template'] @@ -206,7 +204,7 @@ def test_send_notification_with_too_much_personalisation_data( def test_should_not_send_sms_if_restricted_and_not_a_service_user(notify_api, sample_template, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_sms.apply_async') + mocker.patch('app.celery.provider_tasks.send_sms_to_provider.apply_async') sample_template.service.restricted = True dao_update_service(sample_template.service) @@ -224,7 +222,7 @@ def test_should_not_send_sms_if_restricted_and_not_a_service_user(notify_api, sa headers=[('Content-Type', 'application/json'), auth_header]) json_resp = json.loads(response.get_data(as_text=True)) - app.celery.tasks.send_sms.apply_async.assert_not_called() + app.celery.provider_tasks.send_sms_to_provider.apply_async.assert_not_called() assert response.status_code == 400 assert [( @@ -236,7 +234,7 @@ def test_should_not_send_sms_if_restricted_and_not_a_service_user(notify_api, sa def test_should_send_sms_if_restricted_and_a_service_user(notify_api, sample_template, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_sms.apply_async') + mocker.patch('app.celery.provider_tasks.send_sms_to_provider.apply_async') sample_template.service.restricted = True dao_update_service(sample_template.service) @@ -252,14 +250,14 @@ def test_should_send_sms_if_restricted_and_a_service_user(notify_api, sample_tem data=json.dumps(data), headers=[('Content-Type', 'application/json'), auth_header]) - assert app.celery.tasks.send_sms.apply_async.called + assert app.celery.provider_tasks.send_sms_to_provider.apply_async.called assert response.status_code == 201 def test_should_send_email_if_restricted_and_a_service_user(notify_api, sample_email_template, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_email.apply_async') + mocker.patch('app.celery.provider_tasks.send_email_to_provider.apply_async') sample_email_template.service.restricted = True dao_update_service(sample_email_template.service) @@ -275,14 +273,14 @@ def test_should_send_email_if_restricted_and_a_service_user(notify_api, sample_e data=json.dumps(data), headers=[('Content-Type', 'application/json'), auth_header]) - assert app.celery.tasks.send_email.apply_async.called + assert app.celery.provider_tasks.send_email_to_provider.apply_async.called assert response.status_code == 201 def test_should_not_allow_template_from_another_service(notify_api, service_factory, sample_user, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_sms.apply_async') + mocker.patch('app.celery.provider_tasks.send_sms_to_provider.apply_async') service_1 = service_factory.get('service 1', user=sample_user, email_from='service.1') service_2 = service_factory.get('service 2', user=sample_user, email_from='service.2') @@ -301,7 +299,7 @@ def test_should_not_allow_template_from_another_service(notify_api, service_fact headers=[('Content-Type', 'application/json'), auth_header]) json_resp = json.loads(response.get_data(as_text=True)) - app.celery.tasks.send_sms.apply_async.assert_not_called() + app.celery.provider_tasks.send_sms_to_provider.apply_async.assert_not_called() assert response.status_code == 404 test_string = 'No result found' @@ -325,7 +323,7 @@ def test_should_not_allow_template_content_too_large( ): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_email.apply_async') + mocker.patch('app.celery.provider_tasks.send_email_to_provider.apply_async') template = create_sample_template( notify_db, notify_db_session, @@ -362,7 +360,7 @@ def test_should_not_allow_template_content_too_large( def test_should_allow_valid_sms_notification(notify_api, sample_template, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_sms.apply_async') + mocker.patch('app.celery.provider_tasks.send_sms_to_provider.apply_async') mocker.patch('app.encryption.encrypt', return_value="something_encrypted") data = { @@ -379,18 +377,9 @@ def test_should_allow_valid_sms_notification(notify_api, sample_template, mocker response_data = json.loads(response.data)['data'] notification_id = response_data['notification']['id'] - assert app.encryption.encrypt.call_args[0][0]['to'] == '+447700900855' - assert app.encryption.encrypt.call_args[0][0]['template'] == str(sample_template.id) - assert app.encryption.encrypt.call_args[0][0]['template_version'] == sample_template.version - app.celery.tasks.send_sms.apply_async.assert_called_once_with( - (str(sample_template.service_id), - notification_id, - "something_encrypted", - "2016-01-01T11:09:00.061258"), - kwargs=ANY, - queue="db-sms" - ) + app.celery.provider_tasks.send_sms_to_provider.apply_async.assert_called_once_with( + (str(sample_template.service_id), notification_id), queue='send-sms') assert response.status_code == 201 assert notification_id assert 'subject' not in response_data @@ -401,7 +390,7 @@ def test_should_allow_valid_sms_notification(notify_api, sample_template, mocker def test_create_email_should_reject_if_missing_required_fields(notify_api, sample_api_key, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_email.apply_async') + mocker.patch('app.celery.provider_tasks.send_email_to_provider.apply_async') data = {} auth_header = create_authorization_header(service_id=sample_api_key.service_id) @@ -412,7 +401,7 @@ def test_create_email_should_reject_if_missing_required_fields(notify_api, sampl headers=[('Content-Type', 'application/json'), auth_header]) json_resp = json.loads(response.get_data(as_text=True)) - app.celery.tasks.send_email.apply_async.assert_not_called() + app.celery.provider_tasks.send_email_to_provider.apply_async.assert_not_called() assert json_resp['result'] == 'error' assert 'Missing data for required field.' in json_resp['message']['to'][0] assert 'Missing data for required field.' in json_resp['message']['template'][0] @@ -422,7 +411,7 @@ def test_create_email_should_reject_if_missing_required_fields(notify_api, sampl def test_should_reject_email_notification_with_bad_email(notify_api, sample_email_template, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_email.apply_async') + mocker.patch('app.celery.provider_tasks.send_email_to_provider.apply_async') to_address = "bad-email" data = { 'to': to_address, @@ -436,7 +425,7 @@ def test_should_reject_email_notification_with_bad_email(notify_api, sample_emai headers=[('Content-Type', 'application/json'), auth_header]) data = json.loads(response.get_data(as_text=True)) - app.celery.tasks.send_email.apply_async.assert_not_called() + app.celery.provider_tasks.send_email_to_provider.apply_async.assert_not_called() assert response.status_code == 400 assert data['result'] == 'error' assert data['message']['to'][0] == 'Not a valid email address' @@ -446,7 +435,7 @@ def test_should_reject_email_notification_with_template_id_that_cant_be_found( notify_api, sample_email_template, mocker, fake_uuid): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_email.apply_async') + mocker.patch('app.celery.provider_tasks.send_email_to_provider.apply_async') data = { 'to': 'ok@ok.com', 'template': fake_uuid @@ -459,7 +448,7 @@ def test_should_reject_email_notification_with_template_id_that_cant_be_found( headers=[('Content-Type', 'application/json'), auth_header]) data = json.loads(response.get_data(as_text=True)) - app.celery.tasks.send_email.apply_async.assert_not_called() + app.celery.provider_tasks.send_email_to_provider.apply_async.assert_not_called() assert response.status_code == 404 assert data['result'] == 'error' test_string = 'No result found' @@ -469,7 +458,7 @@ def test_should_reject_email_notification_with_template_id_that_cant_be_found( def test_should_not_allow_email_template_from_another_service(notify_api, service_factory, sample_user, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_email.apply_async') + mocker.patch('app.celery.provider_tasks.send_email_to_provider.apply_async') service_1 = service_factory.get('service 1', template_type='email', user=sample_user, email_from='service.1') @@ -491,7 +480,7 @@ def test_should_not_allow_email_template_from_another_service(notify_api, servic headers=[('Content-Type', 'application/json'), auth_header]) json_resp = json.loads(response.get_data(as_text=True)) - app.celery.tasks.send_email.apply_async.assert_not_called() + app.celery.provider_tasks.send_email_to_provider.apply_async.assert_not_called() assert response.status_code == 404 test_string = 'No result found' @@ -501,7 +490,7 @@ def test_should_not_allow_email_template_from_another_service(notify_api, servic def test_should_not_send_email_if_restricted_and_not_a_service_user(notify_api, sample_email_template, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_email.apply_async') + mocker.patch('app.celery.provider_tasks.send_email_to_provider.apply_async') sample_email_template.service.restricted = True dao_update_service(sample_email_template.service) @@ -519,7 +508,7 @@ def test_should_not_send_email_if_restricted_and_not_a_service_user(notify_api, headers=[('Content-Type', 'application/json'), auth_header]) json_resp = json.loads(response.get_data(as_text=True)) - app.celery.tasks.send_email.apply_async.assert_not_called() + app.celery.provider_tasks.send_email_to_provider.apply_async.assert_not_called() assert response.status_code == 400 assert [( @@ -532,7 +521,7 @@ def test_should_not_send_email_if_restricted_and_not_a_service_user(notify_api, def test_should_allow_valid_email_notification(notify_api, sample_email_template, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_email.apply_async') + mocker.patch('app.celery.provider_tasks.send_email_to_provider.apply_async') mocker.patch('app.encryption.encrypt', return_value="something_encrypted") data = { @@ -549,16 +538,9 @@ def test_should_allow_valid_email_notification(notify_api, sample_email_template assert response.status_code == 201 response_data = json.loads(response.get_data(as_text=True))['data'] notification_id = response_data['notification']['id'] - assert app.encryption.encrypt.call_args[0][0]['to'] == 'ok@ok.com' - assert app.encryption.encrypt.call_args[0][0]['template'] == str(sample_email_template.id) - assert app.encryption.encrypt.call_args[0][0]['template_version'] == sample_email_template.version - app.celery.tasks.send_email.apply_async.assert_called_once_with( - (str(sample_email_template.service_id), - notification_id, - "something_encrypted", - "2016-01-01T11:09:00.061258"), - kwargs=ANY, - queue="db-email" + app.celery.provider_tasks.send_email_to_provider.apply_async.assert_called_once_with( + (str(sample_email_template.service_id), notification_id), + queue="send-email" ) assert response.status_code == 201 @@ -568,19 +550,18 @@ def test_should_allow_valid_email_notification(notify_api, sample_email_template assert response_data['template_version'] == sample_email_template.version -@pytest.mark.parametrize('restricted', [True, False]) @freeze_time("2016-01-01 12:00:00.061258") -def test_should_block_api_call_if_over_day_limit_for_restricted_and_live_service(notify_db, - notify_db_session, - notify_api, - mocker, - restricted): +def test_should_not_block_api_call_if_over_day_limit_for_live_service( + notify_db, + notify_db_session, + notify_api, + mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_email.apply_async') + mocker.patch('app.celery.provider_tasks.send_email_to_provider.apply_async') mocker.patch('app.encryption.encrypt', return_value="something_encrypted") - service = create_sample_service(notify_db, notify_db_session, limit=1, restricted=restricted) + service = create_sample_service(notify_db, notify_db_session, limit=1, restricted=False) email_template = create_sample_email_template(notify_db, notify_db_session, service=service) create_sample_notification( notify_db, notify_db_session, template=email_template, service=service, created_at=datetime.utcnow() @@ -597,56 +578,65 @@ def test_should_block_api_call_if_over_day_limit_for_restricted_and_live_service path='/notifications/email', data=json.dumps(data), headers=[('Content-Type', 'application/json'), auth_header]) - json_resp = json.loads(response.get_data(as_text=True)) + json.loads(response.get_data(as_text=True)) - assert response.status_code == 429 - assert 'Exceeded send limits (1) for today' in json_resp['message'] + assert response.status_code == 201 @freeze_time("2016-01-01 12:00:00.061258") -def test_should_block_api_call_if_over_day_limit_regardless_of_type(notify_db, notify_db_session, notify_api, mocker): +def test_should_block_api_call_if_over_day_limit_for_restricted_service( + notify_db, + notify_db_session, + notify_api, + mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_sms.apply_async') + mocker.patch('app.celery.provider_tasks.send_sms_to_provider.apply_async') mocker.patch('app.encryption.encrypt', return_value="something_encrypted") service = create_sample_service(notify_db, notify_db_session, limit=1, restricted=True) email_template = create_sample_email_template(notify_db, notify_db_session, service=service) - sms_template = create_sample_template(notify_db, notify_db_session, service=service) create_sample_notification( notify_db, notify_db_session, template=email_template, service=service, created_at=datetime.utcnow() ) data = { - 'to': '+447234123123', - 'template': str(sms_template.id) + 'to': 'ok@ok.com', + 'template': str(email_template.id) } auth_header = create_authorization_header(service_id=service.id) response = client.post( - path='/notifications/sms', + path='/notifications/email', data=json.dumps(data), headers=[('Content-Type', 'application/json'), auth_header]) - json_resp = json.loads(response.get_data(as_text=True)) + json.loads(response.get_data(as_text=True)) + assert response.status_code == 429 - assert 'Exceeded send limits (1) for today' in json_resp['message'] +@pytest.mark.parametrize('restricted', [True, False]) @freeze_time("2016-01-01 12:00:00.061258") -def test_should_allow_api_call_if_under_day_limit_regardless_of_type(notify_db, notify_db_session, notify_api, mocker): +def test_should_allow_api_call_if_under_day_limit_regardless_of_type( + notify_db, + notify_db_session, + notify_api, + sample_user, + mocker, + restricted): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_sms.apply_async') + mocker.patch('app.celery.provider_tasks.send_sms_to_provider.apply_async') mocker.patch('app.encryption.encrypt', return_value="something_encrypted") - service = create_sample_service(notify_db, notify_db_session, limit=2) + service = create_sample_service(notify_db, notify_db_session, limit=2, restricted=restricted) email_template = create_sample_email_template(notify_db, notify_db_session, service=service) sms_template = create_sample_template(notify_db, notify_db_session, service=service) create_sample_notification(notify_db, notify_db_session, template=email_template, service=service) data = { - 'to': '+447634123123', + 'to': sample_user.mobile_number, 'template': str(sms_template.id) } @@ -663,7 +653,7 @@ def test_should_allow_api_call_if_under_day_limit_regardless_of_type(notify_db, def test_should_not_return_html_in_body(notify_api, notify_db, notify_db_session, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_email.apply_async') + mocker.patch('app.celery.provider_tasks.send_email_to_provider.apply_async') email_template = create_sample_email_template(notify_db, notify_db.session, content='hello\nthere') data = { @@ -683,7 +673,7 @@ def test_should_not_return_html_in_body(notify_api, notify_db, notify_db_session def test_should_not_send_email_if_team_api_key_and_not_a_service_user(notify_api, sample_email_template, mocker): with notify_api.test_request_context(), notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_email.apply_async') + mocker.patch('app.celery.provider_tasks.send_email_to_provider.apply_async') data = { 'to': "not-someone-we-trust@email-address.com", @@ -701,7 +691,7 @@ def test_should_not_send_email_if_team_api_key_and_not_a_service_user(notify_api json_resp = json.loads(response.get_data(as_text=True)) - app.celery.tasks.send_email.apply_async.assert_not_called() + app.celery.provider_tasks.send_email_to_provider.apply_async.assert_not_called() assert response.status_code == 400 assert [ @@ -711,7 +701,7 @@ def test_should_not_send_email_if_team_api_key_and_not_a_service_user(notify_api def test_should_not_send_sms_if_team_api_key_and_not_a_service_user(notify_api, sample_template, mocker): with notify_api.test_request_context(), notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_sms.apply_async') + mocker.patch('app.celery.provider_tasks.send_sms_to_provider.apply_async') data = { 'to': '07123123123', @@ -726,7 +716,7 @@ def test_should_not_send_sms_if_team_api_key_and_not_a_service_user(notify_api, headers=[('Content-Type', 'application/json'), auth_header]) json_resp = json.loads(response.get_data(as_text=True)) - app.celery.tasks.send_sms.apply_async.assert_not_called() + app.celery.provider_tasks.send_sms_to_provider.apply_async.assert_not_called() assert response.status_code == 400 assert [ @@ -734,9 +724,10 @@ def test_should_not_send_sms_if_team_api_key_and_not_a_service_user(notify_api, ] == json_resp['message']['to'] -def test_should_send_email_if_team_api_key_and_a_service_user(notify_api, sample_email_template, mocker): +def test_should_send_email_if_team_api_key_and_a_service_user(notify_api, sample_email_template, fake_uuid, mocker): with notify_api.test_request_context(), notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_email.apply_async') + mocker.patch('app.celery.provider_tasks.send_email_to_provider.apply_async') + mocker.patch('app.notifications.rest.create_uuid', return_value=fake_uuid) data = { 'to': sample_email_template.service.created_by.email_address, @@ -754,23 +745,20 @@ def test_should_send_email_if_team_api_key_and_a_service_user(notify_api, sample data=json.dumps(data), headers=[('Content-Type', 'application/json'), ('Authorization', 'Bearer {}'.format(auth_header))]) - app.celery.tasks.send_email.apply_async.assert_called_once_with( - ANY, - kwargs={ - 'api_key_id': str(api_key.id), - 'key_type': api_key.key_type - }, - queue='db-email') + app.celery.provider_tasks.send_email_to_provider.apply_async.assert_called_once_with( + (str(sample_email_template.service.id), fake_uuid), + queue='send-email') assert response.status_code == 201 @pytest.mark.parametrize('restricted', [True, False]) @pytest.mark.parametrize('limit', [0, 1]) def test_should_send_email_to_anyone_with_test_key( - notify_api, sample_email_template, mocker, restricted, limit + notify_api, sample_email_template, mocker, restricted, limit, fake_uuid ): with notify_api.test_request_context(), notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_email.apply_async') + mocker.patch('app.celery.provider_tasks.send_email_to_provider.apply_async') + mocker.patch('app.notifications.rest.create_uuid', return_value=fake_uuid) data = { 'to': 'anyone123@example.com', @@ -793,20 +781,15 @@ def test_should_send_email_to_anyone_with_test_key( headers=[('Content-Type', 'application/json'), ('Authorization', 'Bearer {}'.format(auth_header))] ) - app.celery.tasks.send_email.apply_async.assert_called_once_with( - ANY, - kwargs={ - 'api_key_id': str(api_key.id), - 'key_type': api_key.key_type - }, - queue='db-email' - ) + app.celery.provider_tasks.send_email_to_provider.apply_async.assert_called_once_with( + (str(sample_email_template.service.id), fake_uuid), queue='send-email') assert response.status_code == 201 -def test_should_send_sms_if_team_api_key_and_a_service_user(notify_api, sample_template, mocker): +def test_should_send_sms_if_team_api_key_and_a_service_user(notify_api, sample_template, fake_uuid, mocker): with notify_api.test_request_context(), notify_api.test_client() as client: - mocker.patch('app.celery.tasks.send_sms.apply_async') + mocker.patch('app.celery.provider_tasks.send_sms_to_provider.apply_async') + mocker.patch('app.notifications.rest.create_uuid', return_value=fake_uuid) data = { 'to': sample_template.service.created_by.mobile_number, @@ -824,11 +807,140 @@ def test_should_send_sms_if_team_api_key_and_a_service_user(notify_api, sample_t data=json.dumps(data), headers=[('Content-Type', 'application/json'), ('Authorization', 'Bearer {}'.format(auth_header))]) - app.celery.tasks.send_sms.apply_async.assert_called_once_with( - ANY, - kwargs={ - 'api_key_id': str(api_key.id), - 'key_type': api_key.key_type - }, - queue='db-sms') + app.celery.provider_tasks.send_sms_to_provider.apply_async.assert_called_once_with( + (str(sample_template.service.id), fake_uuid), queue='send-sms') assert response.status_code == 201 + + +def test_should_persist_sms_notification(notify_api, sample_template, fake_uuid, mocker): + with notify_api.test_request_context(), notify_api.test_client() as client: + mocker.patch('app.celery.provider_tasks.send_sms_to_provider.apply_async') + mocker.patch('app.notifications.rest.create_uuid', return_value=fake_uuid) + + data = { + 'to': sample_template.service.created_by.mobile_number, + 'template': sample_template.id + } + api_key = ApiKey( + service=sample_template.service, + name='team_key', + created_by=sample_template.created_by, + key_type=KEY_TYPE_TEAM) + save_model_api_key(api_key) + auth_header = create_jwt_token(secret=api_key.unsigned_secret, client_id=str(api_key.service_id)) + + response = client.post( + path='/notifications/sms', + data=json.dumps(data), + headers=[('Content-Type', 'application/json'), ('Authorization', 'Bearer {}'.format(auth_header))]) + + app.celery.provider_tasks.send_sms_to_provider.apply_async.assert_called_once_with( + (str(sample_template.service.id), fake_uuid), queue='send-sms') + assert response.status_code == 201 + + notification = notifications_dao.get_notification_by_id(fake_uuid) + assert notification.to == sample_template.service.created_by.mobile_number + assert notification.template_id == sample_template.id + assert notification.notification_type == 'sms' + + +def test_should_persist_email_notification(notify_api, sample_email_template, fake_uuid, mocker): + with notify_api.test_request_context(), notify_api.test_client() as client: + mocker.patch('app.celery.provider_tasks.send_email_to_provider.apply_async') + mocker.patch('app.notifications.rest.create_uuid', return_value=fake_uuid) + + data = { + 'to': sample_email_template.service.created_by.email_address, + 'template': sample_email_template.id + } + api_key = ApiKey( + service=sample_email_template.service, + name='team_key', + created_by=sample_email_template.created_by, + key_type=KEY_TYPE_TEAM) + save_model_api_key(api_key) + auth_header = create_jwt_token(secret=api_key.unsigned_secret, client_id=str(api_key.service_id)) + + response = client.post( + path='/notifications/email', + data=json.dumps(data), + headers=[('Content-Type', 'application/json'), ('Authorization', 'Bearer {}'.format(auth_header))]) + + app.celery.provider_tasks.send_email_to_provider.apply_async.assert_called_once_with( + (str(sample_email_template.service.id), fake_uuid), queue='send-email') + assert response.status_code == 201 + + notification = notifications_dao.get_notification_by_id(fake_uuid) + assert notification.to == sample_email_template.service.created_by.email_address + assert notification.template_id == sample_email_template.id + assert notification.notification_type == 'email' + + +def test_should_delete_email_notification_and_return_error_if_sqs_fails( + notify_api, + sample_email_template, + fake_uuid, + mocker): + with notify_api.test_request_context(), notify_api.test_client() as client: + mocker.patch( + 'app.celery.provider_tasks.send_email_to_provider.apply_async', + side_effect=Exception("failed to talk to SQS") + ) + mocker.patch('app.notifications.rest.create_uuid', return_value=fake_uuid) + + data = { + 'to': sample_email_template.service.created_by.email_address, + 'template': sample_email_template.id + } + api_key = ApiKey( + service=sample_email_template.service, + name='team_key', + created_by=sample_email_template.created_by, + key_type=KEY_TYPE_TEAM) + save_model_api_key(api_key) + auth_header = create_jwt_token(secret=api_key.unsigned_secret, client_id=str(api_key.service_id)) + + response = client.post( + path='/notifications/email', + data=json.dumps(data), + headers=[('Content-Type', 'application/json'), ('Authorization', 'Bearer {}'.format(auth_header))]) + + app.celery.provider_tasks.send_email_to_provider.apply_async.assert_called_once_with( + (str(sample_email_template.service.id), fake_uuid), queue='send-email') + + assert response.status_code == 500 + assert not notifications_dao.get_notification_by_id(fake_uuid) + assert not NotificationHistory.query.get(fake_uuid) + + +def test_should_delete_sms_notification_and_return_error_if_sqs_fails(notify_api, sample_template, fake_uuid, mocker): + with notify_api.test_request_context(), notify_api.test_client() as client: + mocker.patch( + 'app.celery.provider_tasks.send_sms_to_provider.apply_async', + side_effect=Exception("failed to talk to SQS") + ) + mocker.patch('app.notifications.rest.create_uuid', return_value=fake_uuid) + + data = { + 'to': sample_template.service.created_by.mobile_number, + 'template': sample_template.id + } + api_key = ApiKey( + service=sample_template.service, + name='team_key', + created_by=sample_template.created_by, + key_type=KEY_TYPE_TEAM) + save_model_api_key(api_key) + auth_header = create_jwt_token(secret=api_key.unsigned_secret, client_id=str(api_key.service_id)) + + response = client.post( + path='/notifications/sms', + data=json.dumps(data), + headers=[('Content-Type', 'application/json'), ('Authorization', 'Bearer {}'.format(auth_header))]) + + app.celery.provider_tasks.send_sms_to_provider.apply_async.assert_called_once_with( + (str(sample_template.service.id), fake_uuid), queue='send-sms') + + assert response.status_code == 500 + assert not notifications_dao.get_notification_by_id(fake_uuid) + assert not NotificationHistory.query.get(fake_uuid) diff --git a/tests/app/public_contracts/test_POST_notification.py b/tests/app/public_contracts/test_POST_notification.py index ce333cef3..734a4cefe 100644 --- a/tests/app/public_contracts/test_POST_notification.py +++ b/tests/app/public_contracts/test_POST_notification.py @@ -5,7 +5,7 @@ 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.celery.tasks.send_sms_to_provider.apply_async') mocker.patch('app.encryption.encrypt', return_value="something_encrypted") data = { @@ -25,7 +25,7 @@ def test_post_sms_contract(client, mocker, sample_template): def test_post_email_contract(client, mocker, sample_email_template): - mocker.patch('app.celery.tasks.send_email.apply_async') + mocker.patch('app.celery.tasks.send_email_to_provider.apply_async') mocker.patch('app.encryption.encrypt', return_value="something_encrypted") data = { diff --git a/tests/app/test_model.py b/tests/app/test_model.py new file mode 100644 index 000000000..04385a6b6 --- /dev/null +++ b/tests/app/test_model.py @@ -0,0 +1,72 @@ +from datetime import datetime + +from app import DATETIME_FORMAT +from app.models import Notification + + +def test_should_build_notification_from_minimal_set_of_api_derived_params(notify_api): + now = datetime.utcnow() + + notification = { + 'template': 'template', + 'template_version': '1', + 'to': 'someone', + 'personalisation': {} + } + notification = Notification.from_api_request( + created_at=now.strftime(DATETIME_FORMAT), + notification=notification, + notification_id="notification_id", + service_id="service_id", + notification_type='SMS', + api_key_id='api_key_id', + key_type='key_type' + ) + assert notification.created_at == now + assert notification.id == "notification_id" + assert notification.template_id == 'template' + assert notification.template_version == '1' + assert not notification.job_row_number + assert not notification.job_id + assert notification.to == 'someone' + assert notification.service_id == 'service_id' + assert notification.status == 'created' + assert not notification.personalisation + assert notification.notification_type == 'SMS' + assert notification.api_key_id == 'api_key_id' + assert notification.key_type == 'key_type' + + +def test_should_build_notification_from_full_set_of_api_derived_params(notify_api): + now = datetime.utcnow() + + notification = { + 'template': 'template', + 'template_version': '1', + 'to': 'someone', + 'personalisation': {'key': 'value'}, + 'job': 'job_id', + 'row_number': 100 + } + notification = Notification.from_api_request( + created_at=now.strftime(DATETIME_FORMAT), + notification=notification, + notification_id="notification_id", + service_id="service_id", + notification_type='SMS', + api_key_id='api_key_id', + key_type='key_type' + ) + assert notification.created_at == now + assert notification.id == "notification_id" + assert notification.template_id == 'template' + assert notification.template_version == '1' + assert notification.job_row_number == 100 + assert notification.job_id == 'job_id' + assert notification.to == 'someone' + assert notification.service_id == 'service_id' + assert notification.status == 'created' + assert notification.personalisation == {'key': 'value'} + assert notification.notification_type == 'SMS' + assert notification.api_key_id == 'api_key_id' + assert notification.key_type == 'key_type'