From a6e0ce590c480b6d1fed9db2967965d720b9ffa2 Mon Sep 17 00:00:00 2001
From: Yuming Zhu <yzhu@redhat.com>
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 <yzhu@redhat.com>
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 <yzhu@redhat.com>
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 <yzhu@redhat.com>
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 <yzhu@redhat.com>
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 <space> 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 <space>
+# 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 <space>
+# disallowed_methods = *Notification
+
+# Unix shell style patterns of task states to indicate whose notification
+# can be triggered
+# Multiple values are delimited by comma or <space>
+# 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 <yzhu@redhat.com>
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 <yzhu@redhat.com>
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

