diff --git a/app/__init__.py b/app/__init__.py index ad1f44056..e64a1b51e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -18,6 +18,7 @@ db = SQLAlchemy() ma = Marshmallow() notify_alpha_client = NotifyAPIClient() celery = NotifyCelery() + api_user = LocalProxy(lambda: _request_ctx_stack.top.api_user) @@ -31,7 +32,6 @@ def create_app(config_name, config_overrides=None): ma.init_app(application) init_app(application, config_overrides) logging.init_app(application) - notify_alpha_client.init_app(application) celery.init_app(application) @@ -78,7 +78,6 @@ def init_app(app, config_overrides): return response - def convert_to_boolean(value): """Turn strings to bools if they look like them diff --git a/app/authentication/auth.py b/app/authentication/auth.py index 0f4100e6d..8a919fdf6 100644 --- a/app/authentication/auth.py +++ b/app/authentication/auth.py @@ -1,10 +1,11 @@ -from flask import request, jsonify, _request_ctx_stack -from client.authentication import decode_jwt_token, get_token_issuer -from client.errors import TokenDecodeError, TokenRequestError, TokenExpiredError, TokenPayloadError +from flask import request, jsonify, _request_ctx_stack, current_app +from notifications_python_client.authentication import decode_jwt_token, get_token_issuer +from notifications_python_client.errors import TokenDecodeError, TokenRequestError, TokenExpiredError, TokenPayloadError from app.dao.api_key_dao import get_unsigned_secrets def authentication_response(message, code): + current_app.logger.info(message) return jsonify( error=message ), code @@ -27,8 +28,7 @@ def requires_auth(): return authentication_response("Invalid token: signature", 403) if api_client is None: authentication_response("Invalid credentials", 403) - # If the api_client does not have any secrets return response saying that - errors_resp = authentication_response("Invalid token: api client has no secrets", 403) + for secret in api_client['secret']: try: decode_jwt_token( @@ -53,12 +53,16 @@ def requires_auth(): def fetch_client(client): - from flask import current_app if client == current_app.config.get('ADMIN_CLIENT_USER_NAME'): return { "client": client, "secret": [current_app.config.get('ADMIN_CLIENT_SECRET')] } + elif client == current_app.config.get('DELIVERY_CLIENT_USER_NAME'): + return { + "client": client, + "secret": [current_app.config.get('DELIVERY_CLIENT_SECRET')] + } else: return { "client": client, diff --git a/app/aws_sqs.py b/app/aws_sqs.py new file mode 100644 index 000000000..86b28eab8 --- /dev/null +++ b/app/aws_sqs.py @@ -0,0 +1,20 @@ +import uuid +import boto3 +from itsdangerous import URLSafeSerializer +from flask import current_app + + +def add_notification_to_queue(service_id, template_id, type_, notification): + q = boto3.resource( + 'sqs', region_name=current_app.config['AWS_REGION'] + ).create_queue(QueueName="{}_{}".format( + current_app.config['NOTIFICATION_QUEUE_PREFIX'], + str(service_id))) + message_id = str(uuid.uuid4()) + serializer = URLSafeSerializer(current_app.config.get('SECRET_KEY')) + encrypted = serializer.dumps(notification, current_app.config.get('DANGEROUS_SALT')) + q.send_message(MessageBody=encrypted, + MessageAttributes={'type': {'StringValue': type_, 'DataType': 'String'}, + 'message_id': {'StringValue': message_id, 'DataType': 'String'}, + 'service_id': {'StringValue': str(service_id), 'DataType': 'String'}, + 'template_id': {'StringValue': str(template_id), 'DataType': 'String'}}) diff --git a/app/celery/celery.py b/app/celery/celery.py index c98d4367c..183e50bd6 100644 --- a/app/celery/celery.py +++ b/app/celery/celery.py @@ -15,7 +15,3 @@ class NotifyCelery(Celery): with app.app_context(): return TaskBase.__call__(self, *args, **kwargs) self.Task = ContextTask - - - - diff --git a/app/dao/jobs_dao.py b/app/dao/jobs_dao.py index d826c822c..9cb192af0 100644 --- a/app/dao/jobs_dao.py +++ b/app/dao/jobs_dao.py @@ -2,8 +2,14 @@ from app import db from app.models import Job -def save_job(job): - db.session.add(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() diff --git a/app/dao/notifications_dao.py b/app/dao/notifications_dao.py new file mode 100644 index 000000000..ed3cd0f6d --- /dev/null +++ b/app/dao/notifications_dao.py @@ -0,0 +1,22 @@ +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) + db.session.commit() + + +def get_notification(service_id, job_id, notification_id): + return Notification.query.filter_by(service_id=service_id, job_id=job_id, id=notification_id).one() + + +def get_notifications(service_id, job_id): + return Notification.query.filter_by(service_id=service_id, job_id=job_id).all() diff --git a/app/dao/templates_dao.py b/app/dao/templates_dao.py index 42ef387fb..d6557f29d 100644 --- a/app/dao/templates_dao.py +++ b/app/dao/templates_dao.py @@ -23,7 +23,6 @@ def delete_model_template(template): def get_model_templates(template_id=None, service_id=None): - temp = Template.query.first() # TODO need better mapping from function params to sql query. if template_id and service_id: return Template.query.filter_by( diff --git a/app/job/rest.py b/app/job/rest.py index 975b2eb9e..17f1e341c 100644 --- a/app/job/rest.py +++ b/app/job/rest.py @@ -17,9 +17,19 @@ from app.dao.jobs_dao import ( get_jobs_by_service ) +from app.dao.notifications_dao import ( + save_notification, + get_notification, + get_notifications +) + from app.schemas import ( job_schema, - jobs_schema + jobs_schema, + job_schema_load_json, + notification_status_schema, + notifications_status_schema, + notification_status_schema_load_json ) job = Blueprint('job', __name__, url_prefix='/service//job') @@ -56,12 +66,88 @@ def create_job(service_id): return jsonify(data=job_schema.dump(job).data), 201 +@job.route('/', methods=['PUT']) +def update_job(service_id, job_id): + + job = get_job(service_id, job_id) + update_dict, errors = job_schema_load_json.load(request.get_json()) + if errors: + return jsonify(result="error", message=errors), 400 + try: + save_job(job, update_dict=update_dict) + except Exception as e: + return jsonify(result="error", message=str(e)), 400 + 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 + try: + save_notification(notification) + except Exception as e: + return jsonify(result="error", message=str(e)), 500 + 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 = get_notification(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 = get_notifications(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 = get_notification(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 + try: + save_notification(notification, update_dict=update_dict) + except Exception as e: + return jsonify(result="error", message=str(e)), 400 + + 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) - job_json = json.dumps({'job_id': str(job.id), 'service_id': str(job.service.id)}) + 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={'job_id': {'StringValue': str(job.id), 'DataType': 'String'}, - 'service_id': {'StringValue': str(job.service.id), 'DataType': 'String'}}) + 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'}}) diff --git a/app/models.py b/app/models.py index dc260836b..4b53483dc 100644 --- a/app/models.py +++ b/app/models.py @@ -189,3 +189,34 @@ class VerifyCode(db.Model): def check_code(self, cde): return check_hash(cde, self._code) + + +NOTIFICATION_STATUS_TYPES = ['sent', 'failed'] + + +class Notification(db.Model): + + __tablename__ = 'notifications' + + id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + to = db.Column(db.String, nullable=False) + job_id = db.Column(UUID(as_uuid=True), db.ForeignKey('jobs.id'), index=True, unique=False) + job = db.relationship('Job', backref=db.backref('notifications', lazy='dynamic')) + service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), index=True, unique=False) + service = db.relationship('Service') + template_id = db.Column(db.BigInteger, db.ForeignKey('templates.id'), index=True, unique=False) + template = db.relationship('Template') + created_at = db.Column( + db.DateTime, + index=False, + unique=False, + nullable=False, + default=datetime.datetime.now) + updated_at = db.Column( + db.DateTime, + index=False, + unique=False, + nullable=True, + onupdate=datetime.datetime.now) + status = db.Column( + db.Enum(*NOTIFICATION_STATUS_TYPES, name='notification_status_types'), nullable=False, default='sent') diff --git a/app/notifications/rest.py b/app/notifications/rest.py index 1fe16e338..0c3cdf569 100644 --- a/app/notifications/rest.py +++ b/app/notifications/rest.py @@ -1,165 +1,71 @@ -import json +import uuid -import boto3 from flask import ( Blueprint, jsonify, - request, - current_app + request ) -from itsdangerous import URLSafeSerializer -from app import notify_alpha_client -from app import api_user -from app.dao import (templates_dao, services_dao) -import re -from app import celery -mobile_regex = re.compile("^\\+44[\\d]{10}$") +from app import api_user +from app.aws_sqs import add_notification_to_queue +from app.dao import (templates_dao) +from app.schemas import ( + email_notification_schema, sms_template_notification_schema) notifications = Blueprint('notifications', __name__) @notifications.route('/', methods=['GET']) def get_notifications(notification_id): - return jsonify(notify_alpha_client.fetch_notification_by_id(notification_id)), 200 - - -@celery.task(name="make-sms", bind="True") -def send_sms(self, to, template): - print('Executing task id {0.id}, args: {0.args!r} kwargs: {0.kwargs!r}'.format(self.request)) - from time import sleep - sleep(0.5) - print('finished') - #notify_alpha_client.send_sms(mobile_number=to, message=template) + # TODO return notification id details + return jsonify({'id': notification_id}), 200 @notifications.route('/sms', methods=['POST']) def create_sms_notification(): - notification = request.get_json()['notification'] - errors = {} - to, to_errors = validate_to(notification) - if to_errors['to']: - errors.update(to_errors) + resp_json = request.get_json() - # TODO: should create a different endpoint for the admin client to send verify codes. - if api_user['client'] == current_app.config.get('ADMIN_CLIENT_USER_NAME'): - content, content_errors = validate_content_for_admin_client(notification) - if content_errors['content']: - errors.update(content_errors) - if errors: - return jsonify(result="error", message=errors), 400 + notification, errors = sms_template_notification_schema.load(resp_json) + if errors: + return jsonify(result="error", message=errors), 400 - return jsonify(notify_alpha_client.send_sms(mobile_number=to, message=content)), 200 - - else: - to, restricted_errors = validate_to_for_service(to, api_user['client']) - if restricted_errors['restricted']: - errors.update(restricted_errors) - - template, template_errors = validate_template(notification, api_user['client']) - if template_errors['template']: - errors.update(template_errors) - if errors: - return jsonify(result="error", message=errors), 400 - - # add notification to the queue - service = services_dao.get_model_services(api_user['client'], _raise=False) - #_add_notification_to_queue(template.id, service, 'sms', to) - send_sms.apply_async((to, template.content), queue=str(service.id)) - return jsonify(success=True) # notify_alpha_client.send_sms(mobile_number=to, message=template.content)), 200 + add_notification_to_queue(api_user['client'], notification['template'], 'sms', notification) + # TODO data to be returned + return jsonify({}), 204 @notifications.route('/email', methods=['POST']) def create_email_notification(): - notification = request.get_json()['notification'] - errors = {} - for k in ['to', 'from', 'subject', 'message']: - k_error = validate_required_and_something(notification, k) - if k_error: - errors.update(k_error) + resp_json = request.get_json() + notification, errors = email_notification_schema.load(resp_json) + if errors: + return jsonify(result="error", message=errors), 400 + add_notification_to_queue(api_user['client'], "admin", 'email', notification) + # TODO data to be returned + return jsonify({}), 204 + +@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 - return jsonify(notify_alpha_client.send_email( - notification['to'], - notification['message'], - notification['from'], - notification['subject'])) + template_id = notification['template'] + job_id = notification['job'] + # TODO: job/job_id is in notification and can used to update job status -def validate_to_for_service(mob, service_id): - errors = {"restricted": []} - service = services_dao.get_model_services(service_id=service_id) - if service.restricted: - valid = False - for usr in service.users: - if mob == usr.mobile_number: - valid = True - break - if not valid: - errors['restricted'].append('Invalid phone number for restricted service') - return mob, errors + # 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 -def validate_to(json_body): - errors = {"to": []} - mob = json_body.get('to', None) - if not mob: - errors['to'].append('Required data missing') - else: - if not mobile_regex.match(mob): - errors['to'].append('invalid phone number, must be of format +441234123123') - return mob, errors - - -def validate_template(json_body, service_id): - errors = {"template": []} - template_id = json_body.get('template', None) - template = '' - if not template_id: - errors['template'].append('Required data missing') - else: - try: - template = templates_dao.get_model_templates( - template_id=json_body['template'], - service_id=service_id) - except: - errors['template'].append("Unable to load template.") - return template, errors - - -def validate_content_for_admin_client(json_body): - errors = {"content": []} - content = json_body.get('template', None) - if not content: - errors['content'].append('Required content') - - return content, errors - - -def validate_required_and_something(json_body, field): - errors = [] - if field not in json_body and json_body[field]: - errors.append('Required data for field.') - return {field: errors} if errors else None - - -def _add_notification_to_queue(template_id, service, msg_type, to): - q = boto3.resource('sqs', region_name=current_app.config['AWS_REGION']).create_queue( - QueueName=str(service.id)) - import uuid - message_id = str(uuid.uuid4()) - notification = json.dumps({'message_id': message_id, - 'service_id': str(service.id), - 'to': to, - 'message_type': msg_type, - 'template_id': template_id}) - serializer = URLSafeSerializer(current_app.config.get('SECRET_KEY')) - encrypted = serializer.dumps(notification, current_app.config.get('DANGEROUS_SALT')) - q.send_message(MessageBody=encrypted, - MessageAttributes={'type': {'StringValue': msg_type, 'DataType': 'String'}, - 'message_id': {'StringValue': message_id, 'DataType': 'String'}, - 'service_id': {'StringValue': str(service.id), 'DataType': 'String'}, - 'template_id': {'StringValue': str(template_id), 'DataType': 'String'}}) - + add_notification_to_queue(service_id, template_id, 'sms', notification) + # TODO data to be returned + return jsonify({}), 204 diff --git a/app/schemas.py b/app/schemas.py index 4f2c3e3b8..3a0ab41df 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -1,7 +1,10 @@ +import re from flask_marshmallow.fields import fields from . import ma from . import models -from marshmallow import post_load, ValidationError +from marshmallow import (post_load, ValidationError, validates, validates_schema) + +mobile_regex = re.compile("^\\+44[\\d]{10}$") # TODO I think marshmallow provides a better integration and error handling. @@ -59,12 +62,77 @@ class JobSchema(BaseSchema): class RequestVerifyCodeSchema(ma.Schema): - def verify_code_type(self): - if self not in models.VERIFY_CODE_TYPES: + + code_type = fields.Str(required=True) + to = fields.Str(required=False) + + @validates('code_type') + def validate_code_type(self, code): + if code not in models.VERIFY_CODE_TYPES: raise ValidationError('Invalid code type') - code_type = fields.Str(required=True, validate=verify_code_type) - to = fields.Str(required=False) + +# TODO main purpose to be added later +# when processing templates, template will be +# common for all notifications. +class NotificationSchema(ma.Schema): + pass + + +class SmsNotificationSchema(NotificationSchema): + to = fields.Str(required=True) + + @validates('to') + def validate_to(self, value): + if not mobile_regex.match(value): + raise ValidationError('Invalid phone number, must be of format +441234123123') + + +class SmsTemplateNotificationSchema(SmsNotificationSchema): + template = fields.Int(required=True) + job = fields.String() + + @validates('template') + def validate_template(self, value): + if not models.Template.query.filter_by(id=value).first(): + # TODO is this message consistent with what marshmallow + # would normally produce. + raise ValidationError('Template not found') + + @validates_schema + def validate_schema(self, data): + """ + Validate the to field is valid for this template + """ + template_id = data.get('template', None) + template = models.Template.query.filter_by(id=template_id).first() + if template: + service = template.service + if service.restricted: + valid = False + for usr in service.users: + if data['to'] == usr.mobile_number: + valid = True + break + if not valid: + raise ValidationError('Invalid phone number for restricted service', 'restricted') + + +class SmsAdminNotificationSchema(SmsNotificationSchema): + content = fields.Str(required=True) + + +class EmailNotificationSchema(NotificationSchema): + to_address = fields.Str(load_from="to", dump_to='to', required=True) + from_address = fields.Str(load_from="from", dump_to='from', required=True) + subject = fields.Str(required=True) + body = fields.Str(load_from="message", dump_to='message', required=True) + + +class NotificationStatusSchema(BaseSchema): + + class Meta: + model = models.Notification user_schema = UserSchema() @@ -83,3 +151,9 @@ job_schema = JobSchema() job_schema_load_json = JobSchema(load_json=True) jobs_schema = JobSchema(many=True) request_verify_code_schema = RequestVerifyCodeSchema() +sms_admin_notification_schema = SmsAdminNotificationSchema() +sms_template_notification_schema = SmsTemplateNotificationSchema() +email_notification_schema = EmailNotificationSchema() +notification_status_schema = NotificationStatusSchema() +notifications_status_schema = NotificationStatusSchema(many=True) +notification_status_schema_load_json = NotificationStatusSchema(load_json=True) diff --git a/app/user/rest.py b/app/user/rest.py index b4ee71d1e..0fd28f67a 100644 --- a/app/user/rest.py +++ b/app/user/rest.py @@ -1,8 +1,9 @@ from datetime import datetime -from flask import (jsonify, request, abort) +from flask import (jsonify, request, abort, Blueprint) from sqlalchemy.exc import DataError from sqlalchemy.orm.exc import NoResultFound from app.dao.services_dao import get_model_services +from app.aws_sqs import add_notification_to_queue from app.dao.users_dao import ( get_model_users, save_model_user, @@ -16,8 +17,7 @@ from app.dao.users_dao import ( from app.schemas import ( user_schema, users_schema, service_schema, services_schema, request_verify_code_schema, user_schema_load_json) -from app import notify_alpha_client -from flask import Blueprint +from app import api_user user = Blueprint('user', __name__) @@ -133,20 +133,18 @@ def send_user_code(user_id): from app.dao.users_dao import create_secret_code secret_code = create_secret_code() create_user_code(user, secret_code, verify_code.get('code_type')) - # TODO this will need to fixed up when we stop using - # notify_alpha_client if verify_code.get('code_type') == 'sms': mobile = user.mobile_number if verify_code.get('to', None) is None else verify_code.get('to') - notify_alpha_client.send_sms( - mobile_number=mobile, - message=secret_code) + notification = {'to': mobile, 'content': secret_code} + add_notification_to_queue(api_user['client'], 'admin', 'sms', notification) elif verify_code.get('code_type') == 'email': email = user.email_address if verify_code.get('to', None) is None else verify_code.get('to') - notify_alpha_client.send_email( - email, - secret_code, - 'notify@digital.cabinet-office.gov.uk', - 'Verification code') + notification = { + 'to_address': email, + 'from_address': 'notify@digital.cabinet-office.gov.uk', + 'subject': 'Verification code', + 'body': secret_code} + add_notification_to_queue(api_user['client'], 'admin', 'email', notification) else: abort(500) return jsonify({}), 204 diff --git a/config.py b/config.py index eaa62e0ac..61ad93ee5 100644 --- a/config.py +++ b/config.py @@ -14,9 +14,13 @@ class Config(object): NOTIFY_DATA_API_AUTH_TOKEN = os.getenv('NOTIFY_API_TOKEN', "dev-token") ADMIN_CLIENT_USER_NAME = None ADMIN_CLIENT_SECRET = None + DELIVERY_CLIENT_USER_NAME = None + DELIVERY_CLIENT_SECRET = None AWS_REGION = 'eu-west-1' NOTIFY_JOB_QUEUE = os.getenv('NOTIFY_JOB_QUEUE', 'notify-jobs-queue') + # Notification Queue names are a combination of a prefx plus a name + NOTIFICATION_QUEUE_PREFIX = 'notification' BROKER_URL = 'amqp://guest:guest@localhost:5672//' BROKER_TRANSPORT_OPTIONS = { @@ -44,27 +48,32 @@ class Development(Config): DANGEROUS_SALT = 'dangerous-salt' ADMIN_CLIENT_USER_NAME = 'dev-notify-admin' ADMIN_CLIENT_SECRET = 'dev-notify-secret-key' + DELIVERY_CLIENT_USER_NAME = 'dev-notify-delivery' + DELIVERY_CLIENT_SECRET = 'dev-notify-secret-key' + NOTIFICATION_QUEUE_PREFIX = 'notification_development' -class Test(Config): - DEBUG = True +class Test(Development): SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/test_notification_api' - SECRET_KEY = 'secret-key' - DANGEROUS_SALT = 'dangerous-salt' - ADMIN_CLIENT_USER_NAME = 'dev-notify-admin' - ADMIN_CLIENT_SECRET = 'dev-notify-secret-key' + NOTIFICATION_QUEUE_PREFIX = 'notification_test' + + +class Preview(Config): + NOTIFICATION_QUEUE_PREFIX = 'notification_preview' + + +class Staging(Config): + NOTIFICATION_QUEUE_PREFIX = 'notification_staging' class Live(Config): - SECRET_KEY = 'secret-key' - DANGEROUS_SALT = 'dangerous-salt' - ADMIN_CLIENT_USER_NAME = 'dev-notify-admin' - ADMIN_CLIENT_SECRET = 'dev-notify-secret-key' - pass + NOTIFICATION_QUEUE_PREFIX = 'notification_live' configs = { 'development': Development, + 'preview': Preview, + 'staging': Staging, 'test': Test, 'live': Live, } diff --git a/migrations/versions/0013_add_notifications.py b/migrations/versions/0013_add_notifications.py new file mode 100644 index 000000000..61fd55a66 --- /dev/null +++ b/migrations/versions/0013_add_notifications.py @@ -0,0 +1,47 @@ +"""empty message + +Revision ID: 0013_add_notifications +Revises: 0012_add_status_to_job +Create Date: 2016-02-09 11:14:46.708551 + +""" + +# revision identifiers, used by Alembic. +revision = '0013_add_notifications' +down_revision = '0012_add_status_to_job' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('notifications', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('to', sa.String(), nullable=False), + sa.Column('job_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('service_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('template_id', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('status', sa.Enum('sent', 'failed', name='notification_status_types'), nullable=False), + sa.ForeignKeyConstraint(['job_id'], ['jobs.id'], ), + sa.ForeignKeyConstraint(['service_id'], ['services.id'], ), + sa.ForeignKeyConstraint(['template_id'], ['templates.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_notifications_job_id'), 'notifications', ['job_id'], unique=False) + op.create_index(op.f('ix_notifications_service_id'), 'notifications', ['service_id'], unique=False) + op.create_index(op.f('ix_notifications_template_id'), 'notifications', ['template_id'], unique=False) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_notifications_template_id'), table_name='notifications') + op.drop_index(op.f('ix_notifications_service_id'), table_name='notifications') + op.drop_index(op.f('ix_notifications_job_id'), table_name='notifications') + op.drop_table('notifications') + op.get_bind() + op.execute("drop type notification_status_types") + ### end Alembic commands ### diff --git a/migrations/versions/0014_job_id_nullable.py b/migrations/versions/0014_job_id_nullable.py new file mode 100644 index 000000000..8c27b3ce1 --- /dev/null +++ b/migrations/versions/0014_job_id_nullable.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: 0014_job_id_nullable +Revises: 0013_add_notifications +Create Date: 2016-02-10 10:57:39.414061 + +""" + +# revision identifiers, used by Alembic. +revision = '0014_job_id_nullable' +down_revision = '0013_add_notifications' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.alter_column('notifications', 'job_id', + existing_type=postgresql.UUID(), + nullable=True) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.alter_column('notifications', 'job_id', + existing_type=postgresql.UUID(), + nullable=False) + ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt index 2de23f16b..c36305c94 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,8 +16,6 @@ boto3==1.2.3 celery==3.1.20 redis==2.10.5 -git+https://github.com/alphagov/notifications-python-client.git@0.2.1#egg=notifications-python-client==0.2.1 +git+https://github.com/alphagov/notifications-python-client.git@0.2.6#egg=notifications-python-client==0.2.6 git+https://github.com/alphagov/notifications-utils.git@0.0.3#egg=notifications-utils==0.0.3 - -git+https://github.com/alphagov/notify-api-client.git@0.1.6#egg=notify-api-client==0.1.6 diff --git a/tests/__init__.py b/tests/__init__.py index e76b036be..7491049d4 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,5 +1,5 @@ from flask import current_app -from client.authentication import create_jwt_token +from notifications_python_client.authentication import create_jwt_token from app.dao.api_key_dao import get_unsigned_secrets diff --git a/tests/app/authentication/test_authentication.py b/tests/app/authentication/test_authentication.py index ea6567a8a..18156f9e2 100644 --- a/tests/app/authentication/test_authentication.py +++ b/tests/app/authentication/test_authentication.py @@ -1,6 +1,6 @@ from datetime import datetime, timedelta -from client.authentication import create_jwt_token +from notifications_python_client.authentication import create_jwt_token from flask import json, url_for, current_app from app.dao.api_key_dao import get_unsigned_secrets, save_model_api_key, get_unsigned_secret from app.models import ApiKey, Service @@ -214,6 +214,26 @@ def test_authentication_returns_token_expired_when_service_uses_expired_key_and_ assert data['error'] == 'Invalid token: signature' +def test_authentication_returns_error_when_api_client_has_no_secrets(notify_api, + notify_db, + notify_db_session): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + api_secret = notify_api.config.get('ADMIN_CLIENT_SECRET') + token = create_jwt_token(request_method="GET", + request_path=url_for('service.get_service'), + secret=api_secret, + client_id=notify_api.config.get('ADMIN_CLIENT_USER_NAME') + ) + notify_api.config['ADMIN_CLIENT_SECRET'] = '' + response = client.get(url_for('service.get_service'), + headers={'Authorization': 'Bearer {}'.format(token)}) + assert response.status_code == 403 + error_message = json.loads(response.get_data()) + assert error_message['error'] == 'Invalid token: signature' + notify_api.config['ADMIN_CLIENT_SECRET'] = api_secret + + def __create_get_token(service_id): if service_id: return create_jwt_token(request_method="GET", diff --git a/tests/app/conftest.py b/tests/app/conftest.py index e1abea888..26d5179a9 100644 --- a/tests/app/conftest.py +++ b/tests/app/conftest.py @@ -1,12 +1,13 @@ import pytest from flask import jsonify -from app.models import (User, Service, Template, ApiKey, Job, VerifyCode) +from app.models import (User, Service, Template, ApiKey, Job, VerifyCode, Notification) from app.dao.users_dao import (save_model_user, create_user_code, create_secret_code) from app.dao.services_dao import save_model_service from app.dao.templates_dao import save_model_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 import uuid @@ -155,24 +156,6 @@ def sample_admin_service_id(notify_db, notify_db_session): return admin_service.id -@pytest.fixture(scope='function') -def mock_notify_client_send_sms(mocker): - def _send(mobile_number, message): - pass - - mock_class = mocker.patch('app.notify_alpha_client.send_sms', side_effect=_send) - return mock_class - - -@pytest.fixture(scope='function') -def mock_notify_client_send_email(mocker): - def _send(email_address, message, from_address, subject): - pass - - mock_class = mocker.patch('app.notify_alpha_client.send_email', side_effect=_send) - return mock_class - - @pytest.fixture(scope='function') def mock_secret_code(mocker): def _create(): @@ -180,3 +163,31 @@ def mock_secret_code(mocker): mock_class = mocker.patch('app.dao.users_dao.create_secret_code', side_effect=_create) return mock_class + + +@pytest.fixture(scope='function') +def sample_notification(notify_db, + notify_db_session, + service=None, + template=None, + job=None): + if service is None: + service = sample_service(notify_db, notify_db_session) + if template is None: + template = sample_template(notify_db, notify_db_session, service=service) + if job is None: + job = sample_job(notify_db, notify_db_session, service=service, template=template) + + notificaton_id = uuid.uuid4() + to = '+44709123456' + + data = { + 'id': notificaton_id, + 'to': to, + 'job': job, + 'service': service, + 'template': template + } + notification = Notification(**data) + save_notification(notification) + return notification diff --git a/tests/app/dao/test_jobs_dao.py b/tests/app/dao/test_jobs_dao.py index d4134f30e..06f515fd6 100644 --- a/tests/app/dao/test_jobs_dao.py +++ b/tests/app/dao/test_jobs_dao.py @@ -1,4 +1,5 @@ import uuid +import json from app.dao.jobs_dao import ( save_job, @@ -79,3 +80,23 @@ def test_get_all_jobs(notify_db, notify_db_session, sample_template): sample_template) jobs_from_db = _get_jobs() assert len(jobs_from_db) == 5 + + +def test_update_job(notify_db, notify_db_session, 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' + } + + save_job(sample_job, update_dict=update_dict) + + job_from_db = Job.query.get(sample_job.id) + + assert job_from_db.status == 'in progress' diff --git a/tests/app/dao/test_notification_dao.py b/tests/app/dao/test_notification_dao.py new file mode 100644 index 000000000..a28851520 --- /dev/null +++ b/tests/app/dao/test_notification_dao.py @@ -0,0 +1,90 @@ +from app.models import Notification + +from app.dao.notifications_dao import ( + save_notification, + get_notification, + get_notifications +) + + +def test_save_notification(notify_db, notify_db_session, sample_template, sample_job): + + assert Notification.query.count() == 0 + to = '+44709123456' + data = { + 'to': to, + 'job': sample_job, + 'service': sample_template.service, + 'template': sample_template + } + + notification = Notification(**data) + save_notification(notification) + + assert Notification.query.count() == 1 + notification_from_db = Notification.query.all()[0] + assert notification_from_db.id + assert data['to'] == notification_from_db.to + assert data['job'] == notification_from_db.job + assert data['service'] == notification_from_db.service + assert data['template'] == notification_from_db.template + assert 'sent' == notification_from_db.status + + +def test_save_notification_no_job_id(notify_db, notify_db_session, sample_template): + + assert Notification.query.count() == 0 + to = '+44709123456' + data = { + 'to': to, + 'service': sample_template.service, + 'template': sample_template + } + + notification = Notification(**data) + save_notification(notification) + + assert Notification.query.count() == 1 + notification_from_db = Notification.query.all()[0] + assert notification_from_db.id + assert data['to'] == notification_from_db.to + assert data['service'] == notification_from_db.service + assert data['template'] == notification_from_db.template + assert 'sent' == notification_from_db.status + + +def test_get_notification_for_job(notify_db, notify_db_session, sample_notification): + notifcation_from_db = get_notification(sample_notification.service.id, + sample_notification.job_id, + sample_notification.id) + assert sample_notification == notifcation_from_db + + +def test_get_all_notifications_for_job(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) + + notifcations_from_db = get_notifications(sample_job.service.id, sample_job.id) + assert len(notifcations_from_db) == 5 + + +def test_update_notification(notify_db, notify_db_session, 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) + 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 index b8af8f7e9..a10c004da 100644 --- a/tests/app/job/test_job_rest.py +++ b/tests/app/job/test_job_rest.py @@ -119,8 +119,170 @@ def test_create_job(notify_api, notify_db, notify_db_session, sample_template): assert len(messages) == 1 expected_message = json.loads(messages[0].body) - assert expected_message['job_id'] == str(job_id) - assert expected_message['service_id'] == str(service_id) + 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' + } + + 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_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): diff --git a/tests/app/notifications/test_rest.py b/tests/app/notifications/test_rest.py index e940a8e6e..b506bb10a 100644 --- a/tests/app/notifications/test_rest.py +++ b/tests/app/notifications/test_rest.py @@ -1,9 +1,8 @@ -import boto3 import moto +import uuid from tests import create_authorization_header from flask import url_for, json -from app import notify_alpha_client from app.models import Service @@ -14,18 +13,6 @@ def test_get_notifications( """ with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch( - 'app.notify_alpha_client.fetch_notification_by_id', - return_value={ - 'notifications': [ - { - 'id': 'my_id', - 'notification': 'some notify' - } - ] - } - ) - auth_header = create_authorization_header( service_id=sample_api_key.service_id, path=url_for('notifications.get_notifications', notification_id=123), @@ -35,12 +22,7 @@ def test_get_notifications( url_for('notifications.get_notifications', notification_id=123), headers=[auth_header]) - json_resp = json.loads(response.get_data(as_text=True)) assert response.status_code == 200 - assert len(json_resp['notifications']) == 1 - assert json_resp['notifications'][0]['id'] == 'my_id' - assert json_resp['notifications'][0]['notification'] == 'some notify' - notify_alpha_client.fetch_notification_by_id.assert_called_with("123") def test_get_notifications_empty_result( @@ -50,14 +32,6 @@ def test_get_notifications_empty_result( """ with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch( - 'app.notify_alpha_client.fetch_notification_by_id', - return_value={ - 'notifications': [ - ] - } - ) - auth_header = create_authorization_header( service_id=sample_api_key.service_id, path=url_for('notifications.get_notifications', notification_id=123), @@ -67,10 +41,7 @@ def test_get_notifications_empty_result( url_for('notifications.get_notifications', notification_id=123), headers=[auth_header]) - json_resp = json.loads(response.get_data(as_text=True)) assert response.status_code == 200 - assert len(json_resp['notifications']) == 0 - notify_alpha_client.fetch_notification_by_id.assert_called_with("123") def test_create_sms_should_reject_if_no_phone_numbers( @@ -80,14 +51,8 @@ def test_create_sms_should_reject_if_no_phone_numbers( """ with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch( - 'app.notify_alpha_client.send_sms', - return_value='success' - ) data = { - 'notification': { - 'template': "my message" - } + 'template': "my message" } auth_header = create_authorization_header( service_id=sample_api_key.service_id, @@ -103,8 +68,7 @@ def test_create_sms_should_reject_if_no_phone_numbers( json_resp = json.loads(response.get_data(as_text=True)) assert response.status_code == 400 assert json_resp['result'] == 'error' - assert 'Required data missing' in json_resp['message']['to'][0] - assert not notify_alpha_client.send_sms.called + assert 'Missing data for required field.' in json_resp['message']['to'][0] def test_should_reject_bad_phone_numbers( @@ -114,15 +78,9 @@ def test_should_reject_bad_phone_numbers( """ with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch( - 'app.notify_alpha_client.send_sms', - return_value='success' - ) data = { - 'notification': { - 'to': 'invalid', - 'template': "my message" - } + 'to': 'invalid', + 'template': "my message" } auth_header = create_authorization_header( request_body=json.dumps(data), @@ -137,92 +95,7 @@ def test_should_reject_bad_phone_numbers( json_resp = json.loads(response.get_data(as_text=True)) assert response.status_code == 400 assert json_resp['result'] == 'error' - assert 'invalid phone number, must be of format +441234123123' in json_resp['message']['to'] - assert not notify_alpha_client.send_sms.called - - -def test_should_reject_missing_content( - notify_api, notify_db, notify_db_session, mocker): - """ - Tests GET endpoint '/' to retrieve entire service list. - """ - with notify_api.test_request_context(): - with notify_api.test_client() as client: - mocker.patch( - 'app.notify_alpha_client.send_sms', - return_value='success' - ) - data = { - 'notification': { - 'to': '+441234123123' - } - } - auth_header = create_authorization_header( - request_body=json.dumps(data), - path=url_for('notifications.create_sms_notification'), - method='POST') - - response = client.post( - url_for('notifications.create_sms_notification'), - 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 - assert json_resp['result'] == 'error' - assert 'Required content' in json_resp['message']['content'] - assert not notify_alpha_client.send_sms.called - - -@moto.mock_sqs -def test_send_template_content(notify_api, - notify_db, - notify_db_session, - mocker): - """ - Test POST endpoint '/sms' with service notification. - """ - with notify_api.test_request_context(): - with notify_api.test_client() as client: - set_up_mock_queue() - mobile = '+447719087678' - msg = 'Message content' - mocker.patch( - 'app.notify_alpha_client.send_sms', - return_value={ - "notification": { - "createdAt": "2015-11-03T09:37:27.414363Z", - "id": 100, - "jobId": 65, - "message": msg, - "method": "sms", - "status": "created", - "to": mobile - } - } - ) - data = { - 'notification': { - 'to': mobile, - 'template': msg - } - } - auth_header = create_authorization_header( - request_body=json.dumps(data), - path=url_for('notifications.create_sms_notification'), - method='POST') - - response = client.post( - url_for('notifications.create_sms_notification'), - 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 == 200 - assert json_resp['notification']['id'] == 100 - notify_alpha_client.send_sms.assert_called_with( - mobile_number=mobile, - message=msg) + assert 'Invalid phone number, must be of format +441234123123' in json_resp['message']['to'] def test_send_notification_restrict_mobile(notify_api, @@ -238,19 +111,12 @@ def test_send_notification_restrict_mobile(notify_api, """ with notify_api.test_request_context(): with notify_api.test_client() as client: - Service.query.filter_by( id=sample_template.service.id).update({'restricted': True}) invalid_mob = '+449999999999' - mocker.patch( - 'app.notify_alpha_client.send_sms', - return_value={} - ) data = { - 'notification': { - 'to': invalid_mob, - 'template': sample_template.id - } + 'to': invalid_mob, + 'template': sample_template.id } assert invalid_mob != sample_user.mobile_number auth_header = create_authorization_header( @@ -267,36 +133,61 @@ def test_send_notification_restrict_mobile(notify_api, json_resp = json.loads(response.get_data(as_text=True)) assert response.status_code == 400 assert 'Invalid phone number for restricted service' in json_resp['message']['restricted'] - assert not notify_alpha_client.send_sms.called + + +def test_send_notification_invalid_template_id(notify_api, + notify_db, + notify_db_session, + sample_api_key, + sample_template, + sample_user, + mocker): + """ + Tests POST endpoint '/sms' with notifications-admin notification with invalid template id + """ + with notify_api.test_request_context(): + with notify_api.test_client() as client: + + Service.query.filter_by( + id=sample_template.service.id).update({'restricted': True}) + invalid_mob = '+449999999999' + data = { + 'to': invalid_mob, + 'template': 9999 + } + assert invalid_mob != sample_user.mobile_number + auth_header = create_authorization_header( + service_id=sample_template.service.id, + request_body=json.dumps(data), + path=url_for('notifications.create_sms_notification'), + method='POST') + + response = client.post( + url_for('notifications.create_sms_notification'), + 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 + assert 'Template not found' in json_resp['message']['template'] @moto.mock_sqs -def test_should_allow_valid_message(notify_api, notify_db, notify_db_session, mocker): +def test_should_allow_valid_message(notify_api, + notify_db, + notify_db_session, + sqs_client_conn, + sample_user, + sample_template, + mocker): """ Tests POST endpoint '/sms' with notifications-admin notification. """ with notify_api.test_request_context(): with notify_api.test_client() as client: - set_up_mock_queue() - mocker.patch( - 'app.notify_alpha_client.send_sms', - return_value={ - "notification": { - "createdAt": "2015-11-03T09:37:27.414363Z", - "id": 100, - "jobId": 65, - "message": "valid", - "method": "sms", - "status": "created", - "to": "+449999999999" - } - } - ) data = { - 'notification': { - 'to': '+441234123123', - 'template': 'valid' - } + 'to': '+441234123123', + 'template': sample_template.id } auth_header = create_authorization_header( request_body=json.dumps(data), @@ -308,17 +199,16 @@ def test_should_allow_valid_message(notify_api, notify_db, notify_db_session, mo 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 == 200 - assert json_resp['notification']['id'] == 100 - notify_alpha_client.send_sms.assert_called_with(mobile_number='+441234123123', message="valid") + assert response.status_code == 204 +@moto.mock_sqs def test_send_email_valid_data(notify_api, notify_db, notify_db_session, sample_service, sample_admin_service_id, + sqs_client_conn, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: @@ -326,29 +216,11 @@ def test_send_email_valid_data(notify_api, from_address = "from@notify.com" subject = "This is the subject" message = "This is the message" - mocker.patch( - 'app.notify_alpha_client.send_email', - return_value={ - "notification": { - "createdAt": "2015-11-03T09:37:27.414363Z", - "id": 100, - "jobId": 65, - "subject": subject, - "message": message, - "method": "email", - "status": "created", - "to": to_address, - "from": from_address - } - } - ) data = { - 'notification': { - 'to': to_address, - 'from': from_address, - 'subject': subject, - 'message': message - } + 'to': to_address, + 'from': from_address, + 'subject': subject, + 'message': message } auth_header = create_authorization_header( request_body=json.dumps(data), @@ -360,14 +232,73 @@ def test_send_email_valid_data(notify_api, data=json.dumps(data), headers=[('Content-Type', 'application/json'), auth_header]) + assert response.status_code == 204 + + +@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 = url_for('notifications.create_sms_for_service', service_id=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 == 204 + + +@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 = url_for('notifications.create_sms_for_service', service_id=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 == 200 - assert json_resp['notification']['id'] == 100 - notify_alpha_client.send_email.assert_called_with( - to_address, message, from_address, subject) - - -def set_up_mock_queue(): - # set up mock queue - boto3.setup_default_session(region_name='eu-west-1') - conn = boto3.resource('sqs') + 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/user/test_rest_verify.py b/tests/app/user/test_rest_verify.py index a2228d44b..a58fa6a1a 100644 --- a/tests/app/user/test_rest_verify.py +++ b/tests/app/user/test_rest_verify.py @@ -1,4 +1,5 @@ import json +import moto from datetime import (datetime, timedelta) from flask import url_for @@ -55,9 +56,11 @@ def test_user_verify_code_sms_missing_code(notify_api, assert not VerifyCode.query.first().code_used +@moto.mock_sqs def test_user_verify_code_email(notify_api, notify_db, notify_db_session, + sqs_client_conn, sample_email_code): """ Tests POST endpoint '//verify/code' @@ -244,11 +247,12 @@ def test_user_verify_password_missing_password(notify_api, assert 'Required field missing data' in json_resp['message']['password'] +@moto.mock_sqs def test_send_user_code_for_sms(notify_api, notify_db, notify_db_session, sample_sms_code, - mock_notify_client_send_sms, + sqs_client_conn, mock_secret_code): """ Tests POST endpoint '//code' successful sms @@ -266,15 +270,14 @@ def test_send_user_code_for_sms(notify_api, headers=[('Content-Type', 'application/json'), auth_header]) assert resp.status_code == 204 - mock_notify_client_send_sms.assert_called_once_with(mobile_number=sample_sms_code.user.mobile_number, - message='11111') +@moto.mock_sqs def test_send_user_code_for_sms_with_optional_to_field(notify_api, notify_db, notify_db_session, sample_sms_code, - mock_notify_client_send_sms, + sqs_client_conn, mock_secret_code): """ Tests POST endpoint '//code' successful sms with optional to field @@ -292,15 +295,14 @@ def test_send_user_code_for_sms_with_optional_to_field(notify_api, headers=[('Content-Type', 'application/json'), auth_header]) assert resp.status_code == 204 - mock_notify_client_send_sms.assert_called_once_with(mobile_number='+441119876757', - message='11111') +@moto.mock_sqs def test_send_user_code_for_email(notify_api, notify_db, notify_db_session, sample_email_code, - mock_notify_client_send_email, + sqs_client_conn, mock_secret_code): """ Tests POST endpoint '//code' successful email @@ -317,17 +319,14 @@ def test_send_user_code_for_email(notify_api, data=data, headers=[('Content-Type', 'application/json'), auth_header]) assert resp.status_code == 204 - mock_notify_client_send_email.assert_called_once_with(sample_email_code.user.email_address, - '11111', - 'notify@digital.cabinet-office.gov.uk', - 'Verification code') +@moto.mock_sqs def test_send_user_code_for_email_uses_optional_to_field(notify_api, notify_db, notify_db_session, sample_email_code, - mock_notify_client_send_email, + sqs_client_conn, mock_secret_code): """ Tests POST endpoint '//code' successful email with included in body @@ -344,23 +343,16 @@ def test_send_user_code_for_email_uses_optional_to_field(notify_api, data=data, headers=[('Content-Type', 'application/json'), auth_header]) assert resp.status_code == 204 - mock_notify_client_send_email.assert_called_once_with('different@email.gov.uk', - '11111', - 'notify@digital.cabinet-office.gov.uk', - 'Verification code') def test_request_verify_code_schema_invalid_code_type(notify_api, notify_db, notify_db_session, sample_user): - import json from app.schemas import request_verify_code_schema data = json.dumps({'code_type': 'not_sms'}) code, error = request_verify_code_schema.loads(data) - assert code == {} assert error == {'code_type': ['Invalid code type']} def test_request_verify_code_schema_with_to(notify_api, notify_db, notify_db_session, sample_user): - import json from app.schemas import request_verify_code_schema data = json.dumps({'code_type': 'sms', 'to': 'some@one.gov.uk'}) code, error = request_verify_code_schema.loads(data) diff --git a/tests/conftest.py b/tests/conftest.py index 9ca193f57..37ee9bbd3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ import pytest import mock import os +import boto3 from config import configs from alembic.command import upgrade from alembic.config import Config @@ -70,3 +71,9 @@ def os_environ(request): request.addfinalizer(env_patch.stop) return env_patch.start() + + +@pytest.fixture(scope='function') +def sqs_client_conn(request): + boto3.setup_default_session(region_name='eu-west-1') + return boto3.resource('sqs') diff --git a/wsgi.py b/wsgi.py index 6e68a6c86..fd773b3e0 100644 --- a/wsgi.py +++ b/wsgi.py @@ -1,9 +1,17 @@ from app import create_app from credstash import getAllSecrets +import os -#secrets = getAllSecrets(region="eu-west-1") +config = 'live' +default_env_file = '/home/ubuntu/environment' -application = create_app('live', None) +if os.path.isfile(default_env_file): + environment = open(default_env_file, 'r') + config = environment.readline().strip() + +secrets = getAllSecrets(region="eu-west-1") + +application = create_app(config, secrets) if __name__ == "__main__": application.run()