summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authoromnom62 <75066712+omnom62@users.noreply.github.com>2026-08-21 23:54:29 +1000
committerGitHub <noreply@github.com>2026-08-21 08:54:29 -0500
commit885b9462480712210ddeea9ca4a4b7a52e9ef587 (patch)
treea994ef61f704d41bdab770c86a21481a5d95d2b1 /tests
parent6dc9d721513dca5a573c7ee01b74d703ff41f95f (diff)
downloadrest.vyos-885b9462480712210ddeea9ca4a4b7a52e9ef587.tar.gz
rest.vyos-885b9462480712210ddeea9ca4a4b7a52e9ef587.zip
T8989: static_routes module
* T8989 static_routes * T8989 static_routes * T8989 added changelog fragment for static routes module * t8989 static route changelog * t8989 static_routes module * T8989: static_routes SIT overridden and rtt * T8989: SIT updated * T8989: vyos_static_routes dict_op refactor
Diffstat (limited to 'tests')
-rw-r--r--tests/integration/targets/vyos_static_routes/aliases1
-rw-r--r--tests/integration/targets/vyos_static_routes/defaults/main.yaml3
-rw-r--r--tests/integration/targets/vyos_static_routes/tasks/httpapi.yaml21
-rw-r--r--tests/integration/targets/vyos_static_routes/tasks/main.yaml5
-rw-r--r--tests/integration/targets/vyos_static_routes/tests/httpapi.yaml21
-rw-r--r--tests/integration/targets/vyos_static_routes/tests/httpapi/_populate_config.yaml19
-rw-r--r--tests/integration/targets/vyos_static_routes/tests/httpapi/_remove_config.yaml5
-rw-r--r--tests/integration/targets/vyos_static_routes/tests/httpapi/deleted.yaml28
-rw-r--r--tests/integration/targets/vyos_static_routes/tests/httpapi/gathered.yaml20
-rw-r--r--tests/integration/targets/vyos_static_routes/tests/httpapi/merged.yaml37
-rw-r--r--tests/integration/targets/vyos_static_routes/tests/httpapi/overridden.yaml34
-rw-r--r--tests/integration/targets/vyos_static_routes/tests/httpapi/replaced.yaml35
-rw-r--r--tests/integration/targets/vyos_static_routes/tests/httpapi/rtt.yaml71
-rw-r--r--tests/integration/targets/vyos_static_routes/vars/main.yaml1
-rw-r--r--tests/unit/fixtures/static_routes_running.json21
-rw-r--r--tests/unit/modules/test_vyos_static_routes.py365
16 files changed, 687 insertions, 0 deletions
diff --git a/tests/integration/targets/vyos_static_routes/aliases b/tests/integration/targets/vyos_static_routes/aliases
new file mode 100644
index 0000000..cc0afef
--- /dev/null
+++ b/tests/integration/targets/vyos_static_routes/aliases
@@ -0,0 +1 @@
+network/vyos
diff --git a/tests/integration/targets/vyos_static_routes/defaults/main.yaml b/tests/integration/targets/vyos_static_routes/defaults/main.yaml
new file mode 100644
index 0000000..164afea
--- /dev/null
+++ b/tests/integration/targets/vyos_static_routes/defaults/main.yaml
@@ -0,0 +1,3 @@
+---
+testcase: "[^_].*"
+test_items: []
diff --git a/tests/integration/targets/vyos_static_routes/tasks/httpapi.yaml b/tests/integration/targets/vyos_static_routes/tasks/httpapi.yaml
new file mode 100644
index 0000000..4147e6d
--- /dev/null
+++ b/tests/integration/targets/vyos_static_routes/tasks/httpapi.yaml
@@ -0,0 +1,21 @@
+---
+- name: Collect all httpapi test cases
+ ansible.builtin.find:
+ paths: "{{ role_path }}/tests/httpapi"
+ patterns: "{{ testcase }}.yaml"
+ use_regex: true
+ register: test_cases
+ delegate_to: localhost
+
+- name: Set test_items
+ ansible.builtin.set_fact:
+ test_items: "{{ test_cases.files | map(attribute='path') | list }}"
+
+- name: Run test case (connection=httpapi)
+ ansible.builtin.include_tasks: "{{ test_case_to_run }}"
+ vars:
+ ansible_connection: ansible.netcommon.httpapi
+ ansible_network_os: vyos.rest.vyos
+ with_items: "{{ test_items }}"
+ loop_control:
+ loop_var: test_case_to_run
diff --git a/tests/integration/targets/vyos_static_routes/tasks/main.yaml b/tests/integration/targets/vyos_static_routes/tasks/main.yaml
new file mode 100644
index 0000000..b1f6193
--- /dev/null
+++ b/tests/integration/targets/vyos_static_routes/tasks/main.yaml
@@ -0,0 +1,5 @@
+---
+- name: Run httpapi tests
+ ansible.builtin.include_tasks: httpapi.yaml
+ tags:
+ - httpapi
diff --git a/tests/integration/targets/vyos_static_routes/tests/httpapi.yaml b/tests/integration/targets/vyos_static_routes/tests/httpapi.yaml
new file mode 100644
index 0000000..4147e6d
--- /dev/null
+++ b/tests/integration/targets/vyos_static_routes/tests/httpapi.yaml
@@ -0,0 +1,21 @@
+---
+- name: Collect all httpapi test cases
+ ansible.builtin.find:
+ paths: "{{ role_path }}/tests/httpapi"
+ patterns: "{{ testcase }}.yaml"
+ use_regex: true
+ register: test_cases
+ delegate_to: localhost
+
+- name: Set test_items
+ ansible.builtin.set_fact:
+ test_items: "{{ test_cases.files | map(attribute='path') | list }}"
+
+- name: Run test case (connection=httpapi)
+ ansible.builtin.include_tasks: "{{ test_case_to_run }}"
+ vars:
+ ansible_connection: ansible.netcommon.httpapi
+ ansible_network_os: vyos.rest.vyos
+ with_items: "{{ test_items }}"
+ loop_control:
+ loop_var: test_case_to_run
diff --git a/tests/integration/targets/vyos_static_routes/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_static_routes/tests/httpapi/_populate_config.yaml
new file mode 100644
index 0000000..e257075
--- /dev/null
+++ b/tests/integration/targets/vyos_static_routes/tests/httpapi/_populate_config.yaml
@@ -0,0 +1,19 @@
+---
+- name: Populate static_routes config for testing
+ vyos.rest.vyos_static_routes:
+ config:
+ - afi: ipv4
+ routes:
+ - dest: 192.0.2.0/24
+ next_hops:
+ - forward_router_address: 10.0.0.1
+ - dest: 203.0.113.0/24
+ blackhole_config:
+ distance: 200
+ - afi: ipv6
+ routes:
+ - dest: 2001:db8::/32
+ next_hops:
+ - forward_router_address: 2001:db8::1
+ state: merged
+ ignore_errors: true
diff --git a/tests/integration/targets/vyos_static_routes/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_static_routes/tests/httpapi/_remove_config.yaml
new file mode 100644
index 0000000..dd02da6
--- /dev/null
+++ b/tests/integration/targets/vyos_static_routes/tests/httpapi/_remove_config.yaml
@@ -0,0 +1,5 @@
+---
+- name: Remove pre-existing static_routes config
+ vyos.rest.vyos_static_routes:
+ state: deleted
+ ignore_errors: true
diff --git a/tests/integration/targets/vyos_static_routes/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_static_routes/tests/httpapi/deleted.yaml
new file mode 100644
index 0000000..b02e256
--- /dev/null
+++ b/tests/integration/targets/vyos_static_routes/tests/httpapi/deleted.yaml
@@ -0,0 +1,28 @@
+---
+- debug:
+ msg: START vyos_static_routes deleted integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _remove_config.yaml
+- include_tasks: _populate_config.yaml
+
+- block:
+ - name: Delete all static_routes configuration
+ register: result
+ vyos.rest.vyos_static_routes: &id001
+ state: deleted
+
+ - assert:
+ that:
+ - result.changed == true
+
+ - name: Delete static_routes configuration (IDEMPOTENT)
+ register: result
+ vyos.rest.vyos_static_routes: *id001
+
+ - name: Assert idempotent
+ assert:
+ that:
+ - result.changed == false
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_static_routes/tests/httpapi/gathered.yaml b/tests/integration/targets/vyos_static_routes/tests/httpapi/gathered.yaml
new file mode 100644
index 0000000..c6d5e59
--- /dev/null
+++ b/tests/integration/targets/vyos_static_routes/tests/httpapi/gathered.yaml
@@ -0,0 +1,20 @@
+---
+- debug:
+ msg: START vyos_static_routes gathered integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _remove_config.yaml
+- include_tasks: _populate_config.yaml
+
+- block:
+ - name: Gather static_routes configuration
+ register: result
+ vyos.rest.vyos_static_routes:
+ state: gathered
+
+ - assert:
+ that:
+ - result.changed == false
+ - result.gathered | selectattr('afi', 'eq', 'ipv4') | list | length > 0
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_static_routes/tests/httpapi/merged.yaml b/tests/integration/targets/vyos_static_routes/tests/httpapi/merged.yaml
new file mode 100644
index 0000000..6cf6a01
--- /dev/null
+++ b/tests/integration/targets/vyos_static_routes/tests/httpapi/merged.yaml
@@ -0,0 +1,37 @@
+---
+- debug:
+ msg: START vyos_static_routes merged integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _remove_config.yaml
+
+- block:
+ - name: Merge static_routes configuration
+ register: result
+ vyos.rest.vyos_static_routes: &id001
+ config:
+ - afi: ipv4
+ routes:
+ - dest: 192.0.2.0/24
+ next_hops:
+ - forward_router_address: 10.0.0.1
+ - dest: 203.0.113.0/24
+ blackhole_config:
+ distance: 200
+ state: merged
+
+ - assert:
+ that:
+ - result.changed == true
+
+ - name: Merge static_routes configuration (IDEMPOTENT)
+ register: result
+ vyos.rest.vyos_static_routes: *id001
+
+ - name: Assert idempotent
+ assert:
+ that:
+ - result.changed == false
+ - result.commands == []
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_static_routes/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_static_routes/tests/httpapi/overridden.yaml
new file mode 100644
index 0000000..e716d75
--- /dev/null
+++ b/tests/integration/targets/vyos_static_routes/tests/httpapi/overridden.yaml
@@ -0,0 +1,34 @@
+---
+- debug:
+ msg: START vyos_static_routes overridden integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _remove_config.yaml
+- include_tasks: _populate_config.yaml
+
+- block:
+ - name: Override static_routes configuration
+ register: result
+ vyos.rest.vyos_static_routes: &id001
+ config:
+ - afi: ipv4
+ routes:
+ - dest: 192.0.2.0/24
+ next_hops:
+ - forward_router_address: 10.0.0.2
+ state: overridden
+
+ - assert:
+ that:
+ - result.changed == true
+
+ - name: Override static_routes configuration (IDEMPOTENT)
+ register: result
+ vyos.rest.vyos_static_routes: *id001
+
+ - assert:
+ that:
+ - result.changed == false
+ - result.commands == []
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_static_routes/tests/httpapi/replaced.yaml b/tests/integration/targets/vyos_static_routes/tests/httpapi/replaced.yaml
new file mode 100644
index 0000000..a27e637
--- /dev/null
+++ b/tests/integration/targets/vyos_static_routes/tests/httpapi/replaced.yaml
@@ -0,0 +1,35 @@
+---
+- debug:
+ msg: START vyos_static_routes replaced integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _remove_config.yaml
+- include_tasks: _populate_config.yaml
+
+- block:
+ - name: Replace static_routes configuration
+ register: result
+ vyos.rest.vyos_static_routes: &id001
+ config:
+ - afi: ipv4
+ routes:
+ - dest: 192.0.2.0/24
+ next_hops:
+ - forward_router_address: 10.0.0.2
+ state: replaced
+
+ - assert:
+ that:
+ - result.changed == true
+
+ - name: Replace static_routes configuration (IDEMPOTENT)
+ register: result
+ vyos.rest.vyos_static_routes: *id001
+
+ - name: Assert idempotent
+ assert:
+ that:
+ - result.changed == false
+ - result.commands == []
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_static_routes/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_static_routes/tests/httpapi/rtt.yaml
new file mode 100644
index 0000000..e0c76f9
--- /dev/null
+++ b/tests/integration/targets/vyos_static_routes/tests/httpapi/rtt.yaml
@@ -0,0 +1,71 @@
+---
+- debug:
+ msg: START vyos_static_routes round trip integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _remove_config.yaml
+
+- block:
+ - name: RTT - Apply base configuration
+ vyos.rest.vyos_static_routes:
+ config:
+ - afi: ipv4
+ routes:
+ - dest: 192.0.2.0/24
+ next_hops:
+ - forward_router_address: 10.0.0.1
+ - dest: 203.0.113.0/24
+ blackhole_config:
+ distance: 200
+ state: merged
+
+ - name: RTT - Gather configuration
+ register: gathered
+ vyos.rest.vyos_static_routes:
+ state: gathered
+
+ - name: RTT - Assert gathered matches applied
+ vars:
+ ipv4_routes: >-
+ {{ gathered.gathered
+ | selectattr('afi', 'eq', 'ipv4')
+ | map(attribute='routes')
+ | first }}
+ assert:
+ that:
+ - gathered.gathered | selectattr('afi', 'eq', 'ipv4') | list | length == 1
+ - ipv4_routes | selectattr('dest', 'eq', '192.0.2.0/24') | list | length == 1
+
+ - name: RTT - Modify configuration
+ vyos.rest.vyos_static_routes:
+ config:
+ - afi: ipv4
+ routes:
+ - dest: 192.0.2.0/24
+ next_hops:
+ - forward_router_address: 10.0.0.2
+ state: replaced
+
+ - name: RTT - Gather modified configuration
+ register: gathered2
+ vyos.rest.vyos_static_routes:
+ state: gathered
+
+ - name: RTT - Assert modification applied correctly
+ vars:
+ ipv4_routes2: >-
+ {{ gathered2.gathered
+ | selectattr('afi', 'eq', 'ipv4')
+ | map(attribute='routes')
+ | first }}
+ dest_hops: >-
+ {{ ipv4_routes2
+ | selectattr('dest', 'eq', '192.0.2.0/24')
+ | map(attribute='next_hops')
+ | first }}
+ assert:
+ that:
+ - gathered2.gathered | selectattr('afi', 'eq', 'ipv4') | list | length == 1
+ - dest_hops | selectattr('forward_router_address', 'eq', '10.0.0.2') | list | length == 1
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_static_routes/vars/main.yaml b/tests/integration/targets/vyos_static_routes/vars/main.yaml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/tests/integration/targets/vyos_static_routes/vars/main.yaml
@@ -0,0 +1 @@
+---
diff --git a/tests/unit/fixtures/static_routes_running.json b/tests/unit/fixtures/static_routes_running.json
new file mode 100644
index 0000000..46cf700
--- /dev/null
+++ b/tests/unit/fixtures/static_routes_running.json
@@ -0,0 +1,21 @@
+{
+ "route": {
+ "192.0.2.0/24": {
+ "next-hop": {
+ "10.0.0.1": {}
+ }
+ },
+ "203.0.113.0/24": {
+ "blackhole": {
+ "distance": "200"
+ }
+ }
+ },
+ "route6": {
+ "2001:db8::/32": {
+ "next-hop": {
+ "2001:db8::1": {}
+ }
+ }
+ }
+}
diff --git a/tests/unit/modules/test_vyos_static_routes.py b/tests/unit/modules/test_vyos_static_routes.py
new file mode 100644
index 0000000..1a50da9
--- /dev/null
+++ b/tests/unit/modules/test_vyos_static_routes.py
@@ -0,0 +1,365 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import unittest
+
+from unittest.mock import MagicMock
+
+from ansible_collections.vyos.rest.plugins.modules.vyos_static_routes import (
+ _ROUTE_OPTIONS,
+ ARGUMENT_SPEC,
+ _derive_key_field,
+ _device_to_argspec,
+ _keyed_list_from_device,
+ _keyed_list_to_device,
+ _next_hop_entry_from_device,
+ _next_hop_entry_to_device,
+ _route_entry_from_device,
+ _route_entry_to_device,
+ _want_to_device,
+ build_commands,
+ cast_by_spec,
+ get_running_config,
+)
+
+from .base import load_fixture
+
+
+_BASE = ["protocols", "static"]
+
+
+class VyOSModuleTestCase(unittest.TestCase):
+ def setUp(self):
+ self.mock_vyos = MagicMock()
+ self.fixture = load_fixture("static_routes_running.json")
+ self.mock_vyos.get_config = MagicMock(return_value=self.fixture)
+
+
+class TestGetRunningConfig(VyOSModuleTestCase):
+ def test_returns_config_directly(self):
+ result = get_running_config(self.mock_vyos)
+ self.assertIn("route", result)
+ self.assertIn("route6", result)
+
+ def test_empty_config(self):
+ self.mock_vyos.get_config = MagicMock(return_value=None)
+ self.assertEqual(get_running_config(self.mock_vyos), {})
+
+
+class TestDeriveKeyField(unittest.TestCase):
+ def test_derives_dest_key(self):
+ route_opts = ARGUMENT_SPEC["config"]["options"]["routes"]["options"]
+ self.assertEqual(_derive_key_field(route_opts), "dest")
+
+ def test_derives_forward_router_address_key(self):
+ nh_opts = ARGUMENT_SPEC["config"]["options"]["routes"]["options"]["next_hops"]["options"]
+ self.assertEqual(_derive_key_field(nh_opts), "forward_router_address")
+
+ def test_raises_if_none_required(self):
+ with self.assertRaises(ValueError):
+ _derive_key_field({"a": {"type": "str"}})
+
+ def test_raises_if_more_than_one_required(self):
+ with self.assertRaises(ValueError):
+ _derive_key_field({"a": {"required": True}, "b": {"required": True}})
+
+
+class TestKeyedListHelper(unittest.TestCase):
+ def test_to_device_basic(self):
+ result = _keyed_list_to_device(
+ [{"dest": "192.0.2.0/24", "blackhole_config": {"distance": 200}}],
+ "dest",
+ )
+ self.assertEqual(result, {"192.0.2.0/24": {"blackhole_config": {"distance": 200}}})
+
+ def test_from_device_basic(self):
+ result = _keyed_list_from_device({"192.0.2.0/24": {"a": 1}}, "dest")
+ self.assertEqual(result, [{"dest": "192.0.2.0/24", "a": 1}])
+
+ def test_empty(self):
+ self.assertEqual(_keyed_list_to_device([], "dest"), {})
+ self.assertEqual(_keyed_list_from_device({}, "dest"), [])
+
+
+class TestBlackholeNoTypeField(unittest.TestCase):
+ """Regression test for the confirmed hallucinated field: the
+ original module's blackhole_config.type does not correspond to
+ anything on the device -- confirmed against vyos-1x, the
+ "blackhole" node has only "distance" (and "tag", out of scope).
+ The field has been removed entirely."""
+
+ def test_type_not_in_argspec(self):
+ bh_opts = ARGUMENT_SPEC["config"]["options"]["routes"]["options"]["blackhole_config"]
+ self.assertNotIn("type", bh_opts["options"])
+ self.assertEqual(set(bh_opts["options"].keys()), {"distance"})
+
+
+class TestRouteEntryToDeviceFromDevice(unittest.TestCase):
+ def test_blackhole_presence_only(self):
+ """An empty blackhole_config (no distance) still creates a
+ bare presence node -- achieving the same "just blackhole, no
+ distance" result the original's bogus "type" field was used
+ for, without needing any sentinel field at all."""
+ result = _route_entry_to_device({"blackhole_config": {}})
+ self.assertEqual(result, {"blackhole": {}})
+
+ def test_blackhole_with_distance(self):
+ result = _route_entry_to_device({"blackhole_config": {"distance": 200}})
+ self.assertEqual(result, {"blackhole": {"distance": 200}})
+
+ def test_next_hops_keyed_by_address(self):
+ result = _route_entry_to_device(
+ {"next_hops": [{"forward_router_address": "10.0.0.1", "admin_distance": 50}]},
+ )
+ self.assertEqual(result, {"next-hop": {"10.0.0.1": {"distance": 50}}})
+
+ def test_disabled_next_hop(self):
+ result = _route_entry_to_device(
+ {"next_hops": [{"forward_router_address": "10.0.0.1", "enabled": False}]},
+ )
+ self.assertEqual(result["next-hop"]["10.0.0.1"], {"disable": {}})
+
+ def test_enabled_true_produces_no_disable_leaf(self):
+ result = _route_entry_to_device(
+ {"next_hops": [{"forward_router_address": "10.0.0.1", "enabled": True}]},
+ )
+ self.assertNotIn("disable", result["next-hop"]["10.0.0.1"])
+
+ def test_from_device_blackhole(self):
+ """from_device stays purely structural (kebab->snake only);
+ int-casting is cast_by_spec's responsibility, applied
+ downstream in main() -- confirmed separately below."""
+ entry = _route_entry_from_device({"blackhole": {"distance": "200"}})
+ self.assertEqual(entry["blackhole_config"]["distance"], "200")
+
+ def test_from_device_next_hop_disabled(self):
+ entry = _route_entry_from_device({"next-hop": {"10.0.0.1": {"disable": {}}}})
+ self.assertEqual(entry["next_hops"][0]["enabled"], False)
+
+ def test_from_device_next_hop_enabled_omitted(self):
+ """Confirmed device behavior: an enabled next-hop has no
+ "disable" leaf at all -- "enabled" should not appear in the
+ parsed entry either, matching the argspec default."""
+ entry = _route_entry_from_device({"next-hop": {"10.0.0.1": {}}})
+ self.assertNotIn("enabled", entry["next_hops"][0])
+
+ def test_empty(self):
+ self.assertEqual(_route_entry_to_device({}), {})
+ self.assertEqual(_route_entry_from_device({}), {})
+
+
+class TestNextHopEntryToDeviceFromDevice(unittest.TestCase):
+ def test_interface(self):
+ result = _next_hop_entry_to_device({"interface": "eth0"})
+ self.assertEqual(result, {"interface": "eth0"})
+
+ def test_from_device_interface(self):
+ entry = _next_hop_entry_from_device({"interface": "eth0"})
+ self.assertEqual(entry, {"interface": "eth0"})
+
+ def test_from_device_distance_cast_to_int(self):
+ entry = _next_hop_entry_from_device({"distance": "50"})
+ self.assertEqual(entry["admin_distance"], 50)
+ self.assertIsInstance(entry["admin_distance"], int)
+
+
+class TestWantToDevice(unittest.TestCase):
+ def test_empty(self):
+ self.assertEqual(_want_to_device([]), {})
+ self.assertEqual(_want_to_device(None), {})
+
+ def test_afi_without_routes_omitted(self):
+ self.assertEqual(_want_to_device([{"afi": "ipv4"}]), {})
+
+ def test_keyed_by_route_key(self):
+ config = [{"afi": "ipv4", "routes": [{"dest": "192.0.2.0/24"}]}]
+ result = _want_to_device(config)
+ self.assertIn("192.0.2.0/24", result["route"])
+
+ def test_ipv6_uses_route6_key(self):
+ config = [{"afi": "ipv6", "routes": [{"dest": "2001:db8::/32"}]}]
+ result = _want_to_device(config)
+ self.assertIn("route6", result)
+
+
+class TestDeviceToArgspecFixture(VyOSModuleTestCase):
+ def test_both_afis_parsed(self):
+ raw = get_running_config(self.mock_vyos)
+ result = _device_to_argspec(raw)
+ afis = [e["afi"] for e in result]
+ self.assertIn("ipv4", afis)
+ self.assertIn("ipv6", afis)
+
+ def test_blackhole_route_parsed_with_casting(self):
+ """from_device alone leaves distance as the raw device string;
+ cast_by_spec (applied downstream in main(), confirmed here
+ directly) is what casts it to int, since cast_by_spec recurses
+ into type="dict" suboptions like blackhole_config."""
+ raw = get_running_config(self.mock_vyos)
+ result = _device_to_argspec(raw)
+ ipv4_routes = next(e for e in result if e["afi"] == "ipv4")["routes"]
+ bh_route = next(r for r in ipv4_routes if r["dest"] == "203.0.113.0/24")
+ self.assertEqual(bh_route["blackhole_config"]["distance"], "200")
+ cast_by_spec(bh_route, _ROUTE_OPTIONS)
+ self.assertEqual(bh_route["blackhole_config"]["distance"], 200)
+
+ def test_next_hop_route_parsed(self):
+ raw = get_running_config(self.mock_vyos)
+ result = _device_to_argspec(raw)
+ ipv4_routes = next(e for e in result if e["afi"] == "ipv4")["routes"]
+ nh_route = next(r for r in ipv4_routes if r["dest"] == "192.0.2.0/24")
+ self.assertEqual(nh_route["next_hops"][0]["forward_router_address"], "10.0.0.1")
+
+ def test_empty_config(self):
+ self.assertEqual(_device_to_argspec({}), [])
+ self.assertEqual(_device_to_argspec(None), [])
+
+
+class TestBuildCommands(VyOSModuleTestCase):
+ def setUp(self):
+ super().setUp()
+ self.raw = get_running_config(self.mock_vyos)
+
+ def test_merged_idempotent_against_own_fixture(self):
+ have = _device_to_argspec(self.raw)
+ self.assertEqual(build_commands(have, self.raw, "merged"), [])
+
+ def test_replaced_idempotent_against_own_fixture(self):
+ have = _device_to_argspec(self.raw)
+ self.assertEqual(build_commands(have, self.raw, "replaced"), [])
+
+ def test_overridden_idempotent_against_own_fixture(self):
+ have = _device_to_argspec(self.raw)
+ self.assertEqual(build_commands(have, self.raw, "overridden"), [])
+
+ def test_clear_omitted_next_hop_attribute_on_replaced(self):
+ """The primary confirmed bug fix from the PR review: the
+ original _route_cmds only emitted commands for setting values,
+ never for clearing an omitted attribute back to default, and
+ "replaced" state's own change-detection missed this entirely
+ since it only inspected generated set-commands."""
+ raw_have = {"route": {"192.0.2.0/24": {"next-hop": {"10.0.0.1": {"distance": "50"}}}}}
+ config = [
+ {
+ "afi": "ipv4",
+ "routes": [
+ {
+ "dest": "192.0.2.0/24",
+ "next_hops": [
+ {"forward_router_address": "10.0.0.1"},
+ ],
+ },
+ ],
+ },
+ ]
+ cmds = build_commands(config, raw_have, "replaced")
+ expected = ("delete", _BASE + ["route", "192.0.2.0/24", "next-hop", "10.0.0.1", "distance"])
+ self.assertIn(expected, cmds)
+
+ def test_replaced_scoped_to_named_route_only(self):
+ raw_have = {
+ "route": {
+ "192.0.2.0/24": {"next-hop": {"10.0.0.1": {}}},
+ "203.0.113.0/24": {"blackhole": {"distance": "200"}},
+ },
+ }
+ config = [
+ {
+ "afi": "ipv4",
+ "routes": [
+ {
+ "dest": "192.0.2.0/24",
+ "next_hops": [
+ {"forward_router_address": "10.0.0.1"},
+ ],
+ },
+ ],
+ },
+ ]
+ cmds = build_commands(config, raw_have, "replaced")
+ self.assertEqual(cmds, [])
+ self.assertFalse(any("203.0.113.0/24" in str(c) for c in cmds))
+
+ def test_overridden_deletes_omitted_route(self):
+ raw_have = {
+ "route": {
+ "192.0.2.0/24": {"next-hop": {"10.0.0.1": {}}},
+ "203.0.113.0/24": {"blackhole": {"distance": "200"}},
+ },
+ }
+ config = [
+ {
+ "afi": "ipv4",
+ "routes": [
+ {
+ "dest": "192.0.2.0/24",
+ "next_hops": [
+ {"forward_router_address": "10.0.0.1"},
+ ],
+ },
+ ],
+ },
+ ]
+ cmds = build_commands(config, raw_have, "overridden")
+ self.assertIn(("delete", _BASE + ["route", "203.0.113.0/24"]), cmds)
+
+ def test_deleted_named_route(self):
+ cmds = build_commands(
+ [{"afi": "ipv4", "routes": [{"dest": "192.0.2.0/24"}]}],
+ self.raw,
+ "deleted",
+ )
+ self.assertEqual(cmds, [("delete", _BASE + ["route", "192.0.2.0/24"])])
+
+ def test_deleted_named_afi_no_routes(self):
+ cmds = build_commands([{"afi": "ipv4"}], self.raw, "deleted")
+ self.assertEqual(cmds, [("delete", _BASE + ["route"])])
+
+ def test_deleted_no_config_removes_all(self):
+ cmds = build_commands([], self.raw, "deleted")
+ self.assertEqual(cmds, [("delete", _BASE)])
+
+ def test_deleted_named_nonexistent_is_noop(self):
+ cmds = build_commands(
+ [{"afi": "ipv4", "routes": [{"dest": "198.51.100.0/24"}]}],
+ self.raw,
+ "deleted",
+ )
+ self.assertEqual(cmds, [])
+
+ def test_collapsed_bare_route_no_blackhole_or_next_hop(self):
+ raw_have = {"route": {"192.0.2.0/24": {}}}
+ config = [{"afi": "ipv4", "routes": [{"dest": "192.0.2.0/24"}]}]
+ self.assertEqual(build_commands(config, raw_have, "merged"), [])
+
+ def test_merged_new_blackhole_route(self):
+ config = [{"afi": "ipv4", "routes": [{"dest": "198.51.100.0/24", "blackhole_config": {}}]}]
+ cmds = build_commands(config, {}, "merged")
+ self.assertIn(("set", _BASE + ["route", "198.51.100.0/24", "blackhole"]), cmds)
+
+ def test_merged_new_disabled_next_hop(self):
+ config = [
+ {
+ "afi": "ipv4",
+ "routes": [
+ {
+ "dest": "198.51.100.0/24",
+ "next_hops": [
+ {"forward_router_address": "10.0.0.9", "enabled": False},
+ ],
+ },
+ ],
+ },
+ ]
+ cmds = build_commands(config, {}, "merged")
+ self.assertIn(
+ ("set", _BASE + ["route", "198.51.100.0/24", "next-hop", "10.0.0.9", "disable"]),
+ cmds,
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()