Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/nested paths #28

Merged
merged 5 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 25 additions & 28 deletions plugins/module_utils/xml_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from __future__ import (absolute_import, division, print_function)

from typing import Union, Optional, List, Set
from typing import Union, Optional, List
from xml.etree.ElementTree import Element

__metaclass__ = type
Expand Down Expand Up @@ -135,34 +135,31 @@ def _process_dict_list(tag: str, input_dict: dict, root: Optional[Element]) -> O
###############################

def etree_to_dict(input_etree: Element) -> dict:
"""
Converts an ElementTree.Element structure to a Python dictionary.

:param input_etree: Input ElementTree.Element.
:return: The generated dict.
"""
input_children: List[Element] = list(input_etree)

# input element has no children, so it is a 'primitive' element
# with just a tag and a content.
if len(input_children) == 0:
return {input_etree.tag: input_etree.text}

unique_input_tags: Set[str] = set([input_child.tag for input_child in input_etree])

# if any group has more than one sub element a list must be constructed
if len(unique_input_tags) != len(input_children):
child_list: list = []
for child in input_children:
child_list.append(etree_to_dict(child))
return {input_etree.tag: child_list}

# here all children have a unique tag, therefore a dict will be built
child_dict: dict = {}

for child in input_children:
sub = etree_to_dict(child)

child_dict = {**child_dict, **sub}

return {input_etree.tag: child_dict}
return {input_etree.tag: input_etree.text} # Return the text directly

children_results = [etree_to_dict(child) for child in input_children]

# If there's only one child node, return it as a dictionary
if len(input_children) == 1:
return {input_etree.tag: children_results[0]}

# If all child tags are the same, wrap them in a list
if len(set(child.tag for child in input_children)) == 1:
return {input_etree.tag: children_results}

result = {}
for child_data in children_results:
for key, value in child_data.items():
if key in result:
if isinstance(result[key], list):
result[key].append(value)
else:
result[key] = [result[key], value]
else:
result[key] = value

return {input_etree.tag: result}
66 changes: 66 additions & 0 deletions tests/unit/plugins/module_utils/test_config_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ def sample_config_path():
config_content = """<?xml version="1.0"?>
<opnsense>
<test_key>test_value</test_key>
<test_nested_key_1>
<test_nested_key_2>test_value</test_nested_key_2>
</test_nested_key_1>
<new_key>
<new_nested_key></new_nested_key>
</new_key>
</opnsense>"""
with NamedTemporaryFile(delete=False) as temp_file:
temp_file.write(config_content.encode())
Expand Down Expand Up @@ -114,13 +120,16 @@ def test_save(sample_config_path):
"""
with OPNsenseConfig(path=sample_config_path) as config:
config["test_key"] = "modified_value"
config["test_nested_key_1"]["test_nested_key_2"] = "modified_nested_value"
assert config.save()
# Reload the saved config and assert the changes were saved
reloaded_config = xml_utils.etree_to_dict(ElementTree.parse(sample_config_path).getroot())["opnsense"]
assert reloaded_config["test_key"] == "modified_value"
assert reloaded_config["test_nested_key_1"]["test_nested_key_2"] == "modified_nested_value"

with OPNsenseConfig(path=sample_config_path) as new_config:
assert new_config["test_key"] == "modified_value"
assert new_config["test_nested_key_1"]["test_nested_key_2"] == "modified_nested_value"


def test_changed(sample_config_path):
Expand Down Expand Up @@ -153,3 +162,60 @@ def test_exit_without_saving(sample_config_path):
with OPNsenseConfig(path=sample_config_path) as config:
config["test_key"] = "modified_value"
# The RuntimeError should be raised upon exiting the context without saving


def test_get_nested_item(sample_config_path):
"""
Test retrieving a nested value from the config.

Given a sample OPNsense configuration file, the test verifies that a specific nested key-value
pair can be retrieved using the OPNsenseConfig object.

The expected behavior is that the retrieved value matches the original value in the config file.
"""
with OPNsenseConfig(path=sample_config_path) as config:
assert config["test_nested_key_1"]["test_nested_key_2"] == "test_value"
assert not config.save()


def test_set_nested_item(sample_config_path):
"""
Test setting a nested value in the config.

Given a sample OPNsense configuration file, the test verifies that a new nested key-value pair
can be added to the config using the OPNsenseConfig object.

The expected behavior is that the added key-value pair is present in the config
and the `save` method returns True indicating that the config has changed. When using
a new config context the changes are expected to persist.
"""
with OPNsenseConfig(path=sample_config_path) as config:
config["new_key"]["new_nested_key"] = "new_value"
assert config["new_key"]["new_nested_key"] == "new_value"
assert config.save()

with OPNsenseConfig(path=sample_config_path) as new_config:
assert "new_key" in new_config
assert new_config["new_key"]["new_nested_key"] == "new_value"


def test_del_nested_item(sample_config_path):
"""
Test deleting a nested value from the config.

Given a sample OPNsense configuration file, the test verifies that a nested key-value pair
can be removed from the config using the `del` statement with the OPNsenseConfig object.

The expected behavior is that the deleted key is no longer present in the config,
the `changed` property is True indicating that the config has changed,
and the `save` method returns True indicating that the config has changed. When using
a new config context the changes are expected to persist.
"""
with OPNsenseConfig(path=sample_config_path) as config:
del config["test_nested_key_1"]["test_nested_key_2"]
assert "test_nested_key_2" not in config["test_nested_key_1"]
assert config.changed
assert config.save()

with OPNsenseConfig(path=sample_config_path) as new_config:
assert "test_nested_key_2" not in new_config
2 changes: 1 addition & 1 deletion tests/unit/plugins/module_utils/test_xml_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ def etree_root(request: pytest.FixtureRequest) -> Element:
"""
xml_string = request.param
tree: ET.ElementTree = ET.ElementTree(ET.fromstring(xml_string))
yield tree.getroot()
return tree.getroot()


@pytest.mark.parametrize("etree_root", [
Expand Down
Loading