From 319302200642d8029edccbcf776cd8ebc153c41a Mon Sep 17 00:00:00 2001
From: Jana Cupova <root@localhost>
Date: Jan 09 2023 10:00:32 +0000
Subject: Add test cases for help


Fixes: https://pagure.io/koji/issue/3651

---

diff --git a/cli/koji_cli/commands.py b/cli/koji_cli/commands.py
index 17843be..c4cf995 100644
--- a/cli/koji_cli/commands.py
+++ b/cli/koji_cli/commands.py
@@ -7492,7 +7492,8 @@ def handle_add_notification(goptions, session, args):
     parser.add_option("--user", help="Add notifications for this user (admin-only)")
     parser.add_option("--package", help="Add notifications for this package")
     parser.add_option("--tag", help="Add notifications for this tag")
-    parser.add_option("--success-only", action="store_true", default=False, help="")
+    parser.add_option("--success-only", action="store_true", default=False,
+                      help="Enabled notification on successful events only")
     (options, args) = parser.parse_args(args)
 
     if len(args) != 0:
diff --git a/tests/test_cli/test_add_external_repo.py b/tests/test_cli/test_add_external_repo.py
new file mode 100644
index 0000000..afcccc6
--- /dev/null
+++ b/tests/test_cli/test_add_external_repo.py
@@ -0,0 +1,146 @@
+from __future__ import absolute_import
+
+import mock
+import six
+
+from koji_cli.commands import handle_add_external_repo
+from . import utils
+
+
+class TestAddExternalRepo(utils.CliTestCase):
+
+    def setUp(self):
+        self.maxDiff = None
+        self.options = mock.MagicMock()
+        self.session = mock.MagicMock()
+        self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start()
+        self.error_format = """Usage: %s add-external-repo [options] <name> [<url>]
+(Specify the --help global option for a list of other help options)
+
+%s: error: {message}
+""" % (self.progname, self.progname)
+        self.name = 'test-repo'
+        self.url = 'https://path/to/ext/repo'
+        self.rinfo = {'id': 1, 'name': self.name, 'url': self.url}
+        self.tag = 'test-tag'
+        self.priority = 10
+
+    def test_add_external_repo_invalid_mode(self):
+        mode = 'test-mode'
+        arguments = ['--mode', mode]
+        self.assert_system_exit(
+            handle_add_external_repo,
+            self.options, self.session, arguments,
+            stdout='',
+            stderr=self.format_error_message('Invalid mode: %s' % mode),
+            exit_code=2,
+            activate_session=None)
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+
+    def test_add_external_repo_mode_without_tag(self):
+        arguments = ['--mode', 'koji']
+        self.assert_system_exit(
+            handle_add_external_repo,
+            self.options, self.session, arguments,
+            stdout='',
+            stderr=self.format_error_message('The --mode option can only be used with --tag'),
+            exit_code=2,
+            activate_session=None)
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+
+    def test_add_external_repo_one_arg_without_tag(self):
+        repo_info = [
+            {'id': 1,
+             'name': 'test-ext-repo',
+             'url': 'https://path/to/ext/repo'},
+        ]
+        name = 'test-ext-repo'
+        self.session.getExternalRepo.return_value = repo_info
+        arguments = [name]
+        self.assert_system_exit(
+            handle_add_external_repo,
+            self.options, self.session, arguments,
+            stdout='',
+            stderr=self.format_error_message('A url is required to create an external repo entry'),
+            exit_code=2,
+            activate_session=None)
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+        self.session.getExternalRepo.assert_called_once_with(name, strict=True)
+
+    def test_add_external_repo_incorect_num_of_args(self):
+        arguments = []
+        self.assert_system_exit(
+            handle_add_external_repo,
+            self.options, self.session, arguments,
+            stdout='',
+            stderr=self.format_error_message('Incorrect number of arguments'),
+            exit_code=2,
+            activate_session=None)
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+
+    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    def test_add_external_repo_valid(self, stdout):
+        self.session.createExternalRepo.return_value = self.rinfo
+        self.session.addExternalRepoToTag.return_value = None
+
+        handle_add_external_repo(self.options, self.session,
+                                 [self.name, self.url, '--tag',
+                                  '%s::%s' % (self.tag, str(self.priority))])
+        actual = stdout.getvalue()
+        expected = 'Created external repo %i\nAdded external repo %s to tag %s (priority %i)\n' \
+                   % (self.rinfo['id'], self.rinfo['name'], self.tag, self.priority)
+        self.assertMultiLineEqual(actual, expected)
+
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+        self.session.createExternalRepo.assert_called_once_with(self.name, self.url)
+
+    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    def test_add_external_repo_without_priority(self, stdout):
+        self.session.createExternalRepo.return_value = self.rinfo
+        self.session.addExternalRepoToTag.return_value = None
+        self.session.getTagExternalRepos.return_value = None
+
+        handle_add_external_repo(self.options, self.session,
+                                 [self.name, self.url, '--tag', self.tag, '--mode=koji',
+                                  '--arches=arch'])
+        actual = stdout.getvalue()
+        expected = 'Created external repo %i\nAdded external repo %s to tag %s (priority 5)\n' \
+                   % (self.rinfo['id'], self.rinfo['name'], self.tag)
+        self.assertMultiLineEqual(actual, expected)
+
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+        self.session.createExternalRepo.assert_called_once_with(self.name, self.url)
+
+    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    def test_add_external_repo_with_option_priority(self, stdout):
+        self.session.createExternalRepo.return_value = self.rinfo
+        self.session.addExternalRepoToTag.return_value = None
+        self.session.getTagExternalRepos.return_value = None
+        priority = 3
+
+        handle_add_external_repo(self.options, self.session,
+                                 [self.name, self.url, '--tag', self.tag, '--mode=koji',
+                                  '--arches=arch', '--priority', str(priority)])
+        actual = stdout.getvalue()
+        expected = 'Created external repo %i\nAdded external repo %s to tag %s (priority %i)\n' \
+                   % (self.rinfo['id'], self.rinfo['name'], self.tag, priority)
+        self.assertMultiLineEqual(actual, expected)
+
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+        self.session.createExternalRepo.assert_called_once_with(self.name, self.url)
+
+    def test_handle_add_external_repo_help(self):
+        self.assert_help(
+            handle_add_external_repo,
+            """Usage: %s add-external-repo [options] <name> [<url>]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help            show this help message and exit
+  -t TAG, --tag=TAG     Also add repo to tag. Use tag::N to set priority
+  -p PRIORITY, --priority=PRIORITY
+                        Set priority (when adding to tag)
+  -m MODE, --mode=MODE  Set merge mode
+  -a ARCH1,ARCH2, ..., --arches=ARCH1,ARCH2, ...
+                        Use only subset of arches from given repo
+""" % self.progname)
diff --git a/tests/test_cli/test_add_group.py b/tests/test_cli/test_add_group.py
index 2e6a82a..396f172 100644
--- a/tests/test_cli/test_add_group.py
+++ b/tests/test_cli/test_add_group.py
@@ -78,7 +78,7 @@ class TestAddGroup(utils.CliTestCase):
         session.getTagGroups.assert_called_once_with(tag, inherit=False)
         session.groupListAdd.assert_not_called()
 
-    def test_handle_add_group_help(self):
+    def test_handle_add_group_no_args(self):
         arguments = []
         options = mock.MagicMock()
 
@@ -155,6 +155,16 @@ class TestAddGroup(utils.CliTestCase):
         session.getTagGroups.assert_not_called()
         session.groupListAdd.assert_not_called()
 
+    def test_handle_add_group_help(self):
+        self.assert_help(
+            handle_add_group,
+            """Usage: %s add-group <tag> <group>
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help  show this help message and exit
+""" % self.progname)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_cli/test_add_host_to_channel.py b/tests/test_cli/test_add_host_to_channel.py
index 1f47aa0..eb4cb2a 100644
--- a/tests/test_cli/test_add_host_to_channel.py
+++ b/tests/test_cli/test_add_host_to_channel.py
@@ -160,7 +160,7 @@ class TestAddHostToChannel(utils.CliTestCase):
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
-    def test_handle_add_host_to_channel_help(
+    def test_handle_add_host_to_channel_no_args(
             self, activate_session_mock, stderr, stdout):
         arguments = []
         options = mock.MagicMock()
@@ -186,6 +186,18 @@ class TestAddHostToChannel(utils.CliTestCase):
         session.listChannels.assert_not_called()
         session.addHostToChannel.assert_not_called()
 
+    def test_handle_add_host_to_channel_help(self):
+        self.assert_help(
+            handle_add_host_to_channel,
+            """Usage: %s add-host-to-channel [options] <hostname> <channel>
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help  show this help message and exit
+  --new       Create channel if needed
+  --force     force added, if possible
+""" % self.progname)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_cli/test_add_notification.py b/tests/test_cli/test_add_notification.py
index 212c97d..ff3c5ee 100644
--- a/tests/test_cli/test_add_notification.py
+++ b/tests/test_cli/test_add_notification.py
@@ -161,3 +161,17 @@ class TestAddNotification(utils.CliTestCase):
             exit_code=2,
             activate_session=None)
         self.activate_session_mock.assert_called_once_with(self.session, self.options)
+
+    def test_add_notification_help(self):
+        self.assert_help(
+            handle_add_notification,
+            """Usage: %s add-notification [options]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help         show this help message and exit
+  --user=USER        Add notifications for this user (admin-only)
+  --package=PACKAGE  Add notifications for this package
+  --tag=TAG          Add notifications for this tag
+  --success-only     Enabled notification on successful events only
+""" % self.progname)
diff --git a/tests/test_cli/test_add_pkg.py b/tests/test_cli/test_add_pkg.py
index a220e9f..b876fa1 100644
--- a/tests/test_cli/test_add_pkg.py
+++ b/tests/test_cli/test_add_pkg.py
@@ -230,6 +230,20 @@ class TestAddPkg(utils.CliTestCase):
         self.session.multiCall.assert_called_once_with(strict=True)
         self.assertFalse(rv)
 
+    def test_handle_add_pkg_help(self):
+        self.assert_help(
+            handle_add_pkg,
+            """Usage: %s add-pkg [options] --owner <owner> <tag> <package> [<package> ...]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help            show this help message and exit
+  --force               Override blocks if necessary
+  --owner=OWNER         Specify owner
+  --extra-arches=EXTRA_ARCHES
+                        Specify extra arches
+""" % self.progname)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_cli/test_add_tag_inheritance.py b/tests/test_cli/test_add_tag_inheritance.py
index 2bb5770..fe5cd32 100644
--- a/tests/test_cli/test_add_tag_inheritance.py
+++ b/tests/test_cli/test_add_tag_inheritance.py
@@ -158,3 +158,21 @@ class TestAddTagInheritance(utils.CliTestCase):
         self.assert_console_message(stdout, '')
         self.assert_console_message(stderr, '')
         self.activate_session_mock.assert_called_once_with(self.session, self.options)
+
+    def test_add_tag_inheritance_help(self):
+        self.assert_help(
+            handle_add_tag_inheritance,
+            """Usage: %s add-tag-inheritance [options] <tag> <parent-tag>
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help            show this help message and exit
+  --priority=PRIORITY   Specify priority
+  --maxdepth=MAXDEPTH   Specify max depth
+  --intransitive        Set intransitive
+  --noconfig            Set to packages only
+  --pkg-filter=PKG_FILTER
+                        Specify the package filter
+  --force               Force adding a parent to a tag that already has that
+                        parent tag
+""" % self.progname)
diff --git a/tests/test_cli/test_assign_task.py b/tests/test_cli/test_assign_task.py
index 53c69bc..ed38f48 100644
--- a/tests/test_cli/test_assign_task.py
+++ b/tests/test_cli/test_assign_task.py
@@ -79,7 +79,7 @@ class TestAssignTask(utils.CliTestCase):
         self.activate_session_mock.assert_called_with(self.session, self.options)
         self.session.assignTask.assert_called_with(int(task_id), hostname, True)
 
-    def test_handle_assign_task_help(self):
+    def test_handle_assign_task_no_args(self):
         arguments = []
         # Run it and check immediate output
         self.assert_system_exit(
@@ -96,6 +96,17 @@ class TestAssignTask(utils.CliTestCase):
         self.session.hasHost.assert_not_called()
         self.session.addHost.assert_not_called()
 
+    def test_assign_task_help(self):
+        self.assert_help(
+            handle_assign_task,
+            """Usage: %s assign-task <task_id> <hostname>
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help   show this help message and exit
+  -f, --force  force to assign a non-free task
+""" % self.progname)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_cli/test_block_group.py b/tests/test_cli/test_block_group.py
index e3583a5..51345a5 100644
--- a/tests/test_cli/test_block_group.py
+++ b/tests/test_cli/test_block_group.py
@@ -118,3 +118,13 @@ class TestBlockGroup(utils.CliTestCase):
             exit_code=2,
             activate_session=None)
         self.activate_session_mock.assert_called_with(self.session, self.options)
+
+    def test_handle_block_group_help(self):
+        self.assert_help(
+            handle_block_group,
+            """Usage: %s block-group <tag> <group>
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help  show this help message and exit
+""" % self.progname)
diff --git a/tests/test_cli/test_block_notification.py b/tests/test_cli/test_block_notification.py
index 6d93145..a1107a5 100644
--- a/tests/test_cli/test_block_notification.py
+++ b/tests/test_cli/test_block_notification.py
@@ -129,3 +129,17 @@ class TestBlockNotification(utils.CliTestCase):
             activate_session=None,
             exit_code=2)
         self.activate_session_mock.assert_called_once_with(self.session, self.options)
+
+    def test_block_notification_help(self):
+        self.assert_help(
+            handle_block_notification,
+            """Usage: %s block-notification [options]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help         show this help message and exit
+  --user=USER        Block notifications for this user (admin-only)
+  --package=PACKAGE  Block notifications for this package
+  --tag=TAG          Block notifications for this tag
+  --all              Block all notification for this user
+""" % self.progname)
diff --git a/tests/test_cli/test_block_pkg.py b/tests/test_cli/test_block_pkg.py
index 74428c9..f637adb 100644
--- a/tests/test_cli/test_block_pkg.py
+++ b/tests/test_cli/test_block_pkg.py
@@ -183,3 +183,14 @@ class TestBlockPkg(utils.CliTestCase):
         self.session.getTag.assert_not_called()
         self.session.listPackages.assert_not_called()
         self.session.packageListBlock.assert_not_called()
+
+    def test_handle_block_pkg_help(self):
+        self.assert_help(
+            handle_block_pkg,
+            """Usage: %s block-pkg [options] <tag> <package> [<package> ...]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help  show this help message and exit
+  --force     Override blocks and owner if necessary
+""" % self.progname)
diff --git a/tests/test_cli/test_build.py b/tests/test_cli/test_build.py
index ef9918c..fe53995 100644
--- a/tests/test_cli/test_build.py
+++ b/tests/test_cli/test_build.py
@@ -141,8 +141,9 @@ Task info: weburl/taskinfo?taskID=1
         self.watch_tasks_mock.assert_not_called()
 
     def test_handle_build_help(self):
-        arguments = ['--help']
-        expected_stdout = """Usage: %s build [options] <target> <srpm path or scm url>
+        self.assert_help(
+            handle_build,
+            """Usage: %s build [options] <target> <srpm path or scm url>
 
 The first option is the build target, not to be confused with the destination
 tag (where the build eventually lands) or build tag (where the buildroot
@@ -175,16 +176,7 @@ Options:
                         Provide a JSON string of custom metadata to be
                         deserialized and stored under the build's
                         extra.custom_user_metadata field
-""" % (self.progname, self.progname)
-
-        # Run it and check immediate output
-        self.assert_system_exit(
-            handle_build,
-            self.options, self.session, arguments,
-            stderr='',
-            stdout=expected_stdout,
-            activate_session=None,
-            exit_code=0)
+""" % (self.progname, self.progname))
 
         # Finally, assert that things were called as we expected.
         self.activate_session_mock.assert_not_called()
diff --git a/tests/test_cli/test_buildinfo.py b/tests/test_cli/test_buildinfo.py
index 1116c05..e33b7c2 100644
--- a/tests/test_cli/test_buildinfo.py
+++ b/tests/test_cli/test_buildinfo.py
@@ -204,3 +204,14 @@ Changelog:
         self.session.getWinBuild.assert_called_once_with(self.buildinfo['id'])
         self.session.listRPMs.assert_called_once_with(buildID=self.buildinfo['id'])
         self.assertEqual(self.session.listArchives.call_count, 4)
+
+    def test_buildinfo_help(self):
+        self.assert_help(
+            anon_handle_buildinfo,
+            """Usage: %s buildinfo [options] <n-v-r> [<n-v-r> ...]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help   show this help message and exit
+  --changelog  Show the changelog for the build
+""" % self.progname)
diff --git a/tests/test_cli/test_cancel.py b/tests/test_cli/test_cancel.py
index 429ca74..161c8df 100644
--- a/tests/test_cli/test_cancel.py
+++ b/tests/test_cli/test_cancel.py
@@ -123,3 +123,16 @@ class TestCancel(utils.CliTestCase):
         self.session.cancelTask.assert_not_called()
         self.session.cancelTaskFull.assert_not_called()
         self.session.cancelBuild.assert_not_called()
+
+    def test_cancel_help(self):
+        self.assert_help(
+            handle_cancel,
+            """Usage: %s cancel [options] <task_id|build> [<task_id|build> ...]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help  show this help message and exit
+  --justone   Do not cancel subtasks
+  --full      Full cancellation (admin only)
+  --force     Allow subtasks with --full
+""" % self.progname)
diff --git a/tests/test_cli/test_chain_build.py b/tests/test_cli/test_chain_build.py
index e0510f6..8238366 100644
--- a/tests/test_cli/test_chain_build.py
+++ b/tests/test_cli/test_chain_build.py
@@ -110,8 +110,9 @@ Task info: weburl/taskinfo?taskID=1
         self.watch_tasks_mock.assert_not_called()
 
     def test_handle_chain_build_help(self):
-        arguments = ['--help']
-        expected_stdout = """Usage: %s chain-build [options] <target> <URL> [<URL> [:] <URL> [:] <URL> ...]
+        self.assert_help(
+            handle_chain_build,
+            """Usage: %s chain-build [options] <target> <URL> [<URL> [:] <URL> [:] <URL> ...]
 (Specify the --help global option for a list of other help options)
 
 Options:
@@ -120,16 +121,7 @@ Options:
   --nowait      Don't wait on build
   --quiet       Do not print the task information
   --background  Run the build at a lower priority
-""" % self.progname
-
-        # Run it and check immediate output
-        self.assert_system_exit(
-            handle_chain_build,
-            self.options, self.session, arguments,
-            stderr='',
-            stdout=expected_stdout,
-            activate_session=None,
-            exit_code=0)
+""" % self.progname)
 
         # Finally, assert that things were called as we expected.
         self.activate_session_mock.assert_not_called()
diff --git a/tests/test_cli/test_download_logs.py b/tests/test_cli/test_download_logs.py
index c25e583..ca7bb64 100644
--- a/tests/test_cli/test_download_logs.py
+++ b/tests/test_cli/test_download_logs.py
@@ -17,6 +17,7 @@ class TestDownloadLogs(utils.CliTestCase):
         # Mock out the options parsed in main
         self.options = mock.MagicMock()
         self.options.quiet = None
+        self.maxDiff = None
         self.options.topurl = 'https://topurl'
         # Mock out the xmlrpc server
         self.session = mock.MagicMock()
@@ -173,3 +174,26 @@ Note this command only downloads task logs, not build logs.
         self.session.getTaskInfo.assert_called_once_with(self.task_id)
         self.session.downloadTaskOutput.assert_not_called()
         self.session.getTaskChildren.assert_not_called()
+
+    def test_download_logs_help(self):
+        self.assert_help(
+            anon_handle_download_logs,
+            """Usage: %s download-logs [options] <task_id> [<task_id> ...]
+       %s download-logs [options] --nvr <n-v-r> [<n-v-r> ...]
+
+Note this command only downloads task logs, not build logs.
+
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help            show this help message and exit
+  -r, --recurse         Process children of this task as well
+  --nvr                 Get the logs for the task associated with this build
+                        Name-Version-Release.
+  -m PATTERN, --match=PATTERN
+                        Get only log filenames matching PATTERN (fnmatch). May
+                        be used multiple times.
+  -c, --continue        Continue previous download
+  -d DIRECTORY, --dir=DIRECTORY
+                        Write logs to DIRECTORY
+""" % (self.progname, self.progname))
diff --git a/tests/test_cli/test_download_task.py b/tests/test_cli/test_download_task.py
index 5bdd12e..b6138fa 100644
--- a/tests/test_cli/test_download_task.py
+++ b/tests/test_cli/test_download_task.py
@@ -409,16 +409,10 @@ Default behavior without --all option downloads .rpm files only for build and bu
                                                                   self.parent_task_id)
         self.download_file.assert_not_called()
 
-    def test_handle_download_help(self):
-        args = ['--help']
-        # Run it and check immediate output
-        # args: --help
-        # expected: failure
-        with self.assertRaises(SystemExit) as ex:
-            anon_handle_download_task(self.options, self.session, args)
-        self.assertExitCode(ex, 0)
-        actual = self.stdout.getvalue()
-        expected = """Usage: %s download-task <task_id>
+    def test_handle_download_task_help(self):
+        self.assert_help(
+            anon_handle_download_task,
+            """Usage: %s download-task <task_id>
 Default behavior without --all option downloads .rpm files only for build and buildArch tasks.
 
 (Specify the --help global option for a list of other help options)
@@ -440,11 +434,7 @@ Options:
   --parentonly     Download parent's files only
   --filter=FILTER  Regex pattern to filter files
   --skip=SKIP      Regex pattern to skip files
-""" % progname
-        self.assertMultiLineEqual(actual, expected)
-        actual = self.stderr.getvalue()
-        expected = ''
-        self.assertEqual(actual, expected)
+""" % self.progname)
 
     def test_handle_download_no_task_id(self):
         args = []
diff --git a/tests/test_cli/test_edit_host.py b/tests/test_cli/test_edit_host.py
index ce98e9c..b7cca31 100644
--- a/tests/test_cli/test_edit_host.py
+++ b/tests/test_cli/test_edit_host.py
@@ -180,6 +180,21 @@ No changes made, please correct the command line
         self.session.editHost.assert_not_called()
         self.assertEqual(self.session.multiCall.call_count, 1)
 
+    def test_handle_edit_host_help(self):
+        self.assert_help(
+            handle_edit_host,
+            """Usage: %s edit-host <hostname> [<hostname> ...] [options]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help           show this help message and exit
+  --arches=ARCHES      Space or comma-separated list of supported
+                       architectures
+  --capacity=CAPACITY  Capacity of this host
+  --description=DESC   Description of this host
+  --comment=COMMENT    A brief comment about this host
+""" % self.progname)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_cli/test_edit_notification.py b/tests/test_cli/test_edit_notification.py
index 21b8cc0..889335d 100644
--- a/tests/test_cli/test_edit_notification.py
+++ b/tests/test_cli/test_edit_notification.py
@@ -159,3 +159,17 @@ class TestEditNotification(utils.CliTestCase):
         self.session.getBuildNotification.assert_called_once_with(2345)
         self.session.updateNotification.assert_not_called()
         self.activate_session_mock.assert_called_once_with(self.session, self.options)
+
+    def test_edit_notification_help(self):
+        self.assert_help(
+            handle_edit_notification,
+            """Usage: %s edit-notification [options] <notification_id>
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help         show this help message and exit
+  --package=PACKAGE  Notifications for this package, '*' for all
+  --tag=TAG          Notifications for this tag, '*' for all
+  --success-only     Notify only on successful events
+  --no-success-only  Notify on all events
+""" % self.progname)
diff --git a/tests/test_cli/test_edit_permission.py b/tests/test_cli/test_edit_permission.py
index 82c0e8e..0d9a104 100644
--- a/tests/test_cli/test_edit_permission.py
+++ b/tests/test_cli/test_edit_permission.py
@@ -47,3 +47,13 @@ class TestEditPermission(utils.CliTestCase):
         self.assertMultiLineEqual(actual, expected)
         self.session.editPermission.assert_called_once_with(self.perm, self.description)
         self.activate_session_mock.assert_called_once_with(self.session, self.options)
+
+    def test_edit_permission_help(self):
+        self.assert_help(
+            handle_edit_permission,
+            """Usage: %s edit-permission <permission> <description>
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help  show this help message and exit
+""" % self.progname)
diff --git a/tests/test_cli/test_edit_tag_inheritance.py b/tests/test_cli/test_edit_tag_inheritance.py
index c2e345f..fe31adb 100644
--- a/tests/test_cli/test_edit_tag_inheritance.py
+++ b/tests/test_cli/test_edit_tag_inheritance.py
@@ -267,3 +267,19 @@ class TestEditTagInheritance(utils.CliTestCase):
         self.session.getTag.assert_called_once_with(self.parent_tag)
         self.session.getInheritanceData.assert_has_calls([call(1), call(1)])
         self.session.setInheritanceData.assert_not_called()
+
+    def test_edit_tag_inheritance_help(self):
+        self.assert_help(
+            handle_edit_tag_inheritance,
+            """Usage: %s edit-tag-inheritance [options] <tag> <parent> <priority>
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help            show this help message and exit
+  --priority=PRIORITY   Specify a new priority
+  --maxdepth=MAXDEPTH   Specify max depth
+  --intransitive        Set intransitive
+  --noconfig            Set to packages only
+  --pkg-filter=PKG_FILTER
+                        Specify the package filter
+""" % self.progname)
diff --git a/tests/test_cli/test_edit_target.py b/tests/test_cli/test_edit_target.py
index d3d5557..34a449d 100644
--- a/tests/test_cli/test_edit_target.py
+++ b/tests/test_cli/test_edit_target.py
@@ -190,3 +190,17 @@ class TestEditTarget(utils.CliTestCase):
             self.build_target_info['orig_name'], self.build_target_info['name'],
             self.build_target_info['build_tag_name'], self.build_target_info['dest_tag_name'])
         self.activate_session_mock.assert_called_with(self.session, self.options)
+
+    def test_edit_target_help(self):
+        self.assert_help(
+            handle_edit_target,
+            """Usage: %s edit-target [options] <name>
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help            show this help message and exit
+  --rename=RENAME       Specify new name for target
+  --build-tag=BUILD_TAG
+                        Specify a different build tag
+  --dest-tag=DEST_TAG   Specify a different destination tag
+""" % self.progname)
diff --git a/tests/test_cli/test_hostinfo.py b/tests/test_cli/test_hostinfo.py
index ea81f16..7f324a3 100644
--- a/tests/test_cli/test_hostinfo.py
+++ b/tests/test_cli/test_hostinfo.py
@@ -166,3 +166,13 @@ None
         self.session.listChannels.assert_called_once_with(hostID=self.hostinfo['id'])
         self.ensure_connection_mock.assert_called_once_with(self.session, self.options)
         self.assertEqual(self.session.listBuildroots.call_count, 3)
+
+    def test_hostinfo_help(self):
+        self.assert_help(
+            anon_handle_hostinfo,
+            """Usage: %s hostinfo [options] <hostname> [<hostname> ...]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help  show this help message and exit
+""" % self.progname)
diff --git a/tests/test_cli/test_import_archive.py b/tests/test_cli/test_import_archive.py
index 9887df2..dbdfe6b 100644
--- a/tests/test_cli/test_import_archive.py
+++ b/tests/test_cli/test_import_archive.py
@@ -61,7 +61,6 @@ class TestImportArchive(utils.CliTestCase):
         self.session.uploadWrapper.assert_not_called()
         self.session.importArchive.assert_not_called()
 
-
     def test_import_archive_without_type(self):
         self.assert_system_exit(
             handle_import_archive,
@@ -217,3 +216,23 @@ class TestImportArchive(utils.CliTestCase):
         self.session.createImageBuild.assert_not_called()
         self.session.uploadWrapper.assert_not_called()
         self.session.importArchive.assert_not_called()
+
+    def test_import_archive_help(self):
+        self.assert_help(
+            handle_import_archive,
+            """Usage: %s import-archive <build-id|n-v-r> <archive_path> [<archive_path2 ...]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help            show this help message and exit
+  --noprogress          Do not display progress of the upload
+  --create-build        Auto-create builds as needed
+  --link                Attempt to hardlink instead of uploading
+  --type=TYPE           The type of archive being imported. Currently
+                        supported types: maven, win, image
+  --type-info=TYPE_INFO
+                        Type-specific information to associate with the
+                        archives. For Maven archives this should be a local
+                        path to a .pom file. For Windows archives this should
+                        be relpath:platforms[:flags])) Images need an arch
+""" % self.progname)
diff --git a/tests/test_cli/test_import_comps.py b/tests/test_cli/test_import_comps.py
index d28d6d2..466b0fd 100644
--- a/tests/test_cli/test_import_comps.py
+++ b/tests/test_cli/test_import_comps.py
@@ -278,6 +278,17 @@ class TestImportComps(utils.CliTestCase):
             self.session.assert_has_calls(expected)
         self.assertNotEqual(rv, 1)
 
+    def test_import_comps_help(self):
+        self.assert_help(
+            handle_import_comps,
+            """Usage: %s import-comps [options] <file> <tag>
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help  show this help message and exit
+  --force     force import
+""" % self.progname)
+
 
 def _generate_out_calls(method, comps_file, stdout_file, calls_file):
     tag = 'tag'
diff --git a/tests/test_cli/test_list_builds.py b/tests/test_cli/test_list_builds.py
index f1fa3c6..6e67c88 100644
--- a/tests/test_cli/test_list_builds.py
+++ b/tests/test_cli/test_list_builds.py
@@ -528,3 +528,37 @@ test-build-10-12                                         kojitest          CANCE
         self.session.listVolumes.assert_not_called()
         self.session.getBuild.assert_not_called()
         self.session.listBuilds.assert_called_once_with(cgID='test-cg')
+
+    def test_list_builds_help(self):
+        self.assert_help(
+            anon_handle_list_builds,
+            """Usage: %s list-builds [options]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help            show this help message and exit
+  --package=PACKAGE     List builds for this package
+  --buildid=BUILDID     List specific build from ID or nvr
+  --before=BEFORE       List builds built before this time, time is specified
+                        as timestamp or date/time in any format which can be
+                        parsed by dateutil.parser. e.g. "2020-12-31 12:35" or
+                        "December 31st 12:35"
+  --after=AFTER         List builds built after this time (same format as for
+                        --before
+  --state=STATE         List builds in this state
+  --task=TASK           List builds for this task
+  --type=TYPE           List builds of this type.
+  --prefix=PREFIX       Only builds starting with this prefix
+  --pattern=PATTERN     Only list builds matching this GLOB pattern
+  --cg=CG               Only list builds imported by matching content
+                        generator name
+  --source=SOURCE       Only builds where the source field matches (glob
+                        pattern)
+  --owner=OWNER         List builds built by this owner
+  --volume=VOLUME       List builds by volume ID
+  -k FIELD, --sort-key=FIELD
+                        Sort the list by the named field. Allowed sort keys:
+                        build_id, owner_name, state
+  -r, --reverse         Print the list in reverse order
+  --quiet               Do not print the header information
+""" % self.progname)
diff --git a/tests/test_cli/test_list_external_repos.py b/tests/test_cli/test_list_external_repos.py
new file mode 100644
index 0000000..c576053
--- /dev/null
+++ b/tests/test_cli/test_list_external_repos.py
@@ -0,0 +1,52 @@
+from __future__ import absolute_import
+
+import mock
+
+from koji_cli.commands import anon_handle_list_external_repos
+from . import utils
+
+
+class TestListExternalRepo(utils.CliTestCase):
+
+    def setUp(self):
+        self.maxDiff = None
+        self.options = mock.MagicMock()
+        self.session = mock.MagicMock()
+        self.ensure_connection_mock = mock.patch('koji_cli.commands.ensure_connection').start()
+        self.error_format = """Usage: %s list-external-repos [options]
+(Specify the --help global option for a list of other help options)
+
+%s: error: {message}
+""" % (self.progname, self.progname)
+
+    def test_list_external_repos_with_args(self):
+        arguments = ['arg']
+        self.assert_system_exit(
+            anon_handle_list_external_repos,
+            self.options, self.session, arguments,
+            stdout='',
+            stderr=self.format_error_message('This command takes no arguments'),
+            exit_code=2,
+            activate_session=None)
+        self.ensure_connection_mock.assert_not_called()
+
+    def test_list_external_repos_help(self):
+        self.assert_help(
+            anon_handle_list_external_repos,
+            """Usage: %s list-external-repos [options]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help      show this help message and exit
+  --url=URL       Select by url
+  --name=NAME     Select by name
+  --id=ID         Select by id
+  --tag=TAG       Select by tag
+  --used          List which tags use the repo(s)
+  --inherit       Follow tag inheritance when selecting by tag
+  --event=EVENT#  Query at event
+  --ts=TIMESTAMP  Query at last event before timestamp
+  --repo=REPO#    Query at event corresponding to (nonexternal) repo
+  --quiet         Do not display the column headers
+""" % self.progname)
+        self.ensure_connection_mock.assert_not_called()
diff --git a/tests/test_cli/test_list_hosts.py b/tests/test_cli/test_list_hosts.py
index 4019634..a6c0aef 100644
--- a/tests/test_cli/test_list_hosts.py
+++ b/tests/test_cli/test_list_hosts.py
@@ -283,3 +283,24 @@ kojibuilder N   Y    0.0/2.0  x86_64           Tue, 16 Mar 2021 06:19:14 UTC    
         self.session.getLastHostUpdate.assert_called_once_with(self.list_hosts[0]['id'], ts=True)
         self.ensure_connection_mock.assert_called_once_with(self.session, self.options)
         self.session.listChannels.assert_not_called()
+
+    def test_list_hosts_help(self):
+        self.assert_help(
+            anon_handle_list_hosts,
+            """Usage: %s list-hosts [options]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help         show this help message and exit
+  --arch=ARCH        Specify an architecture
+  --channel=CHANNEL  Specify a channel
+  --ready            Limit to ready hosts
+  --not-ready        Limit to not ready hosts
+  --enabled          Limit to enabled hosts
+  --not-enabled      Limit to not enabled hosts
+  --disabled         Alias for --not-enabled
+  --quiet            Do not print header information
+  --show-channels    Show host's channels
+  --comment          Show comments
+  --description      Show descriptions
+""" % self.progname)
diff --git a/tests/test_cli/test_list_notifications.py b/tests/test_cli/test_list_notifications.py
index b69f68f..5e7869d 100644
--- a/tests/test_cli/test_list_notifications.py
+++ b/tests/test_cli/test_list_notifications.py
@@ -178,3 +178,15 @@ Notification blocks
         self.session.getBuildNotifications.assert_called_once_with(321)
         self.ensure_connection_mock.assert_called_once_with(self.session, self.options)
         self.activate_session_mock.asset_not_called()
+
+    def test_list_notifications_help(self):
+        self.assert_help(
+            anon_handle_list_notifications,
+            """Usage: %s list-notifications [options]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help   show this help message and exit
+  --mine       Just print your notifications
+  --user=USER  Only notifications for this user
+""" % self.progname)
diff --git a/tests/test_cli/test_list_pkgs.py b/tests/test_cli/test_list_pkgs.py
index adfa7ba..307c783 100644
--- a/tests/test_cli/test_list_pkgs.py
+++ b/tests/test_cli/test_list_pkgs.py
@@ -275,3 +275,23 @@ test-pkg-2              test-tag-2              x86_64           usertest       
         self.session.getTag.assert_not_called()
         self.session.listPackages.assert_not_called()
         self.ensure_connection_mock.assert_called_once_with(self.session, self.options)
+
+    def test_list_pkgs_help(self):
+        self.assert_help(
+            anon_handle_list_pkgs,
+            """Usage: %s list-pkgs [options]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help         show this help message and exit
+  --owner=OWNER      Specify owner
+  --tag=TAG          Specify tag
+  --package=PACKAGE  Specify package
+  --quiet            Do not print header information
+  --noinherit        Don't follow inheritance
+  --show-blocked     Show blocked packages
+  --show-dups        Show superseded owners
+  --event=EVENT#     query at event
+  --ts=TIMESTAMP     query at last event before timestamp
+  --repo=REPO#       query at event for a repo
+""" % self.progname)
diff --git a/tests/test_cli/test_list_tags.py b/tests/test_cli/test_list_tags.py
index 9b48f9e..023feb3 100644
--- a/tests/test_cli/test_list_tags.py
+++ b/tests/test_cli/test_list_tags.py
@@ -98,3 +98,18 @@ class TestListTags(utils.CliTestCase):
         self.assertEqual(rv, None)
         self.session.listTags.assert_called_once_with(build=None, package=None)
         self.ensure_connection.assert_called_once_with(self.session, self.options)
+
+    def test_list_tags_help(self):
+        self.assert_help(
+            anon_handle_list_tags,
+            """Usage: %s list-tags [options] [pattern]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help         show this help message and exit
+  --show-id          Show tag ids
+  --verbose          Show more information
+  --unlocked         Only show unlocked tags
+  --build=BUILD      Show tags associated with a build
+  --package=PACKAGE  Show tags associated with a package
+""" % self.progname)
diff --git a/tests/test_cli/test_list_targets.py b/tests/test_cli/test_list_targets.py
index be8a0bc..461d9d6 100644
--- a/tests/test_cli/test_list_targets.py
+++ b/tests/test_cli/test_list_targets.py
@@ -101,3 +101,15 @@ class TestCliListTargets(utils.CliTestCase):
             ''
         ]
         self.assertEqual(expected, [re.sub(' +', '|', l) for l in stdout.getvalue().split('\n')])
+
+    def test_list_targets_help(self):
+        self.assert_help(
+            anon_handle_list_targets,
+            """Usage: %s list-targets [options]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help   show this help message and exit
+  --name=NAME  Specify the build target name
+  --quiet      Do not print the header information
+""" % self.progname)
diff --git a/tests/test_cli/test_maven_build.py b/tests/test_cli/test_maven_build.py
index 840b35f..0e54ebb 100644
--- a/tests/test_cli/test_maven_build.py
+++ b/tests/test_cli/test_maven_build.py
@@ -165,28 +165,14 @@ Task info: weburl/taskinfo?taskID=1
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
-    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)
     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)
     def test_handle_maven_build_help(
-            self,
-            watch_tasks_mock,
-            running_in_bg_mock,
-            activate_session_mock,
-            stderr,
-            stdout):
-        args = ['--help']
-        progname = os.path.basename(sys.argv[0]) or 'koji'
-
-        # Run it and check immediate output
-        with self.assertRaises(SystemExit) as ex:
-            handle_maven_build(self.options, self.session, args)
-        self.assertExitCode(ex, 0)
-        actual_stdout = stdout.getvalue()
-        actual_stderr = stderr.getvalue()
-        expected_stdout = """Usage: %s maven-build [options] <target> <URL>
+            self, watch_tasks_mock, running_in_bg_mock, activate_session_mock):
+        self.assert_help(
+            handle_maven_build,
+            """Usage: %s maven-build [options] <target> <URL>
        %s maven-build --ini=CONFIG... [options] <target>
 (Specify the --help global option for a list of other help options)
 
@@ -219,10 +205,7 @@ Options:
   --nowait              Don't wait on build
   --quiet               Do not print the task information
   --background          Run the build at a lower priority
-""" % (progname, progname)
-        expected_stderr = ''
-        self.assertMultiLineEqual(actual_stdout, expected_stdout)
-        self.assertMultiLineEqual(actual_stderr, expected_stderr)
+""" % (self.progname, self.progname))
 
         # Finally, assert that things were called as we expected.
         activate_session_mock.assert_not_called()
@@ -683,12 +666,8 @@ Task info: weburl/taskinfo?taskID=1
     @mock.patch('koji_cli.commands.activate_session')
     @mock.patch('koji_cli.commands._running_in_bg', return_value=True)
     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)
-    def test_handle_maven_build_quiet(
-            self,
-            watch_tasks_mock,
-            running_in_bg_mock,
-            activate_session_mock,
-            stdout):
+    def test_handle_maven_build_without_quiet(
+            self, watch_tasks_mock, running_in_bg_mock, activate_session_mock, stdout):
         target = 'target'
         dest_tag = 'dest_tag'
         dest_tag_id = 2
diff --git a/tests/test_cli/test_maven_chain.py b/tests/test_cli/test_maven_chain.py
index b87078f..18f4e3a 100644
--- a/tests/test_cli/test_maven_chain.py
+++ b/tests/test_cli/test_maven_chain.py
@@ -184,7 +184,7 @@ Options:
   --wait        Wait on build, even if running in the background
   --nowait      Don't wait on build
   --background  Run the build at a lower priority
-""" % (self.progname))
+""" % self.progname)
 
 
 if __name__ == '__main__':
diff --git a/tests/test_cli/test_prune_signed_copies.py b/tests/test_cli/test_prune_signed_copies.py
new file mode 100644
index 0000000..316ab6f
--- /dev/null
+++ b/tests/test_cli/test_prune_signed_copies.py
@@ -0,0 +1,43 @@
+from __future__ import absolute_import
+
+import mock
+
+from koji_cli.commands import handle_prune_signed_copies
+from . import utils
+
+
+class TestPruneSignedCopies(utils.CliTestCase):
+
+    # Show long diffs in error output...
+    maxDiff = None
+
+    def setUp(self):
+        self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start()
+
+    def test_handle_prune_signed_copies_help(self):
+        self.assert_help(
+            handle_prune_signed_copies,
+            """Usage: %s prune-signed-copies [options]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help            show this help message and exit
+  -n, --test            Test mode
+  -v, --verbose         Be more verbose
+  --days=DAYS           Timeout before clearing
+  -p PACKAGE, --package=PACKAGE, --pkg=PACKAGE
+                        Limit to a single package
+  -b BUILD, --build=BUILD
+                        Limit to a single build
+  -i IGNORE_TAG, --ignore-tag=IGNORE_TAG
+                        Ignore these tags when considering whether a build
+                        is/was latest
+  --ignore-tag-file=IGNORE_TAG_FILE
+                        File to read tag ignore patterns from
+  -r PROTECT_TAG, --protect-tag=PROTECT_TAG
+                        Do not prune signed copies from matching tags
+  --protect-tag-file=PROTECT_TAG_FILE
+                        File to read tag protect patterns from
+  --trashcan-tag=TRASHCAN_TAG
+                        Specify trashcan tag
+""" % self.progname)
diff --git a/tests/test_cli/test_remove_group.py b/tests/test_cli/test_remove_group.py
index 2c0947e..c3cacc3 100644
--- a/tests/test_cli/test_remove_group.py
+++ b/tests/test_cli/test_remove_group.py
@@ -93,12 +93,9 @@ class TestRemoveGroup(utils.CliTestCase):
         self.session.groupListRemove.assert_called_once_with(tag, group)
         self.assertEqual(rv, None)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
-    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
-    def test_handle_remove_group_error_handling(self, activate_session_mock, stdout, stderr):
-        expected = self.format_error_message(
-                        "Please specify a tag name and a group name")
+    def test_handle_remove_group_error_handling(self, activate_session_mock):
+        expected = self.format_error_message("Please specify a tag name and a group name")
         for args in [[], ['tag'], ['tag', 'grp', 'etc']]:
             self.assert_system_exit(
                 handle_remove_group,
@@ -106,6 +103,7 @@ class TestRemoveGroup(utils.CliTestCase):
                 self.session,
                 args,
                 stderr=expected,
+                stdout='',
                 activate_session=None)
 
         # if we don't have 'tag' permission
@@ -113,3 +111,13 @@ class TestRemoveGroup(utils.CliTestCase):
         with self.assertRaises(SystemExit):
             handle_remove_group(self.options, self.session, ['tag', 'grp'])
         activate_session_mock.assert_called_with(self.session, self.options)
+
+    def test_handle_remove_group_help(self):
+        self.assert_help(
+            handle_remove_group,
+            """Usage: %s remove-group <tag> <group>
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help  show this help message and exit
+""" % self.progname)
diff --git a/tests/test_cli/test_remove_host_from_channel.py b/tests/test_cli/test_remove_host_from_channel.py
index 05ba32b..76bc8dc 100644
--- a/tests/test_cli/test_remove_host_from_channel.py
+++ b/tests/test_cli/test_remove_host_from_channel.py
@@ -8,6 +8,7 @@ import unittest
 from koji_cli.commands import handle_remove_host_from_channel
 from . import utils
 
+
 class TestRemoveHostFromChannel(utils.CliTestCase):
 
     # Show long diffs in error output...
@@ -107,7 +108,7 @@ class TestRemoveHostFromChannel(utils.CliTestCase):
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
-    def test_handle_remove_host_from_channel_help(
+    def test_handle_remove_host_from_channel_no_args(
             self, activate_session_mock, stderr, stdout):
         args = []
         options = mock.MagicMock()
@@ -139,6 +140,16 @@ class TestRemoveHostFromChannel(utils.CliTestCase):
         session.listChannels.assert_not_called()
         session.removeHostFromChannel.assert_not_called()
 
+    def test_handle_remove_host_from_channel_help(self):
+        self.assert_help(
+            handle_remove_host_from_channel,
+            """Usage: %s remove-host-from-channel [options] <hostname> <channel>
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help  show this help message and exit
+""" % self.progname)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_cli/test_remove_notification.py b/tests/test_cli/test_remove_notification.py
index b07fbd3..bccfb33 100644
--- a/tests/test_cli/test_remove_notification.py
+++ b/tests/test_cli/test_remove_notification.py
@@ -69,3 +69,13 @@ class TestAddHost(utils.CliTestCase):
             handle_remove_notification(self.options, self.session, [])
 
         self.session.deleteNotification.assert_not_called()
+
+    def test_remove_notification_help(self):
+        self.assert_help(
+            handle_remove_notification,
+            """Usage: %s remove-notification [options] <notification_id> [<notification_id> ...]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help  show this help message and exit
+""" % self.progname)
diff --git a/tests/test_cli/test_remove_pkg.py b/tests/test_cli/test_remove_pkg.py
index 5a0f404..04f9076 100644
--- a/tests/test_cli/test_remove_pkg.py
+++ b/tests/test_cli/test_remove_pkg.py
@@ -223,6 +223,17 @@ class TestRemovePkg(utils.CliTestCase):
         self.session.listPackages.assert_not_called()
         self.session.packageListRemove.assert_not_called()
 
+    def test_handle_remove_pkg_help(self):
+        self.assert_help(
+            handle_remove_pkg,
+            """Usage: %s remove-pkg [options] <tag> <package> [<package> ...]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help  show this help message and exit
+  --force     Override blocks if necessary
+""" % self.progname)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_cli/test_remove_tag_inheritance.py b/tests/test_cli/test_remove_tag_inheritance.py
index f97a3ae..f4b88f5 100644
--- a/tests/test_cli/test_remove_tag_inheritance.py
+++ b/tests/test_cli/test_remove_tag_inheritance.py
@@ -208,3 +208,13 @@ class TestRemoveTagInheritance(utils.CliTestCase):
             1, [{'child_id': 1, 'intransitive': False, 'maxdepth': None, 'name': self.tag,
                  'noconfig': False, 'parent_id': 2, 'pkg_filter': '', 'priority': self.priority,
                  'delete link': True}])
+
+    def test_remove_tag_inheritance_help(self):
+        self.assert_help(
+            handle_remove_tag_inheritance,
+            """Usage: %s remove-tag-inheritance <tag> <parent> <priority>
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help  show this help message and exit
+""" % self.progname)
diff --git a/tests/test_cli/test_rpminfo.py b/tests/test_cli/test_rpminfo.py
index c07d7ec..3e2beb3 100644
--- a/tests/test_cli/test_rpminfo.py
+++ b/tests/test_cli/test_rpminfo.py
@@ -163,3 +163,14 @@ Used in 1 buildroots:
         self.session.getBuildroot.assert_not_called()
         self.session.listBuildroots.assert_not_called()
         self.session.getBuild.assert_not_called()
+
+    def test_rpminfo_help(self):
+        self.assert_help(
+            anon_handle_rpminfo,
+            """Usage: %s rpminfo [options] <n-v-r.a> [<n-v-r.a> ...]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help    show this help message and exit
+  --buildroots  show buildroots the rpm was used in
+""" % self.progname)
diff --git a/tests/test_cli/test_show_groups.py b/tests/test_cli/test_show_groups.py
new file mode 100644
index 0000000..fb10a63
--- /dev/null
+++ b/tests/test_cli/test_show_groups.py
@@ -0,0 +1,71 @@
+from __future__ import absolute_import
+
+import mock
+
+from koji_cli.commands import anon_handle_show_groups
+from . import utils
+
+
+class TestShowGroups(utils.CliTestCase):
+
+    def setUp(self):
+        self.maxDiff = None
+        self.options = mock.MagicMock()
+        self.session = mock.MagicMock()
+        self.ensure_connection_mock = mock.patch('koji_cli.commands.ensure_connection').start()
+        self.error_format = """Usage: %s show-groups [options] <tag>
+(Specify the --help global option for a list of other help options)
+
+%s: error: {message}
+""" % (self.progname, self.progname)
+        self.tag = 'test-tag'
+
+    def test_show_groups_incorrect_num_of_args(self):
+        arguments = []
+        self.assert_system_exit(
+            anon_handle_show_groups,
+            self.options, self.session, arguments,
+            stdout='',
+            stderr=self.format_error_message('Incorrect number of arguments'),
+            exit_code=2,
+            activate_session=None)
+        self.ensure_connection_mock.assert_not_called()
+
+    def test_show_groups_show_blocked_and_comps(self):
+        arguments = ['--show-blocked', '--comps', self.tag]
+        self.assert_system_exit(
+            anon_handle_show_groups,
+            self.options, self.session, arguments,
+            stdout='',
+            stderr=self.format_error_message(
+                "--show-blocked doesn't make sense for comps/spec output"),
+            exit_code=2,
+            activate_session=None)
+        self.ensure_connection_mock.assert_not_called()
+
+    def test_show_groups_show_blocked_and_spec(self):
+        arguments = ['--show-blocked', '--spec', self.tag]
+        self.assert_system_exit(
+            anon_handle_show_groups,
+            self.options, self.session, arguments,
+            stdout='',
+            stderr=self.format_error_message(
+                "--show-blocked doesn't make sense for comps/spec output"),
+            exit_code=2,
+            activate_session=None)
+        self.ensure_connection_mock.assert_not_called()
+
+    def test_show_groups_help(self):
+        self.assert_help(
+            anon_handle_show_groups,
+            """Usage: %s show-groups [options] <tag>
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help      show this help message and exit
+  --comps         Print in comps format
+  -x, --expand    Expand groups in comps format
+  --spec          Print build spec
+  --show-blocked  Show blocked packages
+""" % self.progname)
+        self.ensure_connection_mock.assert_not_called()
diff --git a/tests/test_cli/test_taginfo.py b/tests/test_cli/test_taginfo.py
index 1dc6113..aa1ec3e 100644
--- a/tests/test_cli/test_taginfo.py
+++ b/tests/test_cli/test_taginfo.py
@@ -131,3 +131,16 @@ Inheritance:
         self.session.getRepo.assert_has_calls([mock.call(123)], [mock.call(1111)])
         self.session.getTagExternalRepos.assert_called_once_with(tag_info=1111)
         self.session.getInheritanceData.assert_called_once_with(1111)
+
+    def test_taginfo_help(self):
+        self.assert_help(
+            anon_handle_taginfo,
+            """Usage: %s taginfo [options] <tag> [<tag> ...]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help      show this help message and exit
+  --event=EVENT#  query at event
+  --ts=TIMESTAMP  query at last event before timestamp
+  --repo=REPO#    query at event for a repo
+""" % self.progname)
diff --git a/tests/test_cli/test_unblock_notification.py b/tests/test_cli/test_unblock_notification.py
index 8bd3499..fd2c80b 100644
--- a/tests/test_cli/test_unblock_notification.py
+++ b/tests/test_cli/test_unblock_notification.py
@@ -69,3 +69,13 @@ class TestUnblockNotification(utils.CliTestCase):
         actual = stdout.getvalue()
         print(actual)
         self.assertMultiLineEqual(actual, expected)
+
+    def test_unblock_notification_help(self):
+        self.assert_help(
+            handle_unblock_notification,
+            """Usage: %s unblock-notification [options] <notification_id> [<notification_id> ...]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help  show this help message and exit
+""" % self.progname)
diff --git a/tests/test_cli/test_unlock_tag.py b/tests/test_cli/test_unlock_tag.py
index c15b43d..7252c88 100644
--- a/tests/test_cli/test_unlock_tag.py
+++ b/tests/test_cli/test_unlock_tag.py
@@ -103,7 +103,7 @@ class TestUnlockTag(utils.CliTestCase):
         self.assert_console_message(stdout, expected)
         self.session.listTags.assert_called_with()
 
-    def test_lock_tag_help(self):
+    def test_unlock_tag_help(self):
         self.assert_help(
             handle_unlock_tag,
             """Usage: %s unlock-tag [options] <tag> [<tag> ...]
diff --git a/tests/test_cli/test_untag_build.py b/tests/test_cli/test_untag_build.py
index 2fa7fb9..d0cf5cc 100644
--- a/tests/test_cli/test_untag_build.py
+++ b/tests/test_cli/test_untag_build.py
@@ -156,3 +156,20 @@ class TestUntagBuild(utils.CliTestCase):
         self.session.getTag.assert_called_once_with(self.tag)
         self.session.multicall.assert_called_once()
         self.session.untagBuild.assert_not_called()
+
+    def test_untag_build_help(self):
+        self.assert_help(
+            handle_untag_build,
+            """Usage: %s untag-build [options] <tag> <pkg> [<pkg> ...]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help     show this help message and exit
+  --all          untag all versions of the package in this tag, pkg is package
+                 name
+  --non-latest   untag all versions of the package in this tag except the
+                 latest, pkg is package name
+  -n, --test     test mode
+  -v, --verbose  print details
+  --force        force operation
+""" % self.progname)
diff --git a/tests/test_cli/test_userinfo.py b/tests/test_cli/test_userinfo.py
index 5860faf..68ae0df 100644
--- a/tests/test_cli/test_userinfo.py
+++ b/tests/test_cli/test_userinfo.py
@@ -82,3 +82,13 @@ Number of builds: 3
 
         anon_handle_userinfo(self.options, self.session, [self.user])
         self.assert_console_message(stdout, expected)
+
+    def test_userinfo_help(self):
+        self.assert_help(
+            anon_handle_userinfo,
+            """Usage: %s userinfo [options] <username> [<username> ...]
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help  show this help message and exit
+""" % self.progname)
diff --git a/tests/test_cli/test_win_build.py b/tests/test_cli/test_win_build.py
index 9aeac74..fa7f321 100644
--- a/tests/test_cli/test_win_build.py
+++ b/tests/test_cli/test_win_build.py
@@ -17,13 +17,13 @@ class TestWinBuild(utils.CliTestCase):
         self.target = 'test-target'
         self.dest_tag = 'destination-test_tag'
         self.target_info = {'build_tag': 4,
-                       'build_tag_name': 'test_tag',
-                       'dest_tag': 5,
-                       'dest_tag_name': self.dest_tag,
-                       'id': 2,
-                       'name': self.target}
+                            'build_tag_name': 'test_tag',
+                            'dest_tag': 5,
+                            'dest_tag_name': self.dest_tag,
+                            'id': 2,
+                            'name': self.target}
         self.scm_url = 'git://test.redhat.com/rpms/pkg-1.1.0' \
-                    '?#3fab2ea42ecdc30a41daf1306154dfa04c4d64fd'
+                       '?#3fab2ea42ecdc30a41daf1306154dfa04c4d64fd'
         self.vm = 'test-vm'
 
     @mock.patch('sys.stderr', new_callable=StringIO)
@@ -63,11 +63,11 @@ class TestWinBuild(utils.CliTestCase):
         self.assert_console_message(stderr, expected)
 
     @mock.patch('sys.stderr', new_callable=StringIO)
-    def test_handle_build_dest_tag_locked(self, stderr):
+    def test_win_build_dest_tag_locked(self, stderr):
         expected = "Usage: %s win-build [options] <target> <URL> <VM>\n" \
                    "(Specify the --help global option for a list of other help options)\n\n" \
                    "%s: error: Destination tag %s is locked\n" % (self.progname, self.progname,
-                                                                 self.dest_tag)
+                                                                  self.dest_tag)
         dest_tag_info = {'name': self.dest_tag, 'locked': True}
 
         self.session.getBuildTarget.return_value = self.target_info
@@ -76,3 +76,32 @@ class TestWinBuild(utils.CliTestCase):
             handle_win_build(self.options, self.session, [self.target, self.scm_url, self.vm])
         self.assertExitCode(ex, 2)
         self.assert_console_message(stderr, expected)
+
+    def test_win_build_help(self):
+        self.assert_help(
+            handle_win_build,
+            """Usage: %s win-build [options] <target> <URL> <VM>
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help         show this help message and exit
+  --winspec=URL      SCM URL to retrieve the build descriptor from. If not
+                     specified, the winspec must be in the root directory of
+                     the source repository.
+  --patches=URL      SCM URL of a directory containing patches to apply to the
+                     sources before building
+  --cpus=CPUS        Number of cpus to allocate to the build VM (requires
+                     admin access)
+  --mem=MEM          Amount of memory (in megabytes) to allocate to the build
+                     VM (requires admin access)
+  --static-mac       Retain the original MAC address when cloning the VM
+  --specfile=URL     SCM URL of a spec file fragment to use to generate
+                     wrapper RPMs
+  --scratch          Perform a scratch build
+  --repo-id=REPO_ID  Use a specific repo
+  --skip-tag         Do not attempt to tag package
+  --background       Run the build at a lower priority
+  --wait             Wait on the build, even if running in the background
+  --nowait           Don't wait on build
+  --quiet            Do not print the task information
+""" % self.progname)
diff --git a/tests/test_plugins/test_runroot_cli.py b/tests/test_plugins/test_runroot_cli.py
index 2b3c284..b3ad462 100644
--- a/tests/test_plugins/test_runroot_cli.py
+++ b/tests/test_plugins/test_runroot_cli.py
@@ -3,6 +3,8 @@ import io
 import mock
 import six
 import unittest
+import os
+import sys
 
 import koji
 from . import load_plugin
@@ -56,6 +58,7 @@ class TestListCommands(unittest.TestCase):
         runroot.OptionParser = mock.MagicMock(side_effect=self.get_parser)
         self.old_watch_tasks = runroot.watch_tasks
         runroot.watch_tasks = mock.MagicMock(name='watch_tasks')
+        self.progname = os.path.basename(sys.argv[0]) or 'koji'
 
     def tearDown(self):
         runroot.OptionParser = self.old_OptionParser
@@ -169,3 +172,36 @@ class TestListCommands(unittest.TestCase):
         self.assertEqual(actual,
                          b"* The runroot plugin appears to not be installed on the"
                          b" koji hub.  Please contact the administrator.")
+
+    @mock_stdout()
+    def test_runroot_help(self, stdout):
+        with self.assertRaises(SystemExit) as ex:
+            runroot.handle_runroot(self.options, self.session, ['--help'])
+        std_output = get_stdout_value(stdout).decode('utf-8')
+        expected_help = """Usage: %s runroot [options] <tag> <arch> <command>
+(Specify the --help global option for a list of other help options)
+
+Options:
+  -h, --help            show this help message and exit
+  -p PACKAGE, --package=PACKAGE
+                        make sure this package is in the chroot
+  -m MOUNT, --mount=MOUNT
+                        mount this directory read-write in the chroot
+  --skip-setarch        bypass normal setarch in the chroot
+  -w WEIGHT, --weight=WEIGHT
+                        set task weight
+  --channel-override=CHANNEL_OVERRIDE
+                        use a non-standard channel
+  --task-id             Print the ID of the runroot task
+  --use-shell           Run command through a shell, otherwise uses exec
+  --new-chroot          Run command with the --new-chroot (systemd-nspawn)
+                        option to mock
+  --old-chroot          Run command with the --old-chroot (systemd-nspawn)
+                        option to mock
+  --repo-id=REPO_ID     ID of the repo to use
+  --nowait              Do not wait on task
+  --watch               Watch task instead of printing runroot.log
+  --quiet               Do not print the task information
+""" % self.progname
+        self.assertMultiLineEqual(std_output, expected_help)
+        self.assertEqual('0', str(ex.exception))

