Merge pull request #139 from alphagov/reset-password

Reset password
This commit is contained in:
Rebecca Law
2016-03-09 10:42:38 +00:00
11 changed files with 191 additions and 60 deletions

View File

@@ -197,7 +197,7 @@ def send_sms_code(encrypted_verification):
try: try:
firetext_client.send_sms(verification_message['to'], verification_message['secret_code']) firetext_client.send_sms(verification_message['to'], verification_message['secret_code'])
except FiretextClientException as e: except FiretextClientException as e:
current_app.logger.error(e) current_app.logger.exception(e)
@notify_celery.task(name='send-email-code') @notify_celery.task(name='send-email-code')
@@ -209,7 +209,7 @@ def send_email_code(encrypted_verification_message):
"Verification code", "Verification code",
verification_message['secret_code']) verification_message['secret_code'])
except AwsSesClientException as e: except AwsSesClientException as e:
current_app.logger.error(e) current_app.logger.exception(e)
# TODO: when placeholders in templates work, this will be a real template # TODO: when placeholders in templates work, this will be a real template
@@ -251,4 +251,27 @@ def email_invited_user(encrypted_invitation):
subject_line, subject_line,
invitation_content) invitation_content)
except AwsSesClientException as e: except AwsSesClientException as e:
current_app.logger.error(e) current_app.logger.exception(e)
def password_reset_message(name, url):
from string import Template
t = Template("Hi $user_name,\n\n"
"We received a request to reset your password on GOV.UK Notify.\n\n"
"If you didn't request this email, you can ignore it your password has not been changed.\n\n"
"To reset your password, click this link:\n\n"
"$url")
return t.substitute(user_name=name, url=url)
@notify_celery.task(name='email-reset-password')
def email_reset_password(encrypted_reset_password_message):
reset_password_message = encryption.decrypt(encrypted_reset_password_message)
try:
aws_ses_client.send_email(current_app.config['VERIFY_CODE_FROM_EMAIL_ADDRESS'],
reset_password_message['to'],
"Reset your GOV.UK Notify password",
password_reset_message(name=reset_password_message['name'],
url=reset_password_message['reset_password_url']))
except AwsSesClientException as e:
current_app.logger.exception(e)

View File

@@ -16,6 +16,7 @@ def save_model_user(usr, update_dict={}, pwd=None):
if update_dict: if update_dict:
if update_dict.get('id'): if update_dict.get('id'):
del update_dict['id'] del update_dict['id']
update_dict.pop('password_changed_at')
db.session.query(User).filter_by(id=usr.id).update(update_dict) db.session.query(User).filter_by(id=usr.id).update(update_dict)
else: else:
db.session.add(usr) db.session.add(usr)

View File

@@ -41,7 +41,7 @@ def register_errors(blueprint):
@blueprint.app_errorhandler(500) @blueprint.app_errorhandler(500)
def internal_server_error(e): def internal_server_error(e):
if isinstance(e, str): if isinstance(e, str):
current_app.logger.error(e) current_app.logger.exception(e)
elif isinstance(e, Exception): elif isinstance(e, Exception):
current_app.logger.exception(e) current_app.logger.exception(e)
return jsonify(result='error', message="Internal server error"), 500 return jsonify(result='error', message="Internal server error"), 500

View File

@@ -1,10 +1,8 @@
import re
from flask import current_app
from flask_marshmallow.fields import fields from flask_marshmallow.fields import fields
from . import ma from . import ma
from . import models from . import models
from app.dao.permissions_dao import permission_dao from app.dao.permissions_dao import permission_dao
from marshmallow import (post_load, ValidationError, validates, validates_schema) from marshmallow import (post_load, ValidationError, validates)
from marshmallow_sqlalchemy import field_for from marshmallow_sqlalchemy import field_for
from utils.recipients import ( from utils.recipients import (
validate_email_address, InvalidEmailError, validate_email_address, InvalidEmailError,
@@ -50,6 +48,8 @@ class BaseSchema(ma.ModelSchema):
class UserSchema(BaseSchema): class UserSchema(BaseSchema):
permissions = fields.Method("user_permissions", dump_only=True) permissions = fields.Method("user_permissions", dump_only=True)
password_changed_at = field_for(models.User, 'password_changed_at', format='%Y-%m-%d %H:%M:%S.%f')
created_at = field_for(models.User, 'created_at', format='%Y-%m-%d %H:%M:%S.%f')
def user_permissions(self, usr): def user_permissions(self, usr):
retval = {} retval = {}
@@ -95,18 +95,6 @@ class JobSchema(BaseSchema):
model = models.Job model = models.Job
# TODO: Remove this schema once the admin app has stopped using the /user/<user_id>code endpoint
class OldRequestVerifyCodeSchema(ma.Schema):
code_type = fields.Str(required=True)
to = fields.Str(required=False)
@validates('code_type')
def validate_code_type(self, code):
if code not in models.VERIFY_CODE_TYPES:
raise ValidationError('Invalid code type')
class RequestVerifyCodeSchema(ma.Schema): class RequestVerifyCodeSchema(ma.Schema):
to = fields.Str(required=False) to = fields.Str(required=False)
@@ -142,8 +130,8 @@ class EmailNotificationSchema(NotificationSchema):
def validate_to(self, value): def validate_to(self, value):
try: try:
validate_email_address(value) validate_email_address(value)
except InvalidEmailError: except InvalidEmailError as e:
raise ValidationError('Invalid email') raise ValidationError(e.message)
class SmsTemplateNotificationSchema(SmsNotificationSchema): class SmsTemplateNotificationSchema(SmsNotificationSchema):
@@ -180,8 +168,8 @@ class InvitedUserSchema(BaseSchema):
def validate_to(self, value): def validate_to(self, value):
try: try:
validate_email_address(value) validate_email_address(value)
except InvalidEmailError: except InvalidEmailError as e:
raise ValidationError('Invalid email') raise ValidationError(e.message)
class PermissionSchema(BaseSchema): class PermissionSchema(BaseSchema):
@@ -201,6 +189,16 @@ class PermissionSchema(BaseSchema):
exclude = ("created_at",) exclude = ("created_at",)
class EmailDataSchema(ma.Schema):
email = fields.Str(required=False)
@validates('email')
def validate_email(self, value):
try:
validate_email_address(value)
except InvalidEmailError as e:
raise ValidationError(e.message)
user_schema = UserSchema() user_schema = UserSchema()
user_schema_load_json = UserSchema(load_json=True) user_schema_load_json = UserSchema(load_json=True)
service_schema = ServiceSchema() service_schema = ServiceSchema()
@@ -211,7 +209,6 @@ api_key_schema = ApiKeySchema()
api_key_schema_load_json = ApiKeySchema(load_json=True) api_key_schema_load_json = ApiKeySchema(load_json=True)
job_schema = JobSchema() job_schema = JobSchema()
job_schema_load_json = JobSchema(load_json=True) job_schema_load_json = JobSchema(load_json=True)
old_request_verify_code_schema = OldRequestVerifyCodeSchema()
request_verify_code_schema = RequestVerifyCodeSchema() request_verify_code_schema = RequestVerifyCodeSchema()
sms_admin_notification_schema = SmsAdminNotificationSchema() sms_admin_notification_schema = SmsAdminNotificationSchema()
sms_template_notification_schema = SmsTemplateNotificationSchema() sms_template_notification_schema = SmsTemplateNotificationSchema()
@@ -222,4 +219,5 @@ notification_status_schema = NotificationStatusSchema()
notification_status_schema_load_json = NotificationStatusSchema(load_json=True) notification_status_schema_load_json = NotificationStatusSchema(load_json=True)
invited_user_schema = InvitedUserSchema() invited_user_schema = InvitedUserSchema()
permission_schema = PermissionSchema() permission_schema = PermissionSchema()
email_data_request_schema = EmailDataSchema()
notifications_statistics_schema = NotificationsStatisticsSchema() notifications_statistics_schema = NotificationsStatisticsSchema()

View File

@@ -1,5 +1,5 @@
from datetime import datetime from datetime import datetime
from flask import (jsonify, request, abort, Blueprint) from flask import (jsonify, request, abort, Blueprint, current_app)
from app import encryption from app import encryption
from app.dao.users_dao import ( from app.dao.users_dao import (
@@ -17,14 +17,14 @@ from app.dao.permissions_dao import permission_dao
from app.dao.services_dao import dao_fetch_service_by_id from app.dao.services_dao import dao_fetch_service_by_id
from app.schemas import ( from app.schemas import (
old_request_verify_code_schema, email_data_request_schema,
user_schema, user_schema,
request_verify_code_schema, request_verify_code_schema,
user_schema_load_json, user_schema_load_json,
permission_schema permission_schema
) )
from app.celery.tasks import (send_sms_code, send_email_code) from app.celery.tasks import (send_sms_code, send_email_code, email_reset_password)
from app.errors import register_errors from app.errors import register_errors
user = Blueprint('user', __name__) user = Blueprint('user', __name__)
@@ -49,7 +49,7 @@ def create_user():
def update_user(user_id): def update_user(user_id):
user_to_update = get_model_users(user_id=user_id) user_to_update = get_model_users(user_id=user_id)
if not user_to_update: if not user_to_update:
return jsonify(result="error", message="User not found"), 404 return _user_not_found(user_id)
req_json = request.get_json() req_json = request.get_json()
update_dct, errors = user_schema_load_json.load(req_json) update_dct, errors = user_schema_load_json.load(req_json)
@@ -118,7 +118,7 @@ def send_user_sms_code(user_id):
user_to_send_to = get_model_users(user_id=user_id) user_to_send_to = get_model_users(user_id=user_id)
if not user_to_send_to: if not user_to_send_to:
return jsonify(result="error", message="No user found"), 404 return _user_not_found(user_id)
verify_code, errors = request_verify_code_schema.load(request.get_json()) verify_code, errors = request_verify_code_schema.load(request.get_json())
if errors: if errors:
@@ -140,7 +140,7 @@ def send_user_sms_code(user_id):
def send_user_email_code(user_id): def send_user_email_code(user_id):
user_to_send_to = get_model_users(user_id=user_id) user_to_send_to = get_model_users(user_id=user_id)
if not user_to_send_to: if not user_to_send_to:
return jsonify(result="error", message="No user found"), 404 return _user_not_found(user_id)
verify_code, errors = request_verify_code_schema.load(request.get_json()) verify_code, errors = request_verify_code_schema.load(request.get_json())
if errors: if errors:
@@ -174,7 +174,7 @@ def set_permissions(user_id, service_id):
# who is making this request has permission to make the request. # who is making this request has permission to make the request.
user = get_model_users(user_id=user_id) user = get_model_users(user_id=user_id)
if not user: if not user:
abort(404, 'User not found for id: {}'.format(user_id)) _user_not_found(user_id)
service = dao_fetch_service_by_id(service_id=service_id) service = dao_fetch_service_by_id(service_id=service_id)
if not service: if not service:
abort(404, 'Service not found for id: {}'.format(service_id)) abort(404, 'Service not found for id: {}'.format(service_id))
@@ -193,9 +193,45 @@ def get_by_email():
email = request.args.get('email') email = request.args.get('email')
if not email: if not email:
return jsonify(result="error", message="invalid request"), 400 return jsonify(result="error", message="invalid request"), 400
user = get_user_by_email(email) fetched_user = get_user_by_email(email)
if not user: if not fetched_user:
return jsonify(result="error", message="not found"), 404 return _user_not_found_for_email()
result = user_schema.dump(user) result = user_schema.dump(fetched_user)
return jsonify(data=result.data) return jsonify(data=result.data)
@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'])
if not user_to_send_to:
return _user_not_found_for_email()
reset_password_message = {'to': user_to_send_to.email_address,
'name': user_to_send_to.name,
'reset_password_url': _create_reset_password_url(user_to_send_to.email_address)}
email_reset_password.apply_async([encryption.encrypt(reset_password_message)], queue='email-reset-password')
return jsonify({}), 204
def _user_not_found(user_id):
return abort(404, 'User not found for id: {}'.format(user_id))
def _user_not_found_for_email():
return abort(404, 'User not found for email address')
def _create_reset_password_url(email):
from utils.url_safe_token import generate_token
import json
data = json.dumps({'email': email, 'created_at': str(datetime.now())})
token = generate_token(data, current_app.config['SECRET_KEY'], current_app.config['DANGEROUS_SALT'])
return current_app.config['ADMIN_BASE_URL'] + '/new-password/' + token

View File

@@ -46,6 +46,7 @@ class Config(object):
Queue('email', Exchange('default'), routing_key='email'), Queue('email', Exchange('default'), routing_key='email'),
Queue('sms-code', Exchange('default'), routing_key='sms-code'), Queue('sms-code', Exchange('default'), routing_key='sms-code'),
Queue('email-code', Exchange('default'), routing_key='email-code'), Queue('email-code', Exchange('default'), routing_key='email-code'),
Queue('email-reset-password', Exchange('default'), routing_key='email-reset-password'),
Queue('process-job', Exchange('default'), routing_key='process-job'), Queue('process-job', Exchange('default'), routing_key='process-job'),
Queue('bulk-sms', Exchange('default'), routing_key='bulk-sms'), Queue('bulk-sms', Exchange('default'), routing_key='bulk-sms'),
Queue('bulk-email', Exchange('default'), routing_key='bulk-email'), Queue('bulk-email', Exchange('default'), routing_key='bulk-email'),

View File

@@ -1,7 +1,13 @@
import uuid import uuid
import pytest import pytest
from flask import current_app from flask import current_app
from app.celery.tasks import (send_sms, send_sms_code, send_email_code, send_email, process_job, email_invited_user) from app.celery.tasks import (send_sms,
send_sms_code,
send_email_code,
send_email,
process_job,
email_invited_user,
email_reset_password)
from app import (firetext_client, aws_ses_client, encryption, DATETIME_FORMAT) from app import (firetext_client, aws_ses_client, encryption, DATETIME_FORMAT)
from app.clients.email.aws_ses import AwsSesClientException from app.clients.email.aws_ses import AwsSesClientException
from app.clients.sms.firetext import FiretextClientException from app.clients.sms.firetext import FiretextClientException
@@ -504,3 +510,22 @@ def test_email_invited_user_should_send_email(notify_api, mocker):
invitation['to'], invitation['to'],
expected_subject, expected_subject,
expected_content) expected_content)
def test_email_reset_password_should_send_email(notify_api, mocker):
with notify_api.test_request_context():
reset_password_message = {'to': 'someone@it.gov.uk',
'name': 'Some One',
'reset_password_url': 'bah'}
mocker.patch('app.aws_ses_client.send_email')
mocker.patch('app.encryption.decrypt', return_value=reset_password_message)
encrypted_message = encryption.encrypt(reset_password_message)
email_reset_password(encrypted_message)
message = tasks.password_reset_message(reset_password_message['name'],
reset_password_message['reset_password_url'])
aws_ses_client.send_email(current_app.config['VERIFY_CODE_FROM_EMAIL_ADDRESS'],
reset_password_message['to'],
"Reset password for GOV.UK Notify",
message)

View File

@@ -90,7 +90,7 @@ def test_create_invited_user_invalid_email(notify_api, sample_service, mocker):
assert response.status_code == 400 assert response.status_code == 400
json_resp = json.loads(response.get_data(as_text=True)) json_resp = json.loads(response.get_data(as_text=True))
assert json_resp['result'] == 'error' assert json_resp['result'] == 'error'
assert json_resp['message'] == {'email_address': ['Invalid email']} assert json_resp['message'] == {'email_address': ['Not a valid email address']}
app.celery.tasks.email_invited_user.apply_async.assert_not_called() app.celery.tasks.email_invited_user.apply_async.assert_not_called()

View File

@@ -582,7 +582,7 @@ def test_should_reject_email_notification_with_bad_email(notify_api, sample_emai
app.celery.tasks.send_email.apply_async.assert_not_called() app.celery.tasks.send_email.apply_async.assert_not_called()
assert response.status_code == 400 assert response.status_code == 400
assert data['result'] == 'error' assert data['result'] == 'error'
assert data['message']['to'][0] == 'Invalid email' assert data['message']['to'][0] == 'Not a valid email address'
def test_should_reject_email_notification_with_template_id_that_cant_be_found( def test_should_reject_email_notification_with_template_id_that_cant_be_found(

View File

@@ -1,10 +1,12 @@
import json import json
import uuid
from flask import url_for from flask import url_for
import app
from app.models import (User, Permission, MANAGE_SETTINGS, MANAGE_TEMPLATES) from app.models import (User, Permission, MANAGE_SETTINGS, MANAGE_TEMPLATES)
from app.dao.permissions_dao import default_service_permissions from app.dao.permissions_dao import default_service_permissions
from app import db from app import db, encryption
from tests import create_authorization_header from tests import create_authorization_header
@@ -256,7 +258,7 @@ def test_put_user_not_exists(notify_api, notify_db, notify_db_session, sample_us
user = User.query.filter_by(id=sample_user.id).first() user = User.query.filter_by(id=sample_user.id).first()
json_resp = json.loads(resp.get_data(as_text=True)) json_resp = json.loads(resp.get_data(as_text=True))
assert json_resp['result'] == "error" assert json_resp['result'] == "error"
assert json_resp['message'] == "User not found" assert json_resp['message'] == "User not found for id: {}".format("9999")
assert user == sample_user assert user == sample_user
assert user.email_address != new_email assert user.email_address != new_email
@@ -284,7 +286,7 @@ def test_get_user_by_email(notify_api, notify_db, notify_db_session, sample_serv
assert sorted(expected_permissions) == sorted(fetched['permissions'][str(sample_service.id)]) assert sorted(expected_permissions) == sorted(fetched['permissions'][str(sample_service.id)])
def test_get_user_by_email_not_found_returns_400(notify_api, def test_get_user_by_email_not_found_returns_404(notify_api,
notify_db, notify_db,
notify_db_session, notify_db_session,
sample_user): sample_user):
@@ -297,7 +299,7 @@ def test_get_user_by_email_not_found_returns_400(notify_api,
assert resp.status_code == 404 assert resp.status_code == 404
json_resp = json.loads(resp.get_data(as_text=True)) json_resp = json.loads(resp.get_data(as_text=True))
assert json_resp['result'] == 'error' assert json_resp['result'] == 'error'
assert json_resp['message'] == 'not found' assert json_resp['message'] == 'User not found for email address'
def test_get_user_by_email_bad_url_returns_404(notify_api, def test_get_user_by_email_bad_url_returns_404(notify_api,
@@ -426,3 +428,64 @@ def test_set_user_permissions_remove_old(notify_api,
query = Permission.query.filter_by(user=sample_user) query = Permission.query.filter_by(user=sample_user)
assert query.count() == 1 assert query.count() == 1
assert query.first().permission == MANAGE_SETTINGS assert query.first().permission == MANAGE_SETTINGS
def test_send_user_reset_password_should_send_reset_password_link(notify_api,
sample_user,
mocker,
mock_encryption):
with notify_api.test_request_context():
with notify_api.test_client() as client:
mocker.patch('app.celery.tasks.email_reset_password.apply_async')
data = json.dumps({'email': sample_user.email_address})
auth_header = create_authorization_header(
path=url_for('user.send_user_reset_password'),
method='POST',
request_body=data)
resp = client.post(
url_for('user.send_user_reset_password'),
data=data,
headers=[('Content-Type', 'application/json'), auth_header])
assert resp.status_code == 204
app.celery.tasks.email_reset_password.apply_async.assert_called_once_with(['something_encrypted'],
queue='email-reset-password')
def test_send_user_reset_password_should_return_400_when_user_doesnot_exist(notify_api,
mocker):
with notify_api.test_request_context():
with notify_api.test_client() as client:
bad_email_address = 'bad@email.gov.uk'
data = json.dumps({'email': bad_email_address})
auth_header = create_authorization_header(
path=url_for('user.send_user_reset_password'),
method='POST',
request_body=data)
resp = client.post(
url_for('user.send_user_reset_password'),
data=data,
headers=[('Content-Type', 'application/json'), auth_header])
assert resp.status_code == 404
assert json.loads(resp.get_data(as_text=True))['message'] == 'User not found for email address'
def test_send_user_reset_password_should_return_400_when_data_is_not_email_address(notify_api, mocker):
with notify_api.test_request_context():
with notify_api.test_client() as client:
bad_email_address = 'bad.email.gov.uk'
data = json.dumps({'email': bad_email_address})
auth_header = create_authorization_header(
path=url_for('user.send_user_reset_password'),
method='POST',
request_body=data)
resp = client.post(
url_for('user.send_user_reset_password'),
data=data,
headers=[('Content-Type', 'application/json'), auth_header])
assert resp.status_code == 400
assert json.loads(resp.get_data(as_text=True))['message'] == {'email': ['Not a valid email address']}

View File

@@ -10,8 +10,6 @@ from freezegun import freeze_time
def test_user_verify_code_sms(notify_api, def test_user_verify_code_sms(notify_api,
notify_db,
notify_db_session,
sample_sms_code): sample_sms_code):
""" """
Tests POST endpoint '/<user_id>/verify/code' Tests POST endpoint '/<user_id>/verify/code'
@@ -35,8 +33,6 @@ def test_user_verify_code_sms(notify_api,
def test_user_verify_code_sms_missing_code(notify_api, def test_user_verify_code_sms_missing_code(notify_api,
notify_db,
notify_db_session,
sample_sms_code): sample_sms_code):
""" """
Tests POST endpoint '/<user_id>/verify/code' Tests POST endpoint '/<user_id>/verify/code'
@@ -59,8 +55,6 @@ def test_user_verify_code_sms_missing_code(notify_api,
@moto.mock_sqs @moto.mock_sqs
def test_user_verify_code_email(notify_api, def test_user_verify_code_email(notify_api,
notify_db,
notify_db_session,
sqs_client_conn, sqs_client_conn,
sample_email_code): sample_email_code):
""" """
@@ -85,8 +79,6 @@ def test_user_verify_code_email(notify_api,
def test_user_verify_code_email_bad_code(notify_api, def test_user_verify_code_email_bad_code(notify_api,
notify_db,
notify_db_session,
sample_email_code): sample_email_code):
""" """
Tests POST endpoint '/<user_id>/verify/code' Tests POST endpoint '/<user_id>/verify/code'
@@ -110,8 +102,6 @@ def test_user_verify_code_email_bad_code(notify_api,
def test_user_verify_code_email_expired_code(notify_api, def test_user_verify_code_email_expired_code(notify_api,
notify_db,
notify_db_session,
sample_email_code): sample_email_code):
""" """
Tests POST endpoint '/<user_id>/verify/code' Tests POST endpoint '/<user_id>/verify/code'
@@ -162,8 +152,6 @@ def test_user_verify_password(notify_api,
def test_user_verify_password_invalid_password(notify_api, def test_user_verify_password_invalid_password(notify_api,
notify_db,
notify_db_session,
sample_user): sample_user):
""" """
Tests POST endpoint '/<user_id>/verify/password' invalid endpoint. Tests POST endpoint '/<user_id>/verify/password' invalid endpoint.
@@ -189,8 +177,6 @@ def test_user_verify_password_invalid_password(notify_api,
def test_user_verify_password_valid_password_resets_failed_logins(notify_api, def test_user_verify_password_valid_password_resets_failed_logins(notify_api,
notify_db,
notify_db_session,
sample_user): sample_user):
with notify_api.test_request_context(): with notify_api.test_request_context():
with notify_api.test_client() as client: with notify_api.test_client() as client:
@@ -227,8 +213,6 @@ def test_user_verify_password_valid_password_resets_failed_logins(notify_api,
def test_user_verify_password_missing_password(notify_api, def test_user_verify_password_missing_password(notify_api,
notify_db,
notify_db_session,
sample_user): sample_user):
""" """
Tests POST endpoint '/<user_id>/verify/password' missing password. Tests POST endpoint '/<user_id>/verify/password' missing password.
@@ -314,7 +298,7 @@ def test_send_sms_code_returns_404_for_bad_input_data(notify_api, notify_db, not
data=data, data=data,
headers=[('Content-Type', 'application/json'), auth_header]) headers=[('Content-Type', 'application/json'), auth_header])
assert resp.status_code == 404 assert resp.status_code == 404
assert json.loads(resp.get_data(as_text=True))['message'] == 'No user found' assert json.loads(resp.get_data(as_text=True))['message'] == 'User not found for id: {}'.format(int(uuid_))
def test_send_user_email_code(notify_api, def test_send_user_email_code(notify_api,
@@ -356,4 +340,4 @@ def test_send_user_email_code_returns_404_for_when_user_does_not_exist(notify_ap
data=data, data=data,
headers=[('Content-Type', 'application/json'), auth_header]) headers=[('Content-Type', 'application/json'), auth_header])
assert resp.status_code == 404 assert resp.status_code == 404
assert json.loads(resp.get_data(as_text=True))['message'] == 'No user found' assert json.loads(resp.get_data(as_text=True))['message'] == 'User not found for id: {}'.format(1)