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