Merge branch 'master' into schedule-api-notification

This commit is contained in:
Rebecca Law
2017-05-16 10:50:53 +01:00
18 changed files with 577 additions and 60 deletions

View File

@@ -4,7 +4,12 @@ from flask import current_app
FILE_LOCATION_STRUCTURE = 'service-{}-notify/{}.csv'
def get_s3_job_object(bucket_name, file_location):
def get_s3_file(bucket_name, file_location):
s3_file = get_s3_object(bucket_name, file_location)
return s3_file.get()['Body'].read().decode('utf-8')
def get_s3_object(bucket_name, file_location):
s3 = resource('s3')
return s3.Object(bucket_name, file_location)
@@ -12,12 +17,12 @@ def get_s3_job_object(bucket_name, file_location):
def get_job_from_s3(service_id, job_id):
bucket_name = current_app.config['CSV_UPLOAD_BUCKET_NAME']
file_location = FILE_LOCATION_STRUCTURE.format(service_id, job_id)
obj = get_s3_job_object(bucket_name, file_location)
obj = get_s3_object(bucket_name, file_location)
return obj.get()['Body'].read().decode('utf-8')
def remove_job_from_s3(service_id, job_id):
bucket_name = current_app.config['CSV_UPLOAD_BUCKET_NAME']
file_location = FILE_LOCATION_STRUCTURE.format(service_id, job_id)
obj = get_s3_job_object(bucket_name, file_location)
obj = get_s3_object(bucket_name, file_location)
return obj.delete()

View File

@@ -1,6 +1,7 @@
import random
from datetime import (datetime)
from collections import namedtuple
from flask import current_app
from notifications_utils.recipients import (
@@ -354,3 +355,27 @@ def get_template_class(template_type):
# since we don't need rendering capabilities (we only need to extract placeholders) both email and letter can
# use the same base template
return WithSubjectTemplate
@notify_celery.task(bind=True, name='update-letter-notifications-statuses')
@statsd(namespace="tasks")
def update_letter_notifications_statuses(self, filename):
bucket_location = '{}-ftp'.format(current_app.config['NOTIFY_EMAIL_DOMAIN'])
response_file = s3.get_s3_file(bucket_location, filename)
try:
NotificationUpdate = namedtuple('NotificationUpdate', ['reference', 'status', 'page_count', 'cost_threshold'])
notification_updates = [NotificationUpdate(*line.split('|')) for line in response_file.splitlines()]
except TypeError:
current_app.logger.exception('DVLA response file: {} has an invalid format'.format(filename))
raise
else:
if notification_updates:
for update in notification_updates:
current_app.logger.info('DVLA update: {}'.format(str(update)))
# TODO: Update notifications with desired status
return notification_updates
else:
current_app.logger.exception('DVLA response file contained no updates')

View File

@@ -89,6 +89,7 @@ class Config(object):
PASSWORD_RESET_TEMPLATE_ID = '474e9242-823b-4f99-813d-ed392e7f1201'
ALREADY_REGISTERED_EMAIL_TEMPLATE_ID = '0880fbb1-a0c6-46f0-9a8e-36c986381ceb'
CHANGE_EMAIL_CONFIRMATION_TEMPLATE_ID = 'eb4d9930-87ab-4aef-9bce-786762687884'
SERVICE_NOW_LIVE_TEMPLATE_ID = '618185c6-3636-49cd-b7d2-6f6f5eb3bdde'
BROKER_URL = 'sqs://'
BROKER_TRANSPORT_OPTIONS = {

View File

@@ -384,3 +384,12 @@ def dao_suspend_service(service_id):
def dao_resume_service(service_id):
service = Service.query.get(service_id)
service.active = True
def dao_fetch_active_users_for_service(service_id):
query = User.query.filter(
User.user_to_service.any(id=service_id),
User.state == 'active'
)
return query.all()

View File

@@ -31,6 +31,18 @@ from app import (
from app.history_meta import Versioned
from app.utils import get_utc_time_in_bst
SMS_TYPE = 'sms'
EMAIL_TYPE = 'email'
LETTER_TYPE = 'letter'
TEMPLATE_TYPES = [SMS_TYPE, EMAIL_TYPE, LETTER_TYPE]
template_types = db.Enum(*TEMPLATE_TYPES, name='template_type')
NORMAL = 'normal'
PRIORITY = 'priority'
TEMPLATE_PROCESS_TYPE = [NORMAL, PRIORITY]
def filter_null_value_fields(obj):
return dict(
@@ -183,6 +195,30 @@ class Service(db.Model, Versioned):
)
INTERNATIONAL_SMS_TYPE = 'international_sms'
INCOMING_SMS_TYPE = 'incoming_sms'
SERVICE_PERMISSION_TYPES = [EMAIL_TYPE, SMS_TYPE, LETTER_TYPE, INTERNATIONAL_SMS_TYPE, INCOMING_SMS_TYPE]
class ServicePermissionTypes(db.Model):
__tablename__ = 'service_permission_types'
name = db.Column(db.String(255), primary_key=True)
class ServicePermission(db.Model):
__tablename__ = "service_permissions"
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'),
primary_key=True, index=True, nullable=False)
service = db.relationship('Service')
permission = db.Column(db.String(255), db.ForeignKey('service_permission_types.name'),
index=True, primary_key=True, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, nullable=True, onupdate=datetime.datetime.utcnow)
MOBILE_TYPE = 'mobile'
EMAIL_TYPE = 'email'
@@ -293,19 +329,6 @@ class TemplateProcessTypes(db.Model):
name = db.Column(db.String(255), primary_key=True)
SMS_TYPE = 'sms'
EMAIL_TYPE = 'email'
LETTER_TYPE = 'letter'
TEMPLATE_TYPES = [SMS_TYPE, EMAIL_TYPE, LETTER_TYPE]
template_types = db.Enum(*TEMPLATE_TYPES, name='template_type')
NORMAL = 'normal'
PRIORITY = 'priority'
TEMPLATE_PROCESS_TYPE = [NORMAL, PRIORITY]
class Template(db.Model):
__tablename__ = 'templates'

View File

@@ -1,39 +1,57 @@
from datetime import datetime
from functools import wraps
from flask import (
Blueprint,
jsonify,
request,
current_app,
json
current_app
)
from app import statsd_client
from app.clients.email.aws_ses import get_aws_responses
from app.dao import (
notifications_dao
)
from app.celery.tasks import update_letter_notifications_statuses
from app.v2.errors import register_errors
from app.notifications.utils import autoconfirm_subscription
from app.schema_validation import validate
from app.notifications.process_client_response import validate_callback_data
letter_callback_blueprint = Blueprint('notifications_letter_callback', __name__)
from app.errors import (
register_errors,
InvalidRequest
)
register_errors(letter_callback_blueprint)
dvla_sns_callback_schema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "sns callback received on s3 update",
"type": "object",
"title": "dvla internal sns callback",
"properties": {
"Type": {"enum": ["Notification", "SubscriptionConfirmation"]},
"MessageId": {"type": "string"},
"Message": {"type": ["string", "object"]}
},
"required": ["Type", "MessageId", "Message"]
}
def validate_schema(schema):
def decorator(f):
@wraps(f)
def wrapper(*args, **kw):
validate(request.json, schema)
return f(*args, **kw)
return wrapper
return decorator
@letter_callback_blueprint.route('/notifications/letter/dvla', methods=['POST'])
@validate_schema(dvla_sns_callback_schema)
def process_letter_response():
try:
dvla_request = json.loads(request.data)
current_app.logger.info(dvla_request)
return jsonify(
result="success", message="DVLA callback succeeded"
), 200
except ValueError:
error = "DVLA callback failed: invalid json"
raise InvalidRequest(error, status_code=400)
req_json = request.json
if not autoconfirm_subscription(req_json):
# The callback should have one record for an S3 Put Event.
filename = req_json['Message']['Records'][0]['s3']['object']['key']
current_app.logger.info('Received file from DVLA: {}'.format(filename))
current_app.logger.info('DVLA callback: Calling task to update letter notifications')
update_letter_notifications_statuses.apply_async([filename], queue='notify')
return jsonify(
result="success", message="DVLA callback succeeded"
), 200

View File

@@ -13,9 +13,8 @@ from app.clients.email.aws_ses import get_aws_responses
from app.dao import (
notifications_dao
)
from app.notifications.process_client_response import validate_callback_data
from app.notifications.utils import confirm_subscription
from app.notifications.utils import autoconfirm_subscription
ses_callback_blueprint = Blueprint('notifications_ses_callback', __name__)
@@ -32,14 +31,12 @@ def process_ses_response():
try:
ses_request = json.loads(request.data)
if ses_request.get('Type') == 'SubscriptionConfirmation':
current_app.logger.info("SNS subscription confirmation url: {}".format(ses_request['SubscribeURL']))
subscribed_topic = confirm_subscription(ses_request)
if subscribed_topic:
current_app.logger.info("Automatically subscribed to topic: {}".format(subscribed_topic))
return jsonify(
result="success", message="SES callback succeeded"
), 200
subscribed_topic = autoconfirm_subscription(ses_request)
if subscribed_topic:
current_app.logger.info("Automatically subscribed to topic: {}".format(subscribed_topic))
return jsonify(
result="success", message="SES callback succeeded"
), 200
errors = validate_callback_data(data=ses_request, fields=['Message'], client_name=client_name)
if errors:

View File

@@ -16,3 +16,10 @@ def confirm_subscription(confirmation_request):
raise e
return confirmation_request['TopicArn']
def autoconfirm_subscription(req_json):
if req_json.get('Type') == 'SubscriptionConfirmation':
current_app.logger.info("SNS subscription confirmation url: {}".format(req_json['SubscribeURL']))
subscribed_topic = confirm_subscription(req_json)
return subscribed_topic

View File

@@ -46,6 +46,7 @@ from app.errors import (
InvalidRequest, register_errors)
from app.service import statistics
from app.service.utils import get_whitelist_objects
from app.service.sender import send_notification_to_service_users
from app.schemas import (
service_schema,
api_key_schema,
@@ -117,10 +118,25 @@ def create_service():
@service_blueprint.route('/<uuid:service_id>', methods=['POST'])
def update_service(service_id):
fetched_service = dao_fetch_service_by_id(service_id)
# Capture the status change here as Marshmallow changes this later
service_going_live = fetched_service.restricted and not request.get_json().get('restricted', True)
current_data = dict(service_schema.dump(fetched_service).data.items())
current_data.update(request.get_json())
update_dict = service_schema.load(current_data).data
dao_update_service(update_dict)
if service_going_live:
send_notification_to_service_users(
service_id=service_id,
template_id=current_app.config['SERVICE_NOW_LIVE_TEMPLATE_ID'],
personalisation={
'service_name': current_data['name'],
'message_limit': current_data['message_limit']
},
include_user_fields=['name']
)
return jsonify(data=service_schema.dump(fetched_service).data), 200

33
app/service/sender.py Normal file
View File

@@ -0,0 +1,33 @@
from flask import current_app
from app.dao.services_dao import dao_fetch_service_by_id, dao_fetch_active_users_for_service
from app.dao.templates_dao import dao_get_template_by_id
from app.models import EMAIL_TYPE, KEY_TYPE_NORMAL
from app.notifications.process_notifications import persist_notification, send_notification_to_queue
def send_notification_to_service_users(service_id, template_id, personalisation={}, include_user_fields=[]):
template = dao_get_template_by_id(template_id)
service = dao_fetch_service_by_id(service_id)
active_users = dao_fetch_active_users_for_service(service.id)
notify_service = dao_fetch_service_by_id(current_app.config['NOTIFY_SERVICE_ID'])
for user in active_users:
personalisation = _add_user_fields(user, personalisation, include_user_fields)
notification = persist_notification(
template_id=template.id,
template_version=template.version,
recipient=user.email_address if template.template_type == EMAIL_TYPE else user.mobile_number,
service=notify_service,
personalisation=personalisation,
notification_type=template.template_type,
api_key_id=None,
key_type=KEY_TYPE_NORMAL
)
send_notification_to_queue(notification, False, queue='notify')
def _add_user_fields(user, personalisation, fields):
for field in fields:
personalisation[field] = getattr(user, field)
return personalisation

View File

@@ -0,0 +1,53 @@
"""empty message
Revision ID: 0083_add_perm_types_and_svc_perm
Revises: 0082_add_go_live_template
Create Date: 2017-05-12 11:29:32.664811
"""
# revision identifiers, used by Alembic.
revision = '0083_add_perm_types_and_svc_perm'
down_revision = '0082_add_go_live_template'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
service_permission_types = op.create_table('service_permission_types',
sa.Column('name', sa.String(length=255), nullable=False),
sa.PrimaryKeyConstraint('name'))
op.bulk_insert(service_permission_types,
[
{'name': x} for x in {
'letter',
'email',
'sms',
'international_sms',
'incoming_sms'
}
])
op.create_table('service_permissions',
sa.Column('service_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('permission', sa.String(length=255), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['permission'], ['service_permission_types.name'], ),
sa.ForeignKeyConstraint(['service_id'], ['services.id'], ),
sa.PrimaryKeyConstraint('service_id', 'permission'))
op.create_index(op.f('ix_service_permissions_permission'), 'service_permissions', ['permission'], unique=False)
op.create_index(op.f('ix_service_permissions_service_id'), 'service_permissions', ['service_id'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_service_permissions_service_id'), table_name='service_permissions')
op.drop_index(op.f('ix_service_permissions_permission'), table_name='service_permissions')
op.drop_table('service_permissions')
op.drop_table('service_permission_types')
# ### end Alembic commands ###

11
tests/app/aws/test_s3.py Normal file
View File

@@ -0,0 +1,11 @@
from app.aws.s3 import get_s3_file
def test_get_s3_file_makes_correct_call(sample_service, sample_job, mocker):
get_s3_mock = mocker.patch('app.aws.s3.get_s3_object')
get_s3_file('foo-bucket', 'bar-file.txt')
get_s3_mock.assert_called_with(
'foo-bucket',
'bar-file.txt'
)

View File

@@ -12,15 +12,19 @@ from celery.exceptions import Retry
from app import (encryption, DATETIME_FORMAT)
from app.celery import provider_tasks
from app.celery import tasks
from app.celery.tasks import s3, build_dvla_file, create_dvla_file_contents, update_dvla_job_to_error
from app.celery.tasks import (
s3,
build_dvla_file,
create_dvla_file_contents,
update_dvla_job_to_error,
process_job,
process_row,
send_sms,
send_email,
persist_letter,
get_template_class,
update_job_to_sent_to_dvla
update_job_to_sent_to_dvla,
update_letter_notifications_statuses
)
from app.dao import jobs_dao, services_dao
from app.models import (
@@ -34,6 +38,7 @@ from app.models import (
Job)
from tests.app import load_example_csv
from tests.conftest import set_config
from tests.app.conftest import (
sample_service,
sample_template,
@@ -1071,3 +1076,37 @@ def test_update_dvla_job_to_error(sample_letter_template, sample_letter_job):
assert not n.sent_by
assert 'error' == Job.query.filter_by(id=sample_letter_job.id).one().job_status
def test_update_letter_notifications_statuses_raises_for_invalid_format(notify_api, mocker):
invalid_file = 'ref-foo|Sent|1|Unsorted\nref-bar|Sent|2'
mocker.patch('app.celery.tasks.s3.get_s3_file', return_value=invalid_file)
with pytest.raises(TypeError):
update_letter_notifications_statuses(filename='foo.txt')
def test_update_letter_notifications_statuses_calls_with_correct_bucket_location(notify_api, mocker):
s3_mock = mocker.patch('app.celery.tasks.s3.get_s3_object')
with set_config(notify_api, 'NOTIFY_EMAIL_DOMAIN', 'foo.bar'):
update_letter_notifications_statuses(filename='foo.txt')
s3_mock.assert_called_with('{}-ftp'.format(current_app.config['NOTIFY_EMAIL_DOMAIN']), 'foo.txt')
def test_update_letter_notifications_statuses_builds_updates_list(notify_api, mocker):
valid_file = 'ref-foo|Sent|1|Unsorted\nref-bar|Sent|2|Sorted'
mocker.patch('app.celery.tasks.s3.get_s3_file', return_value=valid_file)
updates = update_letter_notifications_statuses(filename='foo.txt')
assert len(updates) == 2
assert updates[0].reference == 'ref-foo'
assert updates[0].status == 'Sent'
assert updates[0].page_count == '1'
assert updates[0].cost_threshold == 'Unsorted'
assert updates[1].reference == 'ref-bar'
assert updates[1].status == 'Sent'
assert updates[1].page_count == '2'
assert updates[1].cost_threshold == 'Sorted'

View File

@@ -24,7 +24,8 @@ from app.dao.services_dao import (
dao_fetch_todays_stats_for_all_services,
fetch_stats_by_date_range_for_all_services,
dao_suspend_service,
dao_resume_service
dao_resume_service,
dao_fetch_active_users_for_service
)
from app.dao.users_dao import save_model_user
from app.models import (
@@ -49,6 +50,7 @@ from app.models import (
KEY_TYPE_TEST
)
from tests.app.db import create_user, create_service
from tests.app.conftest import (
sample_notification as create_notification,
sample_notification_history as create_notification_history,
@@ -783,3 +785,13 @@ def test_fetch_monthly_historical_template_stats_for_service_separates_templates
assert len(result.get('2016-04').keys()) == 2
assert str(template_one.id) in result.get('2016-04').keys()
assert str(template_two.id) in result.get('2016-04').keys()
def test_dao_fetch_active_users_for_service_returns_active_only(notify_db, notify_db_session):
active_user = create_user(email='active@foo.com', state='active')
pending_user = create_user(email='pending@foo.com', state='pending')
service = create_service(user=active_user)
dao_add_user_to_service(service, pending_user)
users = dao_fetch_active_users_for_service(service.id)
assert len(users) == 1

View File

@@ -9,13 +9,14 @@ from app.dao.templates_dao import dao_create_template
from app.dao.services_dao import dao_create_service
def create_user(mobile_number="+447700900986", email="notify@digital.cabinet-office.gov.uk"):
def create_user(mobile_number="+447700900986", email="notify@digital.cabinet-office.gov.uk", state='active'):
data = {
'id': uuid.uuid4(),
'name': 'Test User',
'email_address': email,
'password': 'password',
'mobile_number': mobile_number,
'state': 'active'
'state': state
}
user = User.query.filter_by(email_address=email).first()
if not user:
@@ -24,11 +25,11 @@ def create_user(mobile_number="+447700900986", email="notify@digital.cabinet-off
return user
def create_service(user=None, service_name="Sample service", service_id=None):
def create_service(user=None, service_name="Sample service", service_id=None, restricted=False):
service = Service(
name=service_name,
message_limit=1000,
restricted=False,
restricted=restricted,
email_from=service_name.lower().replace(' ', '.'),
created_by=user or create_user()
)

View File

@@ -10,17 +10,65 @@ from app.dao.notifications_dao import (
get_notification_by_id
)
from app.models import NotificationStatistics
from tests.app.notifications.test_notifications_ses_callback import ses_confirmation_callback
from tests.app.conftest import sample_notification as create_sample_notification
def test_dvla_callback_should_not_need_auth(client):
data = json.dumps({"somekey": "somevalue"})
def test_dvla_callback_returns_400_with_invalid_request(client):
data = json.dumps({"foo": "bar"})
response = client.post(
path='/notifications/letter/dvla',
data=data,
headers=[('Content-Type', 'application/json')])
headers=[('Content-Type', 'application/json')]
)
assert response.status_code == 400
def test_dvla_callback_autoconfirms_subscription(client, mocker):
autoconfirm_mock = mocker.patch('app.notifications.notifications_letter_callback.autoconfirm_subscription')
data = ses_confirmation_callback()
response = client.post(
path='/notifications/letter/dvla',
data=data,
headers=[('Content-Type', 'application/json')]
)
assert response.status_code == 200
assert autoconfirm_mock.called
def test_dvla_callback_autoconfirm_does_not_call_update_letter_notifications_task(client, mocker):
autoconfirm_mock = mocker.patch('app.notifications.notifications_letter_callback.autoconfirm_subscription')
update_task = \
mocker.patch('app.notifications.notifications_letter_callback.update_letter_notifications_statuses.apply_async')
data = ses_confirmation_callback()
response = client.post(
path='/notifications/letter/dvla',
data=data,
headers=[('Content-Type', 'application/json')]
)
assert response.status_code == 200
assert autoconfirm_mock.called
assert not update_task.called
def test_dvla_callback_calls_update_letter_notifications_task(client, mocker):
update_task = \
mocker.patch('app.notifications.notifications_letter_callback.update_letter_notifications_statuses.apply_async')
data = _sample_sns_s3_callback()
response = client.post(
path='/notifications/letter/dvla',
data=data,
headers=[('Content-Type', 'application/json')]
)
assert response.status_code == 200
assert update_task.called
update_task.assert_called_with(['bar.txt'], queue='notify')
def test_firetext_callback_should_not_need_auth(client, mocker):
@@ -458,3 +506,46 @@ def test_firetext_callback_should_record_statsd(client, notify_db, notify_db_ses
def get_notification_stats(service_id):
return NotificationStatistics.query.filter_by(service_id=service_id).one()
def _sample_sns_s3_callback():
return json.dumps({
"SigningCertURL": "foo.pem",
"UnsubscribeURL": "bar",
"Signature": "some-signature",
"Type": "Notification",
"Timestamp": "2016-05-03T08:35:12.884Z",
"SignatureVersion": "1",
"MessageId": "6adbfe0a-d610-509a-9c47-af894e90d32d",
"Subject": "Amazon S3 Notification",
"TopicArn": "sample-topic-arn",
"Message": {
"Records": [{
"eventVersion": "2.0",
"eventSource": "aws:s3",
"awsRegion": "eu-west-1",
"eventTime": "2017-05-03T08:35:12.826Z",
"eventName": "ObjectCreated:Put",
"userIdentity": {"principalId": "some-p-id"},
"requestParameters": {"sourceIPAddress": "8.8.8.8"},
"responseElements": {"x-amz-request-id": "some-req-id", "x-amz-id-2": "some-amz-id"},
"s3": {
"s3SchemaVersion": "1.0",
"configurationId": "some-config-id",
"bucket": {
"name": "some-bucket",
"ownerIdentity": {"principalId": "some-p-id"},
"arn": "some-bucket-arn"
},
"object": {
"key": "bar.txt",
"size": 200,
"eTag": "some-etag",
"versionId": "some-v-id",
"sequencer": "some-seq"
}
}
}
]
}
})

View File

@@ -1658,3 +1658,66 @@ def test_search_for_notification_by_to_field_return_multiple_matches(
assert str(notification1.id) in [n["id"] for n in result["notifications"]]
assert str(notification2.id) in [n["id"] for n in result["notifications"]]
assert str(notification3.id) in [n["id"] for n in result["notifications"]]
def test_update_service_calls_send_notification_as_service_becomes_live(notify_db, notify_db_session, client, mocker):
send_notification_mock = mocker.patch('app.service.rest.send_notification_to_service_users')
restricted_service = create_service(
notify_db,
notify_db_session,
restricted=True
)
data = {
"restricted": False
}
auth_header = create_authorization_header()
resp = client.post(
'service/{}'.format(restricted_service.id),
data=json.dumps(data),
headers=[auth_header],
content_type='application/json'
)
assert resp.status_code == 200
assert send_notification_mock.called
def test_update_service_does_not_call_send_notification_for_live_service(sample_service, client, mocker):
send_notification_mock = mocker.patch('app.service.rest.send_notification_to_service_users')
data = {
"restricted": True
}
auth_header = create_authorization_header()
resp = client.post(
'service/{}'.format(sample_service.id),
data=json.dumps(data),
headers=[auth_header],
content_type='application/json'
)
assert resp.status_code == 200
assert not send_notification_mock.called
def test_update_service_does_not_call_send_notification_when_restricted_not_changed(sample_service, client, mocker):
send_notification_mock = mocker.patch('app.service.rest.send_notification_to_service_users')
data = {
"name": 'Name of service'
}
auth_header = create_authorization_header()
resp = client.post(
'service/{}'.format(sample_service.id),
data=json.dumps(data),
headers=[auth_header],
content_type='application/json'
)
assert resp.status_code == 200
assert not send_notification_mock.called

View File

@@ -0,0 +1,113 @@
import pytest
from flask import current_app
from app.dao.services_dao import dao_add_user_to_service
from app.models import Notification, EMAIL_TYPE, SMS_TYPE
from app.service.sender import send_notification_to_service_users
from tests.app.conftest import (
notify_service as create_notify_service,
sample_service as create_sample_service
)
from tests.app.db import create_template, create_user
@pytest.mark.parametrize('notification_type', [
EMAIL_TYPE,
SMS_TYPE
])
def test_send_notification_to_service_users_persists_notifications_correctly(
notify_db,
notify_db_session,
notification_type,
sample_user,
mocker
):
mocker.patch('app.service.sender.send_notification_to_queue')
create_notify_service(notify_db, notify_db_session)
service = create_sample_service(notify_db, notify_db_session, user=sample_user)
template = create_template(service, template_type=notification_type)
send_notification_to_service_users(service_id=service.id, template_id=template.id)
to = sample_user.email_address if notification_type == EMAIL_TYPE else sample_user.mobile_number
notification = Notification.query.one()
assert Notification.query.count() == 1
assert notification.to == to
assert str(notification.service_id) == current_app.config['NOTIFY_SERVICE_ID']
assert notification.template.id == template.id
assert notification.template.template_type == notification_type
assert notification.notification_type == notification_type
def test_send_notification_to_service_users_sends_to_queue(
notify_db,
notify_db_session,
sample_user,
mocker
):
send_mock = mocker.patch('app.service.sender.send_notification_to_queue')
create_notify_service(notify_db, notify_db_session)
service = create_sample_service(notify_db, notify_db_session, user=sample_user)
template = create_template(service, template_type=EMAIL_TYPE)
send_notification_to_service_users(service_id=service.id, template_id=template.id)
assert send_mock.called
assert send_mock.call_count == 1
def test_send_notification_to_service_users_includes_user_fields_in_personalisation(
notify_db,
notify_db_session,
sample_user,
mocker
):
persist_mock = mocker.patch('app.service.sender.persist_notification')
mocker.patch('app.service.sender.send_notification_to_queue')
create_notify_service(notify_db, notify_db_session)
service = create_sample_service(notify_db, notify_db_session, user=sample_user)
template = create_template(service, template_type=EMAIL_TYPE)
send_notification_to_service_users(
service_id=service.id,
template_id=template.id,
include_user_fields=['name', 'email_address', 'state']
)
persist_call = persist_mock.call_args_list[0][1]
assert len(persist_mock.call_args_list) == 1
assert persist_call['personalisation'] == {
'name': sample_user.name,
'email_address': sample_user.email_address,
'state': sample_user.state,
}
def test_send_notification_to_service_users_sends_to_active_users_only(
notify_db,
notify_db_session,
mocker
):
mocker.patch('app.service.sender.send_notification_to_queue')
create_notify_service(notify_db, notify_db_session)
first_active_user = create_user(email='foo@bar.com', state='active')
second_active_user = create_user(email='foo1@bar.com', state='active')
pending_user = create_user(email='foo2@bar.com', state='pending')
service = create_sample_service(notify_db, notify_db_session, user=first_active_user)
dao_add_user_to_service(service, second_active_user)
dao_add_user_to_service(service, pending_user)
template = create_template(service, template_type=EMAIL_TYPE)
send_notification_to_service_users(service_id=service.id, template_id=template.id)
notifications = Notification.query.all()
assert Notification.query.count() == 2
assert notifications[0].to == first_active_user.email_address
assert notifications[1].to == second_active_user.email_address