From 5aea4e2d5483cfaec648c5e1637c4acdd8c78641 Mon Sep 17 00:00:00 2001
From: Jana Cupova <root@localhost>
Date: Apr 11 2023 13:13:19 +0000
Subject: prune-signed-copies unit tests


---

diff --git a/tests/test_cli/test_prune_signed_copies.py b/tests/test_cli/test_prune_signed_copies.py
index 316ab6f..9c5dfb9 100644
--- a/tests/test_cli/test_prune_signed_copies.py
+++ b/tests/test_cli/test_prune_signed_copies.py
@@ -1,6 +1,10 @@
 from __future__ import absolute_import
 
+import koji
 import mock
+import os
+import six
+import time
 
 from koji_cli.commands import handle_prune_signed_copies
 from . import utils
@@ -8,11 +12,29 @@ from . import utils
 
 class TestPruneSignedCopies(utils.CliTestCase):
 
-    # Show long diffs in error output...
-    maxDiff = None
-
     def setUp(self):
+        self.maxDiff = None
+        self.original_timezone = os.environ.get('TZ')
+        os.environ['TZ'] = 'US/Eastern'
+        time.tzset()
+        self.options = mock.MagicMock()
+        self.options.debug = False
+        self.session = mock.MagicMock()
+        self.session.getAPIVersion.return_value = koji.API_VERSION
         self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start()
+        self.error_format = """Usage: %s prune-signed-copies [options]
+(Specify the --help global option for a list of other help options)
+
+%s: error: {message}
+""" % (self.progname, self.progname)
+
+    def tearDown(self):
+        if self.original_timezone is None:
+            del os.environ['TZ']
+        else:
+            os.environ['TZ'] = self.original_timezone
+        time.tzset()
+        mock.patch.stopall()
 
     def test_handle_prune_signed_copies_help(self):
         self.assert_help(
@@ -41,3 +63,670 @@ Options:
   --trashcan-tag=TRASHCAN_TAG
                         Specify trashcan tag
 """ % self.progname)
+
+    def test_handle_prune_signes_copies_non_exist_build(self):
+        build_id = '123'
+        arguments = ['--build', build_id]
+        self.session.getBuild.return_value = None
+        self.assert_system_exit(
+            handle_prune_signed_copies,
+            self.options, self.session, arguments,
+            stdout='',
+            stderr=self.format_error_message("No such build: %s" % build_id),
+            exit_code=2,
+            activate_session=None)
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+        self.session.getBuild.assert_called_once_with(build_id)
+
+    @mock.patch('time.asctime', return_value='Sat Apr  1 14:08:17 2023')
+    @mock.patch('koji.pathinfo.build', return_value='fakebuildpath')
+    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    def test_handle_prune_signes_copies_with_package_verbose_debug(
+            self, stdout, pathinfobuild, timeasctime):
+        """Returns info about 1 build, 4 tags, 0 files, 0 bytes and ignoring trashcan tag,
+        ignored tag and info about protected tag"""
+        self.options.debug = True
+        arguments = ['--verbose', '--package', 'package-name', '--trashcan-tag', 'test-tag1',
+                     '--protect-tag', 'test-tag3', '--ignore-tag', 'test-tag2']
+        self.session.getPackage.return_value = {'id': 135, 'name': 'package-name'}
+        self.session.listBuilds.side_effect = [[{'build_id': 1, 'nvr': 'package-name-1.3-4',
+                                                 'package_name': 'package-name'}], []]
+        self.session.queryHistory.side_effect = [
+            {'tag_listing': [{'tag.name': 'test-tag1'}, {'tag.name': 'test-tag2'},
+                             {'tag.name': 'test-tag3'}, {'tag.name': 'test-tag4'}]},
+            {'tag_listing': [{'active': True, 'build_id': 1, 'create_event': 11,
+                              'create_ts': 1681191826, 'name': 'package-name', 'release': '4',
+                              'revoke_event': None, 'tag_name': 'test-tag4', 'version': '1.3'},
+                             {'active': True, 'build_id': 1, 'create_event': 11,
+                              'create_ts': 1681191826, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 22, 'revoke_ts': 1681191836, 'tag_name': 'test-tag3',
+                              'version': '1.3'}]}]
+        rv = handle_prune_signed_copies(self.options, self.session, arguments)
+        actual = stdout.getvalue()
+        expected = """Cutoff date: Sat Apr  1 14:08:17 2023
+Getting builds...
+...got 1 builds
+DEBUG: package-name-1.3-4
+Tags: ['test-tag1', 'test-tag2', 'test-tag3', 'test-tag4']
+Ignoring trashcan tag for build package-name-1.3-4
+Ignoring tag test-tag2 for build package-name-1.3-4
+Sat Apr  1 14:08:17 2023: Tagged package-name-1.3-4 with test-tag3 [still active]
+Build package-name-1.3-4 had protected tag test-tag3 until Sat Apr  1 14:08:17 2023
+--- Grand Totals ---
+Files: 0
+Bytes: 0
+"""
+        self.assertMultiLineEqual(actual, expected)
+        self.assertNotEqual(rv, 1)
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+        self.session.getBuild.assert_not_called()
+        self.session.getPackage.assert_called_once_with('package-name')
+
+    @mock.patch('time.asctime', return_value='Sat Apr  1 14:08:17 2023')
+    @mock.patch('koji.pathinfo.build', return_value='fakebuildpath')
+    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    def test_handle_prune_signes_copies_latest_tag(self, stdout, pathinfobuild, timeasctime):
+        """Returns prune signed copies info with package latest tag"""
+        self.options.debug = True
+        arguments = ['--verbose', '--package', 'package-name']
+        self.session.getPackage.return_value = {'id': 135, 'name': 'package-name'}
+        self.session.listBuilds.side_effect = [
+            [{'build_id': 1, 'nvr': 'package-name-1.3-4', 'package_name': 'package-name'}], []]
+        self.session.queryHistory.side_effect = [
+            {'tag_listing': [{'tag.name': 'test-tag1'}, {'tag.name': 'test-tag2'}]},
+            {'tag_listing': [{'active': True, 'build_id': 1, 'create_event': 11,
+                              'create_ts': 1681191826, 'name': 'package-name', 'release': '4',
+                              'revoke_event': None, 'tag_name': 'test-tag1', 'version': '1.3'}]}]
+        rv = handle_prune_signed_copies(self.options, self.session, arguments)
+        actual = stdout.getvalue()
+        expected = """Cutoff date: Sat Apr  1 14:08:17 2023
+Getting builds...
+...got 1 builds
+DEBUG: package-name-1.3-4
+Tags: ['test-tag1', 'test-tag2']
+Sat Apr  1 14:08:17 2023: Tagged package-name-1.3-4 with test-tag1 [still active]
+package-name-1.3-4 is latest in tag test-tag1
+--- Grand Totals ---
+Files: 0
+Bytes: 0
+"""
+        self.assertMultiLineEqual(actual, expected)
+        self.assertNotEqual(rv, 1)
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+        self.session.getBuild.assert_not_called()
+        self.session.getPackage.assert_called_once_with('package-name')
+
+    @mock.patch('time.asctime', return_value='Sat Apr  1 14:08:17 2023')
+    @mock.patch('koji.pathinfo.build', return_value='fakebuildpath')
+    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    def test_handle_prune_signes_copies_cutoff_package(self, stdout, pathinfobuild, timeasctime):
+        """Returns prune signed copies info with cutoff package"""
+        self.options.debug = True
+        arguments = ['--verbose', '--package', 'package-name']
+        self.session.getPackage.return_value = {'id': 135, 'name': 'package-name'}
+        self.session.listBuilds.side_effect = [
+            [{'build_id': 1, 'nvr': 'package-name-1.3-4', 'package_name': 'package-name'}], []]
+        self.session.queryHistory.side_effect = [
+            {'tag_listing': [{'tag.name': 'test-tag1'}, {'tag.name': 'test-tag2'}]},
+            {'tag_listing': [{'active': True, 'build_id': 1, 'create_event': 11,
+                              'create_ts': 1681191826, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 22, 'revoke_ts': 1681191836, 'tag_name': 'test-tag1',
+                              'version': '1.3'},
+                             {'active': True, 'build_id': 1, 'create_event': 33,
+                              'create_ts': 1681191846, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 44, 'revoke_ts': 1681191856, 'tag_name': 'test-tag1',
+                              'version': '1.3'}]}]
+        rv = handle_prune_signed_copies(self.options, self.session, arguments)
+        actual = stdout.getvalue()
+        expected = """Cutoff date: Sat Apr  1 14:08:17 2023
+Getting builds...
+...got 1 builds
+DEBUG: package-name-1.3-4
+Tags: ['test-tag1', 'test-tag2']
+Sat Apr  1 14:08:17 2023: Tagged package-name-1.3-4 with test-tag1 [still active]
+Sat Apr  1 14:08:17 2023: Untagged package-name-1.3-4 from test-tag1
+tag test-tag1: package-name-1.3-4 not latest (revoked Sat Apr  1 14:08:17 2023)
+package-name-1.3-4 was latest past the cutoff
+--- Grand Totals ---
+Files: 0
+Bytes: 0
+"""
+        self.assertMultiLineEqual(actual, expected)
+        self.assertNotEqual(rv, 1)
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+        self.session.getBuild.assert_not_called()
+        self.session.getPackage.assert_called_once_with('package-name')
+
+    def __vm(self, result):
+        m = koji.VirtualCall('mcall_method', [], {})
+        if isinstance(result, dict) and result.get('faultCode'):
+            m._result = result
+        else:
+            m._result = (result,)
+        return m
+
+    @mock.patch('time.time', return_value=1682063424)
+    @mock.patch('time.asctime', return_value='Sat Apr  20 14:08:17 2023')
+    @mock.patch('koji.pathinfo.build', return_value='fakebuildpath')
+    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    def test_handle_prune_signes_copies_without_signatures(
+            self, stdout, pathinfobuild, timeasctime, timetime):
+        """Returns prune signed copies info without signatures"""
+        self.options.debug = True
+        arguments = ['--verbose', '--package', 'package-name']
+        list_rpms = [{'id': 123, 'name': 'test', 'version': '1.3', 'release': 1,
+                      'arch': 'test-arch', 'external_repo_name': 'ext-repo',
+                      'external_repo_id': 456, 'build_id': 1}]
+        self.session.getPackage.return_value = {'id': 135, 'name': 'package-name'}
+        self.session.listBuilds.side_effect = [
+            [{'build_id': 1, 'nvr': 'package-name-1.3-4', 'package_name': 'package-name'}], []]
+        self.session.queryHistory.side_effect = [
+            {'tag_listing': [{'tag.name': 'test-tag1'}, {'tag.name': 'test-tag2'}]},
+            {'tag_listing': [{'active': True, 'build_id': 1, 'create_event': 11,
+                              'create_ts': 1681191826, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 22, 'revoke_ts': 1681191836, 'tag_name': 'test-tag1',
+                              'version': '1.3'},
+                             {'active': True, 'build_id': 1, 'create_event': 33,
+                              'create_ts': 1681191846, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 44, 'revoke_ts': 1681191856, 'tag_name': 'test-tag1',
+                              'version': '1.3'}]},
+            {'tag_listing': [{'active': True, 'build_id': 1, 'create_event': 11,
+                              'create_ts': 1681191826, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 22, 'revoke_ts': 1681191836, 'tag_name': 'test-tag2',
+                              'version': '1.3'},
+                             {'active': True, 'build_id': 1, 'create_event': 33,
+                              'create_ts': 1681191846, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 44, 'revoke_ts': 1681191856, 'tag_name': 'test-tag2',
+                              'version': '1.3'}]}]
+        self.session.listRPMs.return_value = list_rpms
+        self.session.multiCall.return_value = [[[]]]
+        rv = handle_prune_signed_copies(self.options, self.session, arguments)
+        actual = stdout.getvalue()
+        expected = """Cutoff date: Sat Apr  20 14:08:17 2023
+Getting builds...
+...got 1 builds
+DEBUG: package-name-1.3-4
+Tags: ['test-tag1', 'test-tag2']
+Sat Apr  20 14:08:17 2023: Tagged package-name-1.3-4 with test-tag1 [still active]
+Sat Apr  20 14:08:17 2023: Untagged package-name-1.3-4 from test-tag1
+tag test-tag1: package-name-1.3-4 not latest (revoked Sat Apr  20 14:08:17 2023)
+Sat Apr  20 14:08:17 2023: Tagged package-name-1.3-4 with test-tag2 [still active]
+Sat Apr  20 14:08:17 2023: Untagged package-name-1.3-4 from test-tag2
+tag test-tag2: package-name-1.3-4 not latest (revoked Sat Apr  20 14:08:17 2023)
+(build has no signatures)
+--- Grand Totals ---
+Files: 0
+Bytes: 0
+"""
+        self.assertMultiLineEqual(actual, expected)
+        self.assertNotEqual(rv, 1)
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+        self.session.getBuild.assert_not_called()
+        self.session.getPackage.assert_called_once_with('package-name')
+
+    @mock.patch('stat.S_ISREG')
+    @mock.patch('os.lstat')
+    @mock.patch('time.time', return_value=1682063424)
+    @mock.patch('time.asctime', return_value='Sat Apr  20 14:08:17 2023')
+    @mock.patch('koji.pathinfo.build', return_value='fakebuildpath')
+    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    def test_handle_prune_signes_copies_not_regular_file(
+            self, stdout, pathinfobuild, timeasctime, timetime, lstat, isreg):
+        """Returns prune signed copies info without regular file info"""
+        self.options.debug = True
+        arguments = ['--verbose', '--package', 'package-name']
+        list_rpms = [{'id': 123, 'name': 'test', 'version': '1.3', 'release': 1,
+                      'arch': 'test-arch', 'external_repo_name': 'ext-repo',
+                      'external_repo_id': 456, 'build_id': 1}]
+        self.session.getPackage.return_value = {'id': 135, 'name': 'package-name'}
+        self.session.listBuilds.side_effect = [
+            [{'build_id': 1, 'nvr': 'package-name-1.3-4', 'package_name': 'package-name'}], []]
+        self.session.queryHistory.side_effect = [
+            {'tag_listing': [{'tag.name': 'test-tag1'}, {'tag.name': 'test-tag2'}]},
+            {'tag_listing': [{'active': True, 'build_id': 1, 'create_event': 11,
+                              'create_ts': 1681191826, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 22, 'revoke_ts': 1681191836, 'tag_name': 'test-tag1',
+                              'version': '1.3'},
+                             {'active': True, 'build_id': 1, 'create_event': 33,
+                              'create_ts': 1681191846, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 44, 'revoke_ts': 1681191856, 'tag_name': 'test-tag1',
+                              'version': '1.3'}]},
+            {'tag_listing': [{'active': True, 'build_id': 1, 'create_event': 11,
+                              'create_ts': 1681191826, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 22, 'revoke_ts': 1681191836, 'tag_name': 'test-tag2',
+                              'version': '1.3'},
+                             {'active': True, 'build_id': 1, 'create_event': 33,
+                              'create_ts': 1681191846, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 44, 'revoke_ts': 1681191856, 'tag_name': 'test-tag2',
+                              'version': '1.3'}]}
+        ]
+        self.session.listRPMs.return_value = list_rpms
+        sigRpm = [[[{'rpm_id': 123, 'sigkey': 'qwertyuiop'}]]]
+        self.session.multiCall.return_value = sigRpm
+        stat = mock.MagicMock()
+        stat.st_mode = 'mode'
+        stat.st_mtime = 1683191826
+        lstat.return_value = stat
+        isreg.return_value = False
+        rv = handle_prune_signed_copies(self.options, self.session, arguments)
+        actual = stdout.getvalue()
+        expected = """Cutoff date: Sat Apr  20 14:08:17 2023
+Getting builds...
+...got 1 builds
+DEBUG: package-name-1.3-4
+Tags: ['test-tag1', 'test-tag2']
+Sat Apr  20 14:08:17 2023: Tagged package-name-1.3-4 with test-tag1 [still active]
+Sat Apr  20 14:08:17 2023: Untagged package-name-1.3-4 from test-tag1
+tag test-tag1: package-name-1.3-4 not latest (revoked Sat Apr  20 14:08:17 2023)
+Sat Apr  20 14:08:17 2023: Tagged package-name-1.3-4 with test-tag2 [still active]
+Sat Apr  20 14:08:17 2023: Untagged package-name-1.3-4 from test-tag2
+tag test-tag2: package-name-1.3-4 not latest (revoked Sat Apr  20 14:08:17 2023)
+Skipping fakebuildpath/data/signed/qwertyuiop/test-arch/test-1.3-1.test-arch.rpm. Not a regular file
+(build has no signed copies)
+--- Grand Totals ---
+Files: 0
+Bytes: 0
+"""
+        self.assertMultiLineEqual(actual, expected)
+        self.assertNotEqual(rv, 1)
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+        self.session.getBuild.assert_not_called()
+        self.session.getPackage.assert_called_once_with('package-name')
+
+    @mock.patch('stat.S_ISREG')
+    @mock.patch('os.lstat')
+    @mock.patch('time.time', return_value=1682063424)
+    @mock.patch('time.asctime', return_value='Sat Apr  20 14:08:17 2023')
+    @mock.patch('koji.pathinfo.build', return_value='fakebuildpath')
+    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    def test_handle_prune_signes_copies_file_newer(
+            self, stdout, pathinfobuild, timeasctime, timetime, lstat, isreg):
+        """Returns prune signed copies info with file newer info"""
+        self.options.debug = True
+        arguments = ['--verbose', '--package', 'package-name']
+        list_rpms = [{'id': 123, 'name': 'test', 'version': '1.3', 'release': 1,
+                      'arch': 'test-arch', 'external_repo_name': 'ext-repo',
+                      'external_repo_id': 456, 'build_id': 1}]
+        self.session.getPackage.return_value = {'id': 135, 'name': 'package-name'}
+        self.session.listBuilds.side_effect = [
+            [{'build_id': 1, 'nvr': 'package-name-1.3-4', 'package_name': 'package-name'}], []]
+        self.session.queryHistory.side_effect = [
+            {'tag_listing': [{'tag.name': 'test-tag1'}, {'tag.name': 'test-tag2'}]},
+            {'tag_listing': [{'active': True, 'build_id': 1, 'create_event': 11,
+                              'create_ts': 1681191826, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 22, 'revoke_ts': 1681191836, 'tag_name': 'test-tag1',
+                              'version': '1.3'},
+                             {'active': True, 'build_id': 1, 'create_event': 33,
+                              'create_ts': 1681191846, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 44, 'revoke_ts': 1681191856, 'tag_name': 'test-tag1',
+                              'version': '1.3'}]},
+            {'tag_listing': [{'active': True, 'build_id': 1, 'create_event': 11,
+                              'create_ts': 1681191826, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 22, 'revoke_ts': 1681191836, 'tag_name': 'test-tag2',
+                              'version': '1.3'},
+                             {'active': True, 'build_id': 1, 'create_event': 33,
+                              'create_ts': 1681191846, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 44, 'revoke_ts': 1681191856, 'tag_name': 'test-tag2',
+                              'version': '1.3'}]}
+        ]
+        self.session.listRPMs.return_value = list_rpms
+        sigRpm = [[[{'rpm_id': 123, 'sigkey': 'qwertyuiop'}]]]
+        self.session.multiCall.return_value = sigRpm
+        stat = mock.MagicMock()
+        stat.st_mode = 'mode'
+        stat.st_mtime = 1683191826
+        lstat.return_value = stat
+        isreg.return_value = True
+        rv = handle_prune_signed_copies(self.options, self.session, arguments)
+        actual = stdout.getvalue()
+        expected = """Cutoff date: Sat Apr  20 14:08:17 2023
+Getting builds...
+...got 1 builds
+DEBUG: package-name-1.3-4
+Tags: ['test-tag1', 'test-tag2']
+Sat Apr  20 14:08:17 2023: Tagged package-name-1.3-4 with test-tag1 [still active]
+Sat Apr  20 14:08:17 2023: Untagged package-name-1.3-4 from test-tag1
+tag test-tag1: package-name-1.3-4 not latest (revoked Sat Apr  20 14:08:17 2023)
+Sat Apr  20 14:08:17 2023: Tagged package-name-1.3-4 with test-tag2 [still active]
+Sat Apr  20 14:08:17 2023: Untagged package-name-1.3-4 from test-tag2
+tag test-tag2: package-name-1.3-4 not latest (revoked Sat Apr  20 14:08:17 2023)
+Skipping fakebuildpath/data/signed/qwertyuiop/test-arch/test-1.3-1.test-arch.rpm. File newer than cutoff
+(build has no signed copies)
+--- Grand Totals ---
+Files: 0
+Bytes: 0
+"""
+        self.assertMultiLineEqual(actual, expected)
+        self.assertNotEqual(rv, 1)
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+        self.session.getBuild.assert_not_called()
+        self.session.getPackage.assert_called_once_with('package-name')
+
+    @mock.patch('stat.S_ISREG')
+    @mock.patch('os.lstat')
+    @mock.patch('time.time', return_value=1682063424)
+    @mock.patch('time.asctime', return_value='Sat Apr  20 14:08:17 2023')
+    @mock.patch('koji.pathinfo.build', return_value='fakebuildpath')
+    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    def test_handle_prune_signes_copies_with_test(
+            self, stdout, pathinfobuild, timeasctime, timetime, lstat, isreg):
+        """Returns prune signed copies info with test option"""
+        self.options.debug = True
+        arguments = ['--verbose', '--package', 'package-name', '--test']
+        list_rpms = [{'id': 123, 'name': 'test', 'version': '1.3', 'release': 1,
+                      'arch': 'test-arch', 'external_repo_name': 'ext-repo',
+                      'external_repo_id': 456, 'build_id': 1}]
+        self.session.getPackage.return_value = {'id': 135, 'name': 'package-name'}
+        self.session.listBuilds.side_effect = [
+            [{'build_id': 1, 'nvr': 'package-name-1.3-4', 'package_name': 'package-name'}], []]
+        self.session.queryHistory.side_effect = [
+            {'tag_listing': [{'tag.name': 'test-tag1'}, {'tag.name': 'test-tag2'}]},
+            {'tag_listing': [{'active': True, 'build_id': 1, 'create_event': 11,
+                              'create_ts': 1681191826, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 22, 'revoke_ts': 1681191836, 'tag_name': 'test-tag1',
+                              'version': '1.3'},
+                             {'active': True, 'build_id': 1, 'create_event': 33,
+                              'create_ts': 1681191846, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 44, 'revoke_ts': 1681191856, 'tag_name': 'test-tag1',
+                              'version': '1.3'}]},
+            {'tag_listing': [{'active': True, 'build_id': 1, 'create_event': 11,
+                              'create_ts': 1681191826, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 22, 'revoke_ts': 1681191836, 'tag_name': 'test-tag2',
+                              'version': '1.3'},
+                             {'active': True, 'build_id': 1, 'create_event': 33,
+                              'create_ts': 1681191846, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 44, 'revoke_ts': 1681191856, 'tag_name': 'test-tag2',
+                              'version': '1.3'}]}
+        ]
+        self.session.listRPMs.return_value = list_rpms
+        sigRpm = [[[{'rpm_id': 123, 'sigkey': 'qwertyuiop'}]]]
+        self.session.multiCall.return_value = sigRpm
+        stat = mock.MagicMock()
+        stat.st_mode = 'mode'
+        stat.st_mtime = 1681191826
+        lstat.return_value = stat
+        isreg.return_value = True
+        rv = handle_prune_signed_copies(self.options, self.session, arguments)
+        actual = stdout.getvalue()
+        expected = """Cutoff date: Sat Apr  20 14:08:17 2023
+Getting builds...
+...got 1 builds
+DEBUG: package-name-1.3-4
+Tags: ['test-tag1', 'test-tag2']
+Sat Apr  20 14:08:17 2023: Tagged package-name-1.3-4 with test-tag1 [still active]
+Sat Apr  20 14:08:17 2023: Untagged package-name-1.3-4 from test-tag1
+tag test-tag1: package-name-1.3-4 not latest (revoked Sat Apr  20 14:08:17 2023)
+Sat Apr  20 14:08:17 2023: Tagged package-name-1.3-4 with test-tag2 [still active]
+Sat Apr  20 14:08:17 2023: Untagged package-name-1.3-4 from test-tag2
+tag test-tag2: package-name-1.3-4 not latest (revoked Sat Apr  20 14:08:17 2023)
+Would have unlinked: fakebuildpath/data/signed/qwertyuiop/test-arch/test-1.3-1.test-arch.rpm
+Would have removed dir: fakebuildpath/data/signed/qwertyuiop/test-arch
+Would have removed dir: fakebuildpath/data/signed/qwertyuiop
+Build: package-name-1.3-4, Removed 1 signed copies (1 bytes). Total: 1/1
+--- Grand Totals ---
+Files: 1
+Bytes: 1
+"""
+        self.assertMultiLineEqual(actual, expected)
+        self.assertNotEqual(rv, 1)
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+        self.session.getBuild.assert_not_called()
+        self.session.getPackage.assert_called_once_with('package-name')
+
+    @mock.patch('os.unlink', return_value=None)
+    @mock.patch('os.rmdir', return_value=None)
+    @mock.patch('stat.S_ISREG')
+    @mock.patch('os.lstat')
+    @mock.patch('time.time', return_value=1682063424)
+    @mock.patch('time.asctime', return_value='Sat Apr  20 14:08:17 2023')
+    @mock.patch('koji.pathinfo.build', return_value='fakebuildpath')
+    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    def test_handle_prune_signes_copies_with_verbose_removing(
+            self, stdout, pathinfobuild, timeasctime, timetime, lstat, isreg, rmdir, unlink):
+        """Returns prune signed copies info with verbose option and removing files/dirs"""
+        self.options.debug = True
+        arguments = ['--verbose', '--package', 'package-name']
+        list_rpms = [{'id': 123, 'name': 'test', 'version': '1.3', 'release': 1,
+                      'arch': 'test-arch', 'external_repo_name': 'ext-repo',
+                      'external_repo_id': 456, 'build_id': 1}]
+        self.session.getPackage.return_value = {'id': 135, 'name': 'package-name'}
+        self.session.listBuilds.side_effect = [
+            [{'build_id': 1, 'nvr': 'package-name-1.3-4', 'package_name': 'package-name'}], []]
+        self.session.queryHistory.side_effect = [
+            {'tag_listing': [{'tag.name': 'test-tag1'}, {'tag.name': 'test-tag2'}]},
+            {'tag_listing': [{'active': True, 'build_id': 1, 'create_event': 11,
+                              'create_ts': 1681191826, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 22, 'revoke_ts': 1681191836, 'tag_name': 'test-tag1',
+                              'version': '1.3'},
+                             {'active': True, 'build_id': 1, 'create_event': 33,
+                              'create_ts': 1681191846, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 44, 'revoke_ts': 1681191856, 'tag_name': 'test-tag1',
+                              'version': '1.3'}]},
+            {'tag_listing': [{'active': True, 'build_id': 1, 'create_event': 11,
+                              'create_ts': 1681191826, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 22, 'revoke_ts': 1681191836, 'tag_name': 'test-tag2',
+                              'version': '1.3'},
+                             {'active': True, 'build_id': 1, 'create_event': 33,
+                              'create_ts': 1681191846, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 44, 'revoke_ts': 1681191856, 'tag_name': 'test-tag2',
+                              'version': '1.3'}]}]
+        self.session.listRPMs.return_value = list_rpms
+        sigRpm = [[[{'rpm_id': 123, 'sigkey': 'qwertyuiop'}]]]
+        self.session.multiCall.return_value = sigRpm
+        stat = mock.MagicMock()
+        stat.st_mode = 'mode'
+        stat.st_mtime = 1681191826
+        lstat.return_value = stat
+        isreg.return_value = True
+        rv = handle_prune_signed_copies(self.options, self.session, arguments)
+        actual = stdout.getvalue()
+        expected = """Cutoff date: Sat Apr  20 14:08:17 2023
+Getting builds...
+...got 1 builds
+DEBUG: package-name-1.3-4
+Tags: ['test-tag1', 'test-tag2']
+Sat Apr  20 14:08:17 2023: Tagged package-name-1.3-4 with test-tag1 [still active]
+Sat Apr  20 14:08:17 2023: Untagged package-name-1.3-4 from test-tag1
+tag test-tag1: package-name-1.3-4 not latest (revoked Sat Apr  20 14:08:17 2023)
+Sat Apr  20 14:08:17 2023: Tagged package-name-1.3-4 with test-tag2 [still active]
+Sat Apr  20 14:08:17 2023: Untagged package-name-1.3-4 from test-tag2
+tag test-tag2: package-name-1.3-4 not latest (revoked Sat Apr  20 14:08:17 2023)
+Unlinking: fakebuildpath/data/signed/qwertyuiop/test-arch/test-1.3-1.test-arch.rpm
+Removing dir: fakebuildpath/data/signed/qwertyuiop/test-arch
+Removing dir: fakebuildpath/data/signed/qwertyuiop
+Build: package-name-1.3-4, Removed 1 signed copies (1 bytes). Total: 1/1
+--- Grand Totals ---
+Files: 1
+Bytes: 1
+"""
+        self.assertMultiLineEqual(actual, expected)
+        self.assertNotEqual(rv, 1)
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+        self.session.getBuild.assert_not_called()
+        self.session.getPackage.assert_called_once_with('package-name')
+
+    @mock.patch('stat.S_ISREG')
+    @mock.patch('os.lstat')
+    @mock.patch('time.time', return_value=1682063424)
+    @mock.patch('time.asctime', return_value='Sat Apr  20 14:08:17 2023')
+    @mock.patch('koji.pathinfo.build', return_value='fakebuildpath')
+    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    def test_handle_prune_signes_copies_unlink_perms_error(
+            self, stdout, pathinfobuild, timeasctime, timetime, lstat, isreg):
+        """Returns prune signed copies info with unlink perms error"""
+        self.options.debug = True
+        arguments = ['--verbose', '--package', 'package-name']
+        list_rpms = [{'id': 123, 'name': 'test', 'version': '1.3', 'release': 1,
+                      'arch': 'test-arch', 'external_repo_name': 'ext-repo',
+                      'external_repo_id': 456, 'build_id': 1}]
+        self.session.getPackage.return_value = {'id': 135, 'name': 'package-name'}
+        self.session.listBuilds.side_effect = [
+            [{'build_id': 1, 'nvr': 'package-name-1.3-4', 'package_name': 'package-name'}], []]
+        self.session.queryHistory.side_effect = [
+            {'tag_listing': [{'tag.name': 'test-tag1'}, {'tag.name': 'test-tag2'}]},
+            {'tag_listing': [{'active': True, 'build_id': 1, 'create_event': 11,
+                              'create_ts': 1681191826, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 22, 'revoke_ts': 1681191836, 'tag_name': 'test-tag1',
+                              'version': '1.3'},
+                             {'active': True, 'build_id': 1, 'create_event': 33,
+                              'create_ts': 1681191846, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 44, 'revoke_ts': 1681191856, 'tag_name': 'test-tag1',
+                              'version': '1.3'}]},
+            {'tag_listing': [{'active': True, 'build_id': 1, 'create_event': 11,
+                              'create_ts': 1681191826, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 22, 'revoke_ts': 1681191836, 'tag_name': 'test-tag2',
+                              'version': '1.3'},
+                             {'active': True, 'build_id': 1, 'create_event': 33,
+                              'create_ts': 1681191846, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 44, 'revoke_ts': 1681191856, 'tag_name': 'test-tag2',
+                              'version': '1.3'}]}
+        ]
+        self.session.listRPMs.return_value = list_rpms
+        sigRpm = [[[{'rpm_id': 123, 'sigkey': 'qwertyuiop'}]]]
+        self.session.multiCall.return_value = sigRpm
+        stat = mock.MagicMock()
+        stat.st_mode = 'mode'
+        stat.st_mtime = 1681191826
+        lstat.return_value = stat
+        isreg.return_value = True
+        rv = handle_prune_signed_copies(self.options, self.session, arguments)
+        actual = stdout.getvalue()
+        expected = """Cutoff date: Sat Apr  20 14:08:17 2023
+Getting builds...
+...got 1 builds
+DEBUG: package-name-1.3-4
+Tags: ['test-tag1', 'test-tag2']
+Sat Apr  20 14:08:17 2023: Tagged package-name-1.3-4 with test-tag1 [still active]
+Sat Apr  20 14:08:17 2023: Untagged package-name-1.3-4 from test-tag1
+tag test-tag1: package-name-1.3-4 not latest (revoked Sat Apr  20 14:08:17 2023)
+Sat Apr  20 14:08:17 2023: Tagged package-name-1.3-4 with test-tag2 [still active]
+Sat Apr  20 14:08:17 2023: Untagged package-name-1.3-4 from test-tag2
+tag test-tag2: package-name-1.3-4 not latest (revoked Sat Apr  20 14:08:17 2023)
+Unlinking: fakebuildpath/data/signed/qwertyuiop/test-arch/test-1.3-1.test-arch.rpm
+Error removing fakebuildpath/data/signed/qwertyuiop/test-arch/test-1.3-1.test-arch.rpm: [Errno 2] No such file or directory: 'fakebuildpath/data/signed/qwertyuiop/test-arch/test-1.3-1.test-arch.rpm'
+This script needs write access to /mnt/koji
+(build has no signed copies)
+--- Grand Totals ---
+Files: 0
+Bytes: 0
+"""
+        self.assertMultiLineEqual(actual, expected)
+        self.assertNotEqual(rv, 1)
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+        self.session.getBuild.assert_not_called()
+        self.session.getPackage.assert_called_once_with('package-name')
+
+    @mock.patch('os.unlink', return_value=None)
+    @mock.patch('stat.S_ISREG')
+    @mock.patch('os.lstat')
+    @mock.patch('time.time', return_value=1682063424)
+    @mock.patch('time.asctime', return_value='Sat Apr  20 14:08:17 2023')
+    @mock.patch('koji.pathinfo.build', return_value='fakebuildpath')
+    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    def test_handle_prune_signes_copies_error_removing(
+            self, stdout, pathinfobuild, timeasctime, timetime, lstat, isreg, unlink):
+        """Returns prune signed copies info with removing error"""
+        self.options.debug = True
+        arguments = ['--verbose', '--package', 'package-name']
+        list_rpms = [{'id': 123, 'name': 'test', 'version': '1.3', 'release': 1,
+                      'arch': 'test-arch', 'external_repo_name': 'ext-repo',
+                      'external_repo_id': 456, 'build_id': 1}]
+        self.session.getPackage.return_value = {'id': 135, 'name': 'package-name'}
+        self.session.listBuilds.side_effect = [
+            [{'build_id': 1, 'nvr': 'package-name-1.3-4', 'package_name': 'package-name'}], []]
+        self.session.queryHistory.side_effect = [
+            {'tag_listing': [{'tag.name': 'test-tag1'}, {'tag.name': 'test-tag2'}]},
+            {'tag_listing': [{'active': True, 'build_id': 1, 'create_event': 11,
+                              'create_ts': 1681191826, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 22, 'revoke_ts': 1681191836, 'tag_name': 'test-tag1',
+                              'version': '1.3'},
+                             {'active': True, 'build_id': 1, 'create_event': 33,
+                              'create_ts': 1681191846, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 44, 'revoke_ts': 1681191856, 'tag_name': 'test-tag1',
+                              'version': '1.3'}]},
+            {'tag_listing': [{'active': True, 'build_id': 1, 'create_event': 11,
+                              'create_ts': 1681191826, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 22, 'revoke_ts': 1681191836, 'tag_name': 'test-tag2',
+                              'version': '1.3'},
+                             {'active': True, 'build_id': 1, 'create_event': 33,
+                              'create_ts': 1681191846, 'name': 'package-name', 'release': '4',
+                              'revoke_event': 44, 'revoke_ts': 1681191856, 'tag_name': 'test-tag2',
+                              'version': '1.3'}]}
+        ]
+        self.session.listRPMs.return_value = list_rpms
+        sigRpm = [[[{'rpm_id': 123, 'sigkey': 'qwertyuiop'}]]]
+        self.session.multiCall.return_value = sigRpm
+        stat = mock.MagicMock()
+        stat.st_mode = 'mode'
+        stat.st_mtime = 1681191826
+        lstat.return_value = stat
+        isreg.return_value = True
+        rv = handle_prune_signed_copies(self.options, self.session, arguments)
+        actual = stdout.getvalue()
+        expected = """Cutoff date: Sat Apr  20 14:08:17 2023
+Getting builds...
+...got 1 builds
+DEBUG: package-name-1.3-4
+Tags: ['test-tag1', 'test-tag2']
+Sat Apr  20 14:08:17 2023: Tagged package-name-1.3-4 with test-tag1 [still active]
+Sat Apr  20 14:08:17 2023: Untagged package-name-1.3-4 from test-tag1
+tag test-tag1: package-name-1.3-4 not latest (revoked Sat Apr  20 14:08:17 2023)
+Sat Apr  20 14:08:17 2023: Tagged package-name-1.3-4 with test-tag2 [still active]
+Sat Apr  20 14:08:17 2023: Untagged package-name-1.3-4 from test-tag2
+tag test-tag2: package-name-1.3-4 not latest (revoked Sat Apr  20 14:08:17 2023)
+Unlinking: fakebuildpath/data/signed/qwertyuiop/test-arch/test-1.3-1.test-arch.rpm
+Removing dir: fakebuildpath/data/signed/qwertyuiop/test-arch
+Error removing fakebuildpath/data/signed/qwertyuiop/test-arch/test-1.3-1.test-arch.rpm: [Errno 2] No such file or directory: 'fakebuildpath/data/signed/qwertyuiop/test-arch'
+Removing dir: fakebuildpath/data/signed/qwertyuiop
+Error removing fakebuildpath/data/signed/qwertyuiop/test-arch/test-1.3-1.test-arch.rpm: [Errno 2] No such file or directory: 'fakebuildpath/data/signed/qwertyuiop'
+Build: package-name-1.3-4, Removed 1 signed copies (1 bytes). Total: 1/1
+--- Grand Totals ---
+Files: 1
+Bytes: 1
+"""
+        self.assertMultiLineEqual(actual, expected)
+        self.assertNotEqual(rv, 1)
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+        self.session.getBuild.assert_not_called()
+        self.session.getPackage.assert_called_once_with('package-name')
+
+    @mock.patch('time.asctime', return_value='Sat Apr  1 14:08:17 2023')
+    @mock.patch('koji.pathinfo.build', return_value='fakebuildpath')
+    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    def test_handle_prune_signes_copies_not_create_event(self, stdout, pathinfobuild, timeasctime):
+        self.options.debug = True
+        arguments = ['--verbose', '--package', 'package-name']
+        self.session.getPackage.return_value = {'id': 135, 'name': 'package-name'}
+        self.session.listBuilds.side_effect = [
+            [{'build_id': 1, 'nvr': 'package-name-1.3.4', 'package_name': 'package-name'}], []]
+        self.session.queryHistory.side_effect = [
+            {'tag_listing': [{'tag.name': 'test-tag1'}, {'tag.name': 'test-tag2'}]},
+            {'tag_listing': [{'active': True, 'build_id': 1, 'create_event': None,
+                              'create_ts': None, 'name': 'package-name', 'release': '4',
+                              'revoke_event': None, 'revoke_ts': None, 'tag_name': 'test-tag1',
+                              'version': '3'}]}]
+
+        with self.assertRaises(koji.GenericError) as ex:
+            handle_prune_signed_copies(self.options, self.session, arguments)
+        self.assertEqual(str(ex.exception),
+                         "No creation event found for package-name-1.3.4 in test-tag1")
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+        self.session.getBuild.assert_not_called()
+        self.session.getPackage.assert_called_once_with('package-name')
+
+    def test_handle_prune_signes_copies_without_pkg_tag_history(self):
+        self.options.debug = True
+        arguments = ['--verbose', '--package', 'package-name']
+        self.session.getPackage.return_value = {'id': 135, 'name': 'package-name'}
+        self.session.listBuilds.side_effect = [
+            [{'build_id': 1, 'nvr': 'package-name-1.3.4', 'package_name': 'package-name'}], []]
+        self.session.queryHistory.side_effect = [
+            {'tag_listing': [{'tag.name': 'test-tag1'}, {'tag.name': 'test-tag2'}]},
+            {'tag_listing': []}]
+
+        with self.assertRaises(koji.GenericError) as ex:
+            handle_prune_signed_copies(self.options, self.session, arguments)
+        self.assertEqual(str(ex.exception), "No history found for package-name-1.3.4 in test-tag1")
+        self.activate_session_mock.assert_called_once_with(self.session, self.options)
+        self.session.getBuild.assert_not_called()
+        self.session.getPackage.assert_called_once_with('package-name')

