def testProcessIssueReply_NoEditIssuePerm(self): 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, 111L, ['awesome!'], self.services, strip_quoted_lines=True) self.mox.StubOutWithMock(mock_uia, 'Run') # Allow edit is false here because the permission set does not contain # EDIT_ISSUE. mock_uia.Run(self.cnxn, self.services, allow_edit=False) self.mox.ReplayAll() ret = self.inbound.ProcessIssueReply(self.cnxn, self.project, self.issue.local_id, self.project_addr, 'from_addr', 111L, [1, 2, 3], perms, 'awesome!') self.mox.VerifyAll() self.assertIsNone(ret)
def testProcessIssueReply_Success(self): 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, 111L, ['awesome!'], self.services, strip_quoted_lines=True) self.mox.StubOutWithMock(mock_uia, 'Run') mock_uia.Run(self.cnxn, self.services, allow_edit=True) self.mox.ReplayAll() ret = self.inbound.ProcessIssueReply(self.cnxn, self.project, self.issue.local_id, self.project_addr, 'from_addr', 111L, [1, 2, 3], perms, 'awesome!') self.mox.VerifyAll() self.assertIsNone(ret)
def testProcessIssueReply_Success(self): self.services.user.TestAddUser('*****@*****.**', 111) mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn, requester='*****@*****.**') 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)
def testProcessAlert_NewIssue(self, fake_pasicn, fake_pasibn): """When an alert for a new incident comes in, create a new issue.""" self.mox.StubOutWithMock(tracker_helpers, 'LookupComponentIDs') tracker_helpers.LookupComponentIDs(['Infra'], mox.IgnoreArg()).AndReturn([1]) self.mox.StubOutWithMock(self.services.config, 'LookupLabelID') self.services.config.LookupLabelID( self.cnxn, self.project.project_id, 'Incident-Id-incident-1').AndReturn(None) # Mock command parsing. mock_uia = commitlogcommands.UpdateIssueAction(101) self.mox.StubOutWithMock(commitlogcommands, 'UpdateIssueAction') commitlogcommands.UpdateIssueAction(101).AndReturn(mock_uia) self.mox.StubOutWithMock(mock_uia, 'Parse') mock_uia.Parse(self.cnxn, self.project.project_name, 111, ['issue body'], self.services, strip_quoted_lines=True) self.mox.ReplayAll() auth = authdata.AuthData(user_id=111, email='*****@*****.**') ret = self.inbound.ProcessAlert(self.cnxn, self.project, self.project_addr, '*****@*****.**', auth, 'issue title', 'issue body', 'incident-1') self.mox.VerifyAll() self.assertIsNone(ret) actual_issue = self.services.issue.GetIssueByLocalID( self.cnxn, self.project.project_id, 101) actual_comments = self.services.issue.GetCommentsForIssue( self.cnxn, actual_issue.issue_id) self.assertEqual('issue title', actual_issue.summary) self.assertEqual('Available', actual_issue.status) self.assertEqual(111, actual_issue.reporter_id) self.assertEqual([1], actual_issue.component_ids) self.assertEqual(None, actual_issue.owner_id) self.assertEqual([ 'Infra-Troopers-Alerts', 'Restrict-View-Google', 'Pri-2', 'Incident-Id-incident-1' ], actual_issue.labels) self.assertEqual( 'Filed by [email protected] on behalf of [email protected]\n\nissue body', actual_comments[0].content) self.assertEqual(1, len(fake_pasicn.mock_calls)) self.assertEqual(1, len(fake_pasibn.mock_calls))
def testProcessAlert_ExistingIssue(self): """When an alert for an ongoing incident comes in, add a comment.""" self.issue.labels = ['Incident-Id-incident-1'] self.mox.StubOutWithMock(self.services.config, 'LookupLabelID') self.services.config.LookupLabelID( self.cnxn, self.project.project_id, 'Incident-Id-incident-1').AndReturn(1234) self.mox.StubOutWithMock(self.services.issue, 'GetIIDsByLabelIDs') self.services.issue.GetIIDsByLabelIDs(self.cnxn, [1234], self.project.project_id, None).AndReturn([1]) self.mox.StubOutWithMock(self.services.issue, 'GetOpenAndClosedIssues') self.services.issue.GetOpenAndClosedIssues(self.cnxn, [1]).AndReturn( ([self.issue], [])) self.mox.StubOutWithMock(self.services.issue, 'GetComments') self.services.issue.GetComments(self.cnxn, issue_id=[self.issue.issue_id], commenter_id=[111]).AndReturn([]) self.mox.StubOutWithMock(self.services.issue, 'CreateIssueComment') self.services.issue.CreateIssueComment( self.cnxn, self.issue, 111, 'Filed by [email protected] on behalf of [email protected]\n\nissue body' ).AndReturn(None) # Mock command parsing. 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, ['issue body'], self.services, strip_quoted_lines=True) self.mox.ReplayAll() auth = authdata.AuthData(user_id=111, email='*****@*****.**') ret = self.inbound.ProcessAlert(self.cnxn, self.project, self.project_addr, '*****@*****.**', auth, 'issue title', 'issue body', 'incident-1') self.mox.VerifyAll() self.assertIsNone(ret)
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('*****@*****.**', 111) self.outsider = self.services.user.TestAddUser('*****@*****.**', 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 testProcessAlert_NewIssue_Codesearch(self, fake_pasicn, fake_pasibn): """When an alert for a new incident comes in, create a new issue. If the body contains the string 'codesearch' then we should auto-assign to the Infra>Codesearch component.""" self.mox.StubOutWithMock(tracker_helpers, 'LookupComponentIDs') tracker_helpers.LookupComponentIDs(['Infra>Codesearch'], mox.IgnoreArg()).AndReturn([2]) self.mox.StubOutWithMock(self.services.config, 'LookupLabelID') self.services.config.LookupLabelID( self.cnxn, self.project.project_id, 'Incident-Id-incident-1').AndReturn(None) # Mock command parsing. mock_uia = commitlogcommands.UpdateIssueAction(101) self.mox.StubOutWithMock(commitlogcommands, 'UpdateIssueAction') commitlogcommands.UpdateIssueAction(101).AndReturn(mock_uia) self.mox.StubOutWithMock(mock_uia, 'Parse') mock_uia.Parse(self.cnxn, self.project.project_name, 111, ['issue body codesearch'], self.services, strip_quoted_lines=True) self.mox.ReplayAll() auth = authdata.AuthData(user_id=111, email='*****@*****.**') ret = self.inbound.ProcessAlert(self.cnxn, self.project, self.project_addr, '*****@*****.**', auth, 'issue title', 'issue body codesearch', 'incident-1') self.mox.VerifyAll() self.assertIsNone(ret) actual_issue = self.services.issue.GetIssueByLocalID( self.cnxn, self.project.project_id, 101) self.assertEqual([2], actual_issue.component_ids) self.assertEqual(1, len(fake_pasicn.mock_calls)) self.assertEqual(1, len(fake_pasibn.mock_calls))
def ProcessIssueReply( self, mc, project, local_id, project_addr, body): """Examine an issue reply email body and add a comment to the issue. Args: mc: MonorailContext with cnxn and the requester email, user_id, perms. project: Project PB for the project containing the issue. local_id: int ID of the issue being replied to. project_addr: string email address used for outbound emails from that project. body: string email body text of the reply email. Returns: A list of follow-up work items, e.g., to notify other users of the new comment, or to notify the user that their reply was not processed. Side-effect: Adds a new comment to the issue, if no error is reported. """ try: issue = self.services.issue.GetIssueByLocalID( mc.cnxn, project.project_id, local_id) except exceptions.NoSuchIssueException: issue = None if not issue or issue.deleted: # The referenced issue was not found, e.g., it might have been # deleted, or someone messed with the subject line. Reject it. return _MakeErrorMessageReplyTask( project_addr, mc.auth.email, self._templates['no_artifact'], artifact_phrase='issue %d' % local_id, project_name=project.project_name) can_view = mc.perms.CanUsePerm( permissions.VIEW, mc.auth.effective_ids, project, permissions.GetRestrictions(issue)) can_comment = mc.perms.CanUsePerm( permissions.ADD_ISSUE_COMMENT, mc.auth.effective_ids, project, permissions.GetRestrictions(issue)) if not can_view or not can_comment: return _MakeErrorMessageReplyTask( project_addr, mc.auth.email, self._templates['no_perms'], artifact_phrase='issue %d' % local_id, project_name=project.project_name) # TODO(jrobbins): if the user does not have EDIT_ISSUE and the inbound # email tries to make an edit, send back an error message. lines = body.strip().split('\n') uia = commitlogcommands.UpdateIssueAction(local_id) uia.Parse(mc.cnxn, project.project_name, mc.auth.user_id, lines, self.services, strip_quoted_lines=True) uia.Run(mc, self.services)
def setUp(self): self.cnxn = 'fake cnxn' self.services = service_manager.Services(issue=fake.IssueService(), project=fake.ProjectService(), config=fake.ConfigService()) self.project = self.services.project.TestAddProject( 'proj', project_id=987, process_inbound_email=True) self.issue = tracker_pb2.Issue() self.issue.project_id = 987 self.issue.summary = 'summary' self.issue.status = 'Assigned' self.services.issue.TestAddIssue(self.issue) self.uia = commitlogcommands.UpdateIssueAction(self.issue.local_id) self.mox = mox.Mox()
def ProcessEmailNotification(services, cnxn, project, project_addr, from_addr, auth, subject, body, incident_id, msg, trooper_queue=None): """Process an alert notification email to create or update issues."" Args: cnxn: connection to SQL database. project: Project PB for the project containing the issue. project_addr: string email address the alert email was sent to. from_addr: string email address of the user who sent the alert email to our server. auth: AuthData object with user_id and email address of the user who will file the alert issue. subject: the subject of the email message body: the body text of the email message incident_id: string containing an optional unique incident used to de-dupe alert issues. msg: the email.Message object that the notification was delivered via. trooper_queue: the label specifying the trooper queue that the alert notification was sent to. If not given, the notification is sent to Infra-Troopers-Alerts. Side-effect: Creates an issue or issue comment, if no error was reported. """ # Make sure the email address is whitelisted. if not IsWhitelisted(from_addr): logging.info('Unauthorized %s tried to send alert to %s', from_addr, project_addr) return formatted_body = 'Filed by %s on behalf of %s\n\n%s' % (auth.email, from_addr, body) mc = monorailcontext.MonorailContext(services, auth=auth, cnxn=cnxn) mc.LookupLoggedInUserPerms(project) with work_env.WorkEnv(mc, services) as we: alert_props = GetAlertProperties(services, cnxn, project.project_id, incident_id, trooper_queue, msg) alert_issue = FindAlertIssue(services, cnxn, project.project_id, alert_props['incident_label']) if alert_issue: # Add a reply to the existing issue for this incident. services.issue.CreateIssueComment(cnxn, alert_issue, auth.user_id, formatted_body) else: # Create a new issue for this incident. alert_issue, _ = we.CreateIssue( project.project_id, subject, alert_props['status'], alert_props['owner_id'], alert_props['cc_ids'], alert_props['labels'], alert_props['field_values'], alert_props['component_ids'], formatted_body) # Update issue using commands. lines = body.strip().split('\n') uia = commitlogcommands.UpdateIssueAction(alert_issue.local_id) commands_found = uia.Parse(cnxn, project.project_name, auth.user_id, lines, services, strip_quoted_lines=True) if commands_found: uia.Run(mc, services)
def ProcessIssueReply( self, cnxn, project, local_id, project_addr, from_addr, author_id, effective_ids, perms, body): """Examine an issue reply email body and add a comment to the issue. Args: cnxn: connection to SQL database. project: Project PB for the project containing the issue. local_id: int ID of the issue being replied to. project_addr: string email address used for outbound emails from that project. from_addr: string email address of the user who sent the email reply to our server. author_id: int user ID of user who sent the reply email. effective_ids: set of int user IDs for the user (including any groups), or an empty set if user is not signed in. perms: PermissionSet for the user who sent the reply email. body: string email body text of the reply email. Returns: A list of follow-up work items, e.g., to notify other users of the new comment, or to notify the user that their reply was not processed. Side-effect: Adds a new comment to the issue, if no error is reported. """ try: issue = self.services.issue.GetIssueByLocalID( cnxn, project.project_id, local_id) except issue_svc.NoSuchIssueException: issue = None if not issue or issue.deleted: # The referenced issue was not found, e.g., it might have been # deleted, or someone messed with the subject line. Reject it. return _MakeErrorMessageReplyTask( project_addr, from_addr, self._templates['no_artifact'], artifact_phrase='issue %d' % local_id, project_name=project.project_name) can_view = perms.CanUsePerm( permissions.VIEW, effective_ids, project, permissions.GetRestrictions(issue)) can_comment = perms.CanUsePerm( permissions.ADD_ISSUE_COMMENT, effective_ids, project, permissions.GetRestrictions(issue)) if not can_view or not can_comment: return _MakeErrorMessageReplyTask( project_addr, from_addr, self._templates['no_perms'], artifact_phrase='issue %d' % local_id, project_name=project.project_name) allow_edit = permissions.CanEditIssue( effective_ids, perms, project, issue) # TODO(jrobbins): if the user does not have EDIT_ISSUE and the inbound # email tries to make an edit, send back an error message. lines = body.strip().split('\n') uia = commitlogcommands.UpdateIssueAction(local_id) uia.Parse(cnxn, project.project_name, author_id, lines, self.services, strip_quoted_lines=True) uia.Run(cnxn, self.services, allow_edit=allow_edit)
def ProcessAlert(self, cnxn, project, project_addr, from_addr, auth, subject, body, incident_id, owner_email=None, labels=None): """Examine an an alert issue email and create an issue based on the email. Args: cnxn: connection to SQL database. project: Project PB for the project containing the issue. project_addr: string email address the alert email was sent to. from_addr: string email address of the user who sent the alert email to our server. auth: AuthData object with user_id and email address of the user who will file the alert issue. body: string email body text of the reply email. incident_id: string containing an optional unique incident used to de-dupe alert issues. owner_email: string email address of the user the bug will be assigned to. Returns: A list of follow-up work items, e.g., to notify other users of the new comment, or to notify the user that their reply was not processed. Side-effect: Adds a new comment to the issue, if no error is reported. """ # Make sure the email address is whitelisted. if not self.IsWhitelisted(from_addr): logging.info('Unauthorized %s tried to send alert to %s', from_addr, project_addr) return None # Create the actual issue from the email data. # TODO(zhangtiff): Set labels, components, etc based on email content. cc_ids = [] status = 'Available' if not labels: labels = ['Infra-Troopers-Alerts'] labels += ['Restrict-View-Google', 'Pri-2'] field_values = [] # TODO(zhangtiff): Remove this special casing once components can be set via # the email header. if 'codesearch' in body: components = ['Infra>Codesearch'] else: components = ['Infra'] formatted_body = 'Filed by %s on behalf of %s\n\n%s' % ( auth.email, from_addr, body) # Lookup components. config = self.services.config.GetProjectConfig(cnxn, project.project_id) component_ids = tracker_helpers.LookupComponentIDs(components, config) mc = monorailcontext.MonorailContext(self.services, auth=auth, cnxn=cnxn) mc.LookupLoggedInUserPerms(project) with work_env.WorkEnv(mc, self.services) as we: updated_issue = None owner_id = None if owner_email: owner_id = self.services.user.LookupUserID(cnxn, owner_email, autocreate=True) status = 'Assigned' if incident_id: incident_label = 'Incident-Id-' + incident_id labels.append(incident_label) label_id = self.services.config.LookupLabelID( cnxn, project.project_id, incident_label) if label_id: issue_ids = self.services.issue.GetIIDsByLabelIDs( cnxn, [label_id], project.project_id, None) issues, _ = self.services.issue.GetOpenAndClosedIssues( cnxn, issue_ids) latest_issue = None # Find the most recently modified open issue. for issue in issues: if not latest_issue: latest_issue = issue elif issue.modified_timestamp > latest_issue.modified_timestamp: latest_issue = issue if latest_issue: updated_issue = latest_issue # Find all comments on the issue by the current user. comments = self.services.issue.GetComments( cnxn, issue_id=[updated_issue.issue_id], commenter_id=[auth.user_id]) # Timestamp for 24 hours ago in seconds from epoch. yesterday = int(time.time()) - 24 * 60 * 60 for comment in comments: # Stop early if we find a comment created in the last 24 hours. if comment.timestamp > yesterday: logging.info( 'Alert fired again with incident id: %s', incident_id) return None # Add a reply to the existing issue for this incident. self.services.issue.CreateIssueComment( cnxn, updated_issue, auth.user_id, formatted_body) if not updated_issue: updated_issue, _ = we.CreateIssue(project.project_id, subject, status, owner_id, cc_ids, labels, field_values, component_ids, formatted_body) # Update issue using commands. lines = body.strip().split('\n') uia = commitlogcommands.UpdateIssueAction(updated_issue.local_id) commands_found = uia.Parse(cnxn, project.project_name, auth.user_id, lines, self.services, strip_quoted_lines=True) if commands_found: uia.Run(cnxn, self.services, allow_edit=True)