From a6e0ce590c480b6d1fed9db2967965d720b9ffa2 Mon Sep 17 00:00:00 2001 From: Yuming Zhu Date: May 08 2018 08:32:04 +0000 Subject: [PATCH 1/7] Task Notification Plugin --- diff --git a/plugins/builder/task_notification.py b/plugins/builder/task_notification.py new file mode 100644 index 0000000..687e630 --- /dev/null +++ b/plugins/builder/task_notification.py @@ -0,0 +1,116 @@ +import smtplib +import sys + +import koji +import koji.tasks as tasks + +__all__ = ('TaskNotificationTask',) + +class TaskNotificationTask(tasks.BaseTaskHandler): + Methods = ['taskNotification'] + + _taskWeight = 0.1 + + # XXX externalize these templates somewhere + subject_templ = """Task: #%(id)d Status: %(state_str)s Owner: %(owner_name)s""" + message_templ = \ +"""From: %(from_addr)s\r +Subject: %(subject)s\r +To: %(to_addrs)s\r +X-Koji-Task: %(id)s\r +X-Koji-Owner: %(owner_name)s\r +X-Koji-Status: %(state_str)s\r +X-Koji-Parent: %(parent)s\r +X-Koji-Method: %(method)s\r +\r +Task: %(id)s\r +Status: %(state_str)s\r +Owner: %(owner_name)s\r +Host: %(host_name)s\r +Method: %(method)s\r +Parent: %(parent)s\r +Arch: %(arch)s\r +Label: %(label)s\r +Created: %(create_time)s\r +Started: %(start_time)s\r +Finished: %(completion_time)s\r +%(failure)s\r +Task Info: %(weburl)s/taskinfo?taskID=%(id)i\r +""" + + def _get_notification_info(self, task_id, recipient, weburl): + taskinfo = self.session.getTaskInfo(task_id, request=True) + + if not taskinfo: + # invalid task_id + raise koji.GenericError('Cannot find task#%i' % task_id) + + if taskinfo['host_id']: + hostinfo = self.session.getHost(taskinfo['host_id']) + else: + hostinfo = None + + if taskinfo['owner']: + userinfo = self.session.getUser(taskinfo['owner'], strict=True) + taskinfo['owner_name'] = userinfo['name'] + else: + taskinfo['owner_name'] = None + + result = None + try: + result = self.session.getTaskResult(task_id) + except: + excClass, result = sys.exc_info()[:2] + if hasattr(result, 'faultString'): + result = result.faultString + else: + result = '%s: %s' % (excClass.__name__, result) + result = result.strip() + # clear the exception, since we're just using + # it for display purposes + sys.exc_clear() + if not result: + result = 'Unknown' + taskinfo['result'] = result + + noti_info = taskinfo.copy() + noti_info['host_name'] = hostinfo and hostinfo['name'] or None + noti_info['state_str'] = koji.TASK_STATES[taskinfo['state']] + + cancel_info = '' + failure_info = '' + if taskinfo['state'] == koji.TASK_STATES['CANCELED']: + # The owner of the buildNotification task is the one + # who canceled the task, it turns out. + this_task = self.session.getTaskInfo(self.id) + if this_task['owner']: + canceler = self.session.getUser(this_task['owner'], strict=True) + cancel_info = "\r\nCanceled by: %s\r\n" % canceler['name'] + elif taskinfo['state'] == koji.TASK_STATES['FAILED']: + failure_data = taskinfo['result'] + failed_host = '%s (%s)' % (noti_info['host_name'], noti_info['arch']) + failure_info = "\r\nTask#%s failed on %s:\r\n %s" % (task_id, failed_host, failure_data) + + noti_info['failure'] = failure_info or cancel_info or '\r\n' + + noti_info['from_addr'] = self.options.from_addr + noti_info['to_addrs'] = recipient + noti_info['subject'] = self.subject_templ % noti_info + noti_info['weburl'] = weburl + return noti_info + + def handler(self, recipient, task_id, weburl): + noti_info = self._get_notification_info(task_id, recipient, weburl) + + message = self.message_templ % noti_info + # ensure message is in UTF-8 + message = koji.fixEncoding(message) + + server = smtplib.SMTP(self.options.smtphost) + # server.set_debuglevel(True) + self.logger.debug("send notification for task #%i to %s, message:\n%s" % (task_id, recipient, message)) + server.sendmail(noti_info['from_addr'], recipient, message) + server.quit() + + return 'sent notification of task #%i to: %s' % (task_id, recipient) + diff --git a/plugins/hub/task_notification.conf b/plugins/hub/task_notification.conf new file mode 100644 index 0000000..98d0dde --- /dev/null +++ b/plugins/hub/task_notification.conf @@ -0,0 +1,7 @@ +# config file for the Koji task_notification plugin + +[permissions] +# task methods for whose notification can be triggered +# * can be used to allow everything. Multiple values are delimited by comma. +allowed_methods = maven +allowed_states = FAILED,CANCELED diff --git a/plugins/hub/task_notification.py b/plugins/hub/task_notification.py new file mode 100644 index 0000000..cd6803a --- /dev/null +++ b/plugins/hub/task_notification.py @@ -0,0 +1,63 @@ +# koji hub plugin to trigger task notification. +# by default, only failed MavenTask can trigger a TaskNotification, +# which is exported as a task handler in a relative koji builder plugin. + +from koji.context import context +from koji.plugin import callback, ignore_error +import ConfigParser +import sys + +# XXX - have to import kojihub for make_task +sys.path.insert(0, '/usr/share/koji-hub/') +import kojihub + +__all__ = ('task_notification',) + +CONFIG_FILE = '/etc/koji-hub/plugins/task_notification.conf' +config = None +allowed_methods = '*' +disallowed_methods = ['taskNotification'] +allowed_states = '*' + + +def read_config(): + global config, allowed_methods, disallowed_methods, allowed_states + # read configuration only once + if config is None: + config = ConfigParser.SafeConfigParser() + config.read(CONFIG_FILE) + allowed_methods = config.get('permissions', 'allowed_methods').split(',') + if len(allowed_methods) == 1 and allowed_methods[0] == '*': + allowed_methods = '*' + allowed_states = config.get('permissions', 'allowed_states').split(',') + if len(allowed_states) == 1 and allowed_states[0] == '*': + allowed_methods = '*' + + +def task_notification(task_id): + """Trigger a notification of a task to the owner via email""" + if context.opts.get('DisableNotifications'): + return + # sanity check for the existence of task + taskinfo = kojihub.Task(task_id).getInfo(strict=True) + # only send notification to the task's owner + owner = kojihub.get_user(taskinfo['owner'], strict=True)['name'] + email_domain = context.opts['EmailDomain'] + recipient = '%s@%s' % (owner, email_domain) + web_url = context.opts.get('KojiWebURL', 'http://localhost/koji') + kojihub.make_task("taskNotification", [recipient, task_id, web_url]) + + +@callback('postTaskStateChange') +@ignore_error +def task_notification_callback(cbtype, *args, **kws): + global allowed_methods, disallowed_methods, allowed_states + if kws['attribute'] != 'state': + return + read_config() + taskinfo = kws['info'] + new = kws['new'] + if (allowed_states == '*' or new in allowed_states) \ + and (allowed_methods == '*' or taskinfo['method'] in allowed_methods) \ + and taskinfo['method'] not in disallowed_methods: + task_notification(taskinfo['id']) From 7516880a7a334a7f8945654532cbdf6a738950ee Mon Sep 17 00:00:00 2001 From: Yuming Zhu Date: May 08 2018 08:32:04 +0000 Subject: [PATCH 2/7] builder unit tests for task_notification plugin --- diff --git a/tests/test_plugins/test_task_notification_builder.py b/tests/test_plugins/test_task_notification_builder.py new file mode 100644 index 0000000..bf77fd8 --- /dev/null +++ b/tests/test_plugins/test_task_notification_builder.py @@ -0,0 +1,297 @@ +from __future__ import absolute_import +import mock +import os +import sys +import unittest +import xmlrpclib +from mock import call + +import koji + +from task_notification import TaskNotificationTask + +taskinfo = {'id': 111, + 'host_id': 2, + 'owner': 222, + 'state': 0, + 'parent': None, + 'method': 'someMethod', + 'arch': 'someArch', + 'label': None, + 'create_time': '2017-01-01 00:00:00.12131', + 'start_time': '2017-02-01 00:00:00.12131', + 'completion_time': '2017-01-01 00:00:00.12131'} + +hostinfo = {'id': 2, 'name': 'task.host.com'} +userinfo = {'id': 222, 'name': 'somebody'} +taskresult = 'task result' + + +class TestTaskNotification(unittest.TestCase): + def setUp(self): + self.session = mock.MagicMock() + self.session.getTaskInfo.return_value = taskinfo + self.session.getHost.return_value = hostinfo + self.session.getUser.return_value = userinfo + self.session.getTaskResult.return_value = taskresult + self.smtpClass = mock.patch("smtplib.SMTP").start() + self.smtp_server = self.smtpClass.return_value + options = mock.MagicMock() + options.from_addr = 'koji@example.com' + self.task = TaskNotificationTask(666, 'taskNotification', {}, self.session, options) + + def tearDown(self): + mock.patch.stopall() + + def reset_mock(self): + self.session.reset_mock() + self.smtpClass.reset_mock() + + def test_task_notification_canceled(self): + ti = taskinfo.copy() + ti['state'] = 3 # canceled + self.session.getTaskInfo.side_effect = [ti, + {'id': 666, + 'owner': 333}] + self.session.getUser.side_effect = [userinfo, {'id': 666, 'name': 'notitaskowner'}] + + rv = self.task.handler('someone@example.com', 111, 'https://kojiurl.com') + self.assertEqual(rv, 'sent notification of task #111 to: someone@example.com') + self.assertEqual(self.session.getTaskInfo.mock_calls, [call(111, request=True), call(666)]) + self.session.getHost.assert_called_once_with(2) + self.assertEqual(self.session.getUser.mock_calls, [call(222, strict=True), call(333, strict=True)]) + self.session.getTaskResult.assert_called_once_with(111) + self.smtp_server.sendmail.assert_called_once_with('koji@example.com', 'someone@example.com', + 'From: koji@example.com\r\n' + 'Subject: Task: #111 Status: CANCELED Owner: somebody\r\n' + 'To: someone@example.com\r\n' + 'X-Koji-Task: 111\r\n' + 'X-Koji-Owner: somebody\r\n' + 'X-Koji-Status: CANCELED\r\n' + 'X-Koji-Parent: None\r\n' + 'X-Koji-Method: someMethod\r\n\r\n' + 'Task: 111\r\n' + 'Status: CANCELED\r\n' + 'Owner: somebody\r\n' + 'Host: task.host.com\r\n' + 'Method: someMethod\r\n' + 'Parent: None\r\n' + 'Arch: someArch\r\n' + 'Label: None\r\n' + 'Created: 2017-01-01 00:00:00.12131\r\n' + 'Started: 2017-02-01 00:00:00.12131\r\n' + 'Finished: 2017-01-01 00:00:00.12131\r\n\r\n' + 'Canceled by: notitaskowner\r\n\r\n' + 'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n') + + def test_task_notification_failed(self): + ti = taskinfo.copy() + ti['state'] = 5 # failed + self.session.getTaskInfo.return_value = ti + + rv = self.task.handler('someone@example.com', 111, 'https://kojiurl.com') + self.assertEqual(rv, 'sent notification of task #111 to: someone@example.com') + self.session.getTaskInfo.assert_called_once_with(111, request=True) + self.session.getHost.assert_called_once_with(2) + self.session.getUser.assert_called_once_with(222, strict=True) + self.session.getTaskResult.assert_called_once_with(111) + self.smtp_server.sendmail.assert_called_once_with('koji@example.com', 'someone@example.com', + 'From: koji@example.com\r\n' + 'Subject: Task: #111 Status: FAILED Owner: somebody\r\n' + 'To: someone@example.com\r\n' + 'X-Koji-Task: 111\r\n' + 'X-Koji-Owner: somebody\r\n' + 'X-Koji-Status: FAILED\r\n' + 'X-Koji-Parent: None\r\n' + 'X-Koji-Method: someMethod\r\n\r\n' + 'Task: 111\r\n' + 'Status: FAILED\r\n' + 'Owner: somebody\r\n' + 'Host: task.host.com\r\n' + 'Method: someMethod\r\n' + 'Parent: None\r\n' + 'Arch: someArch\r\n' + 'Label: None\r\n' + 'Created: 2017-01-01 00:00:00.12131\r\n' + 'Started: 2017-02-01 00:00:00.12131\r\n' + 'Finished: 2017-01-01 00:00:00.12131\r\n\r\n' + 'Task#111 failed on task.host.com (someArch):\r\n' + ' task result\r\n' + 'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n') + + self.reset_mock() + self.session.getTaskResult.return_value = None + rv = self.task.handler('someone@example.com', 111, 'https://kojiurl.com') + self.assertEqual(rv, 'sent notification of task #111 to: someone@example.com') + self.session.getTaskInfo.assert_called_once_with(111, request=True) + self.session.getHost.assert_called_once_with(2) + self.session.getUser.assert_called_once_with(222, strict=True) + self.session.getTaskResult.assert_called_once_with(111) + self.smtp_server.sendmail.assert_called_once_with('koji@example.com', 'someone@example.com', + 'From: koji@example.com\r\n' + 'Subject: Task: #111 Status: FAILED Owner: somebody\r\n' + 'To: someone@example.com\r\n' + 'X-Koji-Task: 111\r\n' + 'X-Koji-Owner: somebody\r\n' + 'X-Koji-Status: FAILED\r\n' + 'X-Koji-Parent: None\r\n' + 'X-Koji-Method: someMethod\r\n\r\n' + 'Task: 111\r\n' + 'Status: FAILED\r\n' + 'Owner: somebody\r\n' + 'Host: task.host.com\r\n' + 'Method: someMethod\r\n' + 'Parent: None\r\n' + 'Arch: someArch\r\n' + 'Label: None\r\n' + 'Created: 2017-01-01 00:00:00.12131\r\n' + 'Started: 2017-02-01 00:00:00.12131\r\n' + 'Finished: 2017-01-01 00:00:00.12131\r\n\r\n' + 'Task#111 failed on task.host.com (someArch):\r\n' + ' Unknown\r\n' + 'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n') + + self.reset_mock() + self.session.getTaskResult.side_effect = xmlrpclib.Fault(1231, 'xmlrpc fault') + rv = self.task.handler('someone@example.com', 111, 'https://kojiurl.com') + self.assertEqual(rv, 'sent notification of task #111 to: someone@example.com') + self.session.getTaskInfo.assert_called_once_with(111, request=True) + self.session.getHost.assert_called_once_with(2) + self.session.getUser.assert_called_once_with(222, strict=True) + self.session.getTaskResult.assert_called_once_with(111) + self.smtp_server.sendmail.assert_called_once_with('koji@example.com', 'someone@example.com', + 'From: koji@example.com\r\n' + 'Subject: Task: #111 Status: FAILED Owner: somebody\r\n' + 'To: someone@example.com\r\n' + 'X-Koji-Task: 111\r\n' + 'X-Koji-Owner: somebody\r\n' + 'X-Koji-Status: FAILED\r\n' + 'X-Koji-Parent: None\r\n' + 'X-Koji-Method: someMethod\r\n\r\n' + 'Task: 111\r\n' + 'Status: FAILED\r\n' + 'Owner: somebody\r\n' + 'Host: task.host.com\r\n' + 'Method: someMethod\r\n' + 'Parent: None\r\n' + 'Arch: someArch\r\n' + 'Label: None\r\n' + 'Created: 2017-01-01 00:00:00.12131\r\n' + 'Started: 2017-02-01 00:00:00.12131\r\n' + 'Finished: 2017-01-01 00:00:00.12131\r\n\r\n' + 'Task#111 failed on task.host.com (someArch):\r\n' + ' xmlrpc fault\r\n' + 'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n') + + self.reset_mock() + self.session.getTaskResult.side_effect = koji.GenericError('koji generic error') + rv = self.task.handler('someone@example.com', 111, 'https://kojiurl.com') + self.assertEqual(rv, 'sent notification of task #111 to: someone@example.com') + self.session.getTaskInfo.assert_called_once_with(111, request=True) + self.session.getHost.assert_called_once_with(2) + self.session.getUser.assert_called_once_with(222, strict=True) + self.session.getTaskResult.assert_called_once_with(111) + self.smtp_server.sendmail.assert_called_once_with('koji@example.com', 'someone@example.com', + 'From: koji@example.com\r\n' + 'Subject: Task: #111 Status: FAILED Owner: somebody\r\n' + 'To: someone@example.com\r\n' + 'X-Koji-Task: 111\r\n' + 'X-Koji-Owner: somebody\r\n' + 'X-Koji-Status: FAILED\r\n' + 'X-Koji-Parent: None\r\n' + 'X-Koji-Method: someMethod\r\n\r\n' + 'Task: 111\r\n' + 'Status: FAILED\r\n' + 'Owner: somebody\r\n' + 'Host: task.host.com\r\n' + 'Method: someMethod\r\n' + 'Parent: None\r\n' + 'Arch: someArch\r\n' + 'Label: None\r\n' + 'Created: 2017-01-01 00:00:00.12131\r\n' + 'Started: 2017-02-01 00:00:00.12131\r\n' + 'Finished: 2017-01-01 00:00:00.12131\r\n\r\n' + 'Task#111 failed on task.host.com (someArch):\r\n' + ' GenericError: koji generic error\r\n' + 'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n') + + def test_task_notification_other_status(self): + ti = taskinfo.copy() + ti['state'] = 2 # closed + self.session.getTaskInfo.return_value = ti + + rv = self.task.handler('someone@example.com', 111, 'https://kojiurl.com') + self.assertEqual(rv, 'sent notification of task #111 to: someone@example.com') + self.session.getTaskInfo.assert_called_once_with(111, request=True) + self.session.getHost.assert_called_once_with(2) + self.session.getUser.assert_called_once_with(222, strict=True) + self.session.getTaskResult.assert_called_once_with(111) + self.smtp_server.sendmail.assert_called_once_with('koji@example.com', 'someone@example.com', + 'From: koji@example.com\r\n' + 'Subject: Task: #111 Status: CLOSED Owner: somebody\r\n' + 'To: someone@example.com\r\n' + 'X-Koji-Task: 111\r\n' + 'X-Koji-Owner: somebody\r\n' + 'X-Koji-Status: CLOSED\r\n' + 'X-Koji-Parent: None\r\n' + 'X-Koji-Method: someMethod\r\n\r\n' + 'Task: 111\r\n' + 'Status: CLOSED\r\n' + 'Owner: somebody\r\n' + 'Host: task.host.com\r\n' + 'Method: someMethod\r\n' + 'Parent: None\r\n' + 'Arch: someArch\r\n' + 'Label: None\r\n' + 'Created: 2017-01-01 00:00:00.12131\r\n' + 'Started: 2017-02-01 00:00:00.12131\r\n' + 'Finished: 2017-01-01 00:00:00.12131\r\n\r\n\r\n' + 'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n') + + def test_task_notification_no_host_user(self): + ti = taskinfo.copy() + ti['state'] = 2 # closed + ti['host_id'] = None + ti['owner'] = None + self.session.getTaskInfo.return_value = ti + self.session.getHost.return_value = None + self.session.getUser.return_value = None + + rv = self.task.handler('someone@example.com', 111, 'https://kojiurl.com') + self.assertEqual(rv, 'sent notification of task #111 to: someone@example.com') + self.session.getTaskInfo.assert_called_once_with(111, request=True) + self.session.getHost.assert_not_called() + self.session.getUser.assert_not_called() + self.session.getTaskResult.assert_called_once_with(111) + self.smtp_server.sendmail.assert_called_once_with('koji@example.com', 'someone@example.com', + 'From: koji@example.com\r\n' + 'Subject: Task: #111 Status: CLOSED Owner: None\r\n' + 'To: someone@example.com\r\n' + 'X-Koji-Task: 111\r\n' + 'X-Koji-Owner: None\r\n' + 'X-Koji-Status: CLOSED\r\n' + 'X-Koji-Parent: None\r\n' + 'X-Koji-Method: someMethod\r\n\r\n' + 'Task: 111\r\n' + 'Status: CLOSED\r\n' + 'Owner: None\r\n' + 'Host: None\r\n' + 'Method: someMethod\r\n' + 'Parent: None\r\n' + 'Arch: someArch\r\n' + 'Label: None\r\n' + 'Created: 2017-01-01 00:00:00.12131\r\n' + 'Started: 2017-02-01 00:00:00.12131\r\n' + 'Finished: 2017-01-01 00:00:00.12131\r\n\r\n\r\n' + 'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n') + + def test_task_notification_no_taskinfo(self): + self.session.getTaskInfo.return_value = None + with self.assertRaises(koji.GenericError) as cm: + self.task.handler('someone@example.com', 111, 'https://kojiurl.com') + self.assertEqual(cm.exception.args[0], 'Cannot find task#111') + self.session.getTaskInfo.assert_called_once_with(111, request=True) + self.session.getHost.assert_not_called() + self.session.getUser.assert_not_called() + self.session.getTaskResult.assert_not_called() + self.smtp_server.sendmail.assert_not_called() From b289fefa08acb58311f4d6ebc13382370cc14735 Mon Sep 17 00:00:00 2001 From: Yuming Zhu Date: May 08 2018 09:00:31 +0000 Subject: [PATCH 3/7] unit test for task_notification hub plugin --- diff --git a/plugins/hub/task_notification.py b/plugins/hub/task_notification.py index cd6803a..4e6418b 100644 --- a/plugins/hub/task_notification.py +++ b/plugins/hub/task_notification.py @@ -31,7 +31,7 @@ def read_config(): allowed_methods = '*' allowed_states = config.get('permissions', 'allowed_states').split(',') if len(allowed_states) == 1 and allowed_states[0] == '*': - allowed_methods = '*' + allowed_states = '*' def task_notification(task_id): diff --git a/tests/test_plugins/helper.py b/tests/test_plugins/helper.py new file mode 100644 index 0000000..21869f2 --- /dev/null +++ b/tests/test_plugins/helper.py @@ -0,0 +1,26 @@ +import six +import copy + + +class FakeConfigParser(object): + + def __init__(self, config): + self.CONFIG = copy.deepcopy(config) + + def read(self, path): + return + + def sections(self): + return self.CONFIG.keys() + + def has_option(self, section, key): + return section in self.CONFIG and key in self.CONFIG[section] + + def has_section(self, section): + return section in self.CONFIG + + def get(self, section, key): + try: + return self.CONFIG[section][key] + except KeyError: + raise six.moves.configparser.NoOptionError(section, key) diff --git a/tests/test_plugins/test_runroot_builder.py b/tests/test_plugins/test_runroot_builder.py index 426ad35..332b9ff 100644 --- a/tests/test_plugins/test_runroot_builder.py +++ b/tests/test_plugins/test_runroot_builder.py @@ -1,17 +1,21 @@ from __future__ import absolute_import + import copy import unittest + +import __main__ import mock -import six.moves.configparser # inject builder data from tests.test_builder.loadkojid import kojid -import __main__ + __main__.BuildRoot = kojid.BuildRoot -import koji import runroot +import koji +from .helper import FakeConfigParser + CONFIG1 = { 'paths': { @@ -57,37 +61,10 @@ CONFIG2 = { }} -class FakeConfigParser(object): - - def __init__(self, config=None): - if config is None: - self.CONFIG = copy.deepcopy(CONFIG1) - else: - self.CONFIG = copy.deepcopy(config) - - def read(self, path): - return - - def sections(self): - return self.CONFIG.keys() - - def has_option(self, section, key): - return section in self.CONFIG and key in self.CONFIG[section] - - def has_section(self, section): - return section in self.CONFIG - - def get(self, section, key): - try: - return self.CONFIG[section][key] - except KeyError: - raise six.moves.configparser.NoOptionError(section, key) - - class TestRunrootConfig(unittest.TestCase): @mock.patch('ConfigParser.SafeConfigParser') def test_bad_config_paths0(self, safe_config_parser): - cp = FakeConfigParser() + cp = FakeConfigParser(CONFIG1) del cp.CONFIG['path0']['mountpoint'] safe_config_parser.return_value = cp session = mock.MagicMock() @@ -100,7 +77,7 @@ class TestRunrootConfig(unittest.TestCase): @mock.patch('ConfigParser.SafeConfigParser') def test_bad_config_absolute_path(self, safe_config_parser): - cp = FakeConfigParser() + cp = FakeConfigParser(CONFIG1) cp.CONFIG['paths']['default_mounts'] = '' safe_config_parser.return_value = cp session = mock.MagicMock() @@ -113,7 +90,7 @@ class TestRunrootConfig(unittest.TestCase): @mock.patch('ConfigParser.SafeConfigParser') def test_valid_config(self, safe_config_parser): - safe_config_parser.return_value = FakeConfigParser() + safe_config_parser.return_value = FakeConfigParser(CONFIG1) session = mock.MagicMock() options = mock.MagicMock() options.workdir = '/tmp/nonexistentdirectory' @@ -163,7 +140,7 @@ class TestRunrootConfig(unittest.TestCase): class TestMounts(unittest.TestCase): @mock.patch('ConfigParser.SafeConfigParser') def setUp(self, safe_config_parser): - safe_config_parser.return_value = FakeConfigParser() + safe_config_parser.return_value = FakeConfigParser(CONFIG1) self.session = mock.MagicMock() options = mock.MagicMock() options.workdir = '/tmp/nonexistentdirectory' diff --git a/tests/test_plugins/test_task_notification_builder.py b/tests/test_plugins/test_task_notification_builder.py index bf77fd8..faa3d82 100644 --- a/tests/test_plugins/test_task_notification_builder.py +++ b/tests/test_plugins/test_task_notification_builder.py @@ -1,14 +1,12 @@ from __future__ import absolute_import import mock -import os -import sys import unittest import xmlrpclib from mock import call import koji - -from task_notification import TaskNotificationTask +from . import load_plugin +task_notification = load_plugin.load_plugin('builder', 'task_notification') taskinfo = {'id': 111, 'host_id': 2, @@ -38,7 +36,7 @@ class TestTaskNotification(unittest.TestCase): self.smtp_server = self.smtpClass.return_value options = mock.MagicMock() options.from_addr = 'koji@example.com' - self.task = TaskNotificationTask(666, 'taskNotification', {}, self.session, options) + self.task = task_notification.TaskNotificationTask(666, 'taskNotification', {}, self.session, options) def tearDown(self): mock.patch.stopall() diff --git a/tests/test_plugins/test_task_notification_hub.py b/tests/test_plugins/test_task_notification_hub.py new file mode 100644 index 0000000..e32aaf4 --- /dev/null +++ b/tests/test_plugins/test_task_notification_hub.py @@ -0,0 +1,107 @@ +import unittest +import mock +from koji.context import context + +from .helper import FakeConfigParser +from . import load_plugin + +task_notification = load_plugin.load_plugin('hub', 'task_notification') + +CONFIG1 = {'permissions': { + 'allowed_methods': "someMethod,someotherMethod", + 'allowed_states': "FAILED,NEW" +}} + +CONFIG2 = {'permissions': { + 'allowed_methods': "*", + 'allowed_states': "*" +}} + + +class TestTaskNotificationCallback(unittest.TestCase): + def setUp(self): + context.session = mock.MagicMock() + context.opts = {'DisableNotifications': False, + 'EmailDomain': 'example.com', + 'KojiWebURL': 'https://koji.org'} + self.parser = mock.patch('ConfigParser.SafeConfigParser', return_value=FakeConfigParser(CONFIG1)).start() + self.getTaskInfo = mock.patch('kojihub.Task.getInfo').start() + self.get_user = mock.patch('kojihub.get_user', return_value={'id': 999, 'name': 'someone'}).start() + self.make_task = mock.patch('kojihub.make_task').start() + + def tearDown(self): + mock.patch.stopall() + task_notification.config = None + + def test_basic_invocation(self): + task_notification.task_notification_callback( + 'postTaskStateChange', + attribute='state', + new='FAILED', + info={'id': 123, 'method': 'someMethod'}, + ) + self.assertEqual(task_notification.allowed_methods, ['someMethod', 'someotherMethod']) + self.assertEqual(task_notification.allowed_states, ['FAILED', 'NEW']) + self.make_task.assert_called_once_with( + 'taskNotification', + ['someone@example.com', 123, 'https://koji.org']) + + def test_disable_notifications(self): + context.opts['DisableNotifications'] = True + task_notification.task_notification_callback( + 'postTaskStateChange', + attribute='state', + new='FAILED', + info={'id': 123, 'method': 'someMethod'} + ) + self.make_task.assert_not_called() + + def test_not_state_change(self): + task_notification.task_notification_callback( + 'postTaskStateChange', + attribute='others', + new='something', + info={'id': 123, 'method': 'someMethod'} + ) + self.make_task.assert_not_called() + + def test_disallowed_state(self): + task_notification.task_notification_callback( + 'postTaskStateChange', + attribute='state', + new='something', + info={'id': 123, 'method': 'someMethod'} + ) + self.make_task.assert_not_called() + + def test_disallowed_method(self): + task_notification.task_notification_callback( + 'postTaskStateChange', + attribute='state', + new='FAILED', + info={'id': 123, 'method': 'xxxMethod'} + ) + self.make_task.assert_not_called() + + def test_method_self(self): + self.parser.return_value = FakeConfigParser(CONFIG2) + task_notification.task_notification_callback( + 'postTaskStateChange', + attribute='state', + new='somestate', + info={'id': 123, 'method': 'taskNotification'} + ) + self.make_task.assert_not_called() + + def test_allow_everything(self): + self.parser.return_value = FakeConfigParser(CONFIG2) + task_notification.task_notification_callback( + 'postTaskStateChange', + attribute='state', + new='somestate', + info={'id': 123, 'method': 'xxxMethod'} + ) + self.make_task.assert_called_once_with( + 'taskNotification', + ['someone@example.com', 123, 'https://koji.org']) + From 2d542dbdffcdf9ceae350221c08ec11cc8812ab2 Mon Sep 17 00:00:00 2001 From: Yuming Zhu Date: May 08 2018 09:00:55 +0000 Subject: [PATCH 4/7] change date format --- diff --git a/plugins/builder/task_notification.py b/plugins/builder/task_notification.py index 687e630..d7b9d61 100644 --- a/plugins/builder/task_notification.py +++ b/plugins/builder/task_notification.py @@ -74,6 +74,9 @@ Task Info: %(weburl)s/taskinfo?taskID=%(id)i\r taskinfo['result'] = result noti_info = taskinfo.copy() + noti_info['create_time'] = koji.formatTimeLong(taskinfo.get('create_time')) + noti_info['start_time'] = koji.formatTimeLong(taskinfo.get('start_time')) + noti_info['completion_time'] = koji.formatTimeLong(taskinfo.get('completion_time')) noti_info['host_name'] = hostinfo and hostinfo['name'] or None noti_info['state_str'] = koji.TASK_STATES[taskinfo['state']] diff --git a/tests/test_plugins/test_task_notification_builder.py b/tests/test_plugins/test_task_notification_builder.py index faa3d82..de66185 100644 --- a/tests/test_plugins/test_task_notification_builder.py +++ b/tests/test_plugins/test_task_notification_builder.py @@ -1,4 +1,5 @@ from __future__ import absolute_import +import os import mock import unittest import xmlrpclib @@ -16,9 +17,9 @@ taskinfo = {'id': 111, 'method': 'someMethod', 'arch': 'someArch', 'label': None, - 'create_time': '2017-01-01 00:00:00.12131', - 'start_time': '2017-02-01 00:00:00.12131', - 'completion_time': '2017-01-01 00:00:00.12131'} + 'create_time': '2017-01-01 00:00:00.121313', + 'start_time': '2017-02-01 00:00:00.121313', + 'completion_time': '2017-01-01 00:00:00.121313'} hostinfo = {'id': 2, 'name': 'task.host.com'} userinfo = {'id': 222, 'name': 'somebody'} @@ -27,6 +28,8 @@ taskresult = 'task result' class TestTaskNotification(unittest.TestCase): def setUp(self): + self.original_timezone = os.environ.get('TZ') + os.environ['TZ'] = 'US/Eastern' self.session = mock.MagicMock() self.session.getTaskInfo.return_value = taskinfo self.session.getHost.return_value = hostinfo @@ -39,6 +42,10 @@ class TestTaskNotification(unittest.TestCase): self.task = task_notification.TaskNotificationTask(666, 'taskNotification', {}, self.session, options) def tearDown(self): + if self.original_timezone is None: + del os.environ['TZ'] + else: + os.environ['TZ'] = self.original_timezone mock.patch.stopall() def reset_mock(self): @@ -76,9 +83,9 @@ class TestTaskNotification(unittest.TestCase): 'Parent: None\r\n' 'Arch: someArch\r\n' 'Label: None\r\n' - 'Created: 2017-01-01 00:00:00.12131\r\n' - 'Started: 2017-02-01 00:00:00.12131\r\n' - 'Finished: 2017-01-01 00:00:00.12131\r\n\r\n' + 'Created: Sun, 01 Jan 2017 00:00:00 EST\r\n' + 'Started: Wed, 01 Feb 2017 00:00:00 EST\r\n' + 'Finished: Sun, 01 Jan 2017 00:00:00 EST\r\n\r\n' 'Canceled by: notitaskowner\r\n\r\n' 'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n') @@ -110,9 +117,9 @@ class TestTaskNotification(unittest.TestCase): 'Parent: None\r\n' 'Arch: someArch\r\n' 'Label: None\r\n' - 'Created: 2017-01-01 00:00:00.12131\r\n' - 'Started: 2017-02-01 00:00:00.12131\r\n' - 'Finished: 2017-01-01 00:00:00.12131\r\n\r\n' + 'Created: Sun, 01 Jan 2017 00:00:00 EST\r\n' + 'Started: Wed, 01 Feb 2017 00:00:00 EST\r\n' + 'Finished: Sun, 01 Jan 2017 00:00:00 EST\r\n\r\n' 'Task#111 failed on task.host.com (someArch):\r\n' ' task result\r\n' 'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n') @@ -142,9 +149,9 @@ class TestTaskNotification(unittest.TestCase): 'Parent: None\r\n' 'Arch: someArch\r\n' 'Label: None\r\n' - 'Created: 2017-01-01 00:00:00.12131\r\n' - 'Started: 2017-02-01 00:00:00.12131\r\n' - 'Finished: 2017-01-01 00:00:00.12131\r\n\r\n' + 'Created: Sun, 01 Jan 2017 00:00:00 EST\r\n' + 'Started: Wed, 01 Feb 2017 00:00:00 EST\r\n' + 'Finished: Sun, 01 Jan 2017 00:00:00 EST\r\n\r\n' 'Task#111 failed on task.host.com (someArch):\r\n' ' Unknown\r\n' 'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n') @@ -174,9 +181,9 @@ class TestTaskNotification(unittest.TestCase): 'Parent: None\r\n' 'Arch: someArch\r\n' 'Label: None\r\n' - 'Created: 2017-01-01 00:00:00.12131\r\n' - 'Started: 2017-02-01 00:00:00.12131\r\n' - 'Finished: 2017-01-01 00:00:00.12131\r\n\r\n' + 'Created: Sun, 01 Jan 2017 00:00:00 EST\r\n' + 'Started: Wed, 01 Feb 2017 00:00:00 EST\r\n' + 'Finished: Sun, 01 Jan 2017 00:00:00 EST\r\n\r\n' 'Task#111 failed on task.host.com (someArch):\r\n' ' xmlrpc fault\r\n' 'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n') @@ -206,9 +213,9 @@ class TestTaskNotification(unittest.TestCase): 'Parent: None\r\n' 'Arch: someArch\r\n' 'Label: None\r\n' - 'Created: 2017-01-01 00:00:00.12131\r\n' - 'Started: 2017-02-01 00:00:00.12131\r\n' - 'Finished: 2017-01-01 00:00:00.12131\r\n\r\n' + 'Created: Sun, 01 Jan 2017 00:00:00 EST\r\n' + 'Started: Wed, 01 Feb 2017 00:00:00 EST\r\n' + 'Finished: Sun, 01 Jan 2017 00:00:00 EST\r\n\r\n' 'Task#111 failed on task.host.com (someArch):\r\n' ' GenericError: koji generic error\r\n' 'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n') @@ -241,9 +248,9 @@ class TestTaskNotification(unittest.TestCase): 'Parent: None\r\n' 'Arch: someArch\r\n' 'Label: None\r\n' - 'Created: 2017-01-01 00:00:00.12131\r\n' - 'Started: 2017-02-01 00:00:00.12131\r\n' - 'Finished: 2017-01-01 00:00:00.12131\r\n\r\n\r\n' + 'Created: Sun, 01 Jan 2017 00:00:00 EST\r\n' + 'Started: Wed, 01 Feb 2017 00:00:00 EST\r\n' + 'Finished: Sun, 01 Jan 2017 00:00:00 EST\r\n\r\n\r\n' 'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n') def test_task_notification_no_host_user(self): @@ -278,9 +285,9 @@ class TestTaskNotification(unittest.TestCase): 'Parent: None\r\n' 'Arch: someArch\r\n' 'Label: None\r\n' - 'Created: 2017-01-01 00:00:00.12131\r\n' - 'Started: 2017-02-01 00:00:00.12131\r\n' - 'Finished: 2017-01-01 00:00:00.12131\r\n\r\n\r\n' + 'Created: Sun, 01 Jan 2017 00:00:00 EST\r\n' + 'Started: Wed, 01 Feb 2017 00:00:00 EST\r\n' + 'Finished: Sun, 01 Jan 2017 00:00:00 EST\r\n\r\n\r\n' 'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n') def test_task_notification_no_taskinfo(self): From aba48bb6ec5a2dc1228ceb6b2c1438ae68975e1a Mon Sep 17 00:00:00 2001 From: Yuming Zhu Date: May 09 2018 08:59:31 +0000 Subject: [PATCH 5/7] refine config - loading config file every time - renaming config items - configurable disallowed items - using multi_fnmatch - adding as another seperator of config values --- diff --git a/plugins/hub/task_notification.conf b/plugins/hub/task_notification.conf index 98d0dde..678d324 100644 --- a/plugins/hub/task_notification.conf +++ b/plugins/hub/task_notification.conf @@ -1,7 +1,19 @@ # config file for the Koji task_notification plugin -[permissions] -# task methods for whose notification can be triggered -# * can be used to allow everything. Multiple values are delimited by comma. -allowed_methods = maven -allowed_states = FAILED,CANCELED +[filters] +# Unix shell style patterns of task methods to indicate whose notification +# can be triggered +# Multiple values are delimited by comma or +# Default value is '*' +methods = maven,runroot,distRepo + +# Unix shell style patterns of task methods to indicate whose notification +# is forbidden +# Multiple values are delimited by comma or +# disallowed_methods = *Notification + +# Unix shell style patterns of task states to indicate whose notification +# can be triggered +# Multiple values are delimited by comma or +# Default value is '*' +states = FAILED,CANCELED diff --git a/plugins/hub/task_notification.py b/plugins/hub/task_notification.py index 4e6418b..4174953 100644 --- a/plugins/hub/task_notification.py +++ b/plugins/hub/task_notification.py @@ -2,11 +2,17 @@ # by default, only failed MavenTask can trigger a TaskNotification, # which is exported as a task handler in a relative koji builder plugin. -from koji.context import context -from koji.plugin import callback, ignore_error + import ConfigParser +import re import sys +import six + +from koji.context import context +from koji.plugin import callback, ignore_error +from koji.util import multi_fnmatch + # XXX - have to import kojihub for make_task sys.path.insert(0, '/usr/share/koji-hub/') import kojihub @@ -14,24 +20,25 @@ import kojihub __all__ = ('task_notification',) CONFIG_FILE = '/etc/koji-hub/plugins/task_notification.conf' -config = None -allowed_methods = '*' -disallowed_methods = ['taskNotification'] -allowed_states = '*' +FILTERS = {'methods': (['*'], None), + 'disallowed_methods': ([], ['*Notification']), + 'states': (['*'], None)} def read_config(): - global config, allowed_methods, disallowed_methods, allowed_states - # read configuration only once - if config is None: - config = ConfigParser.SafeConfigParser() - config.read(CONFIG_FILE) - allowed_methods = config.get('permissions', 'allowed_methods').split(',') - if len(allowed_methods) == 1 and allowed_methods[0] == '*': - allowed_methods = '*' - allowed_states = config.get('permissions', 'allowed_states').split(',') - if len(allowed_states) == 1 and allowed_states[0] == '*': - allowed_states = '*' + result = {} + config = ConfigParser.SafeConfigParser() + config.read(CONFIG_FILE) + for k, (default, force) in six.iteritems(FILTERS): + try: + value = config.get('filters', k) + value = re.split(r'[\s,]+', value) + except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): + value = default + if force is not None: + value.extend(force) + result[k] = value + return result def task_notification(task_id): @@ -51,13 +58,12 @@ def task_notification(task_id): @callback('postTaskStateChange') @ignore_error def task_notification_callback(cbtype, *args, **kws): - global allowed_methods, disallowed_methods, allowed_states if kws['attribute'] != 'state': return - read_config() + cfg = read_config() taskinfo = kws['info'] new = kws['new'] - if (allowed_states == '*' or new in allowed_states) \ - and (allowed_methods == '*' or taskinfo['method'] in allowed_methods) \ - and taskinfo['method'] not in disallowed_methods: + if multi_fnmatch(new, cfg['states']) \ + and multi_fnmatch(taskinfo['method'], cfg['methods']) \ + and not multi_fnmatch(taskinfo['method'], cfg['disallowed_methods']): task_notification(taskinfo['id']) diff --git a/tests/test_plugins/test_task_notification_hub.py b/tests/test_plugins/test_task_notification_hub.py index e32aaf4..f525a5a 100644 --- a/tests/test_plugins/test_task_notification_hub.py +++ b/tests/test_plugins/test_task_notification_hub.py @@ -7,14 +7,15 @@ from . import load_plugin task_notification = load_plugin.load_plugin('hub', 'task_notification') -CONFIG1 = {'permissions': { - 'allowed_methods': "someMethod,someotherMethod", - 'allowed_states': "FAILED,NEW" +CONFIG1 = {'filters': { + 'methods': "someMethod,someotherMethod", + 'disallowed_methods': 'disallowedMethods,*Whatever', + 'states': "FAILED,NEW" }} -CONFIG2 = {'permissions': { - 'allowed_methods': "*", - 'allowed_states': "*" +CONFIG2 = {'filters': { + 'methods': "*", + 'states': "*" }} @@ -40,8 +41,6 @@ class TestTaskNotificationCallback(unittest.TestCase): new='FAILED', info={'id': 123, 'method': 'someMethod'}, ) - self.assertEqual(task_notification.allowed_methods, ['someMethod', 'someotherMethod']) - self.assertEqual(task_notification.allowed_states, ['FAILED', 'NEW']) self.make_task.assert_called_once_with( 'taskNotification', ['someone@example.com', 123, 'https://koji.org']) @@ -79,9 +78,23 @@ class TestTaskNotificationCallback(unittest.TestCase): 'postTaskStateChange', attribute='state', new='FAILED', + info={'id': 123, 'method': 'disallowedMethod'} + ) + self.make_task.assert_not_called() + task_notification.task_notification_callback( + 'postTaskStateChange', + attribute='state', + new='FAILED', info={'id': 123, 'method': 'xxxMethod'} ) self.make_task.assert_not_called() + task_notification.task_notification_callback( + 'postTaskStateChange', + attribute='state', + new='FAILED', + info={'id': 123, 'method': 'Whatever'} + ) + self.make_task.assert_not_called() def test_method_self(self): self.parser.return_value = FakeConfigParser(CONFIG2) @@ -105,3 +118,13 @@ class TestTaskNotificationCallback(unittest.TestCase): 'taskNotification', ['someone@example.com', 123, 'https://koji.org']) + def test_force_disallowed(self): + self.parser.return_value = FakeConfigParser(CONFIG2) + task_notification.task_notification_callback( + 'postTaskStateChange', + attribute='state', + new='somestate', + info={'id': 123, 'method': 'xxxNotification'} + ) + self.make_task.assert_not_called() + From 5276a24e9b853a43d1bcaba5d7ccf7ad98e62b7d Mon Sep 17 00:00:00 2001 From: Yuming Zhu Date: May 09 2018 09:09:33 +0000 Subject: [PATCH 6/7] remove sanity check for wrong task_id --- diff --git a/plugins/builder/task_notification.py b/plugins/builder/task_notification.py index d7b9d61..28211ed 100644 --- a/plugins/builder/task_notification.py +++ b/plugins/builder/task_notification.py @@ -41,10 +41,6 @@ Task Info: %(weburl)s/taskinfo?taskID=%(id)i\r def _get_notification_info(self, task_id, recipient, weburl): taskinfo = self.session.getTaskInfo(task_id, request=True) - if not taskinfo: - # invalid task_id - raise koji.GenericError('Cannot find task#%i' % task_id) - if taskinfo['host_id']: hostinfo = self.session.getHost(taskinfo['host_id']) else: diff --git a/tests/test_plugins/test_task_notification_builder.py b/tests/test_plugins/test_task_notification_builder.py index de66185..e51d5b0 100644 --- a/tests/test_plugins/test_task_notification_builder.py +++ b/tests/test_plugins/test_task_notification_builder.py @@ -289,14 +289,3 @@ class TestTaskNotification(unittest.TestCase): 'Started: Wed, 01 Feb 2017 00:00:00 EST\r\n' 'Finished: Sun, 01 Jan 2017 00:00:00 EST\r\n\r\n\r\n' 'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n') - - def test_task_notification_no_taskinfo(self): - self.session.getTaskInfo.return_value = None - with self.assertRaises(koji.GenericError) as cm: - self.task.handler('someone@example.com', 111, 'https://kojiurl.com') - self.assertEqual(cm.exception.args[0], 'Cannot find task#111') - self.session.getTaskInfo.assert_called_once_with(111, request=True) - self.session.getHost.assert_not_called() - self.session.getUser.assert_not_called() - self.session.getTaskResult.assert_not_called() - self.smtp_server.sendmail.assert_not_called() From 2e2fe4e817d01d775e328089c03c59fb3f3e7cf5 Mon Sep 17 00:00:00 2001 From: Yuming Zhu Date: May 09 2018 09:46:19 +0000 Subject: [PATCH 7/7] excluding tasks owned by host or disabled user for task_notification --- diff --git a/plugins/hub/task_notification.py b/plugins/hub/task_notification.py index 4174953..fef0902 100644 --- a/plugins/hub/task_notification.py +++ b/plugins/hub/task_notification.py @@ -9,6 +9,7 @@ import sys import six +import koji from koji.context import context from koji.plugin import callback, ignore_error from koji.util import multi_fnmatch @@ -48,9 +49,15 @@ def task_notification(task_id): # sanity check for the existence of task taskinfo = kojihub.Task(task_id).getInfo(strict=True) # only send notification to the task's owner - owner = kojihub.get_user(taskinfo['owner'], strict=True)['name'] + user = kojihub.get_user(taskinfo['owner'], strict=True) + if user['status'] == koji.USER_STATUS['BLOCKED']: + raise koji.GenericError('Unable to send notification to disabled' + ' task#%s owner: %s' % (task_id, user['name'])) + if user['usertype'] == koji.USERTYPES['HOST']: + raise koji.GenericError('Unable to send notification to host: %s whom' + ' owns task#%s' % (user['name'], task_id)) email_domain = context.opts['EmailDomain'] - recipient = '%s@%s' % (owner, email_domain) + recipient = '%s@%s' % (user['name'], email_domain) web_url = context.opts.get('KojiWebURL', 'http://localhost/koji') kojihub.make_task("taskNotification", [recipient, task_id, web_url]) diff --git a/tests/test_plugins/test_task_notification_hub.py b/tests/test_plugins/test_task_notification_hub.py index f525a5a..47ec4b8 100644 --- a/tests/test_plugins/test_task_notification_hub.py +++ b/tests/test_plugins/test_task_notification_hub.py @@ -1,9 +1,11 @@ import unittest + import mock -from koji.context import context -from .helper import FakeConfigParser +from koji import GenericError +from koji.context import context from . import load_plugin +from .helper import FakeConfigParser task_notification = load_plugin.load_plugin('hub', 'task_notification') @@ -25,9 +27,14 @@ class TestTaskNotificationCallback(unittest.TestCase): context.opts = {'DisableNotifications': False, 'EmailDomain': 'example.com', 'KojiWebURL': 'https://koji.org'} - self.parser = mock.patch('ConfigParser.SafeConfigParser', return_value=FakeConfigParser(CONFIG1)).start() + self.parser = mock.patch('ConfigParser.SafeConfigParser', + return_value=FakeConfigParser( + CONFIG1)).start() self.getTaskInfo = mock.patch('kojihub.Task.getInfo').start() - self.get_user = mock.patch('kojihub.get_user', return_value={'id': 999, 'name': 'someone'}).start() + self.get_user = mock.patch('kojihub.get_user', + return_value={'id': 999, 'name': 'someone', + 'status': 0, + 'usertype': 0}).start() self.make_task = mock.patch('kojihub.make_task').start() def tearDown(self): @@ -128,3 +135,46 @@ class TestTaskNotificationCallback(unittest.TestCase): ) self.make_task.assert_not_called() + def test_no_user_found(self): + self.get_user.side_effect = GenericError('not found') + with self.assertRaises(GenericError) as cm: + task_notification.task_notification_callback( + 'postTaskStateChange', + attribute='state', + new='FAILED', + info={'id': 123, 'method': 'someMethod'} + ) + self.assertEqual(cm.exception.args[0], 'not found') + self.make_task.assert_not_called() + + def test_disabled_owner(self): + self.get_user.return_value = {'id': 999, + 'name': 'someone', + 'status': 1, + 'usertype': 0} + with self.assertRaises(GenericError) as cm: + task_notification.task_notification_callback( + 'postTaskStateChange', + attribute='state', + new='FAILED', + info={'id': 123, 'method': 'someMethod'} + ) + self.assertEqual(cm.exception.args[0], 'Unable to send notification to' + ' disabled task#123 owner: someone') + self.make_task.assert_not_called() + + def test_host_owner(self): + self.get_user.return_value = {'id': 999, + 'name': 'somehost', + 'status': 0, + 'usertype': 1} + with self.assertRaises(GenericError) as cm: + task_notification.task_notification_callback( + 'postTaskStateChange', + attribute='state', + new='FAILED', + info={'id': 123, 'method': 'someMethod'} + ) + self.assertEqual(cm.exception.args[0], 'Unable to send notification to' + ' host: somehost whom owns task#123') + self.make_task.assert_not_called() \ No newline at end of file