Register a before_request event for all blueprints, that defines the authentication requirement.

There are three authentication methods:
 - requires_no_auth - public endpoint that does not require an Authorisation header
 - requires_auth - public endpoints that need an API key in the Authorisation header
 - requires_admin_auth - private endpoint that requires an Authorisation header which contains the API key for the defined as the client admin user
This commit is contained in:
Rebecca Law
2017-03-16 18:15:49 +00:00
parent f880604c85
commit 78242812ef
19 changed files with 634 additions and 544 deletions

View File

@@ -2,7 +2,7 @@ import os
import uuid
from flask import Flask, _request_ctx_stack
from flask import request, url_for, g, jsonify
from flask import request, g, jsonify
from flask.ext.sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
from monotonic import monotonic
@@ -20,7 +20,6 @@ from app.clients.sms.mmg import MMGClient
from app.clients.performance_platform.performance_platform_client import PerformancePlatformClient
from app.encryption import Encryption
DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
DATE_FORMAT = "%Y-%m-%d"
@@ -77,10 +76,10 @@ def create_app(app_name=None):
def register_blueprint(application):
from app.service.rest import service_blueprint
from app.user.rest import user as user_blueprint
from app.template.rest import template as template_blueprint
from app.user.rest import user_blueprint
from app.template.rest import template_blueprint
from app.status.healthcheck import status as status_blueprint
from app.job.rest import job as job_blueprint
from app.job.rest import job_blueprint
from app.notifications.rest import notifications as notifications_blueprint
from app.invite.rest import invite as invite_blueprint
from app.accept_invite.rest import accept_invite
@@ -90,49 +89,100 @@ def register_blueprint(application):
from app.spec.rest import spec as spec_blueprint
from app.organisation.rest import organisation_blueprint
from app.delivery.rest import delivery_blueprint
from app.notifications.receive_notifications import receive_notifications_blueprint
from app.notifications.notifications_ses_callback import ses_callback_blueprint
from app.notifications.notifications_sms_callback import sms_callback_blueprint
from app.status.healthcheck import status
from app.authentication.auth import requires_admin_auth, requires_auth, requires_no_auth
service_blueprint.before_request(requires_admin_auth)
application.register_blueprint(service_blueprint, url_prefix='/service')
user_blueprint.before_request(requires_admin_auth)
application.register_blueprint(user_blueprint, url_prefix='/user')
template_blueprint.before_request(requires_admin_auth)
application.register_blueprint(template_blueprint)
status_blueprint.before_request(requires_no_auth)
application.register_blueprint(status_blueprint)
notifications_blueprint.before_request(requires_auth)
receive_notifications_blueprint.before_request(requires_no_auth)
ses_callback_blueprint.before_request(requires_no_auth)
application.register_blueprint(ses_callback_blueprint)
sms_callback_blueprint.before_request(requires_no_auth)
application.register_blueprint(sms_callback_blueprint)
receive_notifications_blueprint.before_request(requires_no_auth)
application.register_blueprint(receive_notifications_blueprint)
notifications_blueprint.before_request(requires_auth)
application.register_blueprint(notifications_blueprint)
job_blueprint.before_request(requires_admin_auth)
application.register_blueprint(job_blueprint)
invite_blueprint.before_request(requires_admin_auth)
application.register_blueprint(invite_blueprint)
delivery_blueprint.before_request(requires_admin_auth)
application.register_blueprint(delivery_blueprint)
accept_invite.before_request(requires_admin_auth)
application.register_blueprint(accept_invite, url_prefix='/invite')
template_statistics_blueprint.before_request(requires_admin_auth)
application.register_blueprint(template_statistics_blueprint)
events_blueprint.before_request(requires_admin_auth)
application.register_blueprint(events_blueprint)
provider_details_blueprint.before_request(requires_admin_auth)
application.register_blueprint(provider_details_blueprint, url_prefix='/provider-details')
spec_blueprint.before_request(requires_no_auth)
application.register_blueprint(spec_blueprint, url_prefix='/spec')
organisation_blueprint.before_request(requires_admin_auth)
application.register_blueprint(organisation_blueprint, url_prefix='/organisation')
status.before_request(requires_no_auth)
application.register_blueprint(status)
def register_v2_blueprints(application):
from app.v2.notifications.post_notifications import notification_blueprint as post_notifications
from app.v2.notifications.get_notifications import notification_blueprint as get_notifications
from app.v2.notifications.post_notifications import v2_notification_blueprint as post_notifications
from app.v2.notifications.get_notifications import v2_notification_blueprint as get_notifications
from app.authentication.auth import requires_auth
post_notifications.before_request(requires_auth)
application.register_blueprint(post_notifications)
get_notifications.before_request(requires_auth)
application.register_blueprint(get_notifications)
def init_app(app):
@app.before_request
def required_authentication():
no_auth_req = [
url_for('status.show_status'),
url_for('notifications.process_ses_response'),
url_for('notifications.process_firetext_response'),
url_for('notifications.process_mmg_response'),
url_for('notifications.receive_mmg_sms'),
url_for('status.show_delivery_status'),
url_for('spec.get_spec')
]
if request.path not in no_auth_req:
from app.authentication import auth
error = auth.requires_auth()
if error:
return error
# @app.before_request
# def required_authentication():
# no_auth_req = [
# url_for('status.show_status'),
# url_for('notifications.process_ses_response'),
# url_for('notifications.process_firetext_response'),
# url_for('notifications.process_mmg_response'),
# url_for('notifications.receive_mmg_sms'),
# url_for('status.show_delivery_status'),
# url_for('spec.get_spec')
# ]
#
# if request.path not in no_auth_req:
# from app.authentication import auth
# error = auth.requires_auth()
# if error:
# return error
@app.before_request
def record_user_agent():

View File

@@ -39,14 +39,13 @@ def get_auth_token(req):
return auth_header[7:]
def requires_no_auth():
pass
def requires_admin_auth():
auth_token = get_auth_token(request)
try:
client = get_token_issuer(auth_token)
except TokenDecodeError as e:
raise AuthError(e.message, 403)
except TokenIssuerError:
raise AuthError("Invalid token: iss not provided", 403)
client = __get_token_issuer(auth_token)
if client == current_app.config.get('ADMIN_CLIENT_USER_NAME'):
g.service_id = current_app.config.get('ADMIN_CLIENT_USER_NAME')
@@ -57,16 +56,7 @@ def requires_admin_auth():
def requires_auth():
auth_token = get_auth_token(request)
try:
client = get_token_issuer(auth_token)
except TokenDecodeError as e:
raise AuthError(e.message, 403)
except TokenIssuerError:
raise AuthError("Invalid token: iss not provided", 403)
if client == current_app.config.get('ADMIN_CLIENT_USER_NAME'):
g.service_id = current_app.config.get('ADMIN_CLIENT_USER_NAME')
return handle_admin_key(auth_token, current_app.config.get('ADMIN_CLIENT_SECRET'))
client = __get_token_issuer(auth_token)
try:
service = dao_fetch_service_by_id(client)
@@ -98,12 +88,22 @@ def requires_auth():
raise AuthError("Invalid token: signature, api token is not valid", 403)
def __get_token_issuer(auth_token):
try:
client = get_token_issuer(auth_token)
except TokenIssuerError:
raise AuthError("Invalid token: iss field not provided", 403)
except TokenDecodeError as e:
raise AuthError("Invalid token: signature, api token is not valid", 403)
return client
def handle_admin_key(auth_token, secret):
try:
get_decode_errors(auth_token, secret)
return
except TokenDecodeError as e:
raise AuthError(e.message, 403)
raise AuthError("Invalid token: signature, api token is not valid", 403)
def get_decode_errors(auth_token, unsigned_secret):

View File

@@ -34,17 +34,17 @@ from app.models import JOB_STATUS_SCHEDULED, JOB_STATUS_PENDING, JOB_STATUS_CANC
from app.utils import pagination_links
job = Blueprint('job', __name__, url_prefix='/service/<uuid:service_id>/job')
job_blueprint = Blueprint('job', __name__, url_prefix='/service/<uuid:service_id>/job')
from app.errors import (
register_errors,
InvalidRequest
)
register_errors(job)
register_errors(job_blueprint)
@job.route('/<job_id>', methods=['GET'])
@job_blueprint.route('/<job_id>', methods=['GET'])
def get_job_by_service_and_job_id(service_id, job_id):
job = dao_get_job_by_service_id_and_job_id(service_id, job_id)
statistics = dao_get_notification_outcomes_for_job(service_id, job_id)
@@ -55,7 +55,7 @@ def get_job_by_service_and_job_id(service_id, job_id):
return jsonify(data=data)
@job.route('/<job_id>/cancel', methods=['POST'])
@job_blueprint.route('/<job_id>/cancel', methods=['POST'])
def cancel_job(service_id, job_id):
job = dao_get_future_scheduled_job_by_id_and_service_id(job_id, service_id)
job.job_status = JOB_STATUS_CANCELLED
@@ -64,7 +64,7 @@ def cancel_job(service_id, job_id):
return get_job_by_service_and_job_id(service_id, job_id)
@job.route('/<job_id>/notifications', methods=['GET'])
@job_blueprint.route('/<job_id>/notifications', methods=['GET'])
def get_all_notifications_for_service_job(service_id, job_id):
data = notifications_filter_schema.load(request.args).data
page = data['page'] if 'page' in data else 1
@@ -100,7 +100,7 @@ def get_all_notifications_for_service_job(service_id, job_id):
), 200
@job.route('', methods=['GET'])
@job_blueprint.route('', methods=['GET'])
def get_jobs_by_service(service_id):
if request.args.get('limit_days'):
try:
@@ -117,7 +117,7 @@ def get_jobs_by_service(service_id):
return jsonify(**get_paginated_jobs(service_id, limit_days, statuses, page))
@job.route('', methods=['POST'])
@job_blueprint.route('', methods=['POST'])
def create_job(service_id):
service = dao_fetch_service_by_id(service_id)
if not service.active:

View File

@@ -0,0 +1,101 @@
from datetime import datetime
from flask import (
Blueprint,
jsonify,
request,
current_app,
json
)
from app import statsd_client
from app.clients.email.aws_ses import get_aws_responses
from app.dao import (
notifications_dao
)
from app.notifications.process_client_response import validate_callback_data
ses_callback_blueprint = Blueprint('notifications_ses_callback_', __name__)
from app.errors import (
register_errors,
InvalidRequest
)
register_errors(ses_callback_blueprint)
@ses_callback_blueprint.route('/notifications/email/ses', methods=['POST'])
def process_ses_response():
client_name = 'SES'
try:
ses_request = json.loads(request.data)
if 'Type' in ses_request and ses_request['Type'] == 'SubscriptionConfirmation':
current_app.logger.info("SNS subscription confirmation url: {}".format(ses_request['SubscribeURL']))
errors = validate_callback_data(data=ses_request, fields=['Message'], client_name=client_name)
if errors:
raise InvalidRequest(errors, status_code=400)
ses_message = json.loads(ses_request['Message'])
errors = validate_callback_data(data=ses_message, fields=['notificationType'], client_name=client_name)
if errors:
raise InvalidRequest(errors, status_code=400)
notification_type = ses_message['notificationType']
if notification_type == 'Bounce':
if ses_message['bounce']['bounceType'] == 'Permanent':
notification_type = ses_message['bounce']['bounceType'] # permanent or not
else:
notification_type = 'Temporary'
try:
aws_response_dict = get_aws_responses(notification_type)
except KeyError:
error = "{} callback failed: status {} not found".format(client_name, notification_type)
raise InvalidRequest(error, status_code=400)
notification_status = aws_response_dict['notification_status']
try:
reference = ses_message['mail']['messageId']
notification = notifications_dao.update_notification_status_by_reference(
reference,
notification_status
)
if not notification:
error = "SES callback failed: notification either not found or already updated " \
"from sending. Status {} for notification reference {}".format(notification_status, reference)
raise InvalidRequest(error, status_code=404)
if not aws_response_dict['success']:
current_app.logger.info(
"SES delivery failed: notification id {} and reference {} has error found. Status {}".format(
notification.id,
reference,
aws_response_dict['message']
)
)
else:
current_app.logger.info('{} callback return status of {} for notification: {}'.format(
client_name,
notification_status,
notification.id))
statsd_client.incr('callback.ses.{}'.format(notification_status))
if notification.sent_at:
statsd_client.timing_with_dates(
'callback.ses.elapsed-time'.format(client_name.lower()),
datetime.utcnow(),
notification.sent_at
)
return jsonify(
result="success", message="SES callback succeeded"
), 200
except KeyError:
message = "SES callback failed: messageId missing"
raise InvalidRequest(message, status_code=400)
except ValueError as ex:
error = "{} callback failed: invalid json".format(client_name)
raise InvalidRequest(error, status_code=400)

View File

@@ -0,0 +1,47 @@
from flask import Blueprint
from flask import json
from flask import request, jsonify
from app.errors import InvalidRequest, register_errors
from app.notifications.process_client_response import validate_callback_data, process_sms_client_response
sms_callback_blueprint = Blueprint("sms_callback_blueprint", __name__, url_prefix="/notifications/sms")
register_errors(sms_callback_blueprint)
@sms_callback_blueprint.route('/mmg', methods=['POST'])
def process_mmg_response():
client_name = 'MMG'
data = json.loads(request.data)
errors = validate_callback_data(data=data,
fields=['status', 'CID'],
client_name=client_name)
if errors:
raise InvalidRequest(errors, status_code=400)
success, errors = process_sms_client_response(status=str(data.get('status')),
reference=data.get('CID'),
client_name=client_name)
if errors:
raise InvalidRequest(errors, status_code=400)
else:
return jsonify(result='success', message=success), 200
@sms_callback_blueprint.route('/firetext', methods=['POST'])
def process_firetext_response():
client_name = 'Firetext'
errors = validate_callback_data(data=request.form,
fields=['status', 'reference'],
client_name=client_name)
if errors:
raise InvalidRequest(errors, status_code=400)
status = request.form.get('status')
success, errors = process_sms_client_response(status=status,
reference=request.form.get('reference'),
client_name=client_name)
if errors:
raise InvalidRequest(errors, status_code=400)
else:
return jsonify(result='success', message=success), 200

View File

@@ -0,0 +1,17 @@
from flask import Blueprint
from flask import current_app
from flask import request
from app.errors import register_errors
receive_notifications_blueprint = Blueprint('receive_notifications', __name__)
register_errors(receive_notifications_blueprint)
@receive_notifications_blueprint.route('/notifications/sms/receive/mmg', methods=['POST'])
def receive_mmg_sms():
post_data = request.get_json()
post_data.pop('MSISDN', None)
current_app.logger.info("Recieve notification form data: {}".format(post_data))
return "RECEIVED"

View File

@@ -1,5 +1,3 @@
from datetime import datetime
from flask import (
Blueprint,
jsonify,
@@ -8,8 +6,7 @@ from flask import (
json
)
from app import api_user, statsd_client
from app.clients.email.aws_ses import get_aws_responses
from app import api_user
from app.dao import (
templates_dao,
services_dao,
@@ -49,129 +46,6 @@ from app.errors import (
register_errors(notifications)
@notifications.route('/notifications/sms/receive/mmg', methods=['POST'])
def receive_mmg_sms():
post_data = request.get_json()
post_data.pop('MSISDN', None)
current_app.logger.info("Recieve notification form data: {}".format(post_data))
return "RECEIVED"
@notifications.route('/notifications/email/ses', methods=['POST'])
def process_ses_response():
client_name = 'SES'
try:
ses_request = json.loads(request.data)
if 'Type' in ses_request and ses_request['Type'] == 'SubscriptionConfirmation':
current_app.logger.info("SNS subscription confirmation url: {}".format(ses_request['SubscribeURL']))
errors = validate_callback_data(data=ses_request, fields=['Message'], client_name=client_name)
if errors:
raise InvalidRequest(errors, status_code=400)
ses_message = json.loads(ses_request['Message'])
errors = validate_callback_data(data=ses_message, fields=['notificationType'], client_name=client_name)
if errors:
raise InvalidRequest(errors, status_code=400)
notification_type = ses_message['notificationType']
if notification_type == 'Bounce':
if ses_message['bounce']['bounceType'] == 'Permanent':
notification_type = ses_message['bounce']['bounceType'] # permanent or not
else:
notification_type = 'Temporary'
try:
aws_response_dict = get_aws_responses(notification_type)
except KeyError:
error = "{} callback failed: status {} not found".format(client_name, notification_type)
raise InvalidRequest(error, status_code=400)
notification_status = aws_response_dict['notification_status']
try:
reference = ses_message['mail']['messageId']
notification = notifications_dao.update_notification_status_by_reference(
reference,
notification_status
)
if not notification:
error = "SES callback failed: notification either not found or already updated " \
"from sending. Status {} for notification reference {}".format(notification_status, reference)
raise InvalidRequest(error, status_code=404)
if not aws_response_dict['success']:
current_app.logger.info(
"SES delivery failed: notification id {} and reference {} has error found. Status {}".format(
notification.id,
reference,
aws_response_dict['message']
)
)
else:
current_app.logger.info('{} callback return status of {} for notification: {}'.format(
client_name,
notification_status,
notification.id))
statsd_client.incr('callback.ses.{}'.format(notification_status))
if notification.sent_at:
statsd_client.timing_with_dates(
'callback.ses.elapsed-time'.format(client_name.lower()),
datetime.utcnow(),
notification.sent_at
)
return jsonify(
result="success", message="SES callback succeeded"
), 200
except KeyError:
message = "SES callback failed: messageId missing"
raise InvalidRequest(message, status_code=400)
except ValueError as ex:
error = "{} callback failed: invalid json".format(client_name)
raise InvalidRequest(error, status_code=400)
@notifications.route('/notifications/sms/mmg', methods=['POST'])
def process_mmg_response():
client_name = 'MMG'
data = json.loads(request.data)
errors = validate_callback_data(data=data,
fields=['status', 'CID'],
client_name=client_name)
if errors:
raise InvalidRequest(errors, status_code=400)
success, errors = process_sms_client_response(status=str(data.get('status')),
reference=data.get('CID'),
client_name=client_name)
if errors:
raise InvalidRequest(errors, status_code=400)
else:
return jsonify(result='success', message=success), 200
@notifications.route('/notifications/sms/firetext', methods=['POST'])
def process_firetext_response():
client_name = 'Firetext'
errors = validate_callback_data(data=request.form,
fields=['status', 'reference'],
client_name=client_name)
if errors:
raise InvalidRequest(errors, status_code=400)
status = request.form.get('status')
success, errors = process_sms_client_response(status=status,
reference=request.form.get('reference'),
client_name=client_name)
if errors:
raise InvalidRequest(errors, status_code=400)
else:
return jsonify(result='success', message=success), 200
@notifications.route('/notifications/<uuid:notification_id>', methods=['GET'])
def get_notification_by_id(notification_id):
notification = notifications_dao.get_notification_with_personalisation(str(api_user.service_id),

View File

@@ -41,8 +41,7 @@ from app.dao import notifications_dao
from app.dao.provider_statistics_dao import get_fragment_count
from app.dao.users_dao import get_user_by_id
from app.errors import (
InvalidRequest,
register_errors)
InvalidRequest, register_errors)
from app.service import statistics
from app.service.utils import get_whitelist_objects
from app.schemas import (
@@ -57,11 +56,8 @@ from app.schemas import (
from app.utils import pagination_links, get_london_midnight_in_utc
from flask import Blueprint
from app.authentication.auth import requires_admin_auth
service_blueprint = Blueprint('service', __name__)
service_blueprint.before_request(requires_admin_auth)
register_errors(service_blueprint)

View File

@@ -17,7 +17,7 @@ from app.dao.services_dao import dao_fetch_service_by_id
from app.models import SMS_TYPE
from app.schemas import (template_schema, template_history_schema)
template = Blueprint('template', __name__, url_prefix='/service/<uuid:service_id>/template')
template_blueprint = Blueprint('template', __name__, url_prefix='/service/<uuid:service_id>/template')
from app.errors import (
register_errors,
@@ -25,7 +25,7 @@ from app.errors import (
)
from app.utils import get_template_instance
register_errors(template)
register_errors(template_blueprint)
def _content_count_greater_than_limit(content, template_type):
@@ -35,7 +35,7 @@ def _content_count_greater_than_limit(content, template_type):
return template.content_count > current_app.config.get('SMS_CHAR_COUNT_LIMIT')
@template.route('', methods=['POST'])
@template_blueprint.route('', methods=['POST'])
def create_template(service_id):
fetched_service = dao_fetch_service_by_id(service_id=service_id)
new_template = template_schema.load(request.get_json()).data
@@ -51,7 +51,7 @@ def create_template(service_id):
return jsonify(data=template_schema.dump(new_template).data), 201
@template.route('/<uuid:template_id>', methods=['POST'])
@template_blueprint.route('/<uuid:template_id>', methods=['POST'])
def update_template(service_id, template_id):
fetched_template = dao_get_template_by_id_and_service_id(template_id=template_id, service_id=service_id)
@@ -73,21 +73,21 @@ def update_template(service_id, template_id):
return jsonify(data=template_schema.dump(update_dict).data), 200
@template.route('', methods=['GET'])
@template_blueprint.route('', methods=['GET'])
def get_all_templates_for_service(service_id):
templates = dao_get_all_templates_for_service(service_id=service_id)
data = template_schema.dump(templates, many=True).data
return jsonify(data=data)
@template.route('/<uuid:template_id>', methods=['GET'])
@template_blueprint.route('/<uuid:template_id>', methods=['GET'])
def get_template_by_id_and_service_id(service_id, template_id):
fetched_template = dao_get_template_by_id_and_service_id(template_id=template_id, service_id=service_id)
data = template_schema.dump(fetched_template).data
return jsonify(data=data)
@template.route('/<uuid:template_id>/preview', methods=['GET'])
@template_blueprint.route('/<uuid:template_id>/preview', methods=['GET'])
def preview_template_by_id_and_service_id(service_id, template_id):
fetched_template = dao_get_template_by_id_and_service_id(template_id=template_id, service_id=service_id)
data = template_schema.dump(fetched_template).data
@@ -106,7 +106,7 @@ def preview_template_by_id_and_service_id(service_id, template_id):
return jsonify(data)
@template.route('/<uuid:template_id>/version/<int:version>')
@template_blueprint.route('/<uuid:template_id>/version/<int:version>')
def get_template_version(service_id, template_id, version):
data = template_history_schema.dump(
dao_get_template_by_id_and_service_id(
@@ -118,7 +118,7 @@ def get_template_version(service_id, template_id, version):
return jsonify(data=data)
@template.route('/<uuid:template_id>/versions')
@template_blueprint.route('/<uuid:template_id>/versions')
def get_template_versions(service_id, template_id):
data = template_history_schema.dump(
dao_get_template_versions(service_id=service_id, template_id=template_id),

View File

@@ -41,11 +41,11 @@ from app.errors import (
)
from app.utils import url_with_token
user = Blueprint('user', __name__)
register_errors(user)
user_blueprint = Blueprint('user', __name__)
register_errors(user_blueprint)
@user.route('', methods=['POST'])
@user_blueprint.route('', methods=['POST'])
def create_user():
user_to_create, errors = user_schema.load(request.get_json())
req_json = request.get_json()
@@ -56,7 +56,7 @@ def create_user():
return jsonify(data=user_schema.dump(user_to_create).data), 201
@user.route('/<uuid:user_id>', methods=['PUT'])
@user_blueprint.route('/<uuid:user_id>', methods=['PUT'])
def update_user(user_id):
user_to_update = get_user_by_id(user_id=user_id)
req_json = request.get_json()
@@ -73,7 +73,7 @@ def update_user(user_id):
return jsonify(data=user_schema.dump(user_to_update).data), 200
@user.route('/<uuid:user_id>', methods=['POST'])
@user_blueprint.route('/<uuid:user_id>', methods=['POST'])
def update_user_attribute(user_id):
user_to_update = get_user_by_id(user_id=user_id)
req_json = request.get_json()
@@ -84,14 +84,14 @@ def update_user_attribute(user_id):
return jsonify(data=user_schema.dump(user_to_update).data), 200
@user.route('/<uuid:user_id>/reset-failed-login-count', methods=['POST'])
@user_blueprint.route('/<uuid:user_id>/reset-failed-login-count', methods=['POST'])
def user_reset_failed_login_count(user_id):
user_to_update = get_user_by_id(user_id=user_id)
reset_failed_login_count(user_to_update)
return jsonify(data=user_schema.dump(user_to_update).data), 200
@user.route('/<uuid:user_id>/verify/password', methods=['POST'])
@user_blueprint.route('/<uuid:user_id>/verify/password', methods=['POST'])
def verify_user_password(user_id):
user_to_verify = get_user_by_id(user_id=user_id)
@@ -112,7 +112,7 @@ def verify_user_password(user_id):
raise InvalidRequest(errors, status_code=400)
@user.route('/<uuid:user_id>/verify/code', methods=['POST'])
@user_blueprint.route('/<uuid:user_id>/verify/code', methods=['POST'])
def verify_user_code(user_id):
user_to_verify = get_user_by_id(user_id=user_id)
@@ -151,7 +151,7 @@ def verify_user_code(user_id):
return jsonify({}), 204
@user.route('/<uuid:user_id>/sms-code', methods=['POST'])
@user_blueprint.route('/<uuid:user_id>/sms-code', methods=['POST'])
def send_user_sms_code(user_id):
user_to_send_to = get_user_by_id(user_id=user_id)
verify_code, errors = request_verify_code_schema.load(request.get_json())
@@ -187,7 +187,7 @@ def send_user_sms_code(user_id):
return jsonify({}), 204
@user.route('/<uuid:user_id>/change-email-verification', methods=['POST'])
@user_blueprint.route('/<uuid:user_id>/change-email-verification', methods=['POST'])
def send_user_confirm_new_email(user_id):
user_to_send_to = get_user_by_id(user_id=user_id)
email, errors = email_data_request_schema.load(request.get_json())
@@ -216,7 +216,7 @@ def send_user_confirm_new_email(user_id):
return jsonify({}), 204
@user.route('/<uuid:user_id>/email-verification', methods=['POST'])
@user_blueprint.route('/<uuid:user_id>/email-verification', methods=['POST'])
def send_user_email_verification(user_id):
user_to_send_to = get_user_by_id(user_id=user_id)
secret_code = create_secret_code()
@@ -244,7 +244,7 @@ def send_user_email_verification(user_id):
return jsonify({}), 204
@user.route('/<uuid:user_id>/email-already-registered', methods=['POST'])
@user_blueprint.route('/<uuid:user_id>/email-already-registered', methods=['POST'])
def send_already_registered_email(user_id):
to, errors = email_data_request_schema.load(request.get_json())
template = dao_get_template_by_id(current_app.config['ALREADY_REGISTERED_EMAIL_TEMPLATE_ID'])
@@ -270,15 +270,15 @@ def send_already_registered_email(user_id):
return jsonify({}), 204
@user.route('/<uuid:user_id>', methods=['GET'])
@user.route('', methods=['GET'])
@user_blueprint.route('/<uuid:user_id>', methods=['GET'])
@user_blueprint.route('', methods=['GET'])
def get_user(user_id=None):
users = get_user_by_id(user_id=user_id)
result = user_schema.dump(users, many=True) if isinstance(users, list) else user_schema.dump(users)
return jsonify(data=result.data)
@user.route('/<uuid:user_id>/service/<uuid:service_id>/permission', methods=['POST'])
@user_blueprint.route('/<uuid:user_id>/service/<uuid:service_id>/permission', methods=['POST'])
def set_permissions(user_id, service_id):
# TODO fix security hole, how do we verify that the user
# who is making this request has permission to make the request.
@@ -293,7 +293,7 @@ def set_permissions(user_id, service_id):
return jsonify({}), 204
@user.route('/email', methods=['GET'])
@user_blueprint.route('/email', methods=['GET'])
def get_by_email():
email = request.args.get('email')
if not email:
@@ -305,7 +305,7 @@ def get_by_email():
return jsonify(data=result.data)
@user.route('/reset-password', methods=['POST'])
@user_blueprint.route('/reset-password', methods=['POST'])
def send_user_reset_password():
email, errors = email_data_request_schema.load(request.get_json())
@@ -332,7 +332,7 @@ def send_user_reset_password():
return jsonify({}), 204
@user.route('/<uuid:user_id>/update-password', methods=['POST'])
@user_blueprint.route('/<uuid:user_id>/update-password', methods=['POST'])
def update_password(user_id):
user = get_user_by_id(user_id=user_id)
req_json = request.get_json()

View File

@@ -1,7 +1,6 @@
from flask import Blueprint
from app.v2.errors import register_errors
notification_blueprint = Blueprint("v2_notifications", __name__, url_prefix='/v2/notifications')
v2_notification_blueprint = Blueprint("v2_notifications", __name__, url_prefix='/v2/notifications')
register_errors(notification_blueprint)
register_errors(v2_notification_blueprint)

View File

@@ -6,11 +6,11 @@ from werkzeug.exceptions import abort
from app import api_user
from app.dao import notifications_dao
from app.schema_validation import validate
from app.v2.notifications import notification_blueprint
from app.v2.notifications import v2_notification_blueprint
from app.v2.notifications.notification_schemas import get_notifications_request
@notification_blueprint.route("/<id>", methods=['GET'])
@v2_notification_blueprint.route("/<id>", methods=['GET'])
def get_notification_by_id(id):
try:
casted_id = uuid.UUID(id)
@@ -23,7 +23,7 @@ def get_notification_by_id(id):
return jsonify(notification.serialize()), 200
@notification_blueprint.route("", methods=['GET'])
@v2_notification_blueprint.route("", methods=['GET'])
def get_notifications():
_data = request.args.to_dict(flat=False)

View File

@@ -15,13 +15,13 @@ from app.notifications.validators import (check_service_message_limit,
validate_and_format_recipient)
from app.schema_validation import validate
from app.v2.errors import BadRequestError
from app.v2.notifications import notification_blueprint
from app.v2.notifications import v2_notification_blueprint
from app.v2.notifications.notification_schemas import (post_sms_request,
create_post_sms_response_from_notification, post_email_request,
create_post_email_response_from_notification)
@notification_blueprint.route('/<notification_type>', methods=['POST'])
@v2_notification_blueprint.route('/<notification_type>', methods=['POST'])
def post_notification(notification_type):
if notification_type == EMAIL_TYPE:
form = validate(request.get_json(), post_email_request)

View File

@@ -35,7 +35,7 @@ def test_should_not_allow_request_with_incorrect_token(client, url):
response = client.get(url, headers={'Authorization': 'Bearer 1234'})
assert response.status_code == 403
data = json.loads(response.get_data())
assert data['message'] == {"token": ['Invalid token: signature']}
assert data['message'] == {"token": ['Invalid token: signature, api token is not valid']}
@pytest.mark.parametrize('url', ['/service', '/notifications'])
@@ -59,8 +59,14 @@ def test_should_not_allow_request_with_no_iss(client, url):
assert data['message'] == {"token": ['Invalid token: iss field not provided']}
@pytest.mark.parametrize('url', ['/service', '/notifications'])
def test_should_not_allow_request_with_no_iat(client, sample_api_key, url):
@pytest.mark.parametrize('url, auth_method',
[('/service', 'requires_admin_auth'),
('/notifications', 'requires_auth')])
def test_should_not_allow_request_with_no_iat(client, sample_api_key, url, auth_method):
if auth_method == 'requires_admin_auth':
iss = current_app.config['ADMIN_CLIENT_USER_NAME']
if auth_method == 'requires_auth':
iss = str(sample_api_key.service_id)
# code copied from notifications_python_client.authentication.py::create_jwt_token
headers = {
"typ": 'JWT',
@@ -68,7 +74,7 @@ def test_should_not_allow_request_with_no_iat(client, sample_api_key, url):
}
claims = {
'iss': str(sample_api_key.service_id)
'iss': iss
# 'iat': not provided
}
@@ -80,13 +86,12 @@ def test_should_not_allow_request_with_no_iat(client, sample_api_key, url):
assert data['message'] == {"token": ['Invalid token: signature, api token is not valid']}
@pytest.mark.parametrize('url', ['/service', '/notifications'])
def test_should_not_allow_invalid_secret(client, sample_api_key, url):
def test_should_not_allow_invalid_secret(client, sample_api_key):
token = create_jwt_token(
secret="not-so-secret",
client_id=str(sample_api_key.service_id))
response = client.get(
url,
'/notifications',
headers={'Authorization': "Bearer {}".format(token)}
)
assert response.status_code == 403
@@ -101,12 +106,11 @@ def test_should_allow_valid_token(client, sample_api_key, scheme):
assert response.status_code == 200
@pytest.mark.parametrize('url', ['/service', '/notifications'])
def test_should_not_allow_service_id_that_is_not_the_wrong_data_type(client, sample_api_key, url):
def test_should_not_allow_service_id_that_is_not_the_wrong_data_type(client, sample_api_key):
token = create_jwt_token(secret=get_unsigned_secrets(sample_api_key.service_id)[0],
client_id=str('not-a-valid-id'))
response = client.get(
url,
'/notifications',
headers={'Authorization': "Bearer {}".format(token)}
)
assert response.status_code == 403
@@ -208,7 +212,23 @@ def test_authentication_returns_error_when_admin_client_has_no_secrets(client):
headers={'Authorization': 'Bearer {}'.format(token)})
assert response.status_code == 403
error_message = json.loads(response.get_data())
assert error_message['message'] == {"token": ['Invalid token: signature']}
assert error_message['message'] == {"token": ["Invalid token: signature, api token is not valid"]}
current_app.config['ADMIN_CLIENT_SECRET'] = api_secret
def test_authentication_returns_error_when_admin_client_secret_is_invalid(client):
api_secret = current_app.config.get('ADMIN_CLIENT_SECRET')
token = create_jwt_token(
secret=api_secret,
client_id=current_app.config.get('ADMIN_CLIENT_USER_NAME')
)
current_app.config['ADMIN_CLIENT_SECRET'] = 'something-wrong'
response = client.get(
'/service',
headers={'Authorization': 'Bearer {}'.format(token)})
assert response.status_code == 403
error_message = json.loads(response.get_data())
assert error_message['message'] == {"token": ["Invalid token: signature, api token is not valid"]}
current_app.config['ADMIN_CLIENT_SECRET'] = api_secret

View File

@@ -17,11 +17,11 @@ from app.dao.templates_dao import dao_update_template
from app.models import NOTIFICATION_STATUS_TYPES, JOB_STATUS_TYPES, JOB_STATUS_PENDING
def test_get_job_with_invalid_service_id_returns404(notify_api, sample_api_key, sample_service):
def test_get_job_with_invalid_service_id_returns404(notify_api, sample_service):
with notify_api.test_request_context():
with notify_api.test_client() as client:
path = '/service/{}/job'.format(sample_service.id)
auth_header = create_authorization_header(service_id=sample_service.id)
auth_header = create_authorization_header()
response = client.get(path, headers=[auth_header])
assert response.status_code == 200
resp_json = json.loads(response.get_data(as_text=True))
@@ -33,7 +33,7 @@ def test_get_job_with_invalid_job_id_returns404(notify_api, sample_template):
with notify_api.test_request_context():
with notify_api.test_client() as client:
path = '/service/{}/job/{}'.format(service_id, "bad-id")
auth_header = create_authorization_header(service_id=sample_template.service.id)
auth_header = create_authorization_header()
response = client.get(path, headers=[auth_header])
assert response.status_code == 404
resp_json = json.loads(response.get_data(as_text=True))
@@ -46,7 +46,7 @@ def test_get_job_with_unknown_id_returns404(notify_api, sample_template, fake_uu
with notify_api.test_request_context():
with notify_api.test_client() as client:
path = '/service/{}/job/{}'.format(service_id, fake_uuid)
auth_header = create_authorization_header(service_id=sample_template.service.id)
auth_header = create_authorization_header()
response = client.get(path, headers=[auth_header])
assert response.status_code == 404
resp_json = json.loads(response.get_data(as_text=True))
@@ -62,7 +62,7 @@ def test_get_job_by_id(notify_api, sample_job):
with notify_api.test_request_context():
with notify_api.test_client() as client:
path = '/service/{}/job/{}'.format(service_id, job_id)
auth_header = create_authorization_header(service_id=sample_job.service.id)
auth_header = create_authorization_header()
response = client.get(path, headers=[auth_header])
assert response.status_code == 200
resp_json = json.loads(response.get_data(as_text=True))
@@ -75,7 +75,7 @@ def test_cancel_job(notify_api, sample_scheduled_job):
service_id = sample_scheduled_job.service.id
with notify_api.test_request_context(), notify_api.test_client() as client:
path = '/service/{}/job/{}/cancel'.format(service_id, job_id)
auth_header = create_authorization_header(service_id=service_id)
auth_header = create_authorization_header()
response = client.post(path, headers=[auth_header])
assert response.status_code == 200
resp_json = json.loads(response.get_data(as_text=True))
@@ -89,7 +89,7 @@ def test_cant_cancel_normal_job(notify_api, sample_job, mocker):
with notify_api.test_request_context(), notify_api.test_client() as client:
mock_update = mocker.patch('app.dao.jobs_dao.dao_update_job')
path = '/service/{}/job/{}/cancel'.format(service_id, job_id)
auth_header = create_authorization_header(service_id=service_id)
auth_header = create_authorization_header()
response = client.post(path, headers=[auth_header])
assert response.status_code == 404
assert mock_update.call_count == 0
@@ -108,7 +108,7 @@ def test_create_unscheduled_job(notify_api, sample_template, mocker, fake_uuid):
'created_by': str(sample_template.created_by.id)
}
path = '/service/{}/job'.format(sample_template.service.id)
auth_header = create_authorization_header(service_id=sample_template.service.id)
auth_header = create_authorization_header()
headers = [('Content-Type', 'application/json'), auth_header]
response = client.post(
@@ -149,7 +149,7 @@ def test_create_scheduled_job(notify_api, sample_template, mocker, fake_uuid):
'scheduled_for': scheduled_date
}
path = '/service/{}/job'.format(sample_template.service.id)
auth_header = create_authorization_header(service_id=sample_template.service.id)
auth_header = create_authorization_header()
headers = [('Content-Type', 'application/json'), auth_header]
response = client.post(
@@ -202,7 +202,7 @@ def test_should_not_create_scheduled_job_more_then_24_hours_hence(notify_api, sa
'scheduled_for': scheduled_date
}
path = '/service/{}/job'.format(sample_template.service.id)
auth_header = create_authorization_header(service_id=sample_template.service.id)
auth_header = create_authorization_header()
headers = [('Content-Type', 'application/json'), auth_header]
print(json.dumps(data))
@@ -237,7 +237,7 @@ def test_should_not_create_scheduled_job_in_the_past(notify_api, sample_template
'scheduled_for': scheduled_date
}
path = '/service/{}/job'.format(sample_template.service.id)
auth_header = create_authorization_header(service_id=sample_template.service.id)
auth_header = create_authorization_header()
headers = [('Content-Type', 'application/json'), auth_header]
print(json.dumps(data))
@@ -263,7 +263,7 @@ def test_create_job_returns_400_if_missing_data(notify_api, sample_template, moc
'template': str(sample_template.id)
}
path = '/service/{}/job'.format(sample_template.service.id)
auth_header = create_authorization_header(service_id=sample_template.service.id)
auth_header = create_authorization_header()
headers = [('Content-Type', 'application/json'), auth_header]
response = client.post(
path,
@@ -288,7 +288,7 @@ def test_create_job_returns_404_if_template_does_not_exist(notify_api, sample_se
'template': str(sample_service.id)
}
path = '/service/{}/job'.format(sample_service.id)
auth_header = create_authorization_header(service_id=sample_service.id)
auth_header = create_authorization_header()
headers = [('Content-Type', 'application/json'), auth_header]
response = client.post(
path,
@@ -310,7 +310,7 @@ def test_create_job_returns_404_if_missing_service(notify_api, sample_template,
random_id = str(uuid.uuid4())
data = {'template': str(sample_template.id)}
path = '/service/{}/job'.format(random_id)
auth_header = create_authorization_header(service_id=sample_template.service.id)
auth_header = create_authorization_header()
headers = [('Content-Type', 'application/json'), auth_header]
response = client.post(
path,
@@ -335,7 +335,7 @@ def test_create_job_returns_400_if_archived_template(notify_api, sample_template
'template': str(sample_template.id)
}
path = '/service/{}/job'.format(sample_template.service.id)
auth_header = create_authorization_header(service_id=sample_template.service.id)
auth_header = create_authorization_header()
headers = [('Content-Type', 'application/json'), auth_header]
response = client.post(
path,
@@ -455,7 +455,7 @@ def test_get_job_by_id(notify_api, sample_job):
with notify_api.test_request_context():
with notify_api.test_client() as client:
path = '/service/{}/job/{}'.format(service_id, job_id)
auth_header = create_authorization_header(service_id=sample_job.service.id)
auth_header = create_authorization_header()
response = client.get(path, headers=[auth_header])
assert response.status_code == 200
resp_json = json.loads(response.get_data(as_text=True))
@@ -480,7 +480,7 @@ def test_get_job_by_id_should_return_statistics(notify_db, notify_db_session, no
with notify_api.test_request_context():
with notify_api.test_client() as client:
path = '/service/{}/job/{}'.format(service_id, job_id)
auth_header = create_authorization_header(service_id=sample_job.service.id)
auth_header = create_authorization_header()
response = client.get(path, headers=[auth_header])
assert response.status_code == 200
resp_json = json.loads(response.get_data(as_text=True))
@@ -514,7 +514,7 @@ def test_get_job_by_id_should_return_summed_statistics(notify_db, notify_db_sess
with notify_api.test_request_context():
with notify_api.test_client() as client:
path = '/service/{}/job/{}'.format(service_id, job_id)
auth_header = create_authorization_header(service_id=sample_job.service.id)
auth_header = create_authorization_header()
response = client.get(path, headers=[auth_header])
assert response.status_code == 200
resp_json = json.loads(response.get_data(as_text=True))
@@ -535,7 +535,7 @@ def test_get_jobs(notify_api, notify_db, notify_db_session, sample_template):
with notify_api.test_request_context():
with notify_api.test_client() as client:
path = '/service/{}/job'.format(service_id)
auth_header = create_authorization_header(service_id=service_id)
auth_header = create_authorization_header()
response = client.get(path, headers=[auth_header])
assert response.status_code == 200
resp_json = json.loads(response.get_data(as_text=True))
@@ -561,7 +561,7 @@ def test_get_jobs_with_limit_days(notify_api, notify_db, notify_db_session, samp
with notify_api.test_request_context():
with notify_api.test_client() as client:
path = '/service/{}/job'.format(service_id)
auth_header = create_authorization_header(service_id=service_id)
auth_header = create_authorization_header()
response = client.get(path, headers=[auth_header], query_string={'limit_days': 5})
assert response.status_code == 200
resp_json = json.loads(response.get_data(as_text=True))
@@ -584,7 +584,7 @@ def test_get_jobs_should_return_statistics(notify_db, notify_db_session, notify_
with notify_api.test_request_context():
with notify_api.test_client() as client:
path = '/service/{}/job'.format(sample_service.id)
auth_header = create_authorization_header(service_id=str(sample_service.id))
auth_header = create_authorization_header()
response = client.get(path, headers=[auth_header])
assert response.status_code == 200
resp_json = json.loads(response.get_data(as_text=True))
@@ -609,7 +609,7 @@ def test_get_jobs_should_return_no_stats_if_no_rows_in_notifications(
with notify_api.test_request_context():
with notify_api.test_client() as client:
path = '/service/{}/job'.format(sample_service.id)
auth_header = create_authorization_header(service_id=str(sample_service.id))
auth_header = create_authorization_header()
response = client.get(path, headers=[auth_header])
assert response.status_code == 200
resp_json = json.loads(response.get_data(as_text=True))
@@ -629,7 +629,7 @@ def test_get_jobs_should_paginate(
create_10_jobs(notify_db, notify_db_session, sample_template.service, sample_template)
path = '/service/{}/job'.format(sample_template.service_id)
auth_header = create_authorization_header(service_id=str(sample_template.service_id))
auth_header = create_authorization_header()
with set_config(client.application, 'PAGE_SIZE', 2):
response = client.get(path, headers=[auth_header])
@@ -654,7 +654,7 @@ def test_get_jobs_accepts_page_parameter(
create_10_jobs(notify_db, notify_db_session, sample_template.service, sample_template)
path = '/service/{}/job'.format(sample_template.service_id)
auth_header = create_authorization_header(service_id=str(sample_template.service_id))
auth_header = create_authorization_header()
with set_config(client.application, 'PAGE_SIZE', 2):
response = client.get(path, headers=[auth_header], query_string={'page': 2})

View File

@@ -399,230 +399,6 @@ def test_mmg_callback_returns_200_when_notification_id_not_found_or_already_upda
assert response.status_code == 200
def test_ses_callback_should_not_need_auth(client):
response = client.post(
path='/notifications/email/ses',
data=ses_notification_callback(),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
assert response.status_code == 404
def test_ses_callback_should_fail_if_invalid_json(client):
response = client.post(
path='/notifications/email/ses',
data="nonsense",
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 400
assert json_resp['result'] == 'error'
assert json_resp['message'] == 'SES callback failed: invalid json'
def test_ses_callback_should_fail_if_invalid_notification_type(client):
response = client.post(
path='/notifications/email/ses',
data=ses_invalid_notification_type_callback(),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 400
assert json_resp['result'] == 'error'
assert json_resp['message'] == 'SES callback failed: status Unknown not found'
def test_ses_callback_should_fail_if_missing_message_id(client):
response = client.post(
path='/notifications/email/ses',
data=ses_missing_notification_id_callback(),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 400
assert json_resp['result'] == 'error'
assert json_resp['message'] == 'SES callback failed: messageId missing'
def test_ses_callback_should_fail_if_notification_cannot_be_found(notify_db, notify_db_session, client):
response = client.post(
path='/notifications/email/ses',
data=ses_invalid_notification_id_callback(),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 404
assert json_resp['result'] == 'error'
assert json_resp['message'] == 'SES callback failed: notification either not found or already updated from sending. Status delivered for notification reference missing' # noqa
def test_ses_callback_should_update_notification_status(
client,
notify_db,
notify_db_session,
sample_email_template,
mocker):
with freeze_time('2001-01-01T12:00:00'):
mocker.patch('app.statsd_client.incr')
mocker.patch('app.statsd_client.timing_with_dates')
notification = create_sample_notification(
notify_db,
notify_db_session,
template=sample_email_template,
reference='ref',
status='sending',
sent_at=datetime.utcnow()
)
assert get_notification_by_id(notification.id).status == 'sending'
response = client.post(
path='/notifications/email/ses',
data=ses_notification_callback(),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 200
assert json_resp['result'] == 'success'
assert json_resp['message'] == 'SES callback succeeded'
assert get_notification_by_id(notification.id).status == 'delivered'
app.statsd_client.timing_with_dates.assert_any_call(
"callback.ses.elapsed-time", datetime.utcnow(), notification.sent_at
)
app.statsd_client.incr.assert_any_call("callback.ses.delivered")
def test_ses_callback_should_update_multiple_notification_status_sent(
client,
notify_db,
notify_db_session,
sample_email_template,
mocker):
notification1 = create_sample_notification(
notify_db,
notify_db_session,
template=sample_email_template,
reference='ref1',
sent_at=datetime.utcnow(),
status='sending')
notification2 = create_sample_notification(
notify_db,
notify_db_session,
template=sample_email_template,
reference='ref2',
sent_at=datetime.utcnow(),
status='sending')
notification3 = create_sample_notification(
notify_db,
notify_db_session,
template=sample_email_template,
reference='ref3',
sent_at=datetime.utcnow(),
status='sending')
resp1 = client.post(
path='/notifications/email/ses',
data=ses_notification_callback(ref='ref1'),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
resp2 = client.post(
path='/notifications/email/ses',
data=ses_notification_callback(ref='ref2'),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
resp3 = client.post(
path='/notifications/email/ses',
data=ses_notification_callback(ref='ref3'),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
assert resp1.status_code == 200
assert resp2.status_code == 200
assert resp3.status_code == 200
def test_ses_callback_should_set_status_to_temporary_failure(client,
notify_db,
notify_db_session,
sample_email_template):
notification = create_sample_notification(
notify_db,
notify_db_session,
template=sample_email_template,
reference='ref',
status='sending',
sent_at=datetime.utcnow()
)
assert get_notification_by_id(notification.id).status == 'sending'
response = client.post(
path='/notifications/email/ses',
data=ses_soft_bounce_callback(),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 200
assert json_resp['result'] == 'success'
assert json_resp['message'] == 'SES callback succeeded'
assert get_notification_by_id(notification.id).status == 'temporary-failure'
def test_ses_callback_should_not_set_status_once_status_is_delivered(client,
notify_db,
notify_db_session,
sample_email_template):
notification = create_sample_notification(
notify_db,
notify_db_session,
template=sample_email_template,
reference='ref',
status='delivered',
sent_at=datetime.utcnow()
)
assert get_notification_by_id(notification.id).status == 'delivered'
response = client.post(
path='/notifications/email/ses',
data=ses_soft_bounce_callback(),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 404
assert json_resp['result'] == 'error'
assert json_resp['message'] == 'SES callback failed: notification either not found or already updated from sending. Status temporary-failure for notification reference ref' # noqa
assert get_notification_by_id(notification.id).status == 'delivered'
def test_ses_callback_should_set_status_to_permanent_failure(client,
notify_db,
notify_db_session,
sample_email_template):
notification = create_sample_notification(
notify_db,
notify_db_session,
template=sample_email_template,
reference='ref',
status='sending',
sent_at=datetime.utcnow()
)
assert get_notification_by_id(notification.id).status == 'sending'
response = client.post(
path='/notifications/email/ses',
data=ses_hard_bounce_callback(),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 200
assert json_resp['result'] == 'success'
assert json_resp['message'] == 'SES callback succeeded'
assert get_notification_by_id(notification.id).status == 'permanent-failure'
def test_process_mmg_response_records_statsd(notify_db, notify_db_session, client, mocker):
with freeze_time('2001-01-01T12:00:00'):
@@ -670,31 +446,5 @@ def test_firetext_callback_should_record_statsd(client, notify_db, notify_db_ses
app.statsd_client.incr.assert_any_call("callback.firetext.delivered")
def ses_notification_callback(ref='ref'):
return str.encode(
'{\n "Type" : "Notification",\n "MessageId" : "%(ref)s",\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",\n "Message" : "{\\"notificationType\\":\\"Delivery\\",\\"mail\\":{\\"timestamp\\":\\"2016-03-14T12:35:25.909Z\\",\\"source\\":\\"test@test-domain.com\\",\\"sourceArn\\":\\"arn:aws:ses:eu-west-1:123456789012:identity/testing-notify\\",\\"sendingAccountId\\":\\"123456789012\\",\\"messageId\\":\\"%(ref)s\\",\\"destination\\":[\\"testing@digital.cabinet-office.gov.uk\\"]},\\"delivery\\":{\\"timestamp\\":\\"2016-03-14T12:35:26.567Z\\",\\"processingTimeMillis\\":658,\\"recipients\\":[\\"testing@digital.cabinet-office.gov.uk\\"],\\"smtpResponse\\":\\"250 2.0.0 OK 1457958926 uo5si26480932wjc.221 - gsmtp\\",\\"reportingMTA\\":\\"a6-238.smtp-out.eu-west-1.amazonses.com\\"}}",\n "Timestamp" : "2016-03-14T12:35:26.665Z",\n "SignatureVersion" : "1",\n "Signature" : "X8d7eTAOZ6wlnrdVVPYanrAlsX0SMPfOzhoTEBnQqYkrNWTqQY91C0f3bxtPdUhUtOowyPAOkTQ4KnZuzphfhVb2p1MyVYMxNKcBFB05/qaCX99+92fjw4x9LeUOwyGwMv5F0Vkfi5qZCcEw69uVrhYLVSTFTrzi/yCtru+yFULMQ6UhbY09GwiP6hjxZMVr8aROQy5lLHglqQzOuSZ4KeD85JjifHdKzlx8jjQ+uj+FLzHXPMAPmPU1JK9kpoHZ1oPshAFgPDpphJe+HwcJ8ezmk+3AEUr3wWli3xF+49y8Z2anASSVp6YI2YP95UT8Rlh3qT3T+V9V8rbSVislxA==",\n "SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-bb750dd426d95ee9390147a5624348ee.pem",\n "UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:302763885840:preview-emails:d6aad3ef-83d6-4cf3-a470-54e2e75916da"\n}' % {'ref': ref} # noqa
)
def ses_invalid_notification_id_callback():
return b'{\n "Type" : "Notification",\n "MessageId" : "missing",\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",\n "Message" : "{\\"notificationType\\":\\"Delivery\\",\\"mail\\":{\\"timestamp\\":\\"2016-03-14T12:35:25.909Z\\",\\"source\\":\\"test@test-domain.com\\",\\"sourceArn\\":\\"arn:aws:ses:eu-west-1:123456789012:identity/testing-notify\\",\\"sendingAccountId\\":\\"123456789012\\",\\"messageId\\":\\"missing\\",\\"destination\\":[\\"testing@digital.cabinet-office.gov.uk\\"]},\\"delivery\\":{\\"timestamp\\":\\"2016-03-14T12:35:26.567Z\\",\\"processingTimeMillis\\":658,\\"recipients\\":[\\"testing@digital.cabinet-office.gov.uk\\"],\\"smtpResponse\\":\\"250 2.0.0 OK 1457958926 uo5si26480932wjc.221 - gsmtp\\",\\"reportingMTA\\":\\"a6-238.smtp-out.eu-west-1.amazonses.com\\"}}",\n "Timestamp" : "2016-03-14T12:35:26.665Z",\n "SignatureVersion" : "1",\n "Signature" : "X8d7eTAOZ6wlnrdVVPYanrAlsX0SMPfOzhoTEBnQqYkrNWTqQY91C0f3bxtPdUhUtOowyPAOkTQ4KnZuzphfhVb2p1MyVYMxNKcBFB05/qaCX99+92fjw4x9LeUOwyGwMv5F0Vkfi5qZCcEw69uVrhYLVSTFTrzi/yCtru+yFULMQ6UhbY09GwiP6hjxZMVr8aROQy5lLHglqQzOuSZ4KeD85JjifHdKzlx8jjQ+uj+FLzHXPMAPmPU1JK9kpoHZ1oPshAFgPDpphJe+HwcJ8ezmk+3AEUr3wWli3xF+49y8Z2anASSVp6YI2YP95UT8Rlh3qT3T+V9V8rbSVislxA==",\n "SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-bb750dd426d95ee9390147a5624348ee.pem",\n "UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:302763885840:preview-emails:d6aad3ef-83d6-4cf3-a470-54e2e75916da"\n}' # noqa
def ses_missing_notification_id_callback():
return b'{\n "Type" : "Notification",\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",\n "Message" : "{\\"notificationType\\":\\"Delivery\\",\\"mail\\":{\\"timestamp\\":\\"2016-03-14T12:35:25.909Z\\",\\"source\\":\\"test@test-domain.com\\",\\"sourceArn\\":\\"arn:aws:ses:eu-west-1:123456789012:identity/testing-notify\\",\\"sendingAccountId\\":\\"123456789012\\",\\"destination\\":[\\"testing@digital.cabinet-office.gov.uk\\"]},\\"delivery\\":{\\"timestamp\\":\\"2016-03-14T12:35:26.567Z\\",\\"processingTimeMillis\\":658,\\"recipients\\":[\\"testing@digital.cabinet-office.gov.uk\\"],\\"smtpResponse\\":\\"250 2.0.0 OK 1457958926 uo5si26480932wjc.221 - gsmtp\\",\\"reportingMTA\\":\\"a6-238.smtp-out.eu-west-1.amazonses.com\\"}}",\n "Timestamp" : "2016-03-14T12:35:26.665Z",\n "SignatureVersion" : "1",\n "Signature" : "X8d7eTAOZ6wlnrdVVPYanrAlsX0SMPfOzhoTEBnQqYkrNWTqQY91C0f3bxtPdUhUtOowyPAOkTQ4KnZuzphfhVb2p1MyVYMxNKcBFB05/qaCX99+92fjw4x9LeUOwyGwMv5F0Vkfi5qZCcEw69uVrhYLVSTFTrzi/yCtru+yFULMQ6UhbY09GwiP6hjxZMVr8aROQy5lLHglqQzOuSZ4KeD85JjifHdKzlx8jjQ+uj+FLzHXPMAPmPU1JK9kpoHZ1oPshAFgPDpphJe+HwcJ8ezmk+3AEUr3wWli3xF+49y8Z2anASSVp6YI2YP95UT8Rlh3qT3T+V9V8rbSVislxA==",\n "SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-bb750dd426d95ee9390147a5624348ee.pem",\n "UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:302763885840:preview-emails:d6aad3ef-83d6-4cf3-a470-54e2e75916da"\n}' # noqa
def ses_invalid_notification_type_callback():
return b'{\n "Type" : "Notification",\n "MessageId" : "ref",\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",\n "Message" : "{\\"notificationType\\":\\"Unknown\\",\\"mail\\":{\\"timestamp\\":\\"2016-03-14T12:35:25.909Z\\",\\"source\\":\\"test@test-domain.com\\",\\"sourceArn\\":\\"arn:aws:ses:eu-west-1:123456789012:identity/testing-notify\\",\\"sendingAccountId\\":\\"123456789012\\",\\"destination\\":[\\"testing@digital.cabinet-office.gov.uk\\"]},\\"delivery\\":{\\"timestamp\\":\\"2016-03-14T12:35:26.567Z\\",\\"processingTimeMillis\\":658,\\"recipients\\":[\\"testing@digital.cabinet-office.gov.uk\\"],\\"smtpResponse\\":\\"250 2.0.0 OK 1457958926 uo5si26480932wjc.221 - gsmtp\\",\\"reportingMTA\\":\\"a6-238.smtp-out.eu-west-1.amazonses.com\\"}}",\n "Timestamp" : "2016-03-14T12:35:26.665Z",\n "SignatureVersion" : "1",\n "Signature" : "X8d7eTAOZ6wlnrdVVPYanrAlsX0SMPfOzhoTEBnQqYkrNWTqQY91C0f3bxtPdUhUtOowyPAOkTQ4KnZuzphfhVb2p1MyVYMxNKcBFB05/qaCX99+92fjw4x9LeUOwyGwMv5F0Vkfi5qZCcEw69uVrhYLVSTFTrzi/yCtru+yFULMQ6UhbY09GwiP6hjxZMVr8aROQy5lLHglqQzOuSZ4KeD85JjifHdKzlx8jjQ+uj+FLzHXPMAPmPU1JK9kpoHZ1oPshAFgPDpphJe+HwcJ8ezmk+3AEUr3wWli3xF+49y8Z2anASSVp6YI2YP95UT8Rlh3qT3T+V9V8rbSVislxA==",\n "SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-bb750dd426d95ee9390147a5624348ee.pem",\n "UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:302763885840:preview-emails:d6aad3ef-83d6-4cf3-a470-54e2e75916da"\n}' # noqa
def ses_hard_bounce_callback():
return b'{\n "Type" : "Notification",\n "MessageId" : "ref",\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",\n "Message" : "{\\"notificationType\\":\\"Bounce\\",\\"bounce\\":{\\"bounceType\\":\\"Permanent\\",\\"bounceSubType\\":\\"General\\"}, \\"mail\\":{\\"messageId\\":\\"ref\\",\\"timestamp\\":\\"2016-03-14T12:35:25.909Z\\",\\"source\\":\\"test@test-domain.com\\",\\"sourceArn\\":\\"arn:aws:ses:eu-west-1:123456789012:identity/testing-notify\\",\\"sendingAccountId\\":\\"123456789012\\",\\"destination\\":[\\"testing@digital.cabinet-office.gov.uk\\"]},\\"delivery\\":{\\"timestamp\\":\\"2016-03-14T12:35:26.567Z\\",\\"processingTimeMillis\\":658,\\"recipients\\":[\\"testing@digital.cabinet-office.gov.uk\\"],\\"smtpResponse\\":\\"250 2.0.0 OK 1457958926 uo5si26480932wjc.221 - gsmtp\\",\\"reportingMTA\\":\\"a6-238.smtp-out.eu-west-1.amazonses.com\\"}}",\n "Timestamp" : "2016-03-14T12:35:26.665Z",\n "SignatureVersion" : "1",\n "Signature" : "X8d7eTAOZ6wlnrdVVPYanrAlsX0SMPfOzhoTEBnQqYkrNWTqQY91C0f3bxtPdUhUtOowyPAOkTQ4KnZuzphfhVb2p1MyVYMxNKcBFB05/qaCX99+92fjw4x9LeUOwyGwMv5F0Vkfi5qZCcEw69uVrhYLVSTFTrzi/yCtru+yFULMQ6UhbY09GwiP6hjxZMVr8aROQy5lLHglqQzOuSZ4KeD85JjifHdKzlx8jjQ+uj+FLzHXPMAPmPU1JK9kpoHZ1oPshAFgPDpphJe+HwcJ8ezmk+3AEUr3wWli3xF+49y8Z2anASSVp6YI2YP95UT8Rlh3qT3T+V9V8rbSVislxA==",\n "SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-bb750dd426d95ee9390147a5624348ee.pem",\n "UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:302763885840:preview-emails:d6aad3ef-83d6-4cf3-a470-54e2e75916da"\n}' # noqa
def ses_soft_bounce_callback():
return b'{\n "Type" : "Notification",\n "MessageId" : "ref",\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",\n "Message" : "{\\"notificationType\\":\\"Bounce\\",\\"bounce\\":{\\"bounceType\\":\\"Undetermined\\",\\"bounceSubType\\":\\"General\\"}, \\"mail\\":{\\"messageId\\":\\"ref\\",\\"timestamp\\":\\"2016-03-14T12:35:25.909Z\\",\\"source\\":\\"test@test-domain.com\\",\\"sourceArn\\":\\"arn:aws:ses:eu-west-1:123456789012:identity/testing-notify\\",\\"sendingAccountId\\":\\"123456789012\\",\\"destination\\":[\\"testing@digital.cabinet-office.gov.uk\\"]},\\"delivery\\":{\\"timestamp\\":\\"2016-03-14T12:35:26.567Z\\",\\"processingTimeMillis\\":658,\\"recipients\\":[\\"testing@digital.cabinet-office.gov.uk\\"],\\"smtpResponse\\":\\"250 2.0.0 OK 1457958926 uo5si26480932wjc.221 - gsmtp\\",\\"reportingMTA\\":\\"a6-238.smtp-out.eu-west-1.amazonses.com\\"}}",\n "Timestamp" : "2016-03-14T12:35:26.665Z",\n "SignatureVersion" : "1",\n "Signature" : "X8d7eTAOZ6wlnrdVVPYanrAlsX0SMPfOzhoTEBnQqYkrNWTqQY91C0f3bxtPdUhUtOowyPAOkTQ4KnZuzphfhVb2p1MyVYMxNKcBFB05/qaCX99+92fjw4x9LeUOwyGwMv5F0Vkfi5qZCcEw69uVrhYLVSTFTrzi/yCtru+yFULMQ6UhbY09GwiP6hjxZMVr8aROQy5lLHglqQzOuSZ4KeD85JjifHdKzlx8jjQ+uj+FLzHXPMAPmPU1JK9kpoHZ1oPshAFgPDpphJe+HwcJ8ezmk+3AEUr3wWli3xF+49y8Z2anASSVp6YI2YP95UT8Rlh3qT3T+V9V8rbSVislxA==",\n "SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-bb750dd426d95ee9390147a5624348ee.pem",\n "UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:302763885840:preview-emails:d6aad3ef-83d6-4cf3-a470-54e2e75916da"\n}' # noqa
def get_notification_stats(service_id):
return NotificationStatistics.query.filter_by(service_id=service_id).one()

View File

@@ -0,0 +1,258 @@
from datetime import datetime
from flask import json
from freezegun import freeze_time
from app import statsd_client
from app.dao.notifications_dao import get_notification_by_id
from tests.app.conftest import sample_notification as create_sample_notification
def test_ses_callback_should_not_need_auth(client):
response = client.post(
path='/notifications/email/ses',
data=ses_notification_callback(),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
assert response.status_code == 404
def test_ses_callback_should_fail_if_invalid_json(client):
response = client.post(
path='/notifications/email/ses',
data="nonsense",
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 400
assert json_resp['result'] == 'error'
assert json_resp['message'] == 'SES callback failed: invalid json'
def test_ses_callback_should_fail_if_invalid_notification_type(client):
response = client.post(
path='/notifications/email/ses',
data=ses_invalid_notification_type_callback(),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 400
assert json_resp['result'] == 'error'
assert json_resp['message'] == 'SES callback failed: status Unknown not found'
def test_ses_callback_should_fail_if_missing_message_id(client):
response = client.post(
path='/notifications/email/ses',
data=ses_missing_notification_id_callback(),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 400
assert json_resp['result'] == 'error'
assert json_resp['message'] == 'SES callback failed: messageId missing'
def test_ses_callback_should_fail_if_notification_cannot_be_found(notify_db, notify_db_session, client):
response = client.post(
path='/notifications/email/ses',
data=ses_invalid_notification_id_callback(),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 404
assert json_resp['result'] == 'error'
assert json_resp['message'] == 'SES callback failed: notification either not found or already updated from sending. Status delivered for notification reference missing' # noqa
def test_ses_callback_should_update_notification_status(
client,
notify_db,
notify_db_session,
sample_email_template,
mocker):
with freeze_time('2001-01-01T12:00:00'):
mocker.patch('app.statsd_client.incr')
mocker.patch('app.statsd_client.timing_with_dates')
notification = create_sample_notification(
notify_db,
notify_db_session,
template=sample_email_template,
reference='ref',
status='sending',
sent_at=datetime.utcnow()
)
assert get_notification_by_id(notification.id).status == 'sending'
response = client.post(
path='/notifications/email/ses',
data=ses_notification_callback(),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 200
assert json_resp['result'] == 'success'
assert json_resp['message'] == 'SES callback succeeded'
assert get_notification_by_id(notification.id).status == 'delivered'
statsd_client.timing_with_dates.assert_any_call(
"callback.ses.elapsed-time", datetime.utcnow(), notification.sent_at
)
statsd_client.incr.assert_any_call("callback.ses.delivered")
def test_ses_callback_should_update_multiple_notification_status_sent(
client,
notify_db,
notify_db_session,
sample_email_template,
mocker):
notification1 = create_sample_notification(
notify_db,
notify_db_session,
template=sample_email_template,
reference='ref1',
sent_at=datetime.utcnow(),
status='sending')
notification2 = create_sample_notification(
notify_db,
notify_db_session,
template=sample_email_template,
reference='ref2',
sent_at=datetime.utcnow(),
status='sending')
notification3 = create_sample_notification(
notify_db,
notify_db_session,
template=sample_email_template,
reference='ref3',
sent_at=datetime.utcnow(),
status='sending')
resp1 = client.post(
path='/notifications/email/ses',
data=ses_notification_callback(ref='ref1'),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
resp2 = client.post(
path='/notifications/email/ses',
data=ses_notification_callback(ref='ref2'),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
resp3 = client.post(
path='/notifications/email/ses',
data=ses_notification_callback(ref='ref3'),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
assert resp1.status_code == 200
assert resp2.status_code == 200
assert resp3.status_code == 200
def test_ses_callback_should_set_status_to_temporary_failure(client,
notify_db,
notify_db_session,
sample_email_template):
notification = create_sample_notification(
notify_db,
notify_db_session,
template=sample_email_template,
reference='ref',
status='sending',
sent_at=datetime.utcnow()
)
assert get_notification_by_id(notification.id).status == 'sending'
response = client.post(
path='/notifications/email/ses',
data=ses_soft_bounce_callback(),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 200
assert json_resp['result'] == 'success'
assert json_resp['message'] == 'SES callback succeeded'
assert get_notification_by_id(notification.id).status == 'temporary-failure'
def test_ses_callback_should_not_set_status_once_status_is_delivered(client,
notify_db,
notify_db_session,
sample_email_template):
notification = create_sample_notification(
notify_db,
notify_db_session,
template=sample_email_template,
reference='ref',
status='delivered',
sent_at=datetime.utcnow()
)
assert get_notification_by_id(notification.id).status == 'delivered'
response = client.post(
path='/notifications/email/ses',
data=ses_soft_bounce_callback(),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 404
assert json_resp['result'] == 'error'
assert json_resp['message'] == 'SES callback failed: notification either not found or already updated from sending. Status temporary-failure for notification reference ref' # noqa
assert get_notification_by_id(notification.id).status == 'delivered'
def test_ses_callback_should_set_status_to_permanent_failure(client,
notify_db,
notify_db_session,
sample_email_template):
notification = create_sample_notification(
notify_db,
notify_db_session,
template=sample_email_template,
reference='ref',
status='sending',
sent_at=datetime.utcnow()
)
assert get_notification_by_id(notification.id).status == 'sending'
response = client.post(
path='/notifications/email/ses',
data=ses_hard_bounce_callback(),
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
)
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 200
assert json_resp['result'] == 'success'
assert json_resp['message'] == 'SES callback succeeded'
assert get_notification_by_id(notification.id).status == 'permanent-failure'
def ses_notification_callback(ref='ref'):
return str.encode(
'{\n "Type" : "Notification",\n "MessageId" : "%(ref)s",\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",\n "Message" : "{\\"notificationType\\":\\"Delivery\\",\\"mail\\":{\\"timestamp\\":\\"2016-03-14T12:35:25.909Z\\",\\"source\\":\\"test@test-domain.com\\",\\"sourceArn\\":\\"arn:aws:ses:eu-west-1:123456789012:identity/testing-notify\\",\\"sendingAccountId\\":\\"123456789012\\",\\"messageId\\":\\"%(ref)s\\",\\"destination\\":[\\"testing@digital.cabinet-office.gov.uk\\"]},\\"delivery\\":{\\"timestamp\\":\\"2016-03-14T12:35:26.567Z\\",\\"processingTimeMillis\\":658,\\"recipients\\":[\\"testing@digital.cabinet-office.gov.uk\\"],\\"smtpResponse\\":\\"250 2.0.0 OK 1457958926 uo5si26480932wjc.221 - gsmtp\\",\\"reportingMTA\\":\\"a6-238.smtp-out.eu-west-1.amazonses.com\\"}}",\n "Timestamp" : "2016-03-14T12:35:26.665Z",\n "SignatureVersion" : "1",\n "Signature" : "X8d7eTAOZ6wlnrdVVPYanrAlsX0SMPfOzhoTEBnQqYkrNWTqQY91C0f3bxtPdUhUtOowyPAOkTQ4KnZuzphfhVb2p1MyVYMxNKcBFB05/qaCX99+92fjw4x9LeUOwyGwMv5F0Vkfi5qZCcEw69uVrhYLVSTFTrzi/yCtru+yFULMQ6UhbY09GwiP6hjxZMVr8aROQy5lLHglqQzOuSZ4KeD85JjifHdKzlx8jjQ+uj+FLzHXPMAPmPU1JK9kpoHZ1oPshAFgPDpphJe+HwcJ8ezmk+3AEUr3wWli3xF+49y8Z2anASSVp6YI2YP95UT8Rlh3qT3T+V9V8rbSVislxA==",\n "SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-bb750dd426d95ee9390147a5624348ee.pem",\n "UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:302763885840:preview-emails:d6aad3ef-83d6-4cf3-a470-54e2e75916da"\n}' % {'ref': ref} # noqa
)
def ses_invalid_notification_id_callback():
return b'{\n "Type" : "Notification",\n "MessageId" : "missing",\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",\n "Message" : "{\\"notificationType\\":\\"Delivery\\",\\"mail\\":{\\"timestamp\\":\\"2016-03-14T12:35:25.909Z\\",\\"source\\":\\"test@test-domain.com\\",\\"sourceArn\\":\\"arn:aws:ses:eu-west-1:123456789012:identity/testing-notify\\",\\"sendingAccountId\\":\\"123456789012\\",\\"messageId\\":\\"missing\\",\\"destination\\":[\\"testing@digital.cabinet-office.gov.uk\\"]},\\"delivery\\":{\\"timestamp\\":\\"2016-03-14T12:35:26.567Z\\",\\"processingTimeMillis\\":658,\\"recipients\\":[\\"testing@digital.cabinet-office.gov.uk\\"],\\"smtpResponse\\":\\"250 2.0.0 OK 1457958926 uo5si26480932wjc.221 - gsmtp\\",\\"reportingMTA\\":\\"a6-238.smtp-out.eu-west-1.amazonses.com\\"}}",\n "Timestamp" : "2016-03-14T12:35:26.665Z",\n "SignatureVersion" : "1",\n "Signature" : "X8d7eTAOZ6wlnrdVVPYanrAlsX0SMPfOzhoTEBnQqYkrNWTqQY91C0f3bxtPdUhUtOowyPAOkTQ4KnZuzphfhVb2p1MyVYMxNKcBFB05/qaCX99+92fjw4x9LeUOwyGwMv5F0Vkfi5qZCcEw69uVrhYLVSTFTrzi/yCtru+yFULMQ6UhbY09GwiP6hjxZMVr8aROQy5lLHglqQzOuSZ4KeD85JjifHdKzlx8jjQ+uj+FLzHXPMAPmPU1JK9kpoHZ1oPshAFgPDpphJe+HwcJ8ezmk+3AEUr3wWli3xF+49y8Z2anASSVp6YI2YP95UT8Rlh3qT3T+V9V8rbSVislxA==",\n "SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-bb750dd426d95ee9390147a5624348ee.pem",\n "UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:302763885840:preview-emails:d6aad3ef-83d6-4cf3-a470-54e2e75916da"\n}' # noqa
def ses_missing_notification_id_callback():
return b'{\n "Type" : "Notification",\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",\n "Message" : "{\\"notificationType\\":\\"Delivery\\",\\"mail\\":{\\"timestamp\\":\\"2016-03-14T12:35:25.909Z\\",\\"source\\":\\"test@test-domain.com\\",\\"sourceArn\\":\\"arn:aws:ses:eu-west-1:123456789012:identity/testing-notify\\",\\"sendingAccountId\\":\\"123456789012\\",\\"destination\\":[\\"testing@digital.cabinet-office.gov.uk\\"]},\\"delivery\\":{\\"timestamp\\":\\"2016-03-14T12:35:26.567Z\\",\\"processingTimeMillis\\":658,\\"recipients\\":[\\"testing@digital.cabinet-office.gov.uk\\"],\\"smtpResponse\\":\\"250 2.0.0 OK 1457958926 uo5si26480932wjc.221 - gsmtp\\",\\"reportingMTA\\":\\"a6-238.smtp-out.eu-west-1.amazonses.com\\"}}",\n "Timestamp" : "2016-03-14T12:35:26.665Z",\n "SignatureVersion" : "1",\n "Signature" : "X8d7eTAOZ6wlnrdVVPYanrAlsX0SMPfOzhoTEBnQqYkrNWTqQY91C0f3bxtPdUhUtOowyPAOkTQ4KnZuzphfhVb2p1MyVYMxNKcBFB05/qaCX99+92fjw4x9LeUOwyGwMv5F0Vkfi5qZCcEw69uVrhYLVSTFTrzi/yCtru+yFULMQ6UhbY09GwiP6hjxZMVr8aROQy5lLHglqQzOuSZ4KeD85JjifHdKzlx8jjQ+uj+FLzHXPMAPmPU1JK9kpoHZ1oPshAFgPDpphJe+HwcJ8ezmk+3AEUr3wWli3xF+49y8Z2anASSVp6YI2YP95UT8Rlh3qT3T+V9V8rbSVislxA==",\n "SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-bb750dd426d95ee9390147a5624348ee.pem",\n "UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:302763885840:preview-emails:d6aad3ef-83d6-4cf3-a470-54e2e75916da"\n}' # noqa
def ses_invalid_notification_type_callback():
return b'{\n "Type" : "Notification",\n "MessageId" : "ref",\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",\n "Message" : "{\\"notificationType\\":\\"Unknown\\",\\"mail\\":{\\"timestamp\\":\\"2016-03-14T12:35:25.909Z\\",\\"source\\":\\"test@test-domain.com\\",\\"sourceArn\\":\\"arn:aws:ses:eu-west-1:123456789012:identity/testing-notify\\",\\"sendingAccountId\\":\\"123456789012\\",\\"destination\\":[\\"testing@digital.cabinet-office.gov.uk\\"]},\\"delivery\\":{\\"timestamp\\":\\"2016-03-14T12:35:26.567Z\\",\\"processingTimeMillis\\":658,\\"recipients\\":[\\"testing@digital.cabinet-office.gov.uk\\"],\\"smtpResponse\\":\\"250 2.0.0 OK 1457958926 uo5si26480932wjc.221 - gsmtp\\",\\"reportingMTA\\":\\"a6-238.smtp-out.eu-west-1.amazonses.com\\"}}",\n "Timestamp" : "2016-03-14T12:35:26.665Z",\n "SignatureVersion" : "1",\n "Signature" : "X8d7eTAOZ6wlnrdVVPYanrAlsX0SMPfOzhoTEBnQqYkrNWTqQY91C0f3bxtPdUhUtOowyPAOkTQ4KnZuzphfhVb2p1MyVYMxNKcBFB05/qaCX99+92fjw4x9LeUOwyGwMv5F0Vkfi5qZCcEw69uVrhYLVSTFTrzi/yCtru+yFULMQ6UhbY09GwiP6hjxZMVr8aROQy5lLHglqQzOuSZ4KeD85JjifHdKzlx8jjQ+uj+FLzHXPMAPmPU1JK9kpoHZ1oPshAFgPDpphJe+HwcJ8ezmk+3AEUr3wWli3xF+49y8Z2anASSVp6YI2YP95UT8Rlh3qT3T+V9V8rbSVislxA==",\n "SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-bb750dd426d95ee9390147a5624348ee.pem",\n "UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:302763885840:preview-emails:d6aad3ef-83d6-4cf3-a470-54e2e75916da"\n}' # noqa
def ses_hard_bounce_callback():
return b'{\n "Type" : "Notification",\n "MessageId" : "ref",\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",\n "Message" : "{\\"notificationType\\":\\"Bounce\\",\\"bounce\\":{\\"bounceType\\":\\"Permanent\\",\\"bounceSubType\\":\\"General\\"}, \\"mail\\":{\\"messageId\\":\\"ref\\",\\"timestamp\\":\\"2016-03-14T12:35:25.909Z\\",\\"source\\":\\"test@test-domain.com\\",\\"sourceArn\\":\\"arn:aws:ses:eu-west-1:123456789012:identity/testing-notify\\",\\"sendingAccountId\\":\\"123456789012\\",\\"destination\\":[\\"testing@digital.cabinet-office.gov.uk\\"]},\\"delivery\\":{\\"timestamp\\":\\"2016-03-14T12:35:26.567Z\\",\\"processingTimeMillis\\":658,\\"recipients\\":[\\"testing@digital.cabinet-office.gov.uk\\"],\\"smtpResponse\\":\\"250 2.0.0 OK 1457958926 uo5si26480932wjc.221 - gsmtp\\",\\"reportingMTA\\":\\"a6-238.smtp-out.eu-west-1.amazonses.com\\"}}",\n "Timestamp" : "2016-03-14T12:35:26.665Z",\n "SignatureVersion" : "1",\n "Signature" : "X8d7eTAOZ6wlnrdVVPYanrAlsX0SMPfOzhoTEBnQqYkrNWTqQY91C0f3bxtPdUhUtOowyPAOkTQ4KnZuzphfhVb2p1MyVYMxNKcBFB05/qaCX99+92fjw4x9LeUOwyGwMv5F0Vkfi5qZCcEw69uVrhYLVSTFTrzi/yCtru+yFULMQ6UhbY09GwiP6hjxZMVr8aROQy5lLHglqQzOuSZ4KeD85JjifHdKzlx8jjQ+uj+FLzHXPMAPmPU1JK9kpoHZ1oPshAFgPDpphJe+HwcJ8ezmk+3AEUr3wWli3xF+49y8Z2anASSVp6YI2YP95UT8Rlh3qT3T+V9V8rbSVislxA==",\n "SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-bb750dd426d95ee9390147a5624348ee.pem",\n "UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:302763885840:preview-emails:d6aad3ef-83d6-4cf3-a470-54e2e75916da"\n}' # noqa
def ses_soft_bounce_callback():
return b'{\n "Type" : "Notification",\n "MessageId" : "ref",\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",\n "Message" : "{\\"notificationType\\":\\"Bounce\\",\\"bounce\\":{\\"bounceType\\":\\"Undetermined\\",\\"bounceSubType\\":\\"General\\"}, \\"mail\\":{\\"messageId\\":\\"ref\\",\\"timestamp\\":\\"2016-03-14T12:35:25.909Z\\",\\"source\\":\\"test@test-domain.com\\",\\"sourceArn\\":\\"arn:aws:ses:eu-west-1:123456789012:identity/testing-notify\\",\\"sendingAccountId\\":\\"123456789012\\",\\"destination\\":[\\"testing@digital.cabinet-office.gov.uk\\"]},\\"delivery\\":{\\"timestamp\\":\\"2016-03-14T12:35:26.567Z\\",\\"processingTimeMillis\\":658,\\"recipients\\":[\\"testing@digital.cabinet-office.gov.uk\\"],\\"smtpResponse\\":\\"250 2.0.0 OK 1457958926 uo5si26480932wjc.221 - gsmtp\\",\\"reportingMTA\\":\\"a6-238.smtp-out.eu-west-1.amazonses.com\\"}}",\n "Timestamp" : "2016-03-14T12:35:26.665Z",\n "SignatureVersion" : "1",\n "Signature" : "X8d7eTAOZ6wlnrdVVPYanrAlsX0SMPfOzhoTEBnQqYkrNWTqQY91C0f3bxtPdUhUtOowyPAOkTQ4KnZuzphfhVb2p1MyVYMxNKcBFB05/qaCX99+92fjw4x9LeUOwyGwMv5F0Vkfi5qZCcEw69uVrhYLVSTFTrzi/yCtru+yFULMQ6UhbY09GwiP6hjxZMVr8aROQy5lLHglqQzOuSZ4KeD85JjifHdKzlx8jjQ+uj+FLzHXPMAPmPU1JK9kpoHZ1oPshAFgPDpphJe+HwcJ8ezmk+3AEUr3wWli3xF+49y8Z2anASSVp6YI2YP95UT8Rlh3qT3T+V9V8rbSVislxA==",\n "SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-bb750dd426d95ee9390147a5624348ee.pem",\n "UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:302763885840:preview-emails:d6aad3ef-83d6-4cf3-a470-54e2e75916da"\n}' # noqa

View File

@@ -1,6 +1,5 @@
import uuid
from app.models import NotificationStatistics
from app.notifications.process_client_response import (
validate_callback_data,
process_sms_client_response

View File

@@ -13,31 +13,41 @@ from tests import create_authorization_header
from tests.app.conftest import sample_notification as create_sample_notification
def test_get_sms_notification_by_id(client, sample_notification):
auth_header = create_authorization_header(service_id=sample_notification.service_id)
@pytest.mark.parametrize('type', ('email', 'sms'))
def test_get_notification_by_id(client, sample_notification, sample_email_notification, type):
if type == 'email':
notification_to_get = sample_email_notification
if type == 'sms':
notification_to_get = sample_notification
auth_header = create_authorization_header(service_id=notification_to_get.service_id)
response = client.get(
'/notifications/{}'.format(sample_notification.id),
'/notifications/{}'.format(notification_to_get.id),
headers=[auth_header])
assert response.status_code == 200
notification = json.loads(response.get_data(as_text=True))['data']['notification']
assert notification['status'] == 'created'
assert notification['template'] == {
'id': str(sample_notification.template.id),
'name': sample_notification.template.name,
'template_type': sample_notification.template.template_type,
'id': str(notification_to_get.template.id),
'name': notification_to_get.template.name,
'template_type': notification_to_get.template.template_type,
'version': 1
}
assert notification['to'] == '+447700900855'
assert notification['service'] == str(sample_notification.service_id)
assert notification['body'] == "This is a template:\nwith a newline"
assert not notification.get('subject')
assert notification['to'] == notification_to_get.to
assert notification['service'] == str(notification_to_get.service_id)
assert notification['body'] == notification_to_get.template.content
assert notification.get('subject', None) == notification_to_get.subject
@pytest.mark.parametrize("id", ["1234-badly-formatted-id-7890", "0"])
def test_get_sms_notification_by_invalid_id(client, sample_notification, id):
auth_header = create_authorization_header(service_id=sample_notification.service_id)
@pytest.mark.parametrize('type', ('email', 'sms'))
def test_get_notification_by_invalid_id(client, sample_notification, sample_email_notification, id, type):
if type == 'email':
notification_to_get = sample_email_notification
if type == 'sms':
notification_to_get = sample_notification
auth_header = create_authorization_header(service_id=notification_to_get.service_id)
response = client.get(
'/notifications/{}'.format(id),
@@ -46,37 +56,6 @@ def test_get_sms_notification_by_invalid_id(client, sample_notification, id):
assert response.status_code == 405
def test_get_email_notification_by_id(client, notify_db, notify_db_session, sample_email_template):
email_notification = create_sample_notification(notify_db,
notify_db_session,
to_field="sample_email@example.com",
service=sample_email_template.service,
template=sample_email_template,
status='sending')
auth_header = create_authorization_header(service_id=email_notification.service_id)
response = client.get(
'/notifications/{}'.format(email_notification.id),
headers=[auth_header])
notification = json.loads(response.get_data(as_text=True))['data']['notification']
assert notification['status'] == 'sending'
assert notification['template'] == {
'id': str(email_notification.template.id),
'name': email_notification.template.name,
'template_type': email_notification.template.template_type,
'version': 1
}
assert notification['to'] == 'sample_email@example.com'
assert notification['service'] == str(email_notification.service_id)
assert response.status_code == 200
assert notification['body'] == sample_email_template.content
assert notification['subject'] == sample_email_template.subject
def test_get_notifications_empty_result(client, sample_api_key):
auth_header = create_authorization_header(service_id=sample_api_key.service_id)