/mandos/trunk

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/trunk

« back to all changes in this revision

Viewing changes to mandos-ctl

  • Committer: Teddy Hogeborn
  • Date: 2019-03-10 02:35:22 UTC
  • Revision ID: teddy@recompile.se-20190310023522-cvos2tw2si6t7if3
mandos-ctl: Minimize number of D-Bus calls

* mandos-ctl (Command.run): Now takes a {objpath: properties} dict,
                            followed by optional "bus" and "mandos"
                            arguments.  Use "bus" to connect to
                            clients when dispatching to method
                            run_on_one_client().  All callers changed.
  (IsEnabledCmd.run): New.
  (IsEnabledCmd.run_on_one_client): Remove.
  (ApproveCmd.run_on_one_client): Add busname to debug output.
  (DenyCmd.run_on_one_client): - '' -
  (main): In D-Bus debug output, change "name" to "busname".  Also,
          don't connect to clients, just use the object path as the
          key of the "clients" dict passed to Command.run().
  (TestCmd.clients): Changed to an {objpath: properties} dict.
  (TestCmd.one_client): - '' -
  (TestCmd.bus): New mock bus object having a get_object() method.

Show diffs side-by-side

added added

removed removed

Lines of Context:
276
276
# Abstract classes first
277
277
class Command(object):
278
278
    """Abstract class for commands"""
279
 
    def run(self, mandos, clients):
 
279
    def run(self, clients, bus=None, mandos=None):
280
280
        """Normal commands should implement run_on_one_client(), but
281
281
        commands which want to operate on all clients at the same time
282
282
        can override this run() method instead."""
283
283
        self.mandos = mandos
284
 
        for client, properties in clients.items():
 
284
        for clientpath, properties in clients.items():
 
285
            log.debug("D-Bus: Connect to: (busname=%r, path=%r)",
 
286
                      busname, str(clientpath))
 
287
            client = bus.get_object(busname, clientpath)
285
288
            self.run_on_one_client(client, properties)
286
289
 
287
290
class PrintCmd(Command):
293
296
                    "LastApprovalRequest", "ApprovalDelay",
294
297
                    "ApprovalDuration", "Checker", "ExtendedTimeout",
295
298
                    "Expires", "LastCheckerStatus")
296
 
    def run(self, mandos, clients):
297
 
        print(self.output(clients))
 
299
    def run(self, clients, bus=None, mandos=None):
 
300
        print(self.output(clients.values()))
 
301
    def output(self, clients):
 
302
        raise NotImplementedError()
298
303
 
299
304
class PropertyCmd(Command):
300
305
    """Abstract class for Actions for setting one client property"""
303
308
        log.debug("D-Bus: %s:%s:%s.Set(%r, %r, %r)", busname,
304
309
                  client.__dbus_object_path__,
305
310
                  dbus.PROPERTIES_IFACE, client_interface,
306
 
                  self.property, self.value_to_set
 
311
                  self.propname, self.value_to_set
307
312
                  if not isinstance(self.value_to_set, dbus.Boolean)
308
313
                  else bool(self.value_to_set))
309
 
        client.Set(client_interface, self.property, self.value_to_set,
 
314
        client.Set(client_interface, self.propname, self.value_to_set,
310
315
                   dbus_interface=dbus.PROPERTIES_IFACE)
 
316
    @property
 
317
    def propname(self):
 
318
        raise NotImplementedError()
311
319
 
312
320
class ValueArgumentMixIn(object):
313
321
    """Mixin class for commands taking a value as argument"""
336
344
        keywords = default_keywords
337
345
        if self.verbose:
338
346
            keywords = self.all_keywords
339
 
        return str(self.TableOfClients(clients.values(), keywords))
 
347
        return str(self.TableOfClients(clients, keywords))
340
348
 
341
349
    class TableOfClients(object):
342
350
        tableheaders = {
428
436
        return value
429
437
 
430
438
class IsEnabledCmd(Command):
431
 
    def run_on_one_client(self, client, properties):
 
439
    def run(self, clients, bus=None, mandos=None):
 
440
        client, properties = next(iter(clients.items()))
432
441
        if self.is_enabled(client, properties):
433
442
            sys.exit(0)
434
443
        sys.exit(1)
435
444
    def is_enabled(self, client, properties):
436
 
        return bool(properties["Enabled"])
 
445
        return properties["Enabled"]
437
446
 
438
447
class RemoveCmd(Command):
439
448
    def run_on_one_client(self, client, properties):
444
453
 
445
454
class ApproveCmd(Command):
446
455
    def run_on_one_client(self, client, properties):
447
 
        log.debug("D-Bus: %s:%s.Approve(True)",
 
456
        log.debug("D-Bus: %s:%s:%s.Approve(True)", busname,
448
457
                  client.__dbus_object_path__, client_interface)
449
458
        client.Approve(dbus.Boolean(True),
450
459
                       dbus_interface=client_interface)
451
460
 
452
461
class DenyCmd(Command):
453
462
    def run_on_one_client(self, client, properties):
454
 
        log.debug("D-Bus: %s:%s.Approve(False)",
 
463
        log.debug("D-Bus: %s:%s:%s.Approve(False)", busname,
455
464
                  client.__dbus_object_path__, client_interface)
456
465
        client.Approve(dbus.Boolean(False),
457
466
                       dbus_interface=client_interface)
458
467
 
459
468
class EnableCmd(PropertyCmd):
460
 
    property = "Enabled"
 
469
    propname = "Enabled"
461
470
    value_to_set = dbus.Boolean(True)
462
471
 
463
472
class DisableCmd(PropertyCmd):
464
 
    property = "Enabled"
 
473
    propname = "Enabled"
465
474
    value_to_set = dbus.Boolean(False)
466
475
 
467
476
class BumpTimeoutCmd(PropertyCmd):
468
 
    property = "LastCheckedOK"
 
477
    propname = "LastCheckedOK"
469
478
    value_to_set = ""
470
479
 
471
480
class StartCheckerCmd(PropertyCmd):
472
 
    property = "CheckerRunning"
 
481
    propname = "CheckerRunning"
473
482
    value_to_set = dbus.Boolean(True)
474
483
 
475
484
class StopCheckerCmd(PropertyCmd):
476
 
    property = "CheckerRunning"
 
485
    propname = "CheckerRunning"
477
486
    value_to_set = dbus.Boolean(False)
478
487
 
479
488
class ApproveByDefaultCmd(PropertyCmd):
480
 
    property = "ApprovedByDefault"
 
489
    propname = "ApprovedByDefault"
481
490
    value_to_set = dbus.Boolean(True)
482
491
 
483
492
class DenyByDefaultCmd(PropertyCmd):
484
 
    property = "ApprovedByDefault"
 
493
    propname = "ApprovedByDefault"
485
494
    value_to_set = dbus.Boolean(False)
486
495
 
487
496
class SetCheckerCmd(PropertyCmd, ValueArgumentMixIn):
488
 
    property = "Checker"
 
497
    propname = "Checker"
489
498
 
490
499
class SetHostCmd(PropertyCmd, ValueArgumentMixIn):
491
 
    property = "Host"
 
500
    propname = "Host"
492
501
 
493
502
class SetSecretCmd(PropertyCmd, ValueArgumentMixIn):
 
503
    propname = "Secret"
494
504
    @property
495
505
    def value_to_set(self):
496
506
        return self._vts
499
509
        """When setting, read data from supplied file object"""
500
510
        self._vts = value.read()
501
511
        value.close()
502
 
    property = "Secret"
503
512
 
504
513
class SetTimeoutCmd(PropertyCmd, MillisecondsValueArgumentMixIn):
505
 
    property = "Timeout"
 
514
    propname = "Timeout"
506
515
 
507
516
class SetExtendedTimeoutCmd(PropertyCmd,
508
517
                            MillisecondsValueArgumentMixIn):
509
 
    property = "ExtendedTimeout"
 
518
    propname = "ExtendedTimeout"
510
519
 
511
520
class SetIntervalCmd(PropertyCmd, MillisecondsValueArgumentMixIn):
512
 
    property = "Interval"
 
521
    propname = "Interval"
513
522
 
514
523
class SetApprovalDelayCmd(PropertyCmd,
515
524
                          MillisecondsValueArgumentMixIn):
516
 
    property = "ApprovalDelay"
 
525
    propname = "ApprovalDelay"
517
526
 
518
527
class SetApprovalDurationCmd(PropertyCmd,
519
528
                             MillisecondsValueArgumentMixIn):
520
 
    property = "ApprovalDuration"
 
529
    propname = "ApprovalDuration"
521
530
 
522
531
def add_command_line_options(parser):
523
532
    parser.add_argument("--version", action="version",
611
620
    if options.is_enabled:
612
621
        commands.append(IsEnabledCmd())
613
622
 
614
 
    if options.remove:
615
 
        commands.append(RemoveCmd())
616
 
 
617
623
    if options.checker is not None:
618
624
        commands.append(SetCheckerCmd(options.checker))
619
625
 
652
658
    if options.deny:
653
659
        commands.append(DenyCmd())
654
660
 
 
661
    if options.remove:
 
662
        commands.append(RemoveCmd())
 
663
 
655
664
    # If no command option has been given, show table of clients,
656
665
    # optionally verbosely
657
666
    if not commands:
695
704
        parser.error("--all requires an action.")
696
705
    if options.is_enabled and len(options.client) > 1:
697
706
        parser.error("--is-enabled requires exactly one client")
 
707
    if options.remove:
 
708
        options.remove = False
 
709
        if has_actions(options) and not options.deny:
 
710
            parser.error("--remove can only be combined with --deny")
 
711
        options.remove = True
698
712
 
699
713
 
700
714
def main():
713
727
 
714
728
    try:
715
729
        bus = dbus.SystemBus()
716
 
        log.debug("D-Bus: Connect to: (name=%r, path=%r)", busname,
 
730
        log.debug("D-Bus: Connect to: (busname=%r, path=%r)", busname,
717
731
                  server_path)
718
732
        mandos_dbus_objc = bus.get_object(busname, server_path)
719
733
    except dbus.exceptions.DBusException:
752
766
    clients = {}
753
767
 
754
768
    if not clientnames:
755
 
        clients = {bus.get_object(busname, path): properties
756
 
                   for path, properties in mandos_clients.items()}
 
769
        clients = {objpath: properties
 
770
                   for objpath, properties in mandos_clients.items()}
757
771
    else:
758
772
        for name in clientnames:
759
 
            for path, client in mandos_clients.items():
760
 
                if client["Name"] == name:
761
 
                    client_objc = bus.get_object(busname, path)
762
 
                    clients[client_objc] = client
 
773
            for objpath, properties in mandos_clients.items():
 
774
                if properties["Name"] == name:
 
775
                    clients[objpath] = properties
763
776
                    break
764
777
            else:
765
778
                log.critical("Client not found on server: %r", name)
768
781
    # Run all commands on clients
769
782
    commands = commands_from_options(options)
770
783
    for command in commands:
771
 
        command.run(mandos_serv, clients)
 
784
        command.run(clients, bus, mandos_serv)
772
785
 
773
786
 
774
787
class Test_milliseconds_to_string(unittest.TestCase):
823
836
        testcase = self
824
837
        class MockClient(object):
825
838
            def __init__(self, name, **attributes):
826
 
                self.__dbus_object_path__ = "objpath_{}".format(name)
 
839
                self.__dbus_object_path__ = "/clients/{}".format(name)
827
840
                self.attributes = attributes
828
841
                self.attributes["Name"] = name
829
842
                self.calls = []
830
 
            def Set(self, interface, property, value, dbus_interface):
831
 
                testcase.assertEqual(interface, client_interface)
832
 
                testcase.assertEqual(dbus_interface,
833
 
                                     dbus.PROPERTIES_IFACE)
834
 
                self.attributes[property] = value
835
 
            def Get(self, interface, property, dbus_interface):
836
 
                testcase.assertEqual(interface, client_interface)
837
 
                testcase.assertEqual(dbus_interface,
838
 
                                     dbus.PROPERTIES_IFACE)
839
 
                return self.attributes[property]
 
843
            def Set(self, interface, propname, value, dbus_interface):
 
844
                testcase.assertEqual(interface, client_interface)
 
845
                testcase.assertEqual(dbus_interface,
 
846
                                     dbus.PROPERTIES_IFACE)
 
847
                self.attributes[propname] = value
 
848
            def Get(self, interface, propname, dbus_interface):
 
849
                testcase.assertEqual(interface, client_interface)
 
850
                testcase.assertEqual(dbus_interface,
 
851
                                     dbus.PROPERTIES_IFACE)
 
852
                return self.attributes[propname]
840
853
            def Approve(self, approve, dbus_interface):
841
854
                testcase.assertEqual(dbus_interface, client_interface)
842
855
                self.calls.append(("Approve", (approve,
891
904
            LastCheckerStatus=-2)
892
905
        self.clients =  collections.OrderedDict(
893
906
            [
894
 
                (self.client, self.client.attributes),
895
 
                (self.other_client, self.other_client.attributes),
 
907
                ("/clients/foo", self.client.attributes),
 
908
                ("/clients/barbar", self.other_client.attributes),
896
909
            ])
897
 
        self.one_client = {self.client: self.client.attributes}
 
910
        self.one_client = {"/clients/foo": self.client.attributes}
 
911
    @property
 
912
    def bus(self):
 
913
        class Bus(object):
 
914
            @staticmethod
 
915
            def get_object(client_bus_name, path):
 
916
                self.assertEqual(client_bus_name, busname)
 
917
                return {
 
918
                    "/clients/foo": self.client,
 
919
                    "/clients/barbar": self.other_client,
 
920
                }[path]
 
921
        return Bus()
898
922
 
899
923
class TestPrintTableCmd(TestCmd):
900
924
    def test_normal(self):
901
 
        output = PrintTableCmd().output(self.clients)
 
925
        output = PrintTableCmd().output(self.clients.values())
902
926
        expected_output = """
903
927
Name   Enabled Timeout  Last Successful Check
904
928
foo    Yes     00:05:00 2019-02-03T00:00:00  
906
930
"""[1:-1]
907
931
        self.assertEqual(output, expected_output)
908
932
    def test_verbose(self):
909
 
        output = PrintTableCmd(verbose=True).output(self.clients)
 
933
        output = PrintTableCmd(verbose=True).output(
 
934
            self.clients.values())
910
935
        expected_output = """
911
936
Name   Enabled Timeout  Last Successful Check Created             Interval Host            Key ID                                                           Fingerprint                              Check Is Running Last Enabled        Approval Is Pending Approved By Default Last Approval Request Approval Delay Approval Duration Checker              Extended Timeout Expires             Last Checker Status
912
937
foo    Yes     00:05:00 2019-02-03T00:00:00   2019-01-02T00:00:00 00:02:00 foo.example.org 92ed150794387c03ce684574b1139a6594a34f895daaaf09fd8ea90a27cddb12 778827225BA7DE539C5A7CFA59CFF7CDBD9A5920 No               2019-01-03T00:00:00 No                  Yes                                       00:00:00       00:00:01          fping -q -- %(host)s 00:15:00         2019-02-04T00:00:00 0                  
914
939
"""[1:-1]
915
940
        self.assertEqual(output, expected_output)
916
941
    def test_one_client(self):
917
 
        output = PrintTableCmd().output(self.one_client)
 
942
        output = PrintTableCmd().output(self.one_client.values())
918
943
        expected_output = """
919
944
Name Enabled Timeout  Last Successful Check
920
945
foo  Yes     00:05:00 2019-02-03T00:00:00  
989
1014
                            for client, properties in self.clients.items()))
990
1015
    def test_is_enabled_run_exits_successfully(self):
991
1016
        with self.assertRaises(SystemExit) as e:
992
 
            IsEnabledCmd().run(None, self.one_client)
 
1017
            IsEnabledCmd().run(self.one_client)
993
1018
        if e.exception.code is not None:
994
1019
            self.assertEqual(e.exception.code, 0)
995
1020
        else:
997
1022
    def test_is_enabled_run_exits_with_failure(self):
998
1023
        self.client.attributes["Enabled"] = dbus.Boolean(False)
999
1024
        with self.assertRaises(SystemExit) as e:
1000
 
            IsEnabledCmd().run(None, self.one_client)
 
1025
            IsEnabledCmd().run(self.one_client)
1001
1026
        if isinstance(e.exception.code, int):
1002
1027
            self.assertNotEqual(e.exception.code, 0)
1003
1028
        else:
1012
1037
                self.calls.append(("RemoveClient", (dbus_path,)))
1013
1038
        mandos = MockMandos()
1014
1039
        super(TestRemoveCmd, self).setUp()
1015
 
        RemoveCmd().run(mandos, self.clients)
 
1040
        RemoveCmd().run(self.clients, self.bus, mandos)
1016
1041
        self.assertEqual(len(mandos.calls), 2)
1017
 
        for client in self.clients:
1018
 
            self.assertIn(("RemoveClient",
1019
 
                           (client.__dbus_object_path__,)),
 
1042
        for clientpath in self.clients:
 
1043
            self.assertIn(("RemoveClient", (clientpath,)),
1020
1044
                          mandos.calls)
1021
1045
 
1022
1046
class TestApproveCmd(TestCmd):
1023
1047
    def test_approve(self):
1024
 
        ApproveCmd().run(None, self.clients)
1025
 
        for client in self.clients:
 
1048
        ApproveCmd().run(self.clients, self.bus)
 
1049
        for clientpath in self.clients:
 
1050
            client = self.bus.get_object(busname, clientpath)
1026
1051
            self.assertIn(("Approve", (True, client_interface)),
1027
1052
                          client.calls)
1028
1053
 
1029
1054
class TestDenyCmd(TestCmd):
1030
1055
    def test_deny(self):
1031
 
        DenyCmd().run(None, self.clients)
1032
 
        for client in self.clients:
 
1056
        DenyCmd().run(self.clients, self.bus)
 
1057
        for clientpath in self.clients:
 
1058
            client = self.bus.get_object(busname, clientpath)
1033
1059
            self.assertIn(("Approve", (False, client_interface)),
1034
1060
                          client.calls)
1035
1061
 
1036
1062
class TestEnableCmd(TestCmd):
1037
1063
    def test_enable(self):
1038
 
        for client in self.clients:
 
1064
        for clientpath in self.clients:
 
1065
            client = self.bus.get_object(busname, clientpath)
1039
1066
            client.attributes["Enabled"] = False
1040
1067
 
1041
 
        EnableCmd().run(None, self.clients)
 
1068
        EnableCmd().run(self.clients, self.bus)
1042
1069
 
1043
 
        for client in self.clients:
 
1070
        for clientpath in self.clients:
 
1071
            client = self.bus.get_object(busname, clientpath)
1044
1072
            self.assertTrue(client.attributes["Enabled"])
1045
1073
 
1046
1074
class TestDisableCmd(TestCmd):
1047
1075
    def test_disable(self):
1048
 
        DisableCmd().run(None, self.clients)
1049
 
 
1050
 
        for client in self.clients:
 
1076
        DisableCmd().run(self.clients, self.bus)
 
1077
        for clientpath in self.clients:
 
1078
            client = self.bus.get_object(busname, clientpath)
1051
1079
            self.assertFalse(client.attributes["Enabled"])
1052
1080
 
1053
1081
class Unique(object):
1063
1091
                                self.values_to_set)
1064
1092
        for value_to_set, value_to_get in zip(self.values_to_set,
1065
1093
                                              values_to_get):
1066
 
            for client in self.clients:
1067
 
                old_value = client.attributes[self.property]
 
1094
            for clientpath in self.clients:
 
1095
                client = self.bus.get_object(busname, clientpath)
 
1096
                old_value = client.attributes[self.propname]
1068
1097
                self.assertNotIsInstance(old_value, Unique)
1069
 
                client.attributes[self.property] = Unique()
 
1098
                client.attributes[self.propname] = Unique()
1070
1099
            self.run_command(value_to_set, self.clients)
1071
 
            for client in self.clients:
1072
 
                value = client.attributes[self.property]
 
1100
            for clientpath in self.clients:
 
1101
                client = self.bus.get_object(busname, clientpath)
 
1102
                value = client.attributes[self.propname]
1073
1103
                self.assertNotIsInstance(value, Unique)
1074
1104
                self.assertEqual(value, value_to_get)
1075
1105
    def run_command(self, value, clients):
1076
 
        self.command().run(None, clients)
 
1106
        self.command().run(clients, self.bus)
1077
1107
 
1078
1108
class TestBumpTimeoutCmd(TestPropertyCmd):
1079
1109
    command = BumpTimeoutCmd
1080
 
    property = "LastCheckedOK"
 
1110
    propname = "LastCheckedOK"
1081
1111
    values_to_set = [""]
1082
1112
 
1083
1113
class TestStartCheckerCmd(TestPropertyCmd):
1084
1114
    command = StartCheckerCmd
1085
 
    property = "CheckerRunning"
 
1115
    propname = "CheckerRunning"
1086
1116
    values_to_set = [dbus.Boolean(True)]
1087
1117
 
1088
1118
class TestStopCheckerCmd(TestPropertyCmd):
1089
1119
    command = StopCheckerCmd
1090
 
    property = "CheckerRunning"
 
1120
    propname = "CheckerRunning"
1091
1121
    values_to_set = [dbus.Boolean(False)]
1092
1122
 
1093
1123
class TestApproveByDefaultCmd(TestPropertyCmd):
1094
1124
    command = ApproveByDefaultCmd
1095
 
    property = "ApprovedByDefault"
 
1125
    propname = "ApprovedByDefault"
1096
1126
    values_to_set = [dbus.Boolean(True)]
1097
1127
 
1098
1128
class TestDenyByDefaultCmd(TestPropertyCmd):
1099
1129
    command = DenyByDefaultCmd
1100
 
    property = "ApprovedByDefault"
 
1130
    propname = "ApprovedByDefault"
1101
1131
    values_to_set = [dbus.Boolean(False)]
1102
1132
 
1103
1133
class TestValueArgumentPropertyCmd(TestPropertyCmd):
1108
1138
            return
1109
1139
        return super(TestValueArgumentPropertyCmd, self).runTest()
1110
1140
    def run_command(self, value, clients):
1111
 
        self.command(value).run(None, clients)
 
1141
        self.command(value).run(clients, self.bus)
1112
1142
 
1113
1143
class TestSetCheckerCmd(TestValueArgumentPropertyCmd):
1114
1144
    command = SetCheckerCmd
1115
 
    property = "Checker"
 
1145
    propname = "Checker"
1116
1146
    values_to_set = ["", ":", "fping -q -- %s"]
1117
1147
 
1118
1148
class TestSetHostCmd(TestValueArgumentPropertyCmd):
1119
1149
    command = SetHostCmd
1120
 
    property = "Host"
 
1150
    propname = "Host"
1121
1151
    values_to_set = ["192.0.2.3", "foo.example.org"]
1122
1152
 
1123
1153
class TestSetSecretCmd(TestValueArgumentPropertyCmd):
1124
1154
    command = SetSecretCmd
1125
 
    property = "Secret"
 
1155
    propname = "Secret"
1126
1156
    values_to_set = [io.BytesIO(b""),
1127
1157
                     io.BytesIO(b"secret\0xyzzy\nbar")]
1128
1158
    values_to_get = [b"", b"secret\0xyzzy\nbar"]
1129
1159
 
1130
1160
class TestSetTimeoutCmd(TestValueArgumentPropertyCmd):
1131
1161
    command = SetTimeoutCmd
1132
 
    property = "Timeout"
 
1162
    propname = "Timeout"
1133
1163
    values_to_set = [datetime.timedelta(),
1134
1164
                     datetime.timedelta(minutes=5),
1135
1165
                     datetime.timedelta(seconds=1),
1139
1169
 
1140
1170
class TestSetExtendedTimeoutCmd(TestValueArgumentPropertyCmd):
1141
1171
    command = SetExtendedTimeoutCmd
1142
 
    property = "ExtendedTimeout"
 
1172
    propname = "ExtendedTimeout"
1143
1173
    values_to_set = [datetime.timedelta(),
1144
1174
                     datetime.timedelta(minutes=5),
1145
1175
                     datetime.timedelta(seconds=1),
1149
1179
 
1150
1180
class TestSetIntervalCmd(TestValueArgumentPropertyCmd):
1151
1181
    command = SetIntervalCmd
1152
 
    property = "Interval"
 
1182
    propname = "Interval"
1153
1183
    values_to_set = [datetime.timedelta(),
1154
1184
                     datetime.timedelta(minutes=5),
1155
1185
                     datetime.timedelta(seconds=1),
1159
1189
 
1160
1190
class TestSetApprovalDelayCmd(TestValueArgumentPropertyCmd):
1161
1191
    command = SetApprovalDelayCmd
1162
 
    property = "ApprovalDelay"
 
1192
    propname = "ApprovalDelay"
1163
1193
    values_to_set = [datetime.timedelta(),
1164
1194
                     datetime.timedelta(minutes=5),
1165
1195
                     datetime.timedelta(seconds=1),
1169
1199
 
1170
1200
class TestSetApprovalDurationCmd(TestValueArgumentPropertyCmd):
1171
1201
    command = SetApprovalDurationCmd
1172
 
    property = "ApprovalDuration"
 
1202
    propname = "ApprovalDuration"
1173
1203
    values_to_set = [datetime.timedelta(),
1174
1204
                     datetime.timedelta(minutes=5),
1175
1205
                     datetime.timedelta(seconds=1),
1355
1385
    def test_is_enabled_short(self):
1356
1386
        self.assert_command_from_args(["-V", "foo"], IsEnabledCmd)
1357
1387
 
 
1388
    def test_deny_before_remove(self):
 
1389
        options = self.parser.parse_args(["--deny", "--remove", "foo"])
 
1390
        check_option_syntax(self.parser, options)
 
1391
        commands = commands_from_options(options)
 
1392
        self.assertEqual(len(commands), 2)
 
1393
        self.assertIsInstance(commands[0], DenyCmd)
 
1394
        self.assertIsInstance(commands[1], RemoveCmd)
 
1395
 
 
1396
    def test_deny_before_remove_reversed(self):
 
1397
        options = self.parser.parse_args(["--remove", "--deny", "--all"])
 
1398
        check_option_syntax(self.parser, options)
 
1399
        commands = commands_from_options(options)
 
1400
        self.assertEqual(len(commands), 2)
 
1401
        self.assertIsInstance(commands[0], DenyCmd)
 
1402
        self.assertIsInstance(commands[1], RemoveCmd)
 
1403
 
1358
1404
 
1359
1405
class Test_check_option_syntax(unittest.TestCase):
1360
1406
    # This mostly corresponds to the definition from has_actions() in
1475
1521
        with self.assertParseError():
1476
1522
            self.check_option_syntax(options)
1477
1523
 
 
1524
    def test_remove_can_only_be_combined_with_action_deny(self):
 
1525
        for action, value in self.actions.items():
 
1526
            if action in {"remove", "deny"}:
 
1527
                continue
 
1528
            options = self.parser.parse_args()
 
1529
            setattr(options, action, value)
 
1530
            options.all = True
 
1531
            options.remove = True
 
1532
            with self.assertParseError():
 
1533
                self.check_option_syntax(options)
 
1534
 
1478
1535
 
1479
1536
 
1480
1537
def should_only_run_tests():