def test_processWithMergeDirectiveEmail(self): """process errors if merge@ address used.""" message = self.factory.makeSignedMessage() file_alias = self.factory.makeLibraryFileAlias( content=message.as_string()) # mail.incoming.handleMail also explicitly does this. switch_dbuser(config.processmail.dbuser) code_handler = CodeHandler() code_handler.process(message, '*****@*****.**', file_alias) notification = pop_notifications()[0] self.assertEqual( 'Merge directive not supported.', notification['Subject'])
def setUp(self): super(TestCodeHandler, self).setUp(user='******') self.code_handler = CodeHandler() self._old_policy = setSecurityPolicy(LaunchpadSecurityPolicy)
class TestCodeHandler(TestCaseWithFactory): """Test the code email hander.""" layer = ZopelessAppServerLayer def setUp(self): super(TestCodeHandler, self).setUp(user='******') self.code_handler = CodeHandler() self._old_policy = setSecurityPolicy(LaunchpadSecurityPolicy) def tearDown(self): setSecurityPolicy(self._old_policy) super(TestCodeHandler, self).tearDown() def test_get(self): handler = mail_handlers.get(config.launchpad.code_domain) self.assertIsInstance(handler, CodeHandler) def test_process(self): """Processing an email creates an appropriate CodeReviewComment.""" mail = self.factory.makeSignedMessage('<my-id>') bmp = self.factory.makeBranchMergeProposal() email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.assertTrue(self.code_handler.process( mail, email_addr, None), "Succeeded, but didn't return True") # if the message has not been created, this raises SQLObjectNotFound MessageSet().get('<my-id>') def test_process_packagebranch(self): """Processing an email related to a package branch works..""" mail = self.factory.makeSignedMessage('<my-id>') target_branch = self.factory.makePackageBranch() bmp = self.factory.makeBranchMergeProposal( target_branch=target_branch) email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.code_handler.process(mail, email_addr, None) self.assertIn( '<my-id>', [comment.message.rfc822msgid for comment in bmp.all_comments]) def test_process_git(self): """Processing an email related to a Git-based merge proposal works.""" mail = self.factory.makeSignedMessage('<my-id>') bmp = self.factory.makeBranchMergeProposalForGit() email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.code_handler.process(mail, email_addr, None) self.assertIn( '<my-id>', [comment.message.rfc822msgid for comment in bmp.all_comments]) def test_processBadAddress(self): """When a bad address is supplied, it returns False.""" mail = self.factory.makeSignedMessage('<my-id>') switch_dbuser(config.processmail.dbuser) self.assertFalse(self.code_handler.process(mail, '*****@*****.**', None)) def test_processNonExistantAddress(self): """When a non-existant address is supplied, it returns False.""" mail = self.factory.makeSignedMessage('<my-id>') switch_dbuser(config.processmail.dbuser) self.assertTrue(self.code_handler.process(mail, '*****@*****.**', None)) notification = pop_notifications()[0] self.assertEqual('Submit Request Failure', notification['subject']) # The returned message is a multipart message, the first part is # the message, and the second is the original message. message, original = notification.get_payload() self.assertIn( "There is no merge proposal at [email protected]\n", message.get_payload(decode=True)) def test_processBadVote(self): """process handles bad votes properly.""" mail = self.factory.makeSignedMessage(body=' vote badvalue') # Make sure that the correct user principal is there. login(mail['From']) bmp = self.factory.makeBranchMergeProposal() # Remove the notifications sent about the new proposal. pop_notifications() email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.assertTrue(self.code_handler.process( mail, email_addr, None), "Didn't return True") notification = pop_notifications()[0] self.assertEqual('Submit Request Failure', notification['subject']) # The returned message is a multipart message, the first part is # the message, and the second is the original message. message, original = notification.get_payload() self.assertEqual(dedent("""\ An error occurred while processing a mail you sent to Launchpad's email interface. Failing command: vote badvalue Error message: The 'review' command expects any of the following arguments: abstain, approve, disapprove, needs-fixing, needs-info, resubmit For example: review needs-fixing --\x20 For more information about using Launchpad by email, see https://help.launchpad.net/EmailInterface or send an email to [email protected]"""), message.get_payload(decode=True)) self.assertEqual(mail['From'], notification['To']) def test_getReplyAddress(self): """getReplyAddress should return From or Reply-to address.""" mail = self.factory.makeSignedMessage() switch_dbuser(config.processmail.dbuser) self.assertEqual( mail['From'], self.code_handler._getReplyAddress(mail)) mail['Reply-to'] = self.factory.getUniqueEmailAddress() self.assertEqual( mail['Reply-to'], self.code_handler._getReplyAddress(mail)) def test_process_for_imported_branch(self): """Make sure that the database user is able refer to import branches. Import branches have different permission checks than other branches. Permission to mark a merge proposal as approved checks launchpad.Edit of the target branch, or membership of the review team on the target branch. For import branches launchpad.Edit also checks the registrant of the code import if there is one, and membership of vcs-imports. So if someone is attempting to review something on an import branch, but they don't have launchpad.Edit but are a member of the review team, then a check against the code import is done. """ mail = self.factory.makeSignedMessage(body=' merge approved') code_import = self.factory.makeCodeImport() bmp = self.factory.makeBranchMergeProposal( target_branch=code_import.branch) email_addr = bmp.address switch_dbuser(config.processmail.dbuser) pop_notifications() self.code_handler.process(mail, email_addr, None) notification = pop_notifications()[0] # The returned message is a multipart message, the first part is # the message, and the second is the original message. message, original = notification.get_payload() self.assertTrue( "You are not a reviewer for the branch" in message.get_payload(decode=True)) def test_processVote(self): """Process respects the vote command.""" mail = self.factory.makeSignedMessage(body=' vote Abstain EBAILIWICK') bmp = self.factory.makeBranchMergeProposal() email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.code_handler.process(mail, email_addr, None) self.assertEqual(CodeReviewVote.ABSTAIN, bmp.all_comments[0].vote) self.assertEqual('ebailiwick', bmp.all_comments[0].vote_tag) def test_processVoteColon(self): """Process respects the vote: command.""" mail = self.factory.makeSignedMessage( body=' vote: Abstain EBAILIWICK') bmp = self.factory.makeBranchMergeProposal() email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.code_handler.process(mail, email_addr, None) self.assertEqual(CodeReviewVote.ABSTAIN, bmp.all_comments[0].vote) self.assertEqual('ebailiwick', bmp.all_comments[0].vote_tag) def test_processReview(self): """Process respects the review command.""" mail = self.factory.makeSignedMessage(body=' review Abstain ROAR!') bmp = self.factory.makeBranchMergeProposal() email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.code_handler.process(mail, email_addr, None) self.assertEqual(CodeReviewVote.ABSTAIN, bmp.all_comments[0].vote) self.assertEqual('roar!', bmp.all_comments[0].vote_tag) def test_processReviewColon(self): """Process respects the review: command.""" mail = self.factory.makeSignedMessage(body=' review: Abstain ROAR!') bmp = self.factory.makeBranchMergeProposal() email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.code_handler.process(mail, email_addr, None) self.assertEqual(CodeReviewVote.ABSTAIN, bmp.all_comments[0].vote) self.assertEqual('roar!', bmp.all_comments[0].vote_tag) def test_processWithExistingVote(self): """Process respects the vote command.""" mail = self.factory.makeSignedMessage(body=' vote Abstain EBAILIWICK') sender = self.factory.makePerson() bmp = self.factory.makeBranchMergeProposal(reviewer=sender) email_addr = bmp.address [vote] = list(bmp.votes) self.assertEqual(sender, vote.reviewer) self.assertTrue(vote.comment is None) switch_dbuser(config.processmail.dbuser) # Login the sender as they are set as the message owner. login_person(sender) self.code_handler.process(mail, email_addr, None) comment = bmp.all_comments[0] self.assertEqual(CodeReviewVote.ABSTAIN, comment.vote) self.assertEqual('ebailiwick', comment.vote_tag) [vote] = list(bmp.votes) self.assertEqual(sender, vote.reviewer) self.assertEqual(comment, vote.comment) def test_processmail_generates_job(self): """Processing mail causes an email job to be created.""" mail = self.factory.makeSignedMessage( body=' vote Abstain EBAILIWICK', subject='subject') bmp = self.factory.makeBranchMergeProposal() # Pop the notifications generated by the new proposal. pop_notifications() subscriber = self.factory.makePerson() bmp.source_branch.subscribe( subscriber, BranchSubscriptionNotificationLevel.NOEMAIL, None, CodeReviewNotificationLevel.FULL, subscriber) email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.code_handler.process(mail, email_addr, None) job = Store.of(bmp).find( BranchMergeProposalJob, BranchMergeProposalJob.branch_merge_proposal == bmp, BranchMergeProposalJob.job_type == BranchMergeProposalJobType.CODE_REVIEW_COMMENT_EMAIL).one() self.assertIsNot(None, job) # Ensure the DB operations violate no constraints. Store.of(bmp).flush() def test_getBranchMergeProposal(self): """The correct BranchMergeProposal is returned for the address.""" bmp = self.factory.makeBranchMergeProposal() switch_dbuser(config.processmail.dbuser) bmp2 = self.code_handler.getBranchMergeProposal(bmp.address) self.assertEqual(bmp, bmp2) def test_getBranchMergeProposalInvalid(self): """InvalidBranchMergeProposalAddress is raised if appropriate.""" switch_dbuser(config.processmail.dbuser) self.assertRaises(InvalidBranchMergeProposalAddress, self.code_handler.getBranchMergeProposal, '') self.assertRaises(InvalidBranchMergeProposalAddress, self.code_handler.getBranchMergeProposal, 'mp+abc@') def test_processWithMergeDirectiveEmail(self): """process errors if merge@ address used.""" message = self.factory.makeSignedMessage() file_alias = self.factory.makeLibraryFileAlias( content=message.as_string()) # mail.incoming.handleMail also explicitly does this. switch_dbuser(config.processmail.dbuser) code_handler = CodeHandler() code_handler.process(message, '*****@*****.**', file_alias) notification = pop_notifications()[0] self.assertEqual( 'Merge directive not supported.', notification['Subject']) def test_reviewer_with_diff(self): """Requesting a review with a diff works.""" bmp = make_merge_proposal_without_reviewers(self.factory) self.factory.makePreviewDiff(merge_proposal=bmp) # To record the diff in the librarian. transaction.commit() eric = self.factory.makePerson(name="eric", email="*****@*****.**") mail = self.factory.makeSignedMessage(body=' reviewer eric') email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.code_handler.process(mail, email_addr, None) [vote] = bmp.votes self.assertEqual(eric, vote.reviewer) def test_processMissingSubject(self): """If the subject is missing, the user is warned by email.""" mail = self.factory.makeSignedMessage( body=' review abstain', subject='') bmp = self.factory.makeBranchMergeProposal() pop_notifications() email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.code_handler.process(mail, email_addr, None) [notification] = pop_notifications() self.assertEqual( notification['Subject'], 'Error Creating Merge Proposal') self.assertEqual( notification.get_payload(decode=True), 'Your message did not contain a subject. Launchpad code ' 'reviews require all\nemails to contain subject lines. ' 'Please re-send your email including the\nsubject line.\n\n') self.assertEqual(notification['to'], mail['from']) self.assertEqual(0, bmp.all_comments.count()) def test_notifies_modification(self): """Changes to the merge proposal itself trigger events.""" mail = self.factory.makeSignedMessage(body=' merge approved') bmp = self.factory.makeBranchMergeProposal() email_addr = bmp.address switch_dbuser(config.processmail.dbuser) login_person(bmp.merge_target.owner) _, events = self.assertNotifies( [ObjectModifiedEvent, ObjectCreatedEvent], False, self.code_handler.process, mail, email_addr, None) self.assertEqual(bmp, events[0].object) self.assertEqual( BranchMergeProposalStatus.WORK_IN_PROGRESS, events[0].object_before_modification.queue_status) self.assertEqual( BranchMergeProposalStatus.CODE_APPROVED, events[0].object.queue_status)
class TestCodeHandler(TestCaseWithFactory): """Test the code email hander.""" layer = ZopelessAppServerLayer def setUp(self): super(TestCodeHandler, self).setUp(user='******') self.code_handler = CodeHandler() self._old_policy = setSecurityPolicy(LaunchpadSecurityPolicy) def tearDown(self): setSecurityPolicy(self._old_policy) super(TestCodeHandler, self).tearDown() def test_get(self): handler = mail_handlers.get(config.launchpad.code_domain) self.assertIsInstance(handler, CodeHandler) def test_process(self): """Processing an email creates an appropriate CodeReviewComment.""" mail = self.factory.makeSignedMessage('<my-id>') bmp = self.factory.makeBranchMergeProposal() email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.assertTrue(self.code_handler.process( mail, email_addr, None), "Succeeded, but didn't return True") # if the message has not been created, this raises SQLObjectNotFound MessageSet().get('<my-id>') def test_process_packagebranch(self): """Processing an email related to a package branch works..""" mail = self.factory.makeSignedMessage('<my-id>') target_branch = self.factory.makePackageBranch() bmp = self.factory.makeBranchMergeProposal( target_branch=target_branch) email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.code_handler.process(mail, email_addr, None) self.assertIn( '<my-id>', [comment.message.rfc822msgid for comment in bmp.all_comments]) def test_processBadAddress(self): """When a bad address is supplied, it returns False.""" mail = self.factory.makeSignedMessage('<my-id>') switch_dbuser(config.processmail.dbuser) self.assertFalse(self.code_handler.process(mail, '*****@*****.**', None)) def test_processNonExistantAddress(self): """When a non-existant address is supplied, it returns False.""" mail = self.factory.makeSignedMessage('<my-id>') switch_dbuser(config.processmail.dbuser) self.assertTrue(self.code_handler.process(mail, '*****@*****.**', None)) notification = pop_notifications()[0] self.assertEqual('Submit Request Failure', notification['subject']) # The returned message is a multipart message, the first part is # the message, and the second is the original message. message, original = notification.get_payload() self.assertIn( "There is no merge proposal at [email protected]\n", message.get_payload(decode=True)) def test_processBadVote(self): """process handles bad votes properly.""" mail = self.factory.makeSignedMessage(body=' vote badvalue') # Make sure that the correct user principal is there. login(mail['From']) bmp = self.factory.makeBranchMergeProposal() # Remove the notifications sent about the new proposal. pop_notifications() email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.assertTrue(self.code_handler.process( mail, email_addr, None), "Didn't return True") notification = pop_notifications()[0] self.assertEqual('Submit Request Failure', notification['subject']) # The returned message is a multipart message, the first part is # the message, and the second is the original message. message, original = notification.get_payload() self.assertEqual(dedent("""\ An error occurred while processing a mail you sent to Launchpad's email interface. Failing command: vote badvalue Error message: The 'review' command expects any of the following arguments: abstain, approve, disapprove, needs-fixing, needs-info, resubmit For example: review needs-fixing --\x20 For more information about using Launchpad by e-mail, see https://help.launchpad.net/EmailInterface or send an email to [email protected]"""), message.get_payload(decode=True)) self.assertEqual(mail['From'], notification['To']) def test_getReplyAddress(self): """getReplyAddress should return From or Reply-to address.""" mail = self.factory.makeSignedMessage() switch_dbuser(config.processmail.dbuser) self.assertEqual( mail['From'], self.code_handler._getReplyAddress(mail)) mail['Reply-to'] = self.factory.getUniqueEmailAddress() self.assertEqual( mail['Reply-to'], self.code_handler._getReplyAddress(mail)) def test_process_for_imported_branch(self): """Make sure that the database user is able refer to import branches. Import branches have different permission checks than other branches. Permission to mark a merge proposal as approved checks launchpad.Edit of the target branch, or membership of the review team on the target branch. For import branches launchpad.Edit also checks the registrant of the code import if there is one, and membership of vcs-imports. So if someone is attempting to review something on an import branch, but they don't have launchpad.Edit but are a member of the review team, then a check against the code import is done. """ mail = self.factory.makeSignedMessage(body=' merge approved') code_import = self.factory.makeCodeImport() bmp = self.factory.makeBranchMergeProposal( target_branch=code_import.branch) email_addr = bmp.address switch_dbuser(config.processmail.dbuser) pop_notifications() self.code_handler.process(mail, email_addr, None) notification = pop_notifications()[0] # The returned message is a multipart message, the first part is # the message, and the second is the original message. message, original = notification.get_payload() self.assertTrue( "You are not a reviewer for the branch" in message.get_payload(decode=True)) def test_processVote(self): """Process respects the vote command.""" mail = self.factory.makeSignedMessage(body=' vote Abstain EBAILIWICK') bmp = self.factory.makeBranchMergeProposal() email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.code_handler.process(mail, email_addr, None) self.assertEqual(CodeReviewVote.ABSTAIN, bmp.all_comments[0].vote) self.assertEqual('ebailiwick', bmp.all_comments[0].vote_tag) def test_processVoteColon(self): """Process respects the vote: command.""" mail = self.factory.makeSignedMessage( body=' vote: Abstain EBAILIWICK') bmp = self.factory.makeBranchMergeProposal() email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.code_handler.process(mail, email_addr, None) self.assertEqual(CodeReviewVote.ABSTAIN, bmp.all_comments[0].vote) self.assertEqual('ebailiwick', bmp.all_comments[0].vote_tag) def test_processReview(self): """Process respects the review command.""" mail = self.factory.makeSignedMessage(body=' review Abstain ROAR!') bmp = self.factory.makeBranchMergeProposal() email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.code_handler.process(mail, email_addr, None) self.assertEqual(CodeReviewVote.ABSTAIN, bmp.all_comments[0].vote) self.assertEqual('roar!', bmp.all_comments[0].vote_tag) def test_processReviewColon(self): """Process respects the review: command.""" mail = self.factory.makeSignedMessage(body=' review: Abstain ROAR!') bmp = self.factory.makeBranchMergeProposal() email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.code_handler.process(mail, email_addr, None) self.assertEqual(CodeReviewVote.ABSTAIN, bmp.all_comments[0].vote) self.assertEqual('roar!', bmp.all_comments[0].vote_tag) def test_processWithExistingVote(self): """Process respects the vote command.""" mail = self.factory.makeSignedMessage(body=' vote Abstain EBAILIWICK') sender = self.factory.makePerson() bmp = self.factory.makeBranchMergeProposal(reviewer=sender) email_addr = bmp.address [vote] = list(bmp.votes) self.assertEqual(sender, vote.reviewer) self.assertTrue(vote.comment is None) switch_dbuser(config.processmail.dbuser) # Login the sender as they are set as the message owner. login_person(sender) self.code_handler.process(mail, email_addr, None) comment = bmp.all_comments[0] self.assertEqual(CodeReviewVote.ABSTAIN, comment.vote) self.assertEqual('ebailiwick', comment.vote_tag) [vote] = list(bmp.votes) self.assertEqual(sender, vote.reviewer) self.assertEqual(comment, vote.comment) def test_processmail_generates_job(self): """Processing mail causes an email job to be created.""" mail = self.factory.makeSignedMessage( body=' vote Abstain EBAILIWICK', subject='subject') bmp = self.factory.makeBranchMergeProposal() # Pop the notifications generated by the new proposal. pop_notifications() subscriber = self.factory.makePerson() bmp.source_branch.subscribe( subscriber, BranchSubscriptionNotificationLevel.NOEMAIL, None, CodeReviewNotificationLevel.FULL, subscriber) email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.code_handler.process(mail, email_addr, None) job = Store.of(bmp).find( BranchMergeProposalJob, BranchMergeProposalJob.branch_merge_proposal == bmp, BranchMergeProposalJob.job_type == BranchMergeProposalJobType.CODE_REVIEW_COMMENT_EMAIL).one() self.assertIsNot(None, job) # Ensure the DB operations violate no constraints. Store.of(bmp).flush() def test_getBranchMergeProposal(self): """The correct BranchMergeProposal is returned for the address.""" bmp = self.factory.makeBranchMergeProposal() switch_dbuser(config.processmail.dbuser) bmp2 = self.code_handler.getBranchMergeProposal(bmp.address) self.assertEqual(bmp, bmp2) def test_getBranchMergeProposalInvalid(self): """InvalidBranchMergeProposalAddress is raised if appropriate.""" switch_dbuser(config.processmail.dbuser) self.assertRaises(InvalidBranchMergeProposalAddress, self.code_handler.getBranchMergeProposal, '') self.assertRaises(InvalidBranchMergeProposalAddress, self.code_handler.getBranchMergeProposal, 'mp+abc@') def test_processWithMergeDirectiveEmail(self): """process errors if merge@ address used.""" message = self.factory.makeSignedMessage() file_alias = self.factory.makeLibraryFileAlias( content=message.as_string()) # mail.incoming.handleMail also explicitly does this. switch_dbuser(config.processmail.dbuser) code_handler = CodeHandler() code_handler.process(message, '*****@*****.**', file_alias) notification = pop_notifications()[0] self.assertEqual( 'Merge directive not supported.', notification['Subject']) def test_reviewer_with_diff(self): """Requesting a review with a diff works.""" bmp = make_merge_proposal_without_reviewers(self.factory) preview_diff = self.factory.makePreviewDiff(merge_proposal=bmp) # To record the diff in the librarian. transaction.commit() eric = self.factory.makePerson(name="eric", email="*****@*****.**") mail = self.factory.makeSignedMessage(body=' reviewer eric') email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.code_handler.process(mail, email_addr, None) [vote] = bmp.votes self.assertEqual(eric, vote.reviewer) def test_processMissingSubject(self): """If the subject is missing, the user is warned by email.""" mail = self.factory.makeSignedMessage( body=' review abstain', subject='') bmp = self.factory.makeBranchMergeProposal() pop_notifications() email_addr = bmp.address switch_dbuser(config.processmail.dbuser) self.code_handler.process(mail, email_addr, None) [notification] = pop_notifications() self.assertEqual( notification['Subject'], 'Error Creating Merge Proposal') self.assertEqual( notification.get_payload(decode=True), 'Your message did not contain a subject. Launchpad code ' 'reviews require all\nemails to contain subject lines. ' 'Please re-send your email including the\nsubject line.\n\n') self.assertEqual(notification['to'], mail['from']) self.assertEqual(0, bmp.all_comments.count())