"""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)