diff --git a/app/__init__.py b/app/__init__.py index 6ca701570..c1f3a9741 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,3 +1,4 @@ +import uuid import os import re from flask import request, url_for @@ -51,7 +52,7 @@ def create_app(): application.register_blueprint(user_blueprint, url_prefix='/user') application.register_blueprint(template_blueprint) application.register_blueprint(status_blueprint, url_prefix='/status') - application.register_blueprint(notifications_blueprint, url_prefix='/notifications') + application.register_blueprint(notifications_blueprint) application.register_blueprint(job_blueprint) application.register_blueprint(invite_blueprint) @@ -101,3 +102,7 @@ def email_safe(string): character.lower() if character.isalnum() or character == "." else "" for character in re.sub("\s+", ".", string.strip()) ]) + + +def create_uuid(): + return str(uuid.uuid4()) diff --git a/app/aws/s3.py b/app/aws/s3.py new file mode 100644 index 000000000..ea7df1f31 --- /dev/null +++ b/app/aws/s3.py @@ -0,0 +1,7 @@ +from boto3 import resource + + +def get_job_from_s3(bucket_name, job_id): + s3 = resource('s3') + key = s3.Object(bucket_name, '{}.csv'.format(job_id)) + return key.get()['Body'].read().decode('utf-8') diff --git a/app/celery/tasks.py b/app/celery/tasks.py index 2ab34b3b9..d4913e24d 100644 --- a/app/celery/tasks.py +++ b/app/celery/tasks.py @@ -1,55 +1,125 @@ +from app import create_uuid from app import notify_celery, encryption, firetext_client, aws_ses_client from app.clients.email.aws_ses import AwsSesClientException from app.clients.sms.firetext import FiretextClientException -from app.dao.templates_dao import get_model_templates -from app.dao.notifications_dao import save_notification +from app.dao.templates_dao import dao_get_template_by_id +from app.dao.notifications_dao import dao_create_notification, dao_update_notification +from app.dao.jobs_dao import dao_update_job, dao_get_job_by_id from app.models import Notification from flask import current_app from sqlalchemy.exc import SQLAlchemyError +from app.aws import s3 +from app.csv import get_recipient_from_csv +from datetime import datetime + + +@notify_celery.task(name="process-job") +def process_job(job_id): + start = datetime.utcnow() + job = dao_get_job_by_id(job_id) + job.status = 'in progress' + dao_update_job(job) + + file = s3.get_job_from_s3(job.bucket_name, job_id) + recipients = get_recipient_from_csv(file) + + for recipient in recipients: + encrypted = encryption.encrypt({ + 'template': job.template_id, + 'job': str(job.id), + 'to': recipient + }) + + if job.template.template_type == 'sms': + send_sms.apply_async(( + str(job.service_id), + str(create_uuid()), + encrypted, + str(datetime.utcnow())), + queue='bulk-sms' + ) + + if job.template.template_type == 'email': + send_email.apply_async(( + str(job.service_id), + str(create_uuid()), + job.template.subject, + "{}@{}".format(job.service.email_from, current_app.config['NOTIFY_EMAIL_DOMAIN']), + encrypted, + str(datetime.utcnow())), + queue='bulk-email') + + finished = datetime.utcnow() + job.status = 'finished' + job.processing_started = start + job.processing_finished = finished + dao_update_job(job) + current_app.logger.info( + "Job {} created at {} started at {} finished at {}".format(job_id, job.created_at, start, finished) + ) @notify_celery.task(name="send-sms") -def send_sms(service_id, notification_id, encrypted_notification): +def send_sms(service_id, notification_id, encrypted_notification, created_at): notification = encryption.decrypt(encrypted_notification) - template = get_model_templates(notification['template']) + template = dao_get_template_by_id(notification['template']) + + client = firetext_client try: + sent_at = datetime.utcnow() notification_db_object = Notification( id=notification_id, template_id=notification['template'], to=notification['to'], service_id=service_id, - status='sent' + job_id=notification.get('job', None), + status='sent', + created_at=created_at, + sent_at=sent_at, + sent_by=client.get_name() + ) - save_notification(notification_db_object) + dao_create_notification(notification_db_object) try: - firetext_client.send_sms(notification['to'], template.content) + client.send_sms(notification['to'], template.content) except FiretextClientException as e: current_app.logger.debug(e) - save_notification(notification_db_object, {"status": "failed"}) + notification_db_object.status = 'failed' + dao_update_notification(notification_db_object) + current_app.logger.info( + "SMS {} created at {} sent at {}".format(notification_id, created_at, sent_at) + ) except SQLAlchemyError as e: current_app.logger.debug(e) @notify_celery.task(name="send-email") -def send_email(service_id, notification_id, subject, from_address, encrypted_notification): +def send_email(service_id, notification_id, subject, from_address, encrypted_notification, created_at): notification = encryption.decrypt(encrypted_notification) - template = get_model_templates(notification['template']) + template = dao_get_template_by_id(notification['template']) + + client = aws_ses_client try: + sent_at = datetime.utcnow() notification_db_object = Notification( id=notification_id, template_id=notification['template'], to=notification['to'], service_id=service_id, - status='sent' + job_id=notification.get('job', None), + status='sent', + created_at=created_at, + sent_at=sent_at, + sent_by=client.get_name() ) - save_notification(notification_db_object) + dao_create_notification(notification_db_object) try: - aws_ses_client.send_email( + client.send_email( from_address, notification['to'], subject, @@ -57,8 +127,12 @@ def send_email(service_id, notification_id, subject, from_address, encrypted_not ) except AwsSesClientException as e: current_app.logger.debug(e) - save_notification(notification_db_object, {"status": "failed"}) + notification_db_object.status = 'failed' + dao_update_notification(notification_db_object) + current_app.logger.info( + "Email {} created at {} sent at {}".format(notification_id, created_at, sent_at) + ) except SQLAlchemyError as e: current_app.logger.debug(e) diff --git a/app/clients/email/__init__.py b/app/clients/email/__init__.py index 98f0cb901..15f250496 100644 --- a/app/clients/email/__init__.py +++ b/app/clients/email/__init__.py @@ -15,3 +15,6 @@ class EmailClient(Client): def send_email(self, *args, **kwargs): raise NotImplemented('TODO Need to implement.') + + def get_name(self): + raise NotImplemented('TODO Need to implement.') diff --git a/app/clients/email/aws_ses.py b/app/clients/email/aws_ses.py index 133368d84..c1f355b43 100644 --- a/app/clients/email/aws_ses.py +++ b/app/clients/email/aws_ses.py @@ -15,6 +15,10 @@ class AwsSesClient(EmailClient): def init_app(self, region, *args, **kwargs): self._client = boto3.client('ses', region_name=region) super(AwsSesClient, self).__init__(*args, **kwargs) + self.name = 'ses' + + def get_name(self): + return self.name def send_email(self, source, diff --git a/app/clients/sms/__init__.py b/app/clients/sms/__init__.py index 8b83ab8b9..90d63ed0d 100644 --- a/app/clients/sms/__init__.py +++ b/app/clients/sms/__init__.py @@ -15,3 +15,6 @@ class SmsClient(Client): def send_sms(self, *args, **kwargs): raise NotImplemented('TODO Need to implement.') + + def get_name(self): + raise NotImplemented('TODO Need to implement.') diff --git a/app/clients/sms/firetext.py b/app/clients/sms/firetext.py index 4a70947a1..235880e83 100644 --- a/app/clients/sms/firetext.py +++ b/app/clients/sms/firetext.py @@ -21,6 +21,10 @@ class FiretextClient(SmsClient): super(SmsClient, self).__init__(*args, **kwargs) self.api_key = config.config.get('FIRETEXT_API_KEY') self.from_number = config.config.get('FIRETEXT_NUMBER') + self.name = 'firetext' + + def get_name(self): + return self.name def send_sms(self, to, content): diff --git a/app/clients/sms/twilio.py b/app/clients/sms/twilio.py index eb5613878..ca33960fb 100644 --- a/app/clients/sms/twilio.py +++ b/app/clients/sms/twilio.py @@ -22,6 +22,10 @@ class TwilioClient(SmsClient): config.config.get('TWILIO_ACCOUNT_SID'), config.config.get('TWILIO_AUTH_TOKEN')) self.from_number = config.config.get('TWILIO_NUMBER') + self.name = 'twilio' + + def get_name(self): + return self.name def send_sms(self, to, content): try: diff --git a/app/csv.py b/app/csv.py new file mode 100644 index 000000000..db0199a48 --- /dev/null +++ b/app/csv.py @@ -0,0 +1,12 @@ +import csv + + +def get_recipient_from_csv(file_data): + numbers = [] + reader = csv.DictReader( + file_data.splitlines(), + lineterminator='\n', + quoting=csv.QUOTE_NONE) + for i, row in enumerate(reader): + numbers.append(row['to'].replace(' ', '')) + return numbers diff --git a/app/dao/jobs_dao.py b/app/dao/jobs_dao.py index 9cb192af0..38b8466e2 100644 --- a/app/dao/jobs_dao.py +++ b/app/dao/jobs_dao.py @@ -2,24 +2,23 @@ from app import db from app.models import Job -def save_job(job, update_dict={}): - if update_dict: - update_dict.pop('id', None) - update_dict.pop('service', None) - update_dict.pop('template', None) - Job.query.filter_by(id=job.id).update(update_dict) - else: - db.session.add(job) - db.session.commit() +def dao_get_job_by_service_id_and_job_id(service_id, job_id): + return Job.query.filter_by(service_id=service_id, id=job_id).first() -def get_job(service_id, job_id): - return Job.query.filter_by(service_id=service_id, id=job_id).one() - - -def get_jobs_by_service(service_id): +def dao_get_jobs_by_service_id(service_id): return Job.query.filter_by(service_id=service_id).all() -def _get_jobs(): - return Job.query.all() +def dao_get_job_by_id(job_id): + return Job.query.filter_by(id=job_id).first() + + +def dao_create_job(job): + db.session.add(job) + db.session.commit() + + +def dao_update_job(job): + db.session.add(job) + db.session.commit() diff --git a/app/dao/notifications_dao.py b/app/dao/notifications_dao.py index f917f4429..f92ac0393 100644 --- a/app/dao/notifications_dao.py +++ b/app/dao/notifications_dao.py @@ -2,15 +2,13 @@ from app import db from app.models import Notification -def save_notification(notification, update_dict={}): - if update_dict: - update_dict.pop('id', None) - update_dict.pop('job', None) - update_dict.pop('service', None) - update_dict.pop('template', None) - Notification.query.filter_by(id=notification.id).update(update_dict) - else: - db.session.add(notification) +def dao_create_notification(notification): + db.session.add(notification) + db.session.commit() + + +def dao_update_notification(notification): + db.session.add(notification) db.session.commit() diff --git a/app/dao/templates_dao.py b/app/dao/templates_dao.py index 3085c412b..2e854f24d 100644 --- a/app/dao/templates_dao.py +++ b/app/dao/templates_dao.py @@ -3,34 +3,6 @@ from app.models import (Template, Service) from sqlalchemy import asc -def save_model_template(template, update_dict=None): - if update_dict: - update_dict.pop('id', None) - service = update_dict.pop('service') - Template.query.filter_by(id=template.id).update(update_dict) - template.service = service - else: - db.session.add(template) - db.session.commit() - - -def delete_model_template(template): - db.session.delete(template) - db.session.commit() - - -def get_model_templates(template_id=None, service_id=None): - # TODO need better mapping from function params to sql query. - if template_id and service_id: - return Template.query.filter_by( - id=template_id, service_id=service_id).one() - elif template_id: - return Template.query.filter_by(id=template_id).one() - elif service_id: - return Template.query.filter_by(service=Service.query.get(service_id)).all() - return Template.query.all() - - def dao_create_template(template): db.session.add(template) db.session.commit() @@ -45,5 +17,9 @@ def dao_get_template_by_id_and_service_id(template_id, service_id): return Template.query.filter_by(id=template_id, service_id=service_id).first() +def dao_get_template_by_id(template_id): + return Template.query.filter_by(id=template_id).first() + + def dao_get_all_templates_for_service(service_id): return Template.query.filter_by(service=Service.query.get(service_id)).order_by(asc(Template.created_at)).all() diff --git a/app/job/rest.py b/app/job/rest.py index c3e4cf772..0c9e90fe8 100644 --- a/app/job/rest.py +++ b/app/job/rest.py @@ -1,148 +1,81 @@ -import boto3 -import json - from flask import ( Blueprint, jsonify, - request, - current_app + request ) -from sqlalchemy.exc import DataError -from sqlalchemy.orm.exc import NoResultFound - from app.dao.jobs_dao import ( - save_job, - get_job, - get_jobs_by_service + dao_create_job, + dao_get_job_by_service_id_and_job_id, + dao_get_jobs_by_service_id, + dao_update_job ) -from app.dao import notifications_dao +from app.dao.services_dao import ( + dao_fetch_service_by_id +) from app.schemas import ( job_schema, - jobs_schema, - job_schema_load_json, - notification_status_schema, - notifications_status_schema, - notification_status_schema_load_json + jobs_schema ) +from app.celery.tasks import process_job + job = Blueprint('job', __name__, url_prefix='/service//job') - from app.errors import register_errors + register_errors(job) @job.route('/', methods=['GET']) +def get_job_by_service_and_job_id(service_id, job_id): + job = dao_get_job_by_service_id_and_job_id(service_id, job_id) + if not job: + return jsonify(result="error", message="Job {} not found for service {}".format(job_id, service_id)), 404 + data, errors = job_schema.dump(job) + return jsonify(data=data) + + @job.route('', methods=['GET']) -def get_job_for_service(service_id, job_id=None): - if job_id: - try: - job = get_job(service_id, job_id) - data, errors = job_schema.dump(job) - return jsonify(data=data) - except DataError: - return jsonify(result="error", message="Invalid job id"), 400 - except NoResultFound: - return jsonify(result="error", message="Job not found"), 404 - else: - jobs = get_jobs_by_service(service_id) - data, errors = jobs_schema.dump(jobs) - return jsonify(data=data) +def get_jobs_by_service(service_id): + jobs = dao_get_jobs_by_service_id(service_id) + data, errors = jobs_schema.dump(jobs) + return jsonify(data=data) @job.route('', methods=['POST']) def create_job(service_id): - job, errors = job_schema.load(request.get_json()) + + service = dao_fetch_service_by_id(service_id) + if not service: + return jsonify(result="error", message="Service {} not found".format(service_id)), 404 + + data = request.get_json() + data.update({ + "service": service_id + }) + job, errors = job_schema.load(data) if errors: return jsonify(result="error", message=errors), 400 - save_job(job) - _enqueue_job(job) - + dao_create_job(job) + process_job.apply_async([str(job.id)], queue="process-job") return jsonify(data=job_schema.dump(job).data), 201 -@job.route('/', methods=['PUT']) +@job.route('/', methods=['POST']) def update_job(service_id, job_id): + fetched_job = dao_get_job_by_service_id_and_job_id(service_id, job_id) + if not fetched_job: + return jsonify(result="error", message="Job {} not found for service {}".format(job_id, service_id)), 404 - job = get_job(service_id, job_id) - update_dict, errors = job_schema_load_json.load(request.get_json()) + current_data = dict(job_schema.dump(fetched_job).data.items()) + current_data.update(request.get_json()) + + update_dict, errors = job_schema.load(current_data) if errors: return jsonify(result="error", message=errors), 400 - - save_job(job, update_dict=update_dict) - - return jsonify(data=job_schema.dump(job).data), 200 - - -@job.route('//notification', methods=['POST']) -def create_notification_for_job(service_id, job_id): - - # TODO assert service_id == payload service id - # and same for job id - notification, errors = notification_status_schema.load(request.get_json()) - if errors: - return jsonify(result="error", message=errors), 400 - - notifications_dao.save_notification(notification) - - return jsonify(data=notification_status_schema.dump(notification).data), 201 - - -@job.route('//notification', methods=['GET']) -@job.route('//notification/') -def get_notification_for_job(service_id, job_id, notification_id=None): - if notification_id: - try: - notification = notifications_dao.get_notification_for_job(service_id, job_id, notification_id) - data, errors = notification_status_schema.dump(notification) - return jsonify(data=data) - except DataError: - return jsonify(result="error", message="Invalid notification id"), 400 - except NoResultFound: - return jsonify(result="error", message="Notification not found"), 404 - else: - notifications = notifications_dao.get_notifications_for_job(service_id, job_id) - data, errors = notifications_status_schema.dump(notifications) - return jsonify(data=data) - - -@job.route('//notification/', methods=['PUT']) -def update_notification_for_job(service_id, job_id, notification_id): - - notification = notifications_dao.get_notification_for_job(service_id, job_id, notification_id) - update_dict, errors = notification_status_schema_load_json.load(request.get_json()) - - if errors: - return jsonify(result="error", message=errors), 400 - - notifications_dao.save_notification(notification, update_dict=update_dict) - - return jsonify(data=job_schema.dump(notification).data), 200 - - -def _enqueue_job(job): - aws_region = current_app.config['AWS_REGION'] - queue_name = current_app.config['NOTIFY_JOB_QUEUE'] - - queue = boto3.resource('sqs', region_name=aws_region).create_queue(QueueName=queue_name) - data = { - 'id': str(job.id), - 'service': str(job.service.id), - 'template': job.template.id, - 'bucket_name': job.bucket_name, - 'file_name': job.file_name, - 'original_file_name': job.original_file_name - } - job_json = json.dumps(data) - queue.send_message(MessageBody=job_json, - MessageAttributes={'id': {'StringValue': str(job.id), 'DataType': 'String'}, - 'service': {'StringValue': str(job.service.id), 'DataType': 'String'}, - 'template': {'StringValue': str(job.template.id), 'DataType': 'String'}, - 'bucket_name': {'StringValue': job.bucket_name, 'DataType': 'String'}, - 'file_name': {'StringValue': job.file_name, 'DataType': 'String'}, - 'original_file_name': {'StringValue': job.original_file_name, - 'DataType': 'String'}}) + dao_update_job(update_dict) + return jsonify(data=job_schema.dump(update_dict).data), 200 diff --git a/app/models.py b/app/models.py index 8d3ba1ba1..384bac10b 100644 --- a/app/models.py +++ b/app/models.py @@ -163,6 +163,16 @@ class Job(db.Model): onupdate=datetime.datetime.now) status = db.Column(db.Enum(*JOB_STATUS_TYPES, name='job_status_types'), nullable=False, default='pending') notification_count = db.Column(db.Integer, nullable=False) + processing_started = db.Column( + db.DateTime, + index=False, + unique=False, + nullable=True) + processing_finished = db.Column( + db.DateTime, + index=False, + unique=False, + nullable=True) VERIFY_CODE_TYPES = ['email', 'sms'] @@ -217,8 +227,13 @@ class Notification(db.Model): db.DateTime, index=False, unique=False, - nullable=False, - default=datetime.datetime.now) + nullable=False) + sent_at = db.Column( + db.DateTime, + index=False, + unique=False, + nullable=True) + sent_by = db.Column(db.String, nullable=True) updated_at = db.Column( db.DateTime, index=False, diff --git a/app/notifications/rest.py b/app/notifications/rest.py index 3035066f1..97db7c5b4 100644 --- a/app/notifications/rest.py +++ b/app/notifications/rest.py @@ -1,4 +1,4 @@ -import uuid +from datetime import datetime from flask import ( Blueprint, @@ -7,11 +7,9 @@ from flask import ( current_app ) -from app import api_user, encryption -from app.aws_sqs import add_notification_to_queue +from app import api_user, encryption, create_uuid from app.dao import ( templates_dao, - users_dao, services_dao, notifications_dao ) @@ -29,12 +27,11 @@ from app.errors import register_errors register_errors(notifications) - -def create_notification_id(): - return str(uuid.uuid4()) +SMS_NOTIFICATION = 'sms' +EMAIL_NOTIFICATION = 'email' -@notifications.route('/', methods=['GET']) +@notifications.route('/notifications/', methods=['GET']) def get_notifications(notification_id): try: notification = notifications_dao.get_notification(api_user['client'], notification_id) @@ -43,87 +40,68 @@ def get_notifications(notification_id): return jsonify(result="error", message="not found"), 404 -@notifications.route('/sms', methods=['POST']) +@notifications.route('/notifications/sms', methods=['POST']) def create_sms_notification(): - notification, errors = sms_template_notification_schema.load(request.get_json()) - if errors: - return jsonify(result="error", message=errors), 400 - - 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': ['Invalid phone number for restricted service']}), 400 - - notification_id = create_notification_id() - - send_sms.apply_async(( - api_user['client'], - notification_id, - encryption.encrypt(notification)), - queue='sms') - return jsonify({'notification_id': notification_id}), 201 + return send_notification(notification_type=SMS_NOTIFICATION) -@notifications.route('/email', methods=['POST']) +@notifications.route('/notifications/email', methods=['POST']) def create_email_notification(): - notification, errors = email_notification_schema.load(request.get_json()) + return send_notification(notification_type=EMAIL_NOTIFICATION) + + +def send_notification(notification_type): + assert notification_type + + service_id = api_user['client'] + + schema = sms_template_notification_schema if notification_type is SMS_NOTIFICATION else email_notification_schema + + notification, errors = schema.load(request.get_json()) if errors: return jsonify(result="error", message=errors), 400 template = templates_dao.dao_get_template_by_id_and_service_id( template_id=notification['template'], - service_id=api_user['client'] + service_id=service_id ) if not template: - return jsonify(result="error", message={'template': ['Template not found']}), 400 + return jsonify( + result="error", + message={ + 'template': ['Template {} not found for service {}'.format(notification['template'], service_id)] + } + ), 404 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 + if notification_type is SMS_NOTIFICATION: + if notification['to'] not in [user.mobile_number for user in service.users]: + return jsonify( + result="error", message={'to': ['Invalid phone number for restricted service']}), 400 + else: + 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() + notification_id = create_uuid() - 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 - - -@notifications.route('/sms/service/', methods=['POST']) -def create_sms_for_service(service_id): - resp_json = request.get_json() - - notification, errors = sms_template_notification_schema.load(resp_json) - if errors: - return jsonify(result="error", message=errors), 400 - - template_id = notification['template'] - job_id = notification['job'] - - # TODO: job/job_id is in notification and can used to update job status - - # TODO: remove once beta is reading notifications from the queue - template = templates_dao.get_model_templates(template_id) - - if template.service.id != uuid.UUID(service_id): - message = "Invalid template: id {} for service id: {}".format(template.id, service_id) - return jsonify(result="error", message=message), 400 - - notification_id = add_notification_to_queue(service_id, template_id, 'sms', notification) + if notification_type is SMS_NOTIFICATION: + send_sms.apply_async(( + service_id, + notification_id, + encryption.encrypt(notification), + str(datetime.utcnow())), + queue='sms') + else: + send_email.apply_async(( + service_id, + notification_id, + template.subject, + "{}@{}".format(service.email_from, current_app.config['NOTIFY_EMAIL_DOMAIN']), + encryption.encrypt(notification), + str(datetime.utcnow())), + queue='email') return jsonify({'notification_id': notification_id}), 201 diff --git a/app/schemas.py b/app/schemas.py index 9851fc3d1..11ecfb08b 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -108,6 +108,16 @@ class SmsTemplateNotificationSchema(SmsNotificationSchema): job = fields.String() +class JobSmsTemplateNotificationSchema(SmsNotificationSchema): + template = fields.Int(required=True) + job = fields.String(required=True) + + +class JobEmailTemplateNotificationSchema(EmailNotificationSchema): + template = fields.Int(required=True) + job = fields.String(required=True) + + class SmsAdminNotificationSchema(SmsNotificationSchema): content = fields.Str(required=True) @@ -143,12 +153,13 @@ api_keys_schema = ApiKeySchema(many=True) job_schema = JobSchema() job_schema_load_json = JobSchema(load_json=True) jobs_schema = JobSchema(many=True) -# TODO: Remove this schema once the admin app has stopped using the /user/code endpoint old_request_verify_code_schema = OldRequestVerifyCodeSchema() request_verify_code_schema = RequestVerifyCodeSchema() sms_admin_notification_schema = SmsAdminNotificationSchema() sms_template_notification_schema = SmsTemplateNotificationSchema() +job_sms_template_notification_schema = JobSmsTemplateNotificationSchema() email_notification_schema = EmailNotificationSchema() +job_email_template_notification_schema = JobEmailTemplateNotificationSchema() notification_status_schema = NotificationStatusSchema() notifications_status_schema = NotificationStatusSchema(many=True) notification_status_schema_load_json = NotificationStatusSchema(load_json=True) diff --git a/migrations/versions/0022_add_processing_dates.py b/migrations/versions/0022_add_processing_dates.py new file mode 100644 index 000000000..996176b6e --- /dev/null +++ b/migrations/versions/0022_add_processing_dates.py @@ -0,0 +1,26 @@ +"""empty message + +Revision ID: 0022_add_processing_dates +Revises: 0021_add_job_metadata +Create Date: 2016-02-24 17:15:47.457200 + +""" + +# revision identifiers, used by Alembic. +revision = '0022_add_processing_dates' +down_revision = '0022_add_invite_users' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('jobs', sa.Column('processing_finished', sa.DateTime(), nullable=True)) + op.add_column('jobs', sa.Column('processing_started', sa.DateTime(), nullable=True)) + op.add_column('notifications', sa.Column('sent_at', sa.DateTime(), nullable=True)) + + +def downgrade(): + op.drop_column('notifications', 'sent_at') + op.drop_column('jobs', 'processing_started') + op.drop_column('jobs', 'processing_finished') diff --git a/migrations/versions/0023_add_sender.py b/migrations/versions/0023_add_sender.py new file mode 100644 index 000000000..654c765ad --- /dev/null +++ b/migrations/versions/0023_add_sender.py @@ -0,0 +1,26 @@ +"""empty message + +Revision ID: 0023_add_sender +Revises: 0022_add_processing_dates +Create Date: 2016-02-24 17:18:21.942772 + +""" + +# revision identifiers, used by Alembic. +revision = '0023_add_sender' +down_revision = '0022_add_processing_dates' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column('notifications', sa.Column('sent_by', sa.String(), nullable=True)) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_column('notifications', 'sent_by') + ### end Alembic commands ### diff --git a/migrations/versions/0023_drop_token.py b/migrations/versions/0023_drop_token.py index bf6f705d7..0a003d186 100644 --- a/migrations/versions/0023_drop_token.py +++ b/migrations/versions/0023_drop_token.py @@ -1,14 +1,14 @@ """empty message Revision ID: 0023_drop_token -Revises: 0022_add_invite_users +Revises: 0023_add_sender Create Date: 2016-02-24 13:58:04.440296 """ # revision identifiers, used by Alembic. revision = '0023_drop_token' -down_revision = '0022_add_invite_users' +down_revision = '0023_add_sender' from alembic import op import sqlalchemy as sa diff --git a/requirements_for_test.txt b/requirements_for_test.txt index 60ceea35b..dbbcc7ae3 100644 --- a/requirements_for_test.txt +++ b/requirements_for_test.txt @@ -4,4 +4,5 @@ pytest==2.8.1 pytest-mock==0.8.1 pytest-cov==2.2.0 mock==1.0.1 -moto==0.4.19 \ No newline at end of file +moto==0.4.19 +freezegun==0.3.6 diff --git a/scripts/run_celery.sh b/scripts/run_celery.sh index 023b54920..eb0e1805b 100755 --- a/scripts/run_celery.sh +++ b/scripts/run_celery.sh @@ -3,4 +3,4 @@ set -e 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,email +celery -A run_celery.notify_celery worker --loglevel=INFO --logfile=/var/log/notify/application.log --concurrency=4 -Q sms,sms-code,email-code,email,process-job,bulk-sms,bulk-email diff --git a/test_csv_files/email.csv b/test_csv_files/email.csv new file mode 100644 index 000000000..581e64627 --- /dev/null +++ b/test_csv_files/email.csv @@ -0,0 +1,2 @@ +to +test@test.com \ No newline at end of file diff --git a/test_csv_files/empty.csv b/test_csv_files/empty.csv new file mode 100644 index 000000000..bf3efa02f --- /dev/null +++ b/test_csv_files/empty.csv @@ -0,0 +1 @@ +to diff --git a/test_csv_files/multiple_email.csv b/test_csv_files/multiple_email.csv new file mode 100644 index 000000000..2e7bab603 --- /dev/null +++ b/test_csv_files/multiple_email.csv @@ -0,0 +1,12 @@ +to +test1@test.com +test2@test.com +test3@test.com +test4@test.com +test5@test.com +test6@test.com +test7@test.com +test8@test.com +test9@test.com +test0@test.com + diff --git a/test_csv_files/multiple_sms.csv b/test_csv_files/multiple_sms.csv new file mode 100644 index 000000000..224a1a4d7 --- /dev/null +++ b/test_csv_files/multiple_sms.csv @@ -0,0 +1,11 @@ +to ++441234123121 ++441234123122 ++441234123123 ++441234123124 ++441234123125 ++441234123126 ++441234123127 ++441234123128 ++441234123129 ++441234123120 diff --git a/test_csv_files/sms.csv b/test_csv_files/sms.csv new file mode 100644 index 000000000..3776c8832 --- /dev/null +++ b/test_csv_files/sms.csv @@ -0,0 +1,2 @@ +to ++441234123123 \ No newline at end of file diff --git a/tests/app/__init__.py b/tests/app/__init__.py index e69de29bb..75631cf9a 100644 --- a/tests/app/__init__.py +++ b/tests/app/__init__.py @@ -0,0 +1,7 @@ +import os + + +def load_example_csv(file): + file_path = os.path.join("test_csv_files", "{}.csv".format(file)) + with open(file_path) as f: + return f.read() diff --git a/tests/app/celery/test_tasks.py b/tests/app/celery/test_tasks.py index e252cfd09..0c25826d5 100644 --- a/tests/app/celery/test_tasks.py +++ b/tests/app/celery/test_tasks.py @@ -1,13 +1,88 @@ import uuid import pytest from flask import current_app -from app.celery.tasks import (send_sms, send_sms_code, send_email_code, send_email) +from app.celery.tasks import (send_sms, send_sms_code, send_email_code, send_email, process_job) 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.dao import notifications_dao +from app.dao import notifications_dao, jobs_dao from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm.exc import NoResultFound +from app.celery.tasks import s3 +from app.celery import tasks +from tests.app import load_example_csv +from datetime import datetime +from freezegun import freeze_time + + +@freeze_time("2016-01-01 11:09:00.061258") +def test_should_process_sms_job(sample_job, mocker): + mocker.patch('app.celery.tasks.s3.get_job_from_s3', return_value=load_example_csv('sms')) + mocker.patch('app.celery.tasks.send_sms.apply_async') + mocker.patch('app.encryption.encrypt', return_value="something_encrypted") + mocker.patch('app.celery.tasks.create_uuid', return_value="uuid") + + process_job(sample_job.id) + + s3.get_job_from_s3.assert_called_once_with(sample_job.bucket_name, sample_job.id) + tasks.send_sms.apply_async.assert_called_once_with( + (str(sample_job.service_id), + "uuid", + "something_encrypted", + "2016-01-01 11:09:00.061258"), + queue="bulk-sms" + ) + job = jobs_dao.dao_get_job_by_id(sample_job.id) + assert job.status == 'finished' + + +def test_should_not_create_send_task_for_empty_file(sample_job, mocker): + mocker.patch('app.celery.tasks.s3.get_job_from_s3', return_value=load_example_csv('empty')) + mocker.patch('app.celery.tasks.send_sms.apply_async') + + process_job(sample_job.id) + + s3.get_job_from_s3.assert_called_once_with(sample_job.bucket_name, sample_job.id) + job = jobs_dao.dao_get_job_by_id(sample_job.id) + assert job.status == 'finished' + tasks.send_sms.apply_async.assert_not_called + + +@freeze_time("2016-01-01 11:09:00.061258") +def test_should_process_email_job(sample_email_job, mocker): + mocker.patch('app.celery.tasks.s3.get_job_from_s3', return_value=load_example_csv('email')) + mocker.patch('app.celery.tasks.send_email.apply_async') + mocker.patch('app.encryption.encrypt', return_value="something_encrypted") + mocker.patch('app.celery.tasks.create_uuid', return_value="uuid") + + process_job(sample_email_job.id) + + s3.get_job_from_s3.assert_called_once_with(sample_email_job.bucket_name, sample_email_job.id) + tasks.send_email.apply_async.assert_called_once_with( + (str(sample_email_job.service_id), + "uuid", + sample_email_job.template.subject, + "{}@{}".format(sample_email_job.service.email_from, "test.notify.com"), + "something_encrypted", + "2016-01-01 11:09:00.061258"), + queue="bulk-email" + ) + job = jobs_dao.dao_get_job_by_id(sample_email_job.id) + assert job.status == 'finished' + + +def test_should_process_all_sms_job(sample_job, mocker): + mocker.patch('app.celery.tasks.s3.get_job_from_s3', return_value=load_example_csv('multiple_sms')) + mocker.patch('app.celery.tasks.send_sms.apply_async') + mocker.patch('app.encryption.encrypt', return_value="something_encrypted") + mocker.patch('app.celery.tasks.create_uuid', return_value="uuid") + + process_job(sample_job.id) + + s3.get_job_from_s3.assert_called_once_with(sample_job.bucket_name, sample_job.id) + tasks.send_sms.apply_async.call_count == 10 + job = jobs_dao.dao_get_job_by_id(sample_job.id) + assert job.status == 'finished' def test_should_send_template_to_correct_sms_provider_and_persist(sample_template, mocker): @@ -17,13 +92,16 @@ def test_should_send_template_to_correct_sms_provider_and_persist(sample_templat } mocker.patch('app.encryption.decrypt', return_value=notification) mocker.patch('app.firetext_client.send_sms') + mocker.patch('app.firetext_client.get_name', return_value="firetext") notification_id = uuid.uuid4() - + now = datetime.utcnow() send_sms( sample_template.service_id, notification_id, - "encrypted-in-reality") + "encrypted-in-reality", + now + ) firetext_client.send_sms.assert_called_once_with("+441234123123", sample_template.content) persisted_notification = notifications_dao.get_notification(sample_template.service_id, notification_id) @@ -31,24 +109,60 @@ def test_should_send_template_to_correct_sms_provider_and_persist(sample_templat assert persisted_notification.to == '+441234123123' assert persisted_notification.template_id == sample_template.id assert persisted_notification.status == 'sent' + assert persisted_notification.created_at == now + assert persisted_notification.sent_at > now + assert persisted_notification.sent_by == 'firetext' + assert not persisted_notification.job_id -def test_should_send_template_to_email_provider_and_persist(sample_email_template, mocker): +def test_should_send_template_to_correct_sms_provider_and_persist_with_job_id(sample_job, mocker): + notification = { + "template": sample_job.template.id, + "job": sample_job.id, + "to": "+441234123123" + } + mocker.patch('app.encryption.decrypt', return_value=notification) + mocker.patch('app.firetext_client.send_sms') + mocker.patch('app.firetext_client.get_name', return_value="firetext") + + notification_id = uuid.uuid4() + now = datetime.utcnow() + send_sms( + sample_job.service.id, + notification_id, + "encrypted-in-reality", + now) + + firetext_client.send_sms.assert_called_once_with("+441234123123", sample_job.template.content) + persisted_notification = notifications_dao.get_notification(sample_job.template.service_id, notification_id) + assert persisted_notification.id == notification_id + assert persisted_notification.to == '+441234123123' + assert persisted_notification.job_id == sample_job.id + assert persisted_notification.template_id == sample_job.template.id + assert persisted_notification.status == 'sent' + assert persisted_notification.sent_at > now + assert persisted_notification.created_at == now + assert persisted_notification.sent_by == 'firetext' + + +def test_should_use_email_template_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') + mocker.patch('app.aws_ses_client.get_name', return_value='ses') notification_id = uuid.uuid4() - + now = datetime.utcnow() send_email( sample_email_template.service_id, notification_id, 'subject', 'email_from', - "encrypted-in-reality") + "encrypted-in-reality", + now) aws_ses_client.send_email.assert_called_once_with( "email_from", @@ -60,7 +174,10 @@ def test_should_send_template_to_email_provider_and_persist(sample_email_templat 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.created_at == now + assert persisted_notification.sent_at > now assert persisted_notification.status == 'sent' + assert persisted_notification.sent_by == 'ses' def test_should_persist_notification_as_failed_if_sms_client_fails(sample_template, mocker): @@ -70,13 +187,16 @@ def test_should_persist_notification_as_failed_if_sms_client_fails(sample_templa } mocker.patch('app.encryption.decrypt', return_value=notification) mocker.patch('app.firetext_client.send_sms', side_effect=FiretextClientException()) + mocker.patch('app.firetext_client.get_name', return_value="firetext") + now = datetime.utcnow() notification_id = uuid.uuid4() send_sms( sample_template.service_id, notification_id, - "encrypted-in-reality") + "encrypted-in-reality", + now) firetext_client.send_sms.assert_called_once_with("+441234123123", sample_template.content) persisted_notification = notifications_dao.get_notification(sample_template.service_id, notification_id) @@ -84,6 +204,9 @@ def test_should_persist_notification_as_failed_if_sms_client_fails(sample_templa assert persisted_notification.to == '+441234123123' assert persisted_notification.template_id == sample_template.id assert persisted_notification.status == 'failed' + assert persisted_notification.created_at == now + assert persisted_notification.sent_at > now + assert persisted_notification.sent_by == 'firetext' def test_should_persist_notification_as_failed_if_email_client_fails(sample_email_template, mocker): @@ -93,6 +216,9 @@ def test_should_persist_notification_as_failed_if_email_client_fails(sample_emai } mocker.patch('app.encryption.decrypt', return_value=notification) mocker.patch('app.aws_ses_client.send_email', side_effect=AwsSesClientException()) + mocker.patch('app.aws_ses_client.get_name', return_value="ses") + + now = datetime.utcnow() notification_id = uuid.uuid4() @@ -101,7 +227,8 @@ def test_should_persist_notification_as_failed_if_email_client_fails(sample_emai notification_id, 'subject', 'email_from', - "encrypted-in-reality") + "encrypted-in-reality", + now) aws_ses_client.send_email.assert_called_once_with( "email_from", @@ -114,6 +241,9 @@ def test_should_persist_notification_as_failed_if_email_client_fails(sample_emai assert persisted_notification.to == 'my_email@my_email.com' assert persisted_notification.template_id == sample_email_template.id assert persisted_notification.status == 'failed' + assert persisted_notification.created_at == now + assert persisted_notification.sent_by == 'ses' + assert persisted_notification.sent_at > now def test_should_not_send_sms_if_db_peristance_failed(sample_template, mocker): @@ -124,13 +254,15 @@ def test_should_not_send_sms_if_db_peristance_failed(sample_template, mocker): mocker.patch('app.encryption.decrypt', return_value=notification) mocker.patch('app.firetext_client.send_sms') mocker.patch('app.db.session.add', side_effect=SQLAlchemyError()) + now = datetime.utcnow() notification_id = uuid.uuid4() send_sms( sample_template.service_id, notification_id, - "encrypted-in-reality") + "encrypted-in-reality", + now) firetext_client.send_sms.assert_not_called() with pytest.raises(NoResultFound) as e: @@ -146,6 +278,7 @@ def test_should_not_send_email_if_db_peristance_failed(sample_email_template, mo 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()) + now = datetime.utcnow() notification_id = uuid.uuid4() @@ -154,7 +287,8 @@ def test_should_not_send_email_if_db_peristance_failed(sample_email_template, mo notification_id, 'subject', 'email_from', - "encrypted-in-reality") + "encrypted-in-reality", + now) aws_ses_client.send_email.assert_not_called() with pytest.raises(NoResultFound) as e: @@ -192,7 +326,9 @@ def test_should_send_email_code(mocker): send_email_code(encrypted_verification) - aws_ses_client.send_email.assert_called_once_with(current_app.config['VERIFY_CODE_FROM_EMAIL_ADDRESS'], - verification['to'], - "Verification code", - verification['secret_code']) + aws_ses_client.send_email.assert_called_once_with( + current_app.config['VERIFY_CODE_FROM_EMAIL_ADDRESS'], + verification['to'], + "Verification code", + verification['secret_code'] + ) diff --git a/tests/app/conftest.py b/tests/app/conftest.py index 9c155d3e6..e917efcb9 100644 --- a/tests/app/conftest.py +++ b/tests/app/conftest.py @@ -1,13 +1,13 @@ import pytest - +from datetime import datetime from app import email_safe from app.models import (User, Service, Template, ApiKey, Job, Notification, InvitedUser) 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.templates_dao import save_model_template +from app.dao.templates_dao import dao_create_template from app.dao.api_key_dao import save_model_api_key -from app.dao.jobs_dao import save_job -from app.dao.notifications_dao import save_notification +from app.dao.jobs_dao import dao_create_job +from app.dao.notifications_dao import dao_create_notification from app.dao.invited_user_dao import save_invited_user import uuid @@ -104,7 +104,6 @@ def sample_service(notify_db, user = sample_user(notify_db, notify_db_session) data = { 'name': service_name, - 'users': [], 'limit': 1000, 'active': False, 'restricted': False, @@ -139,7 +138,7 @@ def sample_template(notify_db, 'subject': subject_line }) template = Template(**data) - save_model_template(template) + dao_create_template(template) return template @@ -166,7 +165,7 @@ def sample_email_template( 'subject': subject_line }) template = Template(**data) - save_model_template(template) + dao_create_template(template) return template @@ -205,7 +204,36 @@ def sample_job(notify_db, 'notification_count': 1 } job = Job(**data) - save_job(job) + dao_create_job(job) + return job + + +@pytest.fixture(scope='function') +def sample_email_job(notify_db, + notify_db_session, + service=None, + template=None): + if service is None: + service = sample_service(notify_db, notify_db_session) + if template is None: + template = sample_email_template( + notify_db, + notify_db_session, + service=service) + job_id = uuid.uuid4() + bucket_name = 'service-{}-notify'.format(service.id) + file_name = '{}.csv'.format(job_id) + data = { + 'id': uuid.uuid4(), + 'service_id': service.id, + 'template_id': template.id, + 'bucket_name': bucket_name, + 'file_name': file_name, + 'original_file_name': 'some.csv', + 'notification_count': 1 + } + job = Job(**data) + dao_create_job(job) return job @@ -249,10 +277,11 @@ def sample_notification(notify_db, 'to': to, 'job': job, 'service': service, - 'template': template + 'template': template, + 'created_at': datetime.utcnow() } notification = Notification(**data) - save_notification(notification) + dao_create_notification(notification) return notification diff --git a/tests/app/dao/test_jobs_dao.py b/tests/app/dao/test_jobs_dao.py index be9fa7b9d..0a80ad398 100644 --- a/tests/app/dao/test_jobs_dao.py +++ b/tests/app/dao/test_jobs_dao.py @@ -1,18 +1,16 @@ import uuid -import json from app.dao.jobs_dao import ( - save_job, - get_job, - get_jobs_by_service, - _get_jobs + dao_get_job_by_service_id_and_job_id, + dao_create_job, + dao_update_job, + dao_get_jobs_by_service_id ) from app.models import Job -def test_save_job(notify_db, notify_db_session, sample_template): - +def test_create_job(sample_template): assert Job.query.count() == 0 job_id = uuid.uuid4() @@ -29,39 +27,33 @@ def test_save_job(notify_db, notify_db_session, sample_template): } job = Job(**data) - save_job(job) + dao_create_job(job) assert Job.query.count() == 1 job_from_db = Job.query.get(job_id) assert job == job_from_db -def test_get_job_by_id(notify_db, notify_db_session, sample_job): - job_from_db = get_job(sample_job.service.id, sample_job.id) +def test_get_job_by_id(sample_job): + job_from_db = dao_get_job_by_service_id_and_job_id(sample_job.service.id, sample_job.id) assert sample_job == job_from_db def test_get_jobs_for_service(notify_db, notify_db_session, sample_template): - from tests.app.conftest import sample_job as create_job from tests.app.conftest import sample_service as create_service from tests.app.conftest import sample_template as create_template from tests.app.conftest import sample_user as create_user - one_job = create_job(notify_db, notify_db_session, sample_template.service, - sample_template) + one_job = create_job(notify_db, notify_db_session, sample_template.service, sample_template) - other_user = create_user(notify_db, notify_db_session, - email="test@digital.cabinet-office.gov.uk") - other_service = create_service(notify_db, notify_db_session, - user=other_user, service_name="other service") - other_template = create_template(notify_db, notify_db_session, - service=other_service) - other_job = create_job(notify_db, notify_db_session, service=other_service, - template=other_template) + other_user = create_user(notify_db, notify_db_session, email="test@digital.cabinet-office.gov.uk") + other_service = create_service(notify_db, notify_db_session, user=other_user, service_name="other service") + other_template = create_template(notify_db, notify_db_session, service=other_service) + other_job = create_job(notify_db, notify_db_session, service=other_service, template=other_template) - one_job_from_db = get_jobs_by_service(one_job.service_id) - other_job_from_db = get_jobs_by_service(other_job.service_id) + one_job_from_db = dao_get_jobs_by_service_id(one_job.service_id) + other_job_from_db = dao_get_jobs_by_service_id(other_job.service_id) assert len(one_job_from_db) == 1 assert one_job == one_job_from_db[0] @@ -72,31 +64,12 @@ def test_get_jobs_for_service(notify_db, notify_db_session, sample_template): assert one_job_from_db != other_job_from_db -def test_get_all_jobs(notify_db, notify_db_session, sample_template): - from tests.app.conftest import sample_job as create_job - for i in range(5): - create_job(notify_db, - notify_db_session, - sample_template.service, - sample_template) - jobs_from_db = _get_jobs() - assert len(jobs_from_db) == 5 - - -def test_update_job(notify_db, notify_db_session, sample_job): +def test_update_job(sample_job): assert sample_job.status == 'pending' - update_dict = { - 'id': sample_job.id, - 'service': sample_job.service.id, - 'template': sample_job.template.id, - 'bucket_name': sample_job.bucket_name, - 'file_name': sample_job.file_name, - 'original_file_name': sample_job.original_file_name, - 'status': 'in progress' - } + sample_job.status = 'in progress' - save_job(sample_job, update_dict=update_dict) + dao_update_job(sample_job) job_from_db = Job.query.get(sample_job.id) diff --git a/tests/app/dao/test_notification_dao.py b/tests/app/dao/test_notification_dao.py index 384be5bff..9ba17c727 100644 --- a/tests/app/dao/test_notification_dao.py +++ b/tests/app/dao/test_notification_dao.py @@ -1,26 +1,27 @@ from app.models import Notification - +from datetime import datetime from app.dao.notifications_dao import ( - save_notification, + dao_create_notification, + dao_update_notification, get_notification, get_notification_for_job, get_notifications_for_job ) -def test_save_notification(notify_db, notify_db_session, sample_template, sample_job): +def test_save_notification(sample_template, sample_job): assert Notification.query.count() == 0 - to = '+44709123456' data = { - 'to': to, + 'to': '+44709123456', 'job': sample_job, 'service': sample_template.service, - 'template': sample_template + 'template': sample_template, + 'created_at': datetime.utcnow() } notification = Notification(**data) - save_notification(notification) + dao_create_notification(notification) assert Notification.query.count() == 1 notification_from_db = Notification.query.all()[0] @@ -29,28 +30,30 @@ def test_save_notification(notify_db, notify_db_session, sample_template, sample assert data['job'] == notification_from_db.job assert data['service'] == notification_from_db.service assert data['template'] == notification_from_db.template + assert data['created_at'] == notification_from_db.created_at assert 'sent' == notification_from_db.status -def test_get_notification(notify_db, notify_db_session, sample_notification): +def test_get_notification(sample_notification): notifcation_from_db = get_notification( sample_notification.service.id, sample_notification.id) assert sample_notification == notifcation_from_db -def test_save_notification_no_job_id(notify_db, notify_db_session, sample_template): +def test_save_notification_no_job_id(sample_template): assert Notification.query.count() == 0 to = '+44709123456' data = { 'to': to, 'service': sample_template.service, - 'template': sample_template + 'template': sample_template, + 'created_at': datetime.utcnow() } notification = Notification(**data) - save_notification(notification) + dao_create_notification(notification) assert Notification.query.count() == 1 notification_from_db = Notification.query.all()[0] @@ -61,7 +64,7 @@ def test_save_notification_no_job_id(notify_db, notify_db_session, sample_templa assert 'sent' == notification_from_db.status -def test_get_notification_for_job(notify_db, notify_db_session, sample_notification): +def test_get_notification_for_job(sample_notification): notifcation_from_db = get_notification_for_job( sample_notification.service.id, sample_notification.job_id, @@ -83,17 +86,9 @@ def test_get_all_notifications_for_job(notify_db, notify_db_session, sample_job) assert len(notifcations_from_db) == 5 -def test_update_notification(notify_db, notify_db_session, sample_notification): +def test_update_notification(sample_notification): assert sample_notification.status == 'sent' - - update_dict = { - 'id': str(sample_notification.id), - 'service': str(sample_notification.service.id), - 'template': sample_notification.template.id, - 'job': str(sample_notification.job.id), - 'status': 'failed' - } - - save_notification(sample_notification, update_dict=update_dict) + sample_notification.status = 'failed' + dao_update_notification(sample_notification) notification_from_db = Notification.query.get(sample_notification.id) assert notification_from_db.status == 'failed' diff --git a/tests/app/job/test_job_rest.py b/tests/app/job/test_job_rest.py deleted file mode 100644 index 66aa83ef1..000000000 --- a/tests/app/job/test_job_rest.py +++ /dev/null @@ -1,328 +0,0 @@ -import boto3 -import moto -import json -import uuid -from flask import url_for - -from tests import create_authorization_header -from tests.app.conftest import sample_job as create_job - - -def test_get_jobs(notify_api, notify_db, notify_db_session, sample_template): - _setup_jobs(notify_db, notify_db_session, sample_template) - - service_id = sample_template.service.id - - with notify_api.test_request_context(): - with notify_api.test_client() as client: - path = url_for('job.get_job_for_service', service_id=service_id) - auth_header = create_authorization_header(service_id=service_id, - path=path, - method='GET') - response = client.get(path, headers=[auth_header]) - assert response.status_code == 200 - resp_json = json.loads(response.get_data(as_text=True)) - assert len(resp_json['data']) == 5 - - -def test_get_job_with_invalid_id_returns400(notify_api, notify_db, - notify_db_session, - sample_template): - service_id = sample_template.service.id - with notify_api.test_request_context(): - with notify_api.test_client() as client: - path = url_for('job.get_job_for_service', job_id='invalid_id', service_id=service_id) - auth_header = create_authorization_header(service_id=sample_template.service.id, - path=path, - method='GET') - response = client.get(path, headers=[auth_header]) - assert response.status_code == 400 - resp_json = json.loads(response.get_data(as_text=True)) - assert resp_json == {'message': 'Invalid job id', - 'result': 'error'} - - -def test_get_job_with_unknown_id_returns404(notify_api, notify_db, - notify_db_session, - sample_template): - random_id = str(uuid.uuid4()) - service_id = sample_template.service.id - with notify_api.test_request_context(): - with notify_api.test_client() as client: - path = url_for('job.get_job_for_service', job_id=random_id, service_id=service_id) - auth_header = create_authorization_header(service_id=sample_template.service.id, - path=path, - method='GET') - response = client.get(path, headers=[auth_header]) - assert response.status_code == 404 - resp_json = json.loads(response.get_data(as_text=True)) - assert resp_json == {'message': 'Job not found', 'result': 'error'} - - -def test_get_job_by_id(notify_api, notify_db, notify_db_session, - sample_job): - job_id = str(sample_job.id) - service_id = sample_job.service.id - with notify_api.test_request_context(): - with notify_api.test_client() as client: - path = url_for('job.get_job_for_service', job_id=job_id, service_id=service_id) - auth_header = create_authorization_header(service_id=sample_job.service.id, - path=path, - method='GET') - response = client.get(path, headers=[auth_header]) - assert response.status_code == 200 - resp_json = json.loads(response.get_data(as_text=True)) - assert resp_json['data']['id'] == job_id - - -@moto.mock_sqs -def test_create_job(notify_api, notify_db, notify_db_session, sample_template): - job_id = uuid.uuid4() - template_id = sample_template.id - service_id = sample_template.service.id - original_file_name = 'thisisatest.csv' - bucket_name = 'service-{}-notify'.format(service_id) - file_name = '{}.csv'.format(job_id) - data = { - 'id': str(job_id), - 'service': str(service_id), - 'template': template_id, - 'original_file_name': original_file_name, - 'bucket_name': bucket_name, - 'file_name': file_name, - 'notification_count': 1 - } - - with notify_api.test_request_context(): - with notify_api.test_client() as client: - path = url_for('job.create_job', service_id=service_id) - auth_header = create_authorization_header(service_id=sample_template.service.id, - path=path, - method='POST', - request_body=json.dumps(data)) - headers = [('Content-Type', 'application/json'), auth_header] - response = client.post( - path, - data=json.dumps(data), - headers=headers) - assert response.status_code == 201 - - resp_json = json.loads(response.get_data(as_text=True)) - - assert resp_json['data']['id'] == str(job_id) - assert resp_json['data']['service'] == str(service_id) - assert resp_json['data']['template'] == template_id - assert resp_json['data']['original_file_name'] == original_file_name - - boto3.setup_default_session(region_name='eu-west-1') - q = boto3.resource('sqs').get_queue_by_name(QueueName=notify_api.config['NOTIFY_JOB_QUEUE']) - messages = q.receive_messages() - assert len(messages) == 1 - - expected_message = json.loads(messages[0].body) - assert expected_message['id'] == str(job_id) - assert expected_message['service'] == str(service_id) - assert expected_message['template'] == template_id - assert expected_message['bucket_name'] == bucket_name - - -def test_get_update_job_status(notify_api, - notify_db, - notify_db_session, - sample_job): - - assert sample_job.status == 'pending' - - job_id = str(sample_job.id) - service_id = str(sample_job.service.id) - - update_data = { - 'id': job_id, - 'service': service_id, - 'template': sample_job.template.id, - 'bucket_name': sample_job.bucket_name, - 'file_name': sample_job.file_name, - 'original_file_name': sample_job.original_file_name, - 'status': 'in progress', - 'notification_count': 1 - } - - with notify_api.test_request_context(): - with notify_api.test_client() as client: - path = url_for('job.update_job', service_id=service_id, job_id=job_id) - - auth_header = create_authorization_header(service_id=service_id, - path=path, - method='PUT', - request_body=json.dumps(update_data)) - - headers = [('Content-Type', 'application/json'), auth_header] - - response = client.put(path, headers=headers, data=json.dumps(update_data)) - - assert response.status_code == 200 - resp_json = json.loads(response.get_data(as_text=True)) - assert resp_json['data']['status'] == 'in progress' - - -def test_get_notification(notify_api, notify_db, notify_db_session, sample_notification): - - with notify_api.test_request_context(): - with notify_api.test_client() as client: - path = url_for('job.get_notification_for_job', - service_id=sample_notification.service.id, - job_id=sample_notification.job.id, - notification_id=sample_notification.id) - - auth_header = create_authorization_header(service_id=sample_notification.service.id, - path=path, - method='GET') - - headers = [('Content-Type', 'application/json'), auth_header] - response = client.get(path, headers=headers) - resp_json = json.loads(response.get_data(as_text=True)) - - assert str(sample_notification.id) == resp_json['data']['id'] - assert str(sample_notification.service.id) == resp_json['data']['service'] - assert sample_notification.template.id == resp_json['data']['template'] - assert str(sample_notification.job.id) == resp_json['data']['job'] - assert sample_notification.status == resp_json['data']['status'] - - -def test_get_notifications(notify_api, notify_db, notify_db_session, sample_job): - - from tests.app.conftest import sample_notification - for i in range(0, 5): - sample_notification(notify_db, - notify_db_session, - service=sample_job.service, - template=sample_job.template, - job=sample_job) - - service_id = str(sample_job.service.id) - job_id = str(sample_job.id) - - with notify_api.test_request_context(): - with notify_api.test_client() as client: - path = url_for('job.get_notification_for_job', - service_id=service_id, - job_id=job_id) - - auth_header = create_authorization_header(service_id=service_id, - path=path, - method='GET') - - headers = [('Content-Type', 'application/json'), auth_header] - response = client.get(path, headers=headers) - resp_json = json.loads(response.get_data(as_text=True)) - - assert len(resp_json['data']) == 5 - - -def test_add_notification(notify_api, notify_db, notify_db_session, sample_job): - - to = '+44709123456' - data = { - 'to': to, - 'job': str(sample_job.id), - 'service': str(sample_job.service.id), - 'template': sample_job.template.id - } - with notify_api.test_request_context(): - with notify_api.test_client() as client: - path = url_for('job.create_notification_for_job', - service_id=sample_job.service.id, - job_id=sample_job.id) - - auth_header = create_authorization_header(service_id=sample_job.service.id, - path=path, - method='POST', - request_body=json.dumps(data)) - - headers = [('Content-Type', 'application/json'), auth_header] - - response = client.post(path, headers=headers, data=json.dumps(data)) - - resp_json = json.loads(response.get_data(as_text=True)) - - assert resp_json['data']['id'] - assert data['to'] == resp_json['data']['to'] - assert data['service'] == resp_json['data']['service'] - assert data['template'] == resp_json['data']['template'] - assert data['job'] == resp_json['data']['job'] - assert 'sent' == resp_json['data']['status'] - - -def test_add_notification_with_id(notify_api, notify_db, notify_db_session, sample_job): - notification_id = str(uuid.uuid4()) - to = '+44709123456' - data = { - 'id': notification_id, - 'to': to, - 'job': str(sample_job.id), - 'service': str(sample_job.service.id), - 'template': sample_job.template.id - } - with notify_api.test_request_context(): - with notify_api.test_client() as client: - path = url_for('job.create_notification_for_job', - service_id=sample_job.service.id, - job_id=sample_job.id) - - auth_header = create_authorization_header(service_id=sample_job.service.id, - path=path, - method='POST', - request_body=json.dumps(data)) - - headers = [('Content-Type', 'application/json'), auth_header] - - response = client.post(path, headers=headers, data=json.dumps(data)) - - resp_json = json.loads(response.get_data(as_text=True)) - - assert resp_json['data']['id'] == notification_id - assert data['to'] == resp_json['data']['to'] - assert data['service'] == resp_json['data']['service'] - assert data['template'] == resp_json['data']['template'] - assert data['job'] == resp_json['data']['job'] - assert 'sent' == resp_json['data']['status'] - - -def test_update_notification(notify_api, notify_db, notify_db_session, sample_notification): - - assert sample_notification.status == 'sent' - - update_data = { - 'id': str(sample_notification.id), - 'to': sample_notification.to, - 'job': str(sample_notification.job.id), - 'service': str(sample_notification.service.id), - 'template': sample_notification.template.id, - 'status': 'failed' - } - with notify_api.test_request_context(): - with notify_api.test_client() as client: - path = url_for('job.update_notification_for_job', - service_id=sample_notification.service.id, - job_id=sample_notification.job.id, - notification_id=sample_notification.id) - - auth_header = create_authorization_header(service_id=sample_notification.service.id, - path=path, - method='PUT', - request_body=json.dumps(update_data)) - - headers = [('Content-Type', 'application/json'), auth_header] - - response = client.put(path, headers=headers, data=json.dumps(update_data)) - - resp_json = json.loads(response.get_data(as_text=True)) - - assert update_data['id'] == resp_json['data']['id'] - assert 'failed' == resp_json['data']['status'] - - -def _setup_jobs(notify_db, notify_db_session, template, number_of_jobs=5): - for i in range(number_of_jobs): - create_job(notify_db, notify_db_session, service=template.service, - template=template) diff --git a/tests/app/job/test_rest.py b/tests/app/job/test_rest.py new file mode 100644 index 000000000..278366732 --- /dev/null +++ b/tests/app/job/test_rest.py @@ -0,0 +1,226 @@ +import json +import uuid +import app.celery.tasks + +from tests import create_authorization_header +from tests.app.conftest import sample_job as create_job + + +def test_get_jobs(notify_api, notify_db, notify_db_session, sample_template): + _setup_jobs(notify_db, notify_db_session, sample_template) + + service_id = sample_template.service.id + + with notify_api.test_request_context(): + with notify_api.test_client() as client: + path = '/service/{}/job'.format(service_id) + auth_header = create_authorization_header( + service_id=service_id, + path=path, + method='GET') + response = client.get(path, headers=[auth_header]) + assert response.status_code == 200 + resp_json = json.loads(response.get_data(as_text=True)) + assert len(resp_json['data']) == 5 + + +def test_get_job_with_invalid_service_id_returns404(notify_api, sample_api_key, sample_service): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + path = '/service/{}/job'.format(sample_service.id) + auth_header = create_authorization_header( + service_id=sample_service.id, + path=path, + method='GET') + response = client.get(path, headers=[auth_header]) + assert response.status_code == 200 + resp_json = json.loads(response.get_data(as_text=True)) + assert len(resp_json['data']) == 0 + + +def test_get_job_with_invalid_job_id_returns404(notify_api, sample_template): + service_id = sample_template.service.id + with notify_api.test_request_context(): + with notify_api.test_client() as client: + path = '/service/{}/job/{}'.format(service_id, "bad-id") + auth_header = create_authorization_header( + service_id=sample_template.service.id, + path=path, + method='GET') + response = client.get(path, headers=[auth_header]) + assert response.status_code == 404 + resp_json = json.loads(response.get_data(as_text=True)) + print(resp_json) + assert resp_json['result'] == 'error' + assert resp_json['message'] == 'No result found' + + +def test_get_job_with_unknown_id_returns404(notify_api, sample_template): + random_id = str(uuid.uuid4()) + service_id = sample_template.service.id + with notify_api.test_request_context(): + with notify_api.test_client() as client: + path = '/service/{}/job/{}'.format(service_id, random_id) + auth_header = create_authorization_header( + service_id=sample_template.service.id, + path=path, + method='GET') + response = client.get(path, headers=[auth_header]) + assert response.status_code == 404 + resp_json = json.loads(response.get_data(as_text=True)) + assert resp_json == { + 'message': 'Job {} not found for service {}'.format(random_id, service_id), + 'result': 'error' + } + + +def test_get_job_by_id(notify_api, sample_job): + job_id = str(sample_job.id) + service_id = sample_job.service.id + with notify_api.test_request_context(): + with notify_api.test_client() as client: + path = '/service/{}/job/{}'.format(service_id, job_id) + auth_header = create_authorization_header( + service_id=sample_job.service.id, + path=path, + method='GET') + response = client.get(path, headers=[auth_header]) + assert response.status_code == 200 + resp_json = json.loads(response.get_data(as_text=True)) + assert resp_json['data']['id'] == job_id + + +def test_create_job(notify_api, sample_template, mocker): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + mocker.patch('app.celery.tasks.process_job.apply_async') + job_id = uuid.uuid4() + data = { + 'id': str(job_id), + 'service': str(sample_template.service.id), + 'template': sample_template.id, + 'original_file_name': 'thisisatest.csv', + 'bucket_name': 'service-{}-notify'.format(sample_template.service.id), + 'file_name': '{}.csv'.format(job_id), + 'notification_count': 1 + } + path = '/service/{}/job'.format(sample_template.service.id) + auth_header = create_authorization_header( + service_id=sample_template.service.id, + path=path, + method='POST', + request_body=json.dumps(data)) + headers = [('Content-Type', 'application/json'), auth_header] + response = client.post( + path, + data=json.dumps(data), + headers=headers) + assert response.status_code == 201 + + app.celery.tasks.process_job.apply_async.assert_called_once_with( + ([str(job_id)]), + queue="process-job" + ) + + resp_json = json.loads(response.get_data(as_text=True)) + + assert resp_json['data']['id'] == str(job_id) + assert resp_json['data']['service'] == str(sample_template.service.id) + assert resp_json['data']['template'] == sample_template.id + assert resp_json['data']['original_file_name'] == 'thisisatest.csv' + + +def test_create_job_returns_400_if_missing_data(notify_api, sample_template, mocker): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + mocker.patch('app.celery.tasks.process_job.apply_async') + data = { + } + path = '/service/{}/job'.format(sample_template.service.id) + auth_header = create_authorization_header( + service_id=sample_template.service.id, + path=path, + method='POST', + request_body=json.dumps(data)) + headers = [('Content-Type', 'application/json'), auth_header] + response = client.post( + path, + data=json.dumps(data), + headers=headers) + + resp_json = json.loads(response.get_data(as_text=True)) + assert response.status_code == 400 + + app.celery.tasks.process_job.apply_async.assert_not_called() + assert resp_json['result'] == 'error' + assert 'Missing data for required field.' in resp_json['message']['original_file_name'] + assert 'Missing data for required field.' in resp_json['message']['file_name'] + assert 'Missing data for required field.' in resp_json['message']['notification_count'] + assert 'Missing data for required field.' in resp_json['message']['id'] + assert 'Missing data for required field.' in resp_json['message']['bucket_name'] + + +def test_create_job_returns_404_if_missing_service(notify_api, sample_template, mocker): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + mocker.patch('app.celery.tasks.process_job.apply_async') + random_id = str(uuid.uuid4()) + data = {} + path = '/service/{}/job'.format(random_id) + auth_header = create_authorization_header( + service_id=sample_template.service.id, + path=path, + method='POST', + request_body=json.dumps(data)) + headers = [('Content-Type', 'application/json'), auth_header] + response = client.post( + path, + data=json.dumps(data), + headers=headers) + + resp_json = json.loads(response.get_data(as_text=True)) + assert response.status_code == 404 + + app.celery.tasks.process_job.apply_async.assert_not_called() + print(resp_json) + assert resp_json['result'] == 'error' + assert resp_json['message'] == 'Service {} not found'.format(random_id) + + +def test_get_update_job(notify_api, sample_job): + assert sample_job.status == 'pending' + + job_id = str(sample_job.id) + service_id = str(sample_job.service.id) + + update_data = { + 'status': 'in progress' + } + + with notify_api.test_request_context(): + with notify_api.test_client() as client: + path = '/service/{}/job/{}'.format(service_id, job_id) + + auth_header = create_authorization_header( + service_id=service_id, + path=path, + method='POST', + request_body=json.dumps(update_data)) + + headers = [('Content-Type', 'application/json'), auth_header] + + response = client.post(path, headers=headers, data=json.dumps(update_data)) + + resp_json = json.loads(response.get_data(as_text=True)) + assert response.status_code == 200 + assert resp_json['data']['status'] == 'in progress' + + +def _setup_jobs(notify_db, notify_db_session, template, number_of_jobs=5): + for i in range(number_of_jobs): + print(i) + create_job( + notify_db, + notify_db_session, + service=template.service, + template=template) diff --git a/tests/app/notifications/test_rest.py b/tests/app/notifications/test_rest.py index 575243e76..f101cac80 100644 --- a/tests/app/notifications/test_rest.py +++ b/tests/app/notifications/test_rest.py @@ -1,11 +1,11 @@ import uuid import app.celery.tasks -import moto from tests import create_authorization_header from flask import json from app.models import Service -from app.dao.templates_dao import get_model_templates +from app.dao.templates_dao import dao_get_all_templates_for_service from app.dao.services_dao import dao_update_service +from freezegun import freeze_time def test_get_notification_by_id(notify_api, sample_notification): @@ -123,9 +123,10 @@ def test_send_notification_invalid_template_id(notify_api, sample_template, mock json_resp = json.loads(response.get_data(as_text=True)) app.celery.tasks.send_sms.apply_async.assert_not_called() - assert response.status_code == 400 + assert response.status_code == 404 assert len(json_resp['message'].keys()) == 1 - assert 'Template not found' in json_resp['message']['template'] + test_string = 'Template {} not found for service {}'.format(9999, sample_template.service.id) + assert test_string in json_resp['message']['template'] def test_prevents_sending_to_any_mobile_on_restricted_service(notify_api, sample_template, mocker): @@ -170,7 +171,7 @@ def test_should_not_allow_template_from_another_service(notify_api, service_fact service_1 = service_factory.get('service 1', user=sample_user) service_2 = service_factory.get('service 2', user=sample_user) - service_2_templates = get_model_templates(service_id=service_2.id) + service_2_templates = dao_get_all_templates_for_service(service_id=service_2.id) data = { 'to': sample_user.mobile_number, 'template': service_2_templates[0].id @@ -190,10 +191,12 @@ def test_should_not_allow_template_from_another_service(notify_api, service_fact json_resp = json.loads(response.get_data(as_text=True)) app.celery.tasks.send_sms.apply_async.assert_not_called() - assert response.status_code == 400 - assert 'Template not found' in json_resp['message']['template'] + assert response.status_code == 404 + test_string = 'Template {} not found for service {}'.format(service_2_templates[0].id, service_1.id) + assert test_string in json_resp['message']['template'] +@freeze_time("2016-01-01 11:09:00.061258") 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: @@ -221,7 +224,8 @@ def test_should_allow_valid_sms_notification(notify_api, sample_template, mocker app.celery.tasks.send_sms.apply_async.assert_called_once_with( (str(sample_template.service_id), notification_id, - "something_encrypted"), + "something_encrypted", + "2016-01-01 11:09:00.061258"), queue="sms" ) assert response.status_code == 201 @@ -302,9 +306,13 @@ def test_should_reject_email_notification_with_template_id_that_cant_be_found( 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 response.status_code == 404 assert data['result'] == 'error' - assert data['message']['template'][0] == 'Template not found' + test_string = 'Template {} not found for service {}'.format( + 1234, + sample_email_template.service.id + ) + assert test_string in data['message']['template'] def test_should_not_allow_email_template_from_another_service(notify_api, service_factory, sample_user, mocker): @@ -315,7 +323,7 @@ def test_should_not_allow_email_template_from_another_service(notify_api, servic 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) + service_2_templates = dao_get_all_templates_for_service(service_id=service_2.id) data = { 'to': sample_user.email_address, @@ -336,8 +344,9 @@ def test_should_not_allow_email_template_from_another_service(notify_api, servic 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'] + assert response.status_code == 404 + test_string = 'Template {} not found for service {}'.format(service_2_templates[0].id, service_1.id) + assert test_string in json_resp['message']['template'] def test_should_not_send_email_if_restricted_and_not_a_service_user(notify_api, sample_email_template, mocker): @@ -371,6 +380,43 @@ def test_should_not_send_email_if_restricted_and_not_a_service_user(notify_api, assert 'Email address not permitted for restricted service' in json_resp['message']['to'] +def test_should_not_send_email_for_job_if_restricted_and_not_a_service_user( + notify_api, + sample_job, + 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_job.template.id, + 'job': sample_job.id + } + + auth_header = create_authorization_header( + service_id=sample_job.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'] + + +@freeze_time("2016-01-01 11:09:00.061258") 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: @@ -400,78 +446,9 @@ def test_should_allow_valid_email_notification(notify_api, sample_email_template notification_id, "Email Subject", "sample.service@test.notify.com", - "something_encrypted"), + "something_encrypted", + "2016-01-01 11:09:00.061258"), queue="email" ) assert response.status_code == 201 assert notification_id - - -@moto.mock_sqs -def test_valid_message_with_service_id(notify_api, - notify_db, - notify_db_session, - sqs_client_conn, - sample_user, - sample_template, - mocker): - with notify_api.test_request_context(): - with notify_api.test_client() as client: - job_id = uuid.uuid4() - service_id = sample_template.service.id - url = '/notifications/sms/service/{}'.format(service_id) - data = { - 'to': '+441234123123', - 'template': sample_template.id, - 'job': job_id - } - auth_header = create_authorization_header( - request_body=json.dumps(data), - path=url, - method='POST') - - response = client.post( - url, - data=json.dumps(data), - headers=[('Content-Type', 'application/json'), auth_header]) - - assert response.status_code == 201 - assert json.loads(response.data)['notification_id'] is not None - - -@moto.mock_sqs -def test_message_with_incorrect_service_id_should_fail(notify_api, - notify_db, - notify_db_session, - sqs_client_conn, - sample_user, - sample_template, - mocker): - with notify_api.test_request_context(): - with notify_api.test_client() as client: - job_id = uuid.uuid4() - invalid_service_id = uuid.uuid4() - - url = '/notifications/sms/service/{}'.format(invalid_service_id) - - data = { - 'to': '+441234123123', - 'template': sample_template.id, - 'job': job_id - } - - auth_header = create_authorization_header( - request_body=json.dumps(data), - path=url, - method='POST') - - response = client.post( - url, - data=json.dumps(data), - headers=[('Content-Type', 'application/json'), auth_header]) - - json_resp = json.loads(response.get_data(as_text=True)) - assert response.status_code == 400 - expected_error = 'Invalid template: id {} for service id: {}'.format(sample_template.id, - invalid_service_id) - assert json_resp['message'] == expected_error diff --git a/tests/app/service/test_rest.py b/tests/app/service/test_rest.py index a006a1945..6e44f68d2 100644 --- a/tests/app/service/test_rest.py +++ b/tests/app/service/test_rest.py @@ -381,8 +381,8 @@ def test_get_users_for_service_returns_empty_list_if_no_users_associated_with_se '/service/{}/users'.format(sample_service.id), headers=[('Content-Type', 'application/json'), auth_header] ) - assert response.status_code == 200 result = json.loads(response.get_data(as_text=True)) + assert response.status_code == 200 assert result['data'] == [] diff --git a/tests/app/test_csv.py b/tests/app/test_csv.py new file mode 100644 index 000000000..df8fdcb70 --- /dev/null +++ b/tests/app/test_csv.py @@ -0,0 +1,28 @@ +from app.csv import get_recipient_from_csv +from tests.app import load_example_csv + + +def test_should_process_single_phone_number_file(): + sms_file = load_example_csv('sms') + len(get_recipient_from_csv(sms_file)) == 1 + assert get_recipient_from_csv(sms_file)[0] == '+441234123123' + + +def test_should_process_multple_phone_number_file_in_order(): + sms_file = load_example_csv('multiple_sms') + len(get_recipient_from_csv(sms_file)) == 10 + assert get_recipient_from_csv(sms_file)[0] == '+441234123121' + assert get_recipient_from_csv(sms_file)[9] == '+441234123120' + + +def test_should_process_single_email_file(): + sms_file = load_example_csv('email') + len(get_recipient_from_csv(sms_file)) == 1 + assert get_recipient_from_csv(sms_file)[0] == 'test@test.com' + + +def test_should_process_multple_email_file_in_order(): + sms_file = load_example_csv('multiple_email') + len(get_recipient_from_csv(sms_file)) == 10 + assert get_recipient_from_csv(sms_file)[0] == 'test1@test.com' + assert get_recipient_from_csv(sms_file)[9] == 'test0@test.com'