blob: cab9d0abdab735aa453952d24f638c52f0dea60b [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# Copyright 2016 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
Copybara854996b2021-09-07 19:36:02 +00004
5"""Unittests for monorail.feature.inboundemail."""
6from __future__ import print_function
7from __future__ import division
8from __future__ import absolute_import
9
10import unittest
Copybara854996b2021-09-07 19:36:02 +000011
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +020012try:
13 from mox3 import mox
14except ImportError:
15 import mox
Copybara854996b2021-09-07 19:36:02 +000016import time
17
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020018from google.appengine.api import mail
Copybara854996b2021-09-07 19:36:02 +000019
20import settings
21from businesslogic import work_env
22from features import alert2issue
23from features import commitlogcommands
24from features import inboundemail
25from framework import authdata
26from framework import emailfmt
27from framework import monorailcontext
28from framework import permissions
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010029from mrproto import project_pb2
30from mrproto import tracker_pb2
31from mrproto import user_pb2
Copybara854996b2021-09-07 19:36:02 +000032from services import service_manager
33from testing import fake
34from testing import testing_helpers
35from tracker import tracker_helpers
36
37
38class InboundEmailTest(unittest.TestCase):
Copybara854996b2021-09-07 19:36:02 +000039 def setUp(self):
40 self.cnxn = 'fake cnxn'
41 self.services = service_manager.Services(
42 config=fake.ConfigService(),
43 issue=fake.IssueService(),
44 user=fake.UserService(),
45 usergroup=fake.UserGroupService(),
46 project=fake.ProjectService())
47 self.project = self.services.project.TestAddProject(
48 'proj', project_id=987, process_inbound_email=True,
49 contrib_ids=[111])
50 self.project_addr = 'proj@monorail.example.com'
51
52 self.issue = tracker_pb2.Issue()
53 self.issue.project_id = 987
54 self.issue.local_id = 100
55 self.services.issue.TestAddIssue(self.issue)
56
57 self.msg = testing_helpers.MakeMessage(
58 testing_helpers.HEADER_LINES, 'awesome!')
59
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +020060 self.inbound = inboundemail.InboundEmail(self.services)
Copybara854996b2021-09-07 19:36:02 +000061 self.mox = mox.Mox()
62
63 def tearDown(self):
64 self.mox.UnsetStubs()
65 self.mox.ResetAll()
66
67 def testTemplates(self):
68 for name, template_path in self.inbound._templates.items():
69 assert(name in inboundemail.MSG_TEMPLATES)
70 assert(
71 template_path.GetTemplatePath().endswith(
72 inboundemail.MSG_TEMPLATES[name]))
73
74 def testProcessMail_MsgTooBig(self):
75 self.mox.StubOutWithMock(emailfmt, 'IsBodyTooBigToParse')
76 emailfmt.IsBodyTooBigToParse(mox.IgnoreArg()).AndReturn(True)
77 self.mox.ReplayAll()
78
79 email_tasks = self.inbound.ProcessMail(self.msg, self.project_addr)
80 self.mox.VerifyAll()
81 self.assertEqual(1, len(email_tasks))
82 email_task = email_tasks[0]
83 self.assertEqual('user@example.com', email_task['to'])
84 self.assertEqual('Email body too long', email_task['subject'])
85
86 def testProcessMail_NoProjectOnToLine(self):
87 self.mox.StubOutWithMock(emailfmt, 'IsProjectAddressOnToLine')
88 emailfmt.IsProjectAddressOnToLine(
89 self.project_addr, [self.project_addr]).AndReturn(False)
90 self.mox.ReplayAll()
91
92 ret = self.inbound.ProcessMail(self.msg, self.project_addr)
93 self.mox.VerifyAll()
94 self.assertIsNone(ret)
95
96 def testProcessMail_IssueUnidentified(self):
97 self.mox.StubOutWithMock(emailfmt, 'IdentifyProjectVerbAndLabel')
98 emailfmt.IdentifyProjectVerbAndLabel(self.project_addr).AndReturn(('proj',
99 None, None))
100
101 self.mox.StubOutWithMock(emailfmt, 'IdentifyIssue')
102 emailfmt.IdentifyIssue('proj', mox.IgnoreArg()).AndReturn((None))
103
104 self.mox.ReplayAll()
105
106 ret = self.inbound.ProcessMail(self.msg, self.project_addr)
107 self.mox.VerifyAll()
108 self.assertIsNone(ret)
109
110 def testProcessMail_ProjectNotLive(self):
111 self.services.user.TestAddUser('user@example.com', 111)
112 self.project.state = project_pb2.ProjectState.DELETABLE
113 email_tasks = self.inbound.ProcessMail(self.msg, self.project_addr)
114 email_task = email_tasks[0]
115 self.assertEqual('user@example.com', email_task['to'])
116 self.assertEqual('Project not found', email_task['subject'])
117
118 def testProcessMail_ProjectInboundEmailDisabled(self):
119 self.services.user.TestAddUser('user@example.com', 111)
120 self.project.process_inbound_email = False
121 email_tasks = self.inbound.ProcessMail(self.msg, self.project_addr)
122 email_task = email_tasks[0]
123 self.assertEqual('user@example.com', email_task['to'])
124 self.assertEqual(
125 'Email replies are not enabled in project proj', email_task['subject'])
126
127 def testProcessMail_NoRefHeader(self):
128 self.services.user.TestAddUser('user@example.com', 111)
129 self.mox.StubOutWithMock(emailfmt, 'ValidateReferencesHeader')
130 emailfmt.ValidateReferencesHeader(
131 mox.IgnoreArg(), self.project, mox.IgnoreArg(),
132 mox.IgnoreArg()).AndReturn(False)
133 emailfmt.ValidateReferencesHeader(
134 mox.IgnoreArg(), self.project, mox.IgnoreArg(),
135 mox.IgnoreArg()).AndReturn(False)
136 self.mox.ReplayAll()
137
138 email_tasks = self.inbound.ProcessMail(self.msg, self.project_addr)
139 self.mox.VerifyAll()
140 self.assertEqual(1, len(email_tasks))
141 email_task = email_tasks[0]
142 self.assertEqual('user@example.com', email_task['to'])
143 self.assertEqual(
144 'Your message is not a reply to a notification email',
145 email_task['subject'])
146
147 def testProcessMail_NoAccount(self):
148 # Note: not calling TestAddUser().
149 email_tasks = self.inbound.ProcessMail(self.msg, self.project_addr)
150 self.mox.VerifyAll()
151 self.assertEqual(1, len(email_tasks))
152 email_task = email_tasks[0]
153 self.assertEqual('user@example.com', email_task['to'])
154 self.assertEqual(
155 'Could not determine account of sender', email_task['subject'])
156
157 def testProcessMail_BannedAccount(self):
158 user_pb = self.services.user.TestAddUser('user@example.com', 111)
159 user_pb.banned = 'banned'
160
161 self.mox.StubOutWithMock(emailfmt, 'ValidateReferencesHeader')
162 emailfmt.ValidateReferencesHeader(
163 mox.IgnoreArg(), self.project, mox.IgnoreArg(),
164 mox.IgnoreArg()).AndReturn(True)
165 self.mox.ReplayAll()
166
167 email_tasks = self.inbound.ProcessMail(self.msg, self.project_addr)
168 self.mox.VerifyAll()
169 self.assertEqual(1, len(email_tasks))
170 email_task = email_tasks[0]
171 self.assertEqual('user@example.com', email_task['to'])
172 self.assertEqual(
173 'You are banned from using this issue tracker', email_task['subject'])
174
175 def testProcessMail_Success(self):
176 self.services.user.TestAddUser('user@example.com', 111)
177
178 self.mox.StubOutWithMock(emailfmt, 'ValidateReferencesHeader')
179 emailfmt.ValidateReferencesHeader(
180 mox.IgnoreArg(), self.project, mox.IgnoreArg(),
181 mox.IgnoreArg()).AndReturn(True)
182
183 self.mox.StubOutWithMock(self.inbound, 'ProcessIssueReply')
184 self.inbound.ProcessIssueReply(
185 mox.IgnoreArg(), self.project, 123, self.project_addr,
186 'awesome!')
187
188 self.mox.ReplayAll()
189
190 ret = self.inbound.ProcessMail(self.msg, self.project_addr)
191 self.mox.VerifyAll()
192 self.assertIsNone(ret)
193
194 def testProcessMail_Success_with_AlertNotification(self):
195 """Test ProcessMail with an alert notification message.
196
197 This is a sanity check for alert2issue.ProcessEmailNotification to ensure
198 that it can be successfully invoked in ProcessMail. Each function of
199 alert2issue module should be tested in aler2issue_test.
200 """
201 project_name = self.project.project_name
202 verb = 'alert'
203 trooper_queue = 'my-trooper'
204 project_addr = '%s+%s+%s@example.com' % (project_name, verb, trooper_queue)
205
206 self.mox.StubOutWithMock(emailfmt, 'IsProjectAddressOnToLine')
207 emailfmt.IsProjectAddressOnToLine(
208 project_addr, mox.IgnoreArg()).AndReturn(True)
209
210 class MockAuthData(object):
211 def __init__(self):
212 self.user_pb = user_pb2.MakeUser(111)
213 self.effective_ids = set([1, 2, 3])
214 self.user_id = 111
215 self.email = 'user@example.com'
216
217 mock_auth_data = MockAuthData()
218 self.mox.StubOutWithMock(authdata.AuthData, 'FromEmail')
219 authdata.AuthData.FromEmail(
220 mox.IgnoreArg(), settings.alert_service_account, self.services,
221 autocreate=True).AndReturn(mock_auth_data)
222
223 self.mox.StubOutWithMock(alert2issue, 'ProcessEmailNotification')
224 alert2issue.ProcessEmailNotification(
225 self.services, mox.IgnoreArg(), self.project, project_addr,
226 mox.IgnoreArg(), mock_auth_data, mox.IgnoreArg(), 'awesome!', '',
227 self.msg, trooper_queue)
228
229 self.mox.ReplayAll()
230 ret = self.inbound.ProcessMail(self.msg, project_addr)
231 self.mox.VerifyAll()
232 self.assertIsNone(ret)
233
234 def testProcessIssueReply_NoIssue(self):
235 nonexistant_local_id = 200
236 mc = monorailcontext.MonorailContext(
237 self.services, cnxn=self.cnxn, requester='user@example.com')
238 mc.LookupLoggedInUserPerms(self.project)
239
240 email_tasks = self.inbound.ProcessIssueReply(
241 mc, self.project, nonexistant_local_id, self.project_addr,
242 'awesome!')
243 self.assertEqual(1, len(email_tasks))
244 email_task = email_tasks[0]
245 self.assertEqual('user@example.com', email_task['to'])
246 self.assertEqual(
247 'Could not find issue %d in project %s' %
248 (nonexistant_local_id, self.project.project_name),
249 email_task['subject'])
250
251 def testProcessIssueReply_DeletedIssue(self):
252 self.issue.deleted = True
253 mc = monorailcontext.MonorailContext(
254 self.services, cnxn=self.cnxn, requester='user@example.com')
255 mc.LookupLoggedInUserPerms(self.project)
256
257 email_tasks = self.inbound.ProcessIssueReply(
258 mc, self.project, self.issue.local_id, self.project_addr,
259 'awesome!')
260 self.assertEqual(1, len(email_tasks))
261 email_task = email_tasks[0]
262 self.assertEqual('user@example.com', email_task['to'])
263 self.assertEqual(
264 'Could not find issue %d in project %s' %
265 (self.issue.local_id, self.project.project_name), email_task['subject'])
266
267 def VerifyUserHasNoPerm(self, perms):
268 mc = monorailcontext.MonorailContext(
269 self.services, cnxn=self.cnxn, requester='user@example.com')
270 mc.perms = perms
271
272 email_tasks = self.inbound.ProcessIssueReply(
273 mc, self.project, self.issue.local_id, self.project_addr,
274 'awesome!')
275 self.assertEqual(1, len(email_tasks))
276 email_task = email_tasks[0]
277 self.assertEqual('user@example.com', email_task['to'])
278 self.assertEqual(
279 'User does not have permission to add a comment', email_task['subject'])
280
281 def testProcessIssueReply_NoViewPerm(self):
282 self.VerifyUserHasNoPerm(permissions.EMPTY_PERMISSIONSET)
283
284 def testProcessIssueReply_CantViewRestrictedIssue(self):
285 self.issue.labels.append('Restrict-View-CoreTeam')
286 self.VerifyUserHasNoPerm(permissions.USER_PERMISSIONSET)
287
288 def testProcessIssueReply_NoAddIssuePerm(self):
289 self.VerifyUserHasNoPerm(permissions.READ_ONLY_PERMISSIONSET)
290
291 def testProcessIssueReply_NoEditIssuePerm(self):
292 self.services.user.TestAddUser('user@example.com', 111)
293 mc = monorailcontext.MonorailContext(
294 self.services, cnxn=self.cnxn, requester='user@example.com')
295 mc.perms = permissions.USER_PERMISSIONSET
296 mock_uia = commitlogcommands.UpdateIssueAction(self.issue.local_id)
297
298 self.mox.StubOutWithMock(commitlogcommands, 'UpdateIssueAction')
299 commitlogcommands.UpdateIssueAction(self.issue.local_id).AndReturn(mock_uia)
300
301 self.mox.StubOutWithMock(mock_uia, 'Parse')
302 mock_uia.Parse(
303 self.cnxn, self.project.project_name, 111, ['awesome!'], self.services,
304 strip_quoted_lines=True)
305 self.mox.StubOutWithMock(mock_uia, 'Run')
306 # mc.perms does not contain permission EDIT_ISSUE.
307 mock_uia.Run(mc, self.services)
308
309 self.mox.ReplayAll()
310 ret = self.inbound.ProcessIssueReply(
311 mc, self.project, self.issue.local_id, self.project_addr,
312 'awesome!')
313 self.mox.VerifyAll()
314 self.assertIsNone(ret)
315
316 def testProcessIssueReply_Success(self):
317 self.services.user.TestAddUser('user@example.com', 111)
318 mc = monorailcontext.MonorailContext(
319 self.services, cnxn=self.cnxn, requester='user@example.com')
320 mc.perms = permissions.COMMITTER_ACTIVE_PERMISSIONSET
321 mock_uia = commitlogcommands.UpdateIssueAction(self.issue.local_id)
322
323 self.mox.StubOutWithMock(commitlogcommands, 'UpdateIssueAction')
324 commitlogcommands.UpdateIssueAction(self.issue.local_id).AndReturn(mock_uia)
325
326 self.mox.StubOutWithMock(mock_uia, 'Parse')
327 mock_uia.Parse(
328 self.cnxn, self.project.project_name, 111, ['awesome!'], self.services,
329 strip_quoted_lines=True)
330 self.mox.StubOutWithMock(mock_uia, 'Run')
331 mock_uia.Run(mc, self.services)
332
333 self.mox.ReplayAll()
334 ret = self.inbound.ProcessIssueReply(
335 mc, self.project, self.issue.local_id, self.project_addr,
336 'awesome!')
337 self.mox.VerifyAll()
338 self.assertIsNone(ret)
339
340
341class BouncedEmailTest(unittest.TestCase):
342
343 def setUp(self):
344 self.cnxn = 'fake cnxn'
345 self.services = service_manager.Services(
346 user=fake.UserService())
347 self.user = self.services.user.TestAddUser('user@example.com', 111)
348
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +0200349 self.servlet = inboundemail.BouncedEmail(self.services)
Copybara854996b2021-09-07 19:36:02 +0000350 self.mox = mox.Mox()
351
352 def tearDown(self):
353 self.mox.UnsetStubs()
354 self.mox.ResetAll()
355
Copybara854996b2021-09-07 19:36:02 +0000356 def testReceive_Normal(self):
357 """Find the user that bounced and set email_bounce_timestamp."""
358 self.assertEqual(0, self.user.email_bounce_timestamp)
359
360 bounce_message = testing_helpers.Blank(original={'to': 'user@example.com'})
361 self.servlet.receive(bounce_message)
362
363 self.assertNotEqual(0, self.user.email_bounce_timestamp)
364
365 def testReceive_NoSuchUser(self):
366 """When not found, log it and ignore without creating a user record."""
Copybara854996b2021-09-07 19:36:02 +0000367 bounce_message = testing_helpers.Blank(
368 original={'to': 'nope@example.com'},
369 notification='notification')
370 self.servlet.receive(bounce_message)
371 self.assertEqual(1, len(self.services.user.users_by_id))