modified: netbox_services/engine/allocators.py modified: netbox_services/engine/context.py new file: netbox_services/engine/mappers.py modified: netbox_services/engine/resolver.py new file: netbox_services/engine/templating.py modified: netbox_services/forms.py modified: netbox_services/models.py modified: netbox_services/tests/test_allocators.py new file: netbox_services/tests/test_mappers.py modified: netbox_services/tests/test_resolver.py new file: netbox_services/tests/test_templating.py Simplify layout to template and service instances
131 lines
4.4 KiB
Python
131 lines
4.4 KiB
Python
"""Tests for Jinja2 attribute templating with .. / ... parent access."""
|
|
|
|
import pytest
|
|
|
|
from netbox_services.engine.templating import (
|
|
preprocess_dots,
|
|
render_value,
|
|
render_attrs,
|
|
)
|
|
|
|
|
|
class TestPreprocessDots:
|
|
def test_double_dot(self):
|
|
assert preprocess_dots("{{ ..name }}") == "{{ p1.name }}"
|
|
|
|
def test_triple_dot(self):
|
|
assert preprocess_dots("{{ ...site.slug }}") == "{{ p2.site.slug }}"
|
|
|
|
def test_quadruple_dot(self):
|
|
assert preprocess_dots("{{ ....region.slug }}") == "{{ p3.region.slug }}"
|
|
|
|
def test_no_dots(self):
|
|
assert preprocess_dots("1000base-t") == "1000base-t"
|
|
|
|
def test_ip_address_preserved(self):
|
|
assert preprocess_dots("10.0.0.1/24") == "10.0.0.1/24"
|
|
|
|
def test_service_ref_preserved(self):
|
|
assert preprocess_dots("{{ service.tenant.slug }}") == "{{ service.tenant.slug }}"
|
|
|
|
def test_mixed(self):
|
|
assert (
|
|
preprocess_dots("rtr-{{ service.tenant.slug }}-{{ ..name }}-{{ ...site.slug }}")
|
|
== "rtr-{{ service.tenant.slug }}-{{ p1.name }}-{{ p2.site.slug }}"
|
|
)
|
|
|
|
def test_plain_string_no_braces(self):
|
|
assert preprocess_dots("active") == "active"
|
|
|
|
def test_empty_string(self):
|
|
assert preprocess_dots("") == ""
|
|
|
|
|
|
class TestRenderValue:
|
|
def test_literal_int(self):
|
|
assert render_value(42, None, [], 0) == 42
|
|
|
|
def test_literal_bool(self):
|
|
assert render_value(True, None, [], 0) is True
|
|
|
|
def test_plain_string(self):
|
|
assert render_value("active", None, [], 0) == "active"
|
|
|
|
def test_ip_address_string(self):
|
|
assert render_value("10.0.0.1/24", None, [], 0) == "10.0.0.1/24"
|
|
|
|
def test_jinja2_with_parent(self):
|
|
parent = {"name": "rtr-01"}
|
|
result = render_value("{{ p1.name }}", None, [parent], 0)
|
|
assert result == "rtr-01"
|
|
|
|
def test_jinja2_with_grandparent(self):
|
|
parent = {"name": "dev", "site": None}
|
|
grandparent = {"site": {"slug": "hq"}}
|
|
result = render_value("{{ p2.site.slug }}", None, [parent, grandparent], 0)
|
|
assert result == "hq"
|
|
|
|
def test_jinja2_with_service(self):
|
|
service = {"tenant": {"slug": "acme"}}
|
|
result = render_value("{{ service.tenant.slug }}", service, [], 0)
|
|
assert result == "acme"
|
|
|
|
def test_jinja2_with_index(self):
|
|
result = render_value("rtr-{{ index + 1 }}", None, [], 0)
|
|
assert result == "rtr-1"
|
|
|
|
def test_jinja2_dotted_parent_access(self):
|
|
parent = {"name": "edge-router", "site": {"slug": "hq"}}
|
|
result = render_value("{{ ..site.slug }}", None, [parent], 0)
|
|
assert result == "hq"
|
|
|
|
def test_jinja2_dotted_parent_name(self):
|
|
parent = {"name": "edge-router"}
|
|
result = render_value("iface-{{ ..name }}", None, [parent], 0)
|
|
assert result == "iface-edge-router"
|
|
|
|
def test_django_model_traversal(self):
|
|
from dcim.models import Device, DeviceRole, Site
|
|
|
|
site = Site(name="HQ", slug="hq")
|
|
role = DeviceRole(name="Router", slug="router")
|
|
parent = Device(name="rtr-01", site=site, role=role)
|
|
result = render_value("{{ p1.site.slug }}-{{ p1.role.slug }}", None, [parent], 0)
|
|
assert result == "hq-router"
|
|
|
|
|
|
class TestRenderAttrs:
|
|
def test_plain_attrs(self):
|
|
attrs = {"name": "eth0", "type": "1000base-t", "mtu": 1500}
|
|
result = render_attrs(attrs, None, [], 0)
|
|
assert result == {"name": "eth0", "type": "1000base-t", "mtu": 1500}
|
|
|
|
def test_templated_attrs(self):
|
|
parent = {"name": "rtr-01"}
|
|
attrs = {"name": "iface-{{ ..name }}", "type": "1000base-t"}
|
|
result = render_attrs(attrs, None, [parent], 0)
|
|
assert result == {"name": "iface-rtr-01", "type": "1000base-t"}
|
|
|
|
def test_empty_attrs(self):
|
|
assert render_attrs({}, None, [], 0) == {}
|
|
|
|
def test_none_attrs(self):
|
|
assert render_attrs(None, None, [], 0) == {}
|
|
|
|
def test_index_in_attrs(self):
|
|
attrs = {"name": "rtr-{{ index + 1 }}"}
|
|
assert render_attrs(attrs, None, [], 0) == {"name": "rtr-1"}
|
|
assert render_attrs(attrs, None, [], 2) == {"name": "rtr-3"}
|
|
|
|
|
|
class TestSandboxSecurity:
|
|
def test_blocks_dunder_class(self):
|
|
parent = {"name": "test"}
|
|
result = render_value("{{ p1.__class__ }}", None, [parent], 0)
|
|
assert "dict" not in str(result)
|
|
|
|
def test_blocks_method_call(self):
|
|
parent = {"name": "test"}
|
|
with pytest.raises(Exception):
|
|
render_value("{{ p1.pop() }}", None, [parent], 0)
|