diff options
| author | omnom62 <omnom62@outlook.com> | 2026-05-27 21:38:51 +1000 |
|---|---|---|
| committer | omnom62 <omnom62@outlook.com> | 2026-05-27 21:38:51 +1000 |
| commit | aeedc73003ddcf84bbf5c2880f982b6d4925e0c9 (patch) | |
| tree | b2bf9705e2fe6db7dbaa51afc5fd60ca0f7878da /tests/unit/modules | |
| parent | 5a6f6976b49fd5ca8e964b2342493bc5ceee85d5 (diff) | |
| download | rest.vyos-aeedc73003ddcf84bbf5c2880f982b6d4925e0c9.tar.gz rest.vyos-aeedc73003ddcf84bbf5c2880f982b6d4925e0c9.zip | |
Test framework
Diffstat (limited to 'tests/unit/modules')
| -rw-r--r-- | tests/unit/modules/base.py | 53 | ||||
| -rw-r--r-- | tests/unit/modules/conftest.py | 2 | ||||
| -rw-r--r-- | tests/unit/modules/test_vyos_logging_global.py | 229 | ||||
| -rw-r--r-- | tests/unit/modules/test_vyos_ntp_global.py | 169 | ||||
| -rw-r--r-- | tests/unit/modules/test_vyos_prefix_lists.py | 209 | ||||
| -rw-r--r-- | tests/unit/modules/test_vyos_route_maps.py | 180 |
6 files changed, 842 insertions, 0 deletions
diff --git a/tests/unit/modules/base.py b/tests/unit/modules/base.py new file mode 100644 index 0000000..e5a602e --- /dev/null +++ b/tests/unit/modules/base.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import json +import os +import unittest + +from unittest.mock import MagicMock, patch # noqa: F401 + + +def load_fixture(filename): + """Load a JSON fixture file from tests/unit/fixtures/.""" + fixtures_dir = os.path.join(os.path.dirname(__file__), "..", "fixtures") + path = os.path.join(fixtures_dir, filename) + with open(path) as f: + return json.load(f) + + +class VyOSModuleTestCase(unittest.TestCase): + """ + Base class for vyos.rest module unit tests. + + Provides a mock VyOSModule that returns fixture data from + get_config() without any device connection. + + Usage: + class TestVyOSNtpGlobal(VyOSModuleTestCase): + def setUp(self): + super().setUp() + self.fixture = load_fixture("ntp_global_running.json") + + def test_merged_adds_new_server(self): + have = self.module.get_running_config_from_fixture(self.fixture) + commands = build_commands(want, have, "merged") + self.assertIn(("set", ["service", "ntp", "server", "1.2.3.4"]), commands) + """ + + def setUp(self): + self.mock_module = MagicMock() + self.mock_module.params = {} + self.mock_module.check_mode = False + + self.mock_vyos = MagicMock() + self.mock_vyos.get_config = MagicMock(return_value={}) + + def set_running_config(self, data): + """Configure mock get_config to return given data.""" + self.mock_vyos.get_config.return_value = data diff --git a/tests/unit/modules/conftest.py b/tests/unit/modules/conftest.py new file mode 100644 index 0000000..26b7d8b --- /dev/null +++ b/tests/unit/modules/conftest.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# conftest.py — shared fixtures for vyos.rest unit tests diff --git a/tests/unit/modules/test_vyos_logging_global.py b/tests/unit/modules/test_vyos_logging_global.py new file mode 100644 index 0000000..e9ecc8c --- /dev/null +++ b/tests/unit/modules/test_vyos_logging_global.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import os +import sys +import unittest + + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from ansible_collections.vyos.rest.plugins.modules.vyos_logging_global import ( + build_commands, + normalize_config, + normalize_running, +) + +from tests.unit.modules.base import VyOSModuleTestCase, load_fixture + + +class TestVyOSLoggingGlobalNormalize(unittest.TestCase): + + def test_normalize_config_console(self): + cfg = { + "console": { + "facilities": [{"facility": "local7", "severity": "err"}], + }, + } + result = normalize_config(cfg) + self.assertIn("local7", result["console"]["facilities"]) + self.assertEqual( + result["console"]["facilities"]["local7"], + {"severity": "err", "protocol": None}, + ) + + def test_normalize_config_hosts(self): + cfg = { + "hosts": [ + { + "hostname": "172.16.0.1", + "port": 514, + "facilities": [ + {"facility": "local7", "severity": "all"}, + {"facility": "all", "protocol": "udp"}, + ], + }, + ], + } + result = normalize_config(cfg) + self.assertIn("172.16.0.1", result["hosts"]) + host = result["hosts"]["172.16.0.1"] + self.assertEqual(host["port"], 514) + self.assertIn("local7", host["facilities"]) + self.assertEqual(host["facilities"]["local7"]["severity"], "all") + self.assertEqual(host["facilities"]["all"]["protocol"], "udp") + + def test_normalize_running_console(self): + raw = { + "console": { + "facility": { + "local7": {"level": "err"}, + "all": {}, + }, + }, + } + result = normalize_running(raw) + self.assertIn("local7", result["console"]["facilities"]) + self.assertEqual(result["console"]["facilities"]["local7"]["severity"], "err") + self.assertIsNone(result["console"]["facilities"]["all"]["severity"]) + + def test_normalize_running_global_archive(self): + raw = { + "global": { + "archive": {"file": "2", "size": "111"}, + "marker": {"interval": "111"}, + "preserve-fqdn": {}, + }, + } + result = normalize_running(raw) + self.assertEqual(result["global"]["archive"]["file_num"], 2) + self.assertEqual(result["global"]["archive"]["size"], 111) + self.assertEqual(result["global"]["marker_interval"], 111) + self.assertTrue(result["global"]["preserve_fqdn"]) + + def test_normalize_running_host_port_cast(self): + raw = { + "host": { + "172.16.0.1": { + "port": "514", + "facility": {}, + }, + }, + } + result = normalize_running(raw) + self.assertEqual(result["hosts"]["172.16.0.1"]["port"], 514) + + def test_normalize_running_empty(self): + result = normalize_running({}) + self.assertEqual(result["console"]["facilities"], {}) + self.assertEqual(result["hosts"], {}) + + +class TestVyOSLoggingGlobalBuildCommands(unittest.TestCase): + + def _empty_have(self): + return { + "console": {"facilities": {}}, + "global": {"facilities": {}}, + "hosts": {}, + "files": {}, + "users": {}, + } + + def test_merged_adds_console_facility(self): + want = self._empty_have() + want["console"]["facilities"]["local7"] = {"severity": "err", "protocol": None} + cmds = build_commands(want, self._empty_have(), "merged") + self.assertIn( + ("set", ["system", "syslog", "console", "facility", "local7", "level", "err"]), + cmds, + ) + + def test_merged_idempotent_console(self): + facs = {"local7": {"severity": "err", "protocol": None}} + want = self._empty_have() + have = self._empty_have() + want["console"]["facilities"] = facs + have["console"]["facilities"] = facs.copy() + cmds = build_commands(want, have, "merged") + self.assertEqual(cmds, []) + + def test_merged_adds_host_with_port(self): + want = self._empty_have() + want["hosts"]["172.16.0.1"] = { + "port": 514, + "facilities": {"local7": {"severity": "all", "protocol": None}}, + } + cmds = build_commands(want, self._empty_have(), "merged") + paths = [c[1] for c in cmds] + self.assertIn(["system", "syslog", "host", "172.16.0.1", "port", "514"], paths) + self.assertIn( + ["system", "syslog", "host", "172.16.0.1", "facility", "local7", "level", "all"], + paths, + ) + + def test_merged_adds_host_facility_protocol(self): + want = self._empty_have() + want["hosts"]["172.16.0.1"] = { + "port": None, + "facilities": {"all": {"severity": None, "protocol": "udp"}}, + } + cmds = build_commands(want, self._empty_have(), "merged") + self.assertIn( + ( + "set", + ["system", "syslog", "host", "172.16.0.1", "facility", "all", "protocol", "udp"], + ), + cmds, + ) + + def test_replaced_removes_extra_host(self): + want = self._empty_have() + have = self._empty_have() + have["hosts"]["172.16.0.1"] = {"port": None, "facilities": {}} + cmds = build_commands(want, have, "replaced") + self.assertIn(("delete", ["system", "syslog", "host", "172.16.0.1"]), cmds) + + def test_deleted_issues_single_delete(self): + have = self._empty_have() + have["console"]["facilities"]["all"] = {"severity": None, "protocol": None} + cmds = build_commands(self._empty_have(), have, "deleted") + self.assertIn(("delete", ["system", "syslog"]), cmds) + + def test_overridden_deletes_then_merges(self): + want = self._empty_have() + have = self._empty_have() + have["console"]["facilities"]["all"] = {"severity": None, "protocol": None} + cmds = build_commands(want, have, "overridden") + self.assertIn(("delete", ["system", "syslog"]), cmds) + + def test_global_preserve_fqdn_added(self): + want = self._empty_have() + want["global"]["preserve_fqdn"] = True + cmds = build_commands(want, self._empty_have(), "merged") + self.assertIn(("set", ["system", "syslog", "global", "preserve-fqdn"]), cmds) + + def test_global_archive(self): + want = self._empty_have() + want["global"]["archive"] = {"file_num": 2, "size": 111} + cmds = build_commands(want, self._empty_have(), "merged") + paths = [c[1] for c in cmds] + self.assertIn(["system", "syslog", "global", "archive", "file", "2"], paths) + self.assertIn(["system", "syslog", "global", "archive", "size", "111"], paths) + + +class TestVyOSLoggingGlobalFixture(VyOSModuleTestCase): + """Test parsing against the confirmed device fixture.""" + + def setUp(self): + super().setUp() + self.fixture = load_fixture("logging_global_running.json") + + def test_fixture_parses_console(self): + result = normalize_running(self.fixture) + self.assertIn("local7", result["console"]["facilities"]) + self.assertEqual(result["console"]["facilities"]["local7"]["severity"], "err") + + def test_fixture_parses_host_port(self): + result = normalize_running(self.fixture) + self.assertEqual(result["hosts"]["172.16.0.1"]["port"], 223) + + def test_fixture_parses_global_archive(self): + result = normalize_running(self.fixture) + self.assertEqual(result["global"]["archive"]["file_num"], 2) + self.assertEqual(result["global"]["archive"]["size"], 111) + + def test_fixture_parses_preserve_fqdn(self): + result = normalize_running(self.fixture) + self.assertTrue(result["global"]["preserve_fqdn"]) + + def test_fixture_parses_marker_interval(self): + result = normalize_running(self.fixture) + self.assertEqual(result["global"]["marker_interval"], 111) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_ntp_global.py b/tests/unit/modules/test_vyos_ntp_global.py new file mode 100644 index 0000000..d646b1a --- /dev/null +++ b/tests/unit/modules/test_vyos_ntp_global.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import os +import sys +import unittest + + +# Allow importing collection modules without full ansible-test runner +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from ansible_collections.vyos.rest.plugins.modules.vyos_ntp_global import ( + build_commands, + get_running_config, + normalize_config, + normalize_servers, +) + +from tests.unit.modules.base import VyOSModuleTestCase, load_fixture + + +class TestVyOSNtpGlobalNormalize(unittest.TestCase): + """Test normalize_config and normalize_servers — no device needed.""" + + def test_normalize_config_empty(self): + result = normalize_config({}) + self.assertEqual(result["allow_clients"], []) + self.assertEqual(result["listen_addresses"], []) + self.assertEqual(result["servers"], {}) + + def test_normalize_config_servers_sorted(self): + config = { + "servers": [ + {"server": "b.example.com", "options": ["prefer", "noselect"]}, + {"server": "a.example.com"}, + ], + } + result = normalize_config(config) + self.assertIn("a.example.com", result["servers"]) + self.assertIn("b.example.com", result["servers"]) + self.assertEqual(result["servers"]["b.example.com"], ["noselect", "prefer"]) + + def test_normalize_servers_dict_with_options(self): + raw = { + "time1.vyos.net": {}, + "203.0.113.0": {"prefer": {}}, + } + result = normalize_servers(raw) + self.assertEqual(result["time1.vyos.net"], []) + self.assertEqual(result["203.0.113.0"], ["prefer"]) + + def test_normalize_servers_list(self): + raw = ["time1.vyos.net", "time2.vyos.net"] + result = normalize_servers(raw) + self.assertEqual(result["time1.vyos.net"], []) + + def test_normalize_servers_string(self): + result = normalize_servers("time1.vyos.net") + self.assertEqual(result["time1.vyos.net"], []) + + +class TestVyOSNtpGlobalGetRunning(VyOSModuleTestCase): + """Test get_running_config parsing against fixture API responses.""" + + def setUp(self): + super().setUp() + self.fixture = load_fixture("ntp_global_running.json") + + def test_parses_allow_clients(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + self.assertIn("10.6.6.0/24", result["allow_clients"]) + + def test_parses_listen_addresses(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + self.assertIn("10.1.3.1", result["listen_addresses"]) + + def test_parses_servers(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + self.assertIn("time1.vyos.net", result["servers"]) + self.assertIn("203.0.113.0", result["servers"]) + self.assertIn("prefer", result["servers"]["203.0.113.0"]) + + def test_empty_config_returns_empty(self): + self.set_running_config({}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result["allow_clients"], []) + self.assertEqual(result["servers"], {}) + + +class TestVyOSNtpGlobalBuildCommands(unittest.TestCase): + """Test build_commands diff logic — no device needed.""" + + def _have(self, **kwargs): + base = {"allow_clients": [], "listen_addresses": [], "servers": {}} + base.update(kwargs) + return base + + def _want(self, **kwargs): + return self._have(**kwargs) + + def test_merged_adds_new_server(self): + want = self._want(servers={"new.server.com": []}) + have = self._have(servers={}) + cmds = build_commands(want, have, "merged") + self.assertIn(("set", ["service", "ntp", "server", "new.server.com"]), cmds) + + def test_merged_idempotent_existing_server(self): + want = self._want(servers={"time1.vyos.net": []}) + have = self._have(servers={"time1.vyos.net": []}) + cmds = build_commands(want, have, "merged") + self.assertEqual(cmds, []) + + def test_merged_adds_server_option(self): + want = self._want(servers={"time1.vyos.net": ["prefer"]}) + have = self._have(servers={"time1.vyos.net": []}) + cmds = build_commands(want, have, "merged") + self.assertIn(("set", ["service", "ntp", "server", "time1.vyos.net", "prefer"]), cmds) + + def test_replaced_removes_extra_server(self): + want = self._want(servers={"time1.vyos.net": []}) + have = self._have(servers={"time1.vyos.net": [], "time2.vyos.net": []}) + cmds = build_commands(want, have, "replaced") + self.assertIn(("delete", ["service", "ntp", "server", "time2.vyos.net"]), cmds) + + def test_replaced_removes_extra_allow_client(self): + want = self._want(allow_clients=["10.1.0.0/24"]) + have = self._have(allow_clients=["10.1.0.0/24", "10.2.0.0/24"]) + cmds = build_commands(want, have, "replaced") + self.assertIn( + ("delete", ["service", "ntp", "allow-client", "address", "10.2.0.0/24"]), + cmds, + ) + + def test_deleted_removes_all(self): + want = self._want(servers={}, allow_clients=[], listen_addresses=[]) + have = self._have( + servers={"time1.vyos.net": []}, + allow_clients=["10.0.0.0/24"], + listen_addresses=["192.168.1.1"], + ) + cmds = build_commands(want, have, "deleted") + paths = [c[1] for c in cmds] + self.assertIn(["service", "ntp", "server", "time1.vyos.net"], paths) + self.assertIn(["service", "ntp", "allow-client", "address", "10.0.0.0/24"], paths) + + def test_overridden_deletes_then_merges(self): + want = self._want(servers={"new.server.com": []}) + have = self._have(servers={"old.server.com": []}) + cmds = build_commands(want, have, "overridden") + paths = [c[1] for c in cmds] + # overridden deletes existing server subtree first + self.assertIn(["service", "ntp", "server"], paths) + # then adds new server + self.assertIn(["service", "ntp", "server", "new.server.com"], paths) + + def test_no_commands_when_already_correct(self): + state = {"allow_clients": ["10.0.0.0/24"], "listen_addresses": [], "servers": {}} + cmds = build_commands(state, state, "merged") + self.assertEqual(cmds, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_prefix_lists.py b/tests/unit/modules/test_vyos_prefix_lists.py new file mode 100644 index 0000000..d8b75df --- /dev/null +++ b/tests/unit/modules/test_vyos_prefix_lists.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import os +import sys +import unittest + + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from ansible_collections.vyos.rest.plugins.modules.vyos_prefix_lists import ( + _normalize, + build_commands, + get_running_config, +) + +from tests.unit.modules.base import VyOSModuleTestCase, load_fixture + + +class TestVyOSPrefixListsGetRunning(VyOSModuleTestCase): + + def setUp(self): + super().setUp() + self.fixture = load_fixture("prefix_lists_running.json") + + def test_parses_ipv4_prefix_list(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + ipv4 = next((e for e in result if e["afi"] == "ipv4"), None) + self.assertIsNotNone(ipv4) + pl = next((p for p in ipv4["prefix_lists"] if p["name"] == "AnsibleIPv4PrefixList"), None) + self.assertIsNotNone(pl) + self.assertEqual(pl["description"], "PL configured by ansible") + + def test_parses_ipv4_rules(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + ipv4 = next(e for e in result if e["afi"] == "ipv4") + pl = ipv4["prefix_lists"][0] + seqs = [r["sequence"] for r in pl["entries"]] + self.assertIn(2, seqs) + self.assertIn(3, seqs) + + def test_parses_ipv4_rule_fields(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + ipv4 = next(e for e in result if e["afi"] == "ipv4") + rule2 = next(r for r in ipv4["prefix_lists"][0]["entries"] if r["sequence"] == 2) + self.assertEqual(rule2["action"], "permit") + self.assertEqual(rule2["prefix"], "92.168.10.0/26") + self.assertEqual(rule2["le"], 32) + + def test_parses_ipv6_prefix_lists(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + ipv6 = next((e for e in result if e["afi"] == "ipv6"), None) + self.assertIsNotNone(ipv6) + names = [p["name"] for p in ipv6["prefix_lists"]] + self.assertIn("AllowIPv6Prefix", names) + self.assertIn("DenyIPv6Prefix", names) + + def test_empty_returns_empty_list(self): + self.set_running_config({}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, []) + + +class TestVyOSPrefixListsNormalize(unittest.TestCase): + + def test_normalize_ipv4(self): + config = [ + { + "afi": "ipv4", + "prefix_lists": [ + { + "name": "PL1", + "entries": [{"sequence": 10, "action": "permit", "prefix": "10.0.0.0/8"}], + }, + ], + }, + ] + result = _normalize(config) + self.assertIn("PL1", result["ipv4"]) + self.assertIn(10, result["ipv4"]["PL1"]["rules"]) + self.assertEqual(result["ipv4"]["PL1"]["rules"][10]["action"], "permit") + + def test_normalize_filters_none_values(self): + config = [ + { + "afi": "ipv4", + "prefix_lists": [ + { + "name": "PL1", + "entries": [ + { + "sequence": 10, + "action": "permit", + "prefix": "10.0.0.0/8", + "ge": None, + "le": None, + }, + ], + }, + ], + }, + ] + result = _normalize(config) + rule = result["ipv4"]["PL1"]["rules"][10] + self.assertNotIn("ge", rule) + self.assertNotIn("le", rule) + + +class TestVyOSPrefixListsBuildCommands(unittest.TestCase): + + def _have_empty(self): + return [] + + def _have_with_ipv4_pl(self): + return [ + { + "afi": "ipv4", + "prefix_lists": [ + { + "name": "PL1", + "entries": [ + { + "sequence": 10, + "action": "permit", + "prefix": "10.0.0.0/8", + }, + ], + }, + ], + }, + ] + + def test_merged_adds_new_prefix_list(self): + config = [ + { + "afi": "ipv4", + "prefix_lists": [ + { + "name": "PL-NEW", + "entries": [ + { + "sequence": 5, + "action": "permit", + "prefix": "192.168.0.0/24", + }, + ], + }, + ], + }, + ] + cmds = build_commands(config, self._have_empty(), "merged") + paths = [c[1] for c in cmds] + self.assertIn(["policy", "prefix-list", "PL-NEW", "rule", "5", "action", "permit"], paths) + self.assertIn( + ["policy", "prefix-list", "PL-NEW", "rule", "5", "prefix", "192.168.0.0/24"], + paths, + ) + + def test_merged_idempotent_existing_rule(self): + config = self._have_with_ipv4_pl() + cmds = build_commands(config, self._have_with_ipv4_pl(), "merged") + self.assertEqual(cmds, []) + + def test_deleted_no_config_deletes_all(self): + cmds = build_commands([], self._have_with_ipv4_pl(), "deleted") + self.assertIn(("delete", ["policy", "prefix-list"]), cmds) + + def test_deleted_with_config_deletes_named(self): + config = [{"afi": "ipv4", "prefix_lists": [{"name": "PL1"}]}] + cmds = build_commands(config, self._have_with_ipv4_pl(), "deleted") + self.assertIn(("delete", ["policy", "prefix-list", "PL1"]), cmds) + + def test_replaced_deletes_then_resets(self): + config = [ + { + "afi": "ipv4", + "prefix_lists": [ + { + "name": "PL1", + "entries": [ + { + "sequence": 10, + "action": "deny", + "prefix": "10.0.0.0/8", + }, + ], + }, + ], + }, + ] + cmds = build_commands(config, self._have_with_ipv4_pl(), "replaced") + # Should delete PL1 first then re-add + ops = [(c[0], c[1]) for c in cmds] + delete_idx = next( + i for i, c in enumerate(ops) if c == ("delete", ["policy", "prefix-list", "PL1"]) + ) + set_idx = next(i for i, c in enumerate(ops) if c[0] == "set" and "deny" in c[1]) + self.assertLess(delete_idx, set_idx) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_route_maps.py b/tests/unit/modules/test_vyos_route_maps.py new file mode 100644 index 0000000..2d45bb7 --- /dev/null +++ b/tests/unit/modules/test_vyos_route_maps.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import os +import sys +import unittest + + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from ansible_collections.vyos.rest.plugins.modules.vyos_route_maps import ( + _want_to_api_match, + _want_to_api_set, + build_commands, + get_running_config, +) + +from tests.unit.modules.base import VyOSModuleTestCase, load_fixture + + +class TestVyOSRouteMapsGetRunning(VyOSModuleTestCase): + + def setUp(self): + super().setUp() + self.fixture = load_fixture("route_maps_running.json") + + def test_unwraps_route_map_nesting(self): + """API returns {"route-map": {"NAME": {...}}} — must unwrap.""" + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + names = [e["route_map"] for e in result] + self.assertIn("RM-TEST-EXPORT-POLICY", names) + self.assertIn("rm1", names) + # "route-map" itself must NOT appear as a route map name + self.assertNotIn("route-map", names) + + def test_parses_rule_action(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + rm = next(e for e in result if e["route_map"] == "RM-TEST-EXPORT-POLICY") + rule = rm["entries"][0] + self.assertEqual(rule["action"], "permit") + self.assertEqual(rule["sequence"], 10) + + def test_parses_match_peer(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + rm = next(e for e in result if e["route_map"] == "RM-TEST-EXPORT-POLICY") + rule = rm["entries"][0] + self.assertEqual(rule["match"]["peer"], "192.0.2.32") + + def test_parses_set_fields(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + rm = next(e for e in result if e["route_map"] == "RM-TEST-EXPORT-POLICY") + rule = rm["entries"][0] + self.assertEqual(rule["set"]["metric"], "5") + self.assertEqual(rule["set"]["aggregator"]["as"], "100") + self.assertEqual(rule["set"]["as-path"]["exclude"], "111") + + def test_empty_returns_empty_list(self): + self.set_running_config({}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, []) + + +class TestVyOSRouteMapsWantToApi(unittest.TestCase): + + def test_as_path_exclude_nested(self): + """as_path_exclude maps to nested as-path.exclude.""" + result = _want_to_api_set({"as_path_exclude": "111"}) + self.assertEqual(result["as-path"]["exclude"], "111") + + def test_metric_flat(self): + result = _want_to_api_set({"metric": "5"}) + self.assertEqual(result["metric"], "5") + + def test_aggregator_as(self): + result = _want_to_api_set({"aggregator": {"as": 100}}) + self.assertEqual(result["aggregator"]["as"], "100") + + def test_aggregator_as_underscore(self): + """aggregator.as_ is an alias for aggregator.as.""" + result = _want_to_api_set({"aggregator": {"as_": 100}}) + self.assertEqual(result["aggregator"]["as"], "100") + + def test_large_community_presence_node(self): + result = _want_to_api_set({"large_community": "none"}) + self.assertEqual(result["large-community"], {"none": {}}) + + def test_match_peer(self): + result = _want_to_api_match({"peer": "192.0.2.32"}) + self.assertEqual(result["peer"], "192.0.2.32") + + +class TestVyOSRouteMapsBuildCommands(unittest.TestCase): + + def _have_empty(self): + return [] + + def _have_with_rm(self): + return [ + { + "route_map": "RM1", + "entries": [ + { + "sequence": 10, + "action": "permit", + "match": {"peer": "192.0.2.32"}, + "set": {"metric": "5", "as-path": {"exclude": "111"}}, + }, + ], + }, + ] + + def test_merged_adds_new_rm(self): + config = [ + { + "route_map": "RM-NEW", + "entries": [{"sequence": 10, "action": "permit"}], + }, + ] + cmds = build_commands(config, self._have_empty(), "merged") + paths = [c[1] for c in cmds] + self.assertIn(["policy", "route-map", "RM-NEW", "rule", "10", "action", "permit"], paths) + + def test_merged_idempotent(self): + config = [ + { + "route_map": "RM1", + "entries": [ + { + "sequence": 10, + "action": "permit", + "match": {"peer": "192.0.2.32"}, + "set": {"metric": "5", "as_path_exclude": "111"}, + }, + ], + }, + ] + cmds = build_commands(config, self._have_with_rm(), "merged") + self.assertEqual(cmds, []) + + def test_deleted_no_config_deletes_all(self): + cmds = build_commands([], self._have_with_rm(), "deleted") + self.assertIn(("delete", ["policy", "route-map"]), cmds) + + def test_deleted_with_config_deletes_named(self): + config = [{"route_map": "RM1"}] + cmds = build_commands(config, self._have_with_rm(), "deleted") + self.assertIn(("delete", ["policy", "route-map", "RM1"]), cmds) + + def test_replaced_deletes_then_resets(self): + config = [ + { + "route_map": "RM1", + "entries": [{"sequence": 10, "action": "deny"}], + }, + ] + cmds = build_commands(config, self._have_with_rm(), "replaced") + ops = [c[0] for c in cmds] + # delete must come before set + self.assertIn("delete", ops) + self.assertIn("set", ops) + delete_idx = ops.index("delete") + set_idx = ops.index("set") + self.assertLess(delete_idx, set_idx) + + def test_overridden_removes_extra_rm(self): + config = [{"route_map": "RM-NEW", "entries": []}] + have = self._have_with_rm() # has RM1 + cmds = build_commands(config, have, "overridden") + self.assertIn(("delete", ["policy", "route-map", "RM1"]), cmds) + + +if __name__ == "__main__": + unittest.main() |
