blob: ce97fbc751d910e9ecb2f64a7d5c1bd8459027b3 [file] [log] [blame]
# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Unittests for monorail.feature.alert2issue."""
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
import email.message
import unittest
from mock import patch
try:
from mox3 import mox
except ImportError:
import mox
from parameterized import parameterized
from features import alert2issue
from framework import authdata
from framework import emailfmt
from mrproto import tracker_pb2
from services import service_manager
from testing import fake
from testing import testing_helpers
from tracker import tracker_helpers
AlertEmailHeader = emailfmt.AlertEmailHeader
class TestData(object):
"""Contains constants or such objects that are intended to be read-only."""
cnxn = 'fake cnxn'
test_issue_local_id = 100
component_id = 123
trooper_queue = 'my-trooper-bug-queue'
project_name = 'proj'
project_addr = '%s+ALERT+%s@monorail.example.com' % (
project_name, trooper_queue)
project_id = 987
from_addr = 'user@monorail.example.com'
user_id = 111
msg_body = 'this is the body'
msg_subject = 'this is the subject'
msg = testing_helpers.MakeMessage(
testing_helpers.ALERT_EMAIL_HEADER_LINES, msg_body)
incident_id = msg.get(AlertEmailHeader.INCIDENT_ID)
incident_label = alert2issue._GetIncidentLabel(incident_id)
# All the tests in this class use the following alert properties, and
# the generator functions/logic should be tested in a separate class.
alert_props = {
'owner_id': 0,
'cc_ids': [],
'status': 'Available',
'incident_label': incident_label,
'priority': 'Pri-0',
'trooper_queue': trooper_queue,
'field_values': [],
'labels': [
'Restrict-View-Google', 'Pri-0', incident_label, trooper_queue
],
'component_ids': [component_id],
}
class ProcessEmailNotificationTests(unittest.TestCase, TestData):
"""Implements unit tests for alert2issue.ProcessEmailNotification."""
def setUp(self):
# services
self.services = service_manager.Services(
config=fake.ConfigService(),
issue=fake.IssueService(),
user=fake.UserService(),
usergroup=fake.UserGroupService(),
project=fake.ProjectService(),
features=fake.FeaturesService())
# project
self.project = self.services.project.TestAddProject(
self.project_name, project_id=self.project_id,
process_inbound_email=True, contrib_ids=[self.user_id])
# config
proj_config = fake.MakeTestConfig(self.project_id, [], ['Available'])
comp_def_1 = tracker_pb2.ComponentDef(
component_id=123, project_id=987, path='FOO', docstring='foo docstring')
proj_config.component_defs = [comp_def_1]
self.services.config.StoreConfig(self.cnxn, proj_config)
# sender
self.auth = authdata.AuthData(user_id=self.user_id, email=self.from_addr)
# issue
self.issue = tracker_pb2.Issue(
project_id=self.project_id,
local_id=self.test_issue_local_id,
summary=self.msg_subject,
reporter_id=self.user_id,
component_ids=[self.component_id],
status=self.alert_props['status'],
labels=self.alert_props['labels'],
)
self.services.issue.TestAddIssue(self.issue)
# Patch send_notifications functions.
self.notification_patchers = [
patch('features.send_notifications.%s' % func, spec=True)
for func in [
'PrepareAndSendIssueBlockingNotification',
'PrepareAndSendIssueChangeNotification',
]
]
self.blocking_notification = self.notification_patchers[0].start()
self.blocking_notification = self.notification_patchers[1].start()
self.mox = mox.Mox()
def tearDown(self):
self.notification_patchers[0].stop()
self.notification_patchers[1].stop()
self.mox.UnsetStubs()
self.mox.ResetAll()
def testGoogleAddrsAreAllowlistedSender(self):
self.assertTrue(alert2issue.IsAllowlisted('test@google.com'))
self.assertFalse(alert2issue.IsAllowlisted('test@notgoogle.com'))
def testSkipNotification_IfFromNonAllowlistedSender(self):
self.mox.StubOutWithMock(alert2issue, 'IsAllowlisted')
alert2issue.IsAllowlisted(self.from_addr).AndReturn(False)
# None of them should be called, if the sender has not been allowlisted.
self.mox.StubOutWithMock(self.services.issue, 'CreateIssueComment')
self.mox.StubOutWithMock(self.services.issue, 'CreateIssue')
self.mox.ReplayAll()
alert2issue.ProcessEmailNotification(
self.services, self.cnxn, self.project, self.project_addr,
self.from_addr, self.auth, self.msg_subject, self.msg_body,
self.incident_label, self.msg, self.trooper_queue)
self.mox.VerifyAll()
def testSkipNotification_TooLongComment(self):
self.mox.StubOutWithMock(alert2issue, 'IsAllowlisted')
alert2issue.IsAllowlisted(self.from_addr).AndReturn(True)
self.mox.StubOutWithMock(alert2issue, 'IsCommentSizeReasonable')
alert2issue.IsCommentSizeReasonable(
'Filed by %s on behalf of %s\n\n%s' %
(self.auth.email, self.from_addr, self.msg_body)).AndReturn(False)
# None of them should be called, if the comment is too long.
self.mox.StubOutWithMock(self.services.issue, 'CreateIssueComment')
self.mox.StubOutWithMock(self.services.issue, 'CreateIssue')
self.mox.ReplayAll()
alert2issue.ProcessEmailNotification(
self.services, self.cnxn, self.project, self.project_addr,
self.from_addr, self.auth, self.msg_subject, self.msg_body,
self.incident_label, self.msg, self.trooper_queue)
self.mox.VerifyAll()
def testProcessNotification_IfFromAllowlistedSender(self):
self.mox.StubOutWithMock(alert2issue, 'IsAllowlisted')
alert2issue.IsAllowlisted(self.from_addr).AndReturn(True)
self.mox.StubOutWithMock(tracker_helpers, 'LookupComponentIDs')
tracker_helpers.LookupComponentIDs(
['Infra'],
mox.IgnoreArg()).AndReturn([1])
self.mox.StubOutWithMock(self.services.issue, 'CreateIssueComment')
self.mox.StubOutWithMock(self.services.issue, 'CreateIssue')
self.mox.ReplayAll()
# Either of the methods should be called, if the sender is allowlisted.
with self.assertRaises(mox.UnexpectedMethodCallError):
alert2issue.ProcessEmailNotification(
self.services, self.cnxn, self.project, self.project_addr,
self.from_addr, self.auth, self.msg_subject, self.msg_body,
self.incident_label, self.msg, self.trooper_queue)
self.mox.VerifyAll()
def testIssueCreated_ForNewIncident(self):
"""Tests if a new issue is created for a new incident."""
self.mox.StubOutWithMock(alert2issue, 'IsAllowlisted')
alert2issue.IsAllowlisted(self.from_addr).AndReturn(True)
# FindAlertIssue() returns None for a new incident.
self.mox.StubOutWithMock(alert2issue, 'FindAlertIssue')
alert2issue.FindAlertIssue(
self.services, self.cnxn, self.project.project_id,
self.incident_label).AndReturn(None)
# Mock GetAlertProperties() to create the issue with the expected
# properties.
self.mox.StubOutWithMock(alert2issue, 'GetAlertProperties')
alert2issue.GetAlertProperties(
self.services, self.cnxn, self.project_id, self.incident_id,
self.trooper_queue, self.msg).AndReturn(self.alert_props)
self.mox.ReplayAll()
alert2issue.ProcessEmailNotification(
self.services, self.cnxn, self.project, self.project_addr,
self.from_addr, self.auth, self.msg_subject, self.msg_body,
self.incident_id, self.msg, self.trooper_queue)
# the local ID of the newly created issue should be +1 from the highest ID
# in the existing issues.
comments = self._verifyIssue(self.test_issue_local_id + 1, self.alert_props)
self.assertEqual(comments[0].content,
'Filed by %s on behalf of %s\n\n%s' % (
self.from_addr, self.from_addr, self.msg_body))
self.mox.VerifyAll()
def testProcessEmailNotification_ExistingIssue(self):
"""When an alert for an ongoing incident comes in, add a comment."""
self.mox.StubOutWithMock(alert2issue, 'IsAllowlisted')
alert2issue.IsAllowlisted(self.from_addr).AndReturn(True)
# FindAlertIssue() returns None for a new incident.
self.mox.StubOutWithMock(alert2issue, 'FindAlertIssue')
alert2issue.FindAlertIssue(
self.services, self.cnxn, self.project.project_id,
self.incident_label).AndReturn(self.issue)
# Mock GetAlertProperties() to create the issue with the expected
# properties.
self.mox.StubOutWithMock(alert2issue, 'GetAlertProperties')
alert2issue.GetAlertProperties(
self.services, self.cnxn, self.project_id, self.incident_id,
self.trooper_queue, self.msg).AndReturn(self.alert_props)
self.mox.ReplayAll()
# Before processing the notification, ensures that there is only 1 comment
# in the test issue.
comments = self._verifyIssue(self.test_issue_local_id, self.alert_props)
self.assertEqual(len(comments), 1)
# Process
alert2issue.ProcessEmailNotification(
self.services, self.cnxn, self.project, self.project_addr,
self.from_addr, self.auth, self.msg_subject, self.msg_body,
self.incident_id, self.msg, self.trooper_queue)
# Now, it should have a new comment added.
comments = self._verifyIssue(self.test_issue_local_id, self.alert_props)
self.assertEqual(len(comments), 2)
self.assertEqual(comments[1].content,
'Filed by %s on behalf of %s\n\n%s' % (
self.from_addr, self.from_addr, self.msg_body))
self.mox.VerifyAll()
def _verifyIssue(self, local_issue_id, alert_props):
actual_issue = self.services.issue.GetIssueByLocalID(
self.cnxn, self.project.project_id, local_issue_id)
actual_comments = self.services.issue.GetCommentsForIssue(
self.cnxn, actual_issue.issue_id)
self.assertEqual(actual_issue.summary, self.msg_subject)
self.assertEqual(actual_issue.status, alert_props['status'])
self.assertEqual(actual_issue.reporter_id, self.user_id)
self.assertEqual(actual_issue.component_ids, [self.component_id])
if alert_props['owner_id']:
self.assertEqual(actual_issue.owner_id, alert_props['owner_id'])
self.assertEqual(sorted(actual_issue.labels), sorted(alert_props['labels']))
return actual_comments
class GetAlertPropertiesTests(unittest.TestCase, TestData):
"""Implements unit tests for alert2issue.GetAlertProperties."""
def assertSubset(self, lhs, rhs):
if not (lhs <= rhs):
raise AssertionError('%s not a subset of %s' % (lhs, rhs))
def assertCaseInsensitiveEqual(self, lhs, rhs):
self.assertEqual(lhs if lhs is None else lhs.lower(),
rhs if lhs is None else rhs.lower())
def setUp(self):
# services
self.services = service_manager.Services(
config=fake.ConfigService(),
issue=fake.IssueService(),
user=fake.UserService(),
usergroup=fake.UserGroupService(),
project=fake.ProjectService())
# project
self.project = self.services.project.TestAddProject(
self.project_name, project_id=self.project_id,
process_inbound_email=True, contrib_ids=[self.user_id])
proj_config = fake.MakeTestConfig(
self.project_id,
[
# test labels for Pri field
'Pri-0', 'Pri-1', 'Pri-2', 'Pri-3',
# test labels for OS field
'OS-Android', 'OS-Windows',
# test labels for Type field
'Type-Bug', 'Type-Bug-Regression', 'Type-Bug-Security', 'Type-Task',
],
['Assigned', 'Available', 'Unconfirmed']
)
self.services.config.StoreConfig(self.cnxn, proj_config)
# create a test email message, which tests can alternate the header values
# to verify the behaviour of a given parser function.
self.test_msg = email.message.Message()
for key, value in self.msg.items():
self.test_msg[key] = value
self.mox = mox.Mox()
@parameterized.expand([
(None,),
('',),
(' ',),
])
def testDefaultComponent(self, header_value):
"""Checks if the default component is Infra."""
self.test_msg.replace_header(AlertEmailHeader.COMPONENT, header_value)
self.mox.StubOutWithMock(tracker_helpers, 'LookupComponentIDs')
tracker_helpers.LookupComponentIDs(
['Infra'],
mox.IgnoreArg()).AndReturn([self.component_id])
self.mox.ReplayAll()
props = alert2issue.GetAlertProperties(
self.services, self.cnxn, self.project_id, self.incident_id,
self.trooper_queue, self.test_msg)
self.assertEqual(props['component_ids'], [self.component_id])
self.mox.VerifyAll()
@parameterized.expand([
# an existing single component with componentID 1
({'Infra': 1}, [1]),
# 3 of existing components
({'Infra': 1, 'Foo': 2, 'Bar': 3}, [1, 2, 3]),
# a non-existing component
({'Infra': None}, []),
# 3 of non-existing components
({'Infra': None, 'Foo': None, 'Bar': None}, []),
# a mix of existing and non-existing components
({'Infra': 1, 'Foo': None, 'Bar': 2}, [1, 2]),
])
def testGetComponentIDs(self, components, expected_component_ids):
"""Tests _GetComponentIDs."""
self.test_msg.replace_header(
AlertEmailHeader.COMPONENT, ','.join(sorted(components.keys())))
self.mox.StubOutWithMock(tracker_helpers, 'LookupComponentIDs')
tracker_helpers.LookupComponentIDs(
sorted(components.keys()),
mox.IgnoreArg()).AndReturn(
[components[key] for key in sorted(components.keys())
if components[key]]
)
self.mox.ReplayAll()
props = alert2issue.GetAlertProperties(
self.services, self.cnxn, self.project_id, self.incident_id,
self.trooper_queue, self.test_msg)
self.assertEqual(sorted(props['component_ids']),
sorted(expected_component_ids))
self.mox.VerifyAll()
def testLabelsWithNecessaryValues(self):
"""Checks if the labels contain all the necessary values."""
props = alert2issue.GetAlertProperties(
self.services, self.cnxn, self.project_id, self.incident_id,
self.trooper_queue, self.test_msg)
# This test assumes that the test message contains non-empty values for
# all the headers.
self.assertTrue(props['incident_label'])
self.assertTrue(props['priority'])
self.assertTrue(props['issue_type'])
self.assertTrue(props['oses'])
# Here are a list of the labels that props['labels'] should contain
self.assertIn('Restrict-View-Google'.lower(), props['labels'])
self.assertIn(self.trooper_queue, props['labels'])
self.assertIn(props['incident_label'], props['labels'])
self.assertIn(props['priority'], props['labels'])
self.assertIn(props['issue_type'], props['labels'])
for os in props['oses']:
self.assertIn(os, props['labels'])
@parameterized.expand([
(None, 0),
('', 0),
(' ', 0),
])
def testDefaultOwnerID(self, header_value, expected_owner_id):
"""Checks if _GetOwnerID returns None in default."""
self.test_msg.replace_header(AlertEmailHeader.OWNER, header_value)
props = alert2issue.GetAlertProperties(
self.services, self.cnxn, self.project_id, self.incident_id,
self.trooper_queue, self.test_msg)
self.assertEqual(props['owner_id'], expected_owner_id)
@parameterized.expand(
[
# an existing user with userID 1.
('owner@example.org', 1),
# a non-existing user.
('owner@example.org', 0),
])
def testGetOwnerID(self, owner, expected_owner_id):
"""Tests _GetOwnerID returns the ID of the owner."""
self.test_msg.replace_header(AlertEmailHeader.CC, '')
self.test_msg.replace_header(AlertEmailHeader.OWNER, owner)
self.mox.StubOutWithMock(self.services.user, 'LookupExistingUserIDs')
self.services.user.LookupExistingUserIDs(self.cnxn, [owner]).AndReturn(
{owner: expected_owner_id})
self.mox.ReplayAll()
props = alert2issue.GetAlertProperties(
self.services, self.cnxn, self.project_id, self.incident_id,
self.trooper_queue, self.test_msg)
self.mox.VerifyAll()
self.assertEqual(props['owner_id'], expected_owner_id)
@parameterized.expand([
(None, []),
('', []),
(' ', []),
])
def testDefaultCCIDs(self, header_value, expected_cc_ids):
"""Checks if _GetCCIDs returns an empty list in default."""
self.test_msg.replace_header(AlertEmailHeader.CC, header_value)
props = alert2issue.GetAlertProperties(
self.services, self.cnxn, self.project_id, self.incident_id,
self.trooper_queue, self.test_msg)
self.assertEqual(props['cc_ids'], expected_cc_ids)
@parameterized.expand([
# with one existing user cc-ed.
({'user1@example.org': 1}, [1]),
# with two of existing users.
({'user1@example.org': 1, 'user2@example.org': 2}, [1, 2]),
# with one non-existing user.
({'user1@example.org': None}, []),
# with two of non-existing users.
({'user1@example.org': None, 'user2@example.org': None}, []),
# with a mix of existing and non-existing users.
({'user1@example.org': 1, 'user2@example.org': None}, [1]),
])
def testGetCCIDs(self, ccers, expected_cc_ids):
"""Tests _GetCCIDs returns the IDs of the email addresses to be cc-ed."""
self.test_msg.replace_header(
AlertEmailHeader.CC, ','.join(sorted(ccers.keys())))
self.test_msg.replace_header(AlertEmailHeader.OWNER, '')
self.mox.StubOutWithMock(self.services.user, 'LookupExistingUserIDs')
self.services.user.LookupExistingUserIDs(
self.cnxn, sorted(ccers.keys())).AndReturn(ccers)
self.mox.ReplayAll()
props = alert2issue.GetAlertProperties(
self.services, self.cnxn, self.project_id, self.incident_id,
self.trooper_queue, self.test_msg)
self.mox.VerifyAll()
self.assertEqual(sorted(props['cc_ids']), sorted(expected_cc_ids))
@parameterized.expand([
# None and '' should result in the default priority returned.
(None, 'Pri-2'),
('', 'Pri-2'),
(' ', 'Pri-2'),
# Tests for valid priority values
('0', 'Pri-0'),
('1', 'Pri-1'),
('2', 'Pri-2'),
('3', 'Pri-3'),
# Tests for invalid priority values
('test', 'Pri-2'),
('foo', 'Pri-2'),
('critical', 'Pri-2'),
('4', 'Pri-2'),
('3x', 'Pri-2'),
('00', 'Pri-2'),
('01', 'Pri-2'),
])
def testGetPriority(self, header_value, expected_priority):
"""Tests _GetPriority."""
self.test_msg.replace_header(AlertEmailHeader.PRIORITY, header_value)
props = alert2issue.GetAlertProperties(
self.services, self.cnxn, self.project_id, self.incident_id,
self.trooper_queue, self.test_msg)
self.assertCaseInsensitiveEqual(props['priority'], expected_priority)
@parameterized.expand([
(None, 'Available'),
('', 'Available'),
(' ', 'Available'),
])
def testDefaultStatus(self, header_value, expected_status):
"""Checks if _GetStatus return Available in default."""
self.test_msg.replace_header(AlertEmailHeader.STATUS, header_value)
props = alert2issue.GetAlertProperties(
self.services, self.cnxn, self.project_id, self.incident_id,
self.trooper_queue, self.test_msg)
self.assertCaseInsensitiveEqual(props['status'], expected_status)
@parameterized.expand([
('random_status', True, 'random_status'),
# If the status is not one of the open statuses, the default status
# should be returned instead.
('random_status', False, 'Available'),
])
def testGetStatusWithoutOwner(self, status, means_open, expected_status):
"""Tests GetStatus without an owner."""
self.test_msg.replace_header(AlertEmailHeader.STATUS, status)
self.mox.StubOutWithMock(tracker_helpers, 'MeansOpenInProject')
tracker_helpers.MeansOpenInProject(status, mox.IgnoreArg()).AndReturn(
means_open)
self.mox.ReplayAll()
props = alert2issue.GetAlertProperties(
self.services, self.cnxn, self.project_id, self.incident_id,
self.trooper_queue, self.test_msg)
self.assertCaseInsensitiveEqual(props['status'], expected_status)
self.mox.VerifyAll()
@parameterized.expand([
# If there is an owner, the status should always be Assigned.
(None, 'Assigned'),
('', 'Assigned'),
(' ', 'Assigned'),
('random_status', 'Assigned'),
('Available', 'Assigned'),
('Unconfirmed', 'Assigned'),
('Fixed', 'Assigned'),
])
def testGetStatusWithOwner(self, status, expected_status):
"""Tests GetStatus with an owner."""
owner = 'owner@example.org'
self.test_msg.replace_header(AlertEmailHeader.OWNER, owner)
self.test_msg.replace_header(AlertEmailHeader.CC, '')
self.test_msg.replace_header(AlertEmailHeader.STATUS, status)
self.mox.StubOutWithMock(self.services.user, 'LookupExistingUserIDs')
self.services.user.LookupExistingUserIDs(self.cnxn, [owner]).AndReturn(
{owner: 1})
self.mox.ReplayAll()
props = alert2issue.GetAlertProperties(
self.services, self.cnxn, self.project_id, self.incident_id,
self.trooper_queue, self.test_msg)
self.assertCaseInsensitiveEqual(props['status'], expected_status)
self.mox.VerifyAll()
@parameterized.expand(
[
# None and '' should result in None returned.
(None, None),
('', None),
(' ', None),
# allowlisted issue types
('Bug', 'Type-Bug'),
('Bug-Regression', 'Type-Bug-Regression'),
('Bug-Security', 'Type-Bug-Security'),
('Task', 'Type-Task'),
# non-allowlisted issue types
('foo', None),
('bar', None),
('Bug,Bug-Regression', None),
('Bug,', None),
(',Task', None),
])
def testGetIssueType(self, header_value, expected_issue_type):
"""Tests _GetIssueType."""
self.test_msg.replace_header(AlertEmailHeader.TYPE, header_value)
props = alert2issue.GetAlertProperties(
self.services, self.cnxn, self.project_id, self.incident_id,
self.trooper_queue, self.test_msg)
self.assertCaseInsensitiveEqual(props['issue_type'], expected_issue_type)
@parameterized.expand(
[
# None and '' should result in an empty list returned.
(None, []),
('', []),
(' ', []),
# a single, allowlisted os
('Android', ['OS-Android']),
# a single, non-allowlisted OS
('Bendroid', []),
# multiple, allowlisted oses
('Android,Windows', ['OS-Android', 'OS-Windows']),
# multiple, non-allowlisted oses
('Bendroid,Findows', []),
# a mix of allowlisted and non-allowlisted oses
('Android,Findows,Windows,Bendroid', ['OS-Android', 'OS-Windows']),
# a mix of allowlisted and non-allowlisted oses with trailing commas.
('Android,Findows,Windows,Bendroid,,', ['OS-Android', 'OS-Windows']),
# a mix of allowlisted and non-allowlisted oses with commas at the
# beginning.
(
',,Android,Findows,Windows,Bendroid,,',
['OS-Android', 'OS-Windows']),
])
def testGetOSes(self, header_value, expected_oses):
"""Tests _GetOSes."""
self.test_msg.replace_header(AlertEmailHeader.OS, header_value)
props = alert2issue.GetAlertProperties(
self.services, self.cnxn, self.project_id, self.incident_id,
self.trooper_queue, self.test_msg)
self.assertEqual(sorted(os if os is None else os.lower()
for os in props['oses']),
sorted(os if os is None else os.lower()
for os in expected_oses))
@parameterized.expand([
# None and '' should result in an empty list + RSVG returned.
(None, []),
('', []),
(' ', []),
('Label-1', ['label-1']),
('Label-1,Label-2', ['label-1', 'label-2',]),
('Label-1,Label-2,Label-3', ['label-1', 'label-2', 'label-3']),
# Duplicates should be removed.
('Label-1,Label-1', ['label-1']),
('Label-1,label-1', ['label-1']),
(',Label-1,label-1,', ['label-1']),
('Label-1,label-1,', ['label-1']),
(',Label-1,,label-1,,,', ['label-1']),
('Label-1,Label-2,Label-1', ['label-1', 'label-2']),
# Whitespaces should be removed from labels.
('La bel - 1 ', ['label-1']),
('La bel - 1 , Label- 1', ['label-1']),
('La bel- 1 , Label - 2', ['label-1', 'label-2']),
# RSVG should be set always.
('Label-1,Label-1,Restrict-View-Google', ['label-1']),
])
def testGetLabels(self, header_value, expected_labels):
"""Tests _GetLabels."""
self.test_msg.replace_header(AlertEmailHeader.LABEL, header_value)
props = alert2issue.GetAlertProperties(
self.services, self.cnxn, self.project_id, self.incident_id,
self.trooper_queue, self.test_msg)
# Check if there are any duplicates
labels = set(props['labels'])
self.assertEqual(sorted(props['labels']), sorted(list(labels)))
# Check the labels that shouldb always be included
self.assertIn('Restrict-View-Google'.lower(), labels)
self.assertIn(props['trooper_queue'], labels)
self.assertIn(props['incident_label'], labels)
self.assertIn(props['priority'], labels)
self.assertIn(props['issue_type'], labels)
self.assertSubset(set(props['oses']), labels)
# All the custom labels should be present.
self.assertSubset(set(expected_labels), labels)