summaryrefslogtreecommitdiff
path: root/tests/unit/modules/test_vyos_firewall_global.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/unit/modules/test_vyos_firewall_global.py')
-rw-r--r--tests/unit/modules/test_vyos_firewall_global.py380
1 files changed, 272 insertions, 108 deletions
diff --git a/tests/unit/modules/test_vyos_firewall_global.py b/tests/unit/modules/test_vyos_firewall_global.py
index a507c0c..e3aff0b 100644
--- a/tests/unit/modules/test_vyos_firewall_global.py
+++ b/tests/unit/modules/test_vyos_firewall_global.py
@@ -4,38 +4,165 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
-import json
-import os
import unittest
-from unittest.mock import MagicMock
-
+from ansible_collections.vyos.rest.plugins.module_utils.vyos import dict_op
from ansible_collections.vyos.rest.plugins.modules.vyos_firewall_global import (
+ _device_to_argspec,
+ _groups_from_device,
+ _groups_to_device,
+ _want_to_device,
build_commands,
get_running_config,
)
+from .base import load_fixture
+
_BASE = ["firewall", "group"]
-def load_fixture(filename):
- fixtures_dir = os.path.join(os.path.dirname(__file__), "..", "fixtures")
- with open(os.path.join(fixtures_dir, filename)) as f:
- return json.load(f)
+class TestGroupHelpers(unittest.TestCase):
+ """Test group list <-> device dict conversion helpers."""
+ def test_groups_to_device_with_members(self):
+ groups = [{"name": "SERVERS", "address": ["192.168.1.10", "192.168.1.11"]}]
+ result = _groups_to_device(groups, "address")
+ self.assertIn("SERVERS", result)
+ self.assertIn("192.168.1.10", result["SERVERS"]["address"])
+ self.assertIn("192.168.1.11", result["SERVERS"]["address"])
-class VyOSModuleTestCase(unittest.TestCase):
- def setUp(self):
- self.mock_vyos = MagicMock()
- self.fixture = load_fixture("firewall_global_running.json")
- self.mock_vyos.get_config = MagicMock(return_value=self.fixture)
+ def test_groups_to_device_with_description(self):
+ groups = [{"name": "LAN", "description": "Local network", "network": ["192.168.0.0/16"]}]
+ result = _groups_to_device(groups, "network")
+ self.assertEqual(result["LAN"]["description"], "Local network")
+
+ def test_groups_to_device_empty(self):
+ self.assertEqual(_groups_to_device([], "address"), {})
+ self.assertEqual(_groups_to_device(None, "address"), {})
+
+ def test_groups_from_device_with_members(self):
+ raw = {"SERVERS": {"address": {"192.168.1.10": {}, "192.168.1.11": {}}}}
+ result = _groups_from_device(raw, "address")
+ self.assertEqual(len(result), 1)
+ self.assertEqual(result[0]["name"], "SERVERS")
+ self.assertIn("192.168.1.10", result[0]["address"])
+
+ def test_groups_from_device_single_member_string(self):
+ # VyOS returns single member as string
+ raw = {"WEB": {"port": "80"}}
+ result = _groups_from_device(raw, "port")
+ self.assertIn("80", result[0]["port"])
+
+ def test_groups_from_device_with_description(self):
+ raw = {"LAN": {"description": "Local", "network": {"192.168.0.0/16": {}}}}
+ result = _groups_from_device(raw, "network")
+ self.assertEqual(result[0]["description"], "Local")
+
+ def test_groups_from_device_sorted(self):
+ raw = {"Z-GROUP": {}, "A-GROUP": {}}
+ result = _groups_from_device(raw, "address")
+ self.assertEqual(result[0]["name"], "A-GROUP")
+ self.assertEqual(result[1]["name"], "Z-GROUP")
+
+ def test_groups_from_device_empty(self):
+ self.assertEqual(_groups_from_device({}, "address"), [])
+ self.assertEqual(_groups_from_device(None, "address"), [])
+
+
+class TestWantToDevice(unittest.TestCase):
+ """Test argspec -> device shape conversion."""
+
+ def test_empty(self):
+ self.assertEqual(_want_to_device({}), {})
+ self.assertEqual(_want_to_device(None), {})
+
+ def test_address_group(self):
+ config = {
+ "group": {
+ "address_group": [
+ {"name": "SERVERS", "address": ["192.168.1.10"]},
+ ],
+ },
+ }
+ result = _want_to_device(config)
+ self.assertIn("address-group", result)
+ self.assertIn("SERVERS", result["address-group"])
+ self.assertIn("192.168.1.10", result["address-group"]["SERVERS"]["address"])
+
+ def test_network_group(self):
+ config = {
+ "group": {
+ "network_group": [{"name": "LAN", "network": ["192.168.0.0/16"]}],
+ },
+ }
+ result = _want_to_device(config)
+ self.assertIn("network-group", result)
+ self.assertIn("LAN", result["network-group"])
+
+ def test_port_group(self):
+ config = {
+ "group": {
+ "port_group": [{"name": "WEB", "port": ["80", "443"]}],
+ },
+ }
+ result = _want_to_device(config)
+ self.assertIn("port-group", result)
+ self.assertIn("80", result["port-group"]["WEB"]["port"])
+
+ def test_interface_group(self):
+ config = {
+ "group": {
+ "interface_group": [{"name": "LAN-IFACES", "interface": ["eth1"]}],
+ },
+ }
+ result = _want_to_device(config)
+ self.assertIn("interface-group", result)
+
+ def test_ipv6_network_group(self):
+ config = {
+ "group": {
+ "ipv6_network_group": [{"name": "IPV6-LAN", "network": ["2001:db8::/32"]}],
+ },
+ }
+ result = _want_to_device(config)
+ self.assertIn("ipv6-network-group", result)
-class TestVyOSFirewallGlobalGetRunning(VyOSModuleTestCase):
+class TestDeviceToArgspec(unittest.TestCase):
+ """Test device response -> argspec shape conversion."""
+
+ def test_empty(self):
+ self.assertEqual(_device_to_argspec({}), {})
+ self.assertEqual(_device_to_argspec(None), {})
+
+ def test_address_group(self):
+ raw = {
+ "address-group": {
+ "SERVERS": {
+ "description": "Web servers",
+ "address": {"192.168.1.10": {}, "192.168.1.11": {}},
+ },
+ },
+ }
+ result = _device_to_argspec(raw)
+ groups = result["group"]["address_group"]
+ servers = next(g for g in groups if g["name"] == "SERVERS")
+ self.assertEqual(servers["description"], "Web servers")
+ self.assertIn("192.168.1.10", servers["address"])
+
+ def test_no_group_returns_empty(self):
+ self.assertEqual(_device_to_argspec({}), {})
+
+
+class TestDeviceToArgspecFixture(unittest.TestCase):
+ """Test _device_to_argspec against fixture."""
+
+ def setUp(self):
+ self.fixture = load_fixture("firewall_global_running.json")
def test_parses_address_groups(self):
- result = get_running_config(self.mock_vyos)
+ result = _device_to_argspec(self.fixture)
groups = result["group"]["address_group"]
names = [g["name"] for g in groups]
self.assertIn("SERVERS", names)
@@ -43,148 +170,185 @@ class TestVyOSFirewallGlobalGetRunning(VyOSModuleTestCase):
servers = next(g for g in groups if g["name"] == "SERVERS")
self.assertEqual(servers["description"], "Web servers")
self.assertIn("192.168.1.10", servers["address"])
- self.assertIn("192.168.1.11", servers["address"])
def test_parses_network_groups(self):
- result = get_running_config(self.mock_vyos)
+ result = _device_to_argspec(self.fixture)
groups = result["group"]["network_group"]
dmz = next(g for g in groups if g["name"] == "DMZ")
self.assertIn("10.0.0.0/8", dmz["network"])
- self.assertIn("172.16.0.0/12", dmz["network"])
def test_parses_port_groups(self):
- result = get_running_config(self.mock_vyos)
+ result = _device_to_argspec(self.fixture)
groups = result["group"]["port_group"]
web = next(g for g in groups if g["name"] == "WEB-PORTS")
self.assertIn("80", web["port"])
- self.assertIn("443", web["port"])
def test_parses_interface_groups(self):
- result = get_running_config(self.mock_vyos)
+ result = _device_to_argspec(self.fixture)
groups = result["group"]["interface_group"]
lan = next(g for g in groups if g["name"] == "LAN-IFACES")
self.assertIn("eth1", lan["interface"])
def test_parses_ipv6_network_groups(self):
- result = get_running_config(self.mock_vyos)
+ result = _device_to_argspec(self.fixture)
groups = result["group"]["ipv6_network_group"]
ipv6 = next(g for g in groups if g["name"] == "IPV6-LAN")
self.assertIn("2001:db8::/32", ipv6["network"])
def test_empty_config(self):
- self.mock_vyos.get_config = MagicMock(return_value={})
- result = get_running_config(self.mock_vyos)
+ result = _device_to_argspec({})
self.assertEqual(result, {})
-class TestVyOSFirewallGlobalBuildCommands(unittest.TestCase):
+class TestDictOpFirewall(unittest.TestCase):
+ """Test dict_op behaviour with firewall group shapes."""
- def _have(self):
- return {
- "group": {
- "address_group": [
- {"name": "SERVERS", "address": ["192.168.1.10", "192.168.1.11"]},
- ],
- "network_group": [
- {"name": "LAN", "network": ["192.168.0.0/16"]},
- ],
+ def test_merged_adds_address_group(self):
+ want = _want_to_device(
+ {
+ "group": {
+ "address_group": [{"name": "SERVERS", "address": ["192.168.1.10"]}],
+ },
},
- }
-
- def test_deleted_with_have(self):
- cmds = build_commands({}, self._have(), "deleted")
- self.assertEqual(cmds, [("delete", _BASE)])
-
- def test_deleted_without_have(self):
- cmds = build_commands({}, {}, "deleted")
- self.assertEqual(cmds, [])
-
- def test_merged_address_group(self):
- config = {
- "group": {
- "address_group": [
- {"name": "SERVERS", "address": ["192.168.1.10"]},
- ],
- },
- }
- cmds = build_commands(config, {}, "merged")
- self.assertIn(
- ("set", _BASE + ["address-group", "SERVERS", "address", "192.168.1.10"]),
- cmds,
- )
-
- def test_merged_network_group(self):
- config = {
- "group": {
- "network_group": [
- {"name": "LAN", "network": ["192.168.0.0/16"]},
- ],
- },
- }
- cmds = build_commands(config, {}, "merged")
- self.assertIn(
- ("set", _BASE + ["network-group", "LAN", "network", "192.168.0.0/16"]),
- cmds,
- )
-
- def test_merged_port_group(self):
- config = {
- "group": {
- "port_group": [
- {"name": "WEB", "port": ["80", "443"]},
- ],
- },
- }
- cmds = build_commands(config, {}, "merged")
- self.assertIn(
- ("set", _BASE + ["port-group", "WEB", "port", "80"]),
- cmds,
)
+ cmds = dict_op(want, {}, _BASE, op="set")
+ paths = [c[1] for c in cmds]
+ self.assertIn(_BASE + ["address-group", "SERVERS", "address", "192.168.1.10"], paths)
def test_merged_idempotent(self):
- have = self._have()
config = {
"group": {
- "address_group": [
- {"name": "SERVERS", "address": ["192.168.1.10", "192.168.1.11"]},
- ],
- "network_group": [
- {"name": "LAN", "network": ["192.168.0.0/16"]},
- ],
+ "address_group": [{"name": "SERVERS", "address": ["192.168.1.10"]}],
},
}
- cmds = build_commands(config, have, "merged")
+ want = _want_to_device(config)
+ have = {"address-group": {"SERVERS": {"address": {"192.168.1.10": {}}}}}
+ cmds = dict_op(want, have, _BASE, op="set")
self.assertEqual(cmds, [])
- def test_replaced_removes_extra_group(self):
- have = self._have()
- config = {
- "group": {
- "network_group": [
- {"name": "DMZ", "network": ["10.0.0.0/8"]},
- ],
+ def test_merged_adds_network_group(self):
+ want = _want_to_device(
+ {
+ "group": {
+ "network_group": [{"name": "LAN", "network": ["192.168.0.0/16"]}],
+ },
},
+ )
+ cmds = dict_op(want, {}, _BASE, op="set")
+ paths = [c[1] for c in cmds]
+ self.assertIn(_BASE + ["network-group", "LAN", "network", "192.168.0.0/16"], paths)
+
+ def test_purge_removes_extra_group(self):
+ want = _want_to_device(
+ {
+ "group": {
+ "network_group": [{"name": "DMZ", "network": ["10.0.0.0/8"]}],
+ },
+ },
+ )
+ have = {
+ "address-group": {"SERVERS": {"address": {"192.168.1.10": {}}}},
+ "network-group": {"LAN": {"network": {"192.168.0.0/16": {}}}},
}
- cmds = build_commands(config, have, "replaced")
+ cmds = dict_op(want, have, _BASE, op="purge")
paths = [c[1] for c in cmds]
- self.assertIn(_BASE + ["address-group", "SERVERS"], paths)
+ # address-group entirely absent from want -> whole type deleted
+ self.assertIn(_BASE + ["address-group"], paths)
self.assertIn(_BASE + ["network-group", "LAN"], paths)
- def test_replaced_idempotent(self):
- have = self._have()
+ def test_merged_idempotent_with_description(self):
+ """Regression test: the original implementation's have-side
+ normalization corrupted plain scalar fields (description) into
+ bogus presence-dicts via the same blanket conversion used for
+ member lists, breaking idempotency for any group with a
+ description set. Fixed by using dict_op's native list handling
+ for members and never touching scalar fields at all."""
config = {
"group": {
"address_group": [
- {"name": "SERVERS", "address": ["192.168.1.10", "192.168.1.11"]},
- ],
- "network_group": [
- {"name": "LAN", "network": ["192.168.0.0/16"]},
+ {"name": "SERVERS", "description": "Web servers", "address": ["10.0.0.1"]},
],
},
}
- cmds = build_commands(config, have, "replaced")
+ want = _want_to_device(config)
+ have = {
+ "address-group": {
+ "SERVERS": {"description": "Web servers", "address": ["10.0.0.1"]},
+ },
+ }
+ cmds = dict_op(want, have, _BASE, op="set")
self.assertEqual(cmds, [])
+ def test_replaced_removes_stale_member(self):
+ """Regression test: dict_op's purge mode originally had no
+ handling for list-valued leaves (only dicts), so a member
+ present on the device but absent from the desired list was
+ silently never removed under 'replaced'. Fixed centrally in
+ dict_op itself."""
+ want = _want_to_device(
+ {"group": {"address_group": [{"name": "SERVERS", "address": ["10.0.0.1"]}]}},
+ )
+ have = {"address-group": {"SERVERS": {"address": ["10.0.0.1", "10.0.0.2"]}}}
+ cmds = dict_op(want, have, _BASE, op="purge")
+ self.assertIn(
+ ("delete", _BASE + ["address-group", "SERVERS", "address", "10.0.0.2"]),
+ cmds,
+ )
+
+ def test_deleted_removes_base(self):
+ # deleted state is handled in main() not dict_op
+ # just verify want_to_device produces correct shape
+ want = _want_to_device({})
+ self.assertEqual(want, {})
+
+
+class TestBuildCommands(unittest.TestCase):
+ """build_commands/get_running_config were extracted from main() during
+ the dict_op refactor so they're independently testable."""
+
+ def test_get_running_config_fetches_at_base(self):
+ from unittest.mock import MagicMock
+
+ mock_vyos = MagicMock()
+ mock_vyos.get_config = MagicMock(return_value={"address-group": {}})
+ get_running_config(mock_vyos)
+ mock_vyos.get_config.assert_called_once_with(_BASE)
+
+ def test_merged_idempotent_against_fixture(self):
+ fixture = load_fixture("firewall_global_running.json")
+ have = _device_to_argspec(fixture)
+ self.assertEqual(build_commands(have, fixture, "merged"), [])
+
+ def test_replaced_idempotent_against_fixture(self):
+ fixture = load_fixture("firewall_global_running.json")
+ have = _device_to_argspec(fixture)
+ self.assertEqual(build_commands(have, fixture, "replaced"), [])
+
+ def test_deleted_no_have_is_noop(self):
+ self.assertEqual(build_commands({}, {}, "deleted"), [])
+
+ def test_deleted_with_have(self):
+ self.assertEqual(
+ build_commands({}, {"address-group": {"SERVERS": {}}}, "deleted"),
+ [("delete", _BASE)],
+ )
+
+ def test_single_value_member_collapse_no_char_iteration_bug(self):
+ """A group with exactly one member, collapsed by the device to a
+ bare string instead of a list, must not be iterated
+ character-by-character."""
+ raw_have = {"port-group": {"WEB": {"port": "80"}}}
+ config = {"group": {"port_group": [{"name": "WEB", "port": ["80"]}]}}
+ self.assertEqual(build_commands(config, raw_have, "merged"), [])
+
+ def test_group_tag_node_collapse_no_char_iteration_bug(self):
+ """A single group with zero other fields, collapsed by the
+ device to a bare group-name string, must not be iterated
+ character-by-character."""
+ raw_have = {"address-group": "SERVERS"}
+ config = {"group": {"address_group": [{"name": "SERVERS"}]}}
+ self.assertEqual(build_commands(config, raw_have, "merged"), [])
+
if __name__ == "__main__":
unittest.main()