Change endpoint responses where there are marshalling, unmarshalling

or param errors to raise invalid data exception. That will cause
those responses to be handled in by errors.py, which will log
the errors.

Set most of schemas to strict mode so that marshmallow will raise
exception rather than checking for errors in return tuple from load.

Added handler to errors.py for marshmallow validation errors.
This commit is contained in:
Adam Shimali
2016-06-14 15:07:23 +01:00
parent 5a66884f0e
commit b33312b855
16 changed files with 240 additions and 267 deletions

View File

@@ -10,7 +10,11 @@ from notifications_utils.url_safe_token import check_token
from app.dao.invited_user_dao import get_invited_user_by_id
from app.errors import register_errors
from app.errors import (
register_errors,
InvalidRequest
)
from app.schemas import invited_user_schema
@@ -30,7 +34,8 @@ def get_invited_user_by_token(token):
max_age_seconds)
except SignatureExpired:
message = 'Invitation with id {} expired'.format(invited_user_id)
return jsonify(result='error', message=message), 400
errors = {'invitation': [message]}
raise InvalidRequest(errors, status_code=400)
invited_user = get_invited_user_by_id(invited_user_id)

View File

@@ -4,9 +4,10 @@ from flask import (
)
from sqlalchemy.exc import SQLAlchemyError, DataError
from sqlalchemy.orm.exc import NoResultFound
from marshmallow import ValidationError
class InvalidData(Exception):
class InvalidRequest(Exception):
def __init__(self, message, status_code):
super().__init__()
@@ -22,7 +23,12 @@ class InvalidData(Exception):
def register_errors(blueprint):
@blueprint.app_errorhandler(InvalidData)
@blueprint.app_errorhandler(ValidationError)
def validation_error(error):
current_app.logger.error(error)
return jsonify(result='error', message=error.messages), 400
@blueprint.app_errorhandler(InvalidRequest)
def invalid_data(error):
response = jsonify(error.to_dict())
response.status_code = error.status_code

View File

@@ -5,6 +5,7 @@ from flask import (
)
from app.errors import register_errors
from app.schemas import event_schema
from app.dao.events_dao import dao_create_event
@@ -15,8 +16,6 @@ register_errors(events)
@events.route('', methods=['POST'])
def create_event():
data = request.get_json()
event, errors = event_schema.load(data)
if errors:
return jsonify(result="error", message=errors), 400
event = event_schema.load(data).data
dao_create_event(event)
return jsonify(data=event_schema.dump(event).data), 201

View File

@@ -19,14 +19,13 @@ from app.celery.tasks import (email_invited_user)
invite = Blueprint('invite', __name__, url_prefix='/service/<service_id>/invite')
from app.errors import register_errors
register_errors(invite)
@invite.route('', methods=['POST'])
def create_invited_user(service_id):
invited_user, errors = invited_user_schema.load(request.get_json())
if errors:
return jsonify(result="error", message=errors), 400
save_invited_user(invited_user)
invitation = _create_invitation(invited_user)
encrypted_invitation = encryption.encrypt(invitation)
@@ -53,9 +52,7 @@ def update_invited_user(service_id, invited_user_id):
current_data = dict(invited_user_schema.dump(fetched).data.items())
current_data.update(request.get_json())
update_dict, errors = invited_user_schema.load(current_data)
if errors:
return jsonify(result='error', message=errors), 400
update_dict = invited_user_schema.load(current_data).data
save_invited_user(update_dict)
return jsonify(data=invited_user_schema.dump(fetched).data), 200

View File

@@ -22,7 +22,10 @@ from app.celery.tasks import process_job
job = Blueprint('job', __name__, url_prefix='/service/<uuid:service_id>/job')
from app.errors import register_errors
from app.errors import (
register_errors,
InvalidRequest
)
register_errors(job)
@@ -30,7 +33,7 @@ register_errors(job)
@job.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)
data, errors = job_schema.dump(job)
data = job_schema.dump(job).data
return jsonify(data=data)
@@ -40,16 +43,13 @@ def get_jobs_by_service(service_id):
try:
limit_days = int(request.args['limit_days'])
except ValueError as e:
error = '{} is not an integer'.format(request.args['limit_days'])
current_app.logger.error(error)
return jsonify(result="error", message={'limit_days': [error]}), 400
errors = {'error': ['{} is not an integer'.format(request.args['limit_days'])]}
raise InvalidRequest(errors, status_code=400)
else:
limit_days = None
jobs = dao_get_jobs_by_service_id(service_id, limit_days)
data, errors = job_schema.dump(jobs, many=True)
if errors:
return jsonify(result="error", message=errors), 400
data = job_schema.dump(jobs, many=True).data
return jsonify(data=data)
@@ -66,13 +66,10 @@ def create_job(service_id):
errors = unarchived_template_schema.validate({'archived': template.archived})
if errors:
return jsonify(result='error', message=errors), 400
raise InvalidRequest(errors, status_code=400)
data.update({"template_version": template.version})
job, errors = job_schema.load(data)
if errors:
return jsonify(result="error", message=errors), 400
job = job_schema.load(data).data
dao_create_job(job)
process_job.apply_async([str(job.id)], queue="process-job")
return jsonify(data=job_schema.dump(job).data), 201

View File

@@ -35,7 +35,10 @@ from app.celery.tasks import send_sms, send_email
notifications = Blueprint('notifications', __name__)
from app.errors import register_errors
from app.errors import (
register_errors,
InvalidRequest
)
register_errors(notifications)
@@ -47,16 +50,12 @@ def process_ses_response():
ses_request = json.loads(request.data)
errors = validate_callback_data(data=ses_request, fields=['Message'], client_name=client_name)
if errors:
return jsonify(
result="error", message=errors
), 400
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:
return jsonify(
result="error", message=errors
), 400
raise InvalidRequest(errors, status_code=400)
notification_type = ses_message['notificationType']
if notification_type == 'Bounce':
@@ -67,12 +66,8 @@ def process_ses_response():
try:
aws_response_dict = get_aws_responses(notification_type)
except KeyError:
message = "{} callback failed: status {} not found".format(client_name, notification_type)
current_app.logger.info(message)
return jsonify(
result="error",
message=message
), 400
error = "{} callback failed: status {} not found".format(client_name, notification_type)
raise InvalidRequest(error, status_code=400)
notification_status = aws_response_dict['notification_status']
notification_statistics_status = aws_response_dict['notification_statistics_status']
@@ -93,15 +88,9 @@ def process_ses_response():
notification_status,
notification_statistics_status
):
message = "SES callback failed: notification either not found or already updated " \
error = "SES callback failed: notification either not found or already updated " \
"from sending. Status {}".format(notification_status)
current_app.logger.info(
message
)
return jsonify(
result="error",
message=message
), 404
raise InvalidRequest(error, status_code=404)
if not aws_response_dict['success']:
current_app.logger.info(
@@ -117,20 +106,12 @@ def process_ses_response():
), 200
except KeyError:
current_app.logger.error(
"SES callback failed: messageId missing"
)
return jsonify(
result="error", message="SES callback failed: messageId missing"
), 400
message = "SES callback failed: messageId missing"
raise InvalidRequest(message, status_code=400)
except ValueError as ex:
current_app.logger.exception(
"{} callback failed: invalid json {}".format(client_name, ex)
)
return jsonify(
result="error", message="{} callback failed: invalid json".format(client_name)
), 400
error = "{} callback failed: invalid json".format(client_name)
raise InvalidRequest(error, status_code=400)
def is_not_a_notification(source):
@@ -149,19 +130,17 @@ def is_not_a_notification(source):
def process_mmg_response():
client_name = 'MMG'
data = json.loads(request.data)
validation_errors = validate_callback_data(data=data,
errors = validate_callback_data(data=data,
fields=['status', 'CID'],
client_name=client_name)
if validation_errors:
[current_app.logger.info(e) for e in validation_errors]
return jsonify(result='error', message=validation_errors), 400
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:
[current_app.logger.info(e) for e in errors]
return jsonify(result='error', message=errors), 400
raise InvalidRequest(errors, status_code=400)
else:
return jsonify(result='success', message=success), 200
@@ -169,12 +148,11 @@ def process_mmg_response():
@notifications.route('/notifications/sms/firetext', methods=['POST'])
def process_firetext_response():
client_name = 'Firetext'
validation_errors = validate_callback_data(data=request.form,
errors = validate_callback_data(data=request.form,
fields=['status', 'reference'],
client_name=client_name)
if validation_errors:
current_app.logger.info(validation_errors)
return jsonify(result='error', message=validation_errors), 400
if errors:
raise InvalidRequest(errors, status_code=400)
response_code = request.form.get('code')
status = request.form.get('status')
@@ -185,8 +163,7 @@ def process_firetext_response():
reference=request.form.get('reference'),
client_name=client_name)
if errors:
[current_app.logger.info(e) for e in errors]
return jsonify(result='error', message=errors), 400
raise InvalidRequest(errors, status_code=400)
else:
return jsonify(result='success', message=success), 200
@@ -199,10 +176,7 @@ def get_notifications(notification_id):
@notifications.route('/notifications', methods=['GET'])
def get_all_notifications():
data, errors = notifications_filter_schema.load(request.args)
if errors:
return jsonify(result="error", message=errors), 400
data = notifications_filter_schema.load(request.args).data
page = data['page'] if 'page' in data else 1
page_size = data['page_size'] if 'page_size' in data else current_app.config.get('PAGE_SIZE')
limit_days = data.get('limit_days')
@@ -228,10 +202,7 @@ def get_all_notifications():
@notifications.route('/service/<service_id>/notifications', methods=['GET'])
@require_admin()
def get_all_notifications_for_service(service_id):
data, errors = notifications_filter_schema.load(request.args)
if errors:
return jsonify(result="error", message=errors), 400
data = notifications_filter_schema.load(request.args).data
page = data['page'] if 'page' in data else 1
page_size = data['page_size'] if 'page_size' in data else current_app.config.get('PAGE_SIZE')
limit_days = data.get('limit_days')
@@ -259,10 +230,7 @@ def get_all_notifications_for_service(service_id):
@notifications.route('/service/<service_id>/job/<job_id>/notifications', methods=['GET'])
@require_admin()
def get_all_notifications_for_service_job(service_id, job_id):
data, errors = notifications_filter_schema.load(request.args)
if errors:
return jsonify(result="error", message=errors), 400
data = notifications_filter_schema.load(request.args).data
page = data['page'] if 'page' in data else 1
page_size = data['page_size'] if 'page_size' in data else current_app.config.get('PAGE_SIZE')
@@ -317,16 +285,13 @@ def send_notification(notification_type):
total_email_count = service_stats.emails_requested
if (total_email_count + total_sms_count >= service.message_limit) and service.restricted:
return jsonify(result="error", message='Exceeded send limits ({}) for today'.format(
service.message_limit)), 429
error = 'Exceeded send limits ({}) for today'.format(service.message_limit)
raise InvalidRequest(error, status_code=429)
notification, errors = (
sms_template_notification_schema if notification_type == 'sms' else email_notification_schema
).load(request.get_json())
if errors:
return jsonify(result="error", message=errors), 400
template = templates_dao.dao_get_template_by_id_and_service_id(
template_id=notification['template'],
service_id=service_id
@@ -334,33 +299,24 @@ def send_notification(notification_type):
errors = unarchived_template_schema.validate({'archived': template.archived})
if errors:
return jsonify(result='error', message=errors), 400
raise InvalidRequest(errors, status_code=400)
template_object = Template(template.__dict__, notification.get('personalisation', {}))
if template_object.missing_data:
return jsonify(
result="error",
message={
'template': ['Missing personalisation: {}'.format(
", ".join(template_object.missing_data)
)]
}
), 400
message = 'Missing personalisation: {}'.format(", ".join(template_object.missing_data))
errors = {'template': [message]}
raise InvalidRequest(errors, status_code=400)
if template_object.additional_data:
return jsonify(
result="error",
message={
'template': ['Personalisation not needed for template: {}'.format(
", ".join(template_object.additional_data)
)]
}
), 400
message = 'Personalisation not needed for template: {}'.format(", ".join(template_object.additional_data))
errors = {'template': [message]}
raise InvalidRequest(errors, status_code=400)
if template_object.replaced_content_count > current_app.config.get('SMS_CHAR_COUNT_LIMIT'):
return jsonify(
result="error",
message={'content': ['Content has a character count greater than the limit of {}'.format(
current_app.config.get('SMS_CHAR_COUNT_LIMIT'))]}), 400
char_count = current_app.config.get('SMS_CHAR_COUNT_LIMIT')
message = 'Content has a character count greater than the limit of {}'.format(char_count)
errors = {'content': [message]}
raise InvalidRequest(errors, status_code=400)
if service.restricted and not allowed_to_send_to(
notification['to'],
@@ -368,11 +324,9 @@ def send_notification(notification_type):
[user.mobile_number, user.email_address] for user in service.users
)
):
return jsonify(
result="error", message={
'to': ['Invalid {} for restricted service'.format(first_column_heading[notification_type])]
}
), 400
message = 'Invalid {} for restricted service'.format(first_column_heading[notification_type])
errors = {'to': [message]}
raise InvalidRequest(errors, status_code=400)
notification_id = create_uuid()
notification.update({"template_version": template.version})
@@ -397,13 +351,9 @@ def send_notification(notification_type):
@notifications.route('/notifications/statistics')
def get_notification_statistics_for_day():
data, errors = day_schema.load(request.args)
if errors:
return jsonify(result='error', message=errors), 400
data = day_schema.load(request.args).data
statistics = notifications_dao.dao_get_potential_notification_statistics_for_day(
day=data['day']
)
data, errors = notifications_statistics_schema.dump(statistics, many=True)
return jsonify(data=data), 200

View File

@@ -19,7 +19,10 @@ notifications_statistics = Blueprint(
__name__, url_prefix='/service/<service_id>/notifications-statistics'
)
from app.errors import register_errors
from app.errors import (
register_errors,
InvalidRequest
)
register_errors(notifications_statistics)
@@ -34,9 +37,9 @@ def get_all_notification_statistics_for_service(service_id):
limit_days=int(request.args['limit_days'])
)
except ValueError as e:
error = '{} is not an integer'.format(request.args['limit_days'])
current_app.logger.error(error)
return jsonify(result="error", message={'limit_days': [error]}), 400
message = '{} is not an integer'.format(request.args['limit_days'])
errors = {'limit_days': [message]}
raise InvalidRequest(errors, status_code=400)
else:
statistics = dao_get_notification_statistics_for_service(service_id=service_id)
@@ -46,9 +49,7 @@ def get_all_notification_statistics_for_service(service_id):
@notifications_statistics.route('/seven_day_aggregate')
def get_notification_statistics_for_service_seven_day_aggregate(service_id):
data, errors = week_aggregate_notification_statistics_schema.load(request.args)
if errors:
return jsonify(result='error', message=errors), 400
data = week_aggregate_notification_statistics_schema.load(request.args).data
date_from = data['date_from'] if 'date_from' in data else date(date.today().year, 4, 1)
week_count = data['week_count'] if 'week_count' in data else 52
stats = dao_get_7_day_agg_notification_statistics_for_service(

View File

@@ -1,5 +1,9 @@
from flask import (
jsonify,
request,
Blueprint
)
from flask import (jsonify, request, abort, Blueprint, current_app)
from app.schemas import permission_schema
from app.errors import register_errors
from app.dao.permissions_dao import permission_dao
@@ -10,17 +14,12 @@ register_errors(permission)
@permission.route('', methods=['GET'])
def get_permissions():
data, errors = permission_schema.dump(
permission_dao.get_query(filter_by_dict=request.args), many=True)
if errors:
abort(500, errors)
data = permission_schema.dump(permission_dao.get_query(filter_by_dict=request.args), many=True).data
return jsonify(data=data)
@permission.route('/<permission_id>', methods=['GET'])
def get_permission(permission_id):
inst = permission_dao.get_query(filter_by_dict={'id': permission_id}).one()
data, errors = permission_schema.dump(inst)
if errors:
abort(500, errors)
data = permission_schema.dump(inst).data
return jsonify(data=data)

View File

@@ -1,25 +1,31 @@
from flask import Blueprint, jsonify, request
from app.schemas import provider_details_schema
from app.dao.provider_details_dao import (
get_provider_details,
get_provider_details_by_id,
get_provider_details_by_id,
dao_update_provider_details
)
from app.errors import (
register_errors,
InvalidRequest
)
provider_details = Blueprint('provider_details', __name__)
register_errors(provider_details)
@provider_details.route('', methods=['GET'])
def get_providers():
data, errors = provider_details_schema.dump(get_provider_details(), many=True)
data = provider_details_schema.dump(get_provider_details(), many=True).data
return jsonify(provider_details=data)
@provider_details.route('/<uuid:provider_details_id>', methods=['GET'])
def get_provider_by_id(provider_details_id):
data, errors = provider_details_schema.dump(get_provider_details_by_id(provider_details_id))
data = provider_details_schema.dump(get_provider_details_by_id(provider_details_id)).data
return jsonify(provider_details=data)
@@ -29,14 +35,12 @@ def update_provider_details(provider_details_id):
current_data = dict(provider_details_schema.dump(fetched_provider_details).data.items())
current_data.update(request.get_json())
update_dict, errors = provider_details_schema.load(current_data)
if errors:
return jsonify(result="error", message=errors), 400
update_dict = provider_details_schema.load(current_data).data
if "identifier" in request.get_json().keys():
return jsonify(message={
"identifier": ["Not permitted to be updated"]
}, result='error'), 400
message = "Not permitted to be updated"
errors = {'identifier': [message]}
raise InvalidRequest(errors, status_code=400)
dao_update_provider_details(update_dict)
return jsonify(provider_details=provider_details_schema.dump(fetched_provider_details).data), 200

View File

@@ -1,13 +1,16 @@
from datetime import date
from datetime import (
datetime,
date
)
from flask_marshmallow.fields import fields
from sqlalchemy.orm import load_only
from marshmallow import (
post_load,
ValidationError,
validates,
validates_schema,
pre_load
pre_load,
pre_dump
)
from marshmallow_sqlalchemy import field_for
@@ -46,6 +49,7 @@ def _validate_not_in_future(dte, msg="Date cannot be in the future"):
class BaseSchema(ma.ModelSchema):
def __init__(self, load_json=False, *args, **kwargs):
self.load_json = load_json
super(BaseSchema, self).__init__(*args, **kwargs)
@@ -82,12 +86,14 @@ class UserSchema(BaseSchema):
exclude = (
"updated_at", "created_at", "user_to_service",
"_password", "verify_codes")
strict = True
class ProviderDetailsSchema(BaseSchema):
class Meta:
model = models.ProviderDetails
exclude = ("provider_rates", "provider_stats")
strict = True
class ServiceSchema(BaseSchema):
@@ -97,11 +103,13 @@ class ServiceSchema(BaseSchema):
class Meta:
model = models.Service
exclude = ("updated_at", "created_at", "api_keys", "templates", "jobs", 'old_id')
strict = True
class NotificationModelSchema(BaseSchema):
class Meta:
model = models.Notification
strict = True
class BaseTemplateSchema(BaseSchema):
@@ -109,6 +117,7 @@ class BaseTemplateSchema(BaseSchema):
class Meta:
model = models.Template
exclude = ("service_id", "jobs")
strict = True
class TemplateSchema(BaseTemplateSchema):
@@ -142,6 +151,13 @@ class TemplateHistorySchema(BaseSchema):
class NotificationsStatisticsSchema(BaseSchema):
class Meta:
model = models.NotificationStatistics
strict = True
@pre_dump
def handle_date_str(self, in_data):
if isinstance(in_data, dict) and 'day' in in_data:
in_data['day'] = datetime.strptime(in_data['day'], '%Y-%m-%d').date()
return in_data
class ApiKeySchema(BaseSchema):
@@ -151,6 +167,7 @@ class ApiKeySchema(BaseSchema):
class Meta:
model = models.ApiKey
exclude = ("service", "secret")
strict = True
class JobSchema(BaseSchema):
@@ -161,13 +178,22 @@ class JobSchema(BaseSchema):
class Meta:
model = models.Job
exclude = ('notifications',)
strict = True
class RequestVerifyCodeSchema(ma.Schema):
class Meta:
strict = True
to = fields.Str(required=False)
class NotificationSchema(ma.Schema):
class Meta:
strict = True
personalisation = fields.Dict(required=False)
@@ -225,12 +251,14 @@ class NotificationStatusSchema(BaseSchema):
class Meta:
model = models.Notification
strict = True
class InvitedUserSchema(BaseSchema):
class Meta:
model = models.InvitedUser
strict = True
@validates('email_address')
def validate_to(self, value):
@@ -255,9 +283,14 @@ class PermissionSchema(BaseSchema):
class Meta:
model = models.Permission
exclude = ("created_at",)
strict = True
class EmailDataSchema(ma.Schema):
class Meta:
strict = True
email = fields.Str(required=False)
@validates('email')
@@ -269,6 +302,10 @@ class EmailDataSchema(ma.Schema):
class NotificationsFilterSchema(ma.Schema):
class Meta:
strict = True
template_type = fields.Nested(BaseTemplateSchema, only=['template_type'], many=True)
status = fields.Nested(NotificationModelSchema, only=['status'], many=True)
page = fields.Int(required=False)
@@ -309,6 +346,7 @@ class TemplateStatisticsSchema(BaseSchema):
class Meta:
model = models.TemplateStatistics
strict = True
class ServiceHistorySchema(ma.Schema):
@@ -337,10 +375,14 @@ class ApiKeyHistorySchema(ma.Schema):
class EventSchema(BaseSchema):
class Meta:
model = models.Event
strict = True
class FromToDateSchema(ma.Schema):
class Meta:
strict = True
date_from = fields.Date()
date_to = fields.Date()
@@ -361,6 +403,10 @@ class FromToDateSchema(ma.Schema):
class DaySchema(ma.Schema):
class Meta:
strict = True
day = fields.Date(required=True)
@validates('day')
@@ -370,6 +416,9 @@ class DaySchema(ma.Schema):
class WeekAggregateNotificationStatisticsSchema(ma.Schema):
class Meta:
strict = True
date_from = fields.Date()
week_count = fields.Int()

View File

@@ -6,9 +6,9 @@ from datetime import (
from flask import (
jsonify,
request,
abort,
Blueprint
)
from sqlalchemy.orm.exc import NoResultFound
from app.dao.api_key_dao import (
@@ -39,11 +39,12 @@ from app.schemas import (
permission_schema
)
from app.errors import register_errors
from app.errors import (
register_errors,
InvalidRequest
)
service = Blueprint('service', __name__)
register_errors(service)
@@ -54,7 +55,7 @@ def get_services():
services = dao_fetch_all_services_by_user(user_id)
else:
services = dao_fetch_all_services()
data, errors = service_schema.dump(services, many=True)
data = service_schema.dump(services, many=True).data
return jsonify(data=data)
@@ -66,7 +67,7 @@ def get_service_by_id(service_id):
else:
fetched = dao_fetch_service_by_id(service_id)
data, errors = service_schema.dump(fetched)
data = service_schema.dump(fetched).data
return jsonify(data=data)
@@ -74,16 +75,12 @@ def get_service_by_id(service_id):
def create_service():
data = request.get_json()
if not data.get('user_id', None):
return jsonify(result="error", message={'user_id': ['Missing data for required field.']}), 400
errors = {'user_id': ['Missing data for required field.']}
raise InvalidRequest(errors, status_code=400)
user = get_model_users(data['user_id'])
data.pop('user_id', None)
valid_service, errors = service_schema.load(request.get_json())
if errors:
return jsonify(result="error", message=errors), 400
valid_service = service_schema.load(request.get_json()).data
dao_create_service(valid_service, user)
return jsonify(data=service_schema.dump(valid_service).data), 201
@@ -93,9 +90,7 @@ def update_service(service_id):
fetched_service = dao_fetch_service_by_id(service_id)
current_data = dict(service_schema.dump(fetched_service).data.items())
current_data.update(request.get_json())
update_dict, errors = service_schema.load(current_data)
if errors:
return jsonify(result="error", message=errors), 400
update_dict = service_schema.load(current_data).data
dao_update_service(update_dict)
return jsonify(data=service_schema.dump(fetched_service).data), 200
@@ -103,14 +98,9 @@ def update_service(service_id):
@service.route('/<uuid:service_id>/api-key', methods=['POST'])
def renew_api_key(service_id=None):
fetched_service = dao_fetch_service_by_id(service_id=service_id)
valid_api_key, errors = api_key_schema.load(request.get_json())
if errors:
return jsonify(result="error", message=errors), 400
valid_api_key = api_key_schema.load(request.get_json()).data
valid_api_key.service = fetched_service
save_model_api_key(valid_api_key)
unsigned_api_key = get_unsigned_secret(valid_api_key.id)
return jsonify(data=unsigned_api_key), 201
@@ -133,7 +123,8 @@ def get_api_keys(service_id, key_id=None):
else:
api_keys = get_model_api_keys(service_id=service_id)
except NoResultFound:
return jsonify(result="error", message="API key not found for id: {}".format(service_id)), 404
error = "API key not found for id: {}".format(service_id)
raise InvalidRequest(error, status_code=404)
return jsonify(apiKeys=api_key_schema.dump(api_keys, many=True).data), 200
@@ -141,7 +132,6 @@ def get_api_keys(service_id, key_id=None):
@service.route('/<uuid:service_id>/users', methods=['GET'])
def get_users_for_service(service_id):
fetched = dao_fetch_service_by_id(service_id)
result = user_schema.dump(fetched.users, many=True)
return jsonify(data=result.data)
@@ -152,15 +142,12 @@ def add_user_to_service(service_id, user_id):
user = get_model_users(user_id=user_id)
if user in service.users:
return jsonify(result='error',
message='User id: {} already part of service id: {}'.format(user_id, service_id)), 400
permissions, errors = permission_schema.load(request.get_json(), many=True)
if errors:
abort(400, errors)
error = 'User id: {} already part of service id: {}'.format(user_id, service_id)
raise InvalidRequest(error, status_code=400)
permissions = permission_schema.load(request.get_json(), many=True).data
dao_add_user_to_service(service, user, permissions)
data, errors = service_schema.dump(service)
data = service_schema.dump(service).data
return jsonify(data=data), 201
@@ -169,13 +156,13 @@ def remove_user_from_service(service_id, user_id):
service = dao_fetch_service_by_id(service_id)
user = get_model_users(user_id=user_id)
if user not in service.users:
return jsonify(
result='error',
message='User not found'), 404
error = 'User not found'
raise InvalidRequest(error, status_code=404)
elif len(service.users) == 1:
return jsonify(
result='error',
message='You cannot remove the only user for a service'), 400
error = 'You cannot remove the only user for a service'
raise InvalidRequest(error, status_code=400)
dao_remove_user_from_service(service, user)
return jsonify({}), 204
@@ -183,10 +170,7 @@ def remove_user_from_service(service_id, user_id):
@service.route('/<uuid:service_id>/fragment/aggregate_statistics')
def get_service_provider_aggregate_statistics(service_id):
service = dao_fetch_service_by_id(service_id)
data, errors = from_to_date_schema.load(request.args)
if errors:
return jsonify(result='error', message=errors), 400
data = from_to_date_schema.load(request.args).data
return jsonify(data=get_fragment_count(
service,
date_from=(data.pop('date_from') if 'date_from' in data else date.today()),
@@ -208,21 +192,15 @@ def get_service_history(service_id):
)
service_history = Service.get_history_model().query.filter_by(id=service_id).all()
service_data, errors = service_history_schema.dump(service_history, many=True)
if errors:
return jsonify(result="error", message=errors), 400
service_data = service_history_schema.dump(service_history, many=True).data
api_key_history = ApiKey.get_history_model().query.filter_by(service_id=service_id).all()
api_keys_data, errors = api_key_history_schema.dump(api_key_history, many=True)
if errors:
return jsonify(result="error", message=errors), 400
api_keys_data = api_key_history_schema.dump(api_key_history, many=True).data
template_history = Template.get_history_model().query.filter_by(service_id=service_id).all()
template_data, errors = template_history_schema.dump(template_history, many=True)
events = Event.query.all()
events_data, errors = event_schema.dump(events, many=True)
events_data = event_schema.dump(events, many=True).data
data = {
'service_history': service_data,

View File

@@ -19,7 +19,10 @@ from app.schemas import (template_schema, template_history_schema)
template = Blueprint('template', __name__, url_prefix='/service/<uuid:service_id>/template')
from app.errors import register_errors
from app.errors import (
register_errors,
InvalidRequest
)
register_errors(template)
@@ -28,27 +31,23 @@ def _content_count_greater_than_limit(content, template_type):
template = Template({'content': content, 'template_type': template_type})
if template_type == 'sms' and \
template.content_count > current_app.config.get('SMS_CHAR_COUNT_LIMIT'):
return True, jsonify(
result="error",
message={'content': ['Content has a character count greater than the limit of {}'.format(
current_app.config.get('SMS_CHAR_COUNT_LIMIT'))]}
)
return False, ''
return True
return False
@template.route('', methods=['POST'])
def create_template(service_id):
fetched_service = dao_fetch_service_by_id(service_id=service_id)
new_template, errors = template_schema.load(request.get_json())
if errors:
return jsonify(result="error", message=errors), 400
new_template = template_schema.load(request.get_json()).data
new_template.service = fetched_service
new_template.content = _strip_html(new_template.content)
over_limit, json_resp = _content_count_greater_than_limit(
new_template.content,
new_template.template_type)
over_limit = _content_count_greater_than_limit(new_template.content, new_template.template_type)
if over_limit:
return json_resp, 400
char_count_limit = current_app.config.get('SMS_CHAR_COUNT_LIMIT')
message = 'Content has a character count greater than the limit of {}'.format(char_count_limit)
errors = {'content': [message]}
raise InvalidRequest(errors, status_code=400)
dao_create_template(new_template)
return jsonify(data=template_schema.dump(new_template).data), 201
@@ -65,14 +64,13 @@ def update_template(service_id, template_id):
if _template_has_not_changed(current_data, updated_template):
return jsonify(data=updated_template), 200
update_dict, errors = template_schema.load(updated_template)
if errors:
return jsonify(result="error", message=errors), 400
over_limit, json_resp = _content_count_greater_than_limit(
updated_template['content'],
fetched_template.template_type)
update_dict = template_schema.load(updated_template).data
over_limit = _content_count_greater_than_limit(updated_template['content'], fetched_template.template_type)
if over_limit:
return json_resp, 400
char_count_limit = current_app.config.get('SMS_CHAR_COUNT_LIMIT')
message = 'Content has a character count greater than the limit of {}'.format(char_count_limit)
errors = {'content': [message]}
raise InvalidRequest(errors, status_code=400)
dao_update_template(update_dict)
return jsonify(data=template_schema.dump(update_dict).data), 200
@@ -80,39 +78,35 @@ def update_template(service_id, template_id):
@template.route('', methods=['GET'])
def get_all_templates_for_service(service_id):
templates = dao_get_all_templates_for_service(service_id=service_id)
data, errors = template_schema.dump(templates, many=True)
data = template_schema.dump(templates, many=True).data
return jsonify(data=data)
@template.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, errors = template_schema.dump(fetched_template)
data = template_schema.dump(fetched_template).data
return jsonify(data=data)
@template.route('/<uuid:template_id>/version/<int:version>')
def get_template_version(service_id, template_id, version):
data, errors = template_history_schema.dump(
data = template_history_schema.dump(
dao_get_template_by_id_and_service_id(
template_id=template_id,
service_id=service_id,
version=version
)
)
if errors:
return jsonify(result='error', message=errors), 400
).data
return jsonify(data=data)
@template.route('/<uuid:template_id>/versions')
def get_template_versions(service_id, template_id):
data, errors = template_history_schema.dump(
data = template_history_schema.dump(
dao_get_template_versions(service_id=service_id, template_id=template_id),
many=True
)
if errors:
return jsonify(result='error', message=errors), 400
).data
return jsonify(data=data)

View File

@@ -15,7 +15,7 @@ template_statistics = Blueprint('template-statistics',
__name__,
url_prefix='/service/<service_id>/template-statistics')
from app.errors import register_errors, InvalidData
from app.errors import register_errors, InvalidRequest
register_errors(template_statistics)
@@ -28,20 +28,16 @@ def get_template_statistics_for_service(service_id):
except ValueError as e:
error = '{} is not an integer'.format(request.args['limit_days'])
message = {'limit_days': [error]}
raise InvalidData(message, status_code=400)
raise InvalidRequest(message, status_code=400)
else:
limit_days = None
stats = dao_get_template_statistics_for_service(service_id, limit_days=limit_days)
data, errors = template_statistics_schema.dump(stats, many=True)
if errors:
raise InvalidData(errors, status_code=400)
data = template_statistics_schema.dump(stats, many=True).data
return jsonify(data=data)
@template_statistics.route('/<template_id>')
def get_template_statistics_for_template_id(service_id, template_id):
stats = dao_get_template_statistics_for_template(template_id)
data, errors = template_statistics_schema.dump(stats, many=True)
if errors:
raise InvalidData(errors, status_code=400)
data = template_statistics_schema.dump(stats, many=True).data
return jsonify(data=data)

View File

@@ -1,7 +1,7 @@
import json
import uuid
from datetime import datetime
from flask import (jsonify, request, abort, Blueprint, current_app)
from flask import (jsonify, request, Blueprint, current_app)
from app import encryption, DATETIME_FORMAT
from app.dao.users_dao import (
get_model_users,
@@ -28,9 +28,13 @@ from app.schemas import (
from app.celery.tasks import (
send_sms,
email_reset_password,
send_email)
send_email
)
from app.errors import register_errors
from app.errors import (
register_errors,
InvalidRequest
)
user = Blueprint('user', __name__)
register_errors(user)
@@ -43,9 +47,7 @@ def create_user():
# TODO password policy, what is valid password
if not req_json.get('password', None):
errors.update({'password': ['Missing data for required field.']})
return jsonify(result="error", message=errors), 400
if errors:
return jsonify(result="error", message=errors), 400
raise InvalidRequest(errors, status_code=400)
save_model_user(user_to_create, pwd=req_json.get('password'))
return jsonify(data=user_schema.dump(user_to_create).data), 201
@@ -60,11 +62,9 @@ def update_user(user_id):
# but would be good to have the same validation here.
if pwd is not None and not pwd:
errors.update({'password': ['Invalid data for field']})
if errors:
return jsonify(result="error", message=errors), 400
status_code = 200
raise InvalidRequest(errors, status_code=400)
save_model_user(user_to_update, update_dict=update_dct, pwd=pwd)
return jsonify(data=user_schema.dump(user_to_update).data), status_code
return jsonify(data=user_schema.dump(user_to_update).data), 200
@user.route('/<uuid:user_id>/verify/password', methods=['POST'])
@@ -75,9 +75,10 @@ def verify_user_password(user_id):
try:
txt_pwd = request.get_json()['password']
except KeyError:
return jsonify(
result="error",
message={'password': ['Required field missing data']}), 400
message = 'Required field missing data'
errors = {'password': [message]}
raise InvalidRequest(errors, status_code=400)
if user_to_verify.check_password(txt_pwd):
user_to_verify.logged_in_at = datetime.utcnow()
save_model_user(user_to_verify)
@@ -85,7 +86,9 @@ def verify_user_password(user_id):
return jsonify({}), 204
else:
increment_failed_login_count(user_to_verify)
return jsonify(result='error', message={'password': ['Incorrect password']}), 400
message = 'Incorrect password'
errors = {'password': [message]}
raise InvalidRequest(errors, status_code=400)
@user.route('/<uuid:user_id>/verify/code', methods=['POST'])
@@ -105,12 +108,13 @@ def verify_user_code(user_id):
except KeyError:
errors.update({'code_type': ['Required field missing data']})
if errors:
return jsonify(result="error", message=errors), 400
raise InvalidRequest(errors, status_code=400)
code = get_user_code(user_to_verify, txt_code, txt_type)
if not code:
return jsonify(result="error", message="Code not found"), 404
raise InvalidRequest("Code not found", status_code=404)
if datetime.utcnow() > code.expiry_datetime or code.code_used:
return jsonify(result="error", message="Code has expired"), 400
raise InvalidRequest("Code has expired", status_code=400)
use_user_code(code.id)
return jsonify({}), 204
@@ -119,8 +123,6 @@ def verify_user_code(user_id):
def send_user_sms_code(user_id):
user_to_send_to = get_model_users(user_id=user_id)
verify_code, errors = request_verify_code_schema.load(request.get_json())
if errors:
return jsonify(result="error", message=errors), 400
secret_code = create_secret_code()
create_user_code(user_to_send_to, secret_code, 'sms')
@@ -150,8 +152,6 @@ def send_user_sms_code(user_id):
def send_user_email_verification(user_id):
user_to_send_to = get_model_users(user_id=user_id)
verify_code, errors = request_verify_code_schema.load(request.get_json())
if errors:
return jsonify(result="error", message=errors), 400
secret_code = create_secret_code()
create_user_code(user_to_send_to, secret_code, 'email')
@@ -191,8 +191,7 @@ def set_permissions(user_id, service_id):
user = get_model_users(user_id=user_id)
service = dao_fetch_service_by_id(service_id=service_id)
permissions, errors = permission_schema.load(request.get_json(), many=True)
if errors:
abort(400, errors)
for p in permissions:
p.user = user
p.service = service
@@ -204,7 +203,8 @@ def set_permissions(user_id, service_id):
def get_by_email():
email = request.args.get('email')
if not email:
return jsonify(result="error", message="invalid request"), 400
error = 'Invalid request. Email query string param required'
raise InvalidRequest(error, status_code=400)
fetched_user = get_user_by_email(email)
result = user_schema.dump(fetched_user)
@@ -214,8 +214,6 @@ def get_by_email():
@user.route('/reset-password', methods=['POST'])
def send_user_reset_password():
email, errors = email_data_request_schema.load(request.get_json())
if errors:
return jsonify(result="error", message=errors), 400
user_to_send_to = get_user_by_email(email['email'])

View File

@@ -267,6 +267,6 @@ def test_get_template_versions(sample_template):
assert x.content == 'new version'
else:
assert x.content == original_content
from app.schemas import (template_history_schema)
from app.schemas import template_history_schema
v = template_history_schema.load(versions, many=True)
assert v.__len__() == 2

View File

@@ -296,7 +296,7 @@ def test_get_user_by_email_bad_url_returns_404(notify_api,
assert resp.status_code == 400
json_resp = json.loads(resp.get_data(as_text=True))
assert json_resp['result'] == 'error'
assert json_resp['message'] == 'invalid request'
assert json_resp['message'] == 'Invalid request. Email query string param required'
def test_get_user_with_permissions(notify_api,