diff options
| author | omnom62 <75066712+omnom62@users.noreply.github.com> | 2026-08-27 04:00:17 +1000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-26 21:00:17 +0300 |
| commit | 433a274ce636573953f8a4be9c68743a2ed1d0a5 (patch) | |
| tree | da9e545f9ffdf50289c1b94580450a456177d74b /tests/unit/modules | |
| parent | 899a6bf7955592ec40670944a860a1bee97b432c (diff) | |
| download | vyos.vyos-433a274ce636573953f8a4be9c68743a2ed1d0a5.tar.gz vyos.vyos-433a274ce636573953f8a4be9c68743a2ed1d0a5.zip | |
T8220: Firewall Zone Policy support (#447)
Add zone base policy firewall module
Diffstat (limited to 'tests/unit/modules')
4 files changed, 647 insertions, 0 deletions
diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v14.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v14.cfg index c883ca78..f58f166c 100644 --- a/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v14.cfg +++ b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v14.cfg @@ -22,3 +22,9 @@ set firewall global-options send-redirects 'enable' set firewall zone ZONE-TEST interface 'eth0.1234' set firewall zone ZONE-TEST description 'zone-test test description' set firewall group address-group ZONE-TEST address '1.2.3.4' +set firewall zone ZONE-IZF description 'zone for izf and sources delete path testing' +set firewall zone ZONE-IZF intra-zone-filtering action 'drop' +set firewall zone ZONE-IZF from zone-src firewall name 'existing-ruleset' +set firewall name TESTRULESET-V4 default-action drop +set firewall ipv6-name TESTRULESET-V6-LEGACY default-action drop +set firewall ipv6 name TESTRULESET-V6-1_4PLUS default-action drop diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v15.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v15.cfg new file mode 100644 index 00000000..23dd9777 --- /dev/null +++ b/tests/unit/modules/network/vyos/fixtures/vyos_firewall_global_config_v15.cfg @@ -0,0 +1,11 @@ +# Intentionally minimal: represents a VyOS 1.5.0 device with one existing +# zone already configured using the 1.5.0 'member interface' syntax. Used +# to test parse_zone's unwrap logic in isolation. TestVyosFirewallRulesModule15's +# other tests (e.g. test_01_merged) create a differently-named zone +# (ZONE-15), so under 'merged' state this existing zone is never touched +# and doesn't interfere. +set firewall zone ZONE-15-EXISTING member interface eth2 +set firewall zone ZONE-15-EXISTING description 'existing 1.5.0 zone for facts parsing test' +set firewall name TESTRULESET-V4 default-action drop +set firewall ipv6-name TESTRULESET-V6-LEGACY default-action drop +set firewall ipv6 name TESTRULESET-V6-1_4PLUS default-action drop diff --git a/tests/unit/modules/network/vyos/test_vyos_firewall_global14.py b/tests/unit/modules/network/vyos/test_vyos_firewall_global14.py index a7abfe8c..397ccfbd 100644 --- a/tests/unit/modules/network/vyos/test_vyos_firewall_global14.py +++ b/tests/unit/modules/network/vyos/test_vyos_firewall_global14.py @@ -158,6 +158,31 @@ class TestVyosFirewallRulesModule14(TestVyosModule): ), ], ), + zone=[ + dict( + name="FZP-2", + default_action="reject", + default_log=True, + description="This is the Firewall zone fzp2", + interfaces=[ + "eth2", + "lo0", + ], + local_zone=True, + sources=[ + dict( + zone="fzp5", + firewall=dict( + name="fzp1", + ipv6_name="fzp1-v6", + ), + ), + ], + intra_zone_filtering=dict( + action="accept", + ), + ), + ], ), state="merged", ), @@ -195,6 +220,15 @@ class TestVyosFirewallRulesModule14(TestVyosModule): "set firewall global-options twa-hazards-protection 'enable'", "set firewall global-options syn-cookies 'enable'", "set firewall global-options source-validation 'strict'", + "set firewall zone FZP-2 default-action 'reject'", + "set firewall zone FZP-2 default-log", + "set firewall zone FZP-2 description 'This is the Firewall zone fzp2'", + "set firewall zone FZP-2 from fzp5 firewall ipv6-name fzp1-v6", + "set firewall zone FZP-2 from fzp5 firewall name fzp1", + "set firewall zone FZP-2 interface eth2", + "set firewall zone FZP-2 interface lo0", + "set firewall zone FZP-2 intra-zone-filtering action accept", + "set firewall zone FZP-2 local-zone", ] self.execute_module(changed=True, commands=commands) @@ -321,6 +355,31 @@ class TestVyosFirewallRulesModule14(TestVyosModule): ), ], ), + zone=[ + dict( + name="FZP-2", + default_action="drop", + default_log=True, + description="This is the Firewall zone fzp2", + interfaces=[ + "eth2", + "lo0", + ], + local_zone=True, + sources=[ + dict( + zone="fzp5", + firewall=dict( + name="fzp1", + ipv6_name="fzp1-v6", + ), + ), + ], + intra_zone_filtering=dict( + action="accept", + ), + ), + ], ), state="replaced", ), @@ -334,15 +393,26 @@ class TestVyosFirewallRulesModule14(TestVyosModule): "delete firewall global-options state-policy related", "delete firewall global-options ipv6-src-route", "delete firewall global-options send-redirects", + "delete firewall zone ZONE-IZF", "set firewall global-options state-policy invalid action 'reject'", "set firewall group address-group RND-HOSTS address 192.0.2.7", "set firewall group address-group RND-HOSTS address 192.0.2.9", "set firewall group address-group ZONE-TEST description 'This is a new description for a address group name that is also in a zone'", + "delete firewall zone ZONE-TEST", "delete firewall group network-group RND description", "delete firewall group ipv6-address-group LOCAL-v6 address fdec:2503:89d6:59b3::1", "set firewall group ipv6-address-group LOCAL-v6 address fdec:2503:89d6:59b3::2", "delete firewall group port-group SSH port 22", "set firewall group port-group SSH port 2222", + "set firewall zone FZP-2 default-action 'drop'", + "set firewall zone FZP-2 default-log", + "set firewall zone FZP-2 description 'This is the Firewall zone fzp2'", + "set firewall zone FZP-2 from fzp5 firewall ipv6-name fzp1-v6", + "set firewall zone FZP-2 from fzp5 firewall name fzp1", + "set firewall zone FZP-2 interface eth2", + "set firewall zone FZP-2 interface lo0", + "set firewall zone FZP-2 intra-zone-filtering action accept", + "set firewall zone FZP-2 local-zone", ] self.execute_module(changed=True, commands=commands) @@ -421,6 +491,24 @@ class TestVyosFirewallRulesModule14(TestVyosModule): ), ], ), + zone=[ + dict( + name="ZONE-TEST", + description="zone-test test description", + interfaces=["eth0.1234"], + ), + dict( + name="ZONE-IZF", + description="zone for izf and sources delete path testing", + intra_zone_filtering=dict(action="drop"), + sources=[ + dict( + zone="zone-src", + firewall=dict(name="existing-ruleset"), + ), + ], + ), + ], ), state="replaced", ), @@ -487,6 +575,15 @@ class TestVyosFirewallRulesModule14(TestVyosModule): ), ], ), + zone=[ + dict( + name="ZONE-TEST", + description="zone-test test description", + interfaces=[ + "lo", + ], + ), + ], ), state="replaced", ), @@ -496,6 +593,7 @@ class TestVyosFirewallRulesModule14(TestVyosModule): "delete firewall group address-group RND-HOSTS address 192.0.2.3", "delete firewall group address-group RND-HOSTS address 192.0.2.5", "delete firewall group address-group ZONE-TEST address 1.2.3.4", + "delete firewall zone ZONE-IZF", "delete firewall global-options all-ping", "delete firewall global-options ipv6-src-route", "delete firewall global-options send-redirects", @@ -506,6 +604,8 @@ class TestVyosFirewallRulesModule14(TestVyosModule): "set firewall group address-group RND-HOSTS address 192.0.2.7", "set firewall group address-group RND-HOSTS address 192.0.2.9", "set firewall group address-group ZONE-TEST address 4.3.2.1", + "delete firewall zone ZONE-TEST interface eth0.1234", + "set firewall zone ZONE-TEST interface lo", "set firewall group address-group ZONE-TEST description 'This is a new description for a address group name that is also in a zone'", "delete firewall group ipv6-address-group LOCAL-v6 address fdec:2503:89d6:59b3::1", "set firewall group ipv6-address-group LOCAL-v6 address fdec:2503:89d6:59b3::2", @@ -518,3 +618,401 @@ class TestVyosFirewallRulesModule14(TestVyosModule): set_module_args(dict(config=dict(), state="deleted")) commands = ["delete firewall"] self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_global_set_03_replaced_izf_action_change(self): + set_module_args( + dict( + config=dict( + ping=dict(all=True), + route_redirects=[ + dict(ip_src_route=True, afi="ipv6"), + dict(icmp_redirects=dict(send=True), afi="ipv4"), + ], + state_policy=[ + dict(connection_type="related", action="accept", log_level="alert"), + ], + group=dict( + address_group=[ + dict(afi="ipv4", name="A-EMPTY"), + dict( + afi="ipv4", + name="RND-HOSTS", + description="This group has the Management hosts address lists", + members=[ + dict(address="192.0.2.1"), + dict(address="192.0.2.3"), + dict(address="192.0.2.5"), + ], + ), + dict( + afi="ipv4", + name="DELETE-HOSTS", + description="The (single) last address from this group will be deleted in the tests", + members=[dict(address="1.2.3.4")], + ), + dict( + afi="ipv4", + name="ZONE-TEST", + members=[dict(address="1.2.3.4")], + ), + dict( + afi="ipv6", + name="LOCAL-v6", + description="This group has the hosts address lists of this machine", + members=[ + dict(address="::1"), + dict(address="fdec:2503:89d6:59b3::1"), + ], + ), + ], + network_group=[ + dict( + afi="ipv4", + name="RND", + description="This group has the Management network addresses", + members=[dict(address="192.0.2.0/24")], + ), + dict( + afi="ipv6", + name="UNIQUE-LOCAL-v6", + description="This group encompasses the ULA address space in IPv6", + members=[dict(address="fc00::/7")], + ), + ], + port_group=[ + dict( + name="SSH", + description="This group has the ssh ports", + members=[dict(port="22")], + ), + ], + ), + zone=[ + dict( + name="ZONE-TEST", + description="zone-test test description", + interfaces=["eth0.1234"], + ), + dict( + name="ZONE-IZF", + description="zone for izf and sources delete path testing", + intra_zone_filtering=dict(action="accept"), # was 'drop' + sources=[ + dict( + zone="zone-src", + firewall=dict(name="existing-ruleset"), + ), + ], + ), + ], + ), + state="replaced", + ), + ) + commands = [ + "delete firewall zone ZONE-IZF intra-zone-filtering action", + "set firewall zone ZONE-IZF intra-zone-filtering action accept", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_global_set_04_replaced_sources_value_change(self): + # ZONE-IZF.sources[0].firewall.name changes from 'existing-ruleset' + # (fixture) to 'new-ruleset'. Exercises _render_sources' `not opr + # and hfw` (delete) branch. Pre-fix this leaked the old value into + # the delete command. + set_module_args( + dict( + config=dict( + ping=dict(all=True), + route_redirects=[ + dict(ip_src_route=True, afi="ipv6"), + dict(icmp_redirects=dict(send=True), afi="ipv4"), + ], + state_policy=[ + dict(connection_type="related", action="accept", log_level="alert"), + ], + group=dict( + address_group=[ + dict(afi="ipv4", name="A-EMPTY"), + dict( + afi="ipv4", + name="RND-HOSTS", + description="This group has the Management hosts address lists", + members=[ + dict(address="192.0.2.1"), + dict(address="192.0.2.3"), + dict(address="192.0.2.5"), + ], + ), + dict( + afi="ipv4", + name="DELETE-HOSTS", + description="The (single) last address from this group will be deleted in the tests", + members=[dict(address="1.2.3.4")], + ), + dict( + afi="ipv4", + name="ZONE-TEST", + members=[dict(address="1.2.3.4")], + ), + dict( + afi="ipv6", + name="LOCAL-v6", + description="This group has the hosts address lists of this machine", + members=[ + dict(address="::1"), + dict(address="fdec:2503:89d6:59b3::1"), + ], + ), + ], + network_group=[ + dict( + afi="ipv4", + name="RND", + description="This group has the Management network addresses", + members=[dict(address="192.0.2.0/24")], + ), + dict( + afi="ipv6", + name="UNIQUE-LOCAL-v6", + description="This group encompasses the ULA address space in IPv6", + members=[dict(address="fc00::/7")], + ), + ], + port_group=[ + dict( + name="SSH", + description="This group has the ssh ports", + members=[dict(port="22")], + ), + ], + ), + zone=[ + dict( + name="ZONE-TEST", + description="zone-test test description", + interfaces=["eth0.1234"], + ), + dict( + name="ZONE-IZF", + description="zone for izf and sources delete path testing", + intra_zone_filtering=dict(action="drop"), + sources=[ + dict( + zone="zone-src", + firewall=dict(name="new-ruleset"), # was 'existing-ruleset' + ), + ], + ), + ], + ), + state="replaced", + ), + ) + commands = [ + "delete firewall zone ZONE-IZF from zone-src firewall name", + "set firewall zone ZONE-IZF from zone-src firewall name new-ruleset", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_global_set_05_replaced_sources_firewall_cleared(self): + # ZONE-IZF.sources[0] keeps the 'zone-src' key but drops the + # 'firewall' sub-dict entirely. Exercises _render_sources' `not opr + # and not hfw` branch -- unreachable before the fix, since it was + # guarded by `not have` instead of `not hfw`, and `have` can never + # be empty here (we're already inside `if zone in have_index`). + set_module_args( + dict( + config=dict( + ping=dict(all=True), + route_redirects=[ + dict(ip_src_route=True, afi="ipv6"), + dict(icmp_redirects=dict(send=True), afi="ipv4"), + ], + state_policy=[ + dict(connection_type="related", action="accept", log_level="alert"), + ], + group=dict( + address_group=[ + dict(afi="ipv4", name="A-EMPTY"), + dict( + afi="ipv4", + name="RND-HOSTS", + description="This group has the Management hosts address lists", + members=[ + dict(address="192.0.2.1"), + dict(address="192.0.2.3"), + dict(address="192.0.2.5"), + ], + ), + dict( + afi="ipv4", + name="DELETE-HOSTS", + description="The (single) last address from this group will be deleted in the tests", + members=[dict(address="1.2.3.4")], + ), + dict( + afi="ipv4", + name="ZONE-TEST", + members=[dict(address="1.2.3.4")], + ), + dict( + afi="ipv6", + name="LOCAL-v6", + description="This group has the hosts address lists of this machine", + members=[ + dict(address="::1"), + dict(address="fdec:2503:89d6:59b3::1"), + ], + ), + ], + network_group=[ + dict( + afi="ipv4", + name="RND", + description="This group has the Management network addresses", + members=[dict(address="192.0.2.0/24")], + ), + dict( + afi="ipv6", + name="UNIQUE-LOCAL-v6", + description="This group encompasses the ULA address space in IPv6", + members=[dict(address="fc00::/7")], + ), + ], + port_group=[ + dict( + name="SSH", + description="This group has the ssh ports", + members=[dict(port="22")], + ), + ], + ), + zone=[ + dict( + name="ZONE-TEST", + description="zone-test test description", + interfaces=["eth0.1234"], + ), + dict( + name="ZONE-IZF", + description="zone for izf and sources delete path testing", + intra_zone_filtering=dict(action="drop"), + sources=[ + dict(zone="zone-src"), # firewall dropped entirely + ], + ), + ], + ), + state="replaced", + ), + ) + commands = [ + "delete firewall zone ZONE-IZF from zone-src", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_global_set_06_replaced_sources_entry_removed(self): + # ZONE-IZF's sources list drops the 'zone-src' entry entirely (not + # just its firewall sub-dict). Exercises the new `elif not opr:` + # branch -- previously this case emitted nothing at all. + set_module_args( + dict( + config=dict( + ping=dict(all=True), + route_redirects=[ + dict(ip_src_route=True, afi="ipv6"), + dict(icmp_redirects=dict(send=True), afi="ipv4"), + ], + state_policy=[ + dict(connection_type="related", action="accept", log_level="alert"), + ], + group=dict( + address_group=[ + dict(afi="ipv4", name="A-EMPTY"), + dict( + afi="ipv4", + name="RND-HOSTS", + description="This group has the Management hosts address lists", + members=[ + dict(address="192.0.2.1"), + dict(address="192.0.2.3"), + dict(address="192.0.2.5"), + ], + ), + dict( + afi="ipv4", + name="DELETE-HOSTS", + description="The (single) last address from this group will be deleted in the tests", + members=[dict(address="1.2.3.4")], + ), + dict( + afi="ipv4", + name="ZONE-TEST", + members=[dict(address="1.2.3.4")], + ), + dict( + afi="ipv6", + name="LOCAL-v6", + description="This group has the hosts address lists of this machine", + members=[ + dict(address="::1"), + dict(address="fdec:2503:89d6:59b3::1"), + ], + ), + ], + network_group=[ + dict( + afi="ipv4", + name="RND", + description="This group has the Management network addresses", + members=[dict(address="192.0.2.0/24")], + ), + dict( + afi="ipv6", + name="UNIQUE-LOCAL-v6", + description="This group encompasses the ULA address space in IPv6", + members=[dict(address="fc00::/7")], + ), + ], + port_group=[ + dict( + name="SSH", + description="This group has the ssh ports", + members=[dict(port="22")], + ), + ], + ), + zone=[ + dict( + name="ZONE-TEST", + description="zone-test test description", + interfaces=["eth0.1234"], + ), + dict( + name="ZONE-IZF", + description="zone for izf and sources delete path testing", + intra_zone_filtering=dict(action="drop"), + # sources omitted entirely + ), + ], + ), + state="replaced", + ), + ) + commands = [ + "delete firewall zone ZONE-IZF from zone-src", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_global_ruleset_lines_filtered_from_facts(self): + # Confirms render_config's pre-filter strips all three ruleset-line + # prefixes (IPv4 name, legacy hyphenated ipv6-name, and 1.4+ + # space-separated "ipv6 name") before zone/global-options parsing + # runs, on a real 1.4.x-shaped fixture alongside existing zone data. + set_module_args(dict(config=dict(), state="gathered")) + result = self.execute_module(changed=False) + facts = result["gathered"] + self.assertNotIn("TESTRULESET-V4", str(facts)) + self.assertNotIn("TESTRULESET-V6-LEGACY", str(facts)) + self.assertNotIn("TESTRULESET-V6-1_4PLUS", str(facts)) diff --git a/tests/unit/modules/network/vyos/test_vyos_firewall_global15.py b/tests/unit/modules/network/vyos/test_vyos_firewall_global15.py new file mode 100644 index 00000000..22ab5364 --- /dev/null +++ b/tests/unit/modules/network/vyos/test_vyos_firewall_global15.py @@ -0,0 +1,132 @@ +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# Make coding more python3-ish +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from unittest.mock import patch + +from ansible_collections.vyos.vyos.plugins.modules import vyos_firewall_global +from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args + +from .vyos_module import TestVyosModule, load_fixture + + +class TestVyosFirewallRulesModule15(TestVyosModule): + module = vyos_firewall_global + + def setUp(self): + super(TestVyosFirewallRulesModule15, self).setUp() + self.mock_get_config = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.network.Config.get_config", + ) + self.get_config = self.mock_get_config.start() + + self.mock_load_config = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.network.Config.load_config", + ) + self.load_config = self.mock_load_config.start() + + self.mock_get_resource_connection_config = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base.get_resource_connection", + ) + self.get_resource_connection_config = self.mock_get_resource_connection_config.start() + + self.mock_get_resource_connection_facts = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.facts.facts.get_resource_connection", + ) + self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start() + + self.mock_execute_show_command = patch( + "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.firewall_global.firewall_global.Firewall_globalFacts.get_device_data", + ) + + self.mock_get_os_version = patch( + "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.firewall_global.firewall_global.get_os_version", + ) + self.get_os_version = self.mock_get_os_version.start() + self.get_os_version.return_value = "1.5" + + self.execute_show_command = self.mock_execute_show_command.start() + self.maxDiff = None + + def tearDown(self): + super(TestVyosFirewallRulesModule15, self).tearDown() + self.mock_get_resource_connection_config.stop() + self.mock_get_resource_connection_facts.stop() + self.mock_get_config.stop() + self.mock_load_config.stop() + self.mock_execute_show_command.stop() + self.mock_get_os_version.stop() + + def load_fixtures(self, commands=None, filename=None): + def load_from_file(*args, **kwargs): + return load_fixture("vyos_firewall_global_config_v15.cfg") + + self.execute_show_command.side_effect = load_from_file + + def test_vyos_firewall_global_set_01_merged_interface_uses_member_keyword(self): + # On a 1.5.0 device (empty fixture, no existing zone), merging a + # zone with an interface should render "member interface", not the + # bare "interface" used on 1.4.x / 1.5-rolling. This is the sole + # thing this class exists to prove right now -- the version-gate + # added to _render_interfaces. + set_module_args( + dict( + config=dict( + zone=[ + dict( + name="ZONE-15", + interfaces=["eth1"], + ), + ], + ), + state="merged", + ), + ) + commands = [ + "set firewall zone ZONE-15 default-action 'drop'", + "set firewall zone ZONE-15 member interface eth1", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_firewall_global_set_02_gathered_member_interface_parsed(self): + # ZONE-15-EXISTING in the fixture uses 1.5.0's "member interface" + # syntax. Before the parse_zone fix, this misparsed into a bogus + # zone['member'] = "interface eth2" string field, and 'interfaces' + # was missing entirely. After the fix, it should land in + # 'interfaces' exactly like the pre-1.5.0 bare "interface" form. + set_module_args(dict(config=dict(), state="gathered")) + result = self.execute_module(changed=False) + zones = result["gathered"]["zone"] + zone = next(z for z in zones if z["name"] == "ZONE-15-EXISTING") + self.assertEqual(zone["interfaces"], ["eth2"]) + self.assertEqual(zone["description"], "existing 1.5.0 zone for facts parsing test") + self.assertNotIn("member", zone) + + def test_vyos_firewall_global_ruleset_lines_filtered_from_facts(self): + # Same coverage as the 1.4 version, confirmed independently on the + # 1.5.0 fixture/version path. + set_module_args(dict(config=dict(), state="gathered")) + result = self.execute_module(changed=False) + facts = result["gathered"] + self.assertNotIn("TESTRULESET-V4", str(facts)) + self.assertNotIn("TESTRULESET-V6-LEGACY", str(facts)) + self.assertNotIn("TESTRULESET-V6-1_4PLUS", str(facts)) |
