Merge pull request #1898 from alphagov/get-all-complaints

Get all complaints
This commit is contained in:
Rebecca Law
2018-06-06 11:00:37 +01:00
committed by GitHub
11 changed files with 139 additions and 56 deletions

View File

@@ -112,6 +112,7 @@ def register_blueprint(application):
from app.billing.rest import billing_blueprint
from app.organisation.rest import organisation_blueprint
from app.organisation.invite_rest import organisation_invite_blueprint
from app.complaint.complaint_rest import complaint_blueprint
service_blueprint.before_request(requires_admin_auth)
application.register_blueprint(service_blueprint, url_prefix='/service')
@@ -188,6 +189,9 @@ def register_blueprint(application):
organisation_invite_blueprint.before_request(requires_admin_auth)
application.register_blueprint(organisation_invite_blueprint)
complaint_blueprint.before_request(requires_admin_auth)
application.register_blueprint(complaint_blueprint)
def register_v2_blueprints(application):
from app.v2.inbound_sms.get_inbound_sms import v2_inbound_sms_blueprint as get_inbound_sms

View File

View File

@@ -0,0 +1,16 @@
from flask import Blueprint, jsonify
from sqlalchemy import desc
from app.errors import register_errors
from app.models import Complaint
complaint_blueprint = Blueprint('complaint', __name__, url_prefix='/complaint')
register_errors(complaint_blueprint)
@complaint_blueprint.route('', methods=['GET'])
def get_all_complaints():
complaints = Complaint.query.order_by(desc(Complaint.created_at)).all()
return jsonify([x.serialize() for x in complaints]), 200

View File

@@ -1819,6 +1819,7 @@ class FactNotificationStatus(db.Model):
class Complaint(db.Model):
__tablename__ = 'complaints'
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
notification_id = db.Column(UUID(as_uuid=True), db.ForeignKey('notification_history.id'),
index=True, nullable=False)
@@ -1828,3 +1829,15 @@ class Complaint(db.Model):
complaint_type = db.Column(db.Text, nullable=True)
complaint_date = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
def serialize(self):
return {
'id': str(self.id),
'notification_id': str(self.notification_id),
'service_id': str(self.service_id),
'service_name': self.service.name,
'ses_feedback_id': str(self.ses_feedback_id),
'complaint_type': self.complaint_type,
'complaint_date': self.complaint_date.strftime(DATETIME_FORMAT) if self.complaint_date else None,
'created_at': self.created_at.strftime(DATETIME_FORMAT),
}

View File

@@ -38,7 +38,7 @@ def process_ses_response(ses_request):
if notification_type == 'Bounce':
notification_type = determine_notification_bounce_type(notification_type, ses_message)
elif notification_type == 'Complaint':
handle_complaint(ses_request)
handle_complaint(ses_message)
return
try:
@@ -108,13 +108,11 @@ def remove_emails_from_bounce(bounce_dict):
recip.pop('emailAddress')
def handle_complaint(ses_request):
ses_message = json.loads(ses_request['Message'])
def handle_complaint(ses_message):
remove_emails_from_complaint(ses_message)
current_app.logger.info("Complaint from SES: \n{}".format(ses_message))
# It is possible that the we get a key error, let this fail so we can investigate.
try:
reference = ses_request['MessageId']
reference = ses_message['mail']['messageId']
except KeyError as e:
current_app.logger.exception("Complaint from SES failed to get reference from message", e)
return
@@ -133,6 +131,7 @@ def handle_complaint(ses_request):
def remove_emails_from_complaint(complaint_dict):
complaint_dict['complaint'].pop('complainedRecipients')
complaint_dict['mail'].pop('destination')
def _check_and_queue_callback_task(notification):

View File

@@ -7,7 +7,7 @@ from app.notifications.notifications_ses_callback import remove_emails_from_comp
from tests.app.db import (
create_notification, ses_complaint_callback,
ses_notification_callback
ses_notification_callback,
)
@@ -40,8 +40,7 @@ def test_process_ses_results_retry_called(notify_db, mocker):
def test_process_ses_results_in_complaint(sample_email_template, mocker):
notification = create_notification(template=sample_email_template, reference='ref1')
mocked = mocker.patch("app.dao.notifications_dao.update_notification_status_by_reference")
response = json.loads(ses_complaint_callback())
process_ses_results(response=response)
process_ses_results(response=ses_complaint_callback())
assert mocked.call_count == 0
complaints = Complaint.query.all()
assert len(complaints) == 1
@@ -49,7 +48,6 @@ def test_process_ses_results_in_complaint(sample_email_template, mocker):
def test_remove_emails_from_complaint():
test_message = ses_complaint_callback()
test_json = json.loads(json.loads(test_message)['Message'])
test_json = json.loads(ses_complaint_callback()['Message'])
remove_emails_from_complaint(test_json)
assert "recipient1@example.com" not in test_json
assert "recipient1@example.com" not in json.dumps(test_json)

View File

View File

@@ -0,0 +1,24 @@
import json
from tests import create_authorization_header
from tests.app.db import create_complaint, create_service, create_template, create_notification
def test_get_all_complaints_returns_list_for_multiple_services_and_complaints(client, notify_db_session):
service = create_service(service_name='service1')
template = create_template(service=service)
notification = create_notification(template=template)
complaint_1 = create_complaint() # default service
complaint_2 = create_complaint(service=service, notification=notification)
response = client.get('/complaint', headers=[create_authorization_header()])
assert response.status_code == 200
assert json.loads(response.get_data(as_text=True)) == [complaint_2.serialize(), complaint_1.serialize()]
def test_get_all_complaints_returns_empty_list(client):
response = client.get('/complaint', headers=[create_authorization_header()])
assert response.status_code == 200
assert json.loads(response.get_data(as_text=True)) == []

View File

@@ -22,7 +22,7 @@ def test_fetch_complaint_by_service_returns_one(sample_service, sample_email_not
assert complaints[0] == complaint
def test_fetch_complaint_by_service_returns_empty_list(sample_service, sample_email_notification):
def test_fetch_complaint_by_service_returns_empty_list(sample_service):
complaints = fetch_complaints_by_service(service_id=sample_service.id)
assert len(complaints) == 0

View File

@@ -34,7 +34,8 @@ from app.models import (
AnnualBilling,
LetterRate,
InvitedOrganisationUser,
FactBilling
FactBilling,
Complaint
)
from app.dao.users_dao import save_model_user
from app.dao.notifications_dao import (
@@ -398,10 +399,10 @@ def create_monthly_billing_entry(
def create_reply_to_email(
service,
email_address,
is_default=True,
archived=False
service,
email_address,
is_default=True,
archived=False
):
data = {
'service': service,
@@ -418,11 +419,11 @@ def create_reply_to_email(
def create_service_sms_sender(
service,
sms_sender,
is_default=True,
inbound_number_id=None,
archived=False
service,
sms_sender,
is_default=True,
inbound_number_id=None,
archived=False
):
data = {
'service_id': service.id,
@@ -440,10 +441,10 @@ def create_service_sms_sender(
def create_letter_contact(
service,
contact_block,
is_default=True,
archived=False
service,
contact_block,
is_default=True,
archived=False
):
data = {
'service': service,
@@ -564,44 +565,65 @@ def create_ft_billing(bst_date,
return data
def create_complaint(service=None,
notification=None):
if not service:
service = create_service()
if not notification:
template = create_template(service=service, template_type='email')
notification = create_notification(template=template)
complaint = Complaint(notification_id=notification.id,
service_id=service.id,
ses_feedback_id=str(uuid.uuid4()),
complaint_type='abuse',
complaint_date=datetime.utcnow()
)
db.session.add(complaint)
db.session.commit()
return complaint
def ses_complaint_callback_malformed_message_id():
return '{\n "Type" : "Notification",\n "msgId" : "ref1",' \
'\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",' \
'\n "Message" : "{\\"notificationType\\":\\"Complaint\\",' \
'\\"complaint\\": {\\"userAgent\\":\\"AnyCompany Feedback Loop (V0.01)\\",' \
'\\"complainedRecipients\\":[{\\"emailAddress\\":\\"recipient1@example.com\\"}],' \
'\\"arrivalDate\\":\\"2009-12-03T04:24:21.000-05:00\\", ' \
'\\"timestamp\\":\\"2012-05-25T14:59:38.623Z\\", ' \
'\\"feedbackId\\":\\"someSESID\\"}}"\n}'
return {
'Signature': 'bb',
'SignatureVersion': '1', 'MessageAttributes': {}, 'MessageId': '98c6e927-af5d-5f3b-9522-bab736f2cbde',
'UnsubscribeUrl': 'https://sns.eu-west-1.amazonaws.com',
'TopicArn': 'arn:ses_notifications', 'Type': 'Notification',
'Timestamp': '2018-06-05T14:00:15.952Z', 'Subject': None,
'Message': '{"notificationType":"Complaint","complaint":{"complainedRecipients":[{"emailAddress":"recipient1@example.com"}],"timestamp":"2018-06-05T13:59:58.000Z","feedbackId":"ses_feedback_id"},"mail":{"timestamp":"2018-06-05T14:00:15.950Z","source":"\\"Some Service\\" <someservicenotifications.service.gov.uk>","sourceArn":"arn:identity/notifications.service.gov.uk","sourceIp":"52.208.24.161","sendingAccountId":"888450439860","badMessageId":"ref1","destination":["recipient1@example.com"]}}', # noqa
'SigningCertUrl': 'https://sns.pem'
}
def ses_complaint_callback_with_missing_complaint_type():
"""
https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#complaint-object
"""
return '{\n "Type" : "Notification",\n "MessageId" : "ref1",' \
'\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",' \
'\n "Message" : "{\\"notificationType\\":\\"Complaint\\",' \
'\\"complaint\\": {\\"userAgent\\":\\"AnyCompany Feedback Loop (V0.01)\\",' \
'\\"complainedRecipients\\":[{\\"emailAddress\\":\\"recipient1@example.com\\"}],' \
'\\"arrivalDate\\":\\"2009-12-03T04:24:21.000-05:00\\", ' \
'\\"timestamp\\":\\"2012-05-25T14:59:38.623Z\\", ' \
'\\"feedbackId\\":\\"someSESID\\"}}"\n}'
return {
'Signature': 'bb',
'SignatureVersion': '1', 'MessageAttributes': {}, 'MessageId': '98c6e927-af5d-5f3b-9522-bab736f2cbde',
'UnsubscribeUrl': 'https://sns.eu-west-1.amazonaws.com',
'TopicArn': 'arn:ses_notifications', 'Type': 'Notification',
'Timestamp': '2018-06-05T14:00:15.952Z', 'Subject': None,
'Message': '{"notificationType":"Complaint","complaint":{"complainedRecipients":[{"emailAddress":"recipient1@example.com"}],"timestamp":"2018-06-05T13:59:58.000Z","feedbackId":"ses_feedback_id"},"mail":{"timestamp":"2018-06-05T14:00:15.950Z","source":"\\"Some Service\\" <someservicenotifications.service.gov.uk>","sourceArn":"arn:identity/notifications.service.gov.uk","sourceIp":"52.208.24.161","sendingAccountId":"888450439860","messageId":"ref1","destination":["recipient1@example.com"]}}', # noqa
'SigningCertUrl': 'https://sns.pem'
}
def ses_complaint_callback():
"""
https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#complaint-object
"""
return '{\n "Type" : "Notification",\n "MessageId" : "ref1",' \
'\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",' \
'\n "Message" : "{\\"notificationType\\":\\"Complaint\\",' \
'\\"complaint\\": {\\"userAgent\\":\\"AnyCompany Feedback Loop (V0.01)\\",' \
'\\"complainedRecipients\\":[{\\"emailAddress\\":\\"recipient1@example.com\\"}],' \
'\\"complaintFeedbackType\\":\\"abuse\\", ' \
'\\"arrivalDate\\":\\"2009-12-03T04:24:21.000-05:00\\", ' \
'\\"timestamp\\":\\"2012-05-25T14:59:38.623Z\\", ' \
'\\"feedbackId\\":\\"someSESID\\"}}"\n}'
return {
'Signature': 'bb',
'SignatureVersion': '1', 'MessageAttributes': {}, 'MessageId': '98c6e927-af5d-5f3b-9522-bab736f2cbde',
'UnsubscribeUrl': 'https://sns.eu-west-1.amazonaws.com',
'TopicArn': 'arn:ses_notifications', 'Type': 'Notification',
'Timestamp': '2018-06-05T14:00:15.952Z', 'Subject': None,
'Message': '{"notificationType":"Complaint","complaint":{"complaintFeedbackType": "abuse", "complainedRecipients":[{"emailAddress":"recipient1@example.com"}],"timestamp":"2018-06-05T13:59:58.000Z","feedbackId":"ses_feedback_id"},"mail":{"timestamp":"2018-06-05T14:00:15.950Z","source":"\\"Some Service\\" <someservicenotifications.service.gov.uk>","sourceArn":"arn:identity/notifications.service.gov.uk","sourceIp":"52.208.24.161","sendingAccountId":"888450439860","messageId":"ref1","destination":["recipient1@example.com"]}}', # noqa
'SigningCertUrl': 'https://sns.pem'
}
def ses_notification_callback():

View File

@@ -1,7 +1,9 @@
from datetime import datetime
import pytest
from flask import json
from freezegun import freeze_time
from sqlalchemy.exc import SQLAlchemyError
from app import statsd_client
from app.dao.notifications_dao import get_notification_by_id
@@ -201,23 +203,28 @@ def test_remove_emails_from_bounce():
def test_process_ses_results_in_complaint(sample_email_template):
notification = create_notification(template=sample_email_template, reference='ref1')
response = json.loads(ses_complaint_callback())
handle_complaint(response)
handle_complaint(json.loads(ses_complaint_callback()['Message']))
complaints = Complaint.query.all()
assert len(complaints) == 1
assert complaints[0].notification_id == notification.id
def test_handle_complaint_does_not_raise_exception_if_reference_is_missing(notify_api):
response = json.loads(ses_complaint_callback_malformed_message_id())
response = json.loads(ses_complaint_callback_malformed_message_id()['Message'])
handle_complaint(response)
assert len(Complaint.query.all()) == 0
def test_handle_complaint_does_raise_exception_if_notification_not_found(notify_api):
response = json.loads(ses_complaint_callback()['Message'])
with pytest.raises(expected_exception=SQLAlchemyError):
handle_complaint(response)
def test_process_ses_results_in_complaint_save_complaint_with_null_complaint_type(notify_api, sample_email_template):
notification = create_notification(template=sample_email_template, reference='ref1')
response = json.loads(ses_complaint_callback_with_missing_complaint_type())
handle_complaint(response)
msg = json.loads(ses_complaint_callback_with_missing_complaint_type()['Message'])
handle_complaint(msg)
complaints = Complaint.query.all()
assert len(complaints) == 1
assert complaints[0].notification_id == notification.id