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