diff --git a/oxi/interfaces/utils.py b/oxi/interfaces/utils.py new file mode 100644 index 0000000..f91466d --- /dev/null +++ b/oxi/interfaces/utils.py @@ -0,0 +1,25 @@ +def expand_vlan_range(value: str | list[str]) -> list[str]: + """Expand values like '1,7,14-15' into individual VLAN IDs.""" + if isinstance(value, list): + value = ",".join(str(item) for item in value) + + result: list[str] = [] + if not value: + return result + for part in value.split(","): + part = part.strip() + if not part: + continue + if "-" in part: + start_s, end_s = part.split("-", 1) + try: + start, end = int(start_s), int(end_s) + except ValueError: + result.append(part) + continue + if start > end: + start, end = end, start + result.extend(str(i) for i in range(start, end + 1)) + else: + result.append(part) + return result diff --git a/tests/test_units.py b/tests/test_units.py new file mode 100644 index 0000000..ad7cf32 --- /dev/null +++ b/tests/test_units.py @@ -0,0 +1,71 @@ +import pytest + +from conftest import load +from oxi.exception import OxiAPIError +from oxi.interfaces import device_registry +from oxi.interfaces.base import BaseDevice +from oxi.interfaces.contract import Interfaces, System +from oxi.interfaces.utils import expand_vlan_range as eltex_expand +from oxi.interfaces.utils import expand_vlan_range as qtech_expand + + +class TestExpandVlanRange: + @pytest.mark.parametrize("expand", [qtech_expand, eltex_expand]) + def test_simple_and_range(self, expand): + assert expand("1,7,14-15") == ["1", "7", "14", "15"] + + @pytest.mark.parametrize("expand", [qtech_expand, eltex_expand]) + def test_reversed_range_is_normalized(self, expand): + assert expand("15-13") == ["13", "14", "15"] + + @pytest.mark.parametrize("expand", [qtech_expand, eltex_expand]) + def test_non_numeric_range_kept_verbatim(self, expand): + assert expand("a-b") == ["a-b"] + + @pytest.mark.parametrize("expand", [qtech_expand, eltex_expand]) + def test_empty(self, expand): + assert expand("") == [] + + @pytest.mark.parametrize("expand", [qtech_expand, eltex_expand]) + def test_list_input(self, expand): + assert expand(["1", "3-4"]) == ["1", "3", "4"] + + +class TestKeeneticDecodeUtf: + @pytest.fixture(scope="class") + def keenetic(self): + return device_registry["keenetic"](load("keenetic")) + + def test_plain_text_passthrough(self, keenetic): + assert keenetic._decode_utf("Plain ASCII") == "Plain ASCII" + + def test_escaped_utf8_is_decoded(self, keenetic): + assert keenetic._decode_utf(r'"\xd0\x94\xd0\xbe\xd0\xbc"') == "Дом" + + +class TestTemplateValidation: + def test_missing_required_group_raises(self): + class OnlySystem(BaseDevice): + template = "dummy.ttp" + + def _load_template(self): + return '' + + with pytest.raises(ValueError, match="missing required groups"): + OnlySystem("data") + + def test_missing_template_file_raises(self): + class NoTemplate(BaseDevice): + template = "does_not_exist.ttp" + + with pytest.raises(FileNotFoundError): + NoTemplate("data") + + +class TestNodeNotFound: + def test_not_found_config_raises_on_parse(self): + device = device_registry["eltex"](load("eltex", "not_found.conf"), name="HQ") + assert device.raw is None + with pytest.raises(OxiAPIError) as exc: + device.parse() + assert exc.value.status_code == 404