Passed
Pull Request — master (#72)
by Matěj
01:34
created

test_rule_handling   F

Complexity

Total Complexity 161

Size/Duplication

Total Lines 561
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 161
eloc 332
dl 0
loc 561
rs 1.5789
c 0
b 0
f 0

35 Functions

Rating   Name   Duplication   Size   Complexity  
A part_rules() 0 5 1
A test_evaluation_passwd_minlen_no_passwd() 0 12 4
A password_data() 0 3 1
A ksdata_mock() 0 3 1
B test_evaluation_passwd_minlen_short_passwd() 0 18 6
A test_evaluation_bootloader_passwd_set() 0 9 2
A rule_data() 0 3 1
B test_evaluation_nonexisting_part_must_exist() 0 23 5
A test_part_rules_setitem() 0 3 1
A test_evaluation_bootloader_passwd_not_set() 0 10 3
A test_rule_data_quoted_opt_values() 0 6 4
F test_evaluation_add_mount_options_nonexisting_part() 0 25 13
A _occurences_not_seen_in_strings() 0 8 4
A test_part_rules_getitem() 0 2 1
B test_evaluation_add_mount_option_prefix() 0 17 5
F test_evaluation_add_mount_options_report_only() 0 32 12
A test_evaluation_add_mount_options() 0 2 1
A test_part_rules_contains() 0 2 2
C test_evaluation_package_rules_report_only() 0 22 7
B test_evaluation_existing_part_must_exist_rules() 0 27 5
A test_evaluation_various_rules() 0 13 3
A test_evaluation_add_mount_options_no_duplicates() 0 2 1
F test_rule_data_artificial() 0 32 13
B get_messages_for_defaults_options() 0 32 6
A test_part_rules_delitem() 0 3 2
B test_rule_data_real_output() 0 14 5
B test_evaluation_passwd_minlen_short_passwd_report_only() 0 15 5
A test_part_rules_len() 0 2 2
C test_evaluation_package_rules() 0 21 7
A test_evaluation_passwd_minlen_good_passwd() 0 9 2
F test_evaluation_passwd_minlen_report_only_not_ignored() 0 36 13
F evaluation_add_mount_options() 0 32 12
A _quoted_keywords_not_seen_in_messages() 0 4 3
A test_evaluation_passwd_minlen_crypted_passwd() 0 14 4
A storage_mock() 0 3 1

3 Methods

Rating   Name   Duplication   Size   Complexity  
A passwordTestData.set_rule() 0 2 1
A passwordTestData.__init__() 0 7 1
A passwordTestData.get_messages() 0 5 1

How to fix   Complexity   

Complexity

Complex classes like test_rule_handling often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import pytest
2
3
import mock
4
5
from org_fedora_oscap import rule_handling, common
6
7
8
@pytest.fixture()
9
def part_rules():
10
    rules = rule_handling.PartRules()
11
    rules.ensure_mount_point("/tmp")
12
    return rules
13
14
15
# simple tests, shouldn't raise exceptions
16
def test_part_rules_getitem(part_rules):
17
    part_rules["/tmp"]
18
19
20
def test_part_rules_setitem(part_rules):
21
    rule = rule_handling.PartRule("/var/log")
22
    part_rules["/var/log"] = rule
23
24
25
def test_part_rules_len(part_rules):
26
    assert len(part_rules) == 1
27
28
29
def test_part_rules_contains(part_rules):
30
    assert "/tmp" in part_rules
31
32
33
def test_part_rules_delitem(part_rules):
34
    del(part_rules["/tmp"])
35
    assert "/tmp" not in part_rules
36
37
38
@pytest.fixture()
39
def rule_data():
40
    return rule_handling.RuleData()
41
42
43
def test_rule_data_artificial(rule_data):
44
    rule_data.new_rule("  part /tmp --mountoptions=nodev,noauto")
45
    rule_data.new_rule("part /var/log  ")
46
    rule_data.new_rule(" passwd   --minlen=14 ")
47
    rule_data.new_rule("package --add=iptables")
48
    rule_data.new_rule(" package --add=firewalld --remove=telnet")
49
    rule_data.new_rule("package --remove=rlogin --remove=sshd")
50
    rule_data.new_rule("bootloader --passwd")
51
52
    # both partitions should appear in rule_data._part_rules
53
    assert "/tmp" in rule_data._part_rules
54
    assert "/var/log" in rule_data._part_rules
55
56
    # mount options should be parsed
57
    assert "nodev" in rule_data._part_rules["/tmp"]._mount_options
58
    assert "noauto" in rule_data._part_rules["/tmp"]._mount_options
59
60
    # no mount options for /var/log
61
    assert not rule_data._part_rules["/var/log"]._mount_options
62
63
    # minimal password length should be parsed and stored correctly
64
    assert rule_data._passwd_rules._minlen == 14
65
66
    # packages should be parsed correctly
67
    assert "iptables" in rule_data._package_rules._add_pkgs
68
    assert "firewalld" in rule_data._package_rules._add_pkgs
69
    assert "telnet" in rule_data._package_rules._remove_pkgs
70
    assert "rlogin" in rule_data._package_rules._remove_pkgs
71
    assert "sshd" in rule_data._package_rules._remove_pkgs
72
73
    # bootloader should require password
74
    assert rule_data._bootloader_rules._require_password
75
76
77
def test_rule_data_quoted_opt_values(rule_data):
78
    rule_data.new_rule('part /tmp --mountoptions="nodev,noauto"')
79
80
    assert "nodev" in rule_data._part_rules["/tmp"]._mount_options
81
    assert "noauto" in rule_data._part_rules["/tmp"]._mount_options
82
    assert '"' not in rule_data._part_rules["/tmp"]._mount_options
83
84
85
def test_rule_data_real_output(rule_data):
86
    output = """
87
    part /tmp
88
89
    part /tmp --mountoptions=nodev
90
    """
91
    for line in output.splitlines():
92
        rule_data.new_rule(line)
93
94
    assert "/tmp" in rule_data._part_rules
95
    assert "nodev" in rule_data._part_rules["/tmp"]._mount_options
96
97
    # should be stripped and merged
98
    assert str(rule_data._part_rules) == "part /tmp --mountoptions=nodev"
99
100
101
@pytest.fixture()
102
def ksdata_mock():
103
    return mock.Mock()
104
105
106
@pytest.fixture()
107
def storage_mock():
108
    return mock.Mock()
109
110
111
def test_evaluation_existing_part_must_exist_rules(
112
        rule_data, ksdata_mock, storage_mock):
113
    rules = [
114
        "part /tmp",
115
        "part /",
116
    ]
117
    for rule in rules:
118
        rule_data.new_rule(rule)
119
120
    tmp_part_mock = mock.Mock()
121
    tmp_part_mock.format.options = "defaults"
122
    root_part_mock = mock.Mock()
123
    root_part_mock.format.options = "defaults"
124
125
    storage_mock.mountpoints = {
126
        "/tmp": tmp_part_mock,
127
        "/": root_part_mock,
128
    }
129
130
    messages = rule_data.eval_rules(ksdata_mock, storage_mock)
131
132
    # partitions exist --> no errors, warnings or additional info
133
    assert not messages
134
135
    # no additional mount options specified
136
    assert tmp_part_mock.format.options == "defaults"
137
    assert root_part_mock.format.options == "defaults"
138
139
140
def test_evaluation_nonexisting_part_must_exist(rule_data, ksdata_mock, storage_mock):
141
    rules = [
142
        "part /tmp",
143
        "part /",
144
    ]
145
    for rule in rules:
146
        rule_data.new_rule(rule)
147
148
    tmp_part_mock = mock.Mock()
149
    tmp_part_mock.format.options = "defaults"
150
151
    storage_mock.mountpoints = {
152
        "/tmp": tmp_part_mock,
153
    }
154
155
    messages = rule_data.eval_rules(ksdata_mock, storage_mock)
156
157
    # / mount point missing --> one error
158
    assert len(messages) == 1
159
    assert messages[0].type == common.MESSAGE_TYPE_FATAL
160
161
    # error has to mention the mount point
162
    assert "/" in messages[0].text
163
164
165
def get_messages_for_defaults_options(
166
        rule_data, ksdata_mock, storage_mock,
167
        rules,
168
        messages_evaluation_count=1,
169
        mountpoints=("/tmp", "/"),
170
        tmp_format_options="defaults",
171
        report_only=False,
172
        ):
173
    assert len(rules) == 2, \
174
        "We need rules for temp partition and root."
175
    for rule in rules:
176
        rule_data.new_rule(rule)
177
178
    tmp_part_mock = mock.Mock()
179
    tmp_part_mock.format.options = tmp_format_options
180
    root_part_mock = mock.Mock()
181
    root_part_mock.format.options = "defaults"
182
183
    potential_mountpoints = {
184
        "/tmp": tmp_part_mock,
185
        "/": root_part_mock,
186
    }
187
    storage_mock.mountpoints = {}
188
    for mountpoint, value in potential_mountpoints.items():
189
        if mountpoint in mountpoints:
190
            storage_mock.mountpoints[mountpoint] = value
191
192
    messages = []
193
    for _ in range(messages_evaluation_count):
194
        messages = rule_data.eval_rules(ksdata_mock, storage_mock, report_only)
195
196
    return messages
197
198
199
def evaluation_add_mount_options(
200
        rule_data, ksdata_mock, storage_mock,
201
        messages_evaluation_count):
202
    rules = [
203
        "part /tmp --mountoptions=defaults,nodev",
204
        "part / --mountoptions=noauto",
205
    ]
206
207
    messages = get_messages_for_defaults_options(
208
        rule_data, ksdata_mock, storage_mock,
209
        rules, messages_evaluation_count)
210
211
    # two mount options added --> two info messages
212
    assert len(messages) == 2
213
    assert all(message.type == common.MESSAGE_TYPE_INFO for message in messages)
214
215
    # newly added mount options should be mentioned in the messages
216
    # together with their mount points
217
    nodev_found = False
218
    noauto_found = False
219
220
    for message in messages:
221
        if "'nodev'" in message.text:
222
            assert "/tmp" in message.text
223
            nodev_found = True
224
        elif "'noauto'" in message.text:
225
            assert "/" in message.text
226
            noauto_found = True
227
228
    assert all([nodev_found, noauto_found])
229
    assert storage_mock.mountpoints["/tmp"].format.options == "defaults,nodev"
230
    assert storage_mock.mountpoints["/"].format.options == "defaults,noauto"
231
232
233
def test_evaluation_add_mount_options(rule_data, ksdata_mock, storage_mock):
234
    evaluation_add_mount_options(rule_data, ksdata_mock, storage_mock, 1)
235
236
237
def test_evaluation_add_mount_options_no_duplicates(rule_data, ksdata_mock, storage_mock):
238
    evaluation_add_mount_options(rule_data, ksdata_mock, storage_mock, 2)
239
240
241
def test_evaluation_add_mount_options_report_only(rule_data, ksdata_mock, storage_mock):
242
    rules = [
243
        "part /tmp --mountoptions=nodev",
244
        "part / --mountoptions=noauto",
245
    ]
246
    messages = get_messages_for_defaults_options(
247
        rule_data, ksdata_mock, storage_mock,
248
        rules, 1, report_only=True)
249
250
    # two mount options added --> two info messages
251
    assert len(messages) == 2
252
    assert messages[0].type == common.MESSAGE_TYPE_INFO
253
    assert messages[1].type == common.MESSAGE_TYPE_INFO
254
255
    # newly added mount options should be mentioned in the messages
256
    # together with their mount points
257
    nodev_found = False
258
    noauto_found = False
259
260
    for message in messages:
261
        if "'nodev'" in message.text:
262
            assert "/tmp" in message.text
263
            nodev_found = True
264
        elif "'noauto'" in message.text:
265
            assert "/" in message.text
266
            noauto_found = True
267
268
    assert all([nodev_found, noauto_found])
269
270
    # no changes should be made
271
    assert storage_mock.mountpoints["/tmp"].format.options == "defaults"
272
    assert storage_mock.mountpoints["/"].format.options == "defaults"
273
274
275
def test_evaluation_add_mount_option_prefix(rule_data, ksdata_mock, storage_mock):
276
    rules = [
277
        "part /tmp --mountoptions=nodev",
278
        "part / --mountoptions=noauto",
279
    ]
280
    messages = get_messages_for_defaults_options(
281
        rule_data, ksdata_mock, storage_mock,
282
        rules, tmp_format_options="defaults,nodevice")
283
284
    # two mount options added (even though it is a prefix of another one)
285
    #   --> two info messages
286
    assert len(messages) == 2
287
    assert messages[0].type == common.MESSAGE_TYPE_INFO
288
    assert messages[1].type == common.MESSAGE_TYPE_INFO
289
290
    # the option should be added even though it is a prefix of another one
291
    assert storage_mock.mountpoints["/tmp"].format.options == "defaults,nodevice,nodev"
292
293
294
def test_evaluation_add_mount_options_nonexisting_part(rule_data, ksdata_mock, storage_mock):
295
    rules = [
296
        "part /tmp --mountoptions=nodev",
297
        "part / --mountoptions=noauto",
298
    ]
299
    messages = get_messages_for_defaults_options(
300
        rule_data, ksdata_mock, storage_mock,
301
        rules, mountpoints=["/"])
302
303
    # one mount option added, one mount point missing (mount options
304
    # cannot be added) --> one info, one error
305
    assert len(messages) == 2
306
    assert any(message.type == common.MESSAGE_TYPE_INFO for message in messages)
307
    assert any(message.type == common.MESSAGE_TYPE_FATAL for message in messages)
308
309
    # the info message should report mount options added to the existing
310
    # mount point, the error message shoud contain the missing mount point
311
    # and not the mount option
312
    for message in messages:
313
        if message.type == common.MESSAGE_TYPE_INFO:
314
            assert "/" in message.text
315
            assert "'noauto'" in message.text
316
        elif message.type == common.MESSAGE_TYPE_FATAL:
317
            assert "/tmp" in message.text
318
            assert "'nodev'" not in message.text
319
320
321
def test_evaluation_passwd_minlen_no_passwd(rule_data, ksdata_mock, storage_mock):
322
    rule_data.new_rule("passwd --minlen=8")
323
324
    ksdata_mock.rootpw.password = ""
325
    messages = rule_data.eval_rules(ksdata_mock, storage_mock)
326
327
    # minimal password length required --> one warning
328
    assert len(messages) == 1
329
    assert messages[0].type == common.MESSAGE_TYPE_WARNING
330
331
    # warning has to mention the length
332
    assert "8" in messages[0].text
333
334
335
class passwordTestData(object):
336
    def __init__(self, rule_data, ksdata_mock, storage_mock):
337
        self.password = None
338
        self.isCrypted = False
339
340
        self.rule_data = rule_data
341
        self.ksdata_mock = ksdata_mock
342
        self.storage_mock = storage_mock
343
344
    def set_rule(self, rule_string):
345
        self.rule_data.new_rule(rule_string)
346
347
    def get_messages(self, report_only=False):
348
        self.ksdata_mock.rootpw.password = self.password
349
        self.ksdata_mock.rootpw.isCrypted = self.isCrypted
350
        return self.rule_data.eval_rules(
351
            self.ksdata_mock, self.storage_mock, report_only=report_only)
352
353
354
@pytest.fixture()
355
def password_data(rule_data, ksdata_mock, storage_mock):
356
    return passwordTestData(rule_data, ksdata_mock, storage_mock)
357
358
359
def test_evaluation_passwd_minlen_short_passwd(password_data):
360
    password_data.set_rule("passwd --minlen=8")
361
    password_data.password = "aaaa"
362
363
    messages = password_data.get_messages()
364
365
    # minimal password length greater than actual length --> one warning
366
    assert len(messages) == 1
367
    assert messages[0].type == common.MESSAGE_TYPE_FATAL
368
369
    # warning has to mention the length
370
    assert "8" in messages[0].text
371
372
    # warning should mention that something is wrong with the old password
373
    assert "is" in messages[0].text
374
375
    # doing changes --> password should not be cleared
376
    assert password_data.ksdata_mock.rootpw.password == "aaaa"
377
378
379
def test_evaluation_passwd_minlen_short_passwd_report_only(password_data):
380
    password_data.set_rule("passwd --minlen=8")
381
    password_data.password = "aaaa"
382
383
    messages = password_data.get_messages(report_only=True)
384
385
    # minimal password length greater than actual length --> one warning
386
    assert len(messages) == 1
387
    assert messages[0].type == common.MESSAGE_TYPE_FATAL
388
389
    # warning has to mention the length
390
    assert "8" in messages[0].text
391
392
    # report only --> password shouldn't be cleared
393
    assert password_data.ksdata_mock.rootpw.password == "aaaa"
394
395
396
def test_evaluation_passwd_minlen_crypted_passwd(password_data):
397
    password_data.set_rule("passwd --minlen=8")
398
399
    password_data.password = "aaaa"
400
    password_data.isCrypted = True
401
402
    messages = password_data.get_messages()
403
404
    # minimal password length greater than actual length --> one warning
405
    assert len(messages) == 1
406
    assert messages[0].type == common.MESSAGE_TYPE_WARNING
407
408
    # warning has to mention that the password cannot be checked
409
    assert "cannot check" in messages[0].text
410
411
412
def test_evaluation_passwd_minlen_good_passwd(password_data):
413
    password_data.set_rule("passwd --minlen=8")
414
415
    password_data.password = "aaaaaaaaaaaaaaaaa"
416
417
    messages = password_data.get_messages()
418
419
    # minimal password length less than actual length --> no warning
420
    assert not messages
421
422
423
def test_evaluation_passwd_minlen_report_only_not_ignored(password_data):
424
    password_data.set_rule("passwd --minlen=8")
425
426
    password_data.password = "aaaaaaaaaaaaaaaaa"
427
428
    messages = password_data.get_messages()
429
430
    # Mock pw_policy returned by anaconda.pwpolicy.get_policy()
431
    pw_policy_mock = mock.Mock()
432
    pw_policy_mock.minlen = 6
433
    pw_policy_mock.strict = False
434
    password_data.ksdata_mock.anaconda.pwpolicy.get_policy.return_value = pw_policy_mock
435
436
    # call eval_rules with report_only=False
437
    # should set password minimal length to 8
438
    messages = password_data.get_messages()
439
440
    # Password Policy changed --> no warnings
441
    assert not messages
442
    assert password_data.rule_data._passwd_rules._orig_minlen == 6
443
    assert not password_data.rule_data._passwd_rules._orig_strict
444
    assert pw_policy_mock.minlen == 8
445
    assert pw_policy_mock.strict
446
    assert password_data.rule_data._passwd_rules._minlen == 8
447
448
    # call of eval_rules with report_only=True
449
    # should not change anything
450
    messages = password_data.get_messages(report_only=True)
451
    # Password Policy stayed the same --> no warnings
452
    assert not messages
453
454
    assert password_data.rule_data._passwd_rules._orig_minlen == 6
455
    assert not password_data.rule_data._passwd_rules._orig_strict
456
    assert pw_policy_mock.minlen == 8
457
    assert pw_policy_mock.strict
458
    assert password_data.rule_data._passwd_rules._minlen == 8
459
460
461
def _occurences_not_seen_in_strings(seeked, strings):
462
    found = set(seeked)
463
    for string in strings:
464
        for might_have_seen in seeked:
465
            if might_have_seen in string:
466
                found.add(string)
467
                break
468
    return set(seeked).difference(found)
469
470
471
def _quoted_keywords_not_seen_in_messages(keywords, messages):
472
    return _occurences_not_seen_in_strings(
473
        {"'{}'".format(kw) for kw in keywords},
474
        [m.text for m in messages],
475
    )
476
477
478
def test_evaluation_package_rules(rule_data, ksdata_mock, storage_mock):
479
    rule_data.new_rule("package --add=firewalld --remove=telnet --add=iptables --add=vim")
480
481
    ksdata_mock.packages.packageList = ["vim"]
482
    ksdata_mock.packages.excludedList = []
483
484
    messages = rule_data.eval_rules(ksdata_mock, storage_mock)
485
486
    # one info message for each (really) added/removed package
487
    assert len(messages) == 3
488
    assert all(message.type == common.MESSAGE_TYPE_INFO for message in messages)
489
490
    # all packages should appear in the messages
491
    not_seen = _quoted_keywords_not_seen_in_messages(
492
        {"firewalld", "telnet", "iptables"},
493
        messages,
494
    )
495
496
    assert not not_seen
497
    assert set(ksdata_mock.packages.packageList) == {"firewalld", "iptables", "vim"}
498
    assert set(ksdata_mock.packages.excludedList) == {"telnet"}
499
500
501
def test_evaluation_package_rules_report_only(rule_data, ksdata_mock, storage_mock):
502
    rule_data.new_rule("package --add=firewalld --remove=telnet --add=iptables")
503
504
    ksdata_mock.packages.packageList = []
505
    ksdata_mock.packages.excludedList = []
506
507
    messages = rule_data.eval_rules(ksdata_mock, storage_mock, report_only=True)
508
509
    # one info message for each added/removed package
510
    assert len(messages) == 3
511
    assert all(message.type == common.MESSAGE_TYPE_INFO for message in messages)
512
513
    not_seen = _quoted_keywords_not_seen_in_messages(
514
        {"firewalld", "telnet", "iptables"},
515
        messages,
516
    )
517
518
    assert not not_seen
519
520
    # report_only --> no packages should be added or excluded
521
    assert not ksdata_mock.packages.packageList
522
    assert not ksdata_mock.packages.excludedList
523
524
525
def test_evaluation_bootloader_passwd_not_set(rule_data, ksdata_mock, storage_mock):
526
    rule_data.new_rule("bootloader --passwd")
527
528
    storage_mock.bootloader.password = None
529
530
    messages = rule_data.eval_rules(ksdata_mock, storage_mock)
531
532
    # bootloader password not set --> one warning
533
    assert len(messages) == 1
534
    assert messages[0].type == common.MESSAGE_TYPE_WARNING
535
536
537
def test_evaluation_bootloader_passwd_set(rule_data, ksdata_mock, storage_mock):
538
    rule_data.new_rule("bootloader --passwd")
539
540
    storage_mock.bootloader.password = "aaaaa"
541
542
    messages = rule_data.eval_rules(ksdata_mock, storage_mock)
543
544
    # bootloader password set --> no warnings
545
    assert messages == []
546
547
548
def test_evaluation_various_rules(rule_data, ksdata_mock, storage_mock):
549
    for rule in ["part /tmp", "part /", "passwd --minlen=14",
550
                 "package --add=firewalld", ]:
551
        rule_data.new_rule(rule)
552
553
    storage_mock.mountpoints = dict()
554
    ksdata_mock.packages.packageList = []
555
    ksdata_mock.packages.excludedList = []
556
557
    messages = rule_data.eval_rules(ksdata_mock, storage_mock)
558
559
    # four rules, all fail --> four messages
560
    assert len(messages) == 4
561