Split app into py2 and py3 services. (#1460)
* Split app into py2 and py3 services. * fix typo * Also deploy app-py3 and upgrade skipfiles to .gcloudignore. * fix typo * addressed review comments
This commit is contained in:
Родитель
3f5c81ce22
Коммит
ecd96ba8dd
|
@ -0,0 +1,46 @@
|
|||
# This file specifies files that are *not* uploaded to Google Cloud Platform
|
||||
# using gcloud. It follows the same syntax as .gitignore, with the addition of
|
||||
# "#!include" directives (which insert the entries of the given .gitignore-style
|
||||
# file at that point).
|
||||
#
|
||||
# For more information, run:
|
||||
# $ gcloud topic gcloudignore
|
||||
#
|
||||
.gcloudignore
|
||||
# If you would like to upload your .git directory, .gitignore file or files
|
||||
# from your .gitignore file, remove the corresponding line
|
||||
# below:
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Python pycache:
|
||||
__pycache__/
|
||||
# Ignored by the build system
|
||||
/setup.cfg
|
||||
|
||||
*~
|
||||
*.pyc
|
||||
*.swp
|
||||
.sass-cache
|
||||
.vscode
|
||||
*oauth2.data
|
||||
.fcm_server_key
|
||||
*.iml
|
||||
.idea/*
|
||||
bulkloader-*
|
||||
cookie
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Source for generated files
|
||||
static/elements/
|
||||
static/sass/
|
||||
static/js-src/
|
||||
|
||||
# OS files
|
||||
*.DS_Store
|
||||
|
||||
# Test coverage tool
|
||||
.coverage
|
||||
htmlcov
|
|
@ -1,3 +1,6 @@
|
|||
# TODO(jrobbins): This GAE service will eventually be almost emptied
|
||||
# out as most handlers are moved over to app-p3.yaml.
|
||||
|
||||
runtime: python27
|
||||
threadsafe: false
|
||||
api_version: 1
|
||||
|
@ -43,13 +46,15 @@ handlers:
|
|||
script: internals.fetchmetrics.app
|
||||
# Any cron job must be harmless if it is called too often or with bad args
|
||||
|
||||
- url: /tasks/.*
|
||||
script: internals.notifier.app
|
||||
# Note: This handler must remain in this file because it requires GAE py2.
|
||||
- url: /tasks/outbound-email
|
||||
script: internals.sendemail.app
|
||||
# Header checks prevent raw access to this handler. Tasks have headers.
|
||||
|
||||
# Note: This handler must remain in this file because it requires GAE py2.
|
||||
- url: /_ah/bounce
|
||||
# TODO(jrobbins): phase out this handler when we redo email.
|
||||
script: internals.notifier.app
|
||||
script: internals.sendemail.app
|
||||
login: admin # Prevents raw access to this handler.
|
||||
|
||||
- url: /admin/blink.*
|
||||
|
@ -119,7 +124,6 @@ handlers:
|
|||
|
||||
|
||||
includes:
|
||||
- skip_files.yaml
|
||||
- env_vars.yaml
|
||||
|
||||
inbound_services:
|
|
@ -0,0 +1,9 @@
|
|||
# TODO(jrobbins): This file is not used yet.
|
||||
# It will contain lines to configure a GAE service running in py3.
|
||||
|
||||
runtime: python39
|
||||
service: app-py3
|
||||
|
||||
handlers:
|
||||
- url: /hello
|
||||
script: auto
|
|
@ -1,3 +1,17 @@
|
|||
dispatch:
|
||||
- url: "*/tasks/*"
|
||||
|
||||
# These will never match, because /_ah/ requests are not routed by dispatch.yaml.
|
||||
# https://cloud.google.com/appengine/docs/standard/python/reference/dispatch-yaml
|
||||
# So, these two handlers must remain in the default service (app-py2).
|
||||
# - url: "*/_ah/bounce"
|
||||
# service: app-py2
|
||||
# - url: "*/tasks/outbound-email"
|
||||
# service: app-py2
|
||||
|
||||
- url: "*/tasks/email-subscribers"
|
||||
service: notifier
|
||||
|
||||
- url: "*/hello"
|
||||
service: app-py3
|
||||
|
||||
# TODO(jrobbins): Gradually route most requests to service app-py3.
|
||||
|
|
|
@ -23,15 +23,12 @@ import logging
|
|||
import datetime
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
from framework import ramcache
|
||||
from google.cloud import ndb
|
||||
from google.appengine.api import mail
|
||||
import requests
|
||||
# from google.appengine.api import users
|
||||
from framework import users
|
||||
from google.appengine.ext.webapp.mail_handlers import BounceNotification
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import conditional_escape as escape
|
||||
|
@ -257,88 +254,21 @@ class FeatureChangeHandler(basehandlers.FlaskHandler):
|
|||
feature, is_update=is_update, changes=changes)
|
||||
logging.info('Processing %d email tasks', len(email_tasks))
|
||||
for one_email_dict in email_tasks:
|
||||
cloud_tasks_helpers.enqueue_task(
|
||||
'/tasks/outbound-email', one_email_dict)
|
||||
if settings.SEND_EMAIL:
|
||||
cloud_tasks_helpers.enqueue_task(
|
||||
'/tasks/outbound-email', one_email_dict)
|
||||
else:
|
||||
logging.info(
|
||||
'Would send the following email:\n'
|
||||
'To: %s\n'
|
||||
'Subject: %s\n'
|
||||
'Body:\n%s',
|
||||
one_email_dict['to'], one_email_dict['subject'],
|
||||
one_email_dict['html'])
|
||||
|
||||
return {'message': 'Done'}
|
||||
|
||||
|
||||
class OutboundEmailHandler(basehandlers.FlaskHandler):
|
||||
"""Task to send a notification email to one recipient."""
|
||||
|
||||
IS_INTERNAL_HANDLER = True
|
||||
|
||||
def process_post_data(self):
|
||||
self.require_task_header()
|
||||
|
||||
to = self.get_param('to', required=True)
|
||||
subject = self.get_param('subject', required=True)
|
||||
email_html = self.get_param('html', required=True)
|
||||
|
||||
if settings.SEND_ALL_EMAIL_TO:
|
||||
to_user, to_domain = to.split('@')
|
||||
to = settings.SEND_ALL_EMAIL_TO % {'user': to_user, 'domain': to_domain}
|
||||
|
||||
message = mail.EmailMessage(
|
||||
sender='Chromestatus <admin@%s.appspotmail.com>' % settings.APP_ID,
|
||||
to=to, subject=subject, html=email_html)
|
||||
message.check_initialized()
|
||||
|
||||
logging.info('Will send the following email:\n')
|
||||
logging.info('To: %s', message.to)
|
||||
logging.info('Subject: %s', message.subject)
|
||||
logging.info('Body:\n%s', message.html)
|
||||
if settings.SEND_EMAIL:
|
||||
message.send()
|
||||
logging.info('Email sent')
|
||||
else:
|
||||
logging.info('Email not sent because of settings.SEND_EMAIL')
|
||||
|
||||
return {'message': 'Done'}
|
||||
|
||||
|
||||
class BouncedEmailHandler(basehandlers.FlaskHandler):
|
||||
BAD_WRAP_RE = re.compile('=\r\n')
|
||||
BAD_EQ_RE = re.compile('=3D')
|
||||
IS_INTERNAL_HANDLER = True
|
||||
|
||||
"""Handler to notice when email to given user is bouncing."""
|
||||
# For docs on AppEngine's bounce email handling, see:
|
||||
# https://cloud.google.com/appengine/docs/python/mail/bounce
|
||||
# Source code is in file:
|
||||
# google_appengine/google/appengine/ext/webapp/mail_handlers.py
|
||||
def process_post_data(self):
|
||||
self.receive(BounceNotification(self.form))
|
||||
return {'message': 'Done'}
|
||||
|
||||
def receive(self, bounce_message):
|
||||
email_addr = bounce_message.original.get('to')
|
||||
subject = 'Mail to %r bounced' % email_addr
|
||||
logging.info(subject)
|
||||
pref_list = models.UserPref.get_prefs_for_emails([email_addr])
|
||||
user_pref = pref_list[0]
|
||||
user_pref.bounced = True
|
||||
user_pref.put()
|
||||
|
||||
# Escalate to someone who might do something about it, e.g.
|
||||
# find a new owner for a component.
|
||||
body = ('The following message bounced.\n'
|
||||
'=================\n'
|
||||
'From: {from}\n'
|
||||
'To: {to}\n'
|
||||
'Subject: {subject}\n\n'
|
||||
'{text}\n'.format(**bounce_message.original))
|
||||
logging.info(body)
|
||||
message = mail.EmailMessage(
|
||||
sender='Chromestatus <admin@%s.appspotmail.com>' % settings.APP_ID,
|
||||
to=settings.BOUNCE_ESCALATION_ADDR, subject=subject, body=body)
|
||||
message.check_initialized()
|
||||
if settings.SEND_EMAIL:
|
||||
message.send()
|
||||
|
||||
|
||||
app = basehandlers.FlaskApplication([
|
||||
('/tasks/email-subscribers', FeatureChangeHandler),
|
||||
('/tasks/outbound-email', OutboundEmailHandler),
|
||||
('/_ah/bounce', BouncedEmailHandler),
|
||||
], debug=settings.DEBUG)
|
||||
|
|
|
@ -23,7 +23,6 @@ import flask
|
|||
import mock
|
||||
import werkzeug.exceptions # Flask HTTP stuff.
|
||||
|
||||
from google.appengine.api import mail
|
||||
# TODO(jrobbins): phase out gae_users.
|
||||
from google.appengine.api import users as gae_users
|
||||
from framework import users
|
||||
|
@ -335,154 +334,3 @@ class FeatureStarTest(testing_config.CustomTestCase):
|
|||
self.assertItemsEqual(
|
||||
[app_user_1.email, app_user_2.email],
|
||||
[au.email for au in actual])
|
||||
|
||||
|
||||
class OutboundEmailHandlerTest(testing_config.CustomTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.handler = notifier.OutboundEmailHandler()
|
||||
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)
|
||||
|
||||
@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,
|
||||
}
|
||||
with notifier.app.test_request_context(self.request_path, json=params):
|
||||
actual_response = self.handler.process_post_data()
|
||||
|
||||
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)
|
||||
|
||||
@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 notifier.app.test_request_context(self.request_path, json=params):
|
||||
actual_response = self.handler.process_post_data()
|
||||
|
||||
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 notifier.app.test_request_context(self.request_path, json=params):
|
||||
actual_response = self.handler.process_post_data()
|
||||
|
||||
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(testing_config.CustomTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.handler = notifier.BouncedEmailHandler()
|
||||
self.sender = ('Chromestatus <admin@%s.appspotmail.com>' %
|
||||
settings.APP_ID)
|
||||
self.expected_to = settings.BOUNCE_ESCALATION_ADDR
|
||||
|
||||
@mock.patch('internals.notifier.BouncedEmailHandler.receive')
|
||||
def test_process_post_data(self, mock_receive):
|
||||
with notifier.app.test_request_context('/_ah/bounce'):
|
||||
actual_json = self.handler.process_post_data()
|
||||
|
||||
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.Blank(
|
||||
original={'to': 'starrer_3@example.com',
|
||||
'from': 'sender',
|
||||
'subject': 'subject',
|
||||
'text': 'body'})
|
||||
|
||||
self.handler.receive(bounce_message)
|
||||
|
||||
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.Blank(
|
||||
original={'to': 'starrer_4@example.com',
|
||||
'from': 'sender',
|
||||
'subject': 'subject',
|
||||
'text': 'body'})
|
||||
|
||||
self.handler.receive(bounce_message)
|
||||
|
||||
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()
|
|
@ -0,0 +1,109 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# 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.
|
||||
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from google.appengine.api import mail
|
||||
from google.appengine.ext.webapp.mail_handlers import BounceNotification
|
||||
|
||||
# TODO(jrobbins): For now, these files and files that they import must
|
||||
# remain compatible with both py2 and py3. We could split those files too.
|
||||
import settings
|
||||
from framework import basehandlers
|
||||
from internals import models
|
||||
|
||||
|
||||
class OutboundEmailHandler(basehandlers.FlaskHandler):
|
||||
"""Task to send a notification email to one recipient."""
|
||||
|
||||
IS_INTERNAL_HANDLER = True
|
||||
|
||||
def process_post_data(self):
|
||||
self.require_task_header()
|
||||
|
||||
to = self.get_param('to', required=True)
|
||||
subject = self.get_param('subject', required=True)
|
||||
email_html = self.get_param('html', required=True)
|
||||
|
||||
if settings.SEND_ALL_EMAIL_TO:
|
||||
to_user, to_domain = to.split('@')
|
||||
to = settings.SEND_ALL_EMAIL_TO % {'user': to_user, 'domain': to_domain}
|
||||
|
||||
message = mail.EmailMessage(
|
||||
sender='Chromestatus <admin@%s.appspotmail.com>' % settings.APP_ID,
|
||||
to=to, subject=subject, html=email_html)
|
||||
message.check_initialized()
|
||||
|
||||
logging.info('Will send the following email:\n')
|
||||
logging.info('To: %s', message.to)
|
||||
logging.info('Subject: %s', message.subject)
|
||||
logging.info('Body:\n%s', message.html)
|
||||
if settings.SEND_EMAIL:
|
||||
message.send()
|
||||
logging.info('Email sent')
|
||||
else:
|
||||
logging.info('Email not sent because of settings.SEND_EMAIL')
|
||||
|
||||
return {'message': 'Done'}
|
||||
|
||||
|
||||
class BouncedEmailHandler(basehandlers.FlaskHandler):
|
||||
BAD_WRAP_RE = re.compile('=\r\n')
|
||||
BAD_EQ_RE = re.compile('=3D')
|
||||
IS_INTERNAL_HANDLER = True
|
||||
|
||||
"""Handler to notice when email to given user is bouncing."""
|
||||
# For docs on AppEngine's bounce email handling, see:
|
||||
# https://cloud.google.com/appengine/docs/python/mail/bounce
|
||||
# Source code is in file:
|
||||
# google_appengine/google/appengine/ext/webapp/mail_handlers.py
|
||||
def process_post_data(self):
|
||||
self.receive(BounceNotification(self.form))
|
||||
return {'message': 'Done'}
|
||||
|
||||
def receive(self, bounce_message):
|
||||
email_addr = bounce_message.original.get('to')
|
||||
subject = 'Mail to %r bounced' % email_addr
|
||||
logging.info(subject)
|
||||
pref_list = models.UserPref.get_prefs_for_emails([email_addr])
|
||||
user_pref = pref_list[0]
|
||||
user_pref.bounced = True
|
||||
user_pref.put()
|
||||
|
||||
# Escalate to someone who might do something about it, e.g.
|
||||
# find a new owner for a component.
|
||||
body = ('The following message bounced.\n'
|
||||
'=================\n'
|
||||
'From: {from}\n'
|
||||
'To: {to}\n'
|
||||
'Subject: {subject}\n\n'
|
||||
'{text}\n'.format(**bounce_message.original))
|
||||
logging.info(body)
|
||||
message = mail.EmailMessage(
|
||||
sender='Chromestatus <admin@%s.appspotmail.com>' % settings.APP_ID,
|
||||
to=settings.BOUNCE_ESCALATION_ADDR, subject=subject, body=body)
|
||||
message.check_initialized()
|
||||
if settings.SEND_EMAIL:
|
||||
message.send()
|
||||
|
||||
|
||||
app = basehandlers.FlaskApplication([
|
||||
('/tasks/outbound-email', OutboundEmailHandler),
|
||||
('/_ah/bounce', BouncedEmailHandler),
|
||||
], debug=settings.DEBUG)
|
|
@ -0,0 +1,180 @@
|
|||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
# 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 collections
|
||||
import json
|
||||
import testing_config # Must be imported before the module under test.
|
||||
|
||||
import flask
|
||||
import mock
|
||||
import werkzeug.exceptions # Flask HTTP stuff.
|
||||
|
||||
from google.appengine.api import mail
|
||||
|
||||
import settings
|
||||
from internals import models
|
||||
from internals import sendemail
|
||||
|
||||
class OutboundEmailHandlerTest(testing_config.CustomTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.handler = sendemail.OutboundEmailHandler()
|
||||
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)
|
||||
|
||||
@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,
|
||||
}
|
||||
with sendemail.app.test_request_context(self.request_path, json=params):
|
||||
actual_response = self.handler.process_post_data()
|
||||
|
||||
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)
|
||||
|
||||
@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 = self.handler.process_post_data()
|
||||
|
||||
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 = self.handler.process_post_data()
|
||||
|
||||
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(testing_config.CustomTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.handler = sendemail.BouncedEmailHandler()
|
||||
self.sender = ('Chromestatus <admin@%s.appspotmail.com>' %
|
||||
settings.APP_ID)
|
||||
self.expected_to = settings.BOUNCE_ESCALATION_ADDR
|
||||
|
||||
@mock.patch('internals.sendemail.BouncedEmailHandler.receive')
|
||||
def test_process_post_data(self, mock_receive):
|
||||
with sendemail.app.test_request_context('/_ah/bounce'):
|
||||
actual_json = self.handler.process_post_data()
|
||||
|
||||
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.Blank(
|
||||
original={'to': 'starrer_3@example.com',
|
||||
'from': 'sender',
|
||||
'subject': 'subject',
|
||||
'text': 'body'})
|
||||
|
||||
self.handler.receive(bounce_message)
|
||||
|
||||
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.Blank(
|
||||
original={'to': 'starrer_4@example.com',
|
||||
'from': 'sender',
|
||||
'subject': 'subject',
|
||||
'text': 'body'})
|
||||
|
||||
self.handler.receive(bounce_message)
|
||||
|
||||
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()
|
|
@ -0,0 +1,21 @@
|
|||
from flask import Flask
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# If `entrypoint` is not defined in app.yaml, App Engine will look for an app
|
||||
# called `app` in `main.py`.
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route('/hello')
|
||||
def hello():
|
||||
"""Return a friendly HTTP greeting."""
|
||||
logging.info('In hello() !')
|
||||
return 'Hello python 3!'
|
||||
|
||||
if __name__ == '__main__':
|
||||
# This is used when running locally only. When deploying to Google App
|
||||
# Engine, a webserver process such as Gunicorn will serve the app. This
|
||||
# can be configured by adding an `entrypoint` to app.yaml.
|
||||
app.run(host='127.0.0.1', port=8080, debug=True)
|
|
@ -9,7 +9,6 @@ handlers:
|
|||
# Header checks prevent raw access to this handler. Tasks have headers.
|
||||
|
||||
includes:
|
||||
- skip_files.yaml
|
||||
- env_vars.yaml
|
||||
|
||||
libraries:
|
||||
|
|
|
@ -29,5 +29,8 @@ gcloud app deploy \
|
|||
--project $appName \
|
||||
--version $deployVersion \
|
||||
--no-promote \
|
||||
$BASEDIR/../app.yaml $BASEDIR/../notifier.yaml \
|
||||
$BASEDIR/../app-py2.yaml $BASEDIR/../notifier.yaml \
|
||||
$BASEDIR/../app-py3.yaml \
|
||||
$BASEDIR/../dispatch.yaml $BASEDIR/../cron.yaml
|
||||
|
||||
# TODO(jrobbins): Add app_py3.yaml when it has some contents.
|
||||
|
|
|
@ -11,4 +11,11 @@ readonly BASEDIR=$(dirname $BASH_SOURCE)
|
|||
dev_appserver.py -A cr-status --enable_console=1 \
|
||||
--support_datastore_emulator=1 --datastore_emulator_port=15606 \
|
||||
--env_var DATASTORE_EMULATOR_HOST='localhost:15606' \
|
||||
$BASEDIR/../app.yaml $BASEDIR/../notifier.yaml
|
||||
$BASEDIR/../dispatch.yaml \
|
||||
$BASEDIR/../app-py2.yaml $BASEDIR/../notifier.yaml \
|
||||
$BASEDIR/../app-py3.yaml
|
||||
|
||||
# TODO(jrobbins): Add app-py3.yaml when it has some contents. At some point in
|
||||
# the future when we cannot run the app-py2 service locally, tasks for
|
||||
# outbound-email would simply queue up forever. However, we don't even create
|
||||
# those tasks in dev mode.
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
skip_files:
|
||||
- ^(.*/)?app\.yaml
|
||||
- ^(.*/)?app\.yml
|
||||
- ^(.*/)?index\.yaml
|
||||
- ^(.*/)?index\.yml
|
||||
- ^(.*/)?#.*#
|
||||
- ^(.*/)?.*~
|
||||
- ^(.*/)?.*\.py[co]
|
||||
- ^(.*/)?.*/RCS/.*
|
||||
- ^(.*/)?\..*
|
||||
- ^(.*/)?.*\.csv$
|
||||
- ^(.*/)?.*\.psd$
|
||||
- ^(.*/)?.*\.sql[3]$
|
||||
- ^(.*/)?.*\.sh$
|
||||
- ^(.*/)?.*\.scss$
|
||||
- ^(.*/)?node_modules
|
||||
- ^(.*/)?tests/
|
||||
- ^(.*/)?.[LICENSE|PATENTS|AUTHORS|CONTRIBUTING|COPYING](\.md)?
|
||||
- ^(.*/)?.*\.md$
|
||||
|
||||
- ^http2push/example
|
||||
- ^http2push/site
|
||||
- ^static/sass
|
Загрузка…
Ссылка в новой задаче