342 строки
13 KiB
Python
342 строки
13 KiB
Python
# Copyright 2021 Google Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License")
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import email
|
|
import collections
|
|
import json
|
|
import testing_config_py2 # Must be imported before the module under test.
|
|
import mock
|
|
import unittest
|
|
|
|
from google.appengine.api import mail
|
|
from google.appengine.api import urlfetch
|
|
|
|
import settings
|
|
import sendemail
|
|
|
|
class OutboundEmailHandlerTest(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
self.request_path = '/tasks/outbound-email'
|
|
|
|
self.to = 'user@example.com'
|
|
self.subject = 'test subject'
|
|
self.html = '<b>body</b>'
|
|
self.sender = ('Chromestatus <admin@%s.appspotmail.com>' %
|
|
settings.APP_ID)
|
|
self.refs = 'fake-message-id-of-previous-message'
|
|
|
|
@mock.patch('settings.SEND_EMAIL', True)
|
|
@mock.patch('settings.SEND_ALL_EMAIL_TO', None)
|
|
@mock.patch('google.appengine.api.mail.EmailMessage')
|
|
def test_post__prod(self, mock_emailmessage_constructor):
|
|
"""On cr-status, we send emails to real users."""
|
|
params = {
|
|
'to': self.to,
|
|
'subject': self.subject,
|
|
'html': self.html,
|
|
'references': self.refs,
|
|
}
|
|
with sendemail.app.test_request_context(self.request_path, json=params):
|
|
actual_response = sendemail.handle_outbound_mail_task()
|
|
|
|
mock_emailmessage_constructor.assert_called_once_with(
|
|
sender=self.sender, to=self.to, subject=self.subject,
|
|
html=self.html)
|
|
mock_message = mock_emailmessage_constructor.return_value
|
|
mock_message.check_initialized.assert_called_once_with()
|
|
mock_message.send.assert_called_once_with()
|
|
self.assertEqual({'message': 'Done'}, actual_response)
|
|
self.assertEqual(self.refs, mock_message.headers['References'])
|
|
self.assertEqual(self.refs, mock_message.headers['In-Reply-To'])
|
|
|
|
@mock.patch('settings.SEND_EMAIL', True)
|
|
@mock.patch('google.appengine.api.mail.EmailMessage')
|
|
def test_post__staging(self, mock_emailmessage_constructor):
|
|
"""On cr-status-staging, we send emails to an archive."""
|
|
params = {
|
|
'to': self.to,
|
|
'subject': self.subject,
|
|
'html': self.html,
|
|
}
|
|
with sendemail.app.test_request_context(self.request_path, json=params):
|
|
actual_response = sendemail.handle_outbound_mail_task()
|
|
|
|
expected_to = 'cr-status-staging-emails+user+example.com@google.com'
|
|
mock_emailmessage_constructor.assert_called_once_with(
|
|
sender=self.sender, to=expected_to, subject=self.subject,
|
|
html=self.html)
|
|
mock_message = mock_emailmessage_constructor.return_value
|
|
mock_message.check_initialized.assert_called_once_with()
|
|
mock_message.send.assert_called_once_with()
|
|
self.assertEqual({'message': 'Done'}, actual_response)
|
|
|
|
@mock.patch('settings.SEND_EMAIL', False)
|
|
@mock.patch('google.appengine.api.mail.EmailMessage')
|
|
def test_post__local(self, mock_emailmessage_constructor):
|
|
"""When running locally, we don't actually send emails."""
|
|
params = {
|
|
'to': self.to,
|
|
'subject': self.subject,
|
|
'html': self.html,
|
|
}
|
|
with sendemail.app.test_request_context(self.request_path, json=params):
|
|
actual_response = sendemail.handle_outbound_mail_task()
|
|
|
|
expected_to = 'cr-status-staging-emails+user+example.com@google.com'
|
|
mock_emailmessage_constructor.assert_called_once_with(
|
|
sender=self.sender, to=expected_to, subject=self.subject,
|
|
html=self.html)
|
|
mock_message = mock_emailmessage_constructor.return_value
|
|
mock_message.check_initialized.assert_called_once_with()
|
|
mock_message.send.assert_not_called()
|
|
self.assertEqual({'message': 'Done'}, actual_response)
|
|
|
|
|
|
class BouncedEmailHandlerTest(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
self.sender = ('Chromestatus <admin@%s.appspotmail.com>' %
|
|
settings.APP_ID)
|
|
self.expected_to = settings.BOUNCE_ESCALATION_ADDR
|
|
|
|
@mock.patch('sendemail.receive')
|
|
def test_process_post_data(self, mock_receive):
|
|
with sendemail.app.test_request_context('/_ah/bounce'):
|
|
actual_json = sendemail.handle_bounce()
|
|
|
|
self.assertEqual({'message': 'Done'}, actual_json)
|
|
mock_receive.assert_called_once()
|
|
|
|
|
|
@mock.patch('settings.SEND_EMAIL', True)
|
|
@mock.patch('google.appengine.api.mail.EmailMessage')
|
|
def test_receive__user_has_prefs(self, mock_emailmessage_constructor):
|
|
"""When we get a bounce, we update the UserPrefs for that user."""
|
|
#starrer_3_pref = models.UserPref(
|
|
# email='starrer_3@example.com',
|
|
# notify_as_starrer=False)
|
|
#starrer_3_pref.put()
|
|
|
|
bounce_message = testing_config_py2.Blank(
|
|
original={'to': 'starrer_3@example.com',
|
|
'from': 'sender',
|
|
'subject': 'subject',
|
|
'text': 'body'})
|
|
|
|
sendemail.receive(bounce_message)
|
|
|
|
# TODO(jrobbins): Redo this testing after this aspect of the code is
|
|
# re-implemented.
|
|
# updated_pref = models.UserPref.get_by_id(starrer_3_pref.key.integer_id())
|
|
# self.assertEqual('starrer_3@example.com', updated_pref.email)
|
|
# self.assertTrue(updated_pref.bounced)
|
|
# self.assertFalse(updated_pref.notify_as_starrer)
|
|
|
|
expected_subject = "Mail to 'starrer_3@example.com' bounced"
|
|
mock_emailmessage_constructor.assert_called_once_with(
|
|
sender=self.sender, to=self.expected_to, subject=expected_subject,
|
|
body=mock.ANY)
|
|
mock_message = mock_emailmessage_constructor.return_value
|
|
mock_message.check_initialized.assert_called_once_with()
|
|
mock_message.send.assert_called()
|
|
|
|
@mock.patch('settings.SEND_EMAIL', True)
|
|
@mock.patch('google.appengine.api.mail.EmailMessage')
|
|
def test_receive__create_prefs(self, mock_emailmessage_constructor):
|
|
"""When we get a bounce, we create the UserPrefs for that user."""
|
|
# Note, no existing UserPref for starrer_4.
|
|
|
|
bounce_message = testing_config_py2.Blank(
|
|
original={'to': 'starrer_4@example.com',
|
|
'from': 'sender',
|
|
'subject': 'subject',
|
|
'text': 'body'})
|
|
|
|
sendemail.receive(bounce_message)
|
|
|
|
# TODO(jrobbins): Redo this testing after this aspect of the code is
|
|
# re-implemented.
|
|
# prefs_list = models.UserPref.get_prefs_for_emails(
|
|
# ['starrer_4@example.com'])
|
|
# updated_pref = prefs_list[0]
|
|
# self.assertEqual('starrer_4@example.com', updated_pref.email)
|
|
# self.assertTrue(updated_pref.bounced)
|
|
# self.assertTrue(updated_pref.notify_as_starrer)
|
|
|
|
expected_subject = "Mail to 'starrer_4@example.com' bounced"
|
|
mock_emailmessage_constructor.assert_called_once_with(
|
|
sender=self.sender, to=self.expected_to, subject=expected_subject,
|
|
body=mock.ANY)
|
|
mock_message = mock_emailmessage_constructor.return_value
|
|
mock_message.check_initialized.assert_called_once_with()
|
|
mock_message.send.assert_called()
|
|
|
|
|
|
class FunctionTest(unittest.TestCase):
|
|
|
|
def test_extract_addrs(self):
|
|
"""We can parse email From: lines."""
|
|
header_val = ''
|
|
self.assertEqual(
|
|
[], sendemail._extract_addrs(header_val))
|
|
|
|
header_val = 'J. Robbins <a@b.com>, c@d.com,\n Nick "Name" Dude <e@f.com>'
|
|
self.assertEqual(
|
|
['a@b.com', 'c@d.com', 'e@f.com'],
|
|
sendemail._extract_addrs(header_val))
|
|
|
|
header_val = ('hot: J. O\'Robbins <a@b.com>; '
|
|
'cool: "friendly" <e.g-h@i-j.k-L.com>')
|
|
self.assertEqual(
|
|
['a@b.com', 'e.g-h@i-j.k-L.com'],
|
|
sendemail._extract_addrs(header_val))
|
|
|
|
@mock.patch('google.appengine.api.urlfetch.fetch')
|
|
def test_call_py3_task_handler(self, mock_fetch):
|
|
"""Our py2 code can make a request to our py3 code."""
|
|
mock_response = testing_config_py2.Blank(
|
|
status_code=200, content='mock content')
|
|
mock_fetch.return_value = mock_response
|
|
|
|
actual = sendemail.call_py3_task_handler('/path', {'a': 1})
|
|
|
|
self.assertEqual(mock_response, actual)
|
|
mock_fetch.assert_called_once_with(
|
|
url='http://localhost:8080/path', method=urlfetch.POST,
|
|
payload=b'{"a": 1}', follow_redirects=False)
|
|
|
|
|
|
def MakeMessage(header_list, body):
|
|
"""Convenience function to make an email.message.Message."""
|
|
msg = email.message.Message()
|
|
for key, value in header_list:
|
|
msg[key] = value
|
|
msg.set_payload(body)
|
|
return msg
|
|
|
|
|
|
HEADER_LINES = [
|
|
('X-Original-From', 'user@example.com'),
|
|
('From', 'mailing-list@example.com'),
|
|
('To', settings.INBOUND_EMAIL_ADDR),
|
|
('Cc', 'other@chromium.org'),
|
|
('Subject', 'Intent to Ship: Featurename'),
|
|
('In-Reply-To', 'fake message id'),
|
|
]
|
|
|
|
|
|
class InboundEmailHandlerTest(unittest.TestCase):
|
|
|
|
def test_handle_incoming_mail__wrong_to_addr(self):
|
|
"""Reject the email if the app was not on the To: line."""
|
|
with sendemail.app.test_request_context('/_ah/mail/other@example.com'):
|
|
actual = sendemail.handle_incoming_mail('other@example.com')
|
|
|
|
self.assertEqual(
|
|
{'message': 'Wrong address'},
|
|
actual)
|
|
|
|
def test_handle_incoming_mail__too_big(self):
|
|
"""Reject the incoming email if it is huge."""
|
|
data = b'x' * sendemail.MAX_BODY_SIZE + b' is too big'
|
|
|
|
with sendemail.app.test_request_context(
|
|
'/_ah/mail/%s' % settings.INBOUND_EMAIL_ADDR, data=data):
|
|
actual = sendemail.handle_incoming_mail(settings.INBOUND_EMAIL_ADDR)
|
|
|
|
self.assertEqual(
|
|
{'message': 'Too big'},
|
|
actual)
|
|
|
|
@mock.patch('sendemail.get_incoming_message')
|
|
def test_handle_incoming_mail__junk_mail(self, mock_get_incoming_message):
|
|
"""Reject the incoming email if it has the wrong precedence header."""
|
|
for precedence in ['Bulk', 'Junk']:
|
|
msg = MakeMessage(
|
|
HEADER_LINES + [('Precedence', precedence)],
|
|
'I am on vacation!')
|
|
mock_get_incoming_message.return_value = msg
|
|
|
|
with sendemail.app.test_request_context(
|
|
'/_ah/mail/%s' % settings.INBOUND_EMAIL_ADDR):
|
|
actual = sendemail.handle_incoming_mail(settings.INBOUND_EMAIL_ADDR)
|
|
|
|
self.assertEqual(
|
|
{'message': 'Wrong precedence'},
|
|
actual)
|
|
|
|
@mock.patch('sendemail.get_incoming_message')
|
|
def test_handle_incoming_mail__unclear_from(self, mock_get_incoming_message):
|
|
"""Reject the incoming email if it we cannot parse the From: line."""
|
|
msg = MakeMessage([], 'Guess who this is')
|
|
mock_get_incoming_message.return_value = msg
|
|
|
|
with sendemail.app.test_request_context(
|
|
'/_ah/mail/%s' % settings.INBOUND_EMAIL_ADDR):
|
|
actual = sendemail.handle_incoming_mail(settings.INBOUND_EMAIL_ADDR)
|
|
|
|
self.assertEqual(
|
|
{'message': 'Missing From'},
|
|
actual)
|
|
|
|
@mock.patch('sendemail.call_py3_task_handler')
|
|
@mock.patch('sendemail.get_incoming_message')
|
|
def test_handle_incoming_mail__normal(
|
|
self, mock_get_incoming_message, mock_call_py3):
|
|
"""A valid incoming email is handed off to py3 code."""
|
|
msg = MakeMessage(HEADER_LINES, 'Please review')
|
|
mock_get_incoming_message.return_value = msg
|
|
mock_call_py3.return_value = testing_config_py2.Blank(status_code=200)
|
|
|
|
with sendemail.app.test_request_context(
|
|
'/_ah/mail/%s' % settings.INBOUND_EMAIL_ADDR):
|
|
actual = sendemail.handle_incoming_mail(settings.INBOUND_EMAIL_ADDR)
|
|
|
|
self.assertEqual({'message': 'Done'}, actual)
|
|
expected_task_dict = {
|
|
'to_addr': settings.INBOUND_EMAIL_ADDR,
|
|
'from_addr': 'user@example.com',
|
|
'subject': 'Intent to Ship: Featurename',
|
|
'in_reply_to': 'fake message id',
|
|
'body': 'Please review',
|
|
}
|
|
mock_call_py3.assert_called_once_with(
|
|
'/tasks/detect-intent', expected_task_dict)
|
|
|
|
@mock.patch('sendemail.call_py3_task_handler')
|
|
@mock.patch('sendemail.get_incoming_message')
|
|
def test_handle_incoming_mail__fallback_to_mailing_list(
|
|
self, mock_get_incoming_message, mock_call_py3):
|
|
"""If there is no personal X-Original-From, use the mailing list From:."""
|
|
msg = MakeMessage(HEADER_LINES, 'Please review')
|
|
del msg['X-Original-From']
|
|
mock_get_incoming_message.return_value = msg
|
|
mock_call_py3.return_value = testing_config_py2.Blank(status_code=200)
|
|
|
|
with sendemail.app.test_request_context(
|
|
'/_ah/mail/%s' % settings.INBOUND_EMAIL_ADDR):
|
|
actual = sendemail.handle_incoming_mail(settings.INBOUND_EMAIL_ADDR)
|
|
|
|
self.assertEqual({'message': 'Done'}, actual)
|
|
expected_task_dict = {
|
|
'to_addr': settings.INBOUND_EMAIL_ADDR,
|
|
'from_addr': 'mailing-list@example.com',
|
|
'subject': 'Intent to Ship: Featurename',
|
|
'in_reply_to': 'fake message id',
|
|
'body': 'Please review',
|
|
}
|
|
mock_call_py3.assert_called_once_with(
|
|
'/tasks/detect-intent', expected_task_dict)
|