Merge branch 'master' into celery-spike

Conflicts:
	app/__init__.py
	app/notifications/rest.py
	config.py
	wsgi.py
This commit is contained in:
Martyn Inglis
2016-02-12 09:36:49 +00:00
26 changed files with 896 additions and 429 deletions

View File

@@ -18,6 +18,7 @@ db = SQLAlchemy()
ma = Marshmallow() ma = Marshmallow()
notify_alpha_client = NotifyAPIClient() notify_alpha_client = NotifyAPIClient()
celery = NotifyCelery() celery = NotifyCelery()
api_user = LocalProxy(lambda: _request_ctx_stack.top.api_user) 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) ma.init_app(application)
init_app(application, config_overrides) init_app(application, config_overrides)
logging.init_app(application) logging.init_app(application)
notify_alpha_client.init_app(application)
celery.init_app(application) celery.init_app(application)
@@ -78,7 +78,6 @@ def init_app(app, config_overrides):
return response return response
def convert_to_boolean(value): def convert_to_boolean(value):
"""Turn strings to bools if they look like them """Turn strings to bools if they look like them

View File

@@ -1,10 +1,11 @@
from flask import request, jsonify, _request_ctx_stack from flask import request, jsonify, _request_ctx_stack, current_app
from client.authentication import decode_jwt_token, get_token_issuer from notifications_python_client.authentication import decode_jwt_token, get_token_issuer
from client.errors import TokenDecodeError, TokenRequestError, TokenExpiredError, TokenPayloadError from notifications_python_client.errors import TokenDecodeError, TokenRequestError, TokenExpiredError, TokenPayloadError
from app.dao.api_key_dao import get_unsigned_secrets from app.dao.api_key_dao import get_unsigned_secrets
def authentication_response(message, code): def authentication_response(message, code):
current_app.logger.info(message)
return jsonify( return jsonify(
error=message error=message
), code ), code
@@ -27,8 +28,7 @@ def requires_auth():
return authentication_response("Invalid token: signature", 403) return authentication_response("Invalid token: signature", 403)
if api_client is None: if api_client is None:
authentication_response("Invalid credentials", 403) 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']: for secret in api_client['secret']:
try: try:
decode_jwt_token( decode_jwt_token(
@@ -53,12 +53,16 @@ def requires_auth():
def fetch_client(client): def fetch_client(client):
from flask import current_app
if client == current_app.config.get('ADMIN_CLIENT_USER_NAME'): if client == current_app.config.get('ADMIN_CLIENT_USER_NAME'):
return { return {
"client": client, "client": client,
"secret": [current_app.config.get('ADMIN_CLIENT_SECRET')] "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: else:
return { return {
"client": client, "client": client,

20
app/aws_sqs.py Normal file
View File

@@ -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'}})

View File

@@ -15,7 +15,3 @@ class NotifyCelery(Celery):
with app.app_context(): with app.app_context():
return TaskBase.__call__(self, *args, **kwargs) return TaskBase.__call__(self, *args, **kwargs)
self.Task = ContextTask self.Task = ContextTask

View File

@@ -2,8 +2,14 @@ from app import db
from app.models import Job from app.models import Job
def save_job(job): def save_job(job, update_dict={}):
db.session.add(job) 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() db.session.commit()

View File

@@ -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()

View File

@@ -23,7 +23,6 @@ def delete_model_template(template):
def get_model_templates(template_id=None, service_id=None): def get_model_templates(template_id=None, service_id=None):
temp = Template.query.first()
# TODO need better mapping from function params to sql query. # TODO need better mapping from function params to sql query.
if template_id and service_id: if template_id and service_id:
return Template.query.filter_by( return Template.query.filter_by(

View File

@@ -17,9 +17,19 @@ from app.dao.jobs_dao import (
get_jobs_by_service get_jobs_by_service
) )
from app.dao.notifications_dao import (
save_notification,
get_notification,
get_notifications
)
from app.schemas import ( from app.schemas import (
job_schema, 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/<service_id>/job') job = Blueprint('job', __name__, url_prefix='/service/<service_id>/job')
@@ -56,12 +66,88 @@ def create_job(service_id):
return jsonify(data=job_schema.dump(job).data), 201 return jsonify(data=job_schema.dump(job).data), 201
@job.route('/<job_id>', 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('/<job_id>/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('/<job_id>/notification', methods=['GET'])
@job.route('/<job_id>/notification/<notification_id>')
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('/<job_id>/notification/<notification_id>', 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): def _enqueue_job(job):
aws_region = current_app.config['AWS_REGION'] aws_region = current_app.config['AWS_REGION']
queue_name = current_app.config['NOTIFY_JOB_QUEUE'] queue_name = current_app.config['NOTIFY_JOB_QUEUE']
queue = boto3.resource('sqs', region_name=aws_region).create_queue(QueueName=queue_name) 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, queue.send_message(MessageBody=job_json,
MessageAttributes={'job_id': {'StringValue': str(job.id), 'DataType': 'String'}, MessageAttributes={'id': {'StringValue': str(job.id), 'DataType': 'String'},
'service_id': {'StringValue': str(job.service.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'}})

View File

@@ -189,3 +189,34 @@ class VerifyCode(db.Model):
def check_code(self, cde): def check_code(self, cde):
return check_hash(cde, self._code) 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')

View File

@@ -1,165 +1,71 @@
import json import uuid
import boto3
from flask import ( from flask import (
Blueprint, Blueprint,
jsonify, jsonify,
request, request
current_app
) )
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 = Blueprint('notifications', __name__)
@notifications.route('/<notification_id>', methods=['GET']) @notifications.route('/<notification_id>', methods=['GET'])
def get_notifications(notification_id): def get_notifications(notification_id):
return jsonify(notify_alpha_client.fetch_notification_by_id(notification_id)), 200 # TODO return notification id details
return jsonify({'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)
@notifications.route('/sms', methods=['POST']) @notifications.route('/sms', methods=['POST'])
def create_sms_notification(): def create_sms_notification():
notification = request.get_json()['notification'] resp_json = request.get_json()
errors = {}
to, to_errors = validate_to(notification)
if to_errors['to']:
errors.update(to_errors)
# TODO: should create a different endpoint for the admin client to send verify codes. notification, errors = sms_template_notification_schema.load(resp_json)
if api_user['client'] == current_app.config.get('ADMIN_CLIENT_USER_NAME'): if errors:
content, content_errors = validate_content_for_admin_client(notification) return jsonify(result="error", message=errors), 400
if content_errors['content']:
errors.update(content_errors)
if errors:
return jsonify(result="error", message=errors), 400
return jsonify(notify_alpha_client.send_sms(mobile_number=to, message=content)), 200 add_notification_to_queue(api_user['client'], notification['template'], 'sms', notification)
# TODO data to be returned
else: return jsonify({}), 204
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
@notifications.route('/email', methods=['POST']) @notifications.route('/email', methods=['POST'])
def create_email_notification(): def create_email_notification():
notification = request.get_json()['notification'] resp_json = request.get_json()
errors = {} notification, errors = email_notification_schema.load(resp_json)
for k in ['to', 'from', 'subject', 'message']: if errors:
k_error = validate_required_and_something(notification, k) return jsonify(result="error", message=errors), 400
if k_error: add_notification_to_queue(api_user['client'], "admin", 'email', notification)
errors.update(k_error) # TODO data to be returned
return jsonify({}), 204
@notifications.route('/sms/service/<service_id>', 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: if errors:
return jsonify(result="error", message=errors), 400 return jsonify(result="error", message=errors), 400
return jsonify(notify_alpha_client.send_email( template_id = notification['template']
notification['to'], job_id = notification['job']
notification['message'],
notification['from'],
notification['subject']))
# TODO: job/job_id is in notification and can used to update job status
def validate_to_for_service(mob, service_id): # TODO: remove once beta is reading notifications from the queue
errors = {"restricted": []} template = templates_dao.get_model_templates(template_id)
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
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): add_notification_to_queue(service_id, template_id, 'sms', notification)
errors = {"to": []} # TODO data to be returned
mob = json_body.get('to', None) return jsonify({}), 204
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'}})

View File

@@ -1,7 +1,10 @@
import re
from flask_marshmallow.fields import fields from flask_marshmallow.fields import fields
from . import ma from . import ma
from . import models 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. # TODO I think marshmallow provides a better integration and error handling.
@@ -59,12 +62,77 @@ class JobSchema(BaseSchema):
class RequestVerifyCodeSchema(ma.Schema): 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') 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() user_schema = UserSchema()
@@ -83,3 +151,9 @@ job_schema = JobSchema()
job_schema_load_json = JobSchema(load_json=True) job_schema_load_json = JobSchema(load_json=True)
jobs_schema = JobSchema(many=True) jobs_schema = JobSchema(many=True)
request_verify_code_schema = RequestVerifyCodeSchema() 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)

View File

@@ -1,8 +1,9 @@
from datetime import datetime from datetime import datetime
from flask import (jsonify, request, abort) from flask import (jsonify, request, abort, Blueprint)
from sqlalchemy.exc import DataError from sqlalchemy.exc import DataError
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
from app.dao.services_dao import get_model_services from app.dao.services_dao import get_model_services
from app.aws_sqs import add_notification_to_queue
from app.dao.users_dao import ( from app.dao.users_dao import (
get_model_users, get_model_users,
save_model_user, save_model_user,
@@ -16,8 +17,7 @@ from app.dao.users_dao import (
from app.schemas import ( from app.schemas import (
user_schema, users_schema, service_schema, services_schema, user_schema, users_schema, service_schema, services_schema,
request_verify_code_schema, user_schema_load_json) request_verify_code_schema, user_schema_load_json)
from app import notify_alpha_client from app import api_user
from flask import Blueprint
user = Blueprint('user', __name__) user = Blueprint('user', __name__)
@@ -133,20 +133,18 @@ def send_user_code(user_id):
from app.dao.users_dao import create_secret_code from app.dao.users_dao import create_secret_code
secret_code = create_secret_code() secret_code = create_secret_code()
create_user_code(user, secret_code, verify_code.get('code_type')) 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': if verify_code.get('code_type') == 'sms':
mobile = user.mobile_number if verify_code.get('to', None) is None else verify_code.get('to') mobile = user.mobile_number if verify_code.get('to', None) is None else verify_code.get('to')
notify_alpha_client.send_sms( notification = {'to': mobile, 'content': secret_code}
mobile_number=mobile, add_notification_to_queue(api_user['client'], 'admin', 'sms', notification)
message=secret_code)
elif verify_code.get('code_type') == 'email': elif verify_code.get('code_type') == 'email':
email = user.email_address if verify_code.get('to', None) is None else verify_code.get('to') email = user.email_address if verify_code.get('to', None) is None else verify_code.get('to')
notify_alpha_client.send_email( notification = {
email, 'to_address': email,
secret_code, 'from_address': 'notify@digital.cabinet-office.gov.uk',
'notify@digital.cabinet-office.gov.uk', 'subject': 'Verification code',
'Verification code') 'body': secret_code}
add_notification_to_queue(api_user['client'], 'admin', 'email', notification)
else: else:
abort(500) abort(500)
return jsonify({}), 204 return jsonify({}), 204

View File

@@ -14,9 +14,13 @@ class Config(object):
NOTIFY_DATA_API_AUTH_TOKEN = os.getenv('NOTIFY_API_TOKEN', "dev-token") NOTIFY_DATA_API_AUTH_TOKEN = os.getenv('NOTIFY_API_TOKEN', "dev-token")
ADMIN_CLIENT_USER_NAME = None ADMIN_CLIENT_USER_NAME = None
ADMIN_CLIENT_SECRET = None ADMIN_CLIENT_SECRET = None
DELIVERY_CLIENT_USER_NAME = None
DELIVERY_CLIENT_SECRET = None
AWS_REGION = 'eu-west-1' AWS_REGION = 'eu-west-1'
NOTIFY_JOB_QUEUE = os.getenv('NOTIFY_JOB_QUEUE', 'notify-jobs-queue') 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_URL = 'amqp://guest:guest@localhost:5672//'
BROKER_TRANSPORT_OPTIONS = { BROKER_TRANSPORT_OPTIONS = {
@@ -44,27 +48,32 @@ class Development(Config):
DANGEROUS_SALT = 'dangerous-salt' DANGEROUS_SALT = 'dangerous-salt'
ADMIN_CLIENT_USER_NAME = 'dev-notify-admin' ADMIN_CLIENT_USER_NAME = 'dev-notify-admin'
ADMIN_CLIENT_SECRET = 'dev-notify-secret-key' 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): class Test(Development):
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/test_notification_api' SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/test_notification_api'
SECRET_KEY = 'secret-key' NOTIFICATION_QUEUE_PREFIX = 'notification_test'
DANGEROUS_SALT = 'dangerous-salt'
ADMIN_CLIENT_USER_NAME = 'dev-notify-admin'
ADMIN_CLIENT_SECRET = 'dev-notify-secret-key' class Preview(Config):
NOTIFICATION_QUEUE_PREFIX = 'notification_preview'
class Staging(Config):
NOTIFICATION_QUEUE_PREFIX = 'notification_staging'
class Live(Config): class Live(Config):
SECRET_KEY = 'secret-key' NOTIFICATION_QUEUE_PREFIX = 'notification_live'
DANGEROUS_SALT = 'dangerous-salt'
ADMIN_CLIENT_USER_NAME = 'dev-notify-admin'
ADMIN_CLIENT_SECRET = 'dev-notify-secret-key'
pass
configs = { configs = {
'development': Development, 'development': Development,
'preview': Preview,
'staging': Staging,
'test': Test, 'test': Test,
'live': Live, 'live': Live,
} }

View File

@@ -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 ###

View File

@@ -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 ###

View File

@@ -16,8 +16,6 @@ boto3==1.2.3
celery==3.1.20 celery==3.1.20
redis==2.10.5 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/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

View File

@@ -1,5 +1,5 @@
from flask import current_app 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 from app.dao.api_key_dao import get_unsigned_secrets

View File

@@ -1,6 +1,6 @@
from datetime import datetime, timedelta 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 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.dao.api_key_dao import get_unsigned_secrets, save_model_api_key, get_unsigned_secret
from app.models import ApiKey, Service 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' 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): def __create_get_token(service_id):
if service_id: if service_id:
return create_jwt_token(request_method="GET", return create_jwt_token(request_method="GET",

View File

@@ -1,12 +1,13 @@
import pytest import pytest
from flask import jsonify 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.users_dao import (save_model_user, create_user_code, create_secret_code)
from app.dao.services_dao import save_model_service from app.dao.services_dao import save_model_service
from app.dao.templates_dao import save_model_template from app.dao.templates_dao import save_model_template
from app.dao.api_key_dao import save_model_api_key from app.dao.api_key_dao import save_model_api_key
from app.dao.jobs_dao import save_job from app.dao.jobs_dao import save_job
from app.dao.notifications_dao import save_notification
import uuid import uuid
@@ -155,24 +156,6 @@ def sample_admin_service_id(notify_db, notify_db_session):
return admin_service.id 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') @pytest.fixture(scope='function')
def mock_secret_code(mocker): def mock_secret_code(mocker):
def _create(): 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) mock_class = mocker.patch('app.dao.users_dao.create_secret_code', side_effect=_create)
return mock_class 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

View File

@@ -1,4 +1,5 @@
import uuid import uuid
import json
from app.dao.jobs_dao import ( from app.dao.jobs_dao import (
save_job, save_job,
@@ -79,3 +80,23 @@ def test_get_all_jobs(notify_db, notify_db_session, sample_template):
sample_template) sample_template)
jobs_from_db = _get_jobs() jobs_from_db = _get_jobs()
assert len(jobs_from_db) == 5 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'

View File

@@ -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'

View File

@@ -119,8 +119,170 @@ def test_create_job(notify_api, notify_db, notify_db_session, sample_template):
assert len(messages) == 1 assert len(messages) == 1
expected_message = json.loads(messages[0].body) expected_message = json.loads(messages[0].body)
assert expected_message['job_id'] == str(job_id) assert expected_message['id'] == str(job_id)
assert expected_message['service_id'] == str(service_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): def _setup_jobs(notify_db, notify_db_session, template, number_of_jobs=5):

View File

@@ -1,9 +1,8 @@
import boto3
import moto import moto
import uuid
from tests import create_authorization_header from tests import create_authorization_header
from flask import url_for, json from flask import url_for, json
from app import notify_alpha_client
from app.models import Service from app.models import Service
@@ -14,18 +13,6 @@ def test_get_notifications(
""" """
with notify_api.test_request_context(): with notify_api.test_request_context():
with notify_api.test_client() as client: with notify_api.test_client() as client:
mocker.patch(
'app.notify_alpha_client.fetch_notification_by_id',
return_value={
'notifications': [
{
'id': 'my_id',
'notification': 'some notify'
}
]
}
)
auth_header = create_authorization_header( auth_header = create_authorization_header(
service_id=sample_api_key.service_id, service_id=sample_api_key.service_id,
path=url_for('notifications.get_notifications', notification_id=123), 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), url_for('notifications.get_notifications', notification_id=123),
headers=[auth_header]) headers=[auth_header])
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 200 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( 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_request_context():
with notify_api.test_client() as client: 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( auth_header = create_authorization_header(
service_id=sample_api_key.service_id, service_id=sample_api_key.service_id,
path=url_for('notifications.get_notifications', notification_id=123), 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), url_for('notifications.get_notifications', notification_id=123),
headers=[auth_header]) headers=[auth_header])
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 200 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( 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_request_context():
with notify_api.test_client() as client: with notify_api.test_client() as client:
mocker.patch(
'app.notify_alpha_client.send_sms',
return_value='success'
)
data = { data = {
'notification': { 'template': "my message"
'template': "my message"
}
} }
auth_header = create_authorization_header( auth_header = create_authorization_header(
service_id=sample_api_key.service_id, 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)) json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 400 assert response.status_code == 400
assert json_resp['result'] == 'error' assert json_resp['result'] == 'error'
assert 'Required data missing' in json_resp['message']['to'][0] assert 'Missing data for required field.' in json_resp['message']['to'][0]
assert not notify_alpha_client.send_sms.called
def test_should_reject_bad_phone_numbers( 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_request_context():
with notify_api.test_client() as client: with notify_api.test_client() as client:
mocker.patch(
'app.notify_alpha_client.send_sms',
return_value='success'
)
data = { data = {
'notification': { 'to': 'invalid',
'to': 'invalid', 'template': "my message"
'template': "my message"
}
} }
auth_header = create_authorization_header( auth_header = create_authorization_header(
request_body=json.dumps(data), 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)) json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 400 assert response.status_code == 400
assert json_resp['result'] == 'error' assert json_resp['result'] == 'error'
assert 'invalid phone number, must be of format +441234123123' in json_resp['message']['to'] 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)
def test_send_notification_restrict_mobile(notify_api, 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_request_context():
with notify_api.test_client() as client: with notify_api.test_client() as client:
Service.query.filter_by( Service.query.filter_by(
id=sample_template.service.id).update({'restricted': True}) id=sample_template.service.id).update({'restricted': True})
invalid_mob = '+449999999999' invalid_mob = '+449999999999'
mocker.patch(
'app.notify_alpha_client.send_sms',
return_value={}
)
data = { data = {
'notification': { 'to': invalid_mob,
'to': invalid_mob, 'template': sample_template.id
'template': sample_template.id
}
} }
assert invalid_mob != sample_user.mobile_number assert invalid_mob != sample_user.mobile_number
auth_header = create_authorization_header( 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)) json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 400 assert response.status_code == 400
assert 'Invalid phone number for restricted service' in json_resp['message']['restricted'] assert 'Invalid phone number for restricted service' in json_resp['message']['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 @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. Tests POST endpoint '/sms' with notifications-admin notification.
""" """
with notify_api.test_request_context(): with notify_api.test_request_context():
with notify_api.test_client() as client: with notify_api.test_client() as client:
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 = { data = {
'notification': { 'to': '+441234123123',
'to': '+441234123123', 'template': sample_template.id
'template': 'valid'
}
} }
auth_header = create_authorization_header( auth_header = create_authorization_header(
request_body=json.dumps(data), 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), data=json.dumps(data),
headers=[('Content-Type', 'application/json'), auth_header]) headers=[('Content-Type', 'application/json'), auth_header])
json_resp = json.loads(response.get_data(as_text=True)) assert response.status_code == 204
assert response.status_code == 200
assert json_resp['notification']['id'] == 100
notify_alpha_client.send_sms.assert_called_with(mobile_number='+441234123123', message="valid")
@moto.mock_sqs
def test_send_email_valid_data(notify_api, def test_send_email_valid_data(notify_api,
notify_db, notify_db,
notify_db_session, notify_db_session,
sample_service, sample_service,
sample_admin_service_id, sample_admin_service_id,
sqs_client_conn,
mocker): mocker):
with notify_api.test_request_context(): with notify_api.test_request_context():
with notify_api.test_client() as client: with notify_api.test_client() as client:
@@ -326,29 +216,11 @@ def test_send_email_valid_data(notify_api,
from_address = "from@notify.com" from_address = "from@notify.com"
subject = "This is the subject" subject = "This is the subject"
message = "This is the message" 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 = { data = {
'notification': { 'to': to_address,
'to': to_address, 'from': from_address,
'from': from_address, 'subject': subject,
'subject': subject, 'message': message
'message': message
}
} }
auth_header = create_authorization_header( auth_header = create_authorization_header(
request_body=json.dumps(data), request_body=json.dumps(data),
@@ -360,14 +232,73 @@ def test_send_email_valid_data(notify_api,
data=json.dumps(data), data=json.dumps(data),
headers=[('Content-Type', 'application/json'), auth_header]) headers=[('Content-Type', 'application/json'), auth_header])
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)) json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 200 assert response.status_code == 400
assert json_resp['notification']['id'] == 100 expected_error = 'Invalid template: id {} for service id: {}'.format(sample_template.id,
notify_alpha_client.send_email.assert_called_with( invalid_service_id)
to_address, message, from_address, subject) assert json_resp['message'] == expected_error
def set_up_mock_queue():
# set up mock queue
boto3.setup_default_session(region_name='eu-west-1')
conn = boto3.resource('sqs')

View File

@@ -1,4 +1,5 @@
import json import json
import moto
from datetime import (datetime, timedelta) from datetime import (datetime, timedelta)
from flask import url_for 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 assert not VerifyCode.query.first().code_used
@moto.mock_sqs
def test_user_verify_code_email(notify_api, def test_user_verify_code_email(notify_api,
notify_db, notify_db,
notify_db_session, notify_db_session,
sqs_client_conn,
sample_email_code): sample_email_code):
""" """
Tests POST endpoint '/<user_id>/verify/code' Tests POST endpoint '/<user_id>/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'] assert 'Required field missing data' in json_resp['message']['password']
@moto.mock_sqs
def test_send_user_code_for_sms(notify_api, def test_send_user_code_for_sms(notify_api,
notify_db, notify_db,
notify_db_session, notify_db_session,
sample_sms_code, sample_sms_code,
mock_notify_client_send_sms, sqs_client_conn,
mock_secret_code): mock_secret_code):
""" """
Tests POST endpoint '/<user_id>/code' successful sms Tests POST endpoint '/<user_id>/code' successful sms
@@ -266,15 +270,14 @@ def test_send_user_code_for_sms(notify_api,
headers=[('Content-Type', 'application/json'), auth_header]) headers=[('Content-Type', 'application/json'), auth_header])
assert resp.status_code == 204 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, def test_send_user_code_for_sms_with_optional_to_field(notify_api,
notify_db, notify_db,
notify_db_session, notify_db_session,
sample_sms_code, sample_sms_code,
mock_notify_client_send_sms, sqs_client_conn,
mock_secret_code): mock_secret_code):
""" """
Tests POST endpoint '/<user_id>/code' successful sms with optional to field Tests POST endpoint '/<user_id>/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]) headers=[('Content-Type', 'application/json'), auth_header])
assert resp.status_code == 204 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, def test_send_user_code_for_email(notify_api,
notify_db, notify_db,
notify_db_session, notify_db_session,
sample_email_code, sample_email_code,
mock_notify_client_send_email, sqs_client_conn,
mock_secret_code): mock_secret_code):
""" """
Tests POST endpoint '/<user_id>/code' successful email Tests POST endpoint '/<user_id>/code' successful email
@@ -317,17 +319,14 @@ def test_send_user_code_for_email(notify_api,
data=data, data=data,
headers=[('Content-Type', 'application/json'), auth_header]) headers=[('Content-Type', 'application/json'), auth_header])
assert resp.status_code == 204 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, def test_send_user_code_for_email_uses_optional_to_field(notify_api,
notify_db, notify_db,
notify_db_session, notify_db_session,
sample_email_code, sample_email_code,
mock_notify_client_send_email, sqs_client_conn,
mock_secret_code): mock_secret_code):
""" """
Tests POST endpoint '/<user_id>/code' successful email with included in body Tests POST endpoint '/<user_id>/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, data=data,
headers=[('Content-Type', 'application/json'), auth_header]) headers=[('Content-Type', 'application/json'), auth_header])
assert resp.status_code == 204 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): 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 from app.schemas import request_verify_code_schema
data = json.dumps({'code_type': 'not_sms'}) data = json.dumps({'code_type': 'not_sms'})
code, error = request_verify_code_schema.loads(data) code, error = request_verify_code_schema.loads(data)
assert code == {}
assert error == {'code_type': ['Invalid code type']} assert error == {'code_type': ['Invalid code type']}
def test_request_verify_code_schema_with_to(notify_api, notify_db, notify_db_session, sample_user): 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 from app.schemas import request_verify_code_schema
data = json.dumps({'code_type': 'sms', 'to': 'some@one.gov.uk'}) data = json.dumps({'code_type': 'sms', 'to': 'some@one.gov.uk'})
code, error = request_verify_code_schema.loads(data) code, error = request_verify_code_schema.loads(data)

View File

@@ -1,6 +1,7 @@
import pytest import pytest
import mock import mock
import os import os
import boto3
from config import configs from config import configs
from alembic.command import upgrade from alembic.command import upgrade
from alembic.config import Config from alembic.config import Config
@@ -70,3 +71,9 @@ def os_environ(request):
request.addfinalizer(env_patch.stop) request.addfinalizer(env_patch.stop)
return env_patch.start() 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')

12
wsgi.py
View File

@@ -1,9 +1,17 @@
from app import create_app from app import create_app
from credstash import getAllSecrets 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__": if __name__ == "__main__":
application.run() application.run()