blob: ce97fbc751d910e9ecb2f64a7d5c1bd8459027b3 [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# Copyright 2019 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.alert2issue."""
6from __future__ import print_function
7from __future__ import division
8from __future__ import absolute_import
9
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010010import email.message
Copybara854996b2021-09-07 19:36:02 +000011import unittest
12from mock import patch
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 +000017from parameterized import parameterized
18
19from features import alert2issue
20from framework import authdata
21from framework import emailfmt
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010022from mrproto import tracker_pb2
Copybara854996b2021-09-07 19:36:02 +000023from services import service_manager
24from testing import fake
25from testing import testing_helpers
26from tracker import tracker_helpers
27
28AlertEmailHeader = emailfmt.AlertEmailHeader
29
30
31class TestData(object):
32 """Contains constants or such objects that are intended to be read-only."""
33 cnxn = 'fake cnxn'
34 test_issue_local_id = 100
35 component_id = 123
36 trooper_queue = 'my-trooper-bug-queue'
37
38 project_name = 'proj'
39 project_addr = '%s+ALERT+%s@monorail.example.com' % (
40 project_name, trooper_queue)
41 project_id = 987
42
43 from_addr = 'user@monorail.example.com'
44 user_id = 111
45
46 msg_body = 'this is the body'
47 msg_subject = 'this is the subject'
48 msg = testing_helpers.MakeMessage(
49 testing_helpers.ALERT_EMAIL_HEADER_LINES, msg_body)
50
51 incident_id = msg.get(AlertEmailHeader.INCIDENT_ID)
52 incident_label = alert2issue._GetIncidentLabel(incident_id)
53
54 # All the tests in this class use the following alert properties, and
55 # the generator functions/logic should be tested in a separate class.
56 alert_props = {
57 'owner_id': 0,
58 'cc_ids': [],
59 'status': 'Available',
60 'incident_label': incident_label,
61 'priority': 'Pri-0',
62 'trooper_queue': trooper_queue,
63 'field_values': [],
64 'labels': [
65 'Restrict-View-Google', 'Pri-0', incident_label, trooper_queue
66 ],
67 'component_ids': [component_id],
68 }
69
70
71class ProcessEmailNotificationTests(unittest.TestCase, TestData):
72 """Implements unit tests for alert2issue.ProcessEmailNotification."""
73 def setUp(self):
74 # services
75 self.services = service_manager.Services(
76 config=fake.ConfigService(),
77 issue=fake.IssueService(),
78 user=fake.UserService(),
79 usergroup=fake.UserGroupService(),
80 project=fake.ProjectService(),
81 features=fake.FeaturesService())
82
83 # project
84 self.project = self.services.project.TestAddProject(
85 self.project_name, project_id=self.project_id,
86 process_inbound_email=True, contrib_ids=[self.user_id])
87
88 # config
89 proj_config = fake.MakeTestConfig(self.project_id, [], ['Available'])
90 comp_def_1 = tracker_pb2.ComponentDef(
91 component_id=123, project_id=987, path='FOO', docstring='foo docstring')
92 proj_config.component_defs = [comp_def_1]
93 self.services.config.StoreConfig(self.cnxn, proj_config)
94
95 # sender
96 self.auth = authdata.AuthData(user_id=self.user_id, email=self.from_addr)
97
98 # issue
99 self.issue = tracker_pb2.Issue(
100 project_id=self.project_id,
101 local_id=self.test_issue_local_id,
102 summary=self.msg_subject,
103 reporter_id=self.user_id,
104 component_ids=[self.component_id],
105 status=self.alert_props['status'],
106 labels=self.alert_props['labels'],
107 )
108 self.services.issue.TestAddIssue(self.issue)
109
110 # Patch send_notifications functions.
111 self.notification_patchers = [
112 patch('features.send_notifications.%s' % func, spec=True)
113 for func in [
114 'PrepareAndSendIssueBlockingNotification',
115 'PrepareAndSendIssueChangeNotification',
116 ]
117 ]
118 self.blocking_notification = self.notification_patchers[0].start()
119 self.blocking_notification = self.notification_patchers[1].start()
120
121 self.mox = mox.Mox()
122
123 def tearDown(self):
124 self.notification_patchers[0].stop()
125 self.notification_patchers[1].stop()
126
127 self.mox.UnsetStubs()
128 self.mox.ResetAll()
129
130 def testGoogleAddrsAreAllowlistedSender(self):
131 self.assertTrue(alert2issue.IsAllowlisted('test@google.com'))
132 self.assertFalse(alert2issue.IsAllowlisted('test@notgoogle.com'))
133
134 def testSkipNotification_IfFromNonAllowlistedSender(self):
135 self.mox.StubOutWithMock(alert2issue, 'IsAllowlisted')
136 alert2issue.IsAllowlisted(self.from_addr).AndReturn(False)
137
138 # None of them should be called, if the sender has not been allowlisted.
139 self.mox.StubOutWithMock(self.services.issue, 'CreateIssueComment')
140 self.mox.StubOutWithMock(self.services.issue, 'CreateIssue')
141 self.mox.ReplayAll()
142
143 alert2issue.ProcessEmailNotification(
144 self.services, self.cnxn, self.project, self.project_addr,
145 self.from_addr, self.auth, self.msg_subject, self.msg_body,
146 self.incident_label, self.msg, self.trooper_queue)
147 self.mox.VerifyAll()
148
149 def testSkipNotification_TooLongComment(self):
150 self.mox.StubOutWithMock(alert2issue, 'IsAllowlisted')
151 alert2issue.IsAllowlisted(self.from_addr).AndReturn(True)
152 self.mox.StubOutWithMock(alert2issue, 'IsCommentSizeReasonable')
153 alert2issue.IsCommentSizeReasonable(
154 'Filed by %s on behalf of %s\n\n%s' %
155 (self.auth.email, self.from_addr, self.msg_body)).AndReturn(False)
156
157 # None of them should be called, if the comment is too long.
158 self.mox.StubOutWithMock(self.services.issue, 'CreateIssueComment')
159 self.mox.StubOutWithMock(self.services.issue, 'CreateIssue')
160 self.mox.ReplayAll()
161
162 alert2issue.ProcessEmailNotification(
163 self.services, self.cnxn, self.project, self.project_addr,
164 self.from_addr, self.auth, self.msg_subject, self.msg_body,
165 self.incident_label, self.msg, self.trooper_queue)
166 self.mox.VerifyAll()
167
168 def testProcessNotification_IfFromAllowlistedSender(self):
169 self.mox.StubOutWithMock(alert2issue, 'IsAllowlisted')
170 alert2issue.IsAllowlisted(self.from_addr).AndReturn(True)
171
172 self.mox.StubOutWithMock(tracker_helpers, 'LookupComponentIDs')
173 tracker_helpers.LookupComponentIDs(
174 ['Infra'],
175 mox.IgnoreArg()).AndReturn([1])
176 self.mox.StubOutWithMock(self.services.issue, 'CreateIssueComment')
177 self.mox.StubOutWithMock(self.services.issue, 'CreateIssue')
178 self.mox.ReplayAll()
179
180 # Either of the methods should be called, if the sender is allowlisted.
181 with self.assertRaises(mox.UnexpectedMethodCallError):
182 alert2issue.ProcessEmailNotification(
183 self.services, self.cnxn, self.project, self.project_addr,
184 self.from_addr, self.auth, self.msg_subject, self.msg_body,
185 self.incident_label, self.msg, self.trooper_queue)
186
187 self.mox.VerifyAll()
188
189 def testIssueCreated_ForNewIncident(self):
190 """Tests if a new issue is created for a new incident."""
191 self.mox.StubOutWithMock(alert2issue, 'IsAllowlisted')
192 alert2issue.IsAllowlisted(self.from_addr).AndReturn(True)
193
194 # FindAlertIssue() returns None for a new incident.
195 self.mox.StubOutWithMock(alert2issue, 'FindAlertIssue')
196 alert2issue.FindAlertIssue(
197 self.services, self.cnxn, self.project.project_id,
198 self.incident_label).AndReturn(None)
199
200 # Mock GetAlertProperties() to create the issue with the expected
201 # properties.
202 self.mox.StubOutWithMock(alert2issue, 'GetAlertProperties')
203 alert2issue.GetAlertProperties(
204 self.services, self.cnxn, self.project_id, self.incident_id,
205 self.trooper_queue, self.msg).AndReturn(self.alert_props)
206
207 self.mox.ReplayAll()
208 alert2issue.ProcessEmailNotification(
209 self.services, self.cnxn, self.project, self.project_addr,
210 self.from_addr, self.auth, self.msg_subject, self.msg_body,
211 self.incident_id, self.msg, self.trooper_queue)
212
213 # the local ID of the newly created issue should be +1 from the highest ID
214 # in the existing issues.
215 comments = self._verifyIssue(self.test_issue_local_id + 1, self.alert_props)
216 self.assertEqual(comments[0].content,
217 'Filed by %s on behalf of %s\n\n%s' % (
218 self.from_addr, self.from_addr, self.msg_body))
219
220 self.mox.VerifyAll()
221
222 def testProcessEmailNotification_ExistingIssue(self):
223 """When an alert for an ongoing incident comes in, add a comment."""
224 self.mox.StubOutWithMock(alert2issue, 'IsAllowlisted')
225 alert2issue.IsAllowlisted(self.from_addr).AndReturn(True)
226
227 # FindAlertIssue() returns None for a new incident.
228 self.mox.StubOutWithMock(alert2issue, 'FindAlertIssue')
229 alert2issue.FindAlertIssue(
230 self.services, self.cnxn, self.project.project_id,
231 self.incident_label).AndReturn(self.issue)
232
233 # Mock GetAlertProperties() to create the issue with the expected
234 # properties.
235 self.mox.StubOutWithMock(alert2issue, 'GetAlertProperties')
236 alert2issue.GetAlertProperties(
237 self.services, self.cnxn, self.project_id, self.incident_id,
238 self.trooper_queue, self.msg).AndReturn(self.alert_props)
239
240 self.mox.ReplayAll()
241
242 # Before processing the notification, ensures that there is only 1 comment
243 # in the test issue.
244 comments = self._verifyIssue(self.test_issue_local_id, self.alert_props)
245 self.assertEqual(len(comments), 1)
246
247 # Process
248 alert2issue.ProcessEmailNotification(
249 self.services, self.cnxn, self.project, self.project_addr,
250 self.from_addr, self.auth, self.msg_subject, self.msg_body,
251 self.incident_id, self.msg, self.trooper_queue)
252
253 # Now, it should have a new comment added.
254 comments = self._verifyIssue(self.test_issue_local_id, self.alert_props)
255 self.assertEqual(len(comments), 2)
256 self.assertEqual(comments[1].content,
257 'Filed by %s on behalf of %s\n\n%s' % (
258 self.from_addr, self.from_addr, self.msg_body))
259
260 self.mox.VerifyAll()
261
262 def _verifyIssue(self, local_issue_id, alert_props):
263 actual_issue = self.services.issue.GetIssueByLocalID(
264 self.cnxn, self.project.project_id, local_issue_id)
265 actual_comments = self.services.issue.GetCommentsForIssue(
266 self.cnxn, actual_issue.issue_id)
267
268 self.assertEqual(actual_issue.summary, self.msg_subject)
269 self.assertEqual(actual_issue.status, alert_props['status'])
270 self.assertEqual(actual_issue.reporter_id, self.user_id)
271 self.assertEqual(actual_issue.component_ids, [self.component_id])
272 if alert_props['owner_id']:
273 self.assertEqual(actual_issue.owner_id, alert_props['owner_id'])
274 self.assertEqual(sorted(actual_issue.labels), sorted(alert_props['labels']))
275 return actual_comments
276
277
278class GetAlertPropertiesTests(unittest.TestCase, TestData):
279 """Implements unit tests for alert2issue.GetAlertProperties."""
280 def assertSubset(self, lhs, rhs):
281 if not (lhs <= rhs):
282 raise AssertionError('%s not a subset of %s' % (lhs, rhs))
283
284 def assertCaseInsensitiveEqual(self, lhs, rhs):
285 self.assertEqual(lhs if lhs is None else lhs.lower(),
286 rhs if lhs is None else rhs.lower())
287
288 def setUp(self):
289 # services
290 self.services = service_manager.Services(
291 config=fake.ConfigService(),
292 issue=fake.IssueService(),
293 user=fake.UserService(),
294 usergroup=fake.UserGroupService(),
295 project=fake.ProjectService())
296
297 # project
298 self.project = self.services.project.TestAddProject(
299 self.project_name, project_id=self.project_id,
300 process_inbound_email=True, contrib_ids=[self.user_id])
301
302 proj_config = fake.MakeTestConfig(
303 self.project_id,
304 [
305 # test labels for Pri field
306 'Pri-0', 'Pri-1', 'Pri-2', 'Pri-3',
307 # test labels for OS field
308 'OS-Android', 'OS-Windows',
309 # test labels for Type field
310 'Type-Bug', 'Type-Bug-Regression', 'Type-Bug-Security', 'Type-Task',
311 ],
312 ['Assigned', 'Available', 'Unconfirmed']
313 )
314 self.services.config.StoreConfig(self.cnxn, proj_config)
315
316 # create a test email message, which tests can alternate the header values
317 # to verify the behaviour of a given parser function.
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100318 self.test_msg = email.message.Message()
Copybara854996b2021-09-07 19:36:02 +0000319 for key, value in self.msg.items():
320 self.test_msg[key] = value
321
322 self.mox = mox.Mox()
323
324 @parameterized.expand([
325 (None,),
326 ('',),
327 (' ',),
328 ])
329 def testDefaultComponent(self, header_value):
330 """Checks if the default component is Infra."""
331 self.test_msg.replace_header(AlertEmailHeader.COMPONENT, header_value)
332 self.mox.StubOutWithMock(tracker_helpers, 'LookupComponentIDs')
333 tracker_helpers.LookupComponentIDs(
334 ['Infra'],
335 mox.IgnoreArg()).AndReturn([self.component_id])
336
337 self.mox.ReplayAll()
338 props = alert2issue.GetAlertProperties(
339 self.services, self.cnxn, self.project_id, self.incident_id,
340 self.trooper_queue, self.test_msg)
341 self.assertEqual(props['component_ids'], [self.component_id])
342 self.mox.VerifyAll()
343
344 @parameterized.expand([
345 # an existing single component with componentID 1
346 ({'Infra': 1}, [1]),
347 # 3 of existing components
348 ({'Infra': 1, 'Foo': 2, 'Bar': 3}, [1, 2, 3]),
349 # a non-existing component
350 ({'Infra': None}, []),
351 # 3 of non-existing components
352 ({'Infra': None, 'Foo': None, 'Bar': None}, []),
353 # a mix of existing and non-existing components
354 ({'Infra': 1, 'Foo': None, 'Bar': 2}, [1, 2]),
355 ])
356 def testGetComponentIDs(self, components, expected_component_ids):
357 """Tests _GetComponentIDs."""
358 self.test_msg.replace_header(
359 AlertEmailHeader.COMPONENT, ','.join(sorted(components.keys())))
360
361 self.mox.StubOutWithMock(tracker_helpers, 'LookupComponentIDs')
362 tracker_helpers.LookupComponentIDs(
363 sorted(components.keys()),
364 mox.IgnoreArg()).AndReturn(
365 [components[key] for key in sorted(components.keys())
366 if components[key]]
367 )
368
369 self.mox.ReplayAll()
370 props = alert2issue.GetAlertProperties(
371 self.services, self.cnxn, self.project_id, self.incident_id,
372 self.trooper_queue, self.test_msg)
373 self.assertEqual(sorted(props['component_ids']),
374 sorted(expected_component_ids))
375 self.mox.VerifyAll()
376
377
378 def testLabelsWithNecessaryValues(self):
379 """Checks if the labels contain all the necessary values."""
380 props = alert2issue.GetAlertProperties(
381 self.services, self.cnxn, self.project_id, self.incident_id,
382 self.trooper_queue, self.test_msg)
383
384 # This test assumes that the test message contains non-empty values for
385 # all the headers.
386 self.assertTrue(props['incident_label'])
387 self.assertTrue(props['priority'])
388 self.assertTrue(props['issue_type'])
389 self.assertTrue(props['oses'])
390
391 # Here are a list of the labels that props['labels'] should contain
392 self.assertIn('Restrict-View-Google'.lower(), props['labels'])
393 self.assertIn(self.trooper_queue, props['labels'])
394 self.assertIn(props['incident_label'], props['labels'])
395 self.assertIn(props['priority'], props['labels'])
396 self.assertIn(props['issue_type'], props['labels'])
397 for os in props['oses']:
398 self.assertIn(os, props['labels'])
399
400 @parameterized.expand([
401 (None, 0),
402 ('', 0),
403 (' ', 0),
404 ])
405 def testDefaultOwnerID(self, header_value, expected_owner_id):
406 """Checks if _GetOwnerID returns None in default."""
407 self.test_msg.replace_header(AlertEmailHeader.OWNER, header_value)
408 props = alert2issue.GetAlertProperties(
409 self.services, self.cnxn, self.project_id, self.incident_id,
410 self.trooper_queue, self.test_msg)
411 self.assertEqual(props['owner_id'], expected_owner_id)
412
413 @parameterized.expand(
414 [
415 # an existing user with userID 1.
416 ('owner@example.org', 1),
417 # a non-existing user.
418 ('owner@example.org', 0),
419 ])
420 def testGetOwnerID(self, owner, expected_owner_id):
421 """Tests _GetOwnerID returns the ID of the owner."""
422 self.test_msg.replace_header(AlertEmailHeader.CC, '')
423 self.test_msg.replace_header(AlertEmailHeader.OWNER, owner)
424
425 self.mox.StubOutWithMock(self.services.user, 'LookupExistingUserIDs')
426 self.services.user.LookupExistingUserIDs(self.cnxn, [owner]).AndReturn(
427 {owner: expected_owner_id})
428
429 self.mox.ReplayAll()
430 props = alert2issue.GetAlertProperties(
431 self.services, self.cnxn, self.project_id, self.incident_id,
432 self.trooper_queue, self.test_msg)
433 self.mox.VerifyAll()
434 self.assertEqual(props['owner_id'], expected_owner_id)
435
436 @parameterized.expand([
437 (None, []),
438 ('', []),
439 (' ', []),
440 ])
441 def testDefaultCCIDs(self, header_value, expected_cc_ids):
442 """Checks if _GetCCIDs returns an empty list in default."""
443 self.test_msg.replace_header(AlertEmailHeader.CC, header_value)
444 props = alert2issue.GetAlertProperties(
445 self.services, self.cnxn, self.project_id, self.incident_id,
446 self.trooper_queue, self.test_msg)
447 self.assertEqual(props['cc_ids'], expected_cc_ids)
448
449 @parameterized.expand([
450 # with one existing user cc-ed.
451 ({'user1@example.org': 1}, [1]),
452 # with two of existing users.
453 ({'user1@example.org': 1, 'user2@example.org': 2}, [1, 2]),
454 # with one non-existing user.
455 ({'user1@example.org': None}, []),
456 # with two of non-existing users.
457 ({'user1@example.org': None, 'user2@example.org': None}, []),
458 # with a mix of existing and non-existing users.
459 ({'user1@example.org': 1, 'user2@example.org': None}, [1]),
460 ])
461 def testGetCCIDs(self, ccers, expected_cc_ids):
462 """Tests _GetCCIDs returns the IDs of the email addresses to be cc-ed."""
463 self.test_msg.replace_header(
464 AlertEmailHeader.CC, ','.join(sorted(ccers.keys())))
465 self.test_msg.replace_header(AlertEmailHeader.OWNER, '')
466
467 self.mox.StubOutWithMock(self.services.user, 'LookupExistingUserIDs')
468 self.services.user.LookupExistingUserIDs(
469 self.cnxn, sorted(ccers.keys())).AndReturn(ccers)
470
471 self.mox.ReplayAll()
472 props = alert2issue.GetAlertProperties(
473 self.services, self.cnxn, self.project_id, self.incident_id,
474 self.trooper_queue, self.test_msg)
475 self.mox.VerifyAll()
476 self.assertEqual(sorted(props['cc_ids']), sorted(expected_cc_ids))
477
478 @parameterized.expand([
479 # None and '' should result in the default priority returned.
480 (None, 'Pri-2'),
481 ('', 'Pri-2'),
482 (' ', 'Pri-2'),
483
484 # Tests for valid priority values
485 ('0', 'Pri-0'),
486 ('1', 'Pri-1'),
487 ('2', 'Pri-2'),
488 ('3', 'Pri-3'),
489
490 # Tests for invalid priority values
491 ('test', 'Pri-2'),
492 ('foo', 'Pri-2'),
493 ('critical', 'Pri-2'),
494 ('4', 'Pri-2'),
495 ('3x', 'Pri-2'),
496 ('00', 'Pri-2'),
497 ('01', 'Pri-2'),
498 ])
499 def testGetPriority(self, header_value, expected_priority):
500 """Tests _GetPriority."""
501 self.test_msg.replace_header(AlertEmailHeader.PRIORITY, header_value)
502 props = alert2issue.GetAlertProperties(
503 self.services, self.cnxn, self.project_id, self.incident_id,
504 self.trooper_queue, self.test_msg)
505 self.assertCaseInsensitiveEqual(props['priority'], expected_priority)
506
507 @parameterized.expand([
508 (None, 'Available'),
509 ('', 'Available'),
510 (' ', 'Available'),
511 ])
512 def testDefaultStatus(self, header_value, expected_status):
513 """Checks if _GetStatus return Available in default."""
514 self.test_msg.replace_header(AlertEmailHeader.STATUS, header_value)
515 props = alert2issue.GetAlertProperties(
516 self.services, self.cnxn, self.project_id, self.incident_id,
517 self.trooper_queue, self.test_msg)
518 self.assertCaseInsensitiveEqual(props['status'], expected_status)
519
520 @parameterized.expand([
521 ('random_status', True, 'random_status'),
522 # If the status is not one of the open statuses, the default status
523 # should be returned instead.
524 ('random_status', False, 'Available'),
525 ])
526 def testGetStatusWithoutOwner(self, status, means_open, expected_status):
527 """Tests GetStatus without an owner."""
528 self.test_msg.replace_header(AlertEmailHeader.STATUS, status)
529 self.mox.StubOutWithMock(tracker_helpers, 'MeansOpenInProject')
530 tracker_helpers.MeansOpenInProject(status, mox.IgnoreArg()).AndReturn(
531 means_open)
532
533 self.mox.ReplayAll()
534 props = alert2issue.GetAlertProperties(
535 self.services, self.cnxn, self.project_id, self.incident_id,
536 self.trooper_queue, self.test_msg)
537 self.assertCaseInsensitiveEqual(props['status'], expected_status)
538 self.mox.VerifyAll()
539
540 @parameterized.expand([
541 # If there is an owner, the status should always be Assigned.
542 (None, 'Assigned'),
543 ('', 'Assigned'),
544 (' ', 'Assigned'),
545
546 ('random_status', 'Assigned'),
547 ('Available', 'Assigned'),
548 ('Unconfirmed', 'Assigned'),
549 ('Fixed', 'Assigned'),
550 ])
551 def testGetStatusWithOwner(self, status, expected_status):
552 """Tests GetStatus with an owner."""
553 owner = 'owner@example.org'
554 self.test_msg.replace_header(AlertEmailHeader.OWNER, owner)
555 self.test_msg.replace_header(AlertEmailHeader.CC, '')
556 self.test_msg.replace_header(AlertEmailHeader.STATUS, status)
557
558 self.mox.StubOutWithMock(self.services.user, 'LookupExistingUserIDs')
559 self.services.user.LookupExistingUserIDs(self.cnxn, [owner]).AndReturn(
560 {owner: 1})
561
562 self.mox.ReplayAll()
563 props = alert2issue.GetAlertProperties(
564 self.services, self.cnxn, self.project_id, self.incident_id,
565 self.trooper_queue, self.test_msg)
566 self.assertCaseInsensitiveEqual(props['status'], expected_status)
567 self.mox.VerifyAll()
568
569 @parameterized.expand(
570 [
571 # None and '' should result in None returned.
572 (None, None),
573 ('', None),
574 (' ', None),
575
576 # allowlisted issue types
577 ('Bug', 'Type-Bug'),
578 ('Bug-Regression', 'Type-Bug-Regression'),
579 ('Bug-Security', 'Type-Bug-Security'),
580 ('Task', 'Type-Task'),
581
582 # non-allowlisted issue types
583 ('foo', None),
584 ('bar', None),
585 ('Bug,Bug-Regression', None),
586 ('Bug,', None),
587 (',Task', None),
588 ])
589 def testGetIssueType(self, header_value, expected_issue_type):
590 """Tests _GetIssueType."""
591 self.test_msg.replace_header(AlertEmailHeader.TYPE, header_value)
592 props = alert2issue.GetAlertProperties(
593 self.services, self.cnxn, self.project_id, self.incident_id,
594 self.trooper_queue, self.test_msg)
595 self.assertCaseInsensitiveEqual(props['issue_type'], expected_issue_type)
596
597 @parameterized.expand(
598 [
599 # None and '' should result in an empty list returned.
600 (None, []),
601 ('', []),
602 (' ', []),
603
604 # a single, allowlisted os
605 ('Android', ['OS-Android']),
606 # a single, non-allowlisted OS
607 ('Bendroid', []),
608 # multiple, allowlisted oses
609 ('Android,Windows', ['OS-Android', 'OS-Windows']),
610 # multiple, non-allowlisted oses
611 ('Bendroid,Findows', []),
612 # a mix of allowlisted and non-allowlisted oses
613 ('Android,Findows,Windows,Bendroid', ['OS-Android', 'OS-Windows']),
614 # a mix of allowlisted and non-allowlisted oses with trailing commas.
615 ('Android,Findows,Windows,Bendroid,,', ['OS-Android', 'OS-Windows']),
616 # a mix of allowlisted and non-allowlisted oses with commas at the
617 # beginning.
618 (
619 ',,Android,Findows,Windows,Bendroid,,',
620 ['OS-Android', 'OS-Windows']),
621 ])
622 def testGetOSes(self, header_value, expected_oses):
623 """Tests _GetOSes."""
624 self.test_msg.replace_header(AlertEmailHeader.OS, header_value)
625 props = alert2issue.GetAlertProperties(
626 self.services, self.cnxn, self.project_id, self.incident_id,
627 self.trooper_queue, self.test_msg)
628 self.assertEqual(sorted(os if os is None else os.lower()
629 for os in props['oses']),
630 sorted(os if os is None else os.lower()
631 for os in expected_oses))
632
633 @parameterized.expand([
634 # None and '' should result in an empty list + RSVG returned.
635 (None, []),
636 ('', []),
637 (' ', []),
638
639 ('Label-1', ['label-1']),
640 ('Label-1,Label-2', ['label-1', 'label-2',]),
641 ('Label-1,Label-2,Label-3', ['label-1', 'label-2', 'label-3']),
642
643 # Duplicates should be removed.
644 ('Label-1,Label-1', ['label-1']),
645 ('Label-1,label-1', ['label-1']),
646 (',Label-1,label-1,', ['label-1']),
647 ('Label-1,label-1,', ['label-1']),
648 (',Label-1,,label-1,,,', ['label-1']),
649 ('Label-1,Label-2,Label-1', ['label-1', 'label-2']),
650
651 # Whitespaces should be removed from labels.
652 ('La bel - 1 ', ['label-1']),
653 ('La bel - 1 , Label- 1', ['label-1']),
654 ('La bel- 1 , Label - 2', ['label-1', 'label-2']),
655
656 # RSVG should be set always.
657 ('Label-1,Label-1,Restrict-View-Google', ['label-1']),
658 ])
659 def testGetLabels(self, header_value, expected_labels):
660 """Tests _GetLabels."""
661 self.test_msg.replace_header(AlertEmailHeader.LABEL, header_value)
662 props = alert2issue.GetAlertProperties(
663 self.services, self.cnxn, self.project_id, self.incident_id,
664 self.trooper_queue, self.test_msg)
665
666 # Check if there are any duplicates
667 labels = set(props['labels'])
668 self.assertEqual(sorted(props['labels']), sorted(list(labels)))
669
670 # Check the labels that shouldb always be included
671 self.assertIn('Restrict-View-Google'.lower(), labels)
672 self.assertIn(props['trooper_queue'], labels)
673 self.assertIn(props['incident_label'], labels)
674 self.assertIn(props['priority'], labels)
675 self.assertIn(props['issue_type'], labels)
676 self.assertSubset(set(props['oses']), labels)
677
678 # All the custom labels should be present.
679 self.assertSubset(set(expected_labels), labels)