style: ruff format and the lint rules of the driver standard
Mechanical: ruff format, and ruff check --fix with the rule set every NAPALM driver uses (E, F, I, UP, B at line length 100, Python 3.10). What ruff cannot fix comes in the next commit.
This commit is contained in:
1 parent
692a584fd2
commit
571d9abb5f
15 files changed
+690
-419
No files matched your search
+223
-107
@@ -1,18 +1,19 @@
|
||||
"""Unit tests for OPNsenseDriver — no real device required."""
|
||||
|
||||
import json
|
||||
import pytest
|
||||
import requests
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from napalm_opnsense.opnsense import OPNsenseDriver
|
||||
from napalm.base.exceptions import ConnectionException, ConnectionClosedException
|
||||
import pytest
|
||||
import requests
|
||||
from napalm.base.exceptions import ConnectionClosedException, ConnectionException
|
||||
|
||||
from napalm_opnsense.opnsense import OPNsenseDriver
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def driver():
|
||||
"""Return a driver instance with a mocked requests.Session."""
|
||||
@@ -110,6 +111,7 @@ ARP_RESPONSE = {
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_json_response(data):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.json.return_value = data
|
||||
@@ -121,6 +123,7 @@ def _make_json_response(data):
|
||||
# open() / close() / is_alive()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestOpenClose:
|
||||
def test_open_raises_connection_exception_on_error(self):
|
||||
drv = OPNsenseDriver(
|
||||
@@ -166,6 +169,7 @@ class TestIsAlive:
|
||||
# _get()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestInternalGet:
|
||||
def test_raises_when_no_session(self):
|
||||
drv = OPNsenseDriver("host", "u", "p")
|
||||
@@ -185,41 +189,40 @@ class TestInternalGet:
|
||||
# get_facts()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetFacts:
|
||||
def test_returns_required_keys(self, driver):
|
||||
driver._get = lambda path: (
|
||||
STATUS_RESPONSE if "status" in path else INTERFACES_RESPONSE
|
||||
)
|
||||
driver._get = lambda path: STATUS_RESPONSE if "status" in path else INTERFACES_RESPONSE
|
||||
facts = driver.get_facts()
|
||||
for key in ("vendor", "model", "hostname", "fqdn", "os_version",
|
||||
"serial_number", "uptime", "interface_list"):
|
||||
for key in (
|
||||
"vendor",
|
||||
"model",
|
||||
"hostname",
|
||||
"fqdn",
|
||||
"os_version",
|
||||
"serial_number",
|
||||
"uptime",
|
||||
"interface_list",
|
||||
):
|
||||
assert key in facts
|
||||
|
||||
def test_vendor_constant(self, driver):
|
||||
driver._get = lambda path: (
|
||||
STATUS_RESPONSE if "status" in path else INTERFACES_RESPONSE
|
||||
)
|
||||
driver._get = lambda path: STATUS_RESPONSE if "status" in path else INTERFACES_RESPONSE
|
||||
facts = driver.get_facts()
|
||||
assert facts["vendor"] == "OPNsense"
|
||||
|
||||
def test_hostname_parsed(self, driver):
|
||||
driver._get = lambda path: (
|
||||
STATUS_RESPONSE if "status" in path else INTERFACES_RESPONSE
|
||||
)
|
||||
driver._get = lambda path: STATUS_RESPONSE if "status" in path else INTERFACES_RESPONSE
|
||||
facts = driver.get_facts()
|
||||
assert facts["hostname"] == "opnsense01"
|
||||
|
||||
def test_os_version_parsed(self, driver):
|
||||
driver._get = lambda path: (
|
||||
STATUS_RESPONSE if "status" in path else INTERFACES_RESPONSE
|
||||
)
|
||||
driver._get = lambda path: STATUS_RESPONSE if "status" in path else INTERFACES_RESPONSE
|
||||
facts = driver.get_facts()
|
||||
assert facts["os_version"] == "24.7"
|
||||
|
||||
def test_interface_list_populated(self, driver):
|
||||
driver._get = lambda path: (
|
||||
STATUS_RESPONSE if "status" in path else INTERFACES_RESPONSE
|
||||
)
|
||||
driver._get = lambda path: STATUS_RESPONSE if "status" in path else INTERFACES_RESPONSE
|
||||
facts = driver.get_facts()
|
||||
assert "em0" in facts["interface_list"]
|
||||
assert "em1" in facts["interface_list"]
|
||||
@@ -235,16 +238,12 @@ class TestGetFacts:
|
||||
assert facts["interface_list"] == []
|
||||
|
||||
def test_serial_number(self, driver):
|
||||
driver._get = lambda path: (
|
||||
STATUS_RESPONSE if "status" in path else INTERFACES_RESPONSE
|
||||
)
|
||||
driver._get = lambda path: STATUS_RESPONSE if "status" in path else INTERFACES_RESPONSE
|
||||
facts = driver.get_facts()
|
||||
assert facts["serial_number"] == "ABC123"
|
||||
|
||||
def test_uptime(self, driver):
|
||||
driver._get = lambda path: (
|
||||
STATUS_RESPONSE if "status" in path else INTERFACES_RESPONSE
|
||||
)
|
||||
driver._get = lambda path: STATUS_RESPONSE if "status" in path else INTERFACES_RESPONSE
|
||||
facts = driver.get_facts()
|
||||
assert facts["uptime"] == 12345
|
||||
|
||||
@@ -253,6 +252,7 @@ class TestGetFacts:
|
||||
# get_interfaces()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetInterfaces:
|
||||
def test_interface_count(self, driver):
|
||||
driver._get = lambda path: INTERFACES_RESPONSE
|
||||
@@ -311,6 +311,7 @@ class TestGetInterfaces:
|
||||
# get_interfaces_ip()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetInterfacesIp:
|
||||
def test_entry_count(self, driver):
|
||||
driver._get = lambda path: ADDRESSES_RESPONSE
|
||||
@@ -340,6 +341,7 @@ class TestGetInterfacesIp:
|
||||
# get_arp_table()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetArpTable:
|
||||
def test_entry_count(self, driver):
|
||||
driver._get = lambda path: ARP_RESPONSE
|
||||
@@ -419,11 +421,18 @@ class TestGetInterfacesCounters:
|
||||
driver._get = lambda path: INTERFACE_STATISTICS_RESPONSE
|
||||
entry = driver.get_interfaces_counters()["em0"]
|
||||
for key in (
|
||||
"tx_errors", "rx_errors", "tx_discards", "rx_discards",
|
||||
"tx_octets", "rx_octets",
|
||||
"tx_unicast_packets", "rx_unicast_packets",
|
||||
"tx_multicast_packets", "rx_multicast_packets",
|
||||
"tx_broadcast_packets", "rx_broadcast_packets",
|
||||
"tx_errors",
|
||||
"rx_errors",
|
||||
"tx_discards",
|
||||
"rx_discards",
|
||||
"tx_octets",
|
||||
"rx_octets",
|
||||
"tx_unicast_packets",
|
||||
"rx_unicast_packets",
|
||||
"tx_multicast_packets",
|
||||
"rx_multicast_packets",
|
||||
"tx_broadcast_packets",
|
||||
"rx_broadcast_packets",
|
||||
):
|
||||
assert key in entry
|
||||
|
||||
@@ -505,7 +514,8 @@ class TestGetEnvironment:
|
||||
def test_temperature_alert_thresholds(self, driver):
|
||||
driver._get = lambda path: (
|
||||
{"data": [{"device": "cpu0", "temperature": "85.0"}]}
|
||||
if "temperature" in path else SYSTEM_RESOURCES_RESPONSE
|
||||
if "temperature" in path
|
||||
else SYSTEM_RESOURCES_RESPONSE
|
||||
)
|
||||
env = driver.get_environment()
|
||||
assert env["temperature"]["cpu0"]["is_alert"] is True
|
||||
@@ -513,6 +523,7 @@ class TestGetEnvironment:
|
||||
|
||||
def test_temperature_endpoint_failing_gracefully(self, driver):
|
||||
"""Driver must not raise if temperature endpoint is unavailable."""
|
||||
|
||||
def fake_get(path):
|
||||
if "temperature" in path:
|
||||
raise Exception("no sensor data")
|
||||
@@ -594,19 +605,22 @@ class TestGetRouteTo:
|
||||
def test_filter_by_protocol(self, driver):
|
||||
driver._get = lambda path: ROUTES_RESPONSE
|
||||
routes = driver.get_route_to(protocol="static")
|
||||
assert all(
|
||||
e["protocol"] == "static"
|
||||
for entries in routes.values()
|
||||
for e in entries
|
||||
)
|
||||
assert all(e["protocol"] == "static" for entries in routes.values() for e in entries)
|
||||
|
||||
def test_required_keys_in_entry(self, driver):
|
||||
driver._get = lambda path: ROUTES_RESPONSE
|
||||
entry = driver.get_route_to()["0.0.0.0/0"][0]
|
||||
for key in (
|
||||
"protocol", "current_active", "last_active", "age",
|
||||
"next_hop", "outgoing_interface", "selected_next_hop",
|
||||
"preference", "inactive_reason", "routing_table",
|
||||
"protocol",
|
||||
"current_active",
|
||||
"last_active",
|
||||
"age",
|
||||
"next_hop",
|
||||
"outgoing_interface",
|
||||
"selected_next_hop",
|
||||
"preference",
|
||||
"inactive_reason",
|
||||
"routing_table",
|
||||
"protocol_attributes",
|
||||
):
|
||||
assert key in entry
|
||||
@@ -707,15 +721,21 @@ class TestGetLldpNeighborsDetail:
|
||||
detail = driver.get_lldp_neighbors_detail()
|
||||
entry = detail["em0"][0]
|
||||
for key in (
|
||||
"remote_chassis_id", "remote_system_name", "remote_port",
|
||||
"remote_port_description", "remote_system_description",
|
||||
"remote_system_capab", "remote_system_enable_capab",
|
||||
"remote_chassis_id",
|
||||
"remote_system_name",
|
||||
"remote_port",
|
||||
"remote_port_description",
|
||||
"remote_system_description",
|
||||
"remote_system_capab",
|
||||
"remote_system_enable_capab",
|
||||
):
|
||||
assert key in entry
|
||||
|
||||
def test_chassis_id(self, driver):
|
||||
driver._get = lambda path: LLDP_RESPONSE
|
||||
assert driver.get_lldp_neighbors_detail()["em0"][0]["remote_chassis_id"] == "aa:bb:cc:dd:ee:ff"
|
||||
assert (
|
||||
driver.get_lldp_neighbors_detail()["em0"][0]["remote_chassis_id"] == "aa:bb:cc:dd:ee:ff"
|
||||
)
|
||||
|
||||
def test_system_name(self, driver):
|
||||
driver._get = lambda path: LLDP_RESPONSE
|
||||
@@ -1010,12 +1030,19 @@ class TestGetBgpNeighbors:
|
||||
def test_required_peer_keys(self, driver):
|
||||
driver._get = self._fake_get
|
||||
peer = driver.get_bgp_neighbors()["global"]["peers"]["10.0.0.1"]
|
||||
for key in ("local_as", "remote_as", "remote_id", "is_up", "is_enabled",
|
||||
"description", "uptime", "address_family"):
|
||||
for key in (
|
||||
"local_as",
|
||||
"remote_as",
|
||||
"remote_id",
|
||||
"is_up",
|
||||
"is_enabled",
|
||||
"description",
|
||||
"uptime",
|
||||
"address_family",
|
||||
):
|
||||
assert key in peer
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_certificates()
|
||||
#
|
||||
@@ -1180,15 +1207,24 @@ class TestGetDdnsStatus:
|
||||
|
||||
RADIUS_CLIENTS_RESPONSE = {
|
||||
"rows": [
|
||||
{"uuid": "e5483f1b-936b-47ba-8cca-24d56ad643c6", "enabled": "1",
|
||||
"name": "ap-lobby", "secret": "s3cr3t", "ip": "10.0.0.5/32"},
|
||||
{
|
||||
"uuid": "e5483f1b-936b-47ba-8cca-24d56ad643c6",
|
||||
"enabled": "1",
|
||||
"name": "ap-lobby",
|
||||
"secret": "s3cr3t",
|
||||
"ip": "10.0.0.5/32",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
RADIUS_USERS_RESPONSE = {
|
||||
"rows": [
|
||||
{"uuid": "71da23fd-03f3-4e1f-a7e1-bb25645b981e", "enabled": "1",
|
||||
"username": "jdoe", "password": "hunter2"},
|
||||
{
|
||||
"uuid": "71da23fd-03f3-4e1f-a7e1-bb25645b981e",
|
||||
"enabled": "1",
|
||||
"username": "jdoe",
|
||||
"password": "hunter2",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1237,10 +1273,13 @@ class TestCreateRadiusClient:
|
||||
|
||||
def test_validation_failure_does_not_reconfigure(self, driver):
|
||||
calls = []
|
||||
driver._post = lambda path, data=None: calls.append(path) or {
|
||||
"result": "failed",
|
||||
"validations": {"client.name": "A value is required."},
|
||||
}
|
||||
driver._post = lambda path, data=None: (
|
||||
calls.append(path)
|
||||
or {
|
||||
"result": "failed",
|
||||
"validations": {"client.name": "A value is required."},
|
||||
}
|
||||
)
|
||||
result = driver.create_radius_client("", "10.0.0.5/32", "s3cr3t")
|
||||
assert result["success"] is False
|
||||
assert result["validations"] == {"client.name": "A value is required."}
|
||||
@@ -1320,10 +1359,13 @@ class TestCreateRadiusUser:
|
||||
|
||||
def test_validation_failure_does_not_reconfigure(self, driver):
|
||||
calls = []
|
||||
driver._post = lambda path, data=None: calls.append(path) or {
|
||||
"result": "failed",
|
||||
"validations": {"user.username": "A value is required."},
|
||||
}
|
||||
driver._post = lambda path, data=None: (
|
||||
calls.append(path)
|
||||
or {
|
||||
"result": "failed",
|
||||
"validations": {"user.username": "A value is required."},
|
||||
}
|
||||
)
|
||||
result = driver.create_radius_user("", "hunter2")
|
||||
assert result["success"] is False
|
||||
assert calls == ["/api/freeradius/user/add_user"]
|
||||
@@ -1358,6 +1400,7 @@ class TestDeleteRadiusUser:
|
||||
# _post()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestInternalPost:
|
||||
def test_raises_when_no_session(self):
|
||||
drv = OPNsenseDriver("host", "u", "p")
|
||||
@@ -1393,6 +1436,7 @@ class TestInternalPost:
|
||||
# send_wake_on_lan()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSendWakeOnLan:
|
||||
def test_raises_value_error_without_interface(self, driver):
|
||||
with pytest.raises(ValueError):
|
||||
@@ -1413,7 +1457,10 @@ class TestSendWakeOnLan:
|
||||
# success — the underlying `wol` CLI tool typically prints nothing.
|
||||
driver.session.post.return_value = _make_json_response({"status": ""})
|
||||
result = driver.send_wake_on_lan("AA:BB:CC:DD:EE:FF", interface="lan")
|
||||
assert result == {"success": True, "output": "Magic packet sent to AA:BB:CC:DD:EE:FF via lan"}
|
||||
assert result == {
|
||||
"success": True,
|
||||
"output": "Magic packet sent to AA:BB:CC:DD:EE:FF via lan",
|
||||
}
|
||||
|
||||
def test_returns_failure_on_error_status(self, driver):
|
||||
driver.session.post.return_value = _make_json_response(
|
||||
@@ -1441,10 +1488,12 @@ class TestSendWakeOnLan:
|
||||
# load_merge_candidate()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
VALID_ROUTES_CONFIG = json.dumps([
|
||||
{"network": "10.0.0.0/8", "gateway": "WAN_GW", "descr": "internal"},
|
||||
{"network": "0.0.0.0/0", "gateway": "WAN_GW", "descr": "default"},
|
||||
])
|
||||
VALID_ROUTES_CONFIG = json.dumps(
|
||||
[
|
||||
{"network": "10.0.0.0/8", "gateway": "WAN_GW", "descr": "internal"},
|
||||
{"network": "0.0.0.0/0", "gateway": "WAN_GW", "descr": "default"},
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class TestLoadMergeCandidate:
|
||||
@@ -1467,38 +1516,45 @@ class TestLoadMergeCandidate:
|
||||
|
||||
def test_raises_if_both_args_given(self, driver):
|
||||
from napalm.base.exceptions import MergeConfigException
|
||||
|
||||
with pytest.raises(MergeConfigException):
|
||||
driver.load_merge_candidate(filename="f.json", config="{}")
|
||||
|
||||
def test_raises_if_no_args_given(self, driver):
|
||||
from napalm.base.exceptions import MergeConfigException
|
||||
|
||||
with pytest.raises(MergeConfigException):
|
||||
driver.load_merge_candidate()
|
||||
|
||||
def test_raises_on_invalid_json(self, driver):
|
||||
from napalm.base.exceptions import MergeConfigException
|
||||
|
||||
with pytest.raises(MergeConfigException, match="Invalid JSON"):
|
||||
driver.load_merge_candidate(config="not json {{{")
|
||||
|
||||
def test_raises_if_not_a_list(self, driver):
|
||||
from napalm.base.exceptions import MergeConfigException
|
||||
|
||||
with pytest.raises(MergeConfigException, match="array"):
|
||||
driver.load_merge_candidate(config='{"network": "1.0.0.0/8"}')
|
||||
|
||||
def test_raises_if_route_missing_network(self, driver):
|
||||
from napalm.base.exceptions import MergeConfigException
|
||||
|
||||
bad = json.dumps([{"gateway": "GW1"}])
|
||||
with pytest.raises(MergeConfigException, match="'network'"):
|
||||
driver.load_merge_candidate(config=bad)
|
||||
|
||||
def test_raises_if_route_missing_gateway(self, driver):
|
||||
from napalm.base.exceptions import MergeConfigException
|
||||
|
||||
bad = json.dumps([{"network": "10.0.0.0/8"}])
|
||||
with pytest.raises(MergeConfigException, match="'gateway'"):
|
||||
driver.load_merge_candidate(config=bad)
|
||||
|
||||
def test_raises_on_missing_file(self, driver):
|
||||
from napalm.base.exceptions import MergeConfigException
|
||||
|
||||
with pytest.raises(MergeConfigException, match="Cannot read"):
|
||||
driver.load_merge_candidate(filename="/nonexistent/path/routes.json")
|
||||
|
||||
@@ -1532,9 +1588,9 @@ class TestCompareConfig:
|
||||
assert "WAN_GW" in diff
|
||||
|
||||
def test_empty_diff_when_config_matches(self, driver):
|
||||
same_config = json.dumps([
|
||||
{"network": "192.168.1.0/24", "gateway": "LAN_GW", "descr": "lan", "disabled": "0"}
|
||||
])
|
||||
same_config = json.dumps(
|
||||
[{"network": "192.168.1.0/24", "gateway": "LAN_GW", "descr": "lan", "disabled": "0"}]
|
||||
)
|
||||
driver._get = lambda path: SEARCH_ROUTE_RESPONSE
|
||||
driver.load_merge_candidate(config=same_config)
|
||||
diff = driver.compare_config()
|
||||
@@ -1547,7 +1603,11 @@ class TestCompareConfig:
|
||||
|
||||
BACKUPS_RESPONSE = {
|
||||
"items": [
|
||||
{"id": "config-opnsense01-1234567890.xml", "time": "1234567890", "description": "before change"},
|
||||
{
|
||||
"id": "config-opnsense01-1234567890.xml",
|
||||
"time": "1234567890",
|
||||
"description": "before change",
|
||||
},
|
||||
{"id": "config-opnsense01-1234567800.xml", "time": "1234567800", "description": "initial"},
|
||||
]
|
||||
}
|
||||
@@ -1556,6 +1616,7 @@ BACKUPS_RESPONSE = {
|
||||
class TestCommitConfig:
|
||||
def test_raises_without_candidate(self, driver):
|
||||
from napalm.base.exceptions import MergeConfigException
|
||||
|
||||
with pytest.raises(MergeConfigException, match="No candidate"):
|
||||
driver.commit_config()
|
||||
|
||||
@@ -1589,8 +1650,9 @@ class TestCommitConfig:
|
||||
assert driver._pre_commit_backup_id is None
|
||||
|
||||
def test_raises_on_api_error(self, driver):
|
||||
from napalm.base.exceptions import MergeConfigException
|
||||
import requests as _req
|
||||
from napalm.base.exceptions import MergeConfigException
|
||||
|
||||
driver._get = lambda path: BACKUPS_RESPONSE
|
||||
driver.load_merge_candidate(config=VALID_ROUTES_CONFIG)
|
||||
driver.session.post.side_effect = _req.exceptions.RequestException("timeout")
|
||||
@@ -1602,6 +1664,7 @@ class TestCommitConfig:
|
||||
# discard_config()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestDiscardConfig:
|
||||
def test_clears_candidate(self, driver):
|
||||
driver.load_merge_candidate(config=VALID_ROUTES_CONFIG)
|
||||
@@ -1617,6 +1680,7 @@ class TestDiscardConfig:
|
||||
# rollback()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRollback:
|
||||
def test_noop_when_no_backups_and_no_commit(self, driver):
|
||||
driver._get = lambda path: {"items": []}
|
||||
@@ -1655,6 +1719,7 @@ class TestRollback:
|
||||
# get_config() — candidate slot
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetConfigCandidate:
|
||||
def test_candidate_empty_without_staged_config(self, driver):
|
||||
driver._get_text = lambda path: "<opnsense/>"
|
||||
@@ -1683,7 +1748,9 @@ class TestGetConfigCandidate:
|
||||
# the getter called .json() on XML and always returned an empty "running".
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
CONFIG_XML = '<?xml version="1.0"?>\n<opnsense><system><hostname>gw</hostname></system></opnsense>\n'
|
||||
CONFIG_XML = (
|
||||
'<?xml version="1.0"?>\n<opnsense><system><hostname>gw</hostname></system></opnsense>\n'
|
||||
)
|
||||
|
||||
|
||||
def _make_xml_response(text):
|
||||
@@ -1767,7 +1834,9 @@ class TestCreateDhcpReservation:
|
||||
assert payload["description"] == "[netork]"
|
||||
|
||||
reconfigure_call = driver.session.post.call_args_list[2]
|
||||
assert reconfigure_call.args[0] == "https://opnsense.example.com/api/kea/service/reconfigure"
|
||||
assert (
|
||||
reconfigure_call.args[0] == "https://opnsense.example.com/api/kea/service/reconfigure"
|
||||
)
|
||||
|
||||
def test_updates_existing_reservation_for_same_ip(self, driver):
|
||||
driver.session.get.return_value = _make_json_response(KEA_SUBNETS_RESPONSE)
|
||||
@@ -1934,7 +2003,9 @@ class TestDeleteDhcpReservationAndLease:
|
||||
== "https://opnsense.example.com/api/kea/dhcpv4/delReservation/existing-uuid-456"
|
||||
)
|
||||
reconfigure_call = driver.session.post.call_args_list[2]
|
||||
assert reconfigure_call.args[0] == "https://opnsense.example.com/api/kea/service/reconfigure"
|
||||
assert (
|
||||
reconfigure_call.args[0] == "https://opnsense.example.com/api/kea/service/reconfigure"
|
||||
)
|
||||
del_lease_call = driver.session.post.call_args_list[3]
|
||||
# IP is a URL path segment, not a JSON body field — verified live
|
||||
# against a real OPNsense instance; a {"ips": [ip]} body (the
|
||||
@@ -2006,6 +2077,7 @@ class TestDeleteDhcpReservationAndLease:
|
||||
# apply_firewall_rule / commit_firewall_rules
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestApplyFirewallRule:
|
||||
def test_add_calls_addRule_with_translated_fields(self, driver):
|
||||
calls = []
|
||||
@@ -2356,7 +2428,9 @@ class TestGetDhcpSubnets:
|
||||
paths = []
|
||||
driver._get = lambda path: (
|
||||
paths.append(path)
|
||||
or (KEA_SUBNET_SEARCH_RESPONSE if "searchSubnet" in path else KEA_SUBNET_DETAIL_RESPONSE)
|
||||
or (
|
||||
KEA_SUBNET_SEARCH_RESPONSE if "searchSubnet" in path else KEA_SUBNET_DETAIL_RESPONSE
|
||||
)
|
||||
)
|
||||
|
||||
driver.get_dhcp_subnets()
|
||||
@@ -2491,10 +2565,7 @@ class TestApplyDhcpSubnet:
|
||||
}
|
||||
)
|
||||
|
||||
assert (
|
||||
calls[0][1]["subnet"]["pools"]
|
||||
== "10.10.99.10-10.10.99.20\n10.10.99.30-10.10.99.40"
|
||||
)
|
||||
assert calls[0][1]["subnet"]["pools"] == "10.10.99.10-10.10.99.20\n10.10.99.30-10.10.99.40"
|
||||
|
||||
def test_explicit_option_data_disables_autocollect(self, driver):
|
||||
"""Kea would otherwise re-fill routers/DNS/NTP and fight the desired state."""
|
||||
@@ -2553,8 +2624,15 @@ class TestKeaSubnetRecordKey:
|
||||
# DNS host overrides
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _override_row(uuid, host="opnsense-office", domain="office.local",
|
||||
ip="10.10.0.1", description="[netork]", **extra):
|
||||
|
||||
def _override_row(
|
||||
uuid,
|
||||
host="opnsense-office",
|
||||
domain="office.local",
|
||||
ip="10.10.0.1",
|
||||
description="[netork]",
|
||||
**extra,
|
||||
):
|
||||
row = {
|
||||
"uuid": uuid,
|
||||
"hostname": host,
|
||||
@@ -2640,9 +2718,17 @@ class TestSyncDnsZone:
|
||||
rows = [_override_row("uuid-1"), _override_row("uuid-2"), _override_row("uuid-3")]
|
||||
calls = self._record_calls(driver, rows)
|
||||
|
||||
driver.sync_dns_zone("office.local", [
|
||||
{"hostname": "opnsense-office", "ip": "10.10.0.1", "record_type": "A", "enabled": True}
|
||||
])
|
||||
driver.sync_dns_zone(
|
||||
"office.local",
|
||||
[
|
||||
{
|
||||
"hostname": "opnsense-office",
|
||||
"ip": "10.10.0.1",
|
||||
"record_type": "A",
|
||||
"enabled": True,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
deleted = [p for p, _ in calls if "delhostoverride" in p]
|
||||
assert sorted(p.rsplit("/", 1)[-1] for p in deleted) == ["uuid-1", "uuid-2", "uuid-3"]
|
||||
@@ -2665,10 +2751,23 @@ class TestSyncDnsZone:
|
||||
in; the device still ends up with a single override."""
|
||||
calls = self._record_calls(driver, [])
|
||||
|
||||
driver.sync_dns_zone("office.local", [
|
||||
{"hostname": "opnsense-office", "ip": "10.10.0.1", "record_type": "A", "enabled": True},
|
||||
{"hostname": "opnsense-office", "ip": "10.10.0.1", "record_type": "A", "enabled": True},
|
||||
])
|
||||
driver.sync_dns_zone(
|
||||
"office.local",
|
||||
[
|
||||
{
|
||||
"hostname": "opnsense-office",
|
||||
"ip": "10.10.0.1",
|
||||
"record_type": "A",
|
||||
"enabled": True,
|
||||
},
|
||||
{
|
||||
"hostname": "opnsense-office",
|
||||
"ip": "10.10.0.1",
|
||||
"record_type": "A",
|
||||
"enabled": True,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
added = [d for p, d in calls if p.endswith("addhostoverride")]
|
||||
assert len(added) == 1
|
||||
@@ -2680,9 +2779,12 @@ class TestSyncDnsZone:
|
||||
rows = [_override_row("by-hand", host="nas", ip="172.22.40.8", description="")]
|
||||
calls = self._record_calls(driver, rows)
|
||||
|
||||
driver.sync_dns_zone("office.local", [
|
||||
{"hostname": "nas", "ip": "172.22.40.8", "record_type": "A", "enabled": True},
|
||||
])
|
||||
driver.sync_dns_zone(
|
||||
"office.local",
|
||||
[
|
||||
{"hostname": "nas", "ip": "172.22.40.8", "record_type": "A", "enabled": True},
|
||||
],
|
||||
)
|
||||
|
||||
assert [p for p, _ in calls if p.endswith("addhostoverride")] == []
|
||||
assert [p for p, _ in calls if "delhostoverride" in p] == []
|
||||
@@ -2695,10 +2797,13 @@ class TestSyncDnsZone:
|
||||
]
|
||||
calls = self._record_calls(driver, rows)
|
||||
|
||||
driver.sync_dns_zone("office.local", [
|
||||
{"hostname": "nas", "ip": "172.22.40.8", "record_type": "A", "enabled": True},
|
||||
{"hostname": "NAS", "ip": "172.22.40.8", "record_type": "A", "enabled": True},
|
||||
])
|
||||
driver.sync_dns_zone(
|
||||
"office.local",
|
||||
[
|
||||
{"hostname": "nas", "ip": "172.22.40.8", "record_type": "A", "enabled": True},
|
||||
{"hostname": "NAS", "ip": "172.22.40.8", "record_type": "A", "enabled": True},
|
||||
],
|
||||
)
|
||||
|
||||
assert [p.rsplit("/", 1)[-1] for p, _ in calls if "delhostoverride" in p] == ["copy"]
|
||||
assert [p for p, _ in calls if p.endswith("addhostoverride")] == []
|
||||
@@ -2707,33 +2812,44 @@ class TestSyncDnsZone:
|
||||
rows = [_override_row("by-hand", host="nas", ip="172.22.40.8", description="")]
|
||||
calls = self._record_calls(driver, rows)
|
||||
|
||||
driver.sync_dns_zone("office.local", [
|
||||
{"hostname": "nas", "ip": "172.22.40.9", "record_type": "A", "enabled": True},
|
||||
])
|
||||
driver.sync_dns_zone(
|
||||
"office.local",
|
||||
[
|
||||
{"hostname": "nas", "ip": "172.22.40.9", "record_type": "A", "enabled": True},
|
||||
],
|
||||
)
|
||||
|
||||
added = [d for p, d in calls if p.endswith("addhostoverride")]
|
||||
assert [d["host"]["server"] for d in added] == ["172.22.40.9"]
|
||||
|
||||
def test_a_disabled_hand_made_override_does_not_stand_in(self, driver):
|
||||
"""It answers nothing, so the record still has to be added."""
|
||||
rows = [_override_row("by-hand", host="nas", ip="172.22.40.8", description="",
|
||||
enabled="0")]
|
||||
rows = [_override_row("by-hand", host="nas", ip="172.22.40.8", description="", enabled="0")]
|
||||
calls = self._record_calls(driver, rows)
|
||||
|
||||
driver.sync_dns_zone("office.local", [
|
||||
{"hostname": "nas", "ip": "172.22.40.8", "record_type": "A", "enabled": True},
|
||||
])
|
||||
driver.sync_dns_zone(
|
||||
"office.local",
|
||||
[
|
||||
{"hostname": "nas", "ip": "172.22.40.8", "record_type": "A", "enabled": True},
|
||||
],
|
||||
)
|
||||
|
||||
assert len([p for p, _ in calls if p.endswith("addhostoverride")]) == 1
|
||||
|
||||
def test_a_hand_made_override_in_another_zone_does_not_stand_in(self, driver):
|
||||
rows = [_override_row("by-hand", host="nas", domain="other.local", ip="172.22.40.8",
|
||||
description="")]
|
||||
rows = [
|
||||
_override_row(
|
||||
"by-hand", host="nas", domain="other.local", ip="172.22.40.8", description=""
|
||||
)
|
||||
]
|
||||
calls = self._record_calls(driver, rows)
|
||||
|
||||
driver.sync_dns_zone("office.local", [
|
||||
{"hostname": "nas", "ip": "172.22.40.8", "record_type": "A", "enabled": True},
|
||||
])
|
||||
driver.sync_dns_zone(
|
||||
"office.local",
|
||||
[
|
||||
{"hostname": "nas", "ip": "172.22.40.8", "record_type": "A", "enabled": True},
|
||||
],
|
||||
)
|
||||
|
||||
assert len([p for p, _ in calls if p.endswith("addhostoverride")]) == 1
|
||||
|
||||
|
||||
@@ -59,7 +59,11 @@ class TestDhcpHa:
|
||||
|
||||
def test_an_explicit_server_name_wins(self):
|
||||
ha = dhcp_ha(
|
||||
{"dhcpv4": {"ha": {"enabled": "1", "this_server_name": "left", "%this_server_name": "x"}}},
|
||||
{
|
||||
"dhcpv4": {
|
||||
"ha": {"enabled": "1", "this_server_name": "left", "%this_server_name": "x"}
|
||||
}
|
||||
},
|
||||
{"rows": []},
|
||||
)
|
||||
assert ha["this_server"] == "left"
|
||||
@@ -124,7 +128,9 @@ class TestConfigSync:
|
||||
|
||||
def test_without_a_peer_selected_items_are_not_pushed(self):
|
||||
"""Ticked items without a target address push nothing."""
|
||||
answer = {"hasync": {"synchronizetoip": "", "syncitems": {"kea": {"value": "Kea", "selected": 1}}}}
|
||||
answer = {
|
||||
"hasync": {"synchronizetoip": "", "syncitems": {"kea": {"value": "Kea", "selected": 1}}}
|
||||
}
|
||||
|
||||
assert config_sync(answer) == {"peer": None, "services": [], "unmapped": []}
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ has to be stopped and removed again. These tests drive that whole lifecycle
|
||||
against a fake ``_get`` / ``_post``.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from napalm_opnsense.opnsense import OPNsenseDriver
|
||||
import pytest
|
||||
|
||||
from napalm_opnsense.opnsense import OPNsenseDriver
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fake API
|
||||
@@ -28,11 +28,11 @@ class FakePingAPI:
|
||||
#: to assume one, which is why nothing caught the payload going to the
|
||||
#: wrong node.
|
||||
self.model_path = tuple(model_path)
|
||||
self.created = [] # payloads passed to set
|
||||
self.started = [] # job ids passed to start
|
||||
self.stopped = [] # job ids passed to stop
|
||||
self.removed = [] # job ids passed to remove
|
||||
self.jobs = {} # job id -> hostname
|
||||
self.created = [] # payloads passed to set
|
||||
self.started = [] # job ids passed to start
|
||||
self.stopped = [] # job ids passed to stop
|
||||
self.removed = [] # job ids passed to remove
|
||||
self.jobs = {} # job id -> hostname
|
||||
self.search_calls = 0
|
||||
self._next_id = 0
|
||||
|
||||
|
||||
@@ -112,7 +112,10 @@ class TestWhereItGoes:
|
||||
assert entry["internal_ip"] == "10.0.0.9"
|
||||
|
||||
def test_an_alias_with_two_hosts_is_two_forwards(self):
|
||||
assert [e["internal_ip"] for e in _read(_rule(target="web_pair"))] == ["10.0.0.5", "10.0.0.6"]
|
||||
assert [e["internal_ip"] for e in _read(_rule(target="web_pair"))] == [
|
||||
"10.0.0.5",
|
||||
"10.0.0.6",
|
||||
]
|
||||
|
||||
def test_an_alias_that_names_a_host_by_dns_is_skipped(self):
|
||||
"""No address to put the forward on; guessing one would be worse."""
|
||||
@@ -129,7 +132,14 @@ class TestWhereItGoes:
|
||||
class TestPorts:
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[("443", 443), ("https", 443), ("http", 80), ("postgres", 5432), ("8000-8010", 8000), ("8000:8010", 8000)],
|
||||
[
|
||||
("443", 443),
|
||||
("https", 443),
|
||||
("http", 80),
|
||||
("postgres", 5432),
|
||||
("8000-8010", 8000),
|
||||
("8000:8010", 8000),
|
||||
],
|
||||
)
|
||||
def test_the_external_port(self, value, expected):
|
||||
(entry,) = _read(_rule(**{"destination.port": value}))
|
||||
@@ -173,7 +183,7 @@ class TestNameAndSource:
|
||||
assert "remote_host" not in entry
|
||||
|
||||
def test_an_inverted_source_has_no_remote_host(self):
|
||||
""""Everyone but X" is as good as anyone for whether it is reachable."""
|
||||
""" "Everyone but X" is as good as anyone for whether it is reachable."""
|
||||
(entry,) = _read(_rule(**{"source.network": "203.0.113.7", "source.not": "1"}))
|
||||
assert "remote_host" not in entry
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ from napalm_opnsense.opnsense import OPNsenseDriver
|
||||
@pytest.fixture
|
||||
def driver():
|
||||
with patch("napalm_opnsense.opnsense.requests.Session"):
|
||||
drv = OPNsenseDriver(hostname="fw", username="k", password="s", optional_args={"verify": False})
|
||||
drv = OPNsenseDriver(
|
||||
hostname="fw", username="k", password="s", optional_args={"verify": False}
|
||||
)
|
||||
drv.session = MagicMock()
|
||||
yield drv
|
||||
|
||||
@@ -26,7 +28,13 @@ def driver():
|
||||
class TestUpdateProgress:
|
||||
@pytest.mark.parametrize(
|
||||
"status, state",
|
||||
[("running", "running"), ("done", "done"), ("error", "failed"), ("", "idle"), (None, "idle")],
|
||||
[
|
||||
("running", "running"),
|
||||
("done", "done"),
|
||||
("error", "failed"),
|
||||
("", "idle"),
|
||||
(None, "idle"),
|
||||
],
|
||||
)
|
||||
def test_the_upgrade_status_as_a_state(self, driver, status, state):
|
||||
st = {"status": status, "log": "***GOT REQUEST TO UPGRADE***\nfetching\n"}
|
||||
@@ -54,8 +62,22 @@ class TestUpdateProgress:
|
||||
|
||||
|
||||
ROWS = [
|
||||
{"uuid": "u1", "enabled": "1", "type": "forward", "domain": "lab.example", "server": "10.0.0.53", "port": "53"},
|
||||
{"uuid": "u2", "enabled": "0", "type": "forward", "domain": "old.example", "server": "10.0.0.9", "port": ""},
|
||||
{
|
||||
"uuid": "u1",
|
||||
"enabled": "1",
|
||||
"type": "forward",
|
||||
"domain": "lab.example",
|
||||
"server": "10.0.0.53",
|
||||
"port": "53",
|
||||
},
|
||||
{
|
||||
"uuid": "u2",
|
||||
"enabled": "0",
|
||||
"type": "forward",
|
||||
"domain": "old.example",
|
||||
"server": "10.0.0.9",
|
||||
"port": "",
|
||||
},
|
||||
{"uuid": "u3", "enabled": "1", "type": "dot", "domain": "", "server": "9.9.9.9", "port": "853"},
|
||||
]
|
||||
|
||||
@@ -86,9 +108,18 @@ class TestDnsForwards:
|
||||
driver.set_dns_forward("new.example.", "10.0.0.53")
|
||||
|
||||
paths = [p for p, _ in calls]
|
||||
assert paths[-2:] == ["/api/unbound/settings/addForward", "/api/unbound/service/reconfigure"]
|
||||
assert paths[-2:] == [
|
||||
"/api/unbound/settings/addForward",
|
||||
"/api/unbound/service/reconfigure",
|
||||
]
|
||||
added = calls[-2][1]["dot"]
|
||||
assert added == {"enabled": "1", "type": "forward", "domain": "new.example", "server": "10.0.0.53", "port": "53"}
|
||||
assert added == {
|
||||
"enabled": "1",
|
||||
"type": "forward",
|
||||
"domain": "new.example",
|
||||
"server": "10.0.0.53",
|
||||
"port": "53",
|
||||
}
|
||||
|
||||
def test_a_known_domain_is_changed_not_doubled(self, driver):
|
||||
calls = []
|
||||
@@ -105,7 +136,10 @@ class TestDnsForwards:
|
||||
return {"rows": []}
|
||||
return {"result": "failed", "validations": {"dot.server": "invalid"}}
|
||||
|
||||
with patch.object(driver, "_post", side_effect=post), pytest.raises(RuntimeError, match="invalid"):
|
||||
with (
|
||||
patch.object(driver, "_post", side_effect=post),
|
||||
pytest.raises(RuntimeError, match="invalid"),
|
||||
):
|
||||
driver.set_dns_forward("x.example", "not-an-ip")
|
||||
|
||||
def test_removing_deletes_every_forward_of_the_domain(self, driver):
|
||||
@@ -114,7 +148,10 @@ class TestDnsForwards:
|
||||
driver.remove_dns_forward("lab.example.")
|
||||
|
||||
paths = [p for p, _ in calls]
|
||||
assert paths[-2:] == ["/api/unbound/settings/delForward/u1", "/api/unbound/service/reconfigure"]
|
||||
assert paths[-2:] == [
|
||||
"/api/unbound/settings/delForward/u1",
|
||||
"/api/unbound/service/reconfigure",
|
||||
]
|
||||
|
||||
def test_removing_an_unknown_domain_changes_nothing(self, driver):
|
||||
calls = []
|
||||
|
||||
@@ -83,7 +83,11 @@ class TestTheCallersValues:
|
||||
assert result["success"] is True
|
||||
general = _body(api, "/api/netsnmp/general/set")["general"]
|
||||
assert general["enabled"] == "1"
|
||||
assert (general["community"], general["contact"], general["location"]) == ("c0mm", "ops", "rack 1")
|
||||
assert (general["community"], general["contact"], general["location"]) == (
|
||||
"c0mm",
|
||||
"ops",
|
||||
"rack 1",
|
||||
)
|
||||
assert general["listen"] == {"10.0.0.1": {"selected": 1}}
|
||||
|
||||
def test_nothing_of_an_application_is_built_in(self, driver, api):
|
||||
|
||||
Reference in new issue
Block a user