Merge branch 'main' into avm99963-monorail

Merged commit 3779da353b36d43cf778e7d4f468097714dd4540

GitOrigin-RevId: 6451a5c6b75afb0fd1f37b3f14521148d0722ea8
diff --git a/features/banspammer.py b/features/banspammer.py
index a6be311..fd28045 100644
--- a/features/banspammer.py
+++ b/features/banspammer.py
@@ -8,7 +8,6 @@
 from __future__ import division
 from __future__ import absolute_import
 
-import logging
 import json
 import time
 
@@ -17,10 +16,10 @@
 from framework import framework_helpers
 from framework import permissions
 from framework import jsonfeed
-from framework import servlet
 from framework import urls
 
-class BanSpammer(servlet.Servlet):
+
+class BanSpammer(flaskservlet.FlaskServlet):
   """Ban a user and mark their content as spam"""
 
   def AssertBasePermission(self, mr):
@@ -57,12 +56,11 @@
         mr, mr.viewed_user_auth.user_view.profile_url, include_project=False,
         saved=1, ts=int(time.time()))
 
-  # def PostBanSpammerPage(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostBanSpammerPage(self, **kwargs):
+    return self.handler(**kwargs)
 
 
-# when convert to flask switch jsonfeed.FlaskInternalTask
-class BanSpammerTask(jsonfeed.InternalTask):
+class BanSpammerTask(jsonfeed.FlaskInternalTask):
   """This task will update all of the comments and issues created by the
      target user with is_spam=True, and also add a manual verdict attached
      to the user who originated the ban request. This is a potentially long
@@ -96,18 +94,10 @@
             self.services.issue, self.services.user, comment.id,
             reporter_id, is_spammer)
 
-    # remove the self.response.body when convert to flask
-    self.response.body = json.dumps({
-      'comments': len(comments),
-      'issues': len(issues),
+    return json.dumps({
+        'comments': len(comments),
+        'issues': len(issues),
     })
-  # return json.dumps({
-  #     'comments': len(comments),
-  #     'issues': len(issues),
-  #   })
 
-  # def GetBanSpammer(self, **kwargs):
-  #   return self.handler(**kwargs)
-
-  # def PostBanSpammer(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostBanSpammer(self, **kwargs):
+    return self.handler(**kwargs)
diff --git a/features/dateaction.py b/features/dateaction.py
index 169f582..0cb3987 100644
--- a/features/dateaction.py
+++ b/features/dateaction.py
@@ -40,8 +40,7 @@
 TEMPLATE_PATH = framework_constants.TEMPLATE_PATH
 
 
-# TODO: change to FlaskInternalTask when convert to Flask
-class DateActionCron(jsonfeed.InternalTask):
+class DateActionCron(jsonfeed.FlaskInternalTask):
   """Find and process issues with date-type values that arrived today."""
 
   def HandleRequest(self, mr):
@@ -86,11 +85,8 @@
         urls.ISSUE_DATE_ACTION_TASK + '.do', params)
     cloud_tasks_helpers.create_task(task)
 
-  # def GetDateActionCron(self, **kwargs):
-  #   return self.handler(**kwargs)
-
-  # def PostDateActionCron(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetDateActionCron(self, **kwargs):
+    return self.handler(**kwargs)
 
 
 def _GetTimestampRange(now):
@@ -234,8 +230,5 @@
     date_str = timestr.TimestampToDateWidgetStr(timestamp)
     return 'The %s date has arrived: %s' % (field.field_name, date_str)
 
-  # def GetIssueDateActionTask(self, **kwargs):
-  #   return self.handler(**kwargs)
-
-  # def PostIssueDateActionTask(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostIssueDateActionTask(self, **kwargs):
+    return self.handler(**kwargs)
diff --git a/features/filterrules.py b/features/filterrules.py
index 724d7e2..119c1b3 100644
--- a/features/filterrules.py
+++ b/features/filterrules.py
@@ -15,8 +15,7 @@
 from tracker import tracker_constants
 
 
-# TODO: change to FlaskInternalTask when convert to flask
-class RecomputeDerivedFieldsTask(jsonfeed.InternalTask):
+class RecomputeDerivedFieldsTask(jsonfeed.FlaskInternalTask):
   """JSON servlet that recomputes derived fields on a batch of issues."""
 
   def HandleRequest(self, mr):
@@ -36,15 +35,11 @@
         'success': True,
         }
 
-  # def GetRecomputeDerivedFieldsTask(self, **kwargs):
-  #   return self.handler(**kwargs)
-
-  # def PostRecomputeDerivedFieldsTask(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostRecomputeDerivedFieldsTask(self, **kwargs):
+    return self.handler(**kwargs)
 
 
-# TODO: change to FlaskInternalTask when convert to Flask
-class ReindexQueueCron(jsonfeed.InternalTask):
+class ReindexQueueCron(jsonfeed.FlaskInternalTask):
   """JSON servlet that reindexes some issues each minute, as needed."""
 
   def HandleRequest(self, mr):
@@ -57,8 +52,5 @@
         'num_reindexed': num_reindexed,
         }
 
-  # def GetReindexQueueCron(self, **kwargs):
-  #   return self.handler(**kwargs)
-
-  # def PostReindexQueueCron(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetReindexQueueCron(self, **kwargs):
+    return self.handler(**kwargs)
diff --git a/features/hotlistcreate.py b/features/hotlistcreate.py
index fa8946f..6913c19 100644
--- a/features/hotlistcreate.py
+++ b/features/hotlistcreate.py
@@ -30,7 +30,7 @@
 _MSG_INVALID_MEMBERS_INPUT = 'One or more editor emails is not valid.'
 
 
-class HotlistCreate(servlet.Servlet):
+class HotlistCreate(flaskservlet.FlaskServlet):
   """HotlistCreate shows a simple page with a form to create a hotlist."""
 
   _PAGE_TEMPLATE = 'features/hotlist-create-page.ezt'
@@ -114,8 +114,8 @@
               mr.cnxn, hotlist, self.services.user),
           include_project=False)
 
-  # def GetCreateHotlist(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetCreateHotlist(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostCreateHotlist(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostCreateHotlist(self, **kwargs):
+    return self.handler(**kwargs)
diff --git a/features/inboundemail.py b/features/inboundemail.py
index d9c36d3..d9b2c37 100644
--- a/features/inboundemail.py
+++ b/features/inboundemail.py
@@ -13,13 +13,18 @@
 import os
 import re
 import time
+import six
 from six.moves import urllib
 
+import flask
 import ezt
 
-from google.appengine.ext.webapp.mail_handlers import BounceNotificationHandler
+from google.appengine.api import mail
+if six.PY2:
+  from google.appengine.ext.webapp.mail_handlers import BounceNotification
+else:
+  from google.appengine.api.mail import BounceNotification
 
-import webapp2
 
 import settings
 from features import alert2issue
@@ -50,36 +55,37 @@
     }
 
 
-class InboundEmail(webapp2.RequestHandler):
+class InboundEmail(object):
   """Servlet to handle inbound email messages."""
 
-  def __init__(self, request, response, services=None, *args, **kwargs):
-    super(InboundEmail, self).__init__(request, response, *args, **kwargs)
-    self.services = services or self.app.config.get('services')
+  def __init__(self, services=None):
+    self.services = services or flask.current_app.config['services']
     self._templates = {}
+    self.request = flask.request
     for name, template_path in MSG_TEMPLATES.items():
       self._templates[name] = template_helpers.MonorailTemplate(
           TEMPLATE_PATH_BASE + template_path,
           compress_whitespace=False, base_format=ezt.FORMAT_RAW)
 
-  # def HandleInboundEmail(self, project_addr=None):
-  #   if self.request.method == 'POST':
-  #     self.post(project_addr)
-  #   elif self.request.method == 'GET':
-  #     self.get(project_addr)
+  def HandleInboundEmail(self, project_addr=None):
+    if self.request.method == 'POST':
+      self.post(project_addr)
+    elif self.request.method == 'GET':
+      self.get(project_addr)
+    return ''
 
   def get(self, project_addr=None):
     logging.info('\n\n\nGET for InboundEmail and project_addr is %r',
                  project_addr)
     self.Handler(
-        mail.InboundEmailMessage(self.request.body),
+        mail.InboundEmailMessage(self.request.get_data()),
         urllib.parse.unquote(project_addr))
 
   def post(self, project_addr=None):
     logging.info('\n\n\nPOST for InboundEmail and project_addr is %r',
                  project_addr)
     self.Handler(
-        mail.InboundEmailMessage(self.request.body),
+        mail.InboundEmailMessage(self.request.get_data()),
         urllib.parse.unquote(project_addr))
 
   def Handler(self, inbound_email_message, project_addr):
@@ -293,29 +299,36 @@
 BAD_EQ_RE = re.compile('=3D')
 
 
-class BouncedEmail(BounceNotificationHandler):
+class BouncedEmail(object):
   """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
+  # For docs on AppEngine's bounce email see:
+  # https://cloud.google.com/appengine/docs/standard/python3/reference
+  # /services/bundled/google/appengine/api/mail/BounceNotification
 
-  def post(self):
+  def __init__(self, services=None):
+    self.services = services or flask.current_app.config['services']
+
+  def postBouncedEmail(self):
     try:
-      super(BouncedEmail, self).post()
+      # Context: https://crbug.com/monorail/11083
+      bounce_message = BounceNotification(flask.request.form)
+      self.receive(bounce_message)
     except AttributeError:
-      # Work-around for
-      # https://code.google.com/p/googleappengine/issues/detail?id=13512
-      raw_message = self.request.POST.get('raw-message')
+      # Context: https://crbug.com/monorail/2105
+      raw_message = flask.request.form.get('raw-message')
       logging.info('raw_message %r', raw_message)
       raw_message = BAD_WRAP_RE.sub('', raw_message)
       raw_message = BAD_EQ_RE.sub('=', raw_message)
       logging.info('fixed raw_message %r', raw_message)
       mime_message = email.message_from_string(raw_message)
       logging.info('get_payload gives %r', mime_message.get_payload())
-      self.request.POST['raw-message'] = mime_message
-      super(BouncedEmail, self).post()  # Retry with mime_message
+      new_form_dict = flask.request.form.copy()
+      new_form_dict['raw-message'] = mime_message
+      # Retry with mime_message
+      bounce_message = BounceNotification(new_form_dict)
+      self.receive(bounce_message)
+    return ''
 
 
   def receive(self, bounce_message):
@@ -335,8 +348,7 @@
         logging.info(
             'bounce message original headers: %r', original_message.items())
 
-    app_config = webapp2.WSGIApplication.app.config
-    services = app_config['services']
+    services = self.services
     cnxn = sql.MonorailConnection()
 
     try:
@@ -346,6 +358,6 @@
       services.user.UpdateUser(cnxn, user_id, user)
     except exceptions.NoSuchUserException:
       logging.info('User %r not found, ignoring', email_addr)
-      logging.info('Received bounce post ... [%s]', self.request)
+      logging.info('Received bounce post ... [%s]', flask.request)
       logging.info('Bounce original: %s', bounce_message.original)
       logging.info('Bounce notification: %s', bounce_message.notification)
diff --git a/features/notify.py b/features/notify.py
index 425041e..230cbf5 100644
--- a/features/notify.py
+++ b/features/notify.py
@@ -219,11 +219,8 @@
 
     return email_tasks
 
-  # def GetNotifyIssueChangeTask(self, **kwargs):
-  #   return self.handler(**kwargs)
-
-  # def PostNotifyIssueChangeTask(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostNotifyIssueChangeTask(self, **kwargs):
+    return self.handler(**kwargs)
 
 
 class NotifyBlockingChangeTask(notify_helpers.NotifyTaskBase):
@@ -356,11 +353,8 @@
 
     return one_issue_email_tasks
 
-  # def GetNotifyBlockingChangeTask(self, **kwargs):
-  #   return self.handler(**kwargs)
-
-  # def PostNotifyBlockingChangeTask(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostNotifyBlockingChangeTask(self, **kwargs):
+    return self.handler(**kwargs)
 
 
 class NotifyBulkChangeTask(notify_helpers.NotifyTaskBase):
@@ -724,11 +718,8 @@
 
     return subject, body
 
-  # def GetNotifyBulkChangeTask(self, **kwargs):
-  #   return self.handler(**kwargs)
-
-  # def PostNotifyBulkChangeTask(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostNotifyBulkChangeTask(self, **kwargs):
+    return self.handler(**kwargs)
 
 
 # For now, this class will not be used to send approval comment notifications
@@ -919,11 +910,8 @@
 
     return list(set(recipient_ids))
 
-  # def GetNotifyApprovalChangeTask(self, **kwargs):
-  #   return self.handler(**kwargs)
-
-  # def PostNotifyApprovalChangeTask(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostNotifyApprovalChangeTask(self, **kwargs):
+    return self.handler(**kwargs)
 
 
 class NotifyRulesDeletedTask(notify_helpers.NotifyTaskBase):
@@ -991,15 +979,11 @@
 
     return email_tasks
 
-  # def GetNotifyRulesDeletedTask(self, **kwargs):
-  #   return self.handler(**kwargs)
-
-  # def PostNotifyRulesDeletedTask(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostNotifyRulesDeletedTask(self, **kwargs):
+    return self.handler(**kwargs)
 
 
-# TODO: change to FlaskInternalTask when convert to flask
-class OutboundEmailTask(jsonfeed.InternalTask):
+class OutboundEmailTask(jsonfeed.FlaskInternalTask):
   """JSON servlet that sends one email.
 
   Handles tasks enqueued from notify_helpers._EnqueueOutboundEmail.
@@ -1018,9 +1002,9 @@
     # To avoid urlencoding the email body, the most salient parameters to this
     # method are passed as a json-encoded POST body.
     try:
-      email_params = json.loads(self.request.body)
+      email_params = json.loads(self.request.get_data(as_text=True))
     except ValueError:
-      logging.error(self.request.body)
+      logging.error(self.request.get_data(as_text=True))
       raise
     # If running on a GAFYD domain, you must define an app alias on the
     # Application Settings admin web page.
@@ -1085,8 +1069,5 @@
         sender=sender, to=to, subject=subject, body=body, html_body=html_body,
         reply_to=reply_to, references=references)
 
-  # def GetOutboundEmailTask(self, **kwargs):
-  #   return self.handler(**kwargs)
-
-  # def PostOutboundEmailTask(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostOutboundEmailTask(self, **kwargs):
+    return self.handler(**kwargs)
diff --git a/features/notify_helpers.py b/features/notify_helpers.py
index 5f77307..f22ed0e 100644
--- a/features/notify_helpers.py
+++ b/features/notify_helpers.py
@@ -123,8 +123,7 @@
   return notified
 
 
-# TODO: change to FlaskInternalTask when convert to flask
-class NotifyTaskBase(jsonfeed.InternalTask):
+class NotifyTaskBase(jsonfeed.FlaskInternalTask):
   """Abstract base class for notification task handler."""
 
   _EMAIL_TEMPLATE = None  # Subclasses must override this.
diff --git a/features/pubsub.py b/features/pubsub.py
index 86bd3ba..c7c28d9 100644
--- a/features/pubsub.py
+++ b/features/pubsub.py
@@ -26,8 +26,7 @@
 from framework import jsonfeed
 
 
-# TODO: change to FlaskInternalTask when convert to flask
-class PublishPubsubIssueChangeTask(jsonfeed.InternalTask):
+class PublishPubsubIssueChangeTask(jsonfeed.FlaskInternalTask):
   """JSON servlet that pushes issue update messages onto a pub/sub topic."""
 
   def HandleRequest(self, mr):
@@ -71,11 +70,8 @@
 
     return {}
 
-  # def GetPublishPubsubIssueChangeTask(self, **kwargs):
-  #   return self.handler(**kwargs)
-
-  # def PostPublishPubsubIssueChangeTask(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostPublishPubsubIssueChangeTask(self, **kwargs):
+    return self.handler(**kwargs)
 
 
 def set_up_pubsub_api():
diff --git a/features/test/activities_test.py b/features/test/activities_test.py
index 4eae1ab..c17eb4b 100644
--- a/features/test/activities_test.py
+++ b/features/test/activities_test.py
@@ -10,7 +10,10 @@
 
 import unittest
 
-import mox
+try:
+  from mox3 import mox
+except ImportError:
+  import mox
 
 from features import activities
 from framework import framework_views
diff --git a/features/test/alert2issue_test.py b/features/test/alert2issue_test.py
index 3b1b6d1..2046b5b 100644
--- a/features/test/alert2issue_test.py
+++ b/features/test/alert2issue_test.py
@@ -11,7 +11,10 @@
 import email
 import unittest
 from mock import patch
-import mox
+try:
+  from mox3 import mox
+except ImportError:
+  import mox
 from parameterized import parameterized
 
 from features import alert2issue
diff --git a/features/test/banspammer_test.py b/features/test/banspammer_test.py
index edf7aba..e12c506 100644
--- a/features/test/banspammer_test.py
+++ b/features/test/banspammer_test.py
@@ -10,10 +10,8 @@
 
 import json
 import mock
-import os
 import unittest
 from six.moves import urllib
-import webapp2
 
 import settings
 from features import banspammer
@@ -35,7 +33,7 @@
         project=fake.ProjectService(),
         spam=fake.SpamService(),
         user=fake.UserService())
-    self.servlet = banspammer.BanSpammer('req', 'res', services=self.services)
+    self.servlet = banspammer.BanSpammer(services=self.services)
 
   @mock.patch('framework.cloud_tasks_helpers._get_client')
   def testProcessFormData_noPermission(self, get_client_mock):
@@ -92,17 +90,15 @@
     self.services = service_manager.Services(
         issue=fake.IssueService(),
         spam=fake.SpamService())
-    self.res = webapp2.Response()
-    self.servlet = banspammer.BanSpammerTask('req', self.res,
-        services=self.services)
+    self.servlet = banspammer.BanSpammerTask(services=self.services)
 
   def testProcessFormData_okNoIssues(self):
     mr = testing_helpers.MakeMonorailRequest(
         path=urls.BAN_SPAMMER_TASK + '.do', method='POST',
         params={'spammer_id': 111, 'reporter_id': 222})
 
-    self.servlet.HandleRequest(mr)
-    self.assertEqual(self.res.body, json.dumps({'comments': 0, 'issues': 0}))
+    res = self.servlet.HandleRequest(mr)
+    self.assertEqual(res, json.dumps({'comments': 0, 'issues': 0}))
 
   def testProcessFormData_okSomeIssues(self):
     mr = testing_helpers.MakeMonorailRequest(
@@ -114,8 +110,8 @@
           1, i, 'issue_summary', 'New', 111, project_name='project-name')
       self.servlet.services.issue.TestAddIssue(issue)
 
-    self.servlet.HandleRequest(mr)
-    self.assertEqual(self.res.body, json.dumps({'comments': 0, 'issues': 10}))
+    res = self.servlet.HandleRequest(mr)
+    self.assertEqual(res, json.dumps({'comments': 0, 'issues': 10}))
 
   def testProcessFormData_okSomeCommentsAndIssues(self):
     mr = testing_helpers.MakeMonorailRequest(
@@ -137,5 +133,5 @@
         comment.user_id = 111
         comment.issue_id = issue.issue_id
         self.servlet.services.issue.TestAddComment(comment, issue.local_id)
-    self.servlet.HandleRequest(mr)
-    self.assertEqual(self.res.body, json.dumps({'comments': 50, 'issues': 10}))
+    res = self.servlet.HandleRequest(mr)
+    self.assertEqual(res, json.dumps({'comments': 50, 'issues': 10}))
diff --git a/features/test/dateaction_test.py b/features/test/dateaction_test.py
index 09e5c5c..8ca5bc3 100644
--- a/features/test/dateaction_test.py
+++ b/features/test/dateaction_test.py
@@ -36,8 +36,7 @@
     self.services = service_manager.Services(
         user=fake.UserService(),
         issue=fake.IssueService())
-    self.servlet = dateaction.DateActionCron(
-        'req', 'res', services=self.services)
+    self.servlet = dateaction.DateActionCron(services=self.services)
     self.TIMESTAMP_MIN = (
         NOW // framework_constants.SECS_PER_DAY *
         framework_constants.SECS_PER_DAY)
@@ -128,8 +127,7 @@
         project=fake.ProjectService(),
         config=fake.ConfigService(),
         issue_star=fake.IssueStarService())
-    self.servlet = dateaction.IssueDateActionTask(
-        'req', 'res', services=self.services)
+    self.servlet = dateaction.IssueDateActionTask(services=self.services)
 
     self.config = self.services.config.GetProjectConfig('cnxn', 789)
     self.config.field_defs = [
diff --git a/features/test/hotlistcreate_test.py b/features/test/hotlistcreate_test.py
index 8cf0012..e6cda4b 100644
--- a/features/test/hotlistcreate_test.py
+++ b/features/test/hotlistcreate_test.py
@@ -8,7 +8,10 @@
 from __future__ import division
 from __future__ import absolute_import
 
-import mox
+try:
+  from mox3 import mox
+except ImportError:
+  import mox
 import unittest
 
 import settings
@@ -30,8 +33,7 @@
                                         user=fake.UserService(),
                                              issue=fake.IssueService(),
                                              features=fake.FeaturesService())
-    self.servlet = hotlistcreate.HotlistCreate('req', 'res',
-                                               services=self.services)
+    self.servlet = hotlistcreate.HotlistCreate(services=self.services)
     self.mox = mox.Mox()
 
   def tearDown(self):
diff --git a/features/test/hotlistdetails_test.py b/features/test/hotlistdetails_test.py
index 9a9e53f..561199c 100644
--- a/features/test/hotlistdetails_test.py
+++ b/features/test/hotlistdetails_test.py
@@ -9,7 +9,10 @@
 from __future__ import absolute_import
 
 import logging
-import mox
+try:
+  from mox3 import mox
+except ImportError:
+  import mox
 import unittest
 import mock
 
diff --git a/features/test/hotlistissues_test.py b/features/test/hotlistissues_test.py
index 49c3270..265c9d1 100644
--- a/features/test/hotlistissues_test.py
+++ b/features/test/hotlistissues_test.py
@@ -8,7 +8,10 @@
 from __future__ import division
 from __future__ import absolute_import
 
-import mox
+try:
+  from mox3 import mox
+except ImportError:
+  import mox
 import mock
 import unittest
 import time
diff --git a/features/test/hotlistpeople_test.py b/features/test/hotlistpeople_test.py
index 74beec3..3ee7925 100644
--- a/features/test/hotlistpeople_test.py
+++ b/features/test/hotlistpeople_test.py
@@ -8,7 +8,10 @@
 from __future__ import division
 from __future__ import absolute_import
 
-import mox
+try:
+  from mox3 import mox
+except ImportError:
+  import mox
 import unittest
 import logging
 
diff --git a/features/test/inboundemail_test.py b/features/test/inboundemail_test.py
index 0eaa281..de05749 100644
--- a/features/test/inboundemail_test.py
+++ b/features/test/inboundemail_test.py
@@ -9,14 +9,14 @@
 from __future__ import absolute_import
 
 import unittest
-import webapp2
-from mock import patch
 
-import mox
+try:
+  from mox3 import mox
+except ImportError:
+  import mox
 import time
 
 from google.appengine.api import mail
-from google.appengine.ext.webapp.mail_handlers import BounceNotificationHandler
 
 import settings
 from businesslogic import work_env
@@ -58,8 +58,7 @@
     self.msg = testing_helpers.MakeMessage(
         testing_helpers.HEADER_LINES, 'awesome!')
 
-    request, _ = testing_helpers.GetRequestObjects()
-    self.inbound = inboundemail.InboundEmail(request, None, self.services)
+    self.inbound = inboundemail.InboundEmail(self.services)
     self.mox = mox.Mox()
 
   def tearDown(self):
@@ -348,10 +347,7 @@
         user=fake.UserService())
     self.user = self.services.user.TestAddUser('user@example.com', 111)
 
-    app = webapp2.WSGIApplication(config={'services': self.services})
-    app.set_globals(app=app)
-
-    self.servlet = inboundemail.BouncedEmail()
+    self.servlet = inboundemail.BouncedEmail(self.services)
     self.mox = mox.Mox()
 
   def tearDown(self):
@@ -369,8 +365,6 @@
 
   def testReceive_NoSuchUser(self):
     """When not found, log it and ignore without creating a user record."""
-    self.servlet.request = webapp2.Request.blank(
-        '/', POST={'raw-message': 'this is an email message'})
     bounce_message = testing_helpers.Blank(
         original={'to': 'nope@example.com'},
         notification='notification')
diff --git a/features/test/notify_test.py b/features/test/notify_test.py
index 9ddcce7..e73488d 100644
--- a/features/test/notify_test.py
+++ b/features/test/notify_test.py
@@ -11,7 +11,7 @@
 import json
 import mock
 import unittest
-import webapp2
+import flask
 
 from google.appengine.ext import testbed
 
@@ -63,7 +63,13 @@
     self.orig_sign_attachment_id = attachment_helpers.SignAttachmentID
     attachment_helpers.SignAttachmentID = (
         lambda aid: 'signed_%d' % aid)
-
+    self.servlet = notify.OutboundEmailTask(services=self.services)
+    self.app = flask.Flask('test_app')
+    self.app.config['TESTING'] = True
+    self.app.add_url_rule(
+        '/_task/outboundEmail.do',
+        view_func=self.servlet.PostOutboundEmailTask,
+        methods=['POST'])
     self.testbed = testbed.Testbed()
     self.testbed.activate()
     self.testbed.init_memcache_stub()
@@ -89,8 +95,7 @@
                        result['params']['issue_ids'])
 
   def testNotifyIssueChangeTask_Normal(self):
-    task = notify.NotifyIssueChangeTask(
-        request=None, response=None, services=self.services)
+    task = notify.NotifyIssueChangeTask(services=self.services)
     params = {'send_email': 1, 'issue_id': 12345001, 'seq': 0,
               'commenter_id': 2}
     mr = testing_helpers.MakeMonorailRequest(
@@ -107,8 +112,7 @@
         project_id=12345, local_id=1, owner_id=1, reporter_id=1,
         is_spam=True)
     self.services.issue.TestAddIssue(issue)
-    task = notify.NotifyIssueChangeTask(
-        request=None, response=None, services=self.services)
+    task = notify.NotifyIssueChangeTask(services=self.services)
     params = {'send_email': 0, 'issue_id': issue.issue_id, 'seq': 0,
               'commenter_id': 2}
     mr = testing_helpers.MakeMonorailRequest(
@@ -124,8 +128,7 @@
     issue2 = MakeTestIssue(
         project_id=12345, local_id=2, owner_id=2, reporter_id=1)
     self.services.issue.TestAddIssue(issue2)
-    task = notify.NotifyBlockingChangeTask(
-        request=None, response=None, services=self.services)
+    task = notify.NotifyBlockingChangeTask(services=self.services)
     params = {
         'send_email': 1, 'issue_id': issue2.issue_id, 'seq': 0,
         'delta_blocker_iids': self.issue1.issue_id, 'commenter_id': 1,
@@ -143,8 +146,7 @@
         project_id=12345, local_id=2, owner_id=2, reporter_id=1,
         is_spam=True)
     self.services.issue.TestAddIssue(issue2)
-    task = notify.NotifyBlockingChangeTask(
-        request=None, response=None, services=self.services)
+    task = notify.NotifyBlockingChangeTask(services=self.services)
     params = {
         'send_email': 1, 'issue_id': issue2.issue_id, 'seq': 0,
         'delta_blocker_iids': self.issue1.issue_id, 'commenter_id': 1}
@@ -163,8 +165,7 @@
         project_id=12345, local_id=2, owner_id=2, reporter_id=1)
     issue2.cc_ids = [3]
     self.services.issue.TestAddIssue(issue2)
-    task = notify.NotifyBulkChangeTask(
-        request=None, response=None, services=self.services)
+    task = notify.NotifyBulkChangeTask(services=self.services)
     params = {
         'send_email': 1, 'seq': 0,
         'issue_ids': '%d,%d' % (self.issue1.issue_id, issue2.issue_id),
@@ -195,8 +196,7 @@
     """We generate email tasks for also-notify addresses."""
     self.issue1.derived_notify_addrs = [
         'mailing-list@example.com', 'member@example.com']
-    task = notify.NotifyBulkChangeTask(
-        request=None, response=None, services=self.services)
+    task = notify.NotifyBulkChangeTask(services=self.services)
     params = {
         'send_email': 1, 'seq': 0,
         'issue_ids': '%d' % (self.issue1.issue_id),
@@ -230,8 +230,7 @@
   def testNotifyBulkChangeTask_ProjectNotify(self, create_task_mock):
     """We generate email tasks for project.issue_notify_address."""
     self.project.issue_notify_address = 'mailing-list@example.com'
-    task = notify.NotifyBulkChangeTask(
-        request=None, response=None, services=self.services)
+    task = notify.NotifyBulkChangeTask(services=self.services)
     params = {
         'send_email': 1, 'seq': 0,
         'issue_ids': '%d' % (self.issue1.issue_id),
@@ -265,8 +264,7 @@
   @mock.patch('framework.cloud_tasks_helpers.create_task')
   def testNotifyBulkChangeTask_SubscriberGetsEmail(self, create_task_mock):
     """If a user subscription matches the issue, notify that user."""
-    task = notify.NotifyBulkChangeTask(
-        request=None, response=None, services=self.services)
+    task = notify.NotifyBulkChangeTask(services=self.services)
     params = {
         'send_email': 1,
         'issue_ids': '%d' % (self.issue1.issue_id),
@@ -293,8 +291,7 @@
   def testNotifyBulkChangeTask_CCAndSubscriberListsIssueOnce(
       self, create_task_mock):
     """If a user both CCs and subscribes, include issue only once."""
-    task = notify.NotifyBulkChangeTask(
-        request=None, response=None, services=self.services)
+    task = notify.NotifyBulkChangeTask(services=self.services)
     params = {
         'send_email': 1,
         'issue_ids': '%d' % (self.issue1.issue_id),
@@ -335,8 +332,7 @@
         project_id=12345, local_id=2, owner_id=2, reporter_id=1,
         is_spam=True)
     self.services.issue.TestAddIssue(issue2)
-    task = notify.NotifyBulkChangeTask(
-        request=None, response=None, services=self.services)
+    task = notify.NotifyBulkChangeTask(services=self.services)
     params = {
         'send_email': 1,
         'issue_ids': '%d,%d' % (self.issue1.issue_id, issue2.issue_id),
@@ -353,8 +349,7 @@
   def testFormatBulkIssues_Normal_Single(self):
     """A user may see full notification details for all changed issues."""
     self.issue1.summary = 'one summary'
-    task = notify.NotifyBulkChangeTask(
-        request=None, response=None, services=self.services)
+    task = notify.NotifyBulkChangeTask(services=self.services)
     users_by_id = {}
     commenter_view = None
     config = self.services.config.GetProjectConfig('cnxn', 12345)
@@ -374,8 +369,7 @@
     """A user may see full notification details for all changed issues."""
     self.issue1.summary = 'one summary'
     self.issue2.summary = 'two summary'
-    task = notify.NotifyBulkChangeTask(
-        request=None, response=None, services=self.services)
+    task = notify.NotifyBulkChangeTask(services=self.services)
     users_by_id = {}
     commenter_view = None
     config = self.services.config.GetProjectConfig('cnxn', 12345)
@@ -396,8 +390,7 @@
     """A user may not see full notification details for some changed issue."""
     self.issue1.summary = 'one summary'
     self.issue1.labels = ['Restrict-View-Google']
-    task = notify.NotifyBulkChangeTask(
-        request=None, response=None, services=self.services)
+    task = notify.NotifyBulkChangeTask(services=self.services)
     users_by_id = {}
     commenter_view = None
     config = self.services.config.GetProjectConfig('cnxn', 12345)
@@ -419,8 +412,7 @@
     self.issue1.summary = 'one summary'
     self.issue1.labels = ['Restrict-View-Google']
     self.issue2.summary = 'two summary'
-    task = notify.NotifyBulkChangeTask(
-        request=None, response=None, services=self.services)
+    task = notify.NotifyBulkChangeTask(services=self.services)
     users_by_id = {}
     commenter_view = None
     config = self.services.config.GetProjectConfig('cnxn', 12345)
@@ -522,8 +514,7 @@
     self.services.issue.TestAddAttachment(
         attach, comment.id, approval_issue.issue_id)
 
-    task = notify.NotifyApprovalChangeTask(
-        request=None, response=None, services=self.services)
+    task = notify.NotifyApprovalChangeTask(services=self.services)
     params = {
         'send_email': 1,
         'issue_id': approval_issue.issue_id,
@@ -556,8 +547,7 @@
         project_id=12345, user_id=999, issue_id=approval_issue.issue_id,
         amendments=[amend2], timestamp=1234567891, content='')
     self.services.issue.TestAddComment(comment2, approval_issue.local_id)
-    task = notify.NotifyApprovalChangeTask(
-        request=None, response=None, services=self.services)
+    task = notify.NotifyApprovalChangeTask(services=self.services)
     params = {
         'send_email': 1,
         'issue_id': approval_issue.issue_id,
@@ -579,8 +569,7 @@
         result['notified'])
 
   def testNotifyApprovalChangeTask_GetApprovalEmailRecipients(self):
-    task = notify.NotifyApprovalChangeTask(
-        request=None, response=None, services=self.services)
+    task = notify.NotifyApprovalChangeTask(services=self.services)
     issue = fake.MakeTestIssue(789, 1, 'summary', 'New', 111)
     approval_value = tracker_pb2.ApprovalValue(
         approver_ids=[222, 333],
@@ -625,8 +614,7 @@
         'proj', owner_ids=[777, 888], project_id=789)
     self.services.user.TestAddUser('owner1@test.com', 777)
     self.services.user.TestAddUser('cow@test.com', 888)
-    task = notify.NotifyRulesDeletedTask(
-        request=None, response=None, services=self.services)
+    task = notify.NotifyRulesDeletedTask(services=self.services)
     params = {'project_id': 789,
               'filter_rules': 'if green make yellow,if orange make blue'}
     mr = testing_helpers.MakeMonorailRequest(
@@ -649,18 +637,12 @@
         'reply_to': 'user@example.com',
         'to': 'user@example.com',
         'subject': 'Test subject'}
-    body = json.dumps(params)
-    request = webapp2.Request.blank('/', body=body)
-    task = notify.OutboundEmailTask(
-        request=request, response=None, services=self.services)
-    mr = testing_helpers.MakeMonorailRequest(
-        user_info={'user_id': 1},
-        payload=body,
-        method='POST',
-        services=self.services)
-    result = task.HandleRequest(mr)
-    self.assertEqual(params['from_addr'], result['sender'])
-    self.assertEqual(params['subject'], result['subject'])
+    data = json.dumps(params)
+    res = self.app.test_client().post('/_task/outboundEmail.do', data=data)
+    res_string = res.get_data()[5:]
+    res_json = json.loads(res_string)
+    self.assertEqual(params['from_addr'], res_json['sender'])
+    self.assertEqual(params['subject'], res_json['subject'])
 
   def testOutboundEmailTask_MissingTo(self):
     """We skip emails that don't specify the To-line."""
@@ -668,36 +650,26 @@
         'from_addr': 'requester@example.com',
         'reply_to': 'user@example.com',
         'subject': 'Test subject'}
-    body = json.dumps(params)
-    request = webapp2.Request.blank('/', body=body)
-    task = notify.OutboundEmailTask(
-        request=request, response=None, services=self.services)
-    mr = testing_helpers.MakeMonorailRequest(
-        user_info={'user_id': 1},
-        payload=body,
-        method='POST',
-        services=self.services)
-    result = task.HandleRequest(mr)
-    self.assertEqual('Skipping because no "to" address found.', result['note'])
-    self.assertNotIn('from_addr', result)
+    data = json.dumps(params)
+    res = self.app.test_client().post('/_task/outboundEmail.do', data=data)
+    res_string = res.get_data()[5:]
+    res_json = json.loads(res_string)
+    self.assertEqual(
+        'Skipping because no "to" address found.', res_json['note'])
+    self.assertNotIn('from_addr', res_string)
 
   def testOutboundEmailTask_BannedUser(self):
     """We don't send emails to banned users.."""
+    self.servlet.services.user.TestAddUser(
+        'banned@example.com', 404, banned=True)
     params = {
         'from_addr': 'requester@example.com',
         'reply_to': 'user@example.com',
         'to': 'banned@example.com',
         'subject': 'Test subject'}
-    body = json.dumps(params)
-    request = webapp2.Request.blank('/', body=body)
-    task = notify.OutboundEmailTask(
-        request=request, response=None, services=self.services)
-    mr = testing_helpers.MakeMonorailRequest(
-        user_info={'user_id': 1},
-        payload=body,
-        method='POST',
-        services=self.services)
-    self.services.user.TestAddUser('banned@example.com', 404, banned=True)
-    result = task.HandleRequest(mr)
-    self.assertEqual('Skipping because user is banned.', result['note'])
-    self.assertNotIn('from_addr', result)
+    data = json.dumps(params)
+    res = self.app.test_client().post('/_task/outboundEmail.do', data=data)
+    res_string = res.get_data()[5:]
+    res_json = json.loads(res_string)
+    self.assertEqual('Skipping because user is banned.', res_json['note'])
+    self.assertNotIn('from_addr', res_string)
diff --git a/features/test/pubsub_test.py b/features/test/pubsub_test.py
index 2044cf7..e86230c 100644
--- a/features/test/pubsub_test.py
+++ b/features/test/pubsub_test.py
@@ -38,8 +38,7 @@
 
   def testPublishPubsubIssueChangeTask_NoIssueIdParam(self):
     """Test case when issue_id param is not passed."""
-    task = pubsub.PublishPubsubIssueChangeTask(
-        request=None, response=None, services=self.services)
+    task = pubsub.PublishPubsubIssueChangeTask(services=self.services)
     mr = testing_helpers.MakeMonorailRequest(
         user_info={'user_id': 1},
         params={},
@@ -54,8 +53,7 @@
   def testPublishPubsubIssueChangeTask_PubSubAPIInitFailure(self):
     """Test case when pub/sub API fails to init."""
     pubsub.set_up_pubsub_api = Mock(return_value=None)
-    task = pubsub.PublishPubsubIssueChangeTask(
-        request=None, response=None, services=self.services)
+    task = pubsub.PublishPubsubIssueChangeTask(services=self.services)
     mr = testing_helpers.MakeMonorailRequest(
         user_info={'user_id': 1},
         params={},
@@ -69,8 +67,7 @@
 
   def testPublishPubsubIssueChangeTask_IssueNotFound(self):
     """Test case when issue is not found."""
-    task = pubsub.PublishPubsubIssueChangeTask(
-        request=None, response=None, services=self.services)
+    task = pubsub.PublishPubsubIssueChangeTask(services=self.services)
     mr = testing_helpers.MakeMonorailRequest(
         user_info={'user_id': 1},
         params={'issue_id': 314159},
@@ -87,8 +84,7 @@
     issue = fake.MakeTestIssue(789, 543, 'sum', 'New', 111, issue_id=78901,
         project_name='rutabaga')
     self.services.issue.TestAddIssue(issue)
-    task = pubsub.PublishPubsubIssueChangeTask(
-        request=None, response=None, services=self.services)
+    task = pubsub.PublishPubsubIssueChangeTask(services=self.services)
     mr = testing_helpers.MakeMonorailRequest(
         user_info={'user_id': 1},
         params={'issue_id': 78901},
diff --git a/features/test/savedqueries_helpers_test.py b/features/test/savedqueries_helpers_test.py
index d635fe1..7f5ad47 100644
--- a/features/test/savedqueries_helpers_test.py
+++ b/features/test/savedqueries_helpers_test.py
@@ -10,7 +10,10 @@
 
 import unittest
 
-import mox
+try:
+  from mox3 import mox
+except ImportError:
+  import mox
 
 from features import savedqueries_helpers
 from testing import fake