Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/features/test/__init__.py b/features/test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/features/test/__init__.py
diff --git a/features/test/activities_test.py b/features/test/activities_test.py
new file mode 100644
index 0000000..4eae1ab
--- /dev/null
+++ b/features/test/activities_test.py
@@ -0,0 +1,143 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unittests for monorail.feature.activities."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+import mox
+
+from features import activities
+from framework import framework_views
+from framework import profiler
+from proto import tracker_pb2
+from proto import user_pb2
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+
+
+class ActivitiesTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService(),
+ )
+
+ self.project_name = 'proj'
+ self.project_id = 987
+ self.project = self.services.project.TestAddProject(
+ self.project_name, project_id=self.project_id,
+ process_inbound_email=True)
+
+ self.issue_id = 11
+ self.issue_local_id = 100
+ self.issue = tracker_pb2.Issue()
+ self.issue.issue_id = self.issue_id
+ self.issue.project_id = self.project_id
+ self.issue.local_id = self.issue_local_id
+ self.services.issue.TestAddIssue(self.issue)
+
+ self.comment_id = 123
+ self.comment_timestamp = 120
+ self.user = self.services.user.TestAddUser('testuser@example.com', 2)
+ self.user_id = self.user.user_id
+ self.mr_after = 1234
+
+ self.mox = mox.Mox()
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def testActivities_NoUpdates(self):
+ mr = testing_helpers.MakeMonorailRequest()
+ updates_data = activities.GatherUpdatesData(
+ self.services, mr, project_ids=[256],
+ user_ids=None, ending=None, updates_page_url=None, autolink=None,
+ highlight=None)
+
+ self.assertIsNone(updates_data['pagination'])
+ self.assertIsNone(updates_data['no_stars'])
+ self.assertIsNone(updates_data['updates_data'])
+ self.assertEqual('yes', updates_data['no_activities'])
+ self.assertIsNone(updates_data['ending_type'])
+
+ def createAndAssertUpdates(self, project_ids=None, user_ids=None,
+ ascending=True):
+ comment_1 = tracker_pb2.IssueComment(
+ id=self.comment_id, issue_id=self.issue_id,
+ project_id=self.project_id, user_id=self.user_id,
+ content='this is the 1st comment',
+ timestamp=self.comment_timestamp)
+ self.mox.StubOutWithMock(self.services.issue, 'GetIssueActivity')
+
+ after = 0
+ if ascending:
+ after = self.mr_after
+ self.services.issue.GetIssueActivity(
+ mox.IgnoreArg(), num=50, before=0, after=after, project_ids=project_ids,
+ user_ids=user_ids, ascending=ascending).AndReturn([comment_1])
+
+ self.mox.ReplayAll()
+
+ mr = testing_helpers.MakeMonorailRequest()
+ if ascending:
+ mr.after = self.mr_after
+
+ updates_page_url='testing/testing'
+ updates_data = activities.GatherUpdatesData(
+ self.services, mr, project_ids=project_ids,
+ user_ids=user_ids, ending=None, autolink=None,
+ highlight='highlightme', updates_page_url=updates_page_url)
+ self.mox.VerifyAll()
+
+ if mr.after:
+ pagination = updates_data['pagination']
+ self.assertIsNone(pagination.last)
+ self.assertEqual(
+ '%s?before=%d' %
+ (updates_page_url.split('/')[-1], self.comment_timestamp),
+ pagination.next_url)
+ self.assertEqual(
+ '%s?after=%d' %
+ (updates_page_url.split('/')[-1], self.comment_timestamp),
+ pagination.prev_url)
+
+ activity_view = updates_data['updates_data'].older[0]
+ self.assertEqual(
+ '<a class="ot-issue-link"\n \n '
+ 'href="/p//issues/detail?id=%s#c_ts%s"\n >'
+ 'issue %s</a>\n\n()\n\n\n\n\n \n commented on' % (
+ self.issue_local_id, self.comment_timestamp, self.issue_local_id),
+ activity_view.escaped_title)
+ self.assertEqual(
+ '<span class="ot-issue-comment">\n this is the 1st comment\n</span>',
+ activity_view.escaped_body)
+ self.assertEqual('highlightme', activity_view.highlight)
+ self.assertEqual(self.project_name, activity_view.project_name)
+
+ def testActivities_AscendingProjectUpdates(self):
+ self.createAndAssertUpdates(project_ids=[self.project_id], ascending=True)
+
+ def testActivities_DescendingProjectUpdates(self):
+ self.createAndAssertUpdates(project_ids=[self.project_id], ascending=False)
+
+ def testActivities_AscendingUserUpdates(self):
+ self.createAndAssertUpdates(user_ids=[self.user_id], ascending=True)
+
+ def testActivities_DescendingUserUpdates(self):
+ self.createAndAssertUpdates(user_ids=[self.user_id], ascending=False)
+
+ def testActivities_SpecifyProjectAndUser(self):
+ self.createAndAssertUpdates(
+ project_ids=[self.project_id], user_ids=[self.user_id], ascending=False)
diff --git a/features/test/alert2issue_test.py b/features/test/alert2issue_test.py
new file mode 100644
index 0000000..3b1b6d1
--- /dev/null
+++ b/features/test/alert2issue_test.py
@@ -0,0 +1,677 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unittests for monorail.feature.alert2issue."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import email
+import unittest
+from mock import patch
+import mox
+from parameterized import parameterized
+
+from features import alert2issue
+from framework import authdata
+from framework import emailfmt
+from proto import tracker_pb2
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+from tracker import tracker_helpers
+
+AlertEmailHeader = emailfmt.AlertEmailHeader
+
+
+class TestData(object):
+ """Contains constants or such objects that are intended to be read-only."""
+ cnxn = 'fake cnxn'
+ test_issue_local_id = 100
+ component_id = 123
+ trooper_queue = 'my-trooper-bug-queue'
+
+ project_name = 'proj'
+ project_addr = '%s+ALERT+%s@monorail.example.com' % (
+ project_name, trooper_queue)
+ project_id = 987
+
+ from_addr = 'user@monorail.example.com'
+ user_id = 111
+
+ msg_body = 'this is the body'
+ msg_subject = 'this is the subject'
+ msg = testing_helpers.MakeMessage(
+ testing_helpers.ALERT_EMAIL_HEADER_LINES, msg_body)
+
+ incident_id = msg.get(AlertEmailHeader.INCIDENT_ID)
+ incident_label = alert2issue._GetIncidentLabel(incident_id)
+
+ # All the tests in this class use the following alert properties, and
+ # the generator functions/logic should be tested in a separate class.
+ alert_props = {
+ 'owner_id': 0,
+ 'cc_ids': [],
+ 'status': 'Available',
+ 'incident_label': incident_label,
+ 'priority': 'Pri-0',
+ 'trooper_queue': trooper_queue,
+ 'field_values': [],
+ 'labels': [
+ 'Restrict-View-Google', 'Pri-0', incident_label, trooper_queue
+ ],
+ 'component_ids': [component_id],
+ }
+
+
+class ProcessEmailNotificationTests(unittest.TestCase, TestData):
+ """Implements unit tests for alert2issue.ProcessEmailNotification."""
+ def setUp(self):
+ # services
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService(),
+ features=fake.FeaturesService())
+
+ # project
+ self.project = self.services.project.TestAddProject(
+ self.project_name, project_id=self.project_id,
+ process_inbound_email=True, contrib_ids=[self.user_id])
+
+ # config
+ proj_config = fake.MakeTestConfig(self.project_id, [], ['Available'])
+ comp_def_1 = tracker_pb2.ComponentDef(
+ component_id=123, project_id=987, path='FOO', docstring='foo docstring')
+ proj_config.component_defs = [comp_def_1]
+ self.services.config.StoreConfig(self.cnxn, proj_config)
+
+ # sender
+ self.auth = authdata.AuthData(user_id=self.user_id, email=self.from_addr)
+
+ # issue
+ self.issue = tracker_pb2.Issue(
+ project_id=self.project_id,
+ local_id=self.test_issue_local_id,
+ summary=self.msg_subject,
+ reporter_id=self.user_id,
+ component_ids=[self.component_id],
+ status=self.alert_props['status'],
+ labels=self.alert_props['labels'],
+ )
+ self.services.issue.TestAddIssue(self.issue)
+
+ # Patch send_notifications functions.
+ self.notification_patchers = [
+ patch('features.send_notifications.%s' % func, spec=True)
+ for func in [
+ 'PrepareAndSendIssueBlockingNotification',
+ 'PrepareAndSendIssueChangeNotification',
+ ]
+ ]
+ self.blocking_notification = self.notification_patchers[0].start()
+ self.blocking_notification = self.notification_patchers[1].start()
+
+ self.mox = mox.Mox()
+
+ def tearDown(self):
+ self.notification_patchers[0].stop()
+ self.notification_patchers[1].stop()
+
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def testGoogleAddrsAreAllowlistedSender(self):
+ self.assertTrue(alert2issue.IsAllowlisted('test@google.com'))
+ self.assertFalse(alert2issue.IsAllowlisted('test@notgoogle.com'))
+
+ def testSkipNotification_IfFromNonAllowlistedSender(self):
+ self.mox.StubOutWithMock(alert2issue, 'IsAllowlisted')
+ alert2issue.IsAllowlisted(self.from_addr).AndReturn(False)
+
+ # None of them should be called, if the sender has not been allowlisted.
+ self.mox.StubOutWithMock(self.services.issue, 'CreateIssueComment')
+ self.mox.StubOutWithMock(self.services.issue, 'CreateIssue')
+ self.mox.ReplayAll()
+
+ alert2issue.ProcessEmailNotification(
+ self.services, self.cnxn, self.project, self.project_addr,
+ self.from_addr, self.auth, self.msg_subject, self.msg_body,
+ self.incident_label, self.msg, self.trooper_queue)
+ self.mox.VerifyAll()
+
+ def testSkipNotification_TooLongComment(self):
+ self.mox.StubOutWithMock(alert2issue, 'IsAllowlisted')
+ alert2issue.IsAllowlisted(self.from_addr).AndReturn(True)
+ self.mox.StubOutWithMock(alert2issue, 'IsCommentSizeReasonable')
+ alert2issue.IsCommentSizeReasonable(
+ 'Filed by %s on behalf of %s\n\n%s' %
+ (self.auth.email, self.from_addr, self.msg_body)).AndReturn(False)
+
+ # None of them should be called, if the comment is too long.
+ self.mox.StubOutWithMock(self.services.issue, 'CreateIssueComment')
+ self.mox.StubOutWithMock(self.services.issue, 'CreateIssue')
+ self.mox.ReplayAll()
+
+ alert2issue.ProcessEmailNotification(
+ self.services, self.cnxn, self.project, self.project_addr,
+ self.from_addr, self.auth, self.msg_subject, self.msg_body,
+ self.incident_label, self.msg, self.trooper_queue)
+ self.mox.VerifyAll()
+
+ def testProcessNotification_IfFromAllowlistedSender(self):
+ self.mox.StubOutWithMock(alert2issue, 'IsAllowlisted')
+ alert2issue.IsAllowlisted(self.from_addr).AndReturn(True)
+
+ self.mox.StubOutWithMock(tracker_helpers, 'LookupComponentIDs')
+ tracker_helpers.LookupComponentIDs(
+ ['Infra'],
+ mox.IgnoreArg()).AndReturn([1])
+ self.mox.StubOutWithMock(self.services.issue, 'CreateIssueComment')
+ self.mox.StubOutWithMock(self.services.issue, 'CreateIssue')
+ self.mox.ReplayAll()
+
+ # Either of the methods should be called, if the sender is allowlisted.
+ with self.assertRaises(mox.UnexpectedMethodCallError):
+ alert2issue.ProcessEmailNotification(
+ self.services, self.cnxn, self.project, self.project_addr,
+ self.from_addr, self.auth, self.msg_subject, self.msg_body,
+ self.incident_label, self.msg, self.trooper_queue)
+
+ self.mox.VerifyAll()
+
+ def testIssueCreated_ForNewIncident(self):
+ """Tests if a new issue is created for a new incident."""
+ self.mox.StubOutWithMock(alert2issue, 'IsAllowlisted')
+ alert2issue.IsAllowlisted(self.from_addr).AndReturn(True)
+
+ # FindAlertIssue() returns None for a new incident.
+ self.mox.StubOutWithMock(alert2issue, 'FindAlertIssue')
+ alert2issue.FindAlertIssue(
+ self.services, self.cnxn, self.project.project_id,
+ self.incident_label).AndReturn(None)
+
+ # Mock GetAlertProperties() to create the issue with the expected
+ # properties.
+ self.mox.StubOutWithMock(alert2issue, 'GetAlertProperties')
+ alert2issue.GetAlertProperties(
+ self.services, self.cnxn, self.project_id, self.incident_id,
+ self.trooper_queue, self.msg).AndReturn(self.alert_props)
+
+ self.mox.ReplayAll()
+ alert2issue.ProcessEmailNotification(
+ self.services, self.cnxn, self.project, self.project_addr,
+ self.from_addr, self.auth, self.msg_subject, self.msg_body,
+ self.incident_id, self.msg, self.trooper_queue)
+
+ # the local ID of the newly created issue should be +1 from the highest ID
+ # in the existing issues.
+ comments = self._verifyIssue(self.test_issue_local_id + 1, self.alert_props)
+ self.assertEqual(comments[0].content,
+ 'Filed by %s on behalf of %s\n\n%s' % (
+ self.from_addr, self.from_addr, self.msg_body))
+
+ self.mox.VerifyAll()
+
+ def testProcessEmailNotification_ExistingIssue(self):
+ """When an alert for an ongoing incident comes in, add a comment."""
+ self.mox.StubOutWithMock(alert2issue, 'IsAllowlisted')
+ alert2issue.IsAllowlisted(self.from_addr).AndReturn(True)
+
+ # FindAlertIssue() returns None for a new incident.
+ self.mox.StubOutWithMock(alert2issue, 'FindAlertIssue')
+ alert2issue.FindAlertIssue(
+ self.services, self.cnxn, self.project.project_id,
+ self.incident_label).AndReturn(self.issue)
+
+ # Mock GetAlertProperties() to create the issue with the expected
+ # properties.
+ self.mox.StubOutWithMock(alert2issue, 'GetAlertProperties')
+ alert2issue.GetAlertProperties(
+ self.services, self.cnxn, self.project_id, self.incident_id,
+ self.trooper_queue, self.msg).AndReturn(self.alert_props)
+
+ self.mox.ReplayAll()
+
+ # Before processing the notification, ensures that there is only 1 comment
+ # in the test issue.
+ comments = self._verifyIssue(self.test_issue_local_id, self.alert_props)
+ self.assertEqual(len(comments), 1)
+
+ # Process
+ alert2issue.ProcessEmailNotification(
+ self.services, self.cnxn, self.project, self.project_addr,
+ self.from_addr, self.auth, self.msg_subject, self.msg_body,
+ self.incident_id, self.msg, self.trooper_queue)
+
+ # Now, it should have a new comment added.
+ comments = self._verifyIssue(self.test_issue_local_id, self.alert_props)
+ self.assertEqual(len(comments), 2)
+ self.assertEqual(comments[1].content,
+ 'Filed by %s on behalf of %s\n\n%s' % (
+ self.from_addr, self.from_addr, self.msg_body))
+
+ self.mox.VerifyAll()
+
+ def _verifyIssue(self, local_issue_id, alert_props):
+ actual_issue = self.services.issue.GetIssueByLocalID(
+ self.cnxn, self.project.project_id, local_issue_id)
+ actual_comments = self.services.issue.GetCommentsForIssue(
+ self.cnxn, actual_issue.issue_id)
+
+ self.assertEqual(actual_issue.summary, self.msg_subject)
+ self.assertEqual(actual_issue.status, alert_props['status'])
+ self.assertEqual(actual_issue.reporter_id, self.user_id)
+ self.assertEqual(actual_issue.component_ids, [self.component_id])
+ if alert_props['owner_id']:
+ self.assertEqual(actual_issue.owner_id, alert_props['owner_id'])
+ self.assertEqual(sorted(actual_issue.labels), sorted(alert_props['labels']))
+ return actual_comments
+
+
+class GetAlertPropertiesTests(unittest.TestCase, TestData):
+ """Implements unit tests for alert2issue.GetAlertProperties."""
+ def assertSubset(self, lhs, rhs):
+ if not (lhs <= rhs):
+ raise AssertionError('%s not a subset of %s' % (lhs, rhs))
+
+ def assertCaseInsensitiveEqual(self, lhs, rhs):
+ self.assertEqual(lhs if lhs is None else lhs.lower(),
+ rhs if lhs is None else rhs.lower())
+
+ def setUp(self):
+ # services
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService())
+
+ # project
+ self.project = self.services.project.TestAddProject(
+ self.project_name, project_id=self.project_id,
+ process_inbound_email=True, contrib_ids=[self.user_id])
+
+ proj_config = fake.MakeTestConfig(
+ self.project_id,
+ [
+ # test labels for Pri field
+ 'Pri-0', 'Pri-1', 'Pri-2', 'Pri-3',
+ # test labels for OS field
+ 'OS-Android', 'OS-Windows',
+ # test labels for Type field
+ 'Type-Bug', 'Type-Bug-Regression', 'Type-Bug-Security', 'Type-Task',
+ ],
+ ['Assigned', 'Available', 'Unconfirmed']
+ )
+ self.services.config.StoreConfig(self.cnxn, proj_config)
+
+ # create a test email message, which tests can alternate the header values
+ # to verify the behaviour of a given parser function.
+ self.test_msg = email.Message.Message()
+ for key, value in self.msg.items():
+ self.test_msg[key] = value
+
+ self.mox = mox.Mox()
+
+ @parameterized.expand([
+ (None,),
+ ('',),
+ (' ',),
+ ])
+ def testDefaultComponent(self, header_value):
+ """Checks if the default component is Infra."""
+ self.test_msg.replace_header(AlertEmailHeader.COMPONENT, header_value)
+ self.mox.StubOutWithMock(tracker_helpers, 'LookupComponentIDs')
+ tracker_helpers.LookupComponentIDs(
+ ['Infra'],
+ mox.IgnoreArg()).AndReturn([self.component_id])
+
+ self.mox.ReplayAll()
+ props = alert2issue.GetAlertProperties(
+ self.services, self.cnxn, self.project_id, self.incident_id,
+ self.trooper_queue, self.test_msg)
+ self.assertEqual(props['component_ids'], [self.component_id])
+ self.mox.VerifyAll()
+
+ @parameterized.expand([
+ # an existing single component with componentID 1
+ ({'Infra': 1}, [1]),
+ # 3 of existing components
+ ({'Infra': 1, 'Foo': 2, 'Bar': 3}, [1, 2, 3]),
+ # a non-existing component
+ ({'Infra': None}, []),
+ # 3 of non-existing components
+ ({'Infra': None, 'Foo': None, 'Bar': None}, []),
+ # a mix of existing and non-existing components
+ ({'Infra': 1, 'Foo': None, 'Bar': 2}, [1, 2]),
+ ])
+ def testGetComponentIDs(self, components, expected_component_ids):
+ """Tests _GetComponentIDs."""
+ self.test_msg.replace_header(
+ AlertEmailHeader.COMPONENT, ','.join(sorted(components.keys())))
+
+ self.mox.StubOutWithMock(tracker_helpers, 'LookupComponentIDs')
+ tracker_helpers.LookupComponentIDs(
+ sorted(components.keys()),
+ mox.IgnoreArg()).AndReturn(
+ [components[key] for key in sorted(components.keys())
+ if components[key]]
+ )
+
+ self.mox.ReplayAll()
+ props = alert2issue.GetAlertProperties(
+ self.services, self.cnxn, self.project_id, self.incident_id,
+ self.trooper_queue, self.test_msg)
+ self.assertEqual(sorted(props['component_ids']),
+ sorted(expected_component_ids))
+ self.mox.VerifyAll()
+
+
+ def testLabelsWithNecessaryValues(self):
+ """Checks if the labels contain all the necessary values."""
+ props = alert2issue.GetAlertProperties(
+ self.services, self.cnxn, self.project_id, self.incident_id,
+ self.trooper_queue, self.test_msg)
+
+ # This test assumes that the test message contains non-empty values for
+ # all the headers.
+ self.assertTrue(props['incident_label'])
+ self.assertTrue(props['priority'])
+ self.assertTrue(props['issue_type'])
+ self.assertTrue(props['oses'])
+
+ # Here are a list of the labels that props['labels'] should contain
+ self.assertIn('Restrict-View-Google'.lower(), props['labels'])
+ self.assertIn(self.trooper_queue, props['labels'])
+ self.assertIn(props['incident_label'], props['labels'])
+ self.assertIn(props['priority'], props['labels'])
+ self.assertIn(props['issue_type'], props['labels'])
+ for os in props['oses']:
+ self.assertIn(os, props['labels'])
+
+ @parameterized.expand([
+ (None, 0),
+ ('', 0),
+ (' ', 0),
+ ])
+ def testDefaultOwnerID(self, header_value, expected_owner_id):
+ """Checks if _GetOwnerID returns None in default."""
+ self.test_msg.replace_header(AlertEmailHeader.OWNER, header_value)
+ props = alert2issue.GetAlertProperties(
+ self.services, self.cnxn, self.project_id, self.incident_id,
+ self.trooper_queue, self.test_msg)
+ self.assertEqual(props['owner_id'], expected_owner_id)
+
+ @parameterized.expand(
+ [
+ # an existing user with userID 1.
+ ('owner@example.org', 1),
+ # a non-existing user.
+ ('owner@example.org', 0),
+ ])
+ def testGetOwnerID(self, owner, expected_owner_id):
+ """Tests _GetOwnerID returns the ID of the owner."""
+ self.test_msg.replace_header(AlertEmailHeader.CC, '')
+ self.test_msg.replace_header(AlertEmailHeader.OWNER, owner)
+
+ self.mox.StubOutWithMock(self.services.user, 'LookupExistingUserIDs')
+ self.services.user.LookupExistingUserIDs(self.cnxn, [owner]).AndReturn(
+ {owner: expected_owner_id})
+
+ self.mox.ReplayAll()
+ props = alert2issue.GetAlertProperties(
+ self.services, self.cnxn, self.project_id, self.incident_id,
+ self.trooper_queue, self.test_msg)
+ self.mox.VerifyAll()
+ self.assertEqual(props['owner_id'], expected_owner_id)
+
+ @parameterized.expand([
+ (None, []),
+ ('', []),
+ (' ', []),
+ ])
+ def testDefaultCCIDs(self, header_value, expected_cc_ids):
+ """Checks if _GetCCIDs returns an empty list in default."""
+ self.test_msg.replace_header(AlertEmailHeader.CC, header_value)
+ props = alert2issue.GetAlertProperties(
+ self.services, self.cnxn, self.project_id, self.incident_id,
+ self.trooper_queue, self.test_msg)
+ self.assertEqual(props['cc_ids'], expected_cc_ids)
+
+ @parameterized.expand([
+ # with one existing user cc-ed.
+ ({'user1@example.org': 1}, [1]),
+ # with two of existing users.
+ ({'user1@example.org': 1, 'user2@example.org': 2}, [1, 2]),
+ # with one non-existing user.
+ ({'user1@example.org': None}, []),
+ # with two of non-existing users.
+ ({'user1@example.org': None, 'user2@example.org': None}, []),
+ # with a mix of existing and non-existing users.
+ ({'user1@example.org': 1, 'user2@example.org': None}, [1]),
+ ])
+ def testGetCCIDs(self, ccers, expected_cc_ids):
+ """Tests _GetCCIDs returns the IDs of the email addresses to be cc-ed."""
+ self.test_msg.replace_header(
+ AlertEmailHeader.CC, ','.join(sorted(ccers.keys())))
+ self.test_msg.replace_header(AlertEmailHeader.OWNER, '')
+
+ self.mox.StubOutWithMock(self.services.user, 'LookupExistingUserIDs')
+ self.services.user.LookupExistingUserIDs(
+ self.cnxn, sorted(ccers.keys())).AndReturn(ccers)
+
+ self.mox.ReplayAll()
+ props = alert2issue.GetAlertProperties(
+ self.services, self.cnxn, self.project_id, self.incident_id,
+ self.trooper_queue, self.test_msg)
+ self.mox.VerifyAll()
+ self.assertEqual(sorted(props['cc_ids']), sorted(expected_cc_ids))
+
+ @parameterized.expand([
+ # None and '' should result in the default priority returned.
+ (None, 'Pri-2'),
+ ('', 'Pri-2'),
+ (' ', 'Pri-2'),
+
+ # Tests for valid priority values
+ ('0', 'Pri-0'),
+ ('1', 'Pri-1'),
+ ('2', 'Pri-2'),
+ ('3', 'Pri-3'),
+
+ # Tests for invalid priority values
+ ('test', 'Pri-2'),
+ ('foo', 'Pri-2'),
+ ('critical', 'Pri-2'),
+ ('4', 'Pri-2'),
+ ('3x', 'Pri-2'),
+ ('00', 'Pri-2'),
+ ('01', 'Pri-2'),
+ ])
+ def testGetPriority(self, header_value, expected_priority):
+ """Tests _GetPriority."""
+ self.test_msg.replace_header(AlertEmailHeader.PRIORITY, header_value)
+ props = alert2issue.GetAlertProperties(
+ self.services, self.cnxn, self.project_id, self.incident_id,
+ self.trooper_queue, self.test_msg)
+ self.assertCaseInsensitiveEqual(props['priority'], expected_priority)
+
+ @parameterized.expand([
+ (None, 'Available'),
+ ('', 'Available'),
+ (' ', 'Available'),
+ ])
+ def testDefaultStatus(self, header_value, expected_status):
+ """Checks if _GetStatus return Available in default."""
+ self.test_msg.replace_header(AlertEmailHeader.STATUS, header_value)
+ props = alert2issue.GetAlertProperties(
+ self.services, self.cnxn, self.project_id, self.incident_id,
+ self.trooper_queue, self.test_msg)
+ self.assertCaseInsensitiveEqual(props['status'], expected_status)
+
+ @parameterized.expand([
+ ('random_status', True, 'random_status'),
+ # If the status is not one of the open statuses, the default status
+ # should be returned instead.
+ ('random_status', False, 'Available'),
+ ])
+ def testGetStatusWithoutOwner(self, status, means_open, expected_status):
+ """Tests GetStatus without an owner."""
+ self.test_msg.replace_header(AlertEmailHeader.STATUS, status)
+ self.mox.StubOutWithMock(tracker_helpers, 'MeansOpenInProject')
+ tracker_helpers.MeansOpenInProject(status, mox.IgnoreArg()).AndReturn(
+ means_open)
+
+ self.mox.ReplayAll()
+ props = alert2issue.GetAlertProperties(
+ self.services, self.cnxn, self.project_id, self.incident_id,
+ self.trooper_queue, self.test_msg)
+ self.assertCaseInsensitiveEqual(props['status'], expected_status)
+ self.mox.VerifyAll()
+
+ @parameterized.expand([
+ # If there is an owner, the status should always be Assigned.
+ (None, 'Assigned'),
+ ('', 'Assigned'),
+ (' ', 'Assigned'),
+
+ ('random_status', 'Assigned'),
+ ('Available', 'Assigned'),
+ ('Unconfirmed', 'Assigned'),
+ ('Fixed', 'Assigned'),
+ ])
+ def testGetStatusWithOwner(self, status, expected_status):
+ """Tests GetStatus with an owner."""
+ owner = 'owner@example.org'
+ self.test_msg.replace_header(AlertEmailHeader.OWNER, owner)
+ self.test_msg.replace_header(AlertEmailHeader.CC, '')
+ self.test_msg.replace_header(AlertEmailHeader.STATUS, status)
+
+ self.mox.StubOutWithMock(self.services.user, 'LookupExistingUserIDs')
+ self.services.user.LookupExistingUserIDs(self.cnxn, [owner]).AndReturn(
+ {owner: 1})
+
+ self.mox.ReplayAll()
+ props = alert2issue.GetAlertProperties(
+ self.services, self.cnxn, self.project_id, self.incident_id,
+ self.trooper_queue, self.test_msg)
+ self.assertCaseInsensitiveEqual(props['status'], expected_status)
+ self.mox.VerifyAll()
+
+ @parameterized.expand(
+ [
+ # None and '' should result in None returned.
+ (None, None),
+ ('', None),
+ (' ', None),
+
+ # allowlisted issue types
+ ('Bug', 'Type-Bug'),
+ ('Bug-Regression', 'Type-Bug-Regression'),
+ ('Bug-Security', 'Type-Bug-Security'),
+ ('Task', 'Type-Task'),
+
+ # non-allowlisted issue types
+ ('foo', None),
+ ('bar', None),
+ ('Bug,Bug-Regression', None),
+ ('Bug,', None),
+ (',Task', None),
+ ])
+ def testGetIssueType(self, header_value, expected_issue_type):
+ """Tests _GetIssueType."""
+ self.test_msg.replace_header(AlertEmailHeader.TYPE, header_value)
+ props = alert2issue.GetAlertProperties(
+ self.services, self.cnxn, self.project_id, self.incident_id,
+ self.trooper_queue, self.test_msg)
+ self.assertCaseInsensitiveEqual(props['issue_type'], expected_issue_type)
+
+ @parameterized.expand(
+ [
+ # None and '' should result in an empty list returned.
+ (None, []),
+ ('', []),
+ (' ', []),
+
+ # a single, allowlisted os
+ ('Android', ['OS-Android']),
+ # a single, non-allowlisted OS
+ ('Bendroid', []),
+ # multiple, allowlisted oses
+ ('Android,Windows', ['OS-Android', 'OS-Windows']),
+ # multiple, non-allowlisted oses
+ ('Bendroid,Findows', []),
+ # a mix of allowlisted and non-allowlisted oses
+ ('Android,Findows,Windows,Bendroid', ['OS-Android', 'OS-Windows']),
+ # a mix of allowlisted and non-allowlisted oses with trailing commas.
+ ('Android,Findows,Windows,Bendroid,,', ['OS-Android', 'OS-Windows']),
+ # a mix of allowlisted and non-allowlisted oses with commas at the
+ # beginning.
+ (
+ ',,Android,Findows,Windows,Bendroid,,',
+ ['OS-Android', 'OS-Windows']),
+ ])
+ def testGetOSes(self, header_value, expected_oses):
+ """Tests _GetOSes."""
+ self.test_msg.replace_header(AlertEmailHeader.OS, header_value)
+ props = alert2issue.GetAlertProperties(
+ self.services, self.cnxn, self.project_id, self.incident_id,
+ self.trooper_queue, self.test_msg)
+ self.assertEqual(sorted(os if os is None else os.lower()
+ for os in props['oses']),
+ sorted(os if os is None else os.lower()
+ for os in expected_oses))
+
+ @parameterized.expand([
+ # None and '' should result in an empty list + RSVG returned.
+ (None, []),
+ ('', []),
+ (' ', []),
+
+ ('Label-1', ['label-1']),
+ ('Label-1,Label-2', ['label-1', 'label-2',]),
+ ('Label-1,Label-2,Label-3', ['label-1', 'label-2', 'label-3']),
+
+ # Duplicates should be removed.
+ ('Label-1,Label-1', ['label-1']),
+ ('Label-1,label-1', ['label-1']),
+ (',Label-1,label-1,', ['label-1']),
+ ('Label-1,label-1,', ['label-1']),
+ (',Label-1,,label-1,,,', ['label-1']),
+ ('Label-1,Label-2,Label-1', ['label-1', 'label-2']),
+
+ # Whitespaces should be removed from labels.
+ ('La bel - 1 ', ['label-1']),
+ ('La bel - 1 , Label- 1', ['label-1']),
+ ('La bel- 1 , Label - 2', ['label-1', 'label-2']),
+
+ # RSVG should be set always.
+ ('Label-1,Label-1,Restrict-View-Google', ['label-1']),
+ ])
+ def testGetLabels(self, header_value, expected_labels):
+ """Tests _GetLabels."""
+ self.test_msg.replace_header(AlertEmailHeader.LABEL, header_value)
+ props = alert2issue.GetAlertProperties(
+ self.services, self.cnxn, self.project_id, self.incident_id,
+ self.trooper_queue, self.test_msg)
+
+ # Check if there are any duplicates
+ labels = set(props['labels'])
+ self.assertEqual(sorted(props['labels']), sorted(list(labels)))
+
+ # Check the labels that shouldb always be included
+ self.assertIn('Restrict-View-Google'.lower(), labels)
+ self.assertIn(props['trooper_queue'], labels)
+ self.assertIn(props['incident_label'], labels)
+ self.assertIn(props['priority'], labels)
+ self.assertIn(props['issue_type'], labels)
+ self.assertSubset(set(props['oses']), labels)
+
+ # All the custom labels should be present.
+ self.assertSubset(set(expected_labels), labels)
diff --git a/features/test/autolink_test.py b/features/test/autolink_test.py
new file mode 100644
index 0000000..a779014
--- /dev/null
+++ b/features/test/autolink_test.py
@@ -0,0 +1,808 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unittest for the autolink feature."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import re
+import unittest
+
+from features import autolink
+from features import autolink_constants
+from framework import template_helpers
+from proto import tracker_pb2
+from testing import fake
+from testing import testing_helpers
+
+
+SIMPLE_EMAIL_RE = re.compile(r'([a-z]+)@([a-z]+)\.com')
+OVER_AMBITIOUS_DOMAIN_RE = re.compile(r'([a-z]+)\.(com|net|org)')
+
+
+class AutolinkTest(unittest.TestCase):
+
+ def RegisterEmailCallbacks(self, aa):
+
+ def LookupUsers(_mr, all_addresses):
+ """Return user objects for only users who are at trusted domains."""
+ return [addr for addr in all_addresses
+ if addr.endswith('@example.com')]
+
+ def Match2Addresses(_mr, match):
+ return [match.group(0)]
+
+ def MakeMailtoLink(_mr, match, comp_ref_artifacts):
+ email = match.group(0)
+ if comp_ref_artifacts and email in comp_ref_artifacts:
+ return [template_helpers.TextRun(
+ tag='a', href='mailto:%s' % email, content=email)]
+ else:
+ return [template_helpers.TextRun('%s AT %s.com' % match.group(1, 2))]
+
+ aa.RegisterComponent('testcomp',
+ LookupUsers,
+ Match2Addresses,
+ {SIMPLE_EMAIL_RE: MakeMailtoLink})
+
+ def RegisterDomainCallbacks(self, aa):
+
+ def LookupDomains(_mr, _all_refs):
+ """Return business objects for only real domains. Always just True."""
+ return True # We don't have domain business objects, accept anything.
+
+ def Match2Domains(_mr, match):
+ return [match.group(0)]
+
+ def MakeHyperLink(_mr, match, _comp_ref_artifacts):
+ domain = match.group(0)
+ return [template_helpers.TextRun(tag='a', href=domain, content=domain)]
+
+ aa.RegisterComponent('testcomp2',
+ LookupDomains,
+ Match2Domains,
+ {OVER_AMBITIOUS_DOMAIN_RE: MakeHyperLink})
+
+ def setUp(self):
+ self.aa = autolink.Autolink()
+ self.RegisterEmailCallbacks(self.aa)
+ self.comment1 = ('Feel free to contact me at a@other.com, '
+ 'or b@example.com, or c@example.org.')
+ self.comment2 = 'no matches in this comment'
+ self.comment3 = 'just matches with no ref: a@other.com, c@example.org'
+ self.comments = [self.comment1, self.comment2, self.comment3]
+
+ def testRegisterComponent(self):
+ self.assertIn('testcomp', self.aa.registry)
+
+ def testGetAllReferencedArtifacts(self):
+ all_ref_artifacts = self.aa.GetAllReferencedArtifacts(
+ None, self.comments)
+
+ self.assertIn('testcomp', all_ref_artifacts)
+ comp_refs = all_ref_artifacts['testcomp']
+ self.assertIn('b@example.com', comp_refs)
+ self.assertTrue(len(comp_refs) == 1)
+
+ def testGetAllReferencedArtifacts_TooBig(self):
+ all_ref_artifacts = self.aa.GetAllReferencedArtifacts(
+ None, self.comments, max_total_length=10)
+
+ self.assertEqual(autolink.SKIP_LOOKUPS, all_ref_artifacts)
+
+ def testMarkupAutolinks(self):
+ all_ref_artifacts = self.aa.GetAllReferencedArtifacts(None, self.comments)
+ result = self.aa.MarkupAutolinks(
+ None, [template_helpers.TextRun(self.comment1)], all_ref_artifacts)
+ self.assertEqual('Feel free to contact me at ', result[0].content)
+ self.assertEqual('a AT other.com', result[1].content)
+ self.assertEqual(', or ', result[2].content)
+ self.assertEqual('b@example.com', result[3].content)
+ self.assertEqual('mailto:b@example.com', result[3].href)
+ self.assertEqual(', or c@example.org.', result[4].content)
+
+ result = self.aa.MarkupAutolinks(
+ None, [template_helpers.TextRun(self.comment2)], all_ref_artifacts)
+ self.assertEqual('no matches in this comment', result[0].content)
+
+ result = self.aa.MarkupAutolinks(
+ None, [template_helpers.TextRun(self.comment3)], all_ref_artifacts)
+ self.assertEqual('just matches with no ref: ', result[0].content)
+ self.assertEqual('a AT other.com', result[1].content)
+ self.assertEqual(', c@example.org', result[2].content)
+
+ def testNonnestedAutolinks(self):
+ """Test that when a substitution yields plain text, others are applied."""
+ self.RegisterDomainCallbacks(self.aa)
+ all_ref_artifacts = self.aa.GetAllReferencedArtifacts(None, self.comments)
+ result = self.aa.MarkupAutolinks(
+ None, [template_helpers.TextRun(self.comment1)], all_ref_artifacts)
+ self.assertEqual('Feel free to contact me at ', result[0].content)
+ self.assertEqual('a AT ', result[1].content)
+ self.assertEqual('other.com', result[2].content)
+ self.assertEqual('other.com', result[2].href)
+ self.assertEqual(', or ', result[3].content)
+ self.assertEqual('b@example.com', result[4].content)
+ self.assertEqual('mailto:b@example.com', result[4].href)
+ self.assertEqual(', or c@', result[5].content)
+ self.assertEqual('example.org', result[6].content)
+ self.assertEqual('example.org', result[6].href)
+ self.assertEqual('.', result[7].content)
+
+ result = self.aa.MarkupAutolinks(
+ None, [template_helpers.TextRun(self.comment2)], all_ref_artifacts)
+ self.assertEqual('no matches in this comment', result[0].content)
+ result = self.aa.MarkupAutolinks(
+ None, [template_helpers.TextRun(self.comment3)], all_ref_artifacts)
+ self.assertEqual('just matches with no ref: ', result[0].content)
+ self.assertEqual('a AT ', result[1].content)
+ self.assertEqual('other.com', result[2].content)
+ self.assertEqual('other.com', result[2].href)
+ self.assertEqual(', c@', result[3].content)
+ self.assertEqual('example.org', result[4].content)
+ self.assertEqual('example.org', result[4].href)
+
+ def testMarkupAutolinks_TooBig(self):
+ """If the issue has too much text, we just do regex-based autolinking."""
+ all_ref_artifacts = self.aa.GetAllReferencedArtifacts(
+ None, self.comments, max_total_length=10)
+ result = self.aa.MarkupAutolinks(
+ None, [template_helpers.TextRun(self.comment1)], all_ref_artifacts)
+ self.assertEqual(5, len(result))
+ self.assertEqual('Feel free to contact me at ', result[0].content)
+ # The test autolink handlers in this file do not link email addresses.
+ self.assertEqual('a AT other.com', result[1].content)
+ self.assertIsNone(result[1].href)
+
+class EmailAutolinkTest(unittest.TestCase):
+
+ def setUp(self):
+ self.user_1 = 'fake user' # Note: no User fields are accessed.
+
+ def DoLinkify(
+ self, content, filter_re=autolink_constants.IS_IMPLIED_EMAIL_RE):
+ """Calls the LinkifyEmail method and returns the result.
+
+ Args:
+ content: string with a hyperlink.
+
+ Returns:
+ A list of TextRuns with some runs having the embedded email hyperlinked.
+ Or, None if no link was detected.
+ """
+ match = filter_re.search(content)
+ if not match:
+ return None
+
+ return autolink.LinkifyEmail(None, match, {'one@example.com': self.user_1})
+
+ def testLinkifyEmail(self):
+ """Test that an address is autolinked when put in the given context."""
+ test = 'one@ or @one'
+ result = self.DoLinkify('Have you met %s' % test)
+ self.assertEqual(None, result)
+
+ test = 'one@example.com'
+ result = self.DoLinkify('Have you met %s' % test)
+ self.assertEqual('/u/' + test, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+ test = 'alias@example.com'
+ result = self.DoLinkify('Please also CC %s' % test)
+ self.assertEqual('mailto:' + test, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+ result = self.DoLinkify('Reviewed-By: Test Person <%s>' % test)
+ self.assertEqual('mailto:' + test, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+
+class URLAutolinkTest(unittest.TestCase):
+
+ def DoLinkify(self, content, filter_re=autolink_constants.IS_A_LINK_RE):
+ """Calls the linkify method and returns the result.
+
+ Args:
+ content: string with a hyperlink.
+
+ Returns:
+ A list of TextRuns with some runs will have the embedded URL hyperlinked.
+ Or, None if no link was detected.
+ """
+ match = filter_re.search(content)
+ if not match:
+ return None
+
+ return autolink.Linkify(None, match, None)
+
+ def testLinkify(self):
+ """Test that given url is autolinked when put in the given context."""
+ # Disallow the linking of URLs with user names and passwords.
+ test = 'http://user:pass@www.yahoo.com'
+ result = self.DoLinkify('What about %s' % test)
+ self.assertEqual(None, result[0].tag)
+ self.assertEqual(None, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+ # Disallow the linking of non-HTTP(S) links
+ test = 'nntp://news.google.com'
+ result = self.DoLinkify('%s' % test)
+ self.assertEqual(None, result)
+
+ # Disallow the linking of file links
+ test = 'file://C:/Windows/System32/cmd.exe'
+ result = self.DoLinkify('%s' % test)
+ self.assertEqual(None, result)
+
+ # Test some known URLs
+ test = 'http://www.example.com'
+ result = self.DoLinkify('What about %s' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+ def testLinkify_FTP(self):
+ """Test that FTP urls are linked."""
+ # Check for a standard ftp link
+ test = 'ftp://ftp.example.com'
+ result = self.DoLinkify('%s' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+ def testLinkify_Email(self):
+ """Test that mailto: urls are linked."""
+ test = 'mailto:user@example.com'
+ result = self.DoLinkify('%s' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+ def testLinkify_ShortLink(self):
+ """Test that shortlinks are linked."""
+ test = 'http://go/monorail'
+ result = self.DoLinkify(
+ '%s' % test, filter_re=autolink_constants.IS_A_SHORT_LINK_RE)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+ test = 'go/monorail'
+ result = self.DoLinkify(
+ '%s' % test, filter_re=autolink_constants.IS_A_SHORT_LINK_RE)
+ self.assertEqual('http://' + test, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+ test = 'b/12345'
+ result = self.DoLinkify(
+ '%s' % test, filter_re=autolink_constants.IS_A_NUMERIC_SHORT_LINK_RE)
+ self.assertEqual('http://' + test, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+ test = 'http://b/12345'
+ result = self.DoLinkify(
+ '%s' % test, filter_re=autolink_constants.IS_A_NUMERIC_SHORT_LINK_RE)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+ test = '/b/12345'
+ result = self.DoLinkify(
+ '%s' % test, filter_re=autolink_constants.IS_A_SHORT_LINK_RE)
+ self.assertIsNone(result)
+
+ test = '/b/12345'
+ result = self.DoLinkify(
+ '%s' % test, filter_re=autolink_constants.IS_A_NUMERIC_SHORT_LINK_RE)
+ self.assertIsNone(result)
+
+ test = 'b/secondFileInDiff'
+ result = self.DoLinkify(
+ '%s' % test, filter_re=autolink_constants.IS_A_NUMERIC_SHORT_LINK_RE)
+ self.assertIsNone(result)
+
+ def testLinkify_ImpliedLink(self):
+ """Test that text with .com, .org, .net, and .edu are linked."""
+ test = 'google.org'
+ result = self.DoLinkify(
+ '%s' % test, filter_re=autolink_constants.IS_IMPLIED_LINK_RE)
+ self.assertEqual('http://' + test, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+ test = 'code.google.com/p/chromium'
+ result = self.DoLinkify(
+ '%s' % test, filter_re=autolink_constants.IS_IMPLIED_LINK_RE)
+ self.assertEqual('http://' + test, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+ # This is not a domain, it is a directory or something.
+ test = 'build.out/p/chromium'
+ result = self.DoLinkify(
+ '%s' % test, filter_re=autolink_constants.IS_IMPLIED_LINK_RE)
+ self.assertEqual(None, result)
+
+ # We do not link the NNTP scheme, and the domain name part of it will not
+ # be linked as an HTTP link because it is preceeded by "/".
+ test = 'nntp://news.google.com'
+ result = self.DoLinkify(
+ '%s' % test, filter_re=autolink_constants.IS_IMPLIED_LINK_RE)
+ self.assertIsNone(result)
+
+ def testLinkify_Context(self):
+ """Test that surrounding syntax is not considered part of the url."""
+ test = 'http://www.example.com'
+
+ # Check for a link followed by a comma at end of English phrase.
+ result = self.DoLinkify('The URL %s, points to a great website.' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual(',', result[1].content)
+
+ # Check for a link followed by a period at end of English sentence.
+ result = self.DoLinkify('The best site ever, %s.' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual('.', result[1].content)
+
+ # Check for a link in paranthesis (), [], or {}
+ result = self.DoLinkify('My fav site (%s).' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual(').', result[1].content)
+
+ result = self.DoLinkify('My fav site [%s].' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual('].', result[1].content)
+
+ result = self.DoLinkify('My fav site {%s}.' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual('}.', result[1].content)
+
+ # Check for a link with trailing colon
+ result = self.DoLinkify('Hit %s: you will love it.' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual(':', result[1].content)
+
+ # Check link with commas in query string, but don't include trailing comma.
+ test = 'http://www.example.com/?v=1,2,3'
+ result = self.DoLinkify('Try %s, ok?' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+ # Check link surrounded by angle-brackets.
+ result = self.DoLinkify('<%s>' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual('>', result[1].content)
+
+ # Check link surrounded by double-quotes.
+ result = self.DoLinkify('"%s"' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual('"', result[1].content)
+
+ # Check link with embedded double-quotes.
+ test = 'http://www.example.com/?q="a+b+c"'
+ result = self.DoLinkify('Try %s, ok?' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual(',', result[1].content)
+
+ # Check link surrounded by single-quotes.
+ result = self.DoLinkify("'%s'" % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual("'", result[1].content)
+
+ # Check link with embedded single-quotes.
+ test = "http://www.example.com/?q='a+b+c'"
+ result = self.DoLinkify('Try %s, ok?' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual(',', result[1].content)
+
+ # Check link with embedded parens.
+ test = 'http://www.example.com/funky(foo)and(bar).asp'
+ result = self.DoLinkify('Try %s, ok?' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual(',', result[1].content)
+
+ test = 'http://www.example.com/funky(foo)and(bar).asp'
+ result = self.DoLinkify('My fav site <%s>' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual('>', result[1].content)
+
+ # Check link with embedded brackets and braces.
+ test = 'http://www.example.com/funky[foo]and{bar}.asp'
+ result = self.DoLinkify('My fav site <%s>' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual('>', result[1].content)
+
+ # Check link with mismatched delimeters inside it or outside it.
+ test = 'http://www.example.com/funky"(foo]and>bar}.asp'
+ result = self.DoLinkify('My fav site <%s>' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual('>', result[1].content)
+
+ test = 'http://www.example.com/funky"(foo]and>bar}.asp'
+ result = self.DoLinkify('My fav site {%s' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+ test = 'http://www.example.com/funky"(foo]and>bar}.asp'
+ result = self.DoLinkify('My fav site %s}' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual('}', result[1].content)
+
+ # Link as part of an HTML example.
+ test = 'http://www.example.com/'
+ result = self.DoLinkify('<a href="%s">' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual('">', result[1].content)
+
+ # Link nested in an HTML tag.
+ result = self.DoLinkify('<span>%s</span>' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+ # Link followed by HTML tag - same bug as above.
+ result = self.DoLinkify('%s<span>foo</span>' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+ # Link followed by unescaped HTML tag.
+ result = self.DoLinkify('%s<span>foo</span>' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+ # Link surrounded by multiple delimiters.
+ result = self.DoLinkify('(e.g. <%s>)' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+ result = self.DoLinkify('(e.g. <%s>),' % test)
+ self.assertEqual(test, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+ def testLinkify_ContextOnBadLink(self):
+ """Test that surrounding text retained in cases where we don't link url."""
+ test = 'http://bad=example'
+ result = self.DoLinkify('<a href="%s">' % test)
+ self.assertEqual(None, result[0].href)
+ self.assertEqual(test + '">', result[0].content)
+ self.assertEqual(1, len(result))
+
+ def testLinkify_UnicodeContext(self):
+ """Test that unicode context does not mess up the link."""
+ test = 'http://www.example.com'
+
+ # This string has a non-breaking space \xa0.
+ result = self.DoLinkify(u'The correct RFC link is\xa0%s' % test)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual(test, result[0].href)
+
+ def testLinkify_UnicodeLink(self):
+ """Test that unicode in a link is OK."""
+ test = u'http://www.example.com?q=division\xc3\xb7sign'
+
+ # This string has a non-breaking space \xa0.
+ result = self.DoLinkify(u'The unicode link is %s' % test)
+ self.assertEqual(test, result[0].content)
+ self.assertEqual(test, result[0].href)
+
+ def testLinkify_LinkTextEscapingDisabled(self):
+ """Test that url-like things that miss validation aren't linked."""
+ # Link matched by the regex but not accepted by the validator.
+ test = 'http://bad_domain/reportdetail?reportid=35aa03e04772358b'
+ result = self.DoLinkify('<span>%s</span>' % test)
+ self.assertEqual(None, result[0].href)
+ self.assertEqual(test, result[0].content)
+
+
+def _Issue(project_name, local_id, summary, status):
+ issue = tracker_pb2.Issue()
+ issue.project_name = project_name
+ issue.local_id = local_id
+ issue.summary = summary
+ issue.status = status
+ return issue
+
+
+class TrackerAutolinkTest(unittest.TestCase):
+
+ COMMENT_TEXT = (
+ 'This relates to issue 1, issue #2, and issue3 \n'
+ 'as well as bug 4, bug #5, and bug6 \n'
+ 'with issue other-project:12 and issue other-project#13. \n'
+ 'Watch out for issues 21, 22, and 23 with oxford comma. \n'
+ 'And also bugs 31, 32 and 33 with no oxford comma.\n'
+ 'Here comes crbug.com/123 and crbug.com/monorail/456.\n'
+ 'We do not match when an issue\n'
+ '999. Is split across lines.'
+ )
+
+ def testExtractProjectAndIssueIdNormal(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/detail?id=1')
+ ref_batches = []
+ for match in autolink._ISSUE_REF_RE.finditer(self.COMMENT_TEXT):
+ new_refs = autolink.ExtractProjectAndIssueIdsNormal(mr, match)
+ ref_batches.append(new_refs)
+
+ self.assertEqual(
+ ref_batches, [
+ [(None, 1)],
+ [(None, 2)],
+ [(None, 3)],
+ [(None, 4)],
+ [(None, 5)],
+ [(None, 6)],
+ [('other-project', 12)],
+ [('other-project', 13)],
+ [(None, 21), (None, 22), (None, 23)],
+ [(None, 31), (None, 32), (None, 33)],
+ ])
+
+
+ def testExtractProjectAndIssueIdCrbug(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/issues/detail?id=1')
+ ref_batches = []
+ for match in autolink._CRBUG_REF_RE.finditer(self.COMMENT_TEXT):
+ new_refs = autolink.ExtractProjectAndIssueIdsCrBug(mr, match)
+ ref_batches.append(new_refs)
+
+ self.assertEqual(ref_batches, [
+ [('chromium', 123)],
+ [('monorail', 456)],
+ ])
+
+ def DoReplaceIssueRef(
+ self, content, regex=autolink._ISSUE_REF_RE,
+ single_issue_regex=autolink._SINGLE_ISSUE_REF_RE,
+ default_project_name=None):
+ """Calls the ReplaceIssueRef method and returns the result.
+
+ Args:
+ content: string that may have a textual reference to an issue.
+ regex: optional regex to use instead of _ISSUE_REF_RE.
+
+ Returns:
+ A list of TextRuns with some runs will have the reference hyperlinked.
+ Or, None if no reference detected.
+ """
+ match = regex.search(content)
+ if not match:
+ return None
+
+ open_dict = {'proj:1': _Issue('proj', 1, 'summary-PROJ-1', 'New'),
+ # Assume there is no issue 3 in PROJ
+ 'proj:4': _Issue('proj', 4, 'summary-PROJ-4', 'New'),
+ 'proj:6': _Issue('proj', 6, 'summary-PROJ-6', 'New'),
+ 'other-project:12': _Issue('other-project', 12,
+ 'summary-OP-12', 'Accepted'),
+ }
+ closed_dict = {'proj:2': _Issue('proj', 2, 'summary-PROJ-2', 'Fixed'),
+ 'proj:5': _Issue('proj', 5, 'summary-PROJ-5', 'Fixed'),
+ 'other-project:13': _Issue('other-project', 13,
+ 'summary-OP-12', 'Invalid'),
+ 'chromium:13': _Issue('chromium', 13,
+ 'summary-Cr-13', 'Invalid'),
+ }
+ comp_ref_artifacts = (open_dict, closed_dict,)
+
+ replacement_runs = autolink._ReplaceIssueRef(
+ match, comp_ref_artifacts, single_issue_regex, default_project_name)
+ return replacement_runs
+
+ def testReplaceIssueRef_NoMatch(self):
+ result = self.DoReplaceIssueRef('What is this all about?')
+ self.assertIsNone(result)
+
+ def testReplaceIssueRef_Normal(self):
+ result = self.DoReplaceIssueRef(
+ 'This relates to issue 1', default_project_name='proj')
+ self.assertEqual('/p/proj/issues/detail?id=1', result[0].href)
+ self.assertEqual('issue 1', result[0].content)
+ self.assertEqual(None, result[0].css_class)
+ self.assertEqual('summary-PROJ-1', result[0].title)
+ self.assertEqual('a', result[0].tag)
+
+ result = self.DoReplaceIssueRef(
+ ', issue #2', default_project_name='proj')
+ self.assertEqual('/p/proj/issues/detail?id=2', result[0].href)
+ self.assertEqual(' issue #2 ', result[0].content)
+ self.assertEqual('closed_ref', result[0].css_class)
+ self.assertEqual('summary-PROJ-2', result[0].title)
+ self.assertEqual('a', result[0].tag)
+
+ result = self.DoReplaceIssueRef(
+ ', and issue3 ', default_project_name='proj')
+ self.assertEqual(None, result[0].href) # There is no issue 3
+ self.assertEqual('issue3', result[0].content)
+
+ result = self.DoReplaceIssueRef(
+ 'as well as bug 4', default_project_name='proj')
+ self.assertEqual('/p/proj/issues/detail?id=4', result[0].href)
+ self.assertEqual('bug 4', result[0].content)
+
+ result = self.DoReplaceIssueRef(
+ ', bug #5, ', default_project_name='proj')
+ self.assertEqual('/p/proj/issues/detail?id=5', result[0].href)
+ self.assertEqual(' bug #5 ', result[0].content)
+
+ result = self.DoReplaceIssueRef(
+ 'and bug6', default_project_name='proj')
+ self.assertEqual('/p/proj/issues/detail?id=6', result[0].href)
+ self.assertEqual('bug6', result[0].content)
+
+ result = self.DoReplaceIssueRef(
+ 'with issue other-project:12', default_project_name='proj')
+ self.assertEqual('/p/other-project/issues/detail?id=12', result[0].href)
+ self.assertEqual('issue other-project:12', result[0].content)
+
+ result = self.DoReplaceIssueRef(
+ 'and issue other-project#13', default_project_name='proj')
+ self.assertEqual('/p/other-project/issues/detail?id=13', result[0].href)
+ self.assertEqual(' issue other-project#13 ', result[0].content)
+
+ def testReplaceIssueRef_CrBug(self):
+ result = self.DoReplaceIssueRef(
+ 'and crbug.com/other-project/13', regex=autolink._CRBUG_REF_RE,
+ single_issue_regex=autolink._CRBUG_REF_RE,
+ default_project_name='chromium')
+ self.assertEqual('/p/other-project/issues/detail?id=13', result[0].href)
+ self.assertEqual(' crbug.com/other-project/13 ', result[0].content)
+
+ result = self.DoReplaceIssueRef(
+ 'and http://crbug.com/13', regex=autolink._CRBUG_REF_RE,
+ single_issue_regex=autolink._CRBUG_REF_RE,
+ default_project_name='chromium')
+ self.assertEqual('/p/chromium/issues/detail?id=13', result[0].href)
+ self.assertEqual(' http://crbug.com/13 ', result[0].content)
+
+ result = self.DoReplaceIssueRef(
+ 'and http://crbug.com/13#c17', regex=autolink._CRBUG_REF_RE,
+ single_issue_regex=autolink._CRBUG_REF_RE,
+ default_project_name='chromium')
+ self.assertEqual('/p/chromium/issues/detail?id=13#c17', result[0].href)
+ self.assertEqual(' http://crbug.com/13#c17 ', result[0].content)
+
+ def testParseProjectNameMatch(self):
+ golden = 'project-name'
+ variations = ['%s', ' %s', '%s ', '%s:', '%s#', '%s#:', '%s:#', '%s :#',
+ '\t%s', '%s\t', '\t%s\t', '\t\t%s\t\t', '\n%s', '%s\n',
+ '\n%s\n', '\n\n%s\n\n', '\t\n%s', '\n\t%s', '%s\t\n',
+ '%s\n\t', '\t\n%s#', '\n\t%s#', '%s\t\n#', '%s\n\t#',
+ '\t\n%s:', '\n\t%s:', '%s\t\n:', '%s\n\t:'
+ ]
+
+ # First pass checks all valid project name results
+ for pattern in variations:
+ self.assertEqual(
+ golden, autolink._ParseProjectNameMatch(pattern % golden))
+
+ # Second pass tests all inputs that should result in None
+ for pattern in variations:
+ self.assertTrue(
+ autolink._ParseProjectNameMatch(pattern % '') in [None, ''])
+
+
+class VCAutolinkTest(unittest.TestCase):
+
+ GIT_HASH_1 = '1' * 40
+ GIT_HASH_2 = '2' * 40
+ GIT_HASH_3 = 'a1' * 20
+ GIT_COMMENT_TEXT = (
+ 'This is a fix for r%s and R%s, by r2d2, who also authored revision %s, '
+ 'revision #%s, revision %s, and revision %s' % (
+ GIT_HASH_1, GIT_HASH_2, GIT_HASH_3,
+ GIT_HASH_1.upper(), GIT_HASH_2.upper(), GIT_HASH_3.upper()))
+ SVN_COMMENT_TEXT = (
+ 'This is a fix for r12 and R3400, by r2d2, who also authored '
+ 'revision r4, '
+ 'revision #1234567, revision 789, and revision 9025. If you have '
+ 'questions, call me at 18005551212')
+
+ def testGetReferencedRevisions(self):
+ refs = ['1', '2', '3']
+ # For now, we do not look up revision objects, result is always None
+ self.assertIsNone(autolink.GetReferencedRevisions(None, refs))
+
+ def testExtractGitHashes(self):
+ refs = []
+ for match in autolink._GIT_HASH_RE.finditer(self.GIT_COMMENT_TEXT):
+ new_refs = autolink.ExtractRevNums(None, match)
+ refs.extend(new_refs)
+
+ self.assertEqual(
+ refs, [
+ self.GIT_HASH_1, self.GIT_HASH_2, self.GIT_HASH_3,
+ self.GIT_HASH_1.upper(),
+ self.GIT_HASH_2.upper(),
+ self.GIT_HASH_3.upper()
+ ])
+
+ def testExtractRevNums(self):
+ refs = []
+ for match in autolink._SVN_REF_RE.finditer(self.SVN_COMMENT_TEXT):
+ new_refs = autolink.ExtractRevNums(None, match)
+ refs.extend(new_refs)
+
+ # Note that we only autolink rNNNN with at least 4 digits.
+ self.assertEqual(refs, ['3400', '1234567', '9025'])
+
+
+ def DoReplaceRevisionRef(self, content, project=None):
+ """Calls the ReplaceRevisionRef method and returns the result.
+
+ Args:
+ content: string with a hyperlink.
+ project: optional project.
+
+ Returns:
+ A list of TextRuns with some runs will have the embedded URL hyperlinked.
+ Or, None if no link was detected.
+ """
+ match = autolink._GIT_HASH_RE.search(content)
+ if not match:
+ return None
+
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/p/proj/source/detail?r=1', project=project)
+ replacement_runs = autolink.ReplaceRevisionRef(mr, match, None)
+ return replacement_runs
+
+ def testReplaceRevisionRef(self):
+ result = self.DoReplaceRevisionRef(
+ 'This is a fix for r%s' % self.GIT_HASH_1)
+ self.assertEqual('https://crrev.com/%s' % self.GIT_HASH_1, result[0].href)
+ self.assertEqual('r%s' % self.GIT_HASH_1, result[0].content)
+
+ result = self.DoReplaceRevisionRef(
+ 'and R%s, by r2d2, who ' % self.GIT_HASH_2)
+ self.assertEqual('https://crrev.com/%s' % self.GIT_HASH_2, result[0].href)
+ self.assertEqual('R%s' % self.GIT_HASH_2, result[0].content)
+
+ result = self.DoReplaceRevisionRef('by r2d2, who ')
+ self.assertEqual(None, result)
+
+ result = self.DoReplaceRevisionRef(
+ 'also authored revision %s, ' % self.GIT_HASH_3)
+ self.assertEqual('https://crrev.com/%s' % self.GIT_HASH_3, result[0].href)
+ self.assertEqual('revision %s' % self.GIT_HASH_3, result[0].content)
+
+ result = self.DoReplaceRevisionRef(
+ 'revision #%s, ' % self.GIT_HASH_1.upper())
+ self.assertEqual(
+ 'https://crrev.com/%s' % self.GIT_HASH_1.upper(), result[0].href)
+ self.assertEqual(
+ 'revision #%s' % self.GIT_HASH_1.upper(), result[0].content)
+
+ result = self.DoReplaceRevisionRef(
+ 'revision %s, ' % self.GIT_HASH_2.upper())
+ self.assertEqual(
+ 'https://crrev.com/%s' % self.GIT_HASH_2.upper(), result[0].href)
+ self.assertEqual('revision %s' % self.GIT_HASH_2.upper(), result[0].content)
+
+ result = self.DoReplaceRevisionRef(
+ 'and revision %s' % self.GIT_HASH_3.upper())
+ self.assertEqual(
+ 'https://crrev.com/%s' % self.GIT_HASH_3.upper(), result[0].href)
+ self.assertEqual('revision %s' % self.GIT_HASH_3.upper(), result[0].content)
+
+ def testReplaceRevisionRef_CustomURL(self):
+ """A project can override the URL used for revision links."""
+ project = fake.Project()
+ project.revision_url_format = 'http://example.com/+/{revnum}'
+ result = self.DoReplaceRevisionRef(
+ 'This is a fix for r%s' % self.GIT_HASH_1, project=project)
+ self.assertEqual(
+ 'http://example.com/+/%s' % self.GIT_HASH_1, result[0].href)
+ self.assertEqual('r%s' % self.GIT_HASH_1, result[0].content)
diff --git a/features/test/banspammer_test.py b/features/test/banspammer_test.py
new file mode 100644
index 0000000..e6fceff
--- /dev/null
+++ b/features/test/banspammer_test.py
@@ -0,0 +1,141 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for the ban spammer feature."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import json
+import mock
+import os
+import unittest
+import urllib
+import webapp2
+
+import settings
+from features import banspammer
+from framework import framework_views
+from framework import permissions
+from framework import urls
+from proto import tracker_pb2
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+
+class BanSpammerTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = 'fake cnxn'
+ self.mr = testing_helpers.MakeMonorailRequest()
+ self.services = service_manager.Services(
+ issue=fake.IssueService(),
+ project=fake.ProjectService(),
+ spam=fake.SpamService(),
+ user=fake.UserService())
+ self.servlet = banspammer.BanSpammer('req', 'res', services=self.services)
+
+ @mock.patch('framework.cloud_tasks_helpers._get_client')
+ def testProcessFormData_noPermission(self, get_client_mock):
+ self.servlet.services.user.TestAddUser('member', 222)
+ self.servlet.services.user.TestAddUser('spammer@domain.com', 111)
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/u/spammer@domain.com/banSpammer.do',
+ perms=permissions.GetPermissions(None, {}, None))
+ mr.viewed_user_auth.user_view = framework_views.MakeUserView(mr.cnxn,
+ self.servlet.services.user, 111)
+ mr.auth.user_id = 222
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+ try:
+ self.servlet.ProcessFormData(mr, {})
+ except permissions.PermissionException:
+ pass
+ self.assertEqual(get_client_mock().queue_path.call_count, 0)
+ self.assertEqual(get_client_mock().create_task.call_count, 0)
+
+ @mock.patch('framework.cloud_tasks_helpers._get_client')
+ def testProcessFormData_ok(self, get_client_mock):
+ self.servlet.services.user.TestAddUser('owner', 222)
+ self.servlet.services.user.TestAddUser('spammer@domain.com', 111)
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/u/spammer@domain.com/banSpammer.do',
+ perms=permissions.ADMIN_PERMISSIONSET)
+ mr.viewed_user_auth.user_view = framework_views.MakeUserView(mr.cnxn,
+ self.servlet.services.user, 111)
+ mr.viewed_user_auth.user_pb.user_id = 111
+ mr.auth.user_id = 222
+ self.servlet.ProcessFormData(mr, {'banned': 'non-empty'})
+
+ params = {'spammer_id': 111, 'reporter_id': 222, 'is_spammer': True}
+ task = {
+ 'app_engine_http_request':
+ {
+ 'relative_uri': urls.BAN_SPAMMER_TASK + '.do',
+ 'body': urllib.urlencode(params),
+ 'headers': {
+ 'Content-type': 'application/x-www-form-urlencoded'
+ }
+ }
+ }
+ get_client_mock().queue_path.assert_called_with(
+ settings.app_id, settings.CLOUD_TASKS_REGION, 'default')
+ get_client_mock().create_task.assert_called_once()
+ ((_parent, called_task), _kwargs) = get_client_mock().create_task.call_args
+ self.assertEqual(called_task, task)
+
+
+class BanSpammerTaskTest(unittest.TestCase):
+ def setUp(self):
+ 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)
+
+ 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}))
+
+ def testProcessFormData_okSomeIssues(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ path=urls.BAN_SPAMMER_TASK + '.do', method='POST',
+ params={'spammer_id': 111, 'reporter_id': 222})
+
+ for i in range(0, 10):
+ issue = fake.MakeTestIssue(
+ 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}))
+
+ def testProcessFormData_okSomeCommentsAndIssues(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ path=urls.BAN_SPAMMER_TASK + '.do', method='POST',
+ params={'spammer_id': 111, 'reporter_id': 222})
+
+ for i in range(0, 12):
+ issue = fake.MakeTestIssue(
+ 1, i, 'issue_summary', 'New', 111, project_name='project-name')
+ self.servlet.services.issue.TestAddIssue(issue)
+
+ for i in range(10, 20):
+ issue = fake.MakeTestIssue(
+ 1, i, 'issue_summary', 'New', 222, project_name='project-name')
+ self.servlet.services.issue.TestAddIssue(issue)
+ for _ in range(0, 5):
+ comment = tracker_pb2.IssueComment()
+ comment.project_id = 1
+ 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}))
diff --git a/features/test/commands_test.py b/features/test/commands_test.py
new file mode 100644
index 0000000..e8bc47b
--- /dev/null
+++ b/features/test/commands_test.py
@@ -0,0 +1,230 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Classes and functions that implement command-line-like issue updates."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import unittest
+
+from features import commands
+from framework import framework_constants
+from proto import tracker_pb2
+from services import service_manager
+from testing import fake
+from tracker import tracker_bizobj
+from tracker import tracker_constants
+
+
+class CommandsTest(unittest.TestCase):
+
+ def VerifyParseQuickEditCommmand(
+ self, cmd, exp_summary='sum', exp_status='New', exp_owner_id=111,
+ exp_cc_ids=None, exp_labels=None):
+
+ issue = tracker_pb2.Issue()
+ issue.project_name = 'proj'
+ issue.local_id = 1
+ issue.summary = 'sum'
+ issue.status = 'New'
+ issue.owner_id = 111
+ issue.cc_ids.extend([222, 333])
+ issue.labels.extend(['Type-Defect', 'Priority-Medium', 'Hot'])
+
+ if exp_cc_ids is None:
+ exp_cc_ids = [222, 333]
+ if exp_labels is None:
+ exp_labels = ['Type-Defect', 'Priority-Medium', 'Hot']
+
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ logged_in_user_id = 999
+ services = service_manager.Services(
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService())
+ services.user.TestAddUser('jrobbins', 333)
+ services.user.TestAddUser('jrobbins@jrobbins.org', 888)
+
+ cnxn = 'fake cnxn'
+ (summary, status, owner_id, cc_ids,
+ labels) = commands.ParseQuickEditCommand(
+ cnxn, cmd, issue, config, logged_in_user_id, services)
+ self.assertEqual(exp_summary, summary)
+ self.assertEqual(exp_status, status)
+ self.assertEqual(exp_owner_id, owner_id)
+ self.assertListEqual(exp_cc_ids, cc_ids)
+ self.assertListEqual(exp_labels, labels)
+
+ def testParseQuickEditCommmand_Empty(self):
+ self.VerifyParseQuickEditCommmand('') # Nothing should change.
+
+ def testParseQuickEditCommmand_BuiltInFields(self):
+ self.VerifyParseQuickEditCommmand(
+ 'status=Fixed', exp_status='Fixed')
+ self.VerifyParseQuickEditCommmand( # Normalized capitalization.
+ 'status=fixed', exp_status='Fixed')
+ self.VerifyParseQuickEditCommmand(
+ 'status=limbo', exp_status='limbo')
+
+ self.VerifyParseQuickEditCommmand(
+ 'owner=me', exp_owner_id=999)
+ self.VerifyParseQuickEditCommmand(
+ 'owner=jrobbins@jrobbins.org', exp_owner_id=888)
+ self.VerifyParseQuickEditCommmand(
+ 'owner=----', exp_owner_id=framework_constants.NO_USER_SPECIFIED)
+
+ self.VerifyParseQuickEditCommmand(
+ 'summary=JustOneWord', exp_summary='JustOneWord')
+ self.VerifyParseQuickEditCommmand(
+ 'summary="quoted sentence"', exp_summary='quoted sentence')
+ self.VerifyParseQuickEditCommmand(
+ "summary='quoted sentence'", exp_summary='quoted sentence')
+
+ self.VerifyParseQuickEditCommmand(
+ 'cc=me', exp_cc_ids=[222, 333, 999])
+ self.VerifyParseQuickEditCommmand(
+ 'cc=jrobbins@jrobbins.org', exp_cc_ids=[222, 333, 888])
+ self.VerifyParseQuickEditCommmand(
+ 'cc=me,jrobbins@jrobbins.org',
+ exp_cc_ids=[222, 333, 999, 888])
+ self.VerifyParseQuickEditCommmand(
+ 'cc=-jrobbins,jrobbins@jrobbins.org',
+ exp_cc_ids=[222, 888])
+
+ def testParseQuickEditCommmand_Labels(self):
+ self.VerifyParseQuickEditCommmand(
+ 'Priority=Low', exp_labels=['Type-Defect', 'Hot', 'Priority-Low'])
+ self.VerifyParseQuickEditCommmand(
+ 'priority=low', exp_labels=['Type-Defect', 'Hot', 'Priority-Low'])
+ self.VerifyParseQuickEditCommmand(
+ 'priority-low', exp_labels=['Type-Defect', 'Hot', 'Priority-Low'])
+ self.VerifyParseQuickEditCommmand(
+ '-priority-low', exp_labels=['Type-Defect', 'Priority-Medium', 'Hot'])
+ self.VerifyParseQuickEditCommmand(
+ '-priority-medium', exp_labels=['Type-Defect', 'Hot'])
+
+ self.VerifyParseQuickEditCommmand(
+ 'Cold', exp_labels=['Type-Defect', 'Priority-Medium', 'Hot', 'Cold'])
+ self.VerifyParseQuickEditCommmand(
+ '+Cold', exp_labels=['Type-Defect', 'Priority-Medium', 'Hot', 'Cold'])
+ self.VerifyParseQuickEditCommmand(
+ '-Hot Cold', exp_labels=['Type-Defect', 'Priority-Medium', 'Cold'])
+ self.VerifyParseQuickEditCommmand(
+ '-Hot', exp_labels=['Type-Defect', 'Priority-Medium'])
+
+ def testParseQuickEditCommmand_Multiple(self):
+ self.VerifyParseQuickEditCommmand(
+ 'Priority=Low -hot owner:me cc:-jrobbins summary="other summary"',
+ exp_summary='other summary', exp_owner_id=999,
+ exp_cc_ids=[222], exp_labels=['Type-Defect', 'Priority-Low'])
+
+ def testBreakCommandIntoParts_Empty(self):
+ self.assertListEqual(
+ [],
+ commands._BreakCommandIntoParts(''))
+
+ def testBreakCommandIntoParts_Single(self):
+ self.assertListEqual(
+ [('summary', 'new summary')],
+ commands._BreakCommandIntoParts('summary="new summary"'))
+ self.assertListEqual(
+ [('summary', 'OneWordSummary')],
+ commands._BreakCommandIntoParts('summary=OneWordSummary'))
+ self.assertListEqual(
+ [('key', 'value')],
+ commands._BreakCommandIntoParts('key=value'))
+ self.assertListEqual(
+ [('key', 'value-with-dashes')],
+ commands._BreakCommandIntoParts('key=value-with-dashes'))
+ self.assertListEqual(
+ [('key', 'value')],
+ commands._BreakCommandIntoParts('key:value'))
+ self.assertListEqual(
+ [('key', 'value')],
+ commands._BreakCommandIntoParts(' key:value '))
+ self.assertListEqual(
+ [('key', 'value')],
+ commands._BreakCommandIntoParts('key:"value"'))
+ self.assertListEqual(
+ [('key', 'user@dom.com')],
+ commands._BreakCommandIntoParts('key:user@dom.com'))
+ self.assertListEqual(
+ [('key', 'a@dom.com,-b@dom.com')],
+ commands._BreakCommandIntoParts('key:a@dom.com,-b@dom.com'))
+ self.assertListEqual(
+ [(None, 'label')],
+ commands._BreakCommandIntoParts('label'))
+ self.assertListEqual(
+ [(None, '-label')],
+ commands._BreakCommandIntoParts('-label'))
+ self.assertListEqual(
+ [(None, '+label')],
+ commands._BreakCommandIntoParts('+label'))
+
+ def testBreakCommandIntoParts_Multiple(self):
+ self.assertListEqual(
+ [('summary', 'new summary'), (None, 'Hot'), (None, '-Cold'),
+ ('owner', 'me'), ('cc', '+a,-b')],
+ commands._BreakCommandIntoParts(
+ 'summary="new summary" Hot -Cold owner:me cc:+a,-b'))
+
+
+class CommandSyntaxParsingTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ user=fake.UserService())
+
+ self.services.project.TestAddProject('proj', owner_ids=[111])
+ self.services.user.TestAddUser('a@example.com', 222)
+
+ cnxn = 'fake connection'
+ config = self.services.config.GetProjectConfig(cnxn, 789)
+
+ for status in ['New', 'ReadyForReview']:
+ config.well_known_statuses.append(tracker_pb2.StatusDef(
+ status=status))
+
+ for label in ['Prioity-Low', 'Priority-High']:
+ config.well_known_labels.append(tracker_pb2.LabelDef(
+ label=label))
+
+ config.exclusive_label_prefixes.extend(
+ tracker_constants.DEFAULT_EXCL_LABEL_PREFIXES)
+
+ self.services.config.StoreConfig(cnxn, config)
+
+ def testStandardizeStatus(self):
+ config = self.services.config.GetProjectConfig('fake cnxn', 789)
+ self.assertEqual('New',
+ commands._StandardizeStatus('NEW', config))
+ self.assertEqual('New',
+ commands._StandardizeStatus('n$Ew ', config))
+ self.assertEqual(
+ 'custom-label',
+ commands._StandardizeLabel('custom=label ', config))
+
+ def testStandardizeLabel(self):
+ config = self.services.config.GetProjectConfig('fake cnxn', 789)
+ self.assertEqual(
+ 'Priority-High',
+ commands._StandardizeLabel('priority-high', config))
+ self.assertEqual(
+ 'Priority-High',
+ commands._StandardizeLabel('PRIORITY=HIGH', config))
+
+ def testLookupMeOrUsername(self):
+ self.assertEqual(
+ 123,
+ commands._LookupMeOrUsername('fake cnxn', 'me', self.services, 123))
+
+ self.assertEqual(
+ 222,
+ commands._LookupMeOrUsername(
+ 'fake cnxn', 'a@example.com', self.services, 0))
diff --git a/features/test/commitlogcommands_test.py b/features/test/commitlogcommands_test.py
new file mode 100644
index 0000000..7e5d566
--- /dev/null
+++ b/features/test/commitlogcommands_test.py
@@ -0,0 +1,111 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unittests for monorail.features.commitlogcommands."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mock
+import unittest
+
+from features import commitlogcommands
+from features import send_notifications
+from framework import monorailcontext
+from proto import tracker_pb2
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+
+
+class InboundEmailTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = 'fake cnxn'
+ self.services = service_manager.Services(
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService(),
+ config=fake.ConfigService())
+
+ self.member = self.services.user.TestAddUser('member@example.com', 111)
+ self.outsider = self.services.user.TestAddUser('outsider@example.com', 222)
+ self.project = self.services.project.TestAddProject(
+ 'proj', project_id=987, process_inbound_email=True,
+ committer_ids=[self.member.user_id])
+ self.issue = tracker_pb2.Issue()
+ self.issue.issue_id = 98701
+ self.issue.project_id = 987
+ self.issue.local_id = 1
+ self.issue.owner_id = 0
+ self.issue.summary = 'summary'
+ self.issue.status = 'Assigned'
+ self.services.issue.TestAddIssue(self.issue)
+
+ self.uia = commitlogcommands.UpdateIssueAction(self.issue.local_id)
+
+ def testParse_NoCommandLines(self):
+ commands_found = self.uia.Parse(self.cnxn, self.project.project_name, 111,
+ ['line 1'], self.services,
+ hostport='testing-app.appspot.com', strip_quoted_lines=True)
+ self.assertEqual(False, commands_found)
+ self.assertEqual('line 1', self.uia.description)
+ self.assertEqual('line 1', self.uia.inbound_message)
+
+ def testParse_StripQuotedLines(self):
+ commands_found = self.uia.Parse(self.cnxn, self.project.project_name, 111,
+ ['summary:something', '> line 1', 'line 2'], self.services,
+ hostport='testing-app.appspot.com', strip_quoted_lines=True)
+ self.assertEqual(True, commands_found)
+ self.assertEqual('line 2', self.uia.description)
+ self.assertEqual(
+ 'summary:something\n> line 1\nline 2', self.uia.inbound_message)
+
+ def testParse_NoStripQuotedLines(self):
+ commands_found = self.uia.Parse(self.cnxn, self.project.project_name, 111,
+ ['summary:something', '> line 1', 'line 2'], self.services,
+ hostport='testing-app.appspot.com')
+ self.assertEqual(True, commands_found)
+ self.assertEqual('> line 1\nline 2', self.uia.description)
+ self.assertIsNone(self.uia.inbound_message)
+
+ def setupAndCallRun(self, mc, commenter_id, mock_pasicn):
+ self.uia.Parse(self.cnxn, self.project.project_name, 111,
+ ['summary:something', 'status:New', '> line 1', '> line 2'],
+ self.services, hostport='testing-app.appspot.com')
+ self.uia.Run(mc, self.services)
+
+ mock_pasicn.assert_called_once_with(
+ self.issue.issue_id, 'testing-app.appspot.com', commenter_id,
+ old_owner_id=self.issue.owner_id, comment_id=1, send_email=True)
+
+ @mock.patch(
+ 'features.send_notifications.PrepareAndSendIssueChangeNotification')
+ def testRun_AllowEdit(self, mock_pasicn):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='member@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ self.setupAndCallRun(mc, 111, mock_pasicn)
+
+ self.assertEqual('> line 1\n> line 2', self.uia.description)
+ # Assert that amendments were made to the issue.
+ self.assertEqual('something', self.issue.summary)
+ self.assertEqual('New', self.issue.status)
+
+ @mock.patch(
+ 'features.send_notifications.PrepareAndSendIssueChangeNotification')
+ def testRun_NoAllowEdit(self, mock_pasicn):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='outsider@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ self.setupAndCallRun(mc, 222, mock_pasicn)
+
+ self.assertEqual('> line 1\n> line 2', self.uia.description)
+ # Assert that amendments were *not* made to the issue.
+ self.assertEqual('summary', self.issue.summary)
+ self.assertEqual('Assigned', self.issue.status)
diff --git a/features/test/component_helpers_test.py b/features/test/component_helpers_test.py
new file mode 100644
index 0000000..aa6c761
--- /dev/null
+++ b/features/test/component_helpers_test.py
@@ -0,0 +1,145 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for component prediction endpoints."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import json
+import mock
+import sys
+import unittest
+
+from services import service_manager
+from testing import fake
+
+# Mock cloudstorage before it's imported by component_helpers
+sys.modules['cloudstorage'] = mock.Mock()
+from features import component_helpers
+
+
+class FakeMLEngine(object):
+ def __init__(self, test):
+ self.test = test
+ self.expected_features = None
+ self.scores = None
+ self._execute_response = None
+
+ def projects(self):
+ return self
+
+ def models(self):
+ return self
+
+ def predict(self, name, body):
+ self.test.assertEqual(component_helpers.MODEL_NAME, name)
+ self.test.assertEqual(
+ {'instances': [{'inputs': self.expected_features}]}, body)
+ self._execute_response = {'predictions': [{'scores': self.scores}]}
+ return self
+
+ def get(self, name):
+ self.test.assertEqual(component_helpers.MODEL_NAME, name)
+ self._execute_response = {'defaultVersion': {'name': 'v_1234'}}
+ return self
+
+ def execute(self):
+ response = self._execute_response
+ self._execute_response = None
+ return response
+
+
+class ComponentHelpersTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ user=fake.UserService())
+ self.project = fake.Project(project_name='proj')
+
+ self._ml_engine = FakeMLEngine(self)
+ self._top_words = None
+ self._components_by_index = None
+
+ mock.patch(
+ 'services.ml_helpers.setup_ml_engine', lambda: self._ml_engine).start()
+ mock.patch(
+ 'features.component_helpers._GetTopWords',
+ lambda _: self._top_words).start()
+ mock.patch('cloudstorage.open', self.cloudstorageOpen).start()
+ mock.patch('settings.component_features', 5).start()
+
+ self.addCleanup(mock.patch.stopall)
+
+ def cloudstorageOpen(self, name, mode):
+ """Create a file mock that returns self._components_by_index when read."""
+ open_fn = mock.mock_open(read_data=json.dumps(self._components_by_index))
+ return open_fn(name, mode)
+
+ def testPredict_Normal(self):
+ """Test normal case when predicted component exists."""
+ component_id = self.services.config.CreateComponentDef(
+ cnxn=None, project_id=self.project.project_id, path='Ruta>Baga',
+ docstring='', deprecated=False, admin_ids=[], cc_ids=[], created=None,
+ creator_id=None, label_ids=[])
+ config = self.services.config.GetProjectConfig(
+ None, self.project.project_id)
+
+ self._top_words = {
+ 'foo': 0,
+ 'bar': 1,
+ 'baz': 2}
+ self._components_by_index = {
+ '0': '123',
+ '1': str(component_id),
+ '2': '789'}
+ self._ml_engine.expected_features = [3, 0, 1, 0, 0]
+ self._ml_engine.scores = [5, 10, 3]
+
+ text = 'foo baz foo foo'
+
+ self.assertEqual(
+ component_id, component_helpers.PredictComponent(text, config))
+
+ def testPredict_UnknownComponentIndex(self):
+ """Test case where the prediction is not in components_by_index."""
+ config = self.services.config.GetProjectConfig(
+ None, self.project.project_id)
+
+ self._top_words = {
+ 'foo': 0,
+ 'bar': 1,
+ 'baz': 2}
+ self._components_by_index = {
+ '0': '123',
+ '1': '456',
+ '2': '789'}
+ self._ml_engine.expected_features = [3, 0, 1, 0, 0]
+ self._ml_engine.scores = [5, 10, 3, 1000]
+
+ text = 'foo baz foo foo'
+
+ self.assertIsNone(component_helpers.PredictComponent(text, config))
+
+ def testPredict_InvalidComponentIndex(self):
+ """Test case where the prediction is not a valid component id."""
+ config = self.services.config.GetProjectConfig(
+ None, self.project.project_id)
+
+ self._top_words = {
+ 'foo': 0,
+ 'bar': 1,
+ 'baz': 2}
+ self._components_by_index = {
+ '0': '123',
+ '1': '456',
+ '2': '789'}
+ self._ml_engine.expected_features = [3, 0, 1, 0, 0]
+ self._ml_engine.scores = [5, 10, 3]
+
+ text = 'foo baz foo foo'
+
+ self.assertIsNone(component_helpers.PredictComponent(text, config))
diff --git a/features/test/componentexport_test.py b/features/test/componentexport_test.py
new file mode 100644
index 0000000..0e5fbf8
--- /dev/null
+++ b/features/test/componentexport_test.py
@@ -0,0 +1,42 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Tests for the componentexport module."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import mock
+import unittest
+import webapp2
+
+import settings
+from features import componentexport
+from framework import urls
+
+
+class ComponentTrainingDataExportTest(unittest.TestCase):
+
+ def test_handler_definition(self):
+ instance = componentexport.ComponentTrainingDataExport()
+ self.assertIsInstance(instance, webapp2.RequestHandler)
+
+ @mock.patch('framework.cloud_tasks_helpers._get_client')
+ def test_enqueues_task(self, get_client_mock):
+ componentexport.ComponentTrainingDataExport().get()
+
+ queue = 'componentexport'
+ task = {
+ 'app_engine_http_request':
+ {
+ 'http_method': 'GET',
+ 'relative_uri': urls.COMPONENT_DATA_EXPORT_TASK
+ }
+ }
+
+ get_client_mock().queue_path.assert_called_with(
+ settings.app_id, settings.CLOUD_TASKS_REGION, queue)
+ get_client_mock().create_task.assert_called_once()
+ ((_parent, called_task), _kwargs) = get_client_mock().create_task.call_args
+ self.assertEqual(called_task, task)
diff --git a/features/test/dateaction_test.py b/features/test/dateaction_test.py
new file mode 100644
index 0000000..09e5c5c
--- /dev/null
+++ b/features/test/dateaction_test.py
@@ -0,0 +1,323 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unittest for the dateaction module."""
+
+from __future__ import division
+from __future__ import print_function
+from __future__ import absolute_import
+
+import logging
+import mock
+import time
+import unittest
+
+from features import dateaction
+from framework import cloud_tasks_helpers
+from framework import framework_constants
+from framework import framework_views
+from framework import timestr
+from framework import urls
+from proto import tracker_pb2
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+from tracker import tracker_bizobj
+
+
+NOW = 1492120863
+
+
+class DateActionCronTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ issue=fake.IssueService())
+ self.servlet = dateaction.DateActionCron(
+ 'req', 'res', services=self.services)
+ self.TIMESTAMP_MIN = (
+ NOW // framework_constants.SECS_PER_DAY *
+ framework_constants.SECS_PER_DAY)
+ self.TIMESTAMP_MAX = self.TIMESTAMP_MIN + framework_constants.SECS_PER_DAY
+ self.left_joins = [
+ ('Issue2FieldValue ON Issue.id = Issue2FieldValue.issue_id', []),
+ ('FieldDef ON Issue2FieldValue.field_id = FieldDef.id', []),
+ ]
+ self.where = [
+ ('FieldDef.field_type = %s', ['date_type']),
+ (
+ 'FieldDef.date_action IN (%s,%s)',
+ ['ping_owner_only', 'ping_participants']),
+ ('Issue2FieldValue.date_value >= %s', [self.TIMESTAMP_MIN]),
+ ('Issue2FieldValue.date_value < %s', [self.TIMESTAMP_MAX]),
+ ]
+ self.order_by = [
+ ('Issue.id', []),
+ ]
+
+ @mock.patch('time.time', return_value=NOW)
+ def testHandleRequest_NoMatches(self, _mock_time):
+ _request, mr = testing_helpers.GetRequestObjects(
+ path=urls.DATE_ACTION_CRON)
+ self.services.issue.RunIssueQuery = mock.MagicMock(return_value=([], False))
+
+ self.servlet.HandleRequest(mr)
+
+ self.services.issue.RunIssueQuery.assert_called_with(
+ mr.cnxn, self.left_joins, self.where + [('Issue.id > %s', [0])],
+ self.order_by)
+
+ @mock.patch('framework.cloud_tasks_helpers._get_client')
+ @mock.patch('time.time', return_value=NOW)
+ def testHandleRequest_OneMatche(self, _mock_time, get_client_mock):
+ _request, mr = testing_helpers.GetRequestObjects(
+ path=urls.DATE_ACTION_CRON)
+ self.services.issue.RunIssueQuery = mock.MagicMock(
+ return_value=([78901], False))
+
+ self.servlet.HandleRequest(mr)
+
+ self.services.issue.RunIssueQuery.assert_called_with(
+ mr.cnxn, self.left_joins, self.where + [('Issue.id > %s', [0])],
+ self.order_by)
+ expected_task = {
+ 'app_engine_http_request':
+ {
+ 'relative_uri': urls.ISSUE_DATE_ACTION_TASK + '.do',
+ 'body': 'issue_id=78901',
+ 'headers': {
+ 'Content-type': 'application/x-www-form-urlencoded'
+ }
+ }
+ }
+ get_client_mock().create_task.assert_any_call(
+ get_client_mock().queue_path(),
+ expected_task,
+ retry=cloud_tasks_helpers._DEFAULT_RETRY)
+
+ @mock.patch('framework.cloud_tasks_helpers._get_client')
+ def testEnqueueDateAction(self, get_client_mock):
+ self.servlet.EnqueueDateAction(78901)
+ expected_task = {
+ 'app_engine_http_request':
+ {
+ 'relative_uri': urls.ISSUE_DATE_ACTION_TASK + '.do',
+ 'body': 'issue_id=78901',
+ 'headers': {
+ 'Content-type': 'application/x-www-form-urlencoded'
+ }
+ }
+ }
+ get_client_mock().create_task.assert_any_call(
+ get_client_mock().queue_path(),
+ expected_task,
+ retry=cloud_tasks_helpers._DEFAULT_RETRY)
+
+
+class IssueDateActionTaskTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ features=fake.FeaturesService(),
+ issue=fake.IssueService(),
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ issue_star=fake.IssueStarService())
+ self.servlet = dateaction.IssueDateActionTask(
+ 'req', 'res', services=self.services)
+
+ self.config = self.services.config.GetProjectConfig('cnxn', 789)
+ self.config.field_defs = [
+ tracker_bizobj.MakeFieldDef(
+ 123, 789, 'NextAction', tracker_pb2.FieldTypes.DATE_TYPE,
+ '', '', False, False, False, None, None, None, False, '',
+ None, None, tracker_pb2.DateAction.PING_OWNER_ONLY,
+ 'Date of next expected progress update', False),
+ tracker_bizobj.MakeFieldDef(
+ 124, 789, 'EoL', tracker_pb2.FieldTypes.DATE_TYPE,
+ '', '', False, False, False, None, None, None, False, '',
+ None, None, tracker_pb2.DateAction.PING_OWNER_ONLY, 'doc', False),
+ tracker_bizobj.MakeFieldDef(
+ 125, 789, 'TLsBirthday', tracker_pb2.FieldTypes.DATE_TYPE,
+ '', '', False, False, False, None, None, None, False, '',
+ None, None, tracker_pb2.DateAction.NO_ACTION, 'doc', False),
+ ]
+ self.services.config.StoreConfig('cnxn', self.config)
+ self.project = self.services.project.TestAddProject('proj', project_id=789)
+ self.owner = self.services.user.TestAddUser('owner@example.com', 111)
+ self.date_action_user = self.services.user.TestAddUser(
+ 'date-action-user@example.com', 555)
+
+ def testHandleRequest_IssueHasNoArrivedDates(self):
+ _request, mr = testing_helpers.GetRequestObjects(
+ path=urls.ISSUE_DATE_ACTION_TASK + '.do?issue_id=78901')
+ self.services.issue.TestAddIssue(fake.MakeTestIssue(
+ 789, 1, 'summary', 'New', 111, issue_id=78901))
+ self.assertEqual(1, len(self.services.issue.GetCommentsForIssue(
+ mr.cnxn, 78901)))
+
+ self.servlet.HandleRequest(mr)
+ self.assertEqual(1, len(self.services.issue.GetCommentsForIssue(
+ mr.cnxn, 78901)))
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testHandleRequest_IssueHasOneArriveDate(self, create_task_mock):
+ _request, mr = testing_helpers.GetRequestObjects(
+ path=urls.ISSUE_DATE_ACTION_TASK + '.do?issue_id=78901')
+
+ now = int(time.time())
+ date_str = timestr.TimestampToDateWidgetStr(now)
+ issue = fake.MakeTestIssue(789, 1, 'summary', 'New', 111, issue_id=78901)
+ self.services.issue.TestAddIssue(issue)
+ issue.field_values = [
+ tracker_bizobj.MakeFieldValue(123, None, None, None, now, None, False)]
+ self.assertEqual(1, len(self.services.issue.GetCommentsForIssue(
+ mr.cnxn, 78901)))
+
+ self.servlet.HandleRequest(mr)
+ comments = self.services.issue.GetCommentsForIssue(mr.cnxn, 78901)
+ self.assertEqual(2, len(comments))
+ self.assertEqual(
+ 'The NextAction date has arrived: %s' % date_str,
+ comments[1].content)
+
+ self.assertEqual(create_task_mock.call_count, 1)
+
+ (args, kwargs) = create_task_mock.call_args
+ self.assertEqual(
+ args[0]['app_engine_http_request']['relative_uri'],
+ urls.OUTBOUND_EMAIL_TASK + '.do')
+ self.assertEqual(kwargs['queue'], 'outboundemail')
+
+ def SetUpFieldValues(self, issue, now):
+ issue.field_values = [
+ tracker_bizobj.MakeFieldValue(123, None, None, None, now, None, False),
+ tracker_bizobj.MakeFieldValue(124, None, None, None, now, None, False),
+ tracker_bizobj.MakeFieldValue(125, None, None, None, now, None, False),
+ ]
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testHandleRequest_IssueHasTwoArriveDates(self, create_task_mock):
+ _request, mr = testing_helpers.GetRequestObjects(
+ path=urls.ISSUE_DATE_ACTION_TASK + '.do?issue_id=78901')
+
+ now = int(time.time())
+ date_str = timestr.TimestampToDateWidgetStr(now)
+ issue = fake.MakeTestIssue(789, 1, 'summary', 'New', 111, issue_id=78901)
+ self.services.issue.TestAddIssue(issue)
+ self.SetUpFieldValues(issue, now)
+ self.assertEqual(1, len(self.services.issue.GetCommentsForIssue(
+ mr.cnxn, 78901)))
+
+ self.servlet.HandleRequest(mr)
+ comments = self.services.issue.GetCommentsForIssue(mr.cnxn, 78901)
+ self.assertEqual(2, len(comments))
+ self.assertEqual(
+ 'The EoL date has arrived: %s\n'
+ 'The NextAction date has arrived: %s' % (date_str, date_str),
+ comments[1].content)
+
+ self.assertEqual(create_task_mock.call_count, 1)
+
+ (args, kwargs) = create_task_mock.call_args
+ self.assertEqual(
+ args[0]['app_engine_http_request']['relative_uri'],
+ urls.OUTBOUND_EMAIL_TASK + '.do')
+ self.assertEqual(kwargs['queue'], 'outboundemail')
+
+ def MakePingComment(self):
+ comment = tracker_pb2.IssueComment()
+ comment.project_id = self.project.project_id
+ comment.user_id = self.date_action_user.user_id
+ comment.content = 'Some date(s) arrived...'
+ return comment
+
+ def testMakeEmailTasks_Owner(self):
+ """The issue owner gets pinged and the email has expected content."""
+ issue = fake.MakeTestIssue(
+ 789, 1, 'summary', 'New', self.owner.user_id, issue_id=78901)
+ self.services.issue.TestAddIssue(issue)
+ now = int(time.time())
+ self.SetUpFieldValues(issue, now)
+ issue.project_name = 'proj'
+ comment = self.MakePingComment()
+ next_action_field_def = self.config.field_defs[0]
+ pings = [(next_action_field_def, now)]
+ users_by_id = framework_views.MakeAllUserViews(
+ 'fake cnxn', self.services.user,
+ [self.owner.user_id, self.date_action_user.user_id])
+
+ tasks = self.servlet._MakeEmailTasks(
+ 'fake cnxn', issue, self.project, self.config, comment,
+ [], 'example-app.appspot.com', users_by_id, pings)
+ self.assertEqual(1, len(tasks))
+ notify_owner_task = tasks[0]
+ self.assertEqual('owner@example.com', notify_owner_task['to'])
+ self.assertEqual(
+ 'Follow up on issue 1 in proj: summary',
+ notify_owner_task['subject'])
+ body = notify_owner_task['body']
+ self.assertIn(comment.content, body)
+ self.assertIn(next_action_field_def.docstring, body)
+
+ def testMakeEmailTasks_Starrer(self):
+ """Users who starred the issue are notified iff they opt in."""
+ issue = fake.MakeTestIssue(
+ 789, 1, 'summary', 'New', 0, issue_id=78901)
+ self.services.issue.TestAddIssue(issue)
+ now = int(time.time())
+ self.SetUpFieldValues(issue, now)
+ issue.project_name = 'proj'
+ comment = self.MakePingComment()
+ next_action_field_def = self.config.field_defs[0]
+ pings = [(next_action_field_def, now)]
+
+ starrer_333 = self.services.user.TestAddUser('starrer333@example.com', 333)
+ starrer_333.notify_starred_ping = True
+ self.services.user.TestAddUser('starrer444@example.com', 444)
+ starrer_ids = [333, 444]
+ users_by_id = framework_views.MakeAllUserViews(
+ 'fake cnxn', self.services.user,
+ [self.owner.user_id, self.date_action_user.user_id],
+ starrer_ids)
+
+ tasks = self.servlet._MakeEmailTasks(
+ 'fake cnxn', issue, self.project, self.config, comment,
+ starrer_ids, 'example-app.appspot.com', users_by_id, pings)
+ self.assertEqual(1, len(tasks))
+ notify_owner_task = tasks[0]
+ self.assertEqual('starrer333@example.com', notify_owner_task['to'])
+
+ def testCalculateIssuePings_Normal(self):
+ """Return a ping for an issue that has a date that happened today."""
+ issue = fake.MakeTestIssue(
+ 789, 1, 'summary', 'New', 0, issue_id=78901)
+ self.services.issue.TestAddIssue(issue)
+ now = int(time.time())
+ self.SetUpFieldValues(issue, now)
+ issue.project_name = 'proj'
+
+ pings = self.servlet._CalculateIssuePings(issue, self.config)
+
+ self.assertEqual(
+ [(self.config.field_defs[1], now),
+ (self.config.field_defs[0], now)],
+ pings)
+
+ def testCalculateIssuePings_Closed(self):
+ """Don't ping for a closed issue."""
+ issue = fake.MakeTestIssue(
+ 789, 1, 'summary', 'Fixed', 0, issue_id=78901)
+ self.services.issue.TestAddIssue(issue)
+ now = int(time.time())
+ self.SetUpFieldValues(issue, now)
+ issue.project_name = 'proj'
+
+ pings = self.servlet._CalculateIssuePings(issue, self.config)
+
+ self.assertEqual([], pings)
diff --git a/features/test/features_bizobj_test.py b/features/test/features_bizobj_test.py
new file mode 100644
index 0000000..1814ae2
--- /dev/null
+++ b/features/test/features_bizobj_test.py
@@ -0,0 +1,134 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for features bizobj functions."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from proto import features_pb2
+from features import features_bizobj
+from testing import fake
+
+class FeaturesBizobjTest(unittest.TestCase):
+
+ def setUp(self):
+ self.local_ids = [1, 2, 3, 4, 5]
+ self.issues = [fake.MakeTestIssue(1000, local_id, '', 'New', 111)
+ for local_id in self.local_ids]
+ self.hotlistitems = [features_pb2.MakeHotlistItem(
+ issue.issue_id, rank=rank*10, adder_id=111, date_added=3) for
+ rank, issue in enumerate(self.issues)]
+ self.iids = [item.issue_id for item in self.hotlistitems]
+
+ def testIssueIsInHotlist(self):
+ hotlist = features_pb2.Hotlist(items=self.hotlistitems)
+ for issue in self.issues:
+ self.assertTrue(features_bizobj.IssueIsInHotlist(hotlist, issue.issue_id))
+
+ self.assertFalse(features_bizobj.IssueIsInHotlist(
+ hotlist, fake.MakeTestIssue(1000, 9, '', 'New', 111)))
+
+ def testSplitHotlistIssueRanks(self):
+ iid_rank_tuples = [(issue.issue_id, issue.rank)
+ for issue in self.hotlistitems]
+ iid_rank_tuples.reverse()
+ ret = features_bizobj.SplitHotlistIssueRanks(
+ 100003, True, iid_rank_tuples)
+ self.assertEqual(ret, (iid_rank_tuples[:2], iid_rank_tuples[2:]))
+
+ iid_rank_tuples.reverse()
+ ret = features_bizobj.SplitHotlistIssueRanks(
+ 100003, False, iid_rank_tuples)
+ self.assertEqual(ret, (iid_rank_tuples[:3], iid_rank_tuples[3:]))
+
+ # target issue not found
+ first_pairs, second_pairs = features_bizobj.SplitHotlistIssueRanks(
+ 100009, True, iid_rank_tuples)
+ self.assertEqual(iid_rank_tuples, first_pairs)
+ self.assertEqual(second_pairs, [])
+
+ def testGetOwnerIds(self):
+ hotlist = features_pb2.Hotlist(owner_ids=[111])
+ self.assertEqual(features_bizobj.GetOwnerIds(hotlist), [111])
+
+ def testUsersInvolvedInHotlists_Empty(self):
+ self.assertEqual(set(), features_bizobj.UsersInvolvedInHotlists([]))
+
+ def testUsersInvolvedInHotlists_Normal(self):
+ hotlist1 = features_pb2.Hotlist(
+ owner_ids=[111, 222], editor_ids=[333, 444, 555],
+ follower_ids=[123])
+ hotlist2 = features_pb2.Hotlist(
+ owner_ids=[111], editor_ids=[222, 123])
+ self.assertEqual(set([111, 222, 333, 444, 555, 123]),
+ features_bizobj.UsersInvolvedInHotlists([hotlist1,
+ hotlist2]))
+
+ def testUserIsInHotlist(self):
+ h = features_pb2.Hotlist()
+ self.assertFalse(features_bizobj.UserIsInHotlist(h, {9}))
+ self.assertFalse(features_bizobj.UserIsInHotlist(h, set()))
+
+ h.owner_ids.extend([1, 2, 3])
+ h.editor_ids.extend([4, 5, 6])
+ h.follower_ids.extend([7, 8, 9])
+ self.assertTrue(features_bizobj.UserIsInHotlist(h, {1}))
+ self.assertTrue(features_bizobj.UserIsInHotlist(h, {4}))
+ self.assertTrue(features_bizobj.UserIsInHotlist(h, {7}))
+ self.assertFalse(features_bizobj.UserIsInHotlist(h, {10}))
+
+ # Membership via group membership
+ self.assertTrue(features_bizobj.UserIsInHotlist(h, {10, 4}))
+
+ # Membership via several group memberships
+ self.assertTrue(features_bizobj.UserIsInHotlist(h, {1, 4}))
+
+ # Several irrelevant group memberships
+ self.assertFalse(features_bizobj.UserIsInHotlist(h, {10, 11, 12}))
+
+ def testDetermineHotlistIssuePosition(self):
+ # normal
+ prev_iid, index, next_iid = features_bizobj.DetermineHotlistIssuePosition(
+ self.issues[2], self.iids)
+ self.assertEqual(prev_iid, self.hotlistitems[1].issue_id)
+ self.assertEqual(index, 2)
+ self.assertEqual(next_iid, self.hotlistitems[3].issue_id)
+
+ # end of list
+ prev_iid, index, next_iid = features_bizobj.DetermineHotlistIssuePosition(
+ self.issues[4], self.iids)
+ self.assertEqual(prev_iid, self.hotlistitems[3].issue_id)
+ self.assertEqual(index, 4)
+ self.assertEqual(next_iid, None)
+
+ # beginning of list
+ prev_iid, index, next_iid = features_bizobj.DetermineHotlistIssuePosition(
+ self.issues[0], self.iids)
+ self.assertEqual(prev_iid, None)
+ self.assertEqual(index, 0)
+ self.assertEqual(next_iid, self.hotlistitems[1].issue_id)
+
+ # one item in list
+ prev_iid, index, next_iid = features_bizobj.DetermineHotlistIssuePosition(
+ self.issues[2], [self.iids[2]])
+ self.assertEqual(prev_iid, None)
+ self.assertEqual(index, 0)
+ self.assertEqual(next_iid, None)
+
+ prev_iid, index, next_iid = features_bizobj.DetermineHotlistIssuePosition(
+ self.issues[2], [self.iids[3]])
+ self.assertEqual(prev_iid, None)
+ self.assertEqual(index, None)
+ self.assertEqual(next_iid, None)
+
+ #none
+ prev_iid, index, next_iid = features_bizobj.DetermineHotlistIssuePosition(
+ self.issues[2], [])
+ self.assertEqual(prev_iid, None)
+ self.assertEqual(index, None)
+ self.assertEqual(next_iid, None)
diff --git a/features/test/federated_test.py b/features/test/federated_test.py
new file mode 100644
index 0000000..1ba088a
--- /dev/null
+++ b/features/test/federated_test.py
@@ -0,0 +1,114 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for monorail.feature.federated."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from features import federated
+from framework.exceptions import InvalidExternalIssueReference
+
+
+# Schema: tracker, shortlink.
+VALID_SHORTLINKS = [
+ ('google', 'b/1'),
+ ('google', 'b/123456'),
+ ('google', 'b/1234567890123')]
+
+
+# Schema: tracker, shortlink.
+INVALID_SHORTLINKS = [
+ ('google', 'b'),
+ ('google', 'b/'),
+ ('google', 'b//123'),
+ ('google', 'b/123/123')]
+
+
+class FederatedTest(unittest.TestCase):
+ """Test public module methods."""
+
+ def testIsShortlinkValid_Valid(self):
+ for _, shortlink in VALID_SHORTLINKS:
+ self.assertTrue(federated.IsShortlinkValid(shortlink),
+ 'Expected %s to be a valid shortlink for any tracker.'
+ % shortlink)
+
+ def testIsShortlinkValid_Invalid(self):
+ for _, shortlink in INVALID_SHORTLINKS:
+ self.assertFalse(federated.IsShortlinkValid(shortlink),
+ 'Expected %s to be an invalid shortlink for any tracker.'
+ % shortlink)
+
+ def testFromShortlink_Valid(self):
+ for _, shortlink in VALID_SHORTLINKS:
+ issue = federated.FromShortlink(shortlink)
+ self.assertEqual(shortlink, issue.shortlink, (
+ 'Expected %s to be converted into a valid tracker object '
+ 'with shortlink %s' % (shortlink, issue.shortlink)))
+
+ def testFromShortlink_Invalid(self):
+ for _, shortlink in INVALID_SHORTLINKS:
+ self.assertIsNone(federated.FromShortlink(shortlink))
+
+
+class FederatedIssueTest(unittest.TestCase):
+
+ def testInit_NotImplemented(self):
+ """By default, __init__ raises NotImplementedError.
+
+ Because __init__ calls IsShortlinkValid. See test below.
+ """
+ with self.assertRaises(NotImplementedError):
+ federated.FederatedIssue('a')
+
+ def testIsShortlinkValid_NotImplemented(self):
+ """By default, IsShortlinkValid raises NotImplementedError."""
+ with self.assertRaises(NotImplementedError):
+ federated.FederatedIssue('a').IsShortlinkValid('rutabaga')
+
+
+class GoogleIssueTrackerIssueTest(unittest.TestCase):
+
+ def setUp(self):
+ self.valid_shortlinks = [s for tracker, s in VALID_SHORTLINKS
+ if tracker == 'google']
+ self.invalid_shortlinks = [s for tracker, s in INVALID_SHORTLINKS
+ if tracker == 'google']
+
+ def testInit_ValidatesValidShortlink(self):
+ for shortlink in self.valid_shortlinks:
+ issue = federated.GoogleIssueTrackerIssue(shortlink)
+ self.assertEqual(issue.shortlink, shortlink)
+
+ def testInit_ValidatesInvalidShortlink(self):
+ for shortlink in self.invalid_shortlinks:
+ with self.assertRaises(InvalidExternalIssueReference):
+ federated.GoogleIssueTrackerIssue(shortlink)
+
+ def testIsShortlinkValid_Valid(self):
+ for shortlink in self.valid_shortlinks:
+ self.assertTrue(
+ federated.GoogleIssueTrackerIssue.IsShortlinkValid(shortlink),
+ 'Expected %s to be a valid shortlink for Google.'
+ % shortlink)
+
+ def testIsShortlinkValid_Invalid(self):
+ for shortlink in self.invalid_shortlinks:
+ self.assertFalse(
+ federated.GoogleIssueTrackerIssue.IsShortlinkValid(shortlink),
+ 'Expected %s to be an invalid shortlink for Google.'
+ % shortlink)
+
+ def testToURL(self):
+ self.assertEqual('https://issuetracker.google.com/issues/123456',
+ federated.GoogleIssueTrackerIssue('b/123456').ToURL())
+
+ def testSummary(self):
+ self.assertEqual('Google Issue Tracker issue 123456.',
+ federated.GoogleIssueTrackerIssue('b/123456').Summary())
diff --git a/features/test/filterrules_helpers_test.py b/features/test/filterrules_helpers_test.py
new file mode 100644
index 0000000..99d22b7
--- /dev/null
+++ b/features/test/filterrules_helpers_test.py
@@ -0,0 +1,927 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for filterrules_helpers feature."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mock
+import unittest
+import urllib
+import urlparse
+
+import settings
+from features import filterrules_helpers
+from framework import cloud_tasks_helpers
+from framework import framework_constants
+from framework import template_helpers
+from framework import urls
+from proto import ast_pb2
+from proto import tracker_pb2
+from search import query2ast
+from services import service_manager
+from testing import fake
+from tracker import tracker_bizobj
+
+
+ORIG_SUMMARY = 'this is the orginal summary'
+ORIG_LABELS = ['one', 'two']
+
+# Fake user id mapping
+TEST_ID_MAP = {
+ 'mike.j.parent': 1,
+ 'jrobbins': 2,
+ 'ningerso': 3,
+ 'ui@example.com': 4,
+ 'db@example.com': 5,
+ 'ui-db@example.com': 6,
+ }
+
+TEST_LABEL_IDS = {
+ 'i18n': 1,
+ 'l10n': 2,
+ 'Priority-High': 3,
+ 'Priority-Medium': 4,
+ }
+
+
+class RecomputeAllDerivedFieldsTest(unittest.TestCase):
+
+ BLOCK = filterrules_helpers.BLOCK
+
+ def setUp(self):
+ self.features = fake.FeaturesService()
+ self.user = fake.UserService()
+ self.services = service_manager.Services(
+ features=self.features,
+ user=self.user,
+ issue=fake.IssueService())
+ self.project = fake.Project(project_name='proj')
+ self.config = 'fake config'
+ self.cnxn = 'fake cnxn'
+
+
+ def testRecomputeDerivedFields_Disabled(self):
+ """Servlet should just call RecomputeAllDerivedFieldsNow with no bounds."""
+ saved_flag = settings.recompute_derived_fields_in_worker
+ settings.recompute_derived_fields_in_worker = False
+
+ filterrules_helpers.RecomputeAllDerivedFields(
+ self.cnxn, self.services, self.project, self.config)
+ self.assertTrue(self.services.issue.get_all_issues_in_project_called)
+ self.assertTrue(self.services.issue.update_issues_called)
+ self.assertTrue(self.services.issue.enqueue_issues_called)
+
+ settings.recompute_derived_fields_in_worker = saved_flag
+
+ def testRecomputeDerivedFields_DisabledNextIDSet(self):
+ """Servlet should just call RecomputeAllDerivedFields with no bounds."""
+ saved_flag = settings.recompute_derived_fields_in_worker
+ settings.recompute_derived_fields_in_worker = False
+ self.services.issue.next_id = 1234
+
+ filterrules_helpers.RecomputeAllDerivedFields(
+ self.cnxn, self.services, self.project, self.config)
+ self.assertTrue(self.services.issue.get_all_issues_in_project_called)
+ self.assertTrue(self.services.issue.enqueue_issues_called)
+
+ settings.recompute_derived_fields_in_worker = saved_flag
+
+ def testRecomputeDerivedFields_NoIssues(self):
+ """Servlet should not call because there is no work to do."""
+ saved_flag = settings.recompute_derived_fields_in_worker
+ settings.recompute_derived_fields_in_worker = True
+
+ filterrules_helpers.RecomputeAllDerivedFields(
+ self.cnxn, self.services, self.project, self.config)
+ self.assertFalse(self.services.issue.get_all_issues_in_project_called)
+ self.assertFalse(self.services.issue.update_issues_called)
+ self.assertFalse(self.services.issue.enqueue_issues_called)
+
+ settings.recompute_derived_fields_in_worker = saved_flag
+
+ @mock.patch('framework.cloud_tasks_helpers._get_client')
+ def testRecomputeDerivedFields_SomeIssues(self, get_client_mock):
+ """Servlet should enqueue one work item rather than call directly."""
+ saved_flag = settings.recompute_derived_fields_in_worker
+ settings.recompute_derived_fields_in_worker = True
+ self.services.issue.next_id = 1234
+ num_calls = (self.services.issue.next_id // self.BLOCK + 1)
+
+ filterrules_helpers.RecomputeAllDerivedFields(
+ self.cnxn, self.services, self.project, self.config)
+ self.assertFalse(self.services.issue.get_all_issues_in_project_called)
+ self.assertFalse(self.services.issue.update_issues_called)
+ self.assertFalse(self.services.issue.enqueue_issues_called)
+
+ get_client_mock().queue_path.assert_any_call(
+ settings.app_id, settings.CLOUD_TASKS_REGION, 'recomputederivedfields')
+ self.assertEqual(get_client_mock().queue_path.call_count, num_calls)
+ self.assertEqual(get_client_mock().create_task.call_count, num_calls)
+
+ parent = get_client_mock().queue_path()
+ highest_id = self.services.issue.GetHighestLocalID(
+ self.cnxn, self.project.project_id)
+ steps = list(range(1, highest_id + 1, self.BLOCK))
+ steps.reverse()
+ shard_id = 0
+ for step in steps:
+ params = {
+ 'project_id': self.project.project_id,
+ 'lower_bound': step,
+ 'upper_bound': min(step + self.BLOCK, highest_id + 1),
+ 'shard_id': shard_id,
+ }
+ task = {
+ 'app_engine_http_request':
+ {
+ 'relative_uri': urls.RECOMPUTE_DERIVED_FIELDS_TASK + '.do',
+ 'body': urllib.urlencode(params),
+ 'headers':
+ {
+ 'Content-type': 'application/x-www-form-urlencoded'
+ }
+ }
+ }
+ get_client_mock().create_task.assert_any_call(
+ parent, task, retry=cloud_tasks_helpers._DEFAULT_RETRY)
+ shard_id = (shard_id + 1) % settings.num_logical_shards
+
+ settings.recompute_derived_fields_in_worker = saved_flag
+
+ @mock.patch('framework.cloud_tasks_helpers._get_client')
+ def testRecomputeDerivedFields_LotsOfIssues(self, get_client_mock):
+ """Servlet should enqueue multiple work items."""
+ saved_flag = settings.recompute_derived_fields_in_worker
+ settings.recompute_derived_fields_in_worker = True
+ self.services.issue.next_id = 12345
+
+ filterrules_helpers.RecomputeAllDerivedFields(
+ self.cnxn, self.services, self.project, self.config)
+
+ self.assertFalse(self.services.issue.get_all_issues_in_project_called)
+ self.assertFalse(self.services.issue.update_issues_called)
+ self.assertFalse(self.services.issue.enqueue_issues_called)
+ num_calls = (self.services.issue.next_id // self.BLOCK + 1)
+ get_client_mock().queue_path.assert_any_call(
+ settings.app_id, settings.CLOUD_TASKS_REGION, 'recomputederivedfields')
+ self.assertEqual(get_client_mock().queue_path.call_count, num_calls)
+ self.assertEqual(get_client_mock().create_task.call_count, num_calls)
+
+ ((_parent, called_task),
+ _kwargs) = get_client_mock().create_task.call_args_list[0]
+ relative_uri = called_task.get('app_engine_http_request').get(
+ 'relative_uri')
+ self.assertEqual(relative_uri, urls.RECOMPUTE_DERIVED_FIELDS_TASK + '.do')
+ encoded_params = called_task.get('app_engine_http_request').get('body')
+ params = {k: v[0] for k, v in urlparse.parse_qs(encoded_params).items()}
+ self.assertEqual(params['project_id'], str(self.project.project_id))
+ self.assertEqual(
+ params['lower_bound'], str(12345 // self.BLOCK * self.BLOCK + 1))
+ self.assertEqual(params['upper_bound'], str(12345))
+
+ ((_parent, called_task), _kwargs) = get_client_mock().create_task.call_args
+ relative_uri = called_task.get('app_engine_http_request').get(
+ 'relative_uri')
+ self.assertEqual(relative_uri, urls.RECOMPUTE_DERIVED_FIELDS_TASK + '.do')
+ encoded_params = called_task.get('app_engine_http_request').get('body')
+ params = {k: v[0] for k, v in urlparse.parse_qs(encoded_params).items()}
+ self.assertEqual(params['project_id'], str(self.project.project_id))
+ self.assertEqual(params['lower_bound'], str(1))
+ self.assertEqual(params['upper_bound'], str(self.BLOCK + 1))
+
+ settings.recompute_derived_fields_in_worker = saved_flag
+
+ @mock.patch(
+ 'features.filterrules_helpers.ApplyGivenRules', return_value=(True, {}))
+ def testRecomputeAllDerivedFieldsNow(self, apply_mock):
+ """Servlet should reapply all filter rules to project's issues."""
+ self.services.issue.next_id = 12345
+ test_issue_1 = fake.MakeTestIssue(
+ project_id=self.project.project_id, local_id=1, issue_id=1001,
+ summary='sum1', owner_id=100, status='New')
+ test_issue_1.assume_stale = False # We will store this issue.
+ test_issue_2 = fake.MakeTestIssue(
+ project_id=self.project.project_id, local_id=2, issue_id=1002,
+ summary='sum2', owner_id=100, status='New')
+ test_issue_2.assume_stale = False # We will store this issue.
+ test_issues = [test_issue_1, test_issue_2]
+ self.services.issue.TestAddIssue(test_issue_1)
+ self.services.issue.TestAddIssue(test_issue_2)
+
+ filterrules_helpers.RecomputeAllDerivedFieldsNow(
+ self.cnxn, self.services, self.project, self.config)
+
+ self.assertTrue(self.services.issue.get_all_issues_in_project_called)
+ self.assertTrue(self.services.issue.update_issues_called)
+ self.assertTrue(self.services.issue.enqueue_issues_called)
+ self.assertEqual(test_issues, self.services.issue.updated_issues)
+ self.assertEqual([issue.issue_id for issue in test_issues],
+ self.services.issue.enqueued_issues)
+ self.assertEqual(apply_mock.call_count, 2)
+ for test_issue in test_issues:
+ apply_mock.assert_any_call(
+ self.cnxn, self.services, test_issue, self.config, [], [])
+
+
+class FilterRulesHelpersTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = 'fake cnxn'
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ project=fake.ProjectService(),
+ issue=fake.IssueService(),
+ config=fake.ConfigService())
+ self.project = self.services.project.TestAddProject('proj', project_id=789)
+ self.other_project = self.services.project.TestAddProject(
+ 'otherproj', project_id=890)
+ for email, user_id in TEST_ID_MAP.items():
+ self.services.user.TestAddUser(email, user_id)
+ self.services.config.TestAddLabelsDict(TEST_LABEL_IDS)
+
+ def testApplyRule(self):
+ cnxn = 'fake sql connection'
+ issue = fake.MakeTestIssue(
+ 789, 1, ORIG_SUMMARY, 'New', 111, labels=ORIG_LABELS)
+ config = tracker_pb2.ProjectIssueConfig(project_id=self.project.project_id)
+ # Empty label set cannot satisfy rule looking for labels.
+ pred = 'label:a label:b'
+ rule = filterrules_helpers.MakeRule(
+ pred, default_owner_id=1, default_status='S')
+ predicate_ast = query2ast.ParseUserQuery(
+ pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
+ self.assertEqual(
+ (None, None, [], [], [], None, None),
+ filterrules_helpers._ApplyRule(
+ cnxn, self.services, rule, predicate_ast, issue, set(), config))
+
+ pred = 'label:a -label:b'
+ rule = filterrules_helpers.MakeRule(
+ pred, default_owner_id=1, default_status='S')
+ predicate_ast = query2ast.ParseUserQuery(
+ pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
+ self.assertEqual(
+ (None, None, [], [], [], None, None),
+ filterrules_helpers._ApplyRule(
+ cnxn, self.services, rule, predicate_ast, issue, set(), config))
+
+ # Empty label set will satisfy rule looking for missing labels.
+ pred = '-label:a -label:b'
+ rule = filterrules_helpers.MakeRule(
+ pred, default_owner_id=1, default_status='S')
+ predicate_ast = query2ast.ParseUserQuery(
+ pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
+ self.assertEqual(
+ (1, 'S', [], [], [], None, None),
+ filterrules_helpers._ApplyRule(
+ cnxn, self.services, rule, predicate_ast, issue, set(), config))
+
+ # Label set has the needed labels.
+ pred = 'label:a label:b'
+ rule = filterrules_helpers.MakeRule(
+ pred, default_owner_id=1, default_status='S')
+ predicate_ast = query2ast.ParseUserQuery(
+ pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
+ self.assertEqual(
+ (1, 'S', [], [], [], None, None),
+ filterrules_helpers._ApplyRule(
+ cnxn, self.services, rule, predicate_ast, issue, {'a', 'b'},
+ config))
+
+ # Label set has the needed labels with test for unicode.
+ pred = 'label:a label:b'
+ rule = filterrules_helpers.MakeRule(
+ pred, default_owner_id=1, default_status='S')
+ predicate_ast = query2ast.ParseUserQuery(
+ pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
+ self.assertEqual(
+ (1, 'S', [], [], [], None, None),
+ filterrules_helpers._ApplyRule(
+ cnxn, self.services, rule, predicate_ast, issue, {u'a', u'b'},
+ config))
+
+ # Label set has the needed labels, capitalization irrelevant.
+ pred = 'label:A label:B'
+ rule = filterrules_helpers.MakeRule(
+ pred, default_owner_id=1, default_status='S')
+ predicate_ast = query2ast.ParseUserQuery(
+ pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
+ self.assertEqual(
+ (1, 'S', [], [], [], None, None),
+ filterrules_helpers._ApplyRule(
+ cnxn, self.services, rule, predicate_ast, issue, {'a', 'b'},
+ config))
+
+ # Label set has a label, the rule negates.
+ pred = 'label:a -label:b'
+ rule = filterrules_helpers.MakeRule(
+ pred, default_owner_id=1, default_status='S')
+ predicate_ast = query2ast.ParseUserQuery(
+ pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
+ self.assertEqual(
+ (None, None, [], [], [], None, None),
+ filterrules_helpers._ApplyRule(
+ cnxn, self.services, rule, predicate_ast, issue, {'a', 'b'},
+ config))
+
+ # Consequence is to add a warning.
+ pred = 'label:a'
+ rule = filterrules_helpers.MakeRule(
+ pred, warning='Hey look out')
+ predicate_ast = query2ast.ParseUserQuery(
+ pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
+ self.assertEqual(
+ (None, None, [], [], [], 'Hey look out', None),
+ filterrules_helpers._ApplyRule(
+ cnxn, self.services, rule, predicate_ast, issue, {'a', 'b'},
+ config))
+
+ # Consequence is to add an error.
+ pred = 'label:a'
+ rule = filterrules_helpers.MakeRule(
+ pred, error='We cannot allow that')
+ predicate_ast = query2ast.ParseUserQuery(
+ pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
+ self.assertEqual(
+ (None, None, [], [], [], None, 'We cannot allow that'),
+ filterrules_helpers._ApplyRule(
+ cnxn, self.services, rule, predicate_ast, issue, {'a', 'b'},
+ config))
+
+ def testComputeDerivedFields_Components(self):
+ cnxn = 'fake sql connection'
+ rules = []
+ component_defs = [
+ tracker_bizobj.MakeComponentDef(
+ 10, 789, 'DB', 'database', False, [],
+ [TEST_ID_MAP['db@example.com'],
+ TEST_ID_MAP['ui-db@example.com']],
+ 0, 0,
+ label_ids=[TEST_LABEL_IDS['i18n'],
+ TEST_LABEL_IDS['Priority-High']]),
+ tracker_bizobj.MakeComponentDef(
+ 20, 789, 'Install', 'installer', False, [],
+ [], 0, 0),
+ tracker_bizobj.MakeComponentDef(
+ 30, 789, 'UI', 'doc', False, [],
+ [TEST_ID_MAP['ui@example.com'],
+ TEST_ID_MAP['ui-db@example.com']],
+ 0, 0,
+ label_ids=[TEST_LABEL_IDS['i18n'],
+ TEST_LABEL_IDS['l10n'],
+ TEST_LABEL_IDS['Priority-Medium']]),
+ ]
+ excl_prefixes = ['Priority', 'type', 'milestone']
+ config = tracker_pb2.ProjectIssueConfig(
+ exclusive_label_prefixes=excl_prefixes,
+ component_defs=component_defs)
+ predicate_asts = filterrules_helpers.ParsePredicateASTs(rules, config, [])
+
+ # No components.
+ issue = fake.MakeTestIssue(
+ 789, 1, ORIG_SUMMARY, 'New', 0, labels=ORIG_LABELS)
+ self.assertEqual(
+ (0, '', [], [], [], {}, [], []),
+ filterrules_helpers._ComputeDerivedFields(
+ cnxn, self.services, issue, config, rules, predicate_asts))
+
+ # One component, no CCs or labels added
+ issue.component_ids = [20]
+ issue = fake.MakeTestIssue(
+ 789, 1, ORIG_SUMMARY, 'New', 0, labels=ORIG_LABELS)
+ self.assertEqual(
+ (0, '', [], [], [], {}, [], []),
+ filterrules_helpers._ComputeDerivedFields(
+ cnxn, self.services, issue, config, rules, predicate_asts))
+
+ # One component, some CCs and labels added
+ issue = fake.MakeTestIssue(
+ 789, 1, ORIG_SUMMARY, 'New', 0, labels=ORIG_LABELS,
+ component_ids=[10])
+ traces = {
+ (tracker_pb2.FieldID.CC, TEST_ID_MAP['db@example.com']):
+ 'Added by component DB',
+ (tracker_pb2.FieldID.CC, TEST_ID_MAP['ui-db@example.com']):
+ 'Added by component DB',
+ (tracker_pb2.FieldID.LABELS, 'i18n'):
+ 'Added by component DB',
+ (tracker_pb2.FieldID.LABELS, 'Priority-High'):
+ 'Added by component DB',
+ }
+ self.assertEqual(
+ (
+ 0, '', [
+ TEST_ID_MAP['db@example.com'], TEST_ID_MAP['ui-db@example.com']
+ ], ['i18n', 'Priority-High'], [], traces, [], []),
+ filterrules_helpers._ComputeDerivedFields(
+ cnxn, self.services, issue, config, rules, predicate_asts))
+
+ # One component, CCs and labels not added because of labels on the issue.
+ issue = fake.MakeTestIssue(
+ 789, 1, ORIG_SUMMARY, 'New', 0, labels=['Priority-Low', 'i18n'],
+ component_ids=[10])
+ issue.cc_ids = [TEST_ID_MAP['db@example.com']]
+ traces = {
+ (tracker_pb2.FieldID.CC, TEST_ID_MAP['ui-db@example.com']):
+ 'Added by component DB',
+ }
+ self.assertEqual(
+ (0, '', [TEST_ID_MAP['ui-db@example.com']], [], [], traces, [], []),
+ filterrules_helpers._ComputeDerivedFields(
+ cnxn, self.services, issue, config, rules, predicate_asts))
+
+ # Multiple components, added CCs treated as a set, exclusive labels in later
+ # components take priority over earlier ones.
+ issue = fake.MakeTestIssue(
+ 789, 1, ORIG_SUMMARY, 'New', 0, labels=ORIG_LABELS,
+ component_ids=[10, 30])
+ traces = {
+ (tracker_pb2.FieldID.CC, TEST_ID_MAP['db@example.com']):
+ 'Added by component DB',
+ (tracker_pb2.FieldID.CC, TEST_ID_MAP['ui-db@example.com']):
+ 'Added by component DB',
+ (tracker_pb2.FieldID.LABELS, 'i18n'):
+ 'Added by component DB',
+ (tracker_pb2.FieldID.LABELS, 'Priority-High'):
+ 'Added by component DB',
+ (tracker_pb2.FieldID.CC, TEST_ID_MAP['ui@example.com']):
+ 'Added by component UI',
+ (tracker_pb2.FieldID.LABELS, 'Priority-Medium'):
+ 'Added by component UI',
+ (tracker_pb2.FieldID.LABELS, 'l10n'):
+ 'Added by component UI',
+ }
+ self.assertEqual(
+ (
+ 0, '', [
+ TEST_ID_MAP['db@example.com'], TEST_ID_MAP['ui-db@example.com'],
+ TEST_ID_MAP['ui@example.com']
+ ], ['i18n', 'l10n', 'Priority-Medium'], [], traces, [], []),
+ filterrules_helpers._ComputeDerivedFields(
+ cnxn, self.services, issue, config, rules, predicate_asts))
+
+ def testComputeDerivedFields_Rules(self):
+ cnxn = 'fake sql connection'
+ rules = [
+ filterrules_helpers.MakeRule(
+ 'label:HasWorkaround', add_labels=['Priority-Low']),
+ filterrules_helpers.MakeRule(
+ 'label:Security', add_labels=['Private']),
+ filterrules_helpers.MakeRule(
+ 'label:Security', add_labels=['Priority-High'],
+ add_notify=['jrobbins@chromium.org']),
+ filterrules_helpers.MakeRule(
+ 'Priority=High label:Regression', add_labels=['Urgent']),
+ filterrules_helpers.MakeRule(
+ 'Size=L', default_owner_id=444),
+ filterrules_helpers.MakeRule(
+ 'Size=XL', warning='It will take too long'),
+ filterrules_helpers.MakeRule(
+ 'Size=XL', warning='It will cost too much'),
+ ]
+ excl_prefixes = ['Priority', 'type', 'milestone']
+ config = tracker_pb2.ProjectIssueConfig(
+ exclusive_label_prefixes=excl_prefixes,
+ project_id=self.project.project_id)
+ predicate_asts = filterrules_helpers.ParsePredicateASTs(rules, config, [])
+
+ # No rules fire.
+ issue = fake.MakeTestIssue(
+ 789, 1, ORIG_SUMMARY, 'New', 0, labels=ORIG_LABELS)
+ self.assertEqual(
+ (0, '', [], [], [], {}, [], []),
+ filterrules_helpers._ComputeDerivedFields(
+ cnxn, self.services, issue, config, rules, predicate_asts))
+
+ issue = fake.MakeTestIssue(
+ 789, 1, ORIG_SUMMARY, 'New', 0, labels=['foo', 'bar'])
+ self.assertEqual(
+ (0, '', [], [], [], {}, [], []),
+ filterrules_helpers._ComputeDerivedFields(
+ cnxn, self.services, issue, config, rules, predicate_asts))
+
+ # One rule fires.
+ issue = fake.MakeTestIssue(
+ 789, 1, ORIG_SUMMARY, 'New', 0, labels=['Size-L'])
+ traces = {
+ (tracker_pb2.FieldID.OWNER, 444):
+ 'Added by rule: IF Size=L THEN SET DEFAULT OWNER',
+ }
+ self.assertEqual(
+ (444, '', [], [], [], traces, [], []),
+ filterrules_helpers._ComputeDerivedFields(
+ cnxn, self.services, issue, config, rules, predicate_asts))
+
+ # One rule fires, but no effect because of explicit fields.
+ issue = fake.MakeTestIssue(
+ 789, 1, ORIG_SUMMARY, 'New', 0,
+ labels=['HasWorkaround', 'Priority-Critical'])
+ traces = {}
+ self.assertEqual(
+ (0, '', [], [], [], traces, [], []),
+ filterrules_helpers._ComputeDerivedFields(
+ cnxn, self.services, issue, config, rules, predicate_asts))
+
+ # One rule fires, another has no effect because of explicit exclusive label.
+ issue = fake.MakeTestIssue(
+ 789, 1, ORIG_SUMMARY, 'New', 0,
+ labels=['Security', 'Priority-Critical'])
+ traces = {
+ (tracker_pb2.FieldID.LABELS, 'Private'):
+ 'Added by rule: IF label:Security THEN ADD LABEL',
+ }
+ self.assertEqual(
+ (0, '', [], ['Private'], ['jrobbins@chromium.org'], traces, [], []),
+ filterrules_helpers._ComputeDerivedFields(
+ cnxn, self.services, issue, config, rules, predicate_asts))
+
+ # Multiple rules have cumulative effect.
+ issue = fake.MakeTestIssue(
+ 789, 1, ORIG_SUMMARY, 'New', 0, labels=['HasWorkaround', 'Size-L'])
+ traces = {
+ (tracker_pb2.FieldID.LABELS, 'Priority-Low'):
+ 'Added by rule: IF label:HasWorkaround THEN ADD LABEL',
+ (tracker_pb2.FieldID.OWNER, 444):
+ 'Added by rule: IF Size=L THEN SET DEFAULT OWNER',
+ }
+ self.assertEqual(
+ (444, '', [], ['Priority-Low'], [], traces, [], []),
+ filterrules_helpers._ComputeDerivedFields(
+ cnxn, self.services, issue, config, rules, predicate_asts))
+
+ # Multiple rules have cumulative warnings.
+ issue = fake.MakeTestIssue(
+ 789, 1, ORIG_SUMMARY, 'New', 0, labels=['Size-XL'])
+ traces = {
+ (tracker_pb2.FieldID.WARNING, 'It will take too long'):
+ 'Added by rule: IF Size=XL THEN ADD WARNING',
+ (tracker_pb2.FieldID.WARNING, 'It will cost too much'):
+ 'Added by rule: IF Size=XL THEN ADD WARNING',
+ }
+ self.assertEqual(
+ (
+ 0, '', [], [], [], traces,
+ ['It will take too long', 'It will cost too much'], []),
+ filterrules_helpers._ComputeDerivedFields(
+ cnxn, self.services, issue, config, rules, predicate_asts))
+
+ # Two rules fire, second overwrites the first.
+ issue = fake.MakeTestIssue(
+ 789, 1, ORIG_SUMMARY, 'New', 0, labels=['HasWorkaround', 'Security'])
+ traces = {
+ (tracker_pb2.FieldID.LABELS, 'Priority-Low'):
+ 'Added by rule: IF label:HasWorkaround THEN ADD LABEL',
+ (tracker_pb2.FieldID.LABELS, 'Priority-High'):
+ 'Added by rule: IF label:Security THEN ADD LABEL',
+ (tracker_pb2.FieldID.LABELS, 'Private'):
+ 'Added by rule: IF label:Security THEN ADD LABEL',
+ }
+ self.assertEqual(
+ (
+ 0, '', [], ['Private', 'Priority-High'], ['jrobbins@chromium.org'],
+ traces, [], []),
+ filterrules_helpers._ComputeDerivedFields(
+ cnxn, self.services, issue, config, rules, predicate_asts))
+
+ # Two rules fire, second triggered by the first.
+ issue = fake.MakeTestIssue(
+ 789, 1, ORIG_SUMMARY, 'New', 0, labels=['Security', 'Regression'])
+ traces = {
+ (tracker_pb2.FieldID.LABELS, 'Priority-High'):
+ 'Added by rule: IF label:Security THEN ADD LABEL',
+ (tracker_pb2.FieldID.LABELS, 'Urgent'):
+ 'Added by rule: IF Priority=High label:Regression THEN ADD LABEL',
+ (tracker_pb2.FieldID.LABELS, 'Private'):
+ 'Added by rule: IF label:Security THEN ADD LABEL',
+ }
+ self.assertEqual(
+ (
+ 0, '', [], ['Private', 'Priority-High', 'Urgent'],
+ ['jrobbins@chromium.org'], traces, [], []),
+ filterrules_helpers._ComputeDerivedFields(
+ cnxn, self.services, issue, config, rules, predicate_asts))
+
+ # Two rules fire, each one wants to add the same CC: only add once.
+ rules.append(filterrules_helpers.MakeRule('Watch', add_cc_ids=[111]))
+ rules.append(filterrules_helpers.MakeRule('Monitor', add_cc_ids=[111]))
+ config = tracker_pb2.ProjectIssueConfig(
+ exclusive_label_prefixes=excl_prefixes,
+ project_id=self.project.project_id)
+ predicate_asts = filterrules_helpers.ParsePredicateASTs(rules, config, [])
+ traces = {
+ (tracker_pb2.FieldID.CC, 111):
+ 'Added by rule: IF Watch THEN ADD CC',
+ }
+ issue = fake.MakeTestIssue(
+ 789, 1, ORIG_SUMMARY, 'New', 111, labels=['Watch', 'Monitor'])
+ self.assertEqual(
+ (0, '', [111], [], [], traces, [], []),
+ filterrules_helpers._ComputeDerivedFields(
+ cnxn, self.services, issue, config, rules, predicate_asts))
+
+ def testCompareComponents_Trivial(self):
+ config = tracker_pb2.ProjectIssueConfig()
+ self.assertTrue(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.IS_DEFINED, [], [123]))
+ self.assertFalse(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.IS_NOT_DEFINED, [], [123]))
+ self.assertFalse(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.IS_DEFINED, [], []))
+ self.assertTrue(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.IS_NOT_DEFINED, [], []))
+ self.assertFalse(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.EQ, [123], []))
+
+ def testCompareComponents_Normal(self):
+ config = tracker_pb2.ProjectIssueConfig()
+ config.component_defs.append(tracker_bizobj.MakeComponentDef(
+ 100, 789, 'UI', 'doc', False, [], [], 0, 0))
+ config.component_defs.append(tracker_bizobj.MakeComponentDef(
+ 110, 789, 'UI>Help', 'doc', False, [], [], 0, 0))
+ config.component_defs.append(tracker_bizobj.MakeComponentDef(
+ 200, 789, 'Networking', 'doc', False, [], [], 0, 0))
+
+ # Check if the issue is in a specified component or subcomponent.
+ self.assertTrue(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.EQ, ['UI'], [100]))
+ self.assertTrue(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.EQ, ['UI>Help'], [110]))
+ self.assertTrue(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.EQ, ['UI'], [100, 110]))
+ self.assertFalse(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.EQ, ['UI'], []))
+ self.assertFalse(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.EQ, ['UI'], [110]))
+ self.assertFalse(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.EQ, ['UI'], [200]))
+ self.assertFalse(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.EQ, ['UI>Help'], [100]))
+ self.assertFalse(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.EQ, ['Networking'], [100]))
+
+ self.assertTrue(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.NE, ['UI'], []))
+ self.assertFalse(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.NE, ['UI'], [100]))
+ self.assertTrue(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.NE, ['Networking'], [100]))
+
+ # Exact vs non-exact.
+ self.assertFalse(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.EQ, ['Help'], [110]))
+ self.assertTrue(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.TEXT_HAS, ['UI'], [110]))
+ self.assertFalse(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.TEXT_HAS, ['Help'], [110]))
+ self.assertFalse(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.NOT_TEXT_HAS, ['UI'], [110]))
+ self.assertTrue(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.NOT_TEXT_HAS, ['Help'], [110]))
+
+ # Multivalued issues and Quick-OR notation
+ self.assertTrue(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.EQ, ['Networking'], [200]))
+ self.assertFalse(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.EQ, ['Networking'], [100, 110]))
+ self.assertTrue(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.EQ, ['UI', 'Networking'], [100]))
+ self.assertFalse(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.EQ, ['UI', 'Networking'], [110]))
+ self.assertTrue(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.EQ, ['UI', 'Networking'], [200]))
+ self.assertTrue(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.EQ, ['UI', 'Networking'], [110, 200]))
+ self.assertTrue(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.TEXT_HAS, ['UI', 'Networking'], [110, 200]))
+ self.assertTrue(filterrules_helpers._CompareComponents(
+ config, ast_pb2.QueryOp.EQ, ['UI>Help', 'Networking'], [110, 200]))
+
+ def testCompareIssueRefs_Trivial(self):
+ self.assertTrue(filterrules_helpers._CompareIssueRefs(
+ self.cnxn, self.services, self.project,
+ ast_pb2.QueryOp.IS_DEFINED, [], [123]))
+ self.assertFalse(filterrules_helpers._CompareIssueRefs(
+ self.cnxn, self.services, self.project,
+ ast_pb2.QueryOp.IS_NOT_DEFINED, [], [123]))
+ self.assertFalse(filterrules_helpers._CompareIssueRefs(
+ self.cnxn, self.services, self.project,
+ ast_pb2.QueryOp.IS_DEFINED, [], []))
+ self.assertTrue(filterrules_helpers._CompareIssueRefs(
+ self.cnxn, self.services, self.project,
+ ast_pb2.QueryOp.IS_NOT_DEFINED, [], []))
+ self.assertFalse(filterrules_helpers._CompareIssueRefs(
+ self.cnxn, self.services, self.project,
+ ast_pb2.QueryOp.EQ, ['1'], []))
+
+ def testCompareIssueRefs_Normal(self):
+ self.services.issue.TestAddIssue(fake.MakeTestIssue(
+ 789, 1, 'summary', 'New', 0, issue_id=123))
+ self.services.issue.TestAddIssue(fake.MakeTestIssue(
+ 789, 2, 'summary', 'New', 0, issue_id=124))
+ self.services.issue.TestAddIssue(fake.MakeTestIssue(
+ 890, 1, 'other summary', 'New', 0, issue_id=125))
+
+ # EQ and NE, implict references to the current project.
+ self.assertTrue(filterrules_helpers._CompareIssueRefs(
+ self.cnxn, self.services, self.project,
+ ast_pb2.QueryOp.EQ, ['1'], [123]))
+ self.assertFalse(filterrules_helpers._CompareIssueRefs(
+ self.cnxn, self.services, self.project,
+ ast_pb2.QueryOp.NE, ['1'], [123]))
+
+ # EQ and NE, explicit project references.
+ self.assertTrue(filterrules_helpers._CompareIssueRefs(
+ self.cnxn, self.services, self.project,
+ ast_pb2.QueryOp.EQ, ['proj:1'], [123]))
+ self.assertTrue(filterrules_helpers._CompareIssueRefs(
+ self.cnxn, self.services, self.project,
+ ast_pb2.QueryOp.EQ, ['otherproj:1'], [125]))
+
+ # Inequalities
+ self.assertTrue(filterrules_helpers._CompareIssueRefs(
+ self.cnxn, self.services, self.project,
+ ast_pb2.QueryOp.GE, ['1'], [123]))
+ self.assertTrue(filterrules_helpers._CompareIssueRefs(
+ self.cnxn, self.services, self.project,
+ ast_pb2.QueryOp.GE, ['1'], [124]))
+ self.assertTrue(filterrules_helpers._CompareIssueRefs(
+ self.cnxn, self.services, self.project,
+ ast_pb2.QueryOp.GE, ['2'], [124]))
+ self.assertFalse(filterrules_helpers._CompareIssueRefs(
+ self.cnxn, self.services, self.project,
+ ast_pb2.QueryOp.GT, ['2'], [124]))
+
+ def testCompareUsers(self):
+ pass # TODO(jrobbins): Add this test.
+
+ def testCompareUserIDs(self):
+ pass # TODO(jrobbins): Add this test.
+
+ def testCompareEmails(self):
+ pass # TODO(jrobbins): Add this test.
+
+ def testCompare(self):
+ pass # TODO(jrobbins): Add this test.
+
+ def testParseOneRuleAddLabels(self):
+ cnxn = 'fake SQL connection'
+ error_list = []
+ rule_pb = filterrules_helpers._ParseOneRule(
+ cnxn, 'label:lab1 label:lab2', 'add_labels', 'hot cOld, ', None, 1,
+ error_list)
+ self.assertEqual('label:lab1 label:lab2', rule_pb.predicate)
+ self.assertEqual(error_list, [])
+ self.assertEqual(len(rule_pb.add_labels), 2)
+ self.assertEqual(rule_pb.add_labels[0], 'hot')
+ self.assertEqual(rule_pb.add_labels[1], 'cOld')
+
+ rule_pb = filterrules_helpers._ParseOneRule(
+ cnxn, '', 'default_status', 'hot cold', None, 1, error_list)
+ self.assertEqual(len(rule_pb.predicate), 0)
+ self.assertEqual(error_list, [])
+
+ def testParseOneRuleDefaultOwner(self):
+ cnxn = 'fake SQL connection'
+ error_list = []
+ rule_pb = filterrules_helpers._ParseOneRule(
+ cnxn, 'label:lab1, label:lab2 ', 'default_owner', 'jrobbins',
+ self.services.user, 1, error_list)
+ self.assertEqual(error_list, [])
+ self.assertEqual(rule_pb.default_owner_id, TEST_ID_MAP['jrobbins'])
+
+ def testParseOneRuleDefaultStatus(self):
+ cnxn = 'fake SQL connection'
+ error_list = []
+ rule_pb = filterrules_helpers._ParseOneRule(
+ cnxn, 'label:lab1', 'default_status', 'InReview',
+ None, 1, error_list)
+ self.assertEqual(error_list, [])
+ self.assertEqual(rule_pb.default_status, 'InReview')
+
+ def testParseOneRuleAddCcs(self):
+ cnxn = 'fake SQL connection'
+ error_list = []
+ rule_pb = filterrules_helpers._ParseOneRule(
+ cnxn, 'label:lab1', 'add_ccs', 'jrobbins, mike.j.parent',
+ self.services.user, 1, error_list)
+ self.assertEqual(error_list, [])
+ self.assertEqual(rule_pb.add_cc_ids[0], TEST_ID_MAP['jrobbins'])
+ self.assertEqual(rule_pb.add_cc_ids[1], TEST_ID_MAP['mike.j.parent'])
+ self.assertEqual(len(rule_pb.add_cc_ids), 2)
+
+ def testParseRulesNone(self):
+ cnxn = 'fake SQL connection'
+ post_data = {}
+ rules = filterrules_helpers.ParseRules(
+ cnxn, post_data, None, template_helpers.EZTError())
+ self.assertEqual(rules, [])
+
+ def testParseRules(self):
+ cnxn = 'fake SQL connection'
+ post_data = {
+ 'predicate1': 'a, b c',
+ 'action_type1': 'default_status',
+ 'action_value1': 'Reviewed',
+ 'predicate2': 'a, b c',
+ 'action_type2': 'default_owner',
+ 'action_value2': 'jrobbins',
+ 'predicate3': 'a, b c',
+ 'action_type3': 'add_ccs',
+ 'action_value3': 'jrobbins, mike.j.parent',
+ 'predicate4': 'a, b c',
+ 'action_type4': 'add_labels',
+ 'action_value4': 'hot, cold',
+ }
+ errors = template_helpers.EZTError()
+ rules = filterrules_helpers.ParseRules(
+ cnxn, post_data, self.services.user, errors)
+ self.assertEqual(rules[0].predicate, 'a, b c')
+ self.assertEqual(rules[0].default_status, 'Reviewed')
+ self.assertEqual(rules[1].default_owner_id, TEST_ID_MAP['jrobbins'])
+ self.assertEqual(rules[2].add_cc_ids[0], TEST_ID_MAP['jrobbins'])
+ self.assertEqual(rules[2].add_cc_ids[1], TEST_ID_MAP['mike.j.parent'])
+ self.assertEqual(rules[3].add_labels[0], 'hot')
+ self.assertEqual(rules[3].add_labels[1], 'cold')
+ self.assertEqual(len(rules), 4)
+ self.assertFalse(errors.AnyErrors())
+
+ def testOwnerCcsInvolvedInFilterRules(self):
+ rules = [
+ tracker_pb2.FilterRule(add_cc_ids=[111, 333], default_owner_id=999),
+ tracker_pb2.FilterRule(default_owner_id=888),
+ tracker_pb2.FilterRule(add_cc_ids=[999, 777]),
+ tracker_pb2.FilterRule(),
+ ]
+ actual_user_ids = filterrules_helpers.OwnerCcsInvolvedInFilterRules(rules)
+ self.assertItemsEqual([111, 333, 777, 888, 999], actual_user_ids)
+
+ def testBuildFilterRuleStrings(self):
+ rules = [
+ tracker_pb2.FilterRule(
+ predicate='label:machu', add_cc_ids=[111, 333, 999]),
+ tracker_pb2.FilterRule(predicate='label:pichu', default_owner_id=222),
+ tracker_pb2.FilterRule(
+ predicate='owner:farmer@test.com',
+ add_labels=['cows-farting', 'chicken', 'machu-pichu']),
+ tracker_pb2.FilterRule(predicate='label:beach', default_status='New'),
+ tracker_pb2.FilterRule(
+ predicate='label:rainforest',
+ add_notify_addrs=['cake@test.com', 'pie@test.com']),
+ ]
+ emails_by_id = {
+ 111: 'cow@test.com', 222: 'fox@test.com', 333: 'llama@test.com'}
+ rule_strs = filterrules_helpers.BuildFilterRuleStrings(rules, emails_by_id)
+
+ self.assertItemsEqual(
+ rule_strs, [
+ 'if label:machu '
+ 'then add cc(s): cow@test.com, llama@test.com, user not found',
+ 'if label:pichu then set default owner: fox@test.com',
+ 'if owner:farmer@test.com '
+ 'then add label(s): cows-farting, chicken, machu-pichu',
+ 'if label:beach then set default status: New',
+ 'if label:rainforest then notify: cake@test.com, pie@test.com',
+ ])
+
+ def testBuildRedactedFilterRuleStrings(self):
+ rules_by_project = {
+ 16: [
+ tracker_pb2.FilterRule(
+ predicate='label:machu', add_cc_ids=[111, 333, 999]),
+ tracker_pb2.FilterRule(
+ predicate='label:pichu', default_owner_id=222)],
+ 19: [
+ tracker_pb2.FilterRule(
+ predicate='owner:farmer@test.com',
+ add_labels=['cows-farting', 'chicken', 'machu-pichu']),
+ tracker_pb2.FilterRule(
+ predicate='label:rainforest',
+ add_notify_addrs=['cake@test.com', 'pie@test.com'])],
+ }
+ deleted_emails = ['farmer@test.com', 'pie@test.com', 'fox@test.com']
+ self.services.user.TestAddUser('cow@test.com', 111)
+ self.services.user.TestAddUser('fox@test.com', 222)
+ self.services.user.TestAddUser('llama@test.com', 333)
+ actual = filterrules_helpers.BuildRedactedFilterRuleStrings(
+ self.cnxn, rules_by_project, self.services.user, deleted_emails)
+
+ self.assertItemsEqual(
+ actual,
+ {16: [
+ 'if label:machu '
+ 'then add cc(s): cow@test.com, llama@test.com, user not found',
+ 'if label:pichu '
+ 'then set default owner: %s' %
+ framework_constants.DELETED_USER_NAME],
+ 19: [
+ 'if owner:%s '
+ 'then add label(s): cows-farting, chicken, machu-pichu' %
+ framework_constants.DELETED_USER_NAME,
+ 'if label:rainforest '
+ 'then notify: cake@test.com, %s' %
+ framework_constants.DELETED_USER_NAME],
+ })
diff --git a/features/test/filterrules_views_test.py b/features/test/filterrules_views_test.py
new file mode 100644
index 0000000..323b6c2
--- /dev/null
+++ b/features/test/filterrules_views_test.py
@@ -0,0 +1,75 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unittest for issue tracker views."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from features import filterrules_views
+from proto import tracker_pb2
+from testing import testing_helpers
+
+
+class RuleViewTest(unittest.TestCase):
+
+ def setUp(self):
+ self.rule = tracker_pb2.FilterRule()
+ self.rule.predicate = 'label:a label:b'
+
+ def testNone(self):
+ view = filterrules_views.RuleView(None, {})
+ self.assertEqual('', view.action_type)
+ self.assertEqual('', view.action_value)
+
+ def testEmpty(self):
+ view = filterrules_views.RuleView(self.rule, {})
+ self.rule.predicate = ''
+ self.assertEqual('', view.predicate)
+ self.assertEqual('', view.action_type)
+ self.assertEqual('', view.action_value)
+
+ def testDefaultStatus(self):
+ self.rule.default_status = 'Unknown'
+ view = filterrules_views.RuleView(self.rule, {})
+ self.assertEqual('label:a label:b', view.predicate)
+ self.assertEqual('default_status', view.action_type)
+ self.assertEqual('Unknown', view.action_value)
+
+ def testDefaultOwner(self):
+ self.rule.default_owner_id = 111
+ view = filterrules_views.RuleView(
+ self.rule, {
+ 111: testing_helpers.Blank(email='jrobbins@chromium.org')})
+ self.assertEqual('label:a label:b', view.predicate)
+ self.assertEqual('default_owner', view.action_type)
+ self.assertEqual('jrobbins@chromium.org', view.action_value)
+
+ def testAddCCs(self):
+ self.rule.add_cc_ids.extend([111, 222])
+ view = filterrules_views.RuleView(
+ self.rule, {
+ 111: testing_helpers.Blank(email='jrobbins@chromium.org'),
+ 222: testing_helpers.Blank(email='jrobbins@gmail.com')})
+ self.assertEqual('label:a label:b', view.predicate)
+ self.assertEqual('add_ccs', view.action_type)
+ self.assertEqual(
+ 'jrobbins@chromium.org, jrobbins@gmail.com', view.action_value)
+
+ def testAddLabels(self):
+ self.rule.add_labels.extend(['Hot', 'Cool'])
+ view = filterrules_views.RuleView(self.rule, {})
+ self.assertEqual('label:a label:b', view.predicate)
+ self.assertEqual('add_labels', view.action_type)
+ self.assertEqual('Hot, Cool', view.action_value)
+
+ def testAlsoNotify(self):
+ self.rule.add_notify_addrs.extend(['a@dom.com', 'b@dom.com'])
+ view = filterrules_views.RuleView(self.rule, {})
+ self.assertEqual('label:a label:b', view.predicate)
+ self.assertEqual('also_notify', view.action_type)
+ self.assertEqual('a@dom.com, b@dom.com', view.action_value)
diff --git a/features/test/generate_features_test.py b/features/test/generate_features_test.py
new file mode 100644
index 0000000..8b1664e
--- /dev/null
+++ b/features/test/generate_features_test.py
@@ -0,0 +1,25 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit test for generate_features."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from features import generate_dataset
+
+
+class GenerateFeaturesTest(unittest.TestCase):
+
+ def testCleanText(self):
+ sampleText = """Here's some sample text...$*IT should l00k much\n\n\t,
+ _much_MUCH better \"cleaned\"!"""
+ self.assertEqual(generate_dataset.CleanText(sampleText),
+ ("heres some sample text it should l00k much much much "
+ "better cleaned"))
+ emptyText = ""
+ self.assertEqual(generate_dataset.CleanText(emptyText), "")
diff --git a/features/test/hotlist_helpers_test.py b/features/test/hotlist_helpers_test.py
new file mode 100644
index 0000000..800a913
--- /dev/null
+++ b/features/test/hotlist_helpers_test.py
@@ -0,0 +1,285 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for helpers module."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from features import hotlist_helpers
+from features import features_constants
+from framework import profiler
+from framework import table_view_helpers
+from framework import sorting
+from services import service_manager
+from testing import testing_helpers
+from testing import fake
+from tracker import tablecell
+from tracker import tracker_bizobj
+from proto import features_pb2
+from proto import tracker_pb2
+
+
+class HotlistTableDataTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ issue=fake.IssueService(),
+ features=fake.FeaturesService(),
+ issue_star=fake.AbstractStarService(),
+ config=fake.ConfigService(),
+ project=fake.ProjectService(),
+ user=fake.UserService(),
+ cache_manager=fake.CacheManager())
+ self.services.project.TestAddProject('ProjectName', project_id=1)
+
+ self.services.user.TestAddUser('annajowang@email.com', 111)
+ self.services.user.TestAddUser('claremont@email.com', 222)
+ issue1 = fake.MakeTestIssue(
+ 1, 1, 'issue_summary', 'New', 111, project_name='ProjectName')
+ self.services.issue.TestAddIssue(issue1)
+ issue2 = fake.MakeTestIssue(
+ 1, 2, 'issue_summary2', 'New', 111, project_name='ProjectName')
+ self.services.issue.TestAddIssue(issue2)
+ issue3 = fake.MakeTestIssue(
+ 1, 3, 'issue_summary3', 'New', 222, project_name='ProjectName')
+ self.services.issue.TestAddIssue(issue3)
+ issues = [issue1, issue2, issue3]
+ hotlist_items = [
+ (issue.issue_id, rank, 222, None, '') for
+ rank, issue in enumerate(issues)]
+
+ self.hotlist_items_list = [
+ features_pb2.MakeHotlistItem(
+ issue_id, rank=rank, adder_id=adder_id,
+ date_added=date, note=note) for (
+ issue_id, rank, adder_id, date, note) in hotlist_items]
+ self.test_hotlist = self.services.features.TestAddHotlist(
+ 'hotlist', hotlist_id=123, owner_ids=[111],
+ hotlist_item_fields=hotlist_items)
+ sorting.InitializeArtValues(self.services)
+ self.mr = None
+
+ def setUpCreateHotlistTableDataTestMR(self, **kwargs):
+ self.mr = testing_helpers.MakeMonorailRequest(**kwargs)
+ self.services.user.TestAddUser('annajo@email.com', 148)
+ self.mr.auth.effective_ids = {148}
+ self.mr.col_spec = 'ID Summary Modified'
+
+ def testCreateHotlistTableData(self):
+ self.setUpCreateHotlistTableDataTestMR(hotlist=self.test_hotlist)
+ table_data, table_related_dict = hotlist_helpers.CreateHotlistTableData(
+ self.mr, self.hotlist_items_list, self.services)
+ self.assertEqual(len(table_data), 3)
+ start_index = 100001
+ for row in table_data:
+ self.assertEqual(row.project_name, 'ProjectName')
+ self.assertEqual(row.issue_id, start_index)
+ start_index += 1
+ self.assertEqual(len(table_related_dict['column_values']), 3)
+
+ # test none of the shown columns show up in unshown_columns
+ self.assertTrue(
+ set(self.mr.col_spec.split()).isdisjoint(
+ table_related_dict['unshown_columns']))
+ self.assertEqual(table_related_dict['is_cross_project'], False)
+ self.assertEqual(len(table_related_dict['pagination'].visible_results), 3)
+
+ def testCreateHotlistTableData_Pagination(self):
+ self.setUpCreateHotlistTableDataTestMR(
+ hotlist=self.test_hotlist, path='/123?num=2')
+ table_data, _ = hotlist_helpers.CreateHotlistTableData(
+ self.mr, self.hotlist_items_list, self.services)
+ self.assertEqual(len(table_data), 2)
+
+ def testCreateHotlistTableData_EndPagination(self):
+ self.setUpCreateHotlistTableDataTestMR(
+ hotlist=self.test_hotlist, path='/123?num=2&start=2')
+ table_data, _ = hotlist_helpers.CreateHotlistTableData(
+ self.mr, self.hotlist_items_list, self.services)
+ self.assertEqual(len(table_data), 1)
+
+
+class MakeTableDataTest(unittest.TestCase):
+
+ def test_MakeTableData(self):
+ issues = [fake.MakeTestIssue(
+ 789, 1, 'issue_summary', 'New', 111, project_name='ProjectName',
+ issue_id=1001)]
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ cell_factories = {
+ 'summary': table_view_helpers.TableCellSummary}
+ table_data = hotlist_helpers._MakeTableData(
+ issues, [], ['summary'], [], {} , cell_factories,
+ {}, set(), config, None, 29, 'stars')
+ self.assertEqual(len(table_data), 1)
+ row = table_data[0]
+ self.assertEqual(row.issue_id, 1001)
+ self.assertEqual(row.local_id, 1)
+ self.assertEqual(row.project_name, 'ProjectName')
+ self.assertEqual(row.issue_ref, 'ProjectName:1')
+ self.assertTrue('hotlist_id=29' in row.issue_ctx_url)
+ self.assertTrue('sort=stars' in row.issue_ctx_url)
+
+
+class GetAllProjectsOfIssuesTest(unittest.TestCase):
+
+ issue_x_1 = tracker_pb2.Issue()
+ issue_x_1.project_id = 789
+
+ issue_x_2 = tracker_pb2.Issue()
+ issue_x_2.project_id = 789
+
+ issue_y_1 = tracker_pb2.Issue()
+ issue_y_1.project_id = 678
+
+ def testGetAllProjectsOfIssues_Normal(self):
+ issues = [self.issue_x_1, self.issue_x_2]
+ self.assertEqual(
+ hotlist_helpers.GetAllProjectsOfIssues(issues),
+ set([789]))
+ issues = [self.issue_x_1, self.issue_x_2, self.issue_y_1]
+ self.assertEqual(
+ hotlist_helpers.GetAllProjectsOfIssues(issues),
+ set([678, 789]))
+
+ def testGetAllProjectsOfIssues_Empty(self):
+ self.assertEqual(
+ hotlist_helpers.GetAllProjectsOfIssues([]),
+ set())
+
+
+class HelpersUnitTest(unittest.TestCase):
+
+ # TODO(jojwang): Write Tests for GetAllConfigsOfProjects
+ def setUp(self):
+ self.services = service_manager.Services(issue=fake.IssueService(),
+ config=fake.ConfigService(),
+ project=fake.ProjectService(),
+ features=fake.FeaturesService(),
+ user=fake.UserService())
+ self.project = self.services.project.TestAddProject(
+ 'ProjectName', project_id=1, owner_ids=[111])
+
+ self.services.user.TestAddUser('annajowang@email.com', 111)
+ self.services.user.TestAddUser('claremont@email.com', 222)
+ self.issue1 = fake.MakeTestIssue(
+ 1, 1, 'issue_summary', 'New', 111,
+ project_name='ProjectName', labels='restrict-view-Googler')
+ self.services.issue.TestAddIssue(self.issue1)
+ self.issue3 = fake.MakeTestIssue(
+ 1, 3, 'issue_summary3', 'New', 222, project_name='ProjectName')
+ self.services.issue.TestAddIssue(self.issue3)
+ self.issue4 = fake.MakeTestIssue(
+ 1, 4, 'issue_summary4', 'Fixed', 222, closed_timestamp=232423,
+ project_name='ProjectName')
+ self.services.issue.TestAddIssue(self.issue4)
+ self.issues = [self.issue1, self.issue3, self.issue4]
+ self.mr = testing_helpers.MakeMonorailRequest()
+
+ def testFilterIssues(self):
+ test_allowed_issues = hotlist_helpers.FilterIssues(
+ self.mr.cnxn, self.mr.auth, 2, self.issues, self.services)
+ self.assertEqual(len(test_allowed_issues), 1)
+ self.assertEqual(test_allowed_issues[0].local_id, 3)
+
+ def testFilterIssues_ShowClosed(self):
+ test_allowed_issues = hotlist_helpers.FilterIssues(
+ self.mr.cnxn, self.mr.auth, 1, self.issues, self.services)
+ self.assertEqual(len(test_allowed_issues), 2)
+ self.assertEqual(test_allowed_issues[0].local_id, 3)
+ self.assertEqual(test_allowed_issues[1].local_id, 4)
+
+ def testMembersWithoutGivenIDs(self):
+ h = features_pb2.Hotlist()
+ owners, editors, followers = hotlist_helpers.MembersWithoutGivenIDs(
+ h, set())
+ # Check lists are empty
+ self.assertFalse(owners)
+ self.assertFalse(editors)
+ self.assertFalse(followers)
+
+ h.owner_ids.extend([1, 2, 3])
+ h.editor_ids.extend([4, 5, 6])
+ h.follower_ids.extend([7, 8, 9])
+ owners, editors, followers = hotlist_helpers.MembersWithoutGivenIDs(
+ h, {10, 11, 12})
+ self.assertEqual(h.owner_ids, owners)
+ self.assertEqual(h.editor_ids, editors)
+ self.assertEqual(h.follower_ids, followers)
+
+ owners, editors, followers = hotlist_helpers.MembersWithoutGivenIDs(
+ h, set())
+ self.assertEqual(h.owner_ids, owners)
+ self.assertEqual(h.editor_ids, editors)
+ self.assertEqual(h.follower_ids, followers)
+
+ owners, editors, followers = hotlist_helpers.MembersWithoutGivenIDs(
+ h, {1, 4, 7})
+ self.assertEqual([2, 3], owners)
+ self.assertEqual([5, 6], editors)
+ self.assertEqual([8, 9], followers)
+
+ def testMembersWithGivenIDs(self):
+ h = features_pb2.Hotlist()
+
+ # empty GivenIDs give empty member lists from originally empty member lists
+ owners, editors, followers = hotlist_helpers.MembersWithGivenIDs(
+ h, set(), 'follower')
+ self.assertFalse(owners)
+ self.assertFalse(editors)
+ self.assertFalse(followers)
+
+ # empty GivenIDs return original non-empty member lists
+ h.owner_ids.extend([1, 2, 3])
+ h.editor_ids.extend([4, 5, 6])
+ h.follower_ids.extend([7, 8, 9])
+ owners, editors, followers = hotlist_helpers.MembersWithGivenIDs(
+ h, set(), 'editor')
+ self.assertEqual(owners, h.owner_ids)
+ self.assertEqual(editors, h.editor_ids)
+ self.assertEqual(followers, h.follower_ids)
+
+ # non-member GivenIDs return updated member lists
+ owners, editors, followers = hotlist_helpers.MembersWithGivenIDs(
+ h, {10, 11, 12}, 'owner')
+ self.assertEqual(owners, [1, 2, 3, 10, 11, 12])
+ self.assertEqual(editors, [4, 5, 6])
+ self.assertEqual(followers, [7, 8, 9])
+
+ # member GivenIDs return updated member lists
+ owners, editors, followers = hotlist_helpers.MembersWithGivenIDs(
+ h, {1, 4, 7}, 'editor')
+ self.assertEqual(owners, [2, 3])
+ self.assertEqual(editors, [5, 6, 1, 4, 7])
+ self.assertEqual(followers, [8, 9])
+
+ def testGetURLOfHotlist(self):
+ cnxn = 'fake cnxn'
+ user = self.services.user.TestAddUser('claremont@email.com', 432)
+ user.obscure_email = False
+ hotlist1 = self.services.features.TestAddHotlist(
+ 'hotlist1', hotlist_id=123, owner_ids=[432])
+ url = hotlist_helpers.GetURLOfHotlist(
+ cnxn, hotlist1, self.services.user)
+ self.assertEqual('/u/claremont@email.com/hotlists/hotlist1', url)
+
+ url = hotlist_helpers.GetURLOfHotlist(
+ cnxn, hotlist1, self.services.user, url_for_token=True)
+ self.assertEqual('/u/432/hotlists/hotlist1', url)
+
+ user.obscure_email = True
+ url = hotlist_helpers.GetURLOfHotlist(
+ cnxn, hotlist1, self.services.user)
+ self.assertEqual('/u/432/hotlists/hotlist1', url)
+
+ # Test that a Hotlist without an owner has an empty URL.
+ hotlist_unowned = self.services.features.TestAddHotlist('hotlist2',
+ hotlist_id=234, owner_ids=[])
+ url = hotlist_helpers.GetURLOfHotlist(cnxn, hotlist_unowned,
+ self.services.user)
+ self.assertFalse(url)
diff --git a/features/test/hotlist_views_test.py b/features/test/hotlist_views_test.py
new file mode 100644
index 0000000..92369ba
--- /dev/null
+++ b/features/test/hotlist_views_test.py
@@ -0,0 +1,125 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for hotlist_views classes."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from features import hotlist_views
+from framework import authdata
+from framework import framework_views
+from framework import permissions
+from services import service_manager
+from testing import fake
+from proto import user_pb2
+
+
+class MemberViewTest(unittest.TestCase):
+
+ def setUp(self):
+ self.hotlist = fake.Hotlist('hotlistName', 123,
+ hotlist_item_fields=[
+ (2, 0, None, None, ''),
+ (1, 0, None, None, ''),
+ (5, 0, None, None, '')],
+ is_private=False, owner_ids=[111])
+ self.user1 = user_pb2.User(user_id=111)
+ self.user1_view = framework_views.UserView(self.user1)
+
+ def testMemberViewCorrect(self):
+ member_view = hotlist_views.MemberView(111, 111, self.user1_view,
+ self.hotlist)
+ self.assertEqual(member_view.user, self.user1_view)
+ self.assertEqual(member_view.detail_url, '/u/111/')
+ self.assertEqual(member_view.role, 'Owner')
+ self.assertTrue(member_view.viewing_self)
+
+
+class HotlistViewTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(user=fake.UserService(),
+ usergroup=fake.UserGroupService())
+ self.user1 = self.services.user.TestAddUser('user1', 111)
+ self.user1.obscure_email = True
+ self.user1_view = framework_views.UserView(self.user1)
+ self.user2 = self.services.user.TestAddUser('user2', 222)
+ self.user2.obscure_email = False
+ self.user2_view = framework_views.UserView(self.user2)
+ self.user3 = self.services.user.TestAddUser('user3', 333)
+ self.user3_view = framework_views.UserView(self.user3)
+ self.user4 = self.services.user.TestAddUser('user4', 444, banned=True)
+ self.user4_view = framework_views.UserView(self.user4)
+
+ self.user_auth = authdata.AuthData.FromEmail(
+ None, 'user3', self.services)
+ self.user_auth.effective_ids = {3}
+ self.user_auth.user_id = 3
+ self.users_by_id = {1: self.user1_view, 2: self.user2_view,
+ 3: self.user3_view, 4: self.user4_view}
+ self.perms = permissions.EMPTY_PERMISSIONSET
+
+ def testNoOwner(self):
+ hotlist = fake.Hotlist('unowned', 500, owner_ids=[])
+ view = hotlist_views.HotlistView(hotlist, self.perms,
+ self.user_auth, 1, self.users_by_id)
+ self.assertFalse(view.url)
+
+ def testBanned(self):
+ # With a banned user
+ hotlist = fake.Hotlist('userBanned', 423, owner_ids=[4])
+ hotlist_view = hotlist_views.HotlistView(
+ hotlist, self.perms, self.user_auth, 1, self.users_by_id)
+ self.assertFalse(hotlist_view.visible)
+
+ # With a user not banned
+ hotlist = fake.Hotlist('userNotBanned', 453, owner_ids=[1])
+ hotlist_view = hotlist_views.HotlistView(
+ hotlist, self.perms, self.user_auth, 1, self.users_by_id)
+ self.assertTrue(hotlist_view.visible)
+
+ def testNoPermissions(self):
+ hotlist = fake.Hotlist(
+ 'private', 333, is_private=True, owner_ids=[1], editor_ids=[2])
+ hotlist_view = hotlist_views.HotlistView(
+ hotlist, self.perms, self.user_auth, 1, self.users_by_id)
+ self.assertFalse(hotlist_view.visible)
+ self.assertEqual(hotlist_view.url, '/u/1/hotlists/private')
+
+ def testFriendlyURL(self):
+ # owner with obscure_email:false
+ hotlist = fake.Hotlist(
+ 'noObscureHotlist', 133, owner_ids=[2], editor_ids=[3])
+ hotlist_view = hotlist_views.HotlistView(
+ hotlist, self.perms, self.user_auth,
+ viewed_user_id=3, users_by_id=self.users_by_id)
+ self.assertEqual(hotlist_view.url, '/u/user2/hotlists/noObscureHotlist')
+
+ #owner with obscure_email:true
+ hotlist = fake.Hotlist('ObscureHotlist', 133, owner_ids=[1], editor_ids=[3])
+ hotlist_view = hotlist_views.HotlistView(
+ hotlist, self.perms, self.user_auth, viewed_user_id=1,
+ users_by_id=self.users_by_id)
+ self.assertEqual(hotlist_view.url, '/u/1/hotlists/ObscureHotlist')
+
+ def testOtherAttributes(self):
+ hotlist = fake.Hotlist(
+ 'hotlistName', 123, hotlist_item_fields=[(2, 0, None, None, ''),
+ (1, 0, None, None, ''),
+ (5, 0, None, None, '')],
+ is_private=False, owner_ids=[1],
+ editor_ids=[2, 3])
+ hotlist_view = hotlist_views.HotlistView(
+ hotlist, self.perms, self.user_auth, viewed_user_id=2,
+ users_by_id=self.users_by_id, is_starred=True)
+ self.assertTrue(hotlist_view.visible, True)
+ self.assertEqual(hotlist_view.role_name, 'editor')
+ self.assertEqual(hotlist_view.owners, [self.user1_view])
+ self.assertEqual(hotlist_view.editors, [self.user2_view, self.user3_view])
+ self.assertEqual(hotlist_view.num_issues, 3)
+ self.assertTrue(hotlist_view.is_starred)
diff --git a/features/test/hotlistcreate_test.py b/features/test/hotlistcreate_test.py
new file mode 100644
index 0000000..8cf0012
--- /dev/null
+++ b/features/test/hotlistcreate_test.py
@@ -0,0 +1,148 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit test for Hotlist creation servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mox
+import unittest
+
+import settings
+from framework import permissions
+from features import hotlistcreate
+from proto import site_pb2
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+
+
+class HotlistCreateTest(unittest.TestCase):
+ """Tests for the HotlistCreate servlet."""
+
+ def setUp(self):
+ self.cnxn = 'fake cnxn'
+ self.mr = testing_helpers.MakeMonorailRequest()
+ self.services = service_manager.Services(project=fake.ProjectService(),
+ user=fake.UserService(),
+ issue=fake.IssueService(),
+ features=fake.FeaturesService())
+ self.servlet = hotlistcreate.HotlistCreate('req', 'res',
+ services=self.services)
+ self.mox = mox.Mox()
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def CheckAssertBasePermissions(
+ self, restriction, expect_admin_ok, expect_nonadmin_ok):
+ old_hotlist_creation_restriction = settings.hotlist_creation_restriction
+ settings.hotlist_creation_restriction = restriction
+
+ mr = testing_helpers.MakeMonorailRequest(
+ perms=permissions.GetPermissions(None, {}, None))
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+
+ mr = testing_helpers.MakeMonorailRequest()
+ if expect_admin_ok:
+ self.servlet.AssertBasePermission(mr)
+ else:
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+
+ mr = testing_helpers.MakeMonorailRequest(
+ perms=permissions.GetPermissions(mr.auth.user_pb, {111}, None))
+ if expect_nonadmin_ok:
+ self.servlet.AssertBasePermission(mr)
+ else:
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+
+ settings.hotlist_creation_restriction = old_hotlist_creation_restriction
+
+ def testAssertBasePermission(self):
+ self.CheckAssertBasePermissions(
+ site_pb2.UserTypeRestriction.ANYONE, True, True)
+ self.CheckAssertBasePermissions(
+ site_pb2.UserTypeRestriction.ADMIN_ONLY, True, False)
+ self.CheckAssertBasePermissions(
+ site_pb2.UserTypeRestriction.NO_ONE, False, False)
+
+ def testGatherPageData(self):
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual('st6', page_data['user_tab_mode'])
+ self.assertEqual('', page_data['initial_name'])
+ self.assertEqual('', page_data['initial_summary'])
+ self.assertEqual('', page_data['initial_description'])
+ self.assertEqual('', page_data['initial_editors'])
+ self.assertEqual('no', page_data['initial_privacy'])
+
+ def testProcessFormData(self):
+ self.servlet.services.user.TestAddUser('owner', 111)
+ self.mr.auth.user_id = 111
+ post_data = fake.PostData(hotlistname=['Hotlist'], summary=['summ'],
+ description=['hey'],
+ editors=[''], is_private=['yes'])
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertTrue('/u/111/hotlists/Hotlist' in url)
+
+ def testProcessFormData_OwnerInEditors(self):
+ self.servlet.services.user.TestAddUser('owner_editor', 222)
+ self.mr.auth.user_id = 222
+ self.mr.cnxn = 'fake cnxn'
+ post_data = fake.PostData(hotlistname=['Hotlist-owner-editor'],
+ summary=['summ'],
+ description=['hi'],
+ editors=['owner_editor'], is_private=['yes'])
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertTrue('/u/222/hotlists/Hotlist-owner-editor' in url)
+ hotlists_by_id = self.servlet.services.features.LookupHotlistIDs(
+ self.mr.cnxn, ['Hotlist-owner-editor'], [222])
+ self.assertTrue(('hotlist-owner-editor', 222) in hotlists_by_id)
+ hotlist_id = hotlists_by_id[('hotlist-owner-editor', 222)]
+ hotlist = self.servlet.services.features.GetHotlist(
+ self.mr.cnxn, hotlist_id, use_cache=False)
+ self.assertEqual(hotlist.owner_ids, [222])
+ self.assertEqual(hotlist.editor_ids, [])
+
+ def testProcessFormData_RejectTemplateInvalid(self):
+ mr = testing_helpers.MakeMonorailRequest()
+ # invalid hotlist name and nonexistent editor
+ post_data = fake.PostData(hotlistname=['123BadName'], summary=['summ'],
+ description=['hey'],
+ editors=['test@email.com'], is_private=['yes'])
+ self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+ self.servlet.PleaseCorrect(
+ mr, initial_name = '123BadName', initial_summary='summ',
+ initial_description='hey',
+ initial_editors='test@email.com', initial_privacy='yes')
+ self.mox.ReplayAll()
+ url = self.servlet.ProcessFormData(mr, post_data)
+ self.mox.VerifyAll()
+ self.assertEqual(mr.errors.hotlistname, 'Invalid hotlist name')
+ self.assertEqual(mr.errors.editors,
+ 'One or more editor emails is not valid.')
+ self.assertIsNone(url)
+
+ def testProcessFormData_RejectTemplateMissing(self):
+ mr = testing_helpers.MakeMonorailRequest()
+ # missing name and summary
+ post_data = fake.PostData()
+ self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+ self.servlet.PleaseCorrect(mr, initial_name = None, initial_summary=None,
+ initial_description='',
+ initial_editors='', initial_privacy=None)
+ self.mox.ReplayAll()
+ url = self.servlet.ProcessFormData(mr, post_data)
+ self.mox.VerifyAll()
+ self.assertEqual(mr.errors.hotlistname, 'Missing hotlist name')
+ self.assertEqual(mr.errors.summary,'Missing hotlist summary')
+ self.assertIsNone(url)
diff --git a/features/test/hotlistdetails_test.py b/features/test/hotlistdetails_test.py
new file mode 100644
index 0000000..9a9e53f
--- /dev/null
+++ b/features/test/hotlistdetails_test.py
@@ -0,0 +1,226 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for hotlistdetails page."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import mox
+import unittest
+import mock
+
+import ezt
+
+from framework import permissions
+from features import features_constants
+from services import service_manager
+from features import hotlistdetails
+from proto import features_pb2
+from testing import fake
+from testing import testing_helpers
+
+class HotlistDetailsTest(unittest.TestCase):
+ """Unit tests for the HotlistDetails servlet class."""
+
+ def setUp(self):
+ self.user_service = fake.UserService()
+ self.user_1 = self.user_service.TestAddUser('111@test.com', 111)
+ self.user_2 = self.user_service.TestAddUser('user2@test.com', 222)
+ services = service_manager.Services(
+ features=fake.FeaturesService(), user=self.user_service)
+ self.servlet = hotlistdetails.HotlistDetails(
+ 'req', 'res', services=services)
+ self.hotlist = self.servlet.services.features.TestAddHotlist(
+ 'hotlist', summary='hotlist summary', description='hotlist description',
+ owner_ids=[111], editor_ids=[222])
+ self.request, self.mr = testing_helpers.GetRequestObjects(
+ hotlist=self.hotlist)
+ self.mr.auth.user_id = 111
+ self.private_hotlist = services.features.TestAddHotlist(
+ 'private_hotlist', owner_ids=[111], editor_ids=[222], is_private=True)
+ self.mox = mox.Mox()
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def testAssertBasePermission(self):
+ # non-members cannot view private hotlists
+ mr = testing_helpers.MakeMonorailRequest(
+ hotlist=self.private_hotlist, perms=permissions.EMPTY_PERMISSIONSET)
+ mr.auth.effective_ids = {333}
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+
+ # members can view private hotlists
+ mr = testing_helpers.MakeMonorailRequest(
+ hotlist=self.private_hotlist)
+ mr.auth.effective_ids = {222, 444}
+ self.servlet.AssertBasePermission(mr)
+
+ # non-members can view public hotlists
+ mr = testing_helpers.MakeMonorailRequest(
+ hotlist=self.hotlist)
+ mr.auth.effective_ids = {333, 444}
+ self.servlet.AssertBasePermission(mr)
+
+ # members can view public hotlists
+ mr = testing_helpers.MakeMonorailRequest(
+ hotlist=self.hotlist)
+ mr.auth.effective_ids = {111, 333}
+ self.servlet.AssertBasePermission(mr)
+
+ def testGatherPageData(self):
+ self.mr.auth.effective_ids = [222]
+ self.mr.perms = permissions.EMPTY_PERMISSIONSET
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual('hotlist summary', page_data['initial_summary'])
+ self.assertEqual('hotlist description', page_data['initial_description'])
+ self.assertEqual('hotlist', page_data['initial_name'])
+ self.assertEqual(features_constants.DEFAULT_COL_SPEC,
+ page_data['initial_default_col_spec'])
+ self.assertEqual(ezt.boolean(False), page_data['initial_is_private'])
+
+ # editor is viewing, so cant_administer_hotlist is True
+ self.assertEqual(ezt.boolean(True), page_data['cant_administer_hotlist'])
+
+ # owner is veiwing, so cant_administer_hotlist is False
+ self.mr.auth.effective_ids = [111]
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual(ezt.boolean(False), page_data['cant_administer_hotlist'])
+
+ def testProcessFormData(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ hotlist=self.hotlist,
+ path='/u/111/hotlists/%s/details' % self.hotlist.hotlist_id,
+ services=service_manager.Services(user=self.user_service),
+ perms=permissions.EMPTY_PERMISSIONSET)
+ mr.auth.effective_ids = {111}
+ mr.auth.user_id = 111
+ post_data = fake.PostData(
+ name=['hotlist'],
+ summary = ['hotlist summary'],
+ description = ['hotlist description'],
+ default_col_spec = ['test default col spec'])
+ url = self.servlet.ProcessFormData(mr, post_data)
+ self.assertTrue((
+ '/u/111/hotlists/%d/details?saved=' % self.hotlist.hotlist_id) in url)
+
+ @mock.patch('features.hotlist_helpers.RemoveHotlist')
+ def testProcessFormData_DeleteHotlist(self, fake_rh):
+ mr = testing_helpers.MakeMonorailRequest(
+ hotlist=self.hotlist,
+ path='/u/111/hotlists/%s/details' % self.hotlist.hotlist_id,
+ services=service_manager.Services(user=self.user_service),
+ perms=permissions.EMPTY_PERMISSIONSET)
+ mr.auth.effective_ids = {self.user_1.user_id}
+ mr.auth.user_id = self.user_1.user_id
+ mr.auth.email = self.user_1.email
+
+ post_data = fake.PostData(deletestate=['true'])
+ url = self.servlet.ProcessFormData(mr, post_data)
+ fake_rh.assert_called_once_with(
+ mr.cnxn, mr.hotlist_id, self.servlet.services)
+ self.assertTrue(('/u/%s/hotlists?saved=' % self.user_1.email) in url)
+
+ def testProcessFormData_RejectTemplate(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ hotlist=self.hotlist,
+ path='/u/111/hotlists/%s/details' % self.hotlist.hotlist_id,
+ services=service_manager.Services(user=self.user_service),
+ perms=permissions.EMPTY_PERMISSIONSET)
+ mr.auth.user_id = 111
+ mr.auth.effective_ids = {111}
+ post_data = fake.PostData(
+ summary = [''],
+ name = [''],
+ description = ['fake description'],
+ default_col_spec = ['test default col spec'])
+ self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+ self.servlet.PleaseCorrect(
+ mr, initial_summary='',
+ initial_description='fake description', initial_name = '',
+ initial_default_col_spec = 'test default col spec')
+ self.mox.ReplayAll()
+
+ url = self.servlet.ProcessFormData(mr, post_data)
+ self.mox.VerifyAll()
+ self.assertEqual(hotlistdetails._MSG_NAME_MISSING, mr.errors.name)
+ self.assertEqual(hotlistdetails._MSG_SUMMARY_MISSING,
+ mr.errors.summary)
+ self.assertIsNone(url)
+
+ def testProcessFormData_DuplicateName(self):
+ self.servlet.services.features.TestAddHotlist(
+ 'FirstHotlist', summary='hotlist summary', description='description',
+ owner_ids=[111], editor_ids=[])
+ mr = testing_helpers.MakeMonorailRequest(
+ hotlist=self.hotlist,
+ path='/u/111/hotlists/%s/details' % (self.hotlist.hotlist_id),
+ services=service_manager.Services(user=self.user_service),
+ perms=permissions.EMPTY_PERMISSIONSET)
+ mr.auth.user_id = 111
+ mr.auth.effective_ids = {111}
+ post_data = fake.PostData(
+ summary = ['hotlist summary'],
+ name = ['FirstHotlist'],
+ description = ['description'],
+ default_col_spec = ['test default col spec'])
+ self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+ self.servlet.PleaseCorrect(
+ mr, initial_summary='hotlist summary',
+ initial_description='description', initial_name = 'FirstHotlist',
+ initial_default_col_spec = 'test default col spec')
+ self.mox.ReplayAll()
+
+ url = self.servlet.ProcessFormData(mr, post_data)
+ self.mox.VerifyAll()
+ self.assertEqual(hotlistdetails._MSG_HOTLIST_NAME_NOT_AVAIL,
+ mr.errors.name)
+ self.assertIsNone(url)
+
+ def testProcessFormData_Bad(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ hotlist=self.hotlist,
+ path='/u/111/hotlists/%s/details' % (self.hotlist.hotlist_id),
+ services=service_manager.Services(user=self.user_service),
+ perms=permissions.EMPTY_PERMISSIONSET)
+ mr.auth.user_id = 111
+ mr.auth.effective_ids = {111}
+ post_data = fake.PostData(
+ summary = ['hotlist summary'],
+ name = ['2badName'],
+ description = ['fake description'],
+ default_col_spec = ['test default col spec'])
+ self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+ self.servlet.PleaseCorrect(
+ mr, initial_summary='hotlist summary',
+ initial_description='fake description', initial_name = '2badName',
+ initial_default_col_spec = 'test default col spec')
+ self.mox.ReplayAll()
+
+ url = self.servlet.ProcessFormData(mr, post_data)
+ self.mox.VerifyAll()
+ self.assertEqual(hotlistdetails._MSG_INVALID_HOTLIST_NAME,
+ mr.errors.name)
+ self.assertIsNone(url)
+
+ def testProcessFormData_NoPermissions(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ hotlist=self.hotlist,
+ path='/u/111/hotlists/%s/details' % (self.hotlist.hotlist_id),
+ services=service_manager.Services(user=self.user_service),
+ perms=permissions.EMPTY_PERMISSIONSET)
+ mr.auth.user_id = self.user_2.user_id
+ mr.auth.effective_ids = {self.user_2.user_id}
+ post_data = fake.PostData(
+ summary = ['hotlist summary'],
+ name = ['hotlist'],
+ description = ['fake description'],
+ default_col_spec = ['test default col spec'])
+ with self.assertRaises(permissions.PermissionException):
+ self.servlet.ProcessFormData(mr, post_data)
diff --git a/features/test/hotlistissues_test.py b/features/test/hotlistissues_test.py
new file mode 100644
index 0000000..49c3270
--- /dev/null
+++ b/features/test/hotlistissues_test.py
@@ -0,0 +1,211 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for issuelist module."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mox
+import mock
+import unittest
+import time
+
+from google.appengine.ext import testbed
+import ezt
+
+from features import hotlistissues
+from features import hotlist_helpers
+from framework import framework_views
+from framework import permissions
+from framework import sorting
+from framework import template_helpers
+from framework import xsrf
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+
+
+class HotlistIssuesUnitTest(unittest.TestCase):
+
+ def setUp(self):
+ self.testbed = testbed.Testbed()
+ self.testbed.activate()
+ self.testbed.init_memcache_stub()
+ self.testbed.init_datastore_v3_stub()
+ self.services = service_manager.Services(
+ issue_star=fake.IssueStarService(),
+ config=fake.ConfigService(),
+ user=fake.UserService(),
+ issue=fake.IssueService(),
+ project=fake.ProjectService(),
+ features=fake.FeaturesService(),
+ cache_manager=fake.CacheManager(),
+ hotlist_star=fake.HotlistStarService())
+ self.servlet = hotlistissues.HotlistIssues(
+ 'req', 'res', services=self.services)
+ self.user1 = self.services.user.TestAddUser('testuser@gmail.com', 111)
+ self.user2 = self.services.user.TestAddUser('testuser2@gmail.com', 222, )
+ self.services.project.TestAddProject('project-name', project_id=1)
+ self.issue1 = fake.MakeTestIssue(
+ 1, 1, 'issue_summary', 'New', 111, project_name='project-name')
+ self.services.issue.TestAddIssue(self.issue1)
+ self.issue2 = fake.MakeTestIssue(
+ 1, 2, 'issue_summary2', 'New', 111, project_name='project-name')
+ self.services.issue.TestAddIssue(self.issue2)
+ self.issue3 = fake.MakeTestIssue(
+ 1, 3, 'issue_summary3', 'New', 222, project_name='project-name')
+ self.services.issue.TestAddIssue(self.issue3)
+ self.issues = [self.issue1, self.issue2, self.issue3]
+ self.hotlist_item_fields = [
+ (issue.issue_id, rank, 111, 1205079300, '') for
+ rank, issue in enumerate(self.issues)]
+ self.test_hotlist = self.services.features.TestAddHotlist(
+ 'hotlist', hotlist_id=123, owner_ids=[222], editor_ids=[111],
+ hotlist_item_fields=self.hotlist_item_fields)
+ self.hotlistissues = self.test_hotlist.items
+ # Unless perms is specified,
+ # MakeMonorailRequest will return an mr with admin permissions.
+ self.mr = testing_helpers.MakeMonorailRequest(
+ hotlist=self.test_hotlist, path='/u/222/hotlists/123',
+ services=self.services, perms=permissions.EMPTY_PERMISSIONSET)
+ self.mr.hotlist_id = self.test_hotlist.hotlist_id
+ self.mr.auth.user_id = 111
+ self.mr.auth.effective_ids = {111}
+ self.mr.viewed_user_auth.user_id = 111
+ sorting.InitializeArtValues(self.services)
+
+ self.mox = mox.Mox()
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.testbed.deactivate()
+
+ def testAssertBasePermissions(self):
+ private_hotlist = self.services.features.TestAddHotlist(
+ 'privateHotlist', hotlist_id=321, owner_ids=[222],
+ hotlist_item_fields=self.hotlist_item_fields, is_private=True)
+ # non-members cannot view private hotlists
+ mr = testing_helpers.MakeMonorailRequest(
+ hotlist=private_hotlist, perms=permissions.EMPTY_PERMISSIONSET)
+ mr.auth.effective_ids = {333}
+ mr.hotlist_id = private_hotlist.hotlist_id
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+
+ # members can view private hotlists
+ mr = testing_helpers.MakeMonorailRequest(
+ hotlist=private_hotlist, perms=permissions.EMPTY_PERMISSIONSET)
+ mr.auth.effective_ids = {222, 444}
+ mr.hotlist_id = private_hotlist.hotlist_id
+ self.servlet.AssertBasePermission(mr)
+
+ # non-members can view public hotlists
+ mr = testing_helpers.MakeMonorailRequest(
+ hotlist=self.test_hotlist, perms=permissions.EMPTY_PERMISSIONSET)
+ mr.auth.effective_ids = {333, 444}
+ mr.hotlist_id = self.test_hotlist.hotlist_id
+ self.servlet.AssertBasePermission(mr)
+
+ # members can view public hotlists
+ mr = testing_helpers.MakeMonorailRequest(
+ hotlist=self.test_hotlist, perms=permissions.EMPTY_PERMISSIONSET)
+ mr.auth.effective_ids = {111, 333}
+ mr.hotlist_id = self.test_hotlist.hotlist_id
+ self.servlet.AssertBasePermission(mr)
+
+ def testGatherPageData(self):
+ self.mr.mode = 'list'
+ self.mr.auth.effective_ids = {111}
+ self.mr.auth.user_id = 111
+ self.mr.sort_spec = 'rank stars'
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual(ezt.boolean(False), page_data['owner_permissions'])
+ self.assertEqual(ezt.boolean(True), page_data['editor_permissions'])
+ self.assertEqual(ezt.boolean(False), page_data['grid_mode'])
+ self.assertEqual(ezt.boolean(True), page_data['allow_rerank'])
+
+ self.mr.sort_spec = 'stars ranks'
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual(ezt.boolean(False), page_data['allow_rerank'])
+
+ def testGetTableViewData(self):
+ now = time.time()
+ self.mox.StubOutWithMock(time, 'time')
+ time.time().MultipleTimes().AndReturn(now)
+ self.mox.ReplayAll()
+
+ self.mr.auth.user_id = 222
+ self.mr.col_spec = 'Stars Projects Rank'
+ table_view_data = self.servlet.GetTableViewData(self.mr)
+ self.assertEqual(table_view_data['edit_hotlist_token'], xsrf.GenerateToken(
+ self.mr.auth.user_id, '/u/222/hotlists/hotlist.do'))
+ self.assertEqual(table_view_data['add_issues_selected'], ezt.boolean(False))
+
+ self.user2.obscure_email = False
+ table_view_data = self.servlet.GetTableViewData(self.mr)
+ self.assertEqual(table_view_data['edit_hotlist_token'], xsrf.GenerateToken(
+ self.mr.auth.user_id, '/u/222/hotlists/hotlist.do'))
+ self.mox.VerifyAll()
+
+ def testGetGridViewData(self):
+ # TODO(jojwang): Write this test
+ pass
+
+ def testProcessFormData_NoNewIssues(self):
+ post_data = fake.PostData(remove=['false'], add_local_ids=[''])
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertTrue(url.endswith('u/222/hotlists/hotlist'))
+ self.assertEqual(self.test_hotlist.items, self.hotlistissues)
+
+ def testProcessFormData_AddBadIssueRef(self):
+ self.servlet.PleaseCorrect = mock.Mock()
+ post_data = fake.PostData(
+ remove=['false'], add_local_ids=['no-such-project:999'])
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertIsNone(url)
+ self.servlet.PleaseCorrect.assert_called_once()
+
+ def testProcessFormData_RemoveBadIssueRef(self):
+ post_data = fake.PostData(
+ remove=['true'], add_local_ids=['no-such-project:999'])
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertIn('u/222/hotlists/hotlist', url)
+ self.assertEqual(self.test_hotlist.items, self.hotlistissues)
+
+ def testProcessFormData_NormalEditIssues(self):
+ issue4 = fake.MakeTestIssue(
+ 1, 4, 'issue_summary4', 'New', 222, project_name='project-name')
+ self.services.issue.TestAddIssue(issue4)
+ issue5 = fake.MakeTestIssue(
+ 1, 5, 'issue_summary5', 'New', 222, project_name='project-name')
+ self.services.issue.TestAddIssue(issue5)
+
+ post_data = fake.PostData(remove=['false'],
+ add_local_ids=['project-name:4, project-name:5'])
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertTrue('u/222/hotlists/hotlist' in url)
+ self.assertEqual(len(self.test_hotlist.items), 5)
+ self.assertEqual(
+ self.test_hotlist.items[3].issue_id, issue4.issue_id)
+ self.assertEqual(
+ self.test_hotlist.items[4].issue_id, issue5.issue_id)
+
+ post_data = fake.PostData(remove=['true'], remove_local_ids=[
+ 'project-name:4, project-name:1, project-name:2'])
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertTrue('u/222/hotlists/hotlist' in url)
+ self.assertTrue(len(self.test_hotlist.items), 2)
+ issue_ids = [issue.issue_id for issue in self.test_hotlist.items]
+ self.assertTrue(issue5.issue_id in issue_ids)
+ self.assertTrue(self.issue3.issue_id in issue_ids)
+
+ def testProcessFormData_NoPermissions(self):
+ post_data = fake.PostData(remove=['false'],
+ add_local_ids=['project-name:4, project-name:5'])
+ self.mr.auth.effective_ids = {333}
+ self.mr.auth.user_id = 333
+ with self.assertRaises(permissions.PermissionException):
+ self.servlet.ProcessFormData(self.mr, post_data)
diff --git a/features/test/hotlistissuescsv_test.py b/features/test/hotlistissuescsv_test.py
new file mode 100644
index 0000000..afa53d5
--- /dev/null
+++ b/features/test/hotlistissuescsv_test.py
@@ -0,0 +1,97 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for issuelistcsv module."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from google.appengine.ext import testbed
+
+import webapp2
+
+from framework import permissions
+from framework import sorting
+from framework import xsrf
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+from features import hotlistissuescsv
+
+
+class HotlistIssuesCsvTest(unittest.TestCase):
+
+ def setUp(self):
+ self.testbed = testbed.Testbed()
+ self.testbed.activate()
+ self.testbed.init_memcache_stub()
+ self.testbed.init_datastore_v3_stub()
+ self.services = service_manager.Services(
+ issue_star=fake.IssueStarService(),
+ config=fake.ConfigService(),
+ user=fake.UserService(),
+ issue=fake.IssueService(),
+ project=fake.ProjectService(),
+ cache_manager=fake.CacheManager(),
+ features=fake.FeaturesService())
+ self.servlet = hotlistissuescsv.HotlistIssuesCsv(
+ 'req', webapp2.Response(), services=self.services)
+ self.user1 = self.services.user.TestAddUser('testuser@gmail.com', 111)
+ self.user2 = self.services.user.TestAddUser('testuser2@gmail.com', 222)
+ self.services.project.TestAddProject('project-name', project_id=1)
+ self.issue1 = fake.MakeTestIssue(
+ 1, 1, 'issue_summary', 'New', 111, project_name='project-name')
+ self.services.issue.TestAddIssue(self.issue1)
+ self.issues = [self.issue1]
+ self.hotlist_item_fields = [
+ (issue.issue_id, rank, 111, 1205079300, '') for
+ rank, issue in enumerate(self.issues)]
+ self.hotlist = self.services.features.TestAddHotlist(
+ 'MyHotlist', hotlist_id=123, owner_ids=[222], editor_ids=[111],
+ hotlist_item_fields=self.hotlist_item_fields)
+ self._MakeMR('/u/222/hotlists/MyHotlist')
+ sorting.InitializeArtValues(self.services)
+
+ def _MakeMR(self, path):
+ self.mr = testing_helpers.MakeMonorailRequest(
+ hotlist=self.hotlist, path=path, services=self.services)
+ self.mr.hotlist_id = self.hotlist.hotlist_id
+ self.mr.hotlist = self.hotlist
+
+ def testGatherPageData_AnonUsers(self):
+ """Anonymous users cannot download the issue list."""
+ self.mr.auth.user_id = 0
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.GatherPageData, self.mr)
+
+ def testGatherPageData_NoXSRF(self):
+ """Users need a valid XSRF token to download the issue list."""
+ # Note no token query-string parameter is set.
+ self.mr.auth.user_id = self.user2.user_id
+ self.assertRaises(xsrf.TokenIncorrect,
+ self.servlet.GatherPageData, self.mr)
+
+ def testGatherPageData_BadXSRF(self):
+ """Users need a valid XSRF token to download the issue list."""
+ for path in ('/u/222/hotlists/MyHotlist',
+ '/u/testuser2@gmail.com/hotlists/MyHotlist'):
+ token = 'bad'
+ self._MakeMR(path + '?token=%s' % token)
+ self.mr.auth.user_id = self.user2.user_id
+ self.assertRaises(xsrf.TokenIncorrect,
+ self.servlet.GatherPageData, self.mr)
+
+ def testGatherPageData_Normal(self):
+ """Users can get the hotlist issue list."""
+ for path in ('/u/222/hotlists/MyHotlist',
+ '/u/testuser2@gmail.com/hotlists/MyHotlist'):
+ form_token_path = self.servlet._FormHandlerURL(path)
+ token = xsrf.GenerateToken(self.user1.user_id, form_token_path)
+ self._MakeMR(path + '?token=%s' % token)
+ self.mr.auth.email = self.user1.email
+ self.mr.auth.user_id = self.user1.user_id
+ self.servlet.GatherPageData(self.mr)
diff --git a/features/test/hotlistpeople_test.py b/features/test/hotlistpeople_test.py
new file mode 100644
index 0000000..74beec3
--- /dev/null
+++ b/features/test/hotlistpeople_test.py
@@ -0,0 +1,253 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unittest for Hotlist People servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mox
+import unittest
+import logging
+
+import ezt
+
+from testing import fake
+from features import hotlistpeople
+from framework import permissions
+from services import service_manager
+from testing import testing_helpers
+
+class HotlistPeopleListTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ features=fake.FeaturesService(),
+ project=fake.ProjectService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService())
+ self.owner_user = self.services.user.TestAddUser('buzbuz@gmail.com', 111)
+ self.editor_user = self.services.user.TestAddUser('monica@gmail.com', 222)
+ self.non_member_user = self.services.user.TestAddUser(
+ 'who-dis@gmail.com', 333)
+ self.private_hotlist = self.services.features.TestAddHotlist(
+ 'PrivateHotlist', 'owner only', [111], [222], is_private=True)
+ self.public_hotlist = self.services.features.TestAddHotlist(
+ 'PublicHotlist', 'everyone', [111], [222], is_private=False)
+ self.servlet = hotlistpeople.HotlistPeopleList(
+ 'req', 'res', services=self.services)
+ self.mox = mox.Mox()
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def testAssertBasePermission(self):
+ # owner can view people in private hotlist
+ mr = testing_helpers.MakeMonorailRequest(
+ hotlist=self.private_hotlist, perms=permissions.EMPTY_PERMISSIONSET)
+ mr.auth.effective_ids = {111, 444}
+ self.servlet.AssertBasePermission(mr)
+
+ # editor can view people in private hotlist
+ mr.auth.effective_ids = {222, 333}
+ self.servlet.AssertBasePermission(mr)
+
+ # non-members cannot view people in private hotlist
+ mr.auth.effective_ids = {444, 333}
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+
+ # owner can view people in public hotlist
+ mr = testing_helpers.MakeMonorailRequest(hotlist=self.public_hotlist)
+ mr.auth.effective_ids = {111, 444}
+ self.servlet.AssertBasePermission(mr)
+
+ # editor can view people in public hotlist
+ mr.auth.effective_ids = {222, 333}
+ self.servlet.AssertBasePermission(mr)
+
+ # non-members cannot view people in public hotlist
+ mr.auth.effective_ids = {444, 333}
+ self.servlet.AssertBasePermission(mr)
+
+ def testGatherPageData(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ hotlist=self.public_hotlist, perms=permissions.EMPTY_PERMISSIONSET)
+ mr.auth.user_id = 111
+ mr.auth.effective_ids = {111}
+ mr.cnxn = 'fake cnxn'
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual(ezt.boolean(True), page_data['offer_membership_editing'])
+ self.assertEqual(ezt.boolean(False), page_data['offer_remove_self'])
+ self.assertEqual(page_data['total_num_owners'], 1)
+ self.assertEqual(page_data['newly_added_views'], [])
+ self.assertEqual(len(page_data['pagination'].visible_results), 2)
+
+ # non-owners cannot edit people list
+ mr.auth.user_id = 222
+ mr.auth.effective_ids = {222}
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual(ezt.boolean(False), page_data['offer_membership_editing'])
+ self.assertEqual(ezt.boolean(True), page_data['offer_remove_self'])
+
+ mr.auth.user_id = 333
+ mr.auth.effective_ids = {333}
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual(ezt.boolean(False), page_data['offer_membership_editing'])
+ self.assertEqual(ezt.boolean(False), page_data['offer_remove_self'])
+
+ def testProcessFormData_Permission(self):
+ """Only owner can change member of hotlist."""
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/u/buzbuz@gmail.com/hotlists/PrivateHotlist/people',
+ hotlist=self.private_hotlist, perms=permissions.EMPTY_PERMISSIONSET)
+ mr.auth.effective_ids = {111, 444}
+ self.servlet.ProcessFormData(mr, {})
+
+ mr.auth.effective_ids = {222, 444}
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.ProcessFormData, mr, {})
+
+ def testProcessRemoveMembers(self):
+ hotlist = self.servlet.services.features.TestAddHotlist(
+ 'HotlistName', 'removing 222, monica', [111], [222])
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/u/buzbuz@gmail.com/hotlists/HotlistName/people',
+ hotlist=hotlist)
+ mr.hotlist_id = hotlist.hotlist_id
+ post_data = fake.PostData(
+ remove = ['monica@gmail.com'])
+ url = self.servlet.ProcessRemoveMembers(
+ mr, post_data, '/u/111/hotlists/HotlistName')
+ self.assertTrue('/u/111/hotlists/HotlistName/people' in url)
+ self.assertEqual(hotlist.editor_ids, [])
+
+ def testProcessRemoveSelf(self):
+ hotlist = self.servlet.services.features.TestAddHotlist(
+ 'HotlistName', 'self removing 222, monica', [111], [222])
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/u/buzbuz@gmail.com/hotlists/HotlistName/people',
+ hotlist=hotlist)
+ mr.hotlist_id = hotlist.hotlist_id
+ mr.cnxn = 'fake cnxn'
+ # The owner cannot be removed using ProcessRemoveSelf(); this is enforced
+ # by permission in ProcessFormData, not in the function itself;
+ # nor may a random user...
+ mr.auth.user_id = 333
+ mr.auth.effective_ids = {333}
+ url = self.servlet.ProcessRemoveSelf(mr, '/u/111/hotlists/HotlistName')
+ self.assertTrue('/u/111/hotlists/HotlistName/people' in url)
+ self.assertEqual(hotlist.owner_ids, [111])
+ self.assertEqual(hotlist.editor_ids, [222])
+ # ...but an editor can.
+ mr.auth.user_id = 222
+ mr.auth.effective_ids = {222}
+ url = self.servlet.ProcessRemoveSelf(mr, '/u/111/hotlists/HotlistName')
+ self.assertTrue('/u/111/hotlists/HotlistName/people' in url)
+ self.assertEqual(hotlist.owner_ids, [111])
+ self.assertEqual(hotlist.editor_ids, [])
+
+ def testProcessAddMembers(self):
+ hotlist = self.servlet.services.features.TestAddHotlist(
+ 'HotlistName', 'adding 333, who-dis', [111], [222])
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/u/buzbuz@gmail.com/hotlists/HotlistName/people',
+ hotlist=hotlist)
+ mr.hotlist_id = hotlist.hotlist_id
+ post_data = fake.PostData(
+ addmembers = ['who-dis@gmail.com'],
+ role = ['editor'])
+ url = self.servlet.ProcessAddMembers(
+ mr, post_data, '/u/111/hotlists/HotlistName')
+ self.assertTrue('/u/111/hotlists/HotlistName/people' in url)
+ self.assertEqual(hotlist.editor_ids, [222, 333])
+
+ def testProcessAddMembers_OwnerToEditor(self):
+ hotlist = self.servlet.services.features.TestAddHotlist(
+ 'HotlistName', 'adding owner 111, buzbuz as editor', [111], [222])
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/u/buzbuz@gmail.com/hotlists/HotlistName/people',
+ hotlist=hotlist)
+ mr.hotlist_id = hotlist.hotlist_id
+ addmembers_input = 'buzbuz@gmail.com'
+ post_data = fake.PostData(
+ addmembers = [addmembers_input],
+ role = ['editor'])
+ self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+ self.servlet.PleaseCorrect(
+ mr, initial_add_members=addmembers_input, initially_expand_form=True)
+ self.mox.ReplayAll()
+ url = self.servlet.ProcessAddMembers(
+ mr, post_data, '/u/111/hotlists/HotlistName')
+ self.mox.VerifyAll()
+ self.assertEqual(
+ 'Cannot have a hotlist without an owner; please leave at least one.',
+ mr.errors.addmembers)
+ self.assertIsNone(url)
+ # Verify that no changes have actually occurred.
+ self.assertEqual(hotlist.owner_ids, [111])
+ self.assertEqual(hotlist.editor_ids, [222])
+
+ def testProcessChangeOwnership(self):
+ hotlist = self.servlet.services.features.TestAddHotlist(
+ 'HotlistName', 'new owner 333, who-dis', [111], [222])
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/u/buzbuz@gmail.com/hotlists/HotlistName/people',
+ hotlist=hotlist)
+ mr.hotlist_id = hotlist.hotlist_id
+ post_data = fake.PostData(
+ changeowners = ['who-dis@gmail.com'],
+ becomeeditor = ['on'])
+ url = self.servlet.ProcessChangeOwnership(mr, post_data)
+ self.assertTrue('/u/333/hotlists/HotlistName/people' in url)
+ self.assertEqual(hotlist.owner_ids, [333])
+ self.assertEqual(hotlist.editor_ids, [222, 111])
+
+ def testProcessChangeOwnership_UnownedHotlist(self):
+ hotlist = self.services.features.TestAddHotlist(
+ 'unowned', 'new owner 333, who-dis', [], [222])
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/whatever',
+ hotlist=hotlist)
+ mr.hotlist_id = hotlist.hotlist_id
+ post_data = fake.PostData(
+ changeowners = ['who-dis@gmail.com'],
+ becomeeditor = ['on'])
+ self.servlet.ProcessChangeOwnership(mr, post_data)
+ self.assertEqual([333], mr.hotlist.owner_ids)
+
+ def testProcessChangeOwnership_BadEmail(self):
+ hotlist = self.servlet.services.features.TestAddHotlist(
+ 'HotlistName', 'new owner 333, who-dis', [111], [222])
+ mr = testing_helpers.MakeMonorailRequest(
+ path='/u/buzbuz@gmail.com/hotlists/HotlistName/people',
+ hotlist=hotlist)
+ mr.hotlist_id = hotlist.hotlist_id
+ changeowners_input = 'who-dis@gmail.com, extra-email@gmail.com'
+ post_data = fake.PostData(
+ changeowners = [changeowners_input],
+ becomeeditor = ['on'])
+ self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+ self.servlet.PleaseCorrect(
+ mr, initial_new_owner_username=changeowners_input, open_dialog='yes')
+ self.mox.ReplayAll()
+ url = self.servlet.ProcessChangeOwnership(mr, post_data)
+ self.mox.VerifyAll()
+ self.assertEqual(
+ 'Please add one valid user email.', mr.errors.transfer_ownership)
+ self.assertIsNone(url)
+
+ def testProcessChangeOwnership_DuplicateName(self):
+ # other_hotlist = self.servlet.services.features.TestAddHotlist(
+ # 'HotlistName', 'hotlist with same name', [333], [])
+ # hotlist = self.servlet.services.features.TestAddHotlist(
+ # 'HotlistName', 'new owner 333, who-dis', [111], [222])
+
+ # in the test_hotlists dict of features_service in testing/fake
+ # 'other_hotlist' is overwritten by 'hotlist'
+ # TODO(jojwang): edit the fake features_service to allow hotlists
+ # with the same name but different owners
+ pass
diff --git a/features/test/inboundemail_test.py b/features/test/inboundemail_test.py
new file mode 100644
index 0000000..6c13827
--- /dev/null
+++ b/features/test/inboundemail_test.py
@@ -0,0 +1,400 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unittests for monorail.feature.inboundemail."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+import webapp2
+from mock import patch
+
+import mox
+import time
+
+from google.appengine.ext.webapp.mail_handlers import BounceNotificationHandler
+
+import settings
+from businesslogic import work_env
+from features import alert2issue
+from features import commitlogcommands
+from features import inboundemail
+from framework import authdata
+from framework import emailfmt
+from framework import monorailcontext
+from framework import permissions
+from proto import project_pb2
+from proto import tracker_pb2
+from proto import user_pb2
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+from tracker import tracker_helpers
+
+
+class InboundEmailTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = 'fake cnxn'
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService())
+ self.project = self.services.project.TestAddProject(
+ 'proj', project_id=987, process_inbound_email=True,
+ contrib_ids=[111])
+ self.project_addr = 'proj@monorail.example.com'
+
+ self.issue = tracker_pb2.Issue()
+ self.issue.project_id = 987
+ self.issue.local_id = 100
+ self.services.issue.TestAddIssue(self.issue)
+
+ self.msg = testing_helpers.MakeMessage(
+ testing_helpers.HEADER_LINES, 'awesome!')
+
+ request, _ = testing_helpers.GetRequestObjects()
+ self.inbound = inboundemail.InboundEmail(request, None, self.services)
+ self.mox = mox.Mox()
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def testTemplates(self):
+ for name, template_path in self.inbound._templates.items():
+ assert(name in inboundemail.MSG_TEMPLATES)
+ assert(
+ template_path.GetTemplatePath().endswith(
+ inboundemail.MSG_TEMPLATES[name]))
+
+ def testProcessMail_MsgTooBig(self):
+ self.mox.StubOutWithMock(emailfmt, 'IsBodyTooBigToParse')
+ emailfmt.IsBodyTooBigToParse(mox.IgnoreArg()).AndReturn(True)
+ self.mox.ReplayAll()
+
+ email_tasks = self.inbound.ProcessMail(self.msg, self.project_addr)
+ self.mox.VerifyAll()
+ self.assertEqual(1, len(email_tasks))
+ email_task = email_tasks[0]
+ self.assertEqual('user@example.com', email_task['to'])
+ self.assertEqual('Email body too long', email_task['subject'])
+
+ def testProcessMail_NoProjectOnToLine(self):
+ self.mox.StubOutWithMock(emailfmt, 'IsProjectAddressOnToLine')
+ emailfmt.IsProjectAddressOnToLine(
+ self.project_addr, [self.project_addr]).AndReturn(False)
+ self.mox.ReplayAll()
+
+ ret = self.inbound.ProcessMail(self.msg, self.project_addr)
+ self.mox.VerifyAll()
+ self.assertIsNone(ret)
+
+ def testProcessMail_IssueUnidentified(self):
+ self.mox.StubOutWithMock(emailfmt, 'IdentifyProjectVerbAndLabel')
+ emailfmt.IdentifyProjectVerbAndLabel(self.project_addr).AndReturn(('proj',
+ None, None))
+
+ self.mox.StubOutWithMock(emailfmt, 'IdentifyIssue')
+ emailfmt.IdentifyIssue('proj', mox.IgnoreArg()).AndReturn((None))
+
+ self.mox.ReplayAll()
+
+ ret = self.inbound.ProcessMail(self.msg, self.project_addr)
+ self.mox.VerifyAll()
+ self.assertIsNone(ret)
+
+ def testProcessMail_ProjectNotLive(self):
+ self.services.user.TestAddUser('user@example.com', 111)
+ self.project.state = project_pb2.ProjectState.DELETABLE
+ email_tasks = self.inbound.ProcessMail(self.msg, self.project_addr)
+ email_task = email_tasks[0]
+ self.assertEqual('user@example.com', email_task['to'])
+ self.assertEqual('Project not found', email_task['subject'])
+
+ def testProcessMail_ProjectInboundEmailDisabled(self):
+ self.services.user.TestAddUser('user@example.com', 111)
+ self.project.process_inbound_email = False
+ email_tasks = self.inbound.ProcessMail(self.msg, self.project_addr)
+ email_task = email_tasks[0]
+ self.assertEqual('user@example.com', email_task['to'])
+ self.assertEqual(
+ 'Email replies are not enabled in project proj', email_task['subject'])
+
+ def testProcessMail_NoRefHeader(self):
+ self.services.user.TestAddUser('user@example.com', 111)
+ self.mox.StubOutWithMock(emailfmt, 'ValidateReferencesHeader')
+ emailfmt.ValidateReferencesHeader(
+ mox.IgnoreArg(), self.project, mox.IgnoreArg(),
+ mox.IgnoreArg()).AndReturn(False)
+ emailfmt.ValidateReferencesHeader(
+ mox.IgnoreArg(), self.project, mox.IgnoreArg(),
+ mox.IgnoreArg()).AndReturn(False)
+ self.mox.ReplayAll()
+
+ email_tasks = self.inbound.ProcessMail(self.msg, self.project_addr)
+ self.mox.VerifyAll()
+ self.assertEqual(1, len(email_tasks))
+ email_task = email_tasks[0]
+ self.assertEqual('user@example.com', email_task['to'])
+ self.assertEqual(
+ 'Your message is not a reply to a notification email',
+ email_task['subject'])
+
+ def testProcessMail_NoAccount(self):
+ # Note: not calling TestAddUser().
+ email_tasks = self.inbound.ProcessMail(self.msg, self.project_addr)
+ self.mox.VerifyAll()
+ self.assertEqual(1, len(email_tasks))
+ email_task = email_tasks[0]
+ self.assertEqual('user@example.com', email_task['to'])
+ self.assertEqual(
+ 'Could not determine account of sender', email_task['subject'])
+
+ def testProcessMail_BannedAccount(self):
+ user_pb = self.services.user.TestAddUser('user@example.com', 111)
+ user_pb.banned = 'banned'
+
+ self.mox.StubOutWithMock(emailfmt, 'ValidateReferencesHeader')
+ emailfmt.ValidateReferencesHeader(
+ mox.IgnoreArg(), self.project, mox.IgnoreArg(),
+ mox.IgnoreArg()).AndReturn(True)
+ self.mox.ReplayAll()
+
+ email_tasks = self.inbound.ProcessMail(self.msg, self.project_addr)
+ self.mox.VerifyAll()
+ self.assertEqual(1, len(email_tasks))
+ email_task = email_tasks[0]
+ self.assertEqual('user@example.com', email_task['to'])
+ self.assertEqual(
+ 'You are banned from using this issue tracker', email_task['subject'])
+
+ def testProcessMail_Success(self):
+ self.services.user.TestAddUser('user@example.com', 111)
+
+ self.mox.StubOutWithMock(emailfmt, 'ValidateReferencesHeader')
+ emailfmt.ValidateReferencesHeader(
+ mox.IgnoreArg(), self.project, mox.IgnoreArg(),
+ mox.IgnoreArg()).AndReturn(True)
+
+ self.mox.StubOutWithMock(self.inbound, 'ProcessIssueReply')
+ self.inbound.ProcessIssueReply(
+ mox.IgnoreArg(), self.project, 123, self.project_addr,
+ 'awesome!')
+
+ self.mox.ReplayAll()
+
+ ret = self.inbound.ProcessMail(self.msg, self.project_addr)
+ self.mox.VerifyAll()
+ self.assertIsNone(ret)
+
+ def testProcessMail_Success_with_AlertNotification(self):
+ """Test ProcessMail with an alert notification message.
+
+ This is a sanity check for alert2issue.ProcessEmailNotification to ensure
+ that it can be successfully invoked in ProcessMail. Each function of
+ alert2issue module should be tested in aler2issue_test.
+ """
+ project_name = self.project.project_name
+ verb = 'alert'
+ trooper_queue = 'my-trooper'
+ project_addr = '%s+%s+%s@example.com' % (project_name, verb, trooper_queue)
+
+ self.mox.StubOutWithMock(emailfmt, 'IsProjectAddressOnToLine')
+ emailfmt.IsProjectAddressOnToLine(
+ project_addr, mox.IgnoreArg()).AndReturn(True)
+
+ class MockAuthData(object):
+ def __init__(self):
+ self.user_pb = user_pb2.MakeUser(111)
+ self.effective_ids = set([1, 2, 3])
+ self.user_id = 111
+ self.email = 'user@example.com'
+
+ mock_auth_data = MockAuthData()
+ self.mox.StubOutWithMock(authdata.AuthData, 'FromEmail')
+ authdata.AuthData.FromEmail(
+ mox.IgnoreArg(), settings.alert_service_account, self.services,
+ autocreate=True).AndReturn(mock_auth_data)
+
+ self.mox.StubOutWithMock(alert2issue, 'ProcessEmailNotification')
+ alert2issue.ProcessEmailNotification(
+ self.services, mox.IgnoreArg(), self.project, project_addr,
+ mox.IgnoreArg(), mock_auth_data, mox.IgnoreArg(), 'awesome!', '',
+ self.msg, trooper_queue)
+
+ self.mox.ReplayAll()
+ ret = self.inbound.ProcessMail(self.msg, project_addr)
+ self.mox.VerifyAll()
+ self.assertIsNone(ret)
+
+ def testProcessIssueReply_NoIssue(self):
+ nonexistant_local_id = 200
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ email_tasks = self.inbound.ProcessIssueReply(
+ mc, self.project, nonexistant_local_id, self.project_addr,
+ 'awesome!')
+ self.assertEqual(1, len(email_tasks))
+ email_task = email_tasks[0]
+ self.assertEqual('user@example.com', email_task['to'])
+ self.assertEqual(
+ 'Could not find issue %d in project %s' %
+ (nonexistant_local_id, self.project.project_name),
+ email_task['subject'])
+
+ def testProcessIssueReply_DeletedIssue(self):
+ self.issue.deleted = True
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ email_tasks = self.inbound.ProcessIssueReply(
+ mc, self.project, self.issue.local_id, self.project_addr,
+ 'awesome!')
+ self.assertEqual(1, len(email_tasks))
+ email_task = email_tasks[0]
+ self.assertEqual('user@example.com', email_task['to'])
+ self.assertEqual(
+ 'Could not find issue %d in project %s' %
+ (self.issue.local_id, self.project.project_name), email_task['subject'])
+
+ def VerifyUserHasNoPerm(self, perms):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@example.com')
+ mc.perms = perms
+
+ email_tasks = self.inbound.ProcessIssueReply(
+ mc, self.project, self.issue.local_id, self.project_addr,
+ 'awesome!')
+ self.assertEqual(1, len(email_tasks))
+ email_task = email_tasks[0]
+ self.assertEqual('user@example.com', email_task['to'])
+ self.assertEqual(
+ 'User does not have permission to add a comment', email_task['subject'])
+
+ def testProcessIssueReply_NoViewPerm(self):
+ self.VerifyUserHasNoPerm(permissions.EMPTY_PERMISSIONSET)
+
+ def testProcessIssueReply_CantViewRestrictedIssue(self):
+ self.issue.labels.append('Restrict-View-CoreTeam')
+ self.VerifyUserHasNoPerm(permissions.USER_PERMISSIONSET)
+
+ def testProcessIssueReply_NoAddIssuePerm(self):
+ self.VerifyUserHasNoPerm(permissions.READ_ONLY_PERMISSIONSET)
+
+ def testProcessIssueReply_NoEditIssuePerm(self):
+ self.services.user.TestAddUser('user@example.com', 111)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@example.com')
+ mc.perms = permissions.USER_PERMISSIONSET
+ mock_uia = commitlogcommands.UpdateIssueAction(self.issue.local_id)
+
+ self.mox.StubOutWithMock(commitlogcommands, 'UpdateIssueAction')
+ commitlogcommands.UpdateIssueAction(self.issue.local_id).AndReturn(mock_uia)
+
+ self.mox.StubOutWithMock(mock_uia, 'Parse')
+ mock_uia.Parse(
+ self.cnxn, self.project.project_name, 111, ['awesome!'], self.services,
+ strip_quoted_lines=True)
+ self.mox.StubOutWithMock(mock_uia, 'Run')
+ # mc.perms does not contain permission EDIT_ISSUE.
+ mock_uia.Run(mc, self.services)
+
+ self.mox.ReplayAll()
+ ret = self.inbound.ProcessIssueReply(
+ mc, self.project, self.issue.local_id, self.project_addr,
+ 'awesome!')
+ self.mox.VerifyAll()
+ self.assertIsNone(ret)
+
+ def testProcessIssueReply_Success(self):
+ self.services.user.TestAddUser('user@example.com', 111)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@example.com')
+ mc.perms = permissions.COMMITTER_ACTIVE_PERMISSIONSET
+ mock_uia = commitlogcommands.UpdateIssueAction(self.issue.local_id)
+
+ self.mox.StubOutWithMock(commitlogcommands, 'UpdateIssueAction')
+ commitlogcommands.UpdateIssueAction(self.issue.local_id).AndReturn(mock_uia)
+
+ self.mox.StubOutWithMock(mock_uia, 'Parse')
+ mock_uia.Parse(
+ self.cnxn, self.project.project_name, 111, ['awesome!'], self.services,
+ strip_quoted_lines=True)
+ self.mox.StubOutWithMock(mock_uia, 'Run')
+ mock_uia.Run(mc, self.services)
+
+ self.mox.ReplayAll()
+ ret = self.inbound.ProcessIssueReply(
+ mc, self.project, self.issue.local_id, self.project_addr,
+ 'awesome!')
+ self.mox.VerifyAll()
+ self.assertIsNone(ret)
+
+
+class BouncedEmailTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = 'fake cnxn'
+ self.services = service_manager.Services(
+ 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.mox = mox.Mox()
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def testPost_Normal(self):
+ """Normally, our post() just calls BounceNotificationHandler post()."""
+ self.mox.StubOutWithMock(BounceNotificationHandler, 'post')
+ BounceNotificationHandler.post()
+ self.mox.ReplayAll()
+
+ self.servlet.post()
+ self.mox.VerifyAll()
+
+ def testPost_Exception(self):
+ """Our post() method works around an escaping bug."""
+ self.servlet.request = webapp2.Request.blank(
+ '/', POST={'raw-message': 'this is an email message'})
+
+ self.mox.StubOutWithMock(BounceNotificationHandler, 'post')
+ BounceNotificationHandler.post().AndRaise(AttributeError())
+ BounceNotificationHandler.post()
+ self.mox.ReplayAll()
+
+ self.servlet.post()
+ self.mox.VerifyAll()
+
+ def testReceive_Normal(self):
+ """Find the user that bounced and set email_bounce_timestamp."""
+ self.assertEqual(0, self.user.email_bounce_timestamp)
+
+ bounce_message = testing_helpers.Blank(original={'to': 'user@example.com'})
+ self.servlet.receive(bounce_message)
+
+ self.assertNotEqual(0, self.user.email_bounce_timestamp)
+
+ 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')
+ self.servlet.receive(bounce_message)
+ self.assertEqual(1, len(self.services.user.users_by_id))
diff --git a/features/test/notify_helpers_test.py b/features/test/notify_helpers_test.py
new file mode 100644
index 0000000..615da38
--- /dev/null
+++ b/features/test/notify_helpers_test.py
@@ -0,0 +1,617 @@
+# -*- coding: utf-8 -*-
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for notify_helpers.py."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import json
+import mock
+import unittest
+import os
+
+from features import features_constants
+from features import notify_helpers
+from features import notify_reasons
+from framework import emailfmt
+from framework import framework_views
+from framework import urls
+from proto import user_pb2
+from services import service_manager
+from testing import fake
+
+
+REPLY_NOT_ALLOWED = notify_reasons.REPLY_NOT_ALLOWED
+REPLY_MAY_COMMENT = notify_reasons.REPLY_MAY_COMMENT
+REPLY_MAY_UPDATE = notify_reasons.REPLY_MAY_UPDATE
+
+
+class TaskQueueingFunctionsTest(unittest.TestCase):
+
+ @mock.patch('framework.cloud_tasks_helpers._get_client')
+ def testAddAllEmailTasks(self, get_client_mock):
+ notify_helpers.AddAllEmailTasks(
+ tasks=[{'to': 'user'}, {'to': 'user2'}])
+
+ self.assertEqual(get_client_mock().create_task.call_count, 2)
+
+ queue_call_args = get_client_mock().queue_path.call_args_list
+ ((_app_id, _region, queue), _kwargs) = queue_call_args[0]
+ self.assertEqual(queue, features_constants.QUEUE_OUTBOUND_EMAIL)
+ ((_app_id, _region, queue), _kwargs) = queue_call_args[1]
+ self.assertEqual(queue, features_constants.QUEUE_OUTBOUND_EMAIL)
+
+ task_call_args = get_client_mock().create_task.call_args_list
+ ((_parent, task), _kwargs) = task_call_args[0]
+ expected_task = {
+ 'app_engine_http_request':
+ {
+ 'relative_uri': urls.OUTBOUND_EMAIL_TASK + '.do',
+ 'body': json.dumps({
+ 'to': 'user'
+ }).encode(),
+ 'headers': {
+ 'Content-type': 'application/json'
+ }
+ }
+ }
+ self.assertEqual(task, expected_task)
+ ((_parent, task), _kwargs) = task_call_args[1]
+ expected_task = {
+ 'app_engine_http_request':
+ {
+ 'relative_uri': urls.OUTBOUND_EMAIL_TASK + '.do',
+ 'body': json.dumps({
+ 'to': 'user2'
+ }).encode(),
+ 'headers': {
+ 'Content-type': 'application/json'
+ }
+ }
+ }
+ self.assertEqual(task, expected_task)
+
+
+class MergeLinkedAccountReasonsTest(unittest.TestCase):
+
+ def setUp(self):
+ parent = user_pb2.User(
+ user_id=111, email='parent@example.org',
+ linked_child_ids=[222])
+ child = user_pb2.User(
+ user_id=222, email='child@example.org',
+ linked_parent_id=111)
+ user_3 = user_pb2.User(
+ user_id=333, email='user4@example.org')
+ user_4 = user_pb2.User(
+ user_id=444, email='user4@example.org')
+ self.addr_perm_parent = notify_reasons.AddrPerm(
+ False, parent.email, parent, notify_reasons.REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs())
+ self.addr_perm_child = notify_reasons.AddrPerm(
+ False, child.email, child, notify_reasons.REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs())
+ self.addr_perm_3 = notify_reasons.AddrPerm(
+ False, user_3.email, user_3, notify_reasons.REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs())
+ self.addr_perm_4 = notify_reasons.AddrPerm(
+ False, user_4.email, user_4, notify_reasons.REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs())
+ self.addr_perm_5 = notify_reasons.AddrPerm(
+ False, 'alias@example.com', None, notify_reasons.REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs())
+
+ def testEmptyDict(self):
+ """Zero users to notify."""
+ self.assertEqual(
+ {},
+ notify_helpers._MergeLinkedAccountReasons({}, {}))
+
+ def testNormal(self):
+ """No users are related."""
+ addr_to_addrperm = {
+ self.addr_perm_parent.address: self.addr_perm_parent,
+ self.addr_perm_3.address: self.addr_perm_3,
+ self.addr_perm_4.address: self.addr_perm_4,
+ self.addr_perm_5.address: self.addr_perm_5,
+ }
+ addr_to_reasons = {
+ self.addr_perm_parent.address: [notify_reasons.REASON_CCD],
+ self.addr_perm_3.address: [notify_reasons.REASON_OWNER],
+ self.addr_perm_4.address: [notify_reasons.REASON_CCD],
+ self.addr_perm_5.address: [notify_reasons.REASON_CCD],
+ }
+ self.assertEqual(
+ {self.addr_perm_parent.address: [notify_reasons.REASON_CCD],
+ self.addr_perm_3.address: [notify_reasons.REASON_OWNER],
+ self.addr_perm_4.address: [notify_reasons.REASON_CCD],
+ self.addr_perm_5.address: [notify_reasons.REASON_CCD]
+ },
+ notify_helpers._MergeLinkedAccountReasons(
+ addr_to_addrperm, addr_to_reasons))
+
+ def testMerged(self):
+ """A child is merged into parent notification."""
+ addr_to_addrperm = {
+ self.addr_perm_parent.address: self.addr_perm_parent,
+ self.addr_perm_child.address: self.addr_perm_child,
+ }
+ addr_to_reasons = {
+ self.addr_perm_parent.address: [notify_reasons.REASON_OWNER],
+ self.addr_perm_child.address: [notify_reasons.REASON_CCD],
+ }
+ self.assertEqual(
+ {self.addr_perm_parent.address:
+ [notify_reasons.REASON_OWNER,
+ notify_reasons.REASON_LINKED_ACCOUNT]
+ },
+ notify_helpers._MergeLinkedAccountReasons(
+ addr_to_addrperm, addr_to_reasons))
+
+
+class MakeBulletedEmailWorkItemsTest(unittest.TestCase):
+
+ def setUp(self):
+ self.project = fake.Project(project_name='proj1')
+ self.commenter_view = framework_views.StuffUserView(
+ 111, 'test@example.com', True)
+ self.issue = fake.MakeTestIssue(
+ self.project.project_id, 1234, 'summary', 'New', 111)
+ self.detail_url = 'http://test-detail-url.com/id=1234'
+
+ def testEmptyAddrs(self):
+ """Test the case where we found zero users to notify."""
+ email_tasks = notify_helpers.MakeBulletedEmailWorkItems(
+ [], self.issue, 'link only body', 'non-member body', 'member body',
+ self.project, 'example.com',
+ self.commenter_view, self.detail_url)
+ self.assertEqual([], email_tasks)
+ email_tasks = notify_helpers.MakeBulletedEmailWorkItems(
+ [([], 'reason')], self.issue, 'link only body', 'non-member body',
+ 'member body', self.project,
+ 'example.com', self.commenter_view, self.detail_url)
+ self.assertEqual([], email_tasks)
+
+
+class LinkOnlyLogicTest(unittest.TestCase):
+
+ def setUp(self):
+ self.user_prefs = user_pb2.UserPrefs()
+ self.user = user_pb2.User()
+ self.issue = fake.MakeTestIssue(
+ 789, 1, 'summary one', 'New', 111)
+ self.rvg_issue = fake.MakeTestIssue(
+ 789, 2, 'summary two', 'New', 111, labels=['Restrict-View-Google'])
+ self.more_restricted_issue = fake.MakeTestIssue(
+ 789, 3, 'summary three', 'New', 111, labels=['Restrict-View-Core'])
+ self.both_restricted_issue = fake.MakeTestIssue(
+ 789, 4, 'summary four', 'New', 111,
+ labels=['Restrict-View-Google', 'Restrict-View-Core'])
+ self.addr_perm = notify_reasons.AddrPerm(
+ False, 'user@example.com', self.user, notify_reasons.REPLY_MAY_COMMENT,
+ self.user_prefs)
+
+ def testGetNotifyRestrictedIssues_NoPrefsPassed(self):
+ """AlsoNotify and all-issues addresses have no UserPrefs. None is used."""
+ actual = notify_helpers._GetNotifyRestrictedIssues(
+ None, 'user@example.com', self.user)
+ self.assertEqual('notify with link only', actual)
+
+ self.user.last_visit_timestamp = 123456789
+ actual = notify_helpers._GetNotifyRestrictedIssues(
+ None, 'user@example.com', self.user)
+ self.assertEqual('notify with details', actual)
+
+ def testGetNotifyRestrictedIssues_PrefIsSet(self):
+ """When the notify_restricted_issues pref is set, we use it."""
+ self.user_prefs.prefs.extend([
+ user_pb2.UserPrefValue(name='x', value='y'),
+ user_pb2.UserPrefValue(name='notify_restricted_issues', value='z'),
+ ])
+ actual = notify_helpers._GetNotifyRestrictedIssues(
+ self.user_prefs, 'user@example.com', self.user)
+ self.assertEqual('z', actual)
+
+ def testGetNotifyRestrictedIssues_UserHasVisited(self):
+ """If user has ever visited, we know that they are not a mailing list."""
+ self.user.last_visit_timestamp = 123456789
+ actual = notify_helpers._GetNotifyRestrictedIssues(
+ self.user_prefs, 'user@example.com', self.user)
+ self.assertEqual('notify with details', actual)
+
+ def testGetNotifyRestrictedIssues_GooglerNeverVisited(self):
+ """It could be a noogler or google mailing list."""
+ actual = notify_helpers._GetNotifyRestrictedIssues(
+ self.user_prefs, 'user@google.com', self.user)
+ self.assertEqual('notify with details: Google', actual)
+
+ def testGetNotifyRestrictedIssues_NonGooglerNeverVisited(self):
+ """It could be a new non-noogler or public mailing list."""
+ actual = notify_helpers._GetNotifyRestrictedIssues(
+ self.user_prefs, 'user@example.com', self.user)
+ self.assertEqual('notify with link only', actual)
+
+ # If email does not match any known user, user object will be None.
+ actual = notify_helpers._GetNotifyRestrictedIssues(
+ self.user_prefs, 'user@example.com', None)
+ self.assertEqual('notify with link only', actual)
+
+ def testShouldUseLinkOnly_UnrestrictedIssue(self):
+ """Issue is not restricted, so go ahead and send comment details."""
+ self.assertFalse(notify_helpers.ShouldUseLinkOnly(
+ self.addr_perm, self.issue))
+
+ def testShouldUseLinkOnly_AlwaysDetailed(self):
+ """Issue is not restricted, so go ahead and send comment details."""
+ self.assertFalse(
+ notify_helpers.ShouldUseLinkOnly(self.addr_perm, self.issue, True))
+
+ @mock.patch('features.notify_helpers._GetNotifyRestrictedIssues')
+ def testShouldUseLinkOnly_NotifyWithDetails(self, fake_gnri):
+ """Issue is restricted, and user is allowed to get full comment details."""
+ fake_gnri.return_value = notify_helpers.NOTIFY_WITH_DETAILS
+ self.assertFalse(notify_helpers.ShouldUseLinkOnly(
+ self.addr_perm, self.rvg_issue))
+ self.assertFalse(notify_helpers.ShouldUseLinkOnly(
+ self.addr_perm, self.more_restricted_issue))
+ self.assertFalse(notify_helpers.ShouldUseLinkOnly(
+ self.addr_perm, self.both_restricted_issue))
+
+
+class MakeEmailWorkItemTest(unittest.TestCase):
+
+ def setUp(self):
+ self.project = fake.Project(project_name='proj1')
+ self.project.process_inbound_email = True
+ self.project2 = fake.Project(project_name='proj2')
+ self.project2.issue_notify_always_detailed = True
+ self.commenter_view = framework_views.StuffUserView(
+ 111, 'test@example.com', True)
+ self.expected_html_footer = (
+ 'You received this message because:<br/> 1. reason<br/><br/>You may '
+ 'adjust your notification preferences at:<br/><a href="https://'
+ 'example.com/hosting/settings">https://example.com/hosting/settings'
+ '</a>')
+ self.services = service_manager.Services(
+ user=fake.UserService())
+ self.member = self.services.user.TestAddUser('member@example.com', 222)
+ self.issue = fake.MakeTestIssue(
+ self.project.project_id, 1234, 'summary', 'New', 111,
+ project_name='proj1')
+ self.detail_url = 'http://test-detail-url.com/id=1234'
+
+ @mock.patch('features.notify_helpers.ShouldUseLinkOnly')
+ def testBodySelection_LinkOnly(self, mock_sulo):
+ """We send a link-only body when ShouldUseLinkOnly() is true."""
+ mock_sulo.return_value = True
+ email_task = notify_helpers._MakeEmailWorkItem(
+ notify_reasons.AddrPerm(
+ True, 'a@a.com', self.member, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs()),
+ ['reason'], self.issue,
+ 'body link-only', 'body mem', 'body mem', self.project,
+ 'example.com', self.commenter_view, self.detail_url)
+ self.assertIn('body link-only', email_task['body'])
+
+ def testBodySelection_Member(self):
+ """We send members the email body that is indented for members."""
+ email_task = notify_helpers._MakeEmailWorkItem(
+ notify_reasons.AddrPerm(
+ True, 'a@a.com', self.member, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs()),
+ ['reason'], self.issue,
+ 'body link-only', 'body mem', 'body mem', self.project,
+ 'example.com', self.commenter_view, self.detail_url)
+ self.assertIn('body mem', email_task['body'])
+
+ def testBodySelection_AlwaysDetailed(self):
+ """Always send full email when project configuration requires it."""
+ email_task = notify_helpers._MakeEmailWorkItem(
+ notify_reasons.AddrPerm(
+ True, 'a@a.com', self.member, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs()), ['reason'], self.issue, 'body link-only',
+ 'body mem', 'body mem', self.project2, 'example.com',
+ self.commenter_view, self.detail_url)
+ self.assertIn('body mem', email_task['body'])
+
+ def testBodySelection_NonMember(self):
+ """We send non-members the email body that is indented for non-members."""
+ email_task = notify_helpers._MakeEmailWorkItem(
+ notify_reasons.AddrPerm(
+ False, 'a@a.com', self.member, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs()),
+ ['reason'], self.issue,
+ 'body link-only', 'body non', 'body mem', self.project,
+ 'example.com', self.commenter_view, self.detail_url)
+
+ self.assertEqual('a@a.com', email_task['to'])
+ self.assertEqual('Issue 1234 in proj1: summary', email_task['subject'])
+ self.assertIn('body non', email_task['body'])
+ self.assertEqual(
+ emailfmt.FormatFromAddr(self.project, commenter_view=self.commenter_view,
+ can_reply_to=False),
+ email_task['from_addr'])
+ self.assertEqual(emailfmt.NoReplyAddress(), email_task['reply_to'])
+
+ def testHtmlBody(self):
+ """"An html body is sent if a detail_url is specified."""
+ email_task = notify_helpers._MakeEmailWorkItem(
+ notify_reasons.AddrPerm(
+ False, 'a@a.com', self.member, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs()),
+ ['reason'], self.issue,
+ 'body link-only', 'body non', 'body mem', self.project,
+ 'example.com', self.commenter_view, self.detail_url)
+
+ expected_html_body = (
+ notify_helpers.HTML_BODY_WITH_GMAIL_ACTION_TEMPLATE % {
+ 'url': self.detail_url,
+ 'body': 'body non-- <br/>%s' % self.expected_html_footer})
+ self.assertEqual(expected_html_body, email_task['html_body'])
+
+ def testHtmlBody_WithUnicodeChars(self):
+ """"An html body is sent if a detail_url is specified."""
+ unicode_content = '\xe2\x9d\xa4 â â'
+ email_task = notify_helpers._MakeEmailWorkItem(
+ notify_reasons.AddrPerm(
+ False, 'a@a.com', self.member, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs()),
+ ['reason'], self.issue,
+ 'body link-only', unicode_content, 'unused body mem',
+ self.project, 'example.com', self.commenter_view, self.detail_url)
+
+ expected_html_body = (
+ notify_helpers.HTML_BODY_WITH_GMAIL_ACTION_TEMPLATE % {
+ 'url': self.detail_url,
+ 'body': '%s-- <br/>%s' % (unicode_content.decode('utf-8'),
+ self.expected_html_footer)})
+ self.assertEqual(expected_html_body, email_task['html_body'])
+
+ def testHtmlBody_WithLinks(self):
+ """"An html body is sent if a detail_url is specified."""
+ email_task = notify_helpers._MakeEmailWorkItem(
+ notify_reasons.AddrPerm(
+ False, 'a@a.com', self.member, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs()),
+ ['reason'], self.issue,
+ 'body link-only', 'test google.com test', 'unused body mem',
+ self.project, 'example.com', self.commenter_view, self.detail_url)
+
+ expected_html_body = (
+ notify_helpers.HTML_BODY_WITH_GMAIL_ACTION_TEMPLATE % {
+ 'url': self.detail_url,
+ 'body': (
+ 'test <a href="http://google.com">google.com</a> test-- <br/>%s' % (
+ self.expected_html_footer))})
+ self.assertEqual(expected_html_body, email_task['html_body'])
+
+ def testHtmlBody_LinkWithinTags(self):
+ """"An html body is sent with correct <a href>s."""
+ email_task = notify_helpers._MakeEmailWorkItem(
+ notify_reasons.AddrPerm(
+ False, 'a@a.com', self.member, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs()),
+ ['reason'], self.issue,
+ 'body link-only', 'a <http://google.com> z', 'unused body',
+ self.project, 'example.com', self.commenter_view,
+ self.detail_url)
+
+ expected_html_body = (
+ notify_helpers.HTML_BODY_WITH_GMAIL_ACTION_TEMPLATE % {
+ 'url': self.detail_url,
+ 'body': (
+ 'a <<a href="http://google.com">http://google.com</a>> '
+ 'z-- <br/>%s' % self.expected_html_footer)})
+ self.assertEqual(expected_html_body, email_task['html_body'])
+
+ def testHtmlBody_EmailWithinTags(self):
+ """"An html body is sent with correct <a href>s."""
+ email_task = notify_helpers._MakeEmailWorkItem(
+ notify_reasons.AddrPerm(
+ False, 'a@a.com', self.member, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs()),
+ ['reason'], self.issue,
+ 'body link-only', 'a <tt@chromium.org> <aa@chromium.org> z',
+ 'unused body mem', self.project, 'example.com', self.commenter_view,
+ self.detail_url)
+
+ expected_html_body = (
+ notify_helpers.HTML_BODY_WITH_GMAIL_ACTION_TEMPLATE % {
+ 'url': self.detail_url,
+ 'body': (
+ 'a <<a href="mailto:tt@chromium.org">tt@chromium.org</a>>'
+ ' <<a href="mailto:aa@chromium.org">aa@chromium.org</a>> '
+ 'z-- <br/>%s' % self.expected_html_footer)})
+ self.assertEqual(expected_html_body, email_task['html_body'])
+
+ def testHtmlBody_WithEscapedHtml(self):
+ """"An html body is sent with html content escaped."""
+ body_with_html_content = (
+ '<a href="http://www.google.com">test</a> \'something\'')
+ email_task = notify_helpers._MakeEmailWorkItem(
+ notify_reasons.AddrPerm(
+ False, 'a@a.com', self.member, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs()),
+ ['reason'], self.issue,
+ 'body link-only', body_with_html_content, 'unused body mem',
+ self.project, 'example.com', self.commenter_view, self.detail_url)
+
+ escaped_body_with_html_content = (
+ '<a href="http://www.google.com">test</a> '
+ ''something'')
+ notify_helpers._MakeNotificationFooter(
+ ['reason'], REPLY_NOT_ALLOWED, 'example.com')
+ expected_html_body = (
+ notify_helpers.HTML_BODY_WITH_GMAIL_ACTION_TEMPLATE % {
+ 'url': self.detail_url,
+ 'body': '%s-- <br/>%s' % (escaped_body_with_html_content,
+ self.expected_html_footer)})
+ self.assertEqual(expected_html_body, email_task['html_body'])
+
+ def doTestAddHTMLTags(self, body, expected):
+ actual = notify_helpers._AddHTMLTags(body)
+ self.assertEqual(expected, actual)
+
+ def testAddHTMLTags_Email(self):
+ """An email address produces <a href="mailto:...">...</a>."""
+ self.doTestAddHTMLTags(
+ 'test test@example.com.',
+ ('test <a href="mailto:test@example.com">'
+ 'test@example.com</a>.'))
+
+ def testAddHTMLTags_EmailInQuotes(self):
+ """Quoted "test@example.com" produces "<a href="...">...</a>"."""
+ self.doTestAddHTMLTags(
+ 'test "test@example.com".',
+ ('test "<a href="mailto:test@example.com">'
+ 'test@example.com</a>".'))
+
+ def testAddHTMLTags_EmailInAngles(self):
+ """Bracketed <test@example.com> produces <<a href="...">...</a>>."""
+ self.doTestAddHTMLTags(
+ 'test <test@example.com>.',
+ ('test <<a href="mailto:test@example.com">'
+ 'test@example.com</a>>.'))
+
+ def testAddHTMLTags_Website(self):
+ """A website URL produces <a href="http:...">...</a>."""
+ self.doTestAddHTMLTags(
+ 'test http://www.example.com.',
+ ('test <a href="http://www.example.com">'
+ 'http://www.example.com</a>.'))
+
+ def testAddHTMLTags_WebsiteInQuotes(self):
+ """A link in quotes gets the quotes escaped."""
+ self.doTestAddHTMLTags(
+ 'test "http://www.example.com".',
+ ('test "<a href="http://www.example.com">'
+ 'http://www.example.com</a>".'))
+
+ def testAddHTMLTags_WebsiteInAngles(self):
+ """Bracketed <www.example.com> produces <<a href="...">...</a>>."""
+ self.doTestAddHTMLTags(
+ 'test <http://www.example.com>.',
+ ('test <<a href="http://www.example.com">'
+ 'http://www.example.com</a>>.'))
+
+ def testReplyInvitation(self):
+ """We include a footer about replying that is appropriate for that user."""
+ email_task = notify_helpers._MakeEmailWorkItem(
+ notify_reasons.AddrPerm(
+ True, 'a@a.com', self.member, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs()),
+ ['reason'], self.issue,
+ 'body link-only', 'body non', 'body mem', self.project,
+ 'example.com', self.commenter_view, self.detail_url)
+ self.assertEqual(emailfmt.NoReplyAddress(), email_task['reply_to'])
+ self.assertNotIn('Reply to this email', email_task['body'])
+
+ email_task = notify_helpers._MakeEmailWorkItem(
+ notify_reasons.AddrPerm(
+ True, 'a@a.com', self.member, REPLY_MAY_COMMENT,
+ user_pb2.UserPrefs()),
+ ['reason'], self.issue,
+ 'body link-only', 'body non', 'body mem', self.project,
+ 'example.com', self.commenter_view, self.detail_url)
+ self.assertEqual(
+ '%s@%s' % (self.project.project_name, emailfmt.MailDomain()),
+ email_task['reply_to'])
+ self.assertIn('Reply to this email to add a comment', email_task['body'])
+ self.assertNotIn('make changes', email_task['body'])
+
+ email_task = notify_helpers._MakeEmailWorkItem(
+ notify_reasons.AddrPerm(
+ True, 'a@a.com', self.member, REPLY_MAY_UPDATE,
+ user_pb2.UserPrefs()),
+ ['reason'], self.issue,
+ 'body link-only', 'body non', 'body mem', self.project,
+ 'example.com', self.commenter_view, self.detail_url)
+ self.assertEqual(
+ '%s@%s' % (self.project.project_name, emailfmt.MailDomain()),
+ email_task['reply_to'])
+ self.assertIn('Reply to this email to add a comment', email_task['body'])
+ self.assertIn('make updates', email_task['body'])
+
+ def testInboundEmailDisabled(self):
+ """We don't invite replies if they are disabled for this project."""
+ self.project.process_inbound_email = False
+ email_task = notify_helpers._MakeEmailWorkItem(
+ notify_reasons.AddrPerm(
+ True, 'a@a.com', self.member, REPLY_MAY_UPDATE,
+ user_pb2.UserPrefs()),
+ ['reason'], self.issue,
+ 'body link-only', 'body non', 'body mem',
+ self.project, 'example.com', self.commenter_view, self.detail_url)
+ self.assertEqual(emailfmt.NoReplyAddress(), email_task['reply_to'])
+
+ def testReasons(self):
+ """The footer lists reasons why that email was sent to that user."""
+ email_task = notify_helpers._MakeEmailWorkItem(
+ notify_reasons.AddrPerm(
+ True, 'a@a.com', self.member, REPLY_MAY_UPDATE,
+ user_pb2.UserPrefs()),
+ ['Funny', 'Caring', 'Near'], self.issue,
+ 'body link-only', 'body non', 'body mem',
+ self.project, 'example.com', self.commenter_view, self.detail_url)
+ self.assertIn('because:', email_task['body'])
+ self.assertIn('1. Funny', email_task['body'])
+ self.assertIn('2. Caring', email_task['body'])
+ self.assertIn('3. Near', email_task['body'])
+
+ email_task = notify_helpers._MakeEmailWorkItem(
+ notify_reasons.AddrPerm(
+ True, 'a@a.com', self.member, REPLY_MAY_UPDATE,
+ user_pb2.UserPrefs()),
+ [], self.issue,
+ 'body link-only', 'body non', 'body mem',
+ self.project, 'example.com', self.commenter_view, self.detail_url)
+ self.assertNotIn('because', email_task['body'])
+
+
+class MakeNotificationFooterTest(unittest.TestCase):
+
+ def testMakeNotificationFooter_NoReason(self):
+ footer = notify_helpers._MakeNotificationFooter(
+ [], REPLY_NOT_ALLOWED, 'example.com')
+ self.assertEqual('', footer)
+
+ def testMakeNotificationFooter_WithReason(self):
+ footer = notify_helpers._MakeNotificationFooter(
+ ['REASON'], REPLY_NOT_ALLOWED, 'example.com')
+ self.assertIn('REASON', footer)
+ self.assertIn('https://example.com/hosting/settings', footer)
+
+ footer = notify_helpers._MakeNotificationFooter(
+ ['REASON'], REPLY_NOT_ALLOWED, 'example.com')
+ self.assertIn('REASON', footer)
+ self.assertIn('https://example.com/hosting/settings', footer)
+
+ def testMakeNotificationFooter_ManyReasons(self):
+ footer = notify_helpers._MakeNotificationFooter(
+ ['Funny', 'Caring', 'Warmblooded'], REPLY_NOT_ALLOWED,
+ 'example.com')
+ self.assertIn('Funny', footer)
+ self.assertIn('Caring', footer)
+ self.assertIn('Warmblooded', footer)
+
+ def testMakeNotificationFooter_WithReplyInstructions(self):
+ footer = notify_helpers._MakeNotificationFooter(
+ ['REASON'], REPLY_NOT_ALLOWED, 'example.com')
+ self.assertNotIn('Reply', footer)
+ self.assertIn('https://example.com/hosting/settings', footer)
+
+ footer = notify_helpers._MakeNotificationFooter(
+ ['REASON'], REPLY_MAY_COMMENT, 'example.com')
+ self.assertIn('add a comment', footer)
+ self.assertNotIn('make updates', footer)
+ self.assertIn('https://example.com/hosting/settings', footer)
+
+ footer = notify_helpers._MakeNotificationFooter(
+ ['REASON'], REPLY_MAY_UPDATE, 'example.com')
+ self.assertIn('add a comment', footer)
+ self.assertIn('make updates', footer)
+ self.assertIn('https://example.com/hosting/settings', footer)
diff --git a/features/test/notify_reasons_test.py b/features/test/notify_reasons_test.py
new file mode 100644
index 0000000..559e322
--- /dev/null
+++ b/features/test/notify_reasons_test.py
@@ -0,0 +1,407 @@
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for notify_reasons.py."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+import os
+
+from features import notify_reasons
+from framework import emailfmt
+from framework import framework_views
+from framework import urls
+from proto import user_pb2
+from proto import usergroup_pb2
+from services import service_manager
+from testing import fake
+from tracker import tracker_bizobj
+
+
+REPLY_NOT_ALLOWED = notify_reasons.REPLY_NOT_ALLOWED
+REPLY_MAY_COMMENT = notify_reasons.REPLY_MAY_COMMENT
+REPLY_MAY_UPDATE = notify_reasons.REPLY_MAY_UPDATE
+
+
+class ComputeIssueChangeAddressPermListTest(unittest.TestCase):
+
+ def setUp(self):
+ self.users_by_id = {
+ 111: framework_views.StuffUserView(111, 'owner@example.com', True),
+ 222: framework_views.StuffUserView(222, 'member@example.com', True),
+ 999: framework_views.StuffUserView(999, 'visitor@example.com', True),
+ }
+ self.services = service_manager.Services(
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService())
+ self.owner = self.services.user.TestAddUser('owner@example.com', 111)
+ self.member = self.services.user.TestAddUser('member@example.com', 222)
+ self.visitor = self.services.user.TestAddUser('visitor@example.com', 999)
+ self.project = self.services.project.TestAddProject(
+ 'proj', owner_ids=[111], committer_ids=[222])
+ self.project.process_inbound_email = True
+ self.issue = fake.MakeTestIssue(
+ self.project.project_id, 1, 'summary', 'New', 111)
+
+ def testEmptyIDs(self):
+ cnxn = 'fake cnxn'
+ addr_perm_list = notify_reasons.ComputeIssueChangeAddressPermList(
+ cnxn, [], self.project, self.issue, self.services, [], {})
+ self.assertEqual([], addr_perm_list)
+
+ def testRecipientIsMember(self):
+ cnxn = 'fake cnxn'
+ ids_to_consider = [111, 222, 999]
+ addr_perm_list = notify_reasons.ComputeIssueChangeAddressPermList(
+ cnxn, ids_to_consider, self.project, self.issue, self.services, set(),
+ self.users_by_id, pref_check_function=lambda *args: True)
+ self.assertEqual(
+ [notify_reasons.AddrPerm(
+ True, 'owner@example.com', self.owner, REPLY_MAY_UPDATE,
+ user_pb2.UserPrefs(user_id=111)),
+ notify_reasons.AddrPerm(
+ True, 'member@example.com', self.member, REPLY_MAY_UPDATE,
+ user_pb2.UserPrefs(user_id=222)),
+ notify_reasons.AddrPerm(
+ False, 'visitor@example.com', self.visitor, REPLY_MAY_COMMENT,
+ user_pb2.UserPrefs(user_id=999))],
+ addr_perm_list)
+
+
+class ComputeProjectAndIssueNotificationAddrListTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = 'fake cnxn'
+ self.services = service_manager.Services(
+ project=fake.ProjectService(),
+ user=fake.UserService())
+ self.project = self.services.project.TestAddProject('project')
+ self.services.user.TestAddUser('alice@gmail.com', 111)
+ self.services.user.TestAddUser('bob@gmail.com', 222)
+ self.services.user.TestAddUser('fred@gmail.com', 555)
+
+ def testNotifyAddress(self):
+ # No mailing list or filter rules are defined
+ addr_perm_list = notify_reasons.ComputeProjectNotificationAddrList(
+ self.cnxn, self.services, self.project, True, set())
+ self.assertListEqual([], addr_perm_list)
+
+ # Only mailing list is notified.
+ self.project.issue_notify_address = 'mailing-list@domain.com'
+ addr_perm_list = notify_reasons.ComputeProjectNotificationAddrList(
+ self.cnxn, self.services, self.project, True, set())
+ self.assertListEqual(
+ [notify_reasons.AddrPerm(
+ False, 'mailing-list@domain.com', None, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs())],
+ addr_perm_list)
+
+ # No one is notified because mailing list was already notified.
+ omit_addrs = {'mailing-list@domain.com'}
+ addr_perm_list = notify_reasons.ComputeProjectNotificationAddrList(
+ self.cnxn, self.services, self.project, False, omit_addrs)
+ self.assertListEqual([], addr_perm_list)
+
+ # No one is notified because anon users cannot view.
+ addr_perm_list = notify_reasons.ComputeProjectNotificationAddrList(
+ self.cnxn, self.services, self.project, False, set())
+ self.assertListEqual([], addr_perm_list)
+
+ def testFilterRuleNotifyAddresses(self):
+ issue = fake.MakeTestIssue(
+ self.project.project_id, 1, 'summary', 'New', 555)
+ issue.derived_notify_addrs.extend(['notify@domain.com'])
+
+ addr_perm_list = notify_reasons.ComputeIssueNotificationAddrList(
+ self.cnxn, self.services, issue, set())
+ self.assertListEqual(
+ [notify_reasons.AddrPerm(
+ False, 'notify@domain.com', None, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs())],
+ addr_perm_list)
+
+ # Also-notify addresses can be omitted (e.g., if it is the same as
+ # the email address of the user who made the change).
+ addr_perm_list = notify_reasons.ComputeIssueNotificationAddrList(
+ self.cnxn, self.services, issue, {'notify@domain.com'})
+ self.assertListEqual([], addr_perm_list)
+
+
+class ComputeGroupReasonListTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ features=fake.FeaturesService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService())
+ self.project = self.services.project.TestAddProject(
+ 'project', project_id=789)
+ self.config = self.services.config.GetProjectConfig('cnxn', 789)
+ self.alice = self.services.user.TestAddUser('alice@example.com', 111)
+ self.bob = self.services.user.TestAddUser('bob@example.com', 222)
+ self.fred = self.services.user.TestAddUser('fred@example.com', 555)
+ self.users_by_id = framework_views.MakeAllUserViews(
+ 'cnxn', self.services.user, [111, 222, 555])
+ self.issue = fake.MakeTestIssue(
+ self.project.project_id, 1, 'summary', 'New', 555)
+
+ def CheckGroupReasonList(
+ self,
+ actual,
+ reporter_apl=None,
+ owner_apl=None,
+ old_owner_apl=None,
+ default_owner_apl=None,
+ ccd_apl=None,
+ group_ccd_apl=None,
+ default_ccd_apl=None,
+ starrer_apl=None,
+ subscriber_apl=None,
+ also_notified_apl=None,
+ all_notifications_apl=None):
+ (
+ you_report, you_own, you_old_owner, you_default_owner, you_ccd,
+ you_group_ccd, you_default_ccd, you_star, you_subscribe,
+ you_also_notify, all_notifications) = actual
+ self.assertEqual(
+ (reporter_apl or [], notify_reasons.REASON_REPORTER),
+ you_report)
+ self.assertEqual(
+ (owner_apl or [], notify_reasons.REASON_OWNER),
+ you_own)
+ self.assertEqual(
+ (old_owner_apl or [], notify_reasons.REASON_OLD_OWNER),
+ you_old_owner)
+ self.assertEqual(
+ (default_owner_apl or [], notify_reasons.REASON_DEFAULT_OWNER),
+ you_default_owner)
+ self.assertEqual(
+ (ccd_apl or [], notify_reasons.REASON_CCD),
+ you_ccd)
+ self.assertEqual(
+ (group_ccd_apl or [], notify_reasons.REASON_GROUP_CCD), you_group_ccd)
+ self.assertEqual(
+ (default_ccd_apl or [], notify_reasons.REASON_DEFAULT_CCD),
+ you_default_ccd)
+ self.assertEqual(
+ (starrer_apl or [], notify_reasons.REASON_STARRER),
+ you_star)
+ self.assertEqual(
+ (subscriber_apl or [], notify_reasons.REASON_SUBSCRIBER),
+ you_subscribe)
+ self.assertEqual(
+ (also_notified_apl or [], notify_reasons.REASON_ALSO_NOTIFY),
+ you_also_notify)
+ self.assertEqual(
+ (all_notifications_apl or [], notify_reasons.REASON_ALL_NOTIFICATIONS),
+ all_notifications)
+
+ def testComputeGroupReasonList_OwnerAndCC(self):
+ """Fred owns the issue, Alice is CC'd."""
+ self.issue.cc_ids = [self.alice.user_id]
+ actual = notify_reasons.ComputeGroupReasonList(
+ 'cnxn', self.services, self.project, self.issue, self.config,
+ self.users_by_id, [], True)
+ self.CheckGroupReasonList(
+ actual,
+ owner_apl=[notify_reasons.AddrPerm(
+ False, self.fred.email, self.fred, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs(user_id=self.fred.user_id))],
+ ccd_apl=[notify_reasons.AddrPerm(
+ False, self.alice.email, self.alice, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs(user_id=self.alice.user_id))])
+
+ def testComputeGroupReasonList_DerivedCcs(self):
+ """Check we correctly compute reasons for component and rule ccs."""
+ member_1 = self.services.user.TestAddUser('member_1@example.com', 991)
+ member_2 = self.services.user.TestAddUser('member_2@example.com', 992)
+ member_3 = self.services.user.TestAddUser('member_3@example.com', 993)
+
+ expanded_group = self.services.user.TestAddUser('group@example.com', 999)
+ self.services.usergroup.CreateGroup(
+ 'cnxn', self.services, expanded_group.email, 'owners')
+ self.services.usergroup.TestAddGroupSettings(
+ expanded_group.user_id,
+ expanded_group.email,
+ notify_members=True,
+ notify_group=False)
+ self.services.usergroup.TestAddMembers(
+ expanded_group.user_id, [member_1.user_id, member_2.user_id])
+
+ group = self.services.user.TestAddUser('group_1@example.com', 888)
+ self.services.usergroup.CreateGroup(
+ 'cnxn', self.services, group.email, 'owners')
+ self.services.usergroup.TestAddGroupSettings(
+ group.user_id, group.email, notify_members=False, notify_group=True)
+ self.services.usergroup.TestAddMembers(
+ group.user_id, [member_2.user_id, member_3.user_id])
+
+ users_by_id = framework_views.MakeAllUserViews(
+ 'cnxn', self.services.user, [
+ self.alice.user_id, self.fred.user_id, member_1.user_id,
+ member_2.user_id, member_3.user_id, group.user_id,
+ expanded_group.user_id
+ ])
+
+ comp_id = 123
+ self.config.component_defs = [
+ fake.MakeTestComponentDef(
+ self.project.project_id,
+ comp_id,
+ path='Chicken',
+ cc_ids=[group.user_id, expanded_group.user_id, self.alice.user_id])
+ ]
+ derived_cc_ids = [
+ self.fred.user_id, # cc'd directly due to a rule
+ self.alice.user_id, # cc'd due to the component
+ expanded_group
+ .user_id, # cc'd due to the component, members notified directly
+ group.user_id, # cc'd due to the component
+ # cc'd directly due to a rule,
+ # not removed from rule cc notifications due to transitive cc of
+ # expanded_group.
+ member_1.user_id,
+ member_3.user_id,
+ ]
+ issue = fake.MakeTestIssue(
+ self.project.project_id,
+ 2,
+ 'summary',
+ 'New',
+ 0,
+ derived_cc_ids=derived_cc_ids,
+ component_ids=[comp_id])
+ actual = notify_reasons.ComputeGroupReasonList(
+ 'cnxn', self.services, self.project, issue, self.config, users_by_id,
+ [], True)
+
+ # Asserts list/reason of derived ccs from rules (not components).
+ # The derived ccs list/reason is the 7th tuple returned by
+ # ComputeGroupReasonList()
+ actual_ccd_apl, actual_ccd_reason = actual[6]
+ self.assertEqual(
+ actual_ccd_apl, [
+ notify_reasons.AddrPerm(
+ False, member_3.email, member_3, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs(user_id=member_3.user_id)),
+ notify_reasons.AddrPerm(
+ False, self.fred.email, self.fred, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs(user_id=self.fred.user_id)),
+ notify_reasons.AddrPerm(
+ False, member_1.email, member_1, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs(user_id=member_1.user_id)),
+ ])
+ self.assertEqual(actual_ccd_reason, notify_reasons.REASON_DEFAULT_CCD)
+
+ # Asserts list/reason of derived ccs from components.
+ # The component derived ccs list/reason is hte 8th tuple returned by
+ # ComputeGroupReasonList() when there are component derived ccs.
+ actual_component_apl, actual_comp_reason = actual[7]
+ self.assertEqual(
+ actual_component_apl, [
+ notify_reasons.AddrPerm(
+ False, group.email, group, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs(user_id=group.user_id)),
+ notify_reasons.AddrPerm(
+ False, self.alice.email, self.alice, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs(user_id=self.alice.user_id)),
+ notify_reasons.AddrPerm(
+ False, member_2.email, member_2, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs(user_id=member_2.user_id)),
+ notify_reasons.AddrPerm(
+ False, member_1.email, member_1, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs(user_id=member_1.user_id)),
+ ])
+ self.assertEqual(
+ actual_comp_reason,
+ "You are auto-CC'd on all issues in component Chicken")
+
+ def testComputeGroupReasonList_Starrers(self):
+ """Bob and Alice starred it, but Alice opts out of notifications."""
+ self.alice.notify_starred_issue_change = False
+ actual = notify_reasons.ComputeGroupReasonList(
+ 'cnxn', self.services, self.project, self.issue, self.config,
+ self.users_by_id, [], True,
+ starrer_ids=[self.alice.user_id, self.bob.user_id])
+ self.CheckGroupReasonList(
+ actual,
+ owner_apl=[notify_reasons.AddrPerm(
+ False, self.fred.email, self.fred, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs(user_id=self.fred.user_id))],
+ starrer_apl=[notify_reasons.AddrPerm(
+ False, self.bob.email, self.bob, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs(user_id=self.bob.user_id))])
+
+ def testComputeGroupReasonList_Subscribers(self):
+ """Bob subscribed."""
+ sq = tracker_bizobj.MakeSavedQuery(
+ 1, 'freds issues', 1, 'owner:fred@example.com',
+ subscription_mode='immediate', executes_in_project_ids=[789])
+ self.services.features.UpdateUserSavedQueries(
+ 'cnxn', self.bob.user_id, [sq])
+ actual = notify_reasons.ComputeGroupReasonList(
+ 'cnxn', self.services, self.project, self.issue, self.config,
+ self.users_by_id, [], True)
+ self.CheckGroupReasonList(
+ actual,
+ owner_apl=[notify_reasons.AddrPerm(
+ False, self.fred.email, self.fred, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs(user_id=self.fred.user_id))],
+ subscriber_apl=[notify_reasons.AddrPerm(
+ False, self.bob.email, self.bob, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs(user_id=self.bob.user_id))])
+
+ # Now with subscriber notifications disabled.
+ actual = notify_reasons.ComputeGroupReasonList(
+ 'cnxn', self.services, self.project, self.issue, self.config,
+ self.users_by_id, [], True, include_subscribers=False)
+ self.CheckGroupReasonList(
+ actual,
+ owner_apl=[notify_reasons.AddrPerm(
+ False, self.fred.email, self.fred, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs(user_id=self.fred.user_id))])
+
+ def testComputeGroupReasonList_NotifyAll(self):
+ """Project is configured to always notify issues@example.com."""
+ self.project.issue_notify_address = 'issues@example.com'
+ actual = notify_reasons.ComputeGroupReasonList(
+ 'cnxn', self.services, self.project, self.issue, self.config,
+ self.users_by_id, [], True)
+ self.CheckGroupReasonList(
+ actual,
+ owner_apl=[notify_reasons.AddrPerm(
+ False, self.fred.email, self.fred, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs(user_id=self.fred.user_id))],
+ all_notifications_apl=[notify_reasons.AddrPerm(
+ False, 'issues@example.com', None, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs())])
+
+ # We don't use the notify-all address when the issue is not public.
+ actual = notify_reasons.ComputeGroupReasonList(
+ 'cnxn', self.services, self.project, self.issue, self.config,
+ self.users_by_id, [], False)
+ self.CheckGroupReasonList(
+ actual,
+ owner_apl=[notify_reasons.AddrPerm(
+ False, self.fred.email, self.fred, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs(user_id=self.fred.user_id))])
+
+ # Now with the notify-all address disabled.
+ actual = notify_reasons.ComputeGroupReasonList(
+ 'cnxn', self.services, self.project, self.issue, self.config,
+ self.users_by_id, [], True, include_notify_all=False)
+ self.CheckGroupReasonList(
+ actual,
+ owner_apl=[notify_reasons.AddrPerm(
+ False, self.fred.email, self.fred, REPLY_NOT_ALLOWED,
+ user_pb2.UserPrefs(user_id=self.fred.user_id))])
diff --git a/features/test/notify_test.py b/features/test/notify_test.py
new file mode 100644
index 0000000..00de106
--- /dev/null
+++ b/features/test/notify_test.py
@@ -0,0 +1,708 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for notify.py."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import json
+import mock
+import unittest
+import webapp2
+
+from google.appengine.ext import testbed
+
+from features import notify
+from features import notify_reasons
+from framework import emailfmt
+from framework import urls
+from proto import tracker_pb2
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+from tracker import attachment_helpers
+from tracker import tracker_bizobj
+
+from third_party import cloudstorage
+
+
+def MakeTestIssue(project_id, local_id, owner_id, reporter_id, is_spam=False):
+ issue = tracker_pb2.Issue()
+ issue.project_id = project_id
+ issue.local_id = local_id
+ issue.issue_id = 1000 * project_id + local_id
+ issue.owner_id = owner_id
+ issue.reporter_id = reporter_id
+ issue.is_spam = is_spam
+ return issue
+
+
+class NotifyTaskHandleRequestTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ issue_star=fake.IssueStarService(),
+ features=fake.FeaturesService())
+ self.requester = self.services.user.TestAddUser('requester@example.com', 1)
+ self.nonmember = self.services.user.TestAddUser('user@example.com', 2)
+ self.member = self.services.user.TestAddUser('member@example.com', 3)
+ self.project = self.services.project.TestAddProject(
+ 'test-project', owner_ids=[1, 3], project_id=12345)
+ self.issue1 = MakeTestIssue(
+ project_id=12345, local_id=1, owner_id=2, reporter_id=1)
+ self.issue2 = MakeTestIssue(
+ project_id=12345, local_id=2, owner_id=2, reporter_id=1)
+ self.services.issue.TestAddIssue(self.issue1)
+
+ self._old_gcs_open = cloudstorage.open
+ cloudstorage.open = fake.gcs_open
+ self.orig_sign_attachment_id = attachment_helpers.SignAttachmentID
+ attachment_helpers.SignAttachmentID = (
+ lambda aid: 'signed_%d' % aid)
+
+ self.testbed = testbed.Testbed()
+ self.testbed.activate()
+ self.testbed.init_memcache_stub()
+ self.testbed.init_datastore_v3_stub()
+
+ def tearDown(self):
+ cloudstorage.open = self._old_gcs_open
+ attachment_helpers.SignAttachmentID = self.orig_sign_attachment_id
+
+ def get_filtered_task_call_args(self, create_task_mock, relative_uri):
+ return [
+ (args, kwargs)
+ for (args, kwargs) in create_task_mock.call_args_list
+ if args[0]['app_engine_http_request']['relative_uri'] == relative_uri
+ ]
+
+ def VerifyParams(self, result, params):
+ self.assertEqual(
+ bool(params['send_email']), result['params']['send_email'])
+ if 'issue_id' in params:
+ self.assertEqual(params['issue_id'], result['params']['issue_id'])
+ if 'issue_ids' in params:
+ self.assertEqual([int(p) for p in params['issue_ids'].split(',')],
+ result['params']['issue_ids'])
+
+ def testNotifyIssueChangeTask_Normal(self):
+ task = notify.NotifyIssueChangeTask(
+ request=None, response=None, services=self.services)
+ params = {'send_email': 1, 'issue_id': 12345001, 'seq': 0,
+ 'commenter_id': 2}
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 1},
+ params=params,
+ method='POST',
+ services=self.services)
+ result = task.HandleRequest(mr)
+ self.VerifyParams(result, params)
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testNotifyIssueChangeTask_Spam(self, _create_task_mock):
+ issue = MakeTestIssue(
+ 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)
+ params = {'send_email': 0, 'issue_id': issue.issue_id, 'seq': 0,
+ 'commenter_id': 2}
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 1},
+ params=params,
+ method='POST',
+ services=self.services)
+ result = task.HandleRequest(mr)
+ self.assertEqual(0, len(result['notified']))
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testNotifyBlockingChangeTask_Normal(self, _create_task_mock):
+ 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)
+ params = {
+ 'send_email': 1, 'issue_id': issue2.issue_id, 'seq': 0,
+ 'delta_blocker_iids': self.issue1.issue_id, 'commenter_id': 1,
+ 'hostport': 'bugs.chromium.org'}
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 1},
+ params=params,
+ method='POST',
+ services=self.services)
+ result = task.HandleRequest(mr)
+ self.VerifyParams(result, params)
+
+ def testNotifyBlockingChangeTask_Spam(self):
+ issue2 = MakeTestIssue(
+ 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)
+ params = {
+ 'send_email': 1, 'issue_id': issue2.issue_id, 'seq': 0,
+ 'delta_blocker_iids': self.issue1.issue_id, 'commenter_id': 1}
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 1},
+ params=params,
+ method='POST',
+ services=self.services)
+ result = task.HandleRequest(mr)
+ self.assertEqual(0, len(result['notified']))
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testNotifyBulkChangeTask_Normal(self, create_task_mock):
+ """We generate email tasks for each user involved in the issues."""
+ issue2 = MakeTestIssue(
+ 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)
+ params = {
+ 'send_email': 1, 'seq': 0,
+ 'issue_ids': '%d,%d' % (self.issue1.issue_id, issue2.issue_id),
+ 'old_owner_ids': '1,1', 'commenter_id': 1}
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 1},
+ params=params,
+ method='POST',
+ services=self.services)
+ result = task.HandleRequest(mr)
+ self.VerifyParams(result, params)
+
+ call_args_list = self.get_filtered_task_call_args(
+ create_task_mock, urls.OUTBOUND_EMAIL_TASK + '.do')
+ self.assertEqual(2, len(call_args_list))
+
+ for (args, _kwargs) in call_args_list:
+ task = args[0]
+ body = json.loads(task['app_engine_http_request']['body'].decode())
+ if 'user' in body['to']:
+ self.assertIn(u'\u2026', body['from_addr'])
+ # Full email for members
+ if 'member' in body['to']:
+ self.assertNotIn(u'\u2026', body['from_addr'])
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testNotifyBulkChangeTask_AlsoNotify(self, create_task_mock):
+ """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)
+ params = {
+ 'send_email': 1, 'seq': 0,
+ 'issue_ids': '%d' % (self.issue1.issue_id),
+ 'old_owner_ids': '1', 'commenter_id': 1}
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 1},
+ params=params,
+ method='POST',
+ services=self.services)
+ result = task.HandleRequest(mr)
+ self.VerifyParams(result, params)
+
+ call_args_list = self.get_filtered_task_call_args(
+ create_task_mock, urls.OUTBOUND_EMAIL_TASK + '.do')
+ self.assertEqual(3, len(call_args_list))
+
+ self.assertItemsEqual(
+ ['user@example.com', 'mailing-list@example.com', 'member@example.com'],
+ result['notified'])
+ for (args, _kwargs) in call_args_list:
+ task = args[0]
+ body = json.loads(task['app_engine_http_request']['body'].decode())
+ # obfuscated email for non-members
+ if 'user' in body['to']:
+ self.assertIn(u'\u2026', body['from_addr'])
+ # Full email for members
+ if 'member' in body['to']:
+ self.assertNotIn(u'\u2026', body['from_addr'])
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ 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)
+ params = {
+ 'send_email': 1, 'seq': 0,
+ 'issue_ids': '%d' % (self.issue1.issue_id),
+ 'old_owner_ids': '1', 'commenter_id': 1}
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 1},
+ params=params,
+ method='POST',
+ services=self.services)
+ result = task.HandleRequest(mr)
+ self.VerifyParams(result, params)
+
+ call_args_list = self.get_filtered_task_call_args(
+ create_task_mock, urls.OUTBOUND_EMAIL_TASK + '.do')
+ self.assertEqual(2, len(call_args_list))
+
+ self.assertItemsEqual(
+ ['user@example.com', 'mailing-list@example.com'],
+ result['notified'])
+
+ for (args, _kwargs) in call_args_list:
+ task = args[0]
+ body = json.loads(task['app_engine_http_request']['body'].decode())
+ # obfuscated email for non-members
+ if 'user' in body['to']:
+ self.assertIn(u'\u2026', body['from_addr'])
+ # Full email for members
+ if 'member' in body['to']:
+ self.assertNotIn(u'\u2026', body['from_addr'])
+
+ @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)
+ params = {
+ 'send_email': 1,
+ 'issue_ids': '%d' % (self.issue1.issue_id),
+ 'seq': 0,
+ 'old_owner_ids': '1', 'commenter_id': 1}
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 1},
+ params=params,
+ method='POST',
+ services=self.services)
+ self.services.user.TestAddUser('subscriber@example.com', 4)
+ sq = tracker_bizobj.MakeSavedQuery(
+ 1, 'all open issues', 2, '', subscription_mode='immediate',
+ executes_in_project_ids=[self.issue1.project_id])
+ self.services.features.UpdateUserSavedQueries('cnxn', 4, [sq])
+ result = task.HandleRequest(mr)
+ self.VerifyParams(result, params)
+
+ call_args_list = self.get_filtered_task_call_args(
+ create_task_mock, urls.OUTBOUND_EMAIL_TASK + '.do')
+ self.assertEqual(2, len(call_args_list))
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ 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)
+ params = {
+ 'send_email': 1,
+ 'issue_ids': '%d' % (self.issue1.issue_id),
+ 'seq': 0,
+ 'old_owner_ids': '1', 'commenter_id': 1}
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 1},
+ params=params,
+ method='POST',
+ services=self.services)
+ self.services.user.TestAddUser('subscriber@example.com', 4)
+ self.issue1.cc_ids = [4]
+ sq = tracker_bizobj.MakeSavedQuery(
+ 1, 'all open issues', 2, '', subscription_mode='immediate',
+ executes_in_project_ids=[self.issue1.project_id])
+ self.services.features.UpdateUserSavedQueries('cnxn', 4, [sq])
+ result = task.HandleRequest(mr)
+ self.VerifyParams(result, params)
+
+ call_args_list = self.get_filtered_task_call_args(
+ create_task_mock, urls.OUTBOUND_EMAIL_TASK + '.do')
+ self.assertEqual(2, len(call_args_list))
+
+ found = False
+ for (args, _kwargs) in call_args_list:
+ task = args[0]
+ body = json.loads(task['app_engine_http_request']['body'].decode())
+ if body['to'] == 'subscriber@example.com':
+ found = True
+ task_body = body['body']
+ self.assertEqual(1, task_body.count('Issue %d' % self.issue1.local_id))
+ self.assertTrue(found)
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testNotifyBulkChangeTask_Spam(self, _create_task_mock):
+ """A spam issue is excluded from notification emails."""
+ issue2 = MakeTestIssue(
+ 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)
+ params = {
+ 'send_email': 1,
+ 'issue_ids': '%d,%d' % (self.issue1.issue_id, issue2.issue_id),
+ 'seq': 0,
+ 'old_owner_ids': '1,1', 'commenter_id': 1}
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 1},
+ params=params,
+ method='POST',
+ services=self.services)
+ result = task.HandleRequest(mr)
+ self.assertEqual(1, len(result['notified']))
+
+ 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)
+ users_by_id = {}
+ commenter_view = None
+ config = self.services.config.GetProjectConfig('cnxn', 12345)
+ addrperm = notify_reasons.AddrPerm(
+ False, 'nonmember@example.com', self.nonmember,
+ notify_reasons.REPLY_NOT_ALLOWED, None)
+
+ subject, body = task._FormatBulkIssues(
+ [self.issue1], users_by_id, commenter_view, 'localhost:8080',
+ 'test comment', [], config, addrperm)
+
+ self.assertIn('one summary', subject)
+ self.assertIn('one summary', body)
+ self.assertIn('test comment', body)
+
+ def testFormatBulkIssues_Normal_Multiple(self):
+ """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)
+ users_by_id = {}
+ commenter_view = None
+ config = self.services.config.GetProjectConfig('cnxn', 12345)
+ addrperm = notify_reasons.AddrPerm(
+ False, 'nonmember@example.com', self.nonmember,
+ notify_reasons.REPLY_NOT_ALLOWED, None)
+
+ subject, body = task._FormatBulkIssues(
+ [self.issue1, self.issue2], users_by_id, commenter_view, 'localhost:8080',
+ 'test comment', [], config, addrperm)
+
+ self.assertIn('2 issues changed', subject)
+ self.assertIn('one summary', body)
+ self.assertIn('two summary', body)
+ self.assertIn('test comment', body)
+
+ def testFormatBulkIssues_LinkOnly_Single(self):
+ """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)
+ users_by_id = {}
+ commenter_view = None
+ config = self.services.config.GetProjectConfig('cnxn', 12345)
+ addrperm = notify_reasons.AddrPerm(
+ False, 'nonmember@example.com', self.nonmember,
+ notify_reasons.REPLY_NOT_ALLOWED, None)
+
+ subject, body = task._FormatBulkIssues(
+ [self.issue1], users_by_id, commenter_view, 'localhost:8080',
+ 'test comment', [], config, addrperm)
+
+ self.assertIn('issue 1', subject)
+ self.assertNotIn('one summary', subject)
+ self.assertNotIn('one summary', body)
+ self.assertNotIn('test comment', body)
+
+ def testFormatBulkIssues_LinkOnly_Multiple(self):
+ """A user may not see full notification details for some changed issue."""
+ 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)
+ users_by_id = {}
+ commenter_view = None
+ config = self.services.config.GetProjectConfig('cnxn', 12345)
+ addrperm = notify_reasons.AddrPerm(
+ False, 'nonmember@example.com', self.nonmember,
+ notify_reasons.REPLY_NOT_ALLOWED, None)
+
+ subject, body = task._FormatBulkIssues(
+ [self.issue1, self.issue2], users_by_id, commenter_view, 'localhost:8080',
+ 'test comment', [], config, addrperm)
+
+ self.assertIn('2 issues', subject)
+ self.assertNotIn('summary', subject)
+ self.assertNotIn('one summary', body)
+ self.assertIn('two summary', body)
+ self.assertNotIn('test comment', body)
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testNotifyApprovalChangeTask_Normal(self, _create_task_mock):
+ config = self.services.config.GetProjectConfig('cnxn', 12345)
+ config.field_defs = [
+ # issue's User field with any_comment is notified.
+ tracker_bizobj.MakeFieldDef(
+ 121, 12345, 'TL', tracker_pb2.FieldTypes.USER_TYPE,
+ '', '', False, False, False, None, None, None, False, '',
+ None, tracker_pb2.NotifyTriggers.ANY_COMMENT, 'no_action',
+ 'TL, notified on everything', False),
+ # issue's User field with never is not notified.
+ tracker_bizobj.MakeFieldDef(
+ 122, 12345, 'silentTL', tracker_pb2.FieldTypes.USER_TYPE,
+ '', '', False, False, False, None, None, None, False, '',
+ None, tracker_pb2.NotifyTriggers.NEVER, 'no_action',
+ 'TL, notified on nothing', False),
+ # approval's User field with any_comment is notified.
+ tracker_bizobj.MakeFieldDef(
+ 123, 12345, 'otherapprovalTL', tracker_pb2.FieldTypes.USER_TYPE,
+ '', '', False, False, False, None, None, None, False, '',
+ None, tracker_pb2.NotifyTriggers.ANY_COMMENT, 'no_action',
+ 'TL on the approvers team', False, approval_id=3),
+ # another approval's User field with any_comment is not notified.
+ tracker_bizobj.MakeFieldDef(
+ 124, 12345, 'otherapprovalTL', tracker_pb2.FieldTypes.USER_TYPE,
+ '', '', False, False, False, None, None, None, False, '',
+ None, tracker_pb2.NotifyTriggers.ANY_COMMENT, 'no_action',
+ 'TL on another approvers team', False, approval_id=4),
+ tracker_bizobj.MakeFieldDef(
+ 3, 12345, 'Goat-Approval', tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ '', '', False, False, False, None, None, None, False, '',
+ None, tracker_pb2.NotifyTriggers.NEVER, 'no_action',
+ 'Get Approval from Goats', False)
+ ]
+ self.services.config.StoreConfig('cnxn', config)
+
+ # Custom user_type field TLs
+ self.services.user.TestAddUser('TL@example.com', 111)
+ self.services.user.TestAddUser('silentTL@example.com', 222)
+ self.services.user.TestAddUser('approvalTL@example.com', 333)
+ self.services.user.TestAddUser('otherapprovalTL@example.com', 444)
+
+ # Approvers
+ self.services.user.TestAddUser('approver_old@example.com', 777)
+ self.services.user.TestAddUser('approver_new@example.com', 888)
+ self.services.user.TestAddUser('approver_still@example.com', 999)
+ self.services.user.TestAddUser('approver_group@example.com', 666)
+ self.services.user.TestAddUser('group_mem1@example.com', 661)
+ self.services.user.TestAddUser('group_mem2@example.com', 662)
+ self.services.user.TestAddUser('group_mem3@example.com', 663)
+ self.services.usergroup.TestAddGroupSettings(
+ 666, 'approver_group@example.com')
+ self.services.usergroup.TestAddMembers(666, [661, 662, 663])
+ canary_phase = tracker_pb2.Phase(
+ name='Canary', phase_id=1, rank=1)
+ approval_values = [
+ tracker_pb2.ApprovalValue(approval_id=3,
+ approver_ids=[888, 999, 666, 661])]
+ approval_issue = MakeTestIssue(
+ project_id=12345, local_id=2, owner_id=2, reporter_id=1,
+ is_spam=True)
+ approval_issue.phases = [canary_phase]
+ approval_issue.approval_values = approval_values
+ approval_issue.field_values = [
+ tracker_bizobj.MakeFieldValue(121, None, None, 111, None, None, False),
+ tracker_bizobj.MakeFieldValue(122, None, None, 222, None, None, False),
+ tracker_bizobj.MakeFieldValue(123, None, None, 333, None, None, False),
+ tracker_bizobj.MakeFieldValue(124, None, None, 444, None, None, False),
+ ]
+ self.services.issue.TestAddIssue(approval_issue)
+
+ amend = tracker_bizobj.MakeApprovalApproversAmendment([888], [777])
+
+ comment = tracker_pb2.IssueComment(
+ project_id=12345, user_id=999, issue_id=approval_issue.issue_id,
+ amendments=[amend], timestamp=1234567890, content='just a comment.')
+ attach = tracker_pb2.Attachment(
+ attachment_id=4567, filename='sploot.jpg', mimetype='image/png',
+ gcs_object_id='/pid/attachments/abcd', filesize=(1024 * 1023))
+ comment.attachments.append(attach)
+ self.services.issue.TestAddComment(comment, approval_issue.local_id)
+ self.services.issue.TestAddAttachment(
+ attach, comment.id, approval_issue.issue_id)
+
+ task = notify.NotifyApprovalChangeTask(
+ request=None, response=None, services=self.services)
+ params = {
+ 'send_email': 1,
+ 'issue_id': approval_issue.issue_id,
+ 'approval_id': 3,
+ 'comment_id': comment.id,
+ }
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 1},
+ params=params,
+ method='POST',
+ services=self.services)
+ result = task.HandleRequest(mr)
+ self.assertTrue('just a comment' in result['tasks'][0]['body'])
+ self.assertTrue('Approvers: -appro...' in result['tasks'][0]['body'])
+ self.assertTrue('sploot.jpg' in result['tasks'][0]['body'])
+ self.assertTrue(
+ '/issues/attachment?aid=4567' in result['tasks'][0]['body'])
+ self.assertItemsEqual(
+ ['user@example.com', 'approver_old@example.com',
+ 'approver_new@example.com', 'TL@example.com',
+ 'approvalTL@example.com', 'group_mem1@example.com',
+ 'group_mem2@example.com', 'group_mem3@example.com'],
+ result['notified'])
+
+ # Test no approvers/groups notified
+ # Status change to NEED_INFO does not email approvers.
+ amend2 = tracker_bizobj.MakeApprovalStatusAmendment(
+ tracker_pb2.ApprovalStatus.NEED_INFO)
+ comment2 = tracker_pb2.IssueComment(
+ 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)
+ params = {
+ 'send_email': 1,
+ 'issue_id': approval_issue.issue_id,
+ 'approval_id': 3,
+ 'comment_id': comment2.id,
+ }
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 1},
+ params=params,
+ method='POST',
+ services=self.services)
+ result = task.HandleRequest(mr)
+
+ self.assertIsNotNone(result['tasks'][0].get('references'))
+ self.assertEqual(result['tasks'][0]['reply_to'], emailfmt.NoReplyAddress())
+ self.assertTrue('Status: need_info' in result['tasks'][0]['body'])
+ self.assertItemsEqual(
+ ['user@example.com', 'TL@example.com', 'approvalTL@example.com'],
+ result['notified'])
+
+ def testNotifyApprovalChangeTask_GetApprovalEmailRecipients(self):
+ task = notify.NotifyApprovalChangeTask(
+ request=None, response=None, services=self.services)
+ issue = fake.MakeTestIssue(789, 1, 'summary', 'New', 111)
+ approval_value = tracker_pb2.ApprovalValue(
+ approver_ids=[222, 333],
+ status=tracker_pb2.ApprovalStatus.APPROVED)
+ comment = tracker_pb2.IssueComment(
+ project_id=789, user_id=1, issue_id=78901)
+
+ # Comment with not amendments notifies everyone.
+ rids = task._GetApprovalEmailRecipients(
+ approval_value, comment, issue, [777, 888])
+ self.assertItemsEqual(rids, [111, 222, 333, 777, 888])
+
+ # New APPROVED status notifies owners and any_comment users.
+ amendment = tracker_bizobj.MakeApprovalStatusAmendment(
+ tracker_pb2.ApprovalStatus.APPROVED)
+ comment.amendments = [amendment]
+ rids = task._GetApprovalEmailRecipients(
+ approval_value, comment, issue, [777, 888])
+ self.assertItemsEqual(rids, [111, 777, 888])
+
+ # New REVIEW_REQUESTED status notifies approvers.
+ approval_value.status = tracker_pb2.ApprovalStatus.REVIEW_REQUESTED
+ amendment = tracker_bizobj.MakeApprovalStatusAmendment(
+ tracker_pb2.ApprovalStatus.REVIEW_REQUESTED)
+ comment.amendments = [amendment]
+ rids = task._GetApprovalEmailRecipients(
+ approval_value, comment, issue, [777, 888])
+ self.assertItemsEqual(rids, [222, 333])
+
+ # Approvers change notifies everyone.
+ amendment = tracker_bizobj.MakeApprovalApproversAmendment(
+ [222], [555])
+ comment.amendments = [amendment]
+ approval_value.approver_ids = [222]
+ rids = task._GetApprovalEmailRecipients(
+ approval_value, comment, issue, [777], omit_ids=[444, 333])
+ self.assertItemsEqual(rids, [111, 222, 555, 777])
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testNotifyRulesDeletedTask(self, _create_task_mock):
+ self.services.project.TestAddProject(
+ '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)
+ params = {'project_id': 789,
+ 'filter_rules': 'if green make yellow,if orange make blue'}
+ mr = testing_helpers.MakeMonorailRequest(
+ params=params,
+ method='POST',
+ services=self.services)
+ result = task.HandleRequest(mr)
+ self.assertEqual(len(result['tasks']), 2)
+ body = result['tasks'][0]['body']
+ self.assertTrue('if green make yellow' in body)
+ self.assertTrue('if green make yellow' in body)
+ self.assertTrue('/p/proj/adminRules' in body)
+ self.assertItemsEqual(
+ ['cow@test.com', 'owner1@test.com'], result['notified'])
+
+ def testOutboundEmailTask_Normal(self):
+ """We can send an email."""
+ params = {
+ 'from_addr': 'requester@example.com',
+ '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'])
+
+ def testOutboundEmailTask_MissingTo(self):
+ """We skip emails that don't specify the To-line."""
+ params = {
+ '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)
+
+ def testOutboundEmailTask_BannedUser(self):
+ """We don't send emails to banned users.."""
+ 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)
diff --git a/features/test/prettify_test.py b/features/test/prettify_test.py
new file mode 100644
index 0000000..07fce43
--- /dev/null
+++ b/features/test/prettify_test.py
@@ -0,0 +1,92 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unittest for the prettify module."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+import ezt
+
+from features import prettify
+
+
+class SourceBrowseTest(unittest.TestCase):
+
+ def testPrepareSourceLinesForHighlighting(self):
+ # String representing an empty source file
+ src = ''
+
+ file_lines = prettify.PrepareSourceLinesForHighlighting(src)
+ self.assertEqual(len(file_lines), 0)
+
+ def testPrepareSourceLinesForHighlightingNoBreaks(self):
+ # seven lines of text with no blank lines
+ src = ' 1\n 2\n 3\n 4\n 5\n 6\n 7'
+
+ file_lines = prettify.PrepareSourceLinesForHighlighting(src)
+ self.assertEqual(len(file_lines), 7)
+ out_lines = [fl.line for fl in file_lines]
+ self.assertEqual('\n'.join(out_lines), src)
+
+ file_lines = prettify.PrepareSourceLinesForHighlighting(src)
+ self.assertEqual(len(file_lines), 7)
+
+ def testPrepareSourceLinesForHighlightingWithBreaks(self):
+ # seven lines of text with line 5 being blank
+ src = ' 1\n 2\n 3\n 4\n\n 6\n 7'
+
+ file_lines = prettify.PrepareSourceLinesForHighlighting(src)
+ self.assertEqual(len(file_lines), 7)
+
+
+class BuildPrettifyDataTest(unittest.TestCase):
+
+ def testNonSourceFile(self):
+ prettify_data = prettify.BuildPrettifyData(0, '/dev/null')
+ self.assertDictEqual(
+ dict(should_prettify=ezt.boolean(False),
+ prettify_class=None),
+ prettify_data)
+
+ prettify_data = prettify.BuildPrettifyData(10, 'readme.txt')
+ self.assertDictEqual(
+ dict(should_prettify=ezt.boolean(False),
+ prettify_class=None),
+ prettify_data)
+
+ def testGenericLanguage(self):
+ prettify_data = prettify.BuildPrettifyData(123, 'trunk/src/hello.php')
+ self.assertDictEqual(
+ dict(should_prettify=ezt.boolean(True),
+ prettify_class=''),
+ prettify_data)
+
+ def testSpecificLanguage(self):
+ prettify_data = prettify.BuildPrettifyData(123, 'trunk/src/hello.java')
+ self.assertDictEqual(
+ dict(should_prettify=ezt.boolean(True),
+ prettify_class='lang-java'),
+ prettify_data)
+
+ def testThirdPartyExtensionLanguages(self):
+ for ext in ['apollo', 'agc', 'aea', 'el', 'scm', 'cl', 'lisp',
+ 'go', 'hs', 'lua', 'fs', 'ml', 'proto', 'scala',
+ 'sql', 'vb', 'vbs', 'vhdl', 'vhd', 'wiki', 'yaml',
+ 'yml', 'clj']:
+ prettify_data = prettify.BuildPrettifyData(123, '/trunk/src/hello.' + ext)
+ self.assertDictEqual(
+ dict(should_prettify=ezt.boolean(True),
+ prettify_class='lang-' + ext),
+ prettify_data)
+
+ def testExactFilename(self):
+ prettify_data = prettify.BuildPrettifyData(123, 'trunk/src/Makefile')
+ self.assertDictEqual(
+ dict(should_prettify=ezt.boolean(True),
+ prettify_class='lang-sh'),
+ prettify_data)
diff --git a/features/test/pubsub_test.py b/features/test/pubsub_test.py
new file mode 100644
index 0000000..2044cf7
--- /dev/null
+++ b/features/test/pubsub_test.py
@@ -0,0 +1,110 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is govered by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for features.pubsub."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from mock import Mock
+
+from features import pubsub
+from services import service_manager
+from testing import fake
+from testing import testing_helpers
+
+
+class PublishPubsubIssueChangeTaskTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ features=fake.FeaturesService())
+ self.services.project.TestAddProject(
+ 'test-project', owner_ids=[1, 3],
+ project_id=12345)
+
+ # Stub the pubsub API (there is no pubsub testbed stub).
+ self.pubsub_client_mock = Mock()
+ pubsub.set_up_pubsub_api = Mock(return_value=self.pubsub_client_mock)
+
+ def testPublishPubsubIssueChangeTask_NoIssueIdParam(self):
+ """Test case when issue_id param is not passed."""
+ task = pubsub.PublishPubsubIssueChangeTask(
+ request=None, response=None, services=self.services)
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 1},
+ params={},
+ method='POST',
+ services=self.services)
+ result = task.HandleRequest(mr)
+ expected_body = {
+ 'error': 'Cannot proceed without a valid issue ID.',
+ }
+ self.assertEqual(result, expected_body)
+
+ 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)
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 1},
+ params={},
+ method='POST',
+ services=self.services)
+ result = task.HandleRequest(mr)
+ expected_body = {
+ 'error': 'Pub/Sub API init failure.',
+ }
+ self.assertEqual(result, expected_body)
+
+ def testPublishPubsubIssueChangeTask_IssueNotFound(self):
+ """Test case when issue is not found."""
+ task = pubsub.PublishPubsubIssueChangeTask(
+ request=None, response=None, services=self.services)
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 1},
+ params={'issue_id': 314159},
+ method='POST',
+ services=self.services)
+ result = task.HandleRequest(mr)
+ expected_body = {
+ 'error': 'Could not find issue with ID 314159',
+ }
+ self.assertEqual(result, expected_body)
+
+ def testPublishPubsubIssueChangeTask_Normal(self):
+ """Test normal happy-path case."""
+ 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)
+ mr = testing_helpers.MakeMonorailRequest(
+ user_info={'user_id': 1},
+ params={'issue_id': 78901},
+ method='POST',
+ services=self.services)
+ result = task.HandleRequest(mr)
+
+ self.pubsub_client_mock.projects().topics().publish.assert_called_once_with(
+ topic='projects/testing-app/topics/issue-updates',
+ body={
+ 'messages': [{
+ 'attributes': {
+ 'local_id': '543',
+ 'project_name': 'rutabaga',
+ },
+ }],
+ }
+ )
+ self.assertEqual(result, {})
diff --git a/features/test/savedqueries_helpers_test.py b/features/test/savedqueries_helpers_test.py
new file mode 100644
index 0000000..d635fe1
--- /dev/null
+++ b/features/test/savedqueries_helpers_test.py
@@ -0,0 +1,112 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for savedqueries_helpers feature."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+import mox
+
+from features import savedqueries_helpers
+from testing import fake
+from tracker import tracker_bizobj
+
+
+class SavedQueriesHelperTest(unittest.TestCase):
+
+ def setUp(self):
+ self.features = fake.FeaturesService()
+ self.project = fake.ProjectService()
+ self.cnxn = 'fake cnxn'
+ self.mox = mox.Mox()
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def testParseSavedQueries(self):
+ post_data = {
+ 'xyz_savedquery_name_1': '',
+ 'xyz_savedquery_name_2': 'name2',
+ 'xyz_savedquery_name_3': 'name3',
+ 'xyz_savedquery_id_1': 1,
+ 'xyz_savedquery_id_2': 2,
+ 'xyz_savedquery_id_3': 3,
+ 'xyz_savedquery_projects_1': '123',
+ 'xyz_savedquery_projects_2': 'abc',
+ 'xyz_savedquery_projects_3': 'def',
+ 'xyz_savedquery_base_1': 4,
+ 'xyz_savedquery_base_2': 5,
+ 'xyz_savedquery_base_3': 6,
+ 'xyz_savedquery_query_1': 'query1',
+ 'xyz_savedquery_query_2': 'query2',
+ 'xyz_savedquery_query_3': 'query3',
+ 'xyz_savedquery_sub_mode_1': 'sub_mode1',
+ 'xyz_savedquery_sub_mode_2': 'sub_mode2',
+ 'xyz_savedquery_sub_mode_3': 'sub_mode3',
+ }
+ self.project.TestAddProject(name='abc', project_id=1001)
+ self.project.TestAddProject(name='def', project_id=1002)
+
+ saved_queries = savedqueries_helpers.ParseSavedQueries(
+ self.cnxn, post_data, self.project, prefix='xyz_')
+ self.assertEqual(2, len(saved_queries))
+
+ # pylint: disable=unbalanced-tuple-unpacking
+ saved_query1, saved_query2 = saved_queries
+ # Assert contents of saved_query1.
+ self.assertEqual(2, saved_query1.query_id)
+ self.assertEqual('name2', saved_query1.name)
+ self.assertEqual(5, saved_query1.base_query_id)
+ self.assertEqual('query2', saved_query1.query)
+ self.assertEqual([1001], saved_query1.executes_in_project_ids)
+ self.assertEqual('sub_mode2', saved_query1.subscription_mode)
+ # Assert contents of saved_query2.
+ self.assertEqual(3, saved_query2.query_id)
+ self.assertEqual('name3', saved_query2.name)
+ self.assertEqual(6, saved_query2.base_query_id)
+ self.assertEqual('query3', saved_query2.query)
+ self.assertEqual([1002], saved_query2.executes_in_project_ids)
+ self.assertEqual('sub_mode3', saved_query2.subscription_mode)
+
+ def testSavedQueryToCond(self):
+ class MockSavedQuery:
+ def __init__(self):
+ self.base_query_id = 1
+ self.query = 'query'
+ saved_query = MockSavedQuery()
+
+ cond_for_missing_query = savedqueries_helpers.SavedQueryToCond(None)
+ self.assertEqual('', cond_for_missing_query)
+
+ cond_with_no_base = savedqueries_helpers.SavedQueryToCond(saved_query)
+ self.assertEqual('query', cond_with_no_base)
+
+ self.mox.StubOutWithMock(tracker_bizobj, 'GetBuiltInQuery')
+ tracker_bizobj.GetBuiltInQuery(1).AndReturn('base')
+ self.mox.ReplayAll()
+ cond_with_base = savedqueries_helpers.SavedQueryToCond(saved_query)
+ self.assertEqual('base query', cond_with_base)
+ self.mox.VerifyAll()
+
+ def testSavedQueryIDToCond(self):
+ self.mox.StubOutWithMock(savedqueries_helpers, 'SavedQueryToCond')
+ savedqueries_helpers.SavedQueryToCond(mox.IgnoreArg()).AndReturn('ret')
+ self.mox.ReplayAll()
+ query_cond = savedqueries_helpers.SavedQueryIDToCond(
+ self.cnxn, self.features, 1)
+ self.assertEqual('ret', query_cond)
+ self.mox.VerifyAll()
+
+ self.mox.StubOutWithMock(tracker_bizobj, 'GetBuiltInQuery')
+ tracker_bizobj.GetBuiltInQuery(1).AndReturn('built_in_query')
+ self.mox.ReplayAll()
+ query_cond = savedqueries_helpers.SavedQueryIDToCond(
+ self.cnxn, self.features, 1)
+ self.assertEqual('built_in_query', query_cond)
+ self.mox.VerifyAll()
diff --git a/features/test/savedqueries_test.py b/features/test/savedqueries_test.py
new file mode 100644
index 0000000..08624a2
--- /dev/null
+++ b/features/test/savedqueries_test.py
@@ -0,0 +1,43 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for savedqueries feature."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from features import savedqueries
+from framework import monorailrequest
+from framework import permissions
+from services import service_manager
+from testing import fake
+
+
+class SavedQueriesTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ user=fake.UserService())
+ self.servlet = savedqueries.SavedQueries(
+ 'req', 'res', services=self.services)
+ self.services.user.TestAddUser('a@example.com', 111)
+
+ def testAssertBasePermission(self):
+ """Only permit site admins and users viewing themselves."""
+ mr = monorailrequest.MonorailRequest(self.services)
+ mr.viewed_user_auth.user_id = 111
+ mr.auth.user_id = 222
+
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+
+ mr.auth.user_id = 111
+ self.servlet.AssertBasePermission(mr)
+
+ mr.auth.user_id = 222
+ mr.auth.user_pb.is_site_admin = True
+ self.servlet.AssertBasePermission(mr)
diff --git a/features/test/send_notifications_test.py b/features/test/send_notifications_test.py
new file mode 100644
index 0000000..435a67d
--- /dev/null
+++ b/features/test/send_notifications_test.py
@@ -0,0 +1,141 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for prepareandsend.py"""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mock
+import unittest
+import urlparse
+
+from features import send_notifications
+from framework import urls
+from tracker import tracker_bizobj
+
+
+class SendNotificationTest(unittest.TestCase):
+
+ def _get_filtered_task_call_args(self, create_task_mock, relative_uri):
+ return [
+ (args, _kwargs)
+ for (args, _kwargs) in create_task_mock.call_args_list
+ if args[0]['app_engine_http_request']['relative_uri'].startswith(
+ relative_uri)
+ ]
+
+ def _get_create_task_path_and_params(self, call):
+ (args, _kwargs) = call
+ path = args[0]['app_engine_http_request']['relative_uri']
+ encoded_params = args[0]['app_engine_http_request']['body']
+ params = {
+ k: v[0] for k, v in urlparse.parse_qs(encoded_params, True).items()
+ }
+ return path, params
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testPrepareAndSendIssueChangeNotification(self, create_task_mock):
+ send_notifications.PrepareAndSendIssueChangeNotification(
+ issue_id=78901,
+ hostport='testbed-test.appspotmail.com',
+ commenter_id=1,
+ old_owner_id=2,
+ send_email=True)
+
+ call_args_list = self._get_filtered_task_call_args(
+ create_task_mock, urls.NOTIFY_ISSUE_CHANGE_TASK + '.do')
+ self.assertEqual(1, len(call_args_list))
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testPrepareAndSendIssueBlockingNotification(self, create_task_mock):
+ send_notifications.PrepareAndSendIssueBlockingNotification(
+ issue_id=78901,
+ hostport='testbed-test.appspotmail.com',
+ delta_blocker_iids=[],
+ commenter_id=1,
+ send_email=True)
+
+ call_args_list = self._get_filtered_task_call_args(
+ create_task_mock, urls.NOTIFY_BLOCKING_CHANGE_TASK + '.do')
+ self.assertEqual(0, len(call_args_list))
+
+ send_notifications.PrepareAndSendIssueBlockingNotification(
+ issue_id=78901,
+ hostport='testbed-test.appspotmail.com',
+ delta_blocker_iids=[2],
+ commenter_id=1,
+ send_email=True)
+
+ call_args_list = self._get_filtered_task_call_args(
+ create_task_mock, urls.NOTIFY_BLOCKING_CHANGE_TASK + '.do')
+ self.assertEqual(1, len(call_args_list))
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testPrepareAndSendApprovalChangeNotification(self, create_task_mock):
+ send_notifications.PrepareAndSendApprovalChangeNotification(
+ 78901, 3, 'testbed-test.appspotmail.com', 55)
+
+ call_args_list = self._get_filtered_task_call_args(
+ create_task_mock, urls.NOTIFY_APPROVAL_CHANGE_TASK + '.do')
+ self.assertEqual(1, len(call_args_list))
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testSendIssueBulkChangeNotification_CommentOnly(self, create_task_mock):
+ send_notifications.SendIssueBulkChangeNotification(
+ issue_ids=[78901],
+ hostport='testbed-test.appspotmail.com',
+ old_owner_ids=[2],
+ comment_text='comment',
+ commenter_id=1,
+ amendments=[],
+ send_email=True,
+ users_by_id=2)
+
+ call_args_list = self._get_filtered_task_call_args(
+ create_task_mock, urls.NOTIFY_BULK_CHANGE_TASK + '.do')
+ self.assertEqual(1, len(call_args_list))
+ _path, params = self._get_create_task_path_and_params(call_args_list[0])
+ self.assertEqual(params['comment_text'], 'comment')
+ self.assertEqual(params['amendments'], '')
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testSendIssueBulkChangeNotification_Normal(self, create_task_mock):
+ send_notifications.SendIssueBulkChangeNotification(
+ issue_ids=[78901],
+ hostport='testbed-test.appspotmail.com',
+ old_owner_ids=[2],
+ comment_text='comment',
+ commenter_id=1,
+ amendments=[
+ tracker_bizobj.MakeStatusAmendment('New', 'Old'),
+ tracker_bizobj.MakeLabelsAmendment(['Added'], ['Removed']),
+ tracker_bizobj.MakeStatusAmendment('New', 'Old'),
+ ],
+ send_email=True,
+ users_by_id=2)
+
+ call_args_list = self._get_filtered_task_call_args(
+ create_task_mock, urls.NOTIFY_BULK_CHANGE_TASK + '.do')
+ self.assertEqual(1, len(call_args_list))
+ _path, params = self._get_create_task_path_and_params(call_args_list[0])
+ self.assertEqual(params['comment_text'], 'comment')
+ self.assertEqual(
+ params['amendments'].split('\n'),
+ [' Status: New', ' Labels: -Removed Added'])
+
+ @mock.patch('framework.cloud_tasks_helpers.create_task')
+ def testPrepareAndSendDeletedFilterRulesNotifications(self, create_task_mock):
+ filter_rule_strs = ['if yellow make orange', 'if orange make blue']
+ send_notifications.PrepareAndSendDeletedFilterRulesNotification(
+ 789, 'testbed-test.appspotmail.com', filter_rule_strs)
+
+ call_args_list = self._get_filtered_task_call_args(
+ create_task_mock, urls.NOTIFY_RULES_DELETED_TASK + '.do')
+ self.assertEqual(1, len(call_args_list))
+ _path, params = self._get_create_task_path_and_params(call_args_list[0])
+ self.assertEqual(params['project_id'], '789')
+ self.assertEqual(
+ params['filter_rules'], 'if yellow make orange,if orange make blue')
diff --git a/features/test/spammodel_test.py b/features/test/spammodel_test.py
new file mode 100644
index 0000000..3e99c8f
--- /dev/null
+++ b/features/test/spammodel_test.py
@@ -0,0 +1,39 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Tests for the spammodel module."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import mock
+import unittest
+import webapp2
+
+from features import spammodel
+from framework import urls
+
+
+class TrainingDataExportTest(unittest.TestCase):
+
+ def test_handler_definition(self):
+ instance = spammodel.TrainingDataExport()
+ self.assertIsInstance(instance, webapp2.RequestHandler)
+
+ @mock.patch('framework.cloud_tasks_helpers._get_client')
+ def test_enqueues_task(self, get_client_mock):
+ spammodel.TrainingDataExport().get()
+ task = {
+ 'app_engine_http_request':
+ {
+ 'relative_uri': urls.SPAM_DATA_EXPORT_TASK + '.do',
+ 'body': '',
+ 'headers': {
+ 'Content-type': 'application/x-www-form-urlencoded'
+ }
+ }
+ }
+ get_client_mock().create_task.assert_called_once()
+ ((_parent, called_task), _kwargs) = get_client_mock().create_task.call_args
+ self.assertEqual(called_task, task)