mirror of
https://github.com/GSA/notifications-api.git
synced 2025-12-20 15:31:15 -05:00
Send Email via the API
- uses the new subject/email from fields present on the templates / service tables - brings the send email api call into line with the sms one. - same fields (to/template_id) - same rules regarding restricted services - wired in as a task into celery Requires - new celery queue - new env property (NOTIFY_EMAIL_DOMAIN)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from flask import request, url_for
|
from flask import request, url_for
|
||||||
from flask import Flask, _request_ctx_stack
|
from flask import Flask, _request_ctx_stack
|
||||||
from flask.ext.sqlalchemy import SQLAlchemy
|
from flask.ext.sqlalchemy import SQLAlchemy
|
||||||
@@ -92,3 +92,10 @@ def get_db_version():
|
|||||||
return full_name.split('_')[0]
|
return full_name.split('_')[0]
|
||||||
except:
|
except:
|
||||||
return 'n/a'
|
return 'n/a'
|
||||||
|
|
||||||
|
|
||||||
|
def email_safe(string):
|
||||||
|
return "".join([
|
||||||
|
character.lower() if character.isalnum() or character == "." else ""
|
||||||
|
for character in re.sub("\s+", ".", string.strip())
|
||||||
|
])
|
||||||
|
|||||||
@@ -33,6 +33,36 @@ def send_sms(service_id, notification_id, encrypted_notification):
|
|||||||
current_app.logger.debug(e)
|
current_app.logger.debug(e)
|
||||||
|
|
||||||
|
|
||||||
|
@notify_celery.task(name="send-email")
|
||||||
|
def send_email(service_id, notification_id, subject, from_address, encrypted_notification):
|
||||||
|
notification = encryption.decrypt(encrypted_notification)
|
||||||
|
template = get_model_templates(notification['template'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
notification_db_object = Notification(
|
||||||
|
id=notification_id,
|
||||||
|
template_id=notification['template'],
|
||||||
|
to=notification['to'],
|
||||||
|
service_id=service_id,
|
||||||
|
status='sent'
|
||||||
|
)
|
||||||
|
save_notification(notification_db_object)
|
||||||
|
|
||||||
|
try:
|
||||||
|
aws_ses_client.send_email(
|
||||||
|
from_address,
|
||||||
|
notification['to'],
|
||||||
|
subject,
|
||||||
|
template.content
|
||||||
|
)
|
||||||
|
except AwsSesClientException as e:
|
||||||
|
current_app.logger.debug(e)
|
||||||
|
save_notification(notification_db_object, {"status": "failed"})
|
||||||
|
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
current_app.logger.debug(e)
|
||||||
|
|
||||||
|
|
||||||
@notify_celery.task(name='send-sms-code')
|
@notify_celery.task(name='send-sms-code')
|
||||||
def send_sms_code(encrypted_verification):
|
def send_sms_code(encrypted_verification):
|
||||||
verification_message = encryption.decrypt(encrypted_verification)
|
verification_message = encryption.decrypt(encrypted_verification)
|
||||||
|
|||||||
@@ -3,23 +3,30 @@ import uuid
|
|||||||
from flask import (
|
from flask import (
|
||||||
Blueprint,
|
Blueprint,
|
||||||
jsonify,
|
jsonify,
|
||||||
request
|
request,
|
||||||
|
current_app
|
||||||
)
|
)
|
||||||
|
|
||||||
from app import api_user, encryption
|
from app import api_user, encryption
|
||||||
from app.aws_sqs import add_notification_to_queue
|
from app.aws_sqs import add_notification_to_queue
|
||||||
from app.dao import (templates_dao, notifications_dao)
|
from app.dao import (
|
||||||
|
templates_dao,
|
||||||
|
users_dao,
|
||||||
|
services_dao,
|
||||||
|
notifications_dao
|
||||||
|
)
|
||||||
from app.schemas import (
|
from app.schemas import (
|
||||||
email_notification_schema,
|
email_notification_schema,
|
||||||
sms_template_notification_schema,
|
sms_template_notification_schema,
|
||||||
notification_status_schema
|
notification_status_schema
|
||||||
)
|
)
|
||||||
from app.celery.tasks import send_sms
|
from app.celery.tasks import send_sms, send_email
|
||||||
from sqlalchemy.orm.exc import NoResultFound
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
|
||||||
notifications = Blueprint('notifications', __name__)
|
notifications = Blueprint('notifications', __name__)
|
||||||
|
|
||||||
from app.errors import register_errors
|
from app.errors import register_errors
|
||||||
|
|
||||||
register_errors(notifications)
|
register_errors(notifications)
|
||||||
|
|
||||||
|
|
||||||
@@ -47,6 +54,12 @@ def create_sms_notification():
|
|||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
return jsonify(result="error", message={'template': ['Template not found']}), 400
|
return jsonify(result="error", message={'template': ['Template not found']}), 400
|
||||||
|
|
||||||
|
service = services_dao.dao_fetch_service_by_id(api_user['client'])
|
||||||
|
|
||||||
|
if service.restricted:
|
||||||
|
if notification['to'] not in [user.email_address for user in service.users]:
|
||||||
|
return jsonify(result="error", message={'to': ['Invalid phone number for restricted service']}), 400
|
||||||
|
|
||||||
notification_id = create_notification_id()
|
notification_id = create_notification_id()
|
||||||
|
|
||||||
send_sms.apply_async((
|
send_sms.apply_async((
|
||||||
@@ -59,17 +72,38 @@ def create_sms_notification():
|
|||||||
|
|
||||||
@notifications.route('/email', methods=['POST'])
|
@notifications.route('/email', methods=['POST'])
|
||||||
def create_email_notification():
|
def create_email_notification():
|
||||||
resp_json = request.get_json()
|
notification, errors = email_notification_schema.load(request.get_json())
|
||||||
notification, errors = email_notification_schema.load(resp_json)
|
|
||||||
if errors:
|
if errors:
|
||||||
return jsonify(result="error", message=errors), 400
|
return jsonify(result="error", message=errors), 400
|
||||||
notification_id = add_notification_to_queue(api_user['client'], "admin", 'email', notification)
|
|
||||||
|
template = templates_dao.dao_get_template_by_id_and_service_id(
|
||||||
|
template_id=notification['template'],
|
||||||
|
service_id=api_user['client']
|
||||||
|
)
|
||||||
|
|
||||||
|
if not template:
|
||||||
|
return jsonify(result="error", message={'template': ['Template not found']}), 400
|
||||||
|
|
||||||
|
service = services_dao.dao_fetch_service_by_id(api_user['client'])
|
||||||
|
|
||||||
|
if service.restricted:
|
||||||
|
if notification['to'] not in [user.email_address for user in service.users]:
|
||||||
|
return jsonify(result="error", message={'to': ['Email address not permitted for restricted service']}), 400
|
||||||
|
|
||||||
|
notification_id = create_notification_id()
|
||||||
|
|
||||||
|
send_email.apply_async((
|
||||||
|
api_user['client'],
|
||||||
|
notification_id,
|
||||||
|
template.subject,
|
||||||
|
"{}@{}".format(service.email_from, current_app.config['NOTIFY_EMAIL_DOMAIN']),
|
||||||
|
encryption.encrypt(notification)),
|
||||||
|
queue='email')
|
||||||
return jsonify({'notification_id': notification_id}), 201
|
return jsonify({'notification_id': notification_id}), 201
|
||||||
|
|
||||||
|
|
||||||
@notifications.route('/sms/service/<service_id>', methods=['POST'])
|
@notifications.route('/sms/service/<service_id>', methods=['POST'])
|
||||||
def create_sms_for_service(service_id):
|
def create_sms_for_service(service_id):
|
||||||
|
|
||||||
resp_json = request.get_json()
|
resp_json = request.get_json()
|
||||||
|
|
||||||
notification, errors = sms_template_notification_schema.load(resp_json)
|
notification, errors = sms_template_notification_schema.load(resp_json)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ from marshmallow import (post_load, ValidationError, validates, validates_schema
|
|||||||
|
|
||||||
mobile_regex = re.compile("^\\+44[\\d]{10}$")
|
mobile_regex = re.compile("^\\+44[\\d]{10}$")
|
||||||
|
|
||||||
|
email_regex = re.compile("(^[^@^\\s]+@[^@^\\.^\\s]+(\\.[^@^\\.^\\s]*)*\.(.+))")
|
||||||
|
|
||||||
|
|
||||||
# TODO I think marshmallow provides a better integration and error handling.
|
# TODO I think marshmallow provides a better integration and error handling.
|
||||||
# Would be better to replace functionality in dao with the marshmallow supported
|
# Would be better to replace functionality in dao with the marshmallow supported
|
||||||
@@ -78,9 +80,6 @@ class RequestVerifyCodeSchema(ma.Schema):
|
|||||||
to = fields.Str(required=False)
|
to = fields.Str(required=False)
|
||||||
|
|
||||||
|
|
||||||
# TODO main purpose to be added later
|
|
||||||
# when processing templates, template will be
|
|
||||||
# common for all notifications.
|
|
||||||
class NotificationSchema(ma.Schema):
|
class NotificationSchema(ma.Schema):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -94,50 +93,25 @@ class SmsNotificationSchema(NotificationSchema):
|
|||||||
raise ValidationError('Invalid phone number, must be of format +441234123123')
|
raise ValidationError('Invalid phone number, must be of format +441234123123')
|
||||||
|
|
||||||
|
|
||||||
|
class EmailNotificationSchema(NotificationSchema):
|
||||||
|
to = fields.Str(required=True)
|
||||||
|
template = fields.Int(required=True)
|
||||||
|
|
||||||
|
@validates('to')
|
||||||
|
def validate_to(self, value):
|
||||||
|
if not email_regex.match(value):
|
||||||
|
raise ValidationError('Invalid email')
|
||||||
|
|
||||||
|
|
||||||
class SmsTemplateNotificationSchema(SmsNotificationSchema):
|
class SmsTemplateNotificationSchema(SmsNotificationSchema):
|
||||||
template = fields.Int(required=True)
|
template = fields.Int(required=True)
|
||||||
job = fields.String()
|
job = fields.String()
|
||||||
|
|
||||||
@validates_schema
|
|
||||||
def validate_schema(self, data):
|
|
||||||
"""
|
|
||||||
Validate the to field is valid for this notification
|
|
||||||
"""
|
|
||||||
from app import api_user
|
|
||||||
template_id = data.get('template', None)
|
|
||||||
template = models.Template.query.filter_by(id=template_id).first()
|
|
||||||
if template:
|
|
||||||
service = template.service
|
|
||||||
# Validate restricted service,
|
|
||||||
# restricted services can only send to one of its users.
|
|
||||||
if service.restricted:
|
|
||||||
valid = False
|
|
||||||
for usr in service.users:
|
|
||||||
if data['to'] == usr.mobile_number:
|
|
||||||
valid = True
|
|
||||||
break
|
|
||||||
if not valid:
|
|
||||||
raise ValidationError('Invalid phone number for restricted service', 'restricted')
|
|
||||||
# Assert the template is valid for the service which made the request.
|
|
||||||
service = api_user['client']
|
|
||||||
admin_users = [current_app.config.get('ADMIN_CLIENT_USER_NAME'),
|
|
||||||
current_app.config.get('DELIVERY_CLIENT_USER_NAME')]
|
|
||||||
if (service not in admin_users and
|
|
||||||
template.service != models.Service.query.filter_by(id=service).first()):
|
|
||||||
raise ValidationError('Invalid template', 'restricted')
|
|
||||||
|
|
||||||
|
|
||||||
class SmsAdminNotificationSchema(SmsNotificationSchema):
|
class SmsAdminNotificationSchema(SmsNotificationSchema):
|
||||||
content = fields.Str(required=True)
|
content = fields.Str(required=True)
|
||||||
|
|
||||||
|
|
||||||
class EmailNotificationSchema(NotificationSchema):
|
|
||||||
to_address = fields.Str(load_from="to", dump_to='to', required=True)
|
|
||||||
from_address = fields.Str(load_from="from", dump_to='from', required=True)
|
|
||||||
subject = fields.Str(required=True)
|
|
||||||
body = fields.Str(load_from="message", dump_to='message', required=True)
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationStatusSchema(BaseSchema):
|
class NotificationStatusSchema(BaseSchema):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import re
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
@@ -30,6 +29,7 @@ from app.schemas import (
|
|||||||
service_schema,
|
service_schema,
|
||||||
api_keys_schema
|
api_keys_schema
|
||||||
)
|
)
|
||||||
|
from app import email_safe
|
||||||
|
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ def create_service():
|
|||||||
|
|
||||||
data.pop('user_id', None)
|
data.pop('user_id', None)
|
||||||
if 'name' in data:
|
if 'name' in data:
|
||||||
data['email_from'] = _email_safe(data.get('name', None))
|
data['email_from'] = email_safe(data.get('name', None))
|
||||||
|
|
||||||
valid_service, errors = service_schema.load(request.get_json())
|
valid_service, errors = service_schema.load(request.get_json())
|
||||||
|
|
||||||
@@ -87,13 +87,6 @@ def create_service():
|
|||||||
return jsonify(data=service_schema.dump(valid_service).data), 201
|
return jsonify(data=service_schema.dump(valid_service).data), 201
|
||||||
|
|
||||||
|
|
||||||
def _email_safe(string):
|
|
||||||
return "".join([
|
|
||||||
character.lower() if character.isalnum() or character == "." else ""
|
|
||||||
for character in re.sub("\s+", ".", string.strip())
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
@service.route('/<service_id>', methods=['POST'])
|
@service.route('/<service_id>', methods=['POST'])
|
||||||
def update_service(service_id):
|
def update_service(service_id):
|
||||||
fetched_service = dao_fetch_service_by_id(service_id)
|
fetched_service = dao_fetch_service_by_id(service_id)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class Config(object):
|
|||||||
SQLALCHEMY_DATABASE_URI = os.environ['SQLALCHEMY_DATABASE_URI']
|
SQLALCHEMY_DATABASE_URI = os.environ['SQLALCHEMY_DATABASE_URI']
|
||||||
SQLALCHEMY_RECORD_QUERIES = True
|
SQLALCHEMY_RECORD_QUERIES = True
|
||||||
VERIFY_CODE_FROM_EMAIL_ADDRESS = os.environ['VERIFY_CODE_FROM_EMAIL_ADDRESS']
|
VERIFY_CODE_FROM_EMAIL_ADDRESS = os.environ['VERIFY_CODE_FROM_EMAIL_ADDRESS']
|
||||||
|
NOTIFY_EMAIL_DOMAIN = os.environ['NOTIFY_EMAIL_DOMAIN']
|
||||||
|
|
||||||
BROKER_URL = 'sqs://'
|
BROKER_URL = 'sqs://'
|
||||||
BROKER_TRANSPORT_OPTIONS = {
|
BROKER_TRANSPORT_OPTIONS = {
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ export TWILIO_ACCOUNT_SID="test"
|
|||||||
export TWILIO_AUTH_TOKEN="test"
|
export TWILIO_AUTH_TOKEN="test"
|
||||||
export TWILIO_NUMBER="test"
|
export TWILIO_NUMBER="test"
|
||||||
export FIRETEXT_API_KEY="Firetext"
|
export FIRETEXT_API_KEY="Firetext"
|
||||||
export FIRETEXT_NUMBER="Firetext"
|
export FIRETEXT_NUMBER="Firetext"
|
||||||
|
export NOTIFY_EMAIL_DOMAIN="test.notify.com"
|
||||||
|
|||||||
@@ -3,4 +3,4 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
source environment.sh
|
source environment.sh
|
||||||
celery -A run_celery.notify_celery worker --loglevel=INFO --logfile=/var/log/notify/application.log --concurrency=4 -Q sms,sms-code,email-code
|
celery -A run_celery.notify_celery worker --loglevel=INFO --logfile=/var/log/notify/application.log --concurrency=4 -Q sms,sms-code,email-code,email
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import uuid
|
import uuid
|
||||||
import pytest
|
import pytest
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from app.celery.tasks import (send_sms, send_sms_code, send_email_code)
|
from app.celery.tasks import (send_sms, send_sms_code, send_email_code, send_email)
|
||||||
from app import (firetext_client, aws_ses_client, encryption)
|
from app import (firetext_client, aws_ses_client, encryption)
|
||||||
|
from app.clients.email.aws_ses import AwsSesClientException
|
||||||
from app.clients.sms.firetext import FiretextClientException
|
from app.clients.sms.firetext import FiretextClientException
|
||||||
from app.dao import notifications_dao
|
from app.dao import notifications_dao
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
@@ -32,6 +33,36 @@ def test_should_send_template_to_correct_sms_provider_and_persist(sample_templat
|
|||||||
assert persisted_notification.status == 'sent'
|
assert persisted_notification.status == 'sent'
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_send_template_to_email_provider_and_persist(sample_email_template, mocker):
|
||||||
|
notification = {
|
||||||
|
"template": sample_email_template.id,
|
||||||
|
"to": "my_email@my_email.com"
|
||||||
|
}
|
||||||
|
mocker.patch('app.encryption.decrypt', return_value=notification)
|
||||||
|
mocker.patch('app.aws_ses_client.send_email')
|
||||||
|
|
||||||
|
notification_id = uuid.uuid4()
|
||||||
|
|
||||||
|
send_email(
|
||||||
|
sample_email_template.service_id,
|
||||||
|
notification_id,
|
||||||
|
'subject',
|
||||||
|
'email_from',
|
||||||
|
"encrypted-in-reality")
|
||||||
|
|
||||||
|
aws_ses_client.send_email.assert_called_once_with(
|
||||||
|
"email_from",
|
||||||
|
"my_email@my_email.com",
|
||||||
|
"subject",
|
||||||
|
sample_email_template.content
|
||||||
|
)
|
||||||
|
persisted_notification = notifications_dao.get_notification(sample_email_template.service_id, notification_id)
|
||||||
|
assert persisted_notification.id == notification_id
|
||||||
|
assert persisted_notification.to == 'my_email@my_email.com'
|
||||||
|
assert persisted_notification.template_id == sample_email_template.id
|
||||||
|
assert persisted_notification.status == 'sent'
|
||||||
|
|
||||||
|
|
||||||
def test_should_persist_notification_as_failed_if_sms_client_fails(sample_template, mocker):
|
def test_should_persist_notification_as_failed_if_sms_client_fails(sample_template, mocker):
|
||||||
notification = {
|
notification = {
|
||||||
"template": sample_template.id,
|
"template": sample_template.id,
|
||||||
@@ -55,13 +86,43 @@ def test_should_persist_notification_as_failed_if_sms_client_fails(sample_templa
|
|||||||
assert persisted_notification.status == 'failed'
|
assert persisted_notification.status == 'failed'
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_persist_notification_as_failed_if_email_client_fails(sample_email_template, mocker):
|
||||||
|
notification = {
|
||||||
|
"template": sample_email_template.id,
|
||||||
|
"to": "my_email@my_email.com"
|
||||||
|
}
|
||||||
|
mocker.patch('app.encryption.decrypt', return_value=notification)
|
||||||
|
mocker.patch('app.aws_ses_client.send_email', side_effect=AwsSesClientException())
|
||||||
|
|
||||||
|
notification_id = uuid.uuid4()
|
||||||
|
|
||||||
|
send_email(
|
||||||
|
sample_email_template.service_id,
|
||||||
|
notification_id,
|
||||||
|
'subject',
|
||||||
|
'email_from',
|
||||||
|
"encrypted-in-reality")
|
||||||
|
|
||||||
|
aws_ses_client.send_email.assert_called_once_with(
|
||||||
|
"email_from",
|
||||||
|
"my_email@my_email.com",
|
||||||
|
"subject",
|
||||||
|
sample_email_template.content
|
||||||
|
)
|
||||||
|
persisted_notification = notifications_dao.get_notification(sample_email_template.service_id, notification_id)
|
||||||
|
assert persisted_notification.id == notification_id
|
||||||
|
assert persisted_notification.to == 'my_email@my_email.com'
|
||||||
|
assert persisted_notification.template_id == sample_email_template.id
|
||||||
|
assert persisted_notification.status == 'failed'
|
||||||
|
|
||||||
|
|
||||||
def test_should_not_send_sms_if_db_peristance_failed(sample_template, mocker):
|
def test_should_not_send_sms_if_db_peristance_failed(sample_template, mocker):
|
||||||
notification = {
|
notification = {
|
||||||
"template": sample_template.id,
|
"template": sample_template.id,
|
||||||
"to": "+441234123123"
|
"to": "+441234123123"
|
||||||
}
|
}
|
||||||
mocker.patch('app.encryption.decrypt', return_value=notification)
|
mocker.patch('app.encryption.decrypt', return_value=notification)
|
||||||
mocker.patch('app.firetext_client.send_sms', side_effect=FiretextClientException())
|
mocker.patch('app.firetext_client.send_sms')
|
||||||
mocker.patch('app.db.session.add', side_effect=SQLAlchemyError())
|
mocker.patch('app.db.session.add', side_effect=SQLAlchemyError())
|
||||||
|
|
||||||
notification_id = uuid.uuid4()
|
notification_id = uuid.uuid4()
|
||||||
@@ -77,6 +138,30 @@ def test_should_not_send_sms_if_db_peristance_failed(sample_template, mocker):
|
|||||||
assert 'No row was found for one' in str(e.value)
|
assert 'No row was found for one' in str(e.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_not_send_email_if_db_peristance_failed(sample_email_template, mocker):
|
||||||
|
notification = {
|
||||||
|
"template": sample_email_template.id,
|
||||||
|
"to": "my_email@my_email.com"
|
||||||
|
}
|
||||||
|
mocker.patch('app.encryption.decrypt', return_value=notification)
|
||||||
|
mocker.patch('app.aws_ses_client.send_email')
|
||||||
|
mocker.patch('app.db.session.add', side_effect=SQLAlchemyError())
|
||||||
|
|
||||||
|
notification_id = uuid.uuid4()
|
||||||
|
|
||||||
|
send_email(
|
||||||
|
sample_email_template.service_id,
|
||||||
|
notification_id,
|
||||||
|
'subject',
|
||||||
|
'email_from',
|
||||||
|
"encrypted-in-reality")
|
||||||
|
|
||||||
|
aws_ses_client.send_email.assert_not_called()
|
||||||
|
with pytest.raises(NoResultFound) as e:
|
||||||
|
notifications_dao.get_notification(sample_email_template.service_id, notification_id)
|
||||||
|
assert 'No row was found for one' in str(e.value)
|
||||||
|
|
||||||
|
|
||||||
def test_should_send_sms_code(mocker):
|
def test_should_send_sms_code(mocker):
|
||||||
notification = {'to': '+441234123123',
|
notification = {'to': '+441234123123',
|
||||||
'secret_code': '12345'}
|
'secret_code': '12345'}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from app import email_safe
|
||||||
from app.models import (User, Service, Template, ApiKey, Job, Notification)
|
from app.models import (User, Service, Template, ApiKey, Job, Notification)
|
||||||
from app.dao.users_dao import (save_model_user, create_user_code, create_secret_code)
|
from app.dao.users_dao import (save_model_user, create_user_code, create_secret_code)
|
||||||
from app.dao.services_dao import dao_create_service
|
from app.dao.services_dao import dao_create_service
|
||||||
@@ -13,11 +14,24 @@ import uuid
|
|||||||
@pytest.fixture(scope='function')
|
@pytest.fixture(scope='function')
|
||||||
def service_factory(notify_db, notify_db_session):
|
def service_factory(notify_db, notify_db_session):
|
||||||
class ServiceFactory(object):
|
class ServiceFactory(object):
|
||||||
def get(self, service_name, user=None):
|
def get(self, service_name, user=None, template_type=None):
|
||||||
if not user:
|
if not user:
|
||||||
user = sample_user(notify_db, notify_db_session)
|
user = sample_user(notify_db, notify_db_session)
|
||||||
service = sample_service(notify_db, notify_db_session, service_name, user)
|
service = sample_service(notify_db, notify_db_session, service_name, user)
|
||||||
sample_template(notify_db, notify_db_session, service=service)
|
if template_type == 'email':
|
||||||
|
sample_template(
|
||||||
|
notify_db,
|
||||||
|
notify_db_session,
|
||||||
|
template_type=template_type,
|
||||||
|
subject_line=email_safe(service_name),
|
||||||
|
service=service
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
sample_template(
|
||||||
|
notify_db,
|
||||||
|
notify_db_session,
|
||||||
|
service=service
|
||||||
|
)
|
||||||
return service
|
return service
|
||||||
|
|
||||||
return ServiceFactory()
|
return ServiceFactory()
|
||||||
@@ -93,7 +107,7 @@ def sample_service(notify_db,
|
|||||||
'limit': 1000,
|
'limit': 1000,
|
||||||
'active': False,
|
'active': False,
|
||||||
'restricted': False,
|
'restricted': False,
|
||||||
'email_from': service_name
|
'email_from': email_safe(service_name)
|
||||||
}
|
}
|
||||||
service = Service.query.filter_by(name=service_name).first()
|
service = Service.query.filter_by(name=service_name).first()
|
||||||
if not service:
|
if not service:
|
||||||
@@ -119,6 +133,33 @@ def sample_template(notify_db,
|
|||||||
'content': content,
|
'content': content,
|
||||||
'service': service
|
'service': service
|
||||||
}
|
}
|
||||||
|
if template_type == 'email':
|
||||||
|
data.update({
|
||||||
|
'subject': subject_line
|
||||||
|
})
|
||||||
|
template = Template(**data)
|
||||||
|
save_model_template(template)
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def sample_email_template(
|
||||||
|
notify_db,
|
||||||
|
notify_db_session,
|
||||||
|
template_name="Email Template Name",
|
||||||
|
template_type="email",
|
||||||
|
content="This is a template",
|
||||||
|
subject_line='Email Subject',
|
||||||
|
service=None):
|
||||||
|
if service is None:
|
||||||
|
service = sample_service(notify_db, notify_db_session)
|
||||||
|
sample_api_key(notify_db, notify_db_session, service=service)
|
||||||
|
data = {
|
||||||
|
'name': template_name,
|
||||||
|
'template_type': template_type,
|
||||||
|
'content': content,
|
||||||
|
'service': service
|
||||||
|
}
|
||||||
if subject_line:
|
if subject_line:
|
||||||
data.update({
|
data.update({
|
||||||
'subject': subject_line
|
'subject': subject_line
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from tests import create_authorization_header
|
|||||||
from flask import json
|
from flask import json
|
||||||
from app.models import Service
|
from app.models import Service
|
||||||
from app.dao.templates_dao import get_model_templates
|
from app.dao.templates_dao import get_model_templates
|
||||||
|
from app.dao.services_dao import dao_update_service
|
||||||
|
|
||||||
|
|
||||||
def test_get_notification_by_id(notify_api, sample_notification):
|
def test_get_notification_by_id(notify_api, sample_notification):
|
||||||
@@ -158,21 +159,20 @@ def test_prevents_sending_to_any_mobile_on_restricted_service(notify_api, sample
|
|||||||
app.celery.tasks.send_sms.apply_async.assert_not_called()
|
app.celery.tasks.send_sms.apply_async.assert_not_called()
|
||||||
|
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert 'Invalid phone number for restricted service' in json_resp['message']['restricted']
|
assert 'Invalid phone number for restricted service' in json_resp['message']['to']
|
||||||
|
|
||||||
|
|
||||||
def test_should_not_allow_template_from_another_service(notify_api, service_factory, mocker):
|
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_request_context():
|
||||||
with notify_api.test_client() as client:
|
with notify_api.test_client() as client:
|
||||||
mocker.patch('app.celery.tasks.send_sms.apply_async')
|
mocker.patch('app.celery.tasks.send_sms.apply_async')
|
||||||
|
|
||||||
service_1 = service_factory.get('service 1')
|
service_1 = service_factory.get('service 1', user=sample_user)
|
||||||
service_2 = service_factory.get('service 2')
|
service_2 = service_factory.get('service 2', user=sample_user)
|
||||||
|
|
||||||
service_2_templates = get_model_templates(service_id=service_2.id)
|
service_2_templates = get_model_templates(service_id=service_2.id)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'to': '+441234123123',
|
'to': sample_user.mobile_number,
|
||||||
'template': service_2_templates[0].id
|
'template': service_2_templates[0].id
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ def test_should_not_allow_template_from_another_service(notify_api, service_fact
|
|||||||
app.celery.tasks.send_sms.apply_async.assert_not_called()
|
app.celery.tasks.send_sms.apply_async.assert_not_called()
|
||||||
|
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert 'Invalid template' in json_resp['message']['restricted']
|
assert 'Template not found' in json_resp['message']['template']
|
||||||
|
|
||||||
|
|
||||||
def test_should_allow_valid_sms_notification(notify_api, sample_template, mocker):
|
def test_should_allow_valid_sms_notification(notify_api, sample_template, mocker):
|
||||||
@@ -228,27 +228,14 @@ def test_should_allow_valid_sms_notification(notify_api, sample_template, mocker
|
|||||||
assert notification_id
|
assert notification_id
|
||||||
|
|
||||||
|
|
||||||
@moto.mock_sqs
|
def test_create_email_should_reject_if_missing_required_fields(notify_api, sample_api_key, mocker):
|
||||||
def test_send_email_valid_data(notify_api,
|
|
||||||
notify_db,
|
|
||||||
notify_db_session,
|
|
||||||
sample_service,
|
|
||||||
sample_admin_service_id,
|
|
||||||
sqs_client_conn,
|
|
||||||
mocker):
|
|
||||||
with notify_api.test_request_context():
|
with notify_api.test_request_context():
|
||||||
with notify_api.test_client() as client:
|
with notify_api.test_client() as client:
|
||||||
to_address = "to@notify.com"
|
mocker.patch('app.celery.tasks.send_email.apply_async')
|
||||||
from_address = "from@notify.com"
|
|
||||||
subject = "This is the subject"
|
data = {}
|
||||||
message = "This is the message"
|
|
||||||
data = {
|
|
||||||
'to': to_address,
|
|
||||||
'from': from_address,
|
|
||||||
'subject': subject,
|
|
||||||
'message': message
|
|
||||||
}
|
|
||||||
auth_header = create_authorization_header(
|
auth_header = create_authorization_header(
|
||||||
|
service_id=sample_api_key.service_id,
|
||||||
request_body=json.dumps(data),
|
request_body=json.dumps(data),
|
||||||
path='/notifications/email',
|
path='/notifications/email',
|
||||||
method='POST')
|
method='POST')
|
||||||
@@ -258,8 +245,166 @@ def test_send_email_valid_data(notify_api,
|
|||||||
data=json.dumps(data),
|
data=json.dumps(data),
|
||||||
headers=[('Content-Type', 'application/json'), auth_header])
|
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()
|
||||||
|
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]
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
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')
|
||||||
|
to_address = "bad-email"
|
||||||
|
data = {
|
||||||
|
'to': to_address,
|
||||||
|
'template': sample_email_template.service.id
|
||||||
|
}
|
||||||
|
auth_header = create_authorization_header(
|
||||||
|
service_id=sample_email_template.service.id,
|
||||||
|
request_body=json.dumps(data),
|
||||||
|
path='/notifications/email',
|
||||||
|
method='POST')
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
path='/notifications/email',
|
||||||
|
data=json.dumps(data),
|
||||||
|
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()
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert data['result'] == 'error'
|
||||||
|
assert data['message']['to'][0] == 'Invalid email'
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_reject_email_notification_with_template_id_that_cant_be_found(
|
||||||
|
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')
|
||||||
|
data = {
|
||||||
|
'to': 'ok@ok.com',
|
||||||
|
'template': 1234
|
||||||
|
}
|
||||||
|
auth_header = create_authorization_header(
|
||||||
|
service_id=sample_email_template.service.id,
|
||||||
|
request_body=json.dumps(data),
|
||||||
|
path='/notifications/email',
|
||||||
|
method='POST')
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
path='/notifications/email',
|
||||||
|
data=json.dumps(data),
|
||||||
|
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()
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert data['result'] == 'error'
|
||||||
|
assert data['message']['template'][0] == 'Template not 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')
|
||||||
|
|
||||||
|
service_1 = service_factory.get('service 1', template_type='email', user=sample_user)
|
||||||
|
service_2 = service_factory.get('service 2', template_type='email', user=sample_user)
|
||||||
|
|
||||||
|
service_2_templates = get_model_templates(service_id=service_2.id)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'to': sample_user.email_address,
|
||||||
|
'template': service_2_templates[0].id
|
||||||
|
}
|
||||||
|
|
||||||
|
auth_header = create_authorization_header(
|
||||||
|
service_id=service_1.id,
|
||||||
|
request_body=json.dumps(data),
|
||||||
|
path='/notifications/email',
|
||||||
|
method='POST')
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
path='/notifications/email',
|
||||||
|
data=json.dumps(data),
|
||||||
|
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()
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert 'Template not found' in json_resp['message']['template']
|
||||||
|
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
sample_email_template.service.restricted = True
|
||||||
|
dao_update_service(sample_email_template)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'to': "not-someone-we-trust@email-address.com",
|
||||||
|
'template': sample_email_template.id
|
||||||
|
}
|
||||||
|
|
||||||
|
auth_header = create_authorization_header(
|
||||||
|
service_id=sample_email_template.service.id,
|
||||||
|
request_body=json.dumps(data),
|
||||||
|
path='/notifications/email',
|
||||||
|
method='POST')
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
path='/notifications/email',
|
||||||
|
data=json.dumps(data),
|
||||||
|
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()
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert 'Email address not permitted for restricted service' in json_resp['message']['to']
|
||||||
|
|
||||||
|
|
||||||
|
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.encryption.encrypt', return_value="something_encrypted")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'to': 'ok@ok.com',
|
||||||
|
'template': sample_email_template.id
|
||||||
|
}
|
||||||
|
|
||||||
|
auth_header = create_authorization_header(
|
||||||
|
request_body=json.dumps(data),
|
||||||
|
path='/notifications/email',
|
||||||
|
method='POST',
|
||||||
|
service_id=sample_email_template.service_id
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
path='/notifications/email',
|
||||||
|
data=json.dumps(data),
|
||||||
|
headers=[('Content-Type', 'application/json'), auth_header])
|
||||||
|
|
||||||
|
notification_id = json.loads(response.data)['notification_id']
|
||||||
|
app.celery.tasks.send_email.apply_async.assert_called_once_with(
|
||||||
|
(str(sample_email_template.service_id),
|
||||||
|
notification_id,
|
||||||
|
"Email Subject",
|
||||||
|
"sample.service@test.notify.com",
|
||||||
|
"something_encrypted"),
|
||||||
|
queue="email"
|
||||||
|
)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
assert json.loads(response.data)['notification_id'] is not None
|
assert notification_id
|
||||||
|
|
||||||
|
|
||||||
@moto.mock_sqs
|
@moto.mock_sqs
|
||||||
|
|||||||
Reference in New Issue
Block a user