Files
Netbox-Utilities/netbox_utilities/tests/test_topology_views.py
T

286 lines
12 KiB
Python

from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from django.http import HttpResponse
from django.test import RequestFactory, SimpleTestCase
from netbox_utilities.topology_views import (
_apply_topology_widths_to_device_tags,
_inject_topology_widths,
_topology_data_for_visible_devices,
_visible_topology_device_urls,
apply_topology_rack_widths,
get_topology_rack_width_data,
is_topology_rack_elevation_request,
serialize_topology_placement,
topology_rack_width_enabled,
)
class TopologyViewsRackWidthTest(SimpleTestCase):
def setUp(self):
self.request = RequestFactory().get("/plugins/netbox_topology_views/rack-elevation/?rack_id=3")
self.request.user = MagicMock()
def test_detects_only_topology_rack_elevation_route(self):
self.request.resolver_match = SimpleNamespace(view_name="plugins:netbox_topology_views:rack_elevation")
self.assertTrue(is_topology_rack_elevation_request(self.request))
self.request.resolver_match = SimpleNamespace(view_name="dcim:rack_list")
self.assertFalse(is_topology_rack_elevation_request(self.request))
def test_skips_integration_when_optional_plugin_is_missing(self):
self.request.resolver_match = SimpleNamespace(view_name="plugins:netbox_topology_views:rack_elevation")
with (
patch("netbox_utilities.topology_views.apps.is_installed", return_value=False),
patch("netbox_utilities.topology_views.get_plugin_config", return_value=True),
):
self.assertFalse(topology_rack_width_enabled(self.request))
def test_skips_integration_when_admin_disabled_it(self):
self.request.resolver_match = SimpleNamespace(view_name="plugins:netbox_topology_views:rack_elevation")
with (
patch("netbox_utilities.topology_views.apps.is_installed", return_value=True),
patch("netbox_utilities.topology_views.get_plugin_config", return_value=False),
):
self.assertFalse(topology_rack_width_enabled(self.request))
def test_serializes_fractional_width_geometry_and_device_url(self):
placement = SimpleNamespace(
device_id=17,
device=SimpleNamespace(pk=17, get_absolute_url=lambda: "/dcim/devices/17/"),
width=3,
horizontal_position=2,
)
self.assertEqual(
serialize_topology_placement(placement),
{
"device_id": 17,
"url": "/dcim/devices/17/",
"width": 3,
"horizontal_position": 2,
"left_percent": 33.33333333,
"width_percent": 33.33333333,
"width_source": "stored",
},
)
@patch("netbox_utilities.topology_views.topology_rack_width_enabled", return_value=True)
def test_queries_all_permitted_devices_and_infers_shared_unit_widths(self, _enabled):
self.request.resolver_match = SimpleNamespace(view_name="plugins:netbox_topology_views:rack_elevation")
rack_queryset = MagicMock()
rack_queryset.filter.return_value = rack_queryset
rack_queryset.values.return_value = "selected-rack-query"
rack_model = MagicMock()
rack_model.objects.restrict.return_value = rack_queryset
device_queryset = MagicMock()
selected_devices = MagicMock()
device_queryset.filter.return_value = selected_devices
device_model = MagicMock()
device_model.objects.restrict.return_value = device_queryset
devices = [
SimpleNamespace(
pk=17,
rack_id=3,
face="front",
position=11,
device_type=SimpleNamespace(u_height=1),
netbox_utilities_rack_placement=None,
get_absolute_url=lambda: "/dcim/devices/17/",
),
SimpleNamespace(
pk=18,
rack_id=3,
face="front",
position=11,
device_type=SimpleNamespace(u_height=1),
netbox_utilities_rack_placement=None,
get_absolute_url=lambda: "/dcim/devices/18/",
),
]
selected_devices.select_related.return_value.order_by.return_value = devices
with patch.dict("sys.modules", {"dcim.models": SimpleNamespace(Device=device_model, Rack=rack_model)}):
result = get_topology_rack_width_data(self.request)
rack_queryset.filter.assert_called_once_with(pk__in=["3"])
device_queryset.filter.assert_called_once_with(
rack_id__in="selected-rack-query",
position__gt=0,
device_type__u_height__gt=0,
)
self.assertEqual(result["status"], "ready")
self.assertEqual(result["schema_version"], 3)
self.assertTrue(result["complete"])
self.assertEqual(len(result["devices"]), 2)
self.assertEqual(result["devices"][0]["width"], 2)
self.assertEqual(result["devices"][0]["horizontal_position"], 1)
self.assertEqual(result["devices"][1]["horizontal_position"], 2)
self.assertEqual(result["devices"][0]["width_source"], "inferred")
def test_frontend_supports_live_view_and_all_rack_export_formats(self):
script = (Path(__file__).parents[1] / "static" / "netbox_utilities" / "topology-rack-width.js").read_text(
encoding="utf-8"
)
self.assertIn("netbox-utilities-partial-width", script)
self.assertIn("horizontalGeometry", script)
self.assertIn("buildRackSvg", script)
self.assertIn("buildRackDrawio", script)
self.assertIn("format === 'svg'", script)
self.assertIn("format === 'drawio'", script)
self.assertIn("canvas.toBlob", script)
self.assertIn("event.stopImmediatePropagation()", script)
self.assertIn("rackExportCompatible", script)
self.assertIn("liveViewCompatible", script)
self.assertIn("payload?.devices", script)
self.assertIn("payload.status !== 'ready'", script)
self.assertIn("payload?.schema_version !== 3", script)
self.assertIn("placement.source", script)
self.assertLess(script.index("liveViewCompatible"), script.index("netbox-utilities-partial-width"))
self.assertLess(script.index("exportGraphic(graphic"), script.index("event.preventDefault()"))
@patch("netbox_utilities.topology_views.topology_rack_width_enabled", return_value=True)
def test_malformed_partial_placement_fails_open(self, _enabled):
self.request.resolver_match = SimpleNamespace(view_name="plugins:netbox_topology_views:rack_elevation")
with patch("dcim.models.Rack.objects.restrict", side_effect=AttributeError("stale")):
result = get_topology_rack_width_data(self.request)
self.assertEqual(result["status"], "native-fallback")
self.assertEqual(result["devices"], [])
self.assertFalse(result["complete"])
def test_partial_width_css_uses_topology_device_slot_geometry(self):
stylesheet = (Path(__file__).parents[1] / "static" / "netbox_utilities" / "netbox_utilities.css").read_text(
encoding="utf-8"
)
self.assertIn(".rack-device.netbox-utilities-partial-width", stylesheet)
self.assertIn("--netbox-utilities-rack-device-left", stylesheet)
self.assertIn("--netbox-utilities-rack-device-width", stylesheet)
def test_extracts_only_devices_rendered_by_the_authorized_topology_view(self):
html = """
<a class="rack-device" href="/dcim/devices/334/">Fritzbox</a>
<a class="rack-device active" href="/dcim/devices/340/">Grandstream</a>
<a href="/dcim/devices/999/">Not a rack device</a>
"""
self.assertEqual(
_visible_topology_device_urls(html),
{334: "/dcim/devices/334/", 340: "/dcim/devices/340/"},
)
@patch("dcim.models.Device")
def test_builds_both_stored_half_widths_from_visible_he11_devices(self, device_model):
devices = [
SimpleNamespace(
pk=334,
rack_id=3,
face="front",
position=11,
device_type=SimpleNamespace(u_height=1),
netbox_utilities_rack_placement=SimpleNamespace(width=2, horizontal_position=1),
get_absolute_url=lambda: "/dcim/devices/334/",
),
SimpleNamespace(
pk=340,
rack_id=3,
face="front",
position=11,
device_type=SimpleNamespace(u_height=1),
netbox_utilities_rack_placement=SimpleNamespace(width=2, horizontal_position=2),
get_absolute_url=lambda: "/dcim/devices/340/",
),
]
device_model.objects.filter.return_value.select_related.return_value.order_by.return_value = devices
result = _topology_data_for_visible_devices({334: "/dcim/devices/334/", 340: "/dcim/devices/340/"})
self.assertTrue(result["complete"])
self.assertEqual(
[(item["device_id"], item["width"], item["horizontal_position"]) for item in result["devices"]],
[(334, 2, 1), (340, 2, 2)],
)
@patch("netbox_utilities.topology_views._topology_data_for_visible_devices")
def test_injects_stored_widths_into_the_completed_foreign_response(self, get_data):
get_data.return_value = {
"devices": [
{
"device_id": 334,
"url": "/dcim/devices/334/",
"width": 2,
"horizontal_position": 1,
"width_source": "stored",
"left_percent": 0,
"width_percent": 50,
},
{
"device_id": 340,
"url": "/dcim/devices/340/",
"width": 2,
"horizontal_position": 2,
"width_source": "stored",
"left_percent": 50,
"width_percent": 50,
},
],
"status": "ready",
"schema_version": 3,
"complete": True,
}
response = HttpResponse(
'<html><head></head><body><a class="rack-device" href="/dcim/devices/334/">A</a>'
'<a class="rack-device" href="/dcim/devices/340/">B</a></body></html>'
)
result = _inject_topology_widths(response)
html = result.content.decode()
get_data.assert_called_once_with({334: "/dcim/devices/334/", 340: "/dcim/devices/340/"})
self.assertIn('id="netbox-utilities-topology-rack-width-styles"', html)
self.assertIn('.rack-device[href="/dcim/devices/334/"]', html)
self.assertIn("left: calc(50% + 3px) !important", html)
self.assertIn("netbox_utilities/topology-rack-width.js?v=0.9.8", html)
self.assertIn("left:calc(0% + 3px)!important", html)
self.assertIn("left:calc(50% + 3px)!important", html)
self.assertIn("width:calc(50% - 6px)!important", html)
def test_preserves_existing_topology_style_while_applying_width(self):
data = {
"devices": [
{
"url": "/dcim/devices/334/",
"left_percent": 0,
"width_percent": 50,
}
]
}
html = (
'<a class="rack-device" href="/dcim/devices/334/" '
'style="bottom: 10%; height: 2%; border-color: #f00">Fritzbox</a>'
)
result = _apply_topology_widths_to_device_tags(html, data)
self.assertIn("bottom: 10%", result)
self.assertIn("height: 2%", result)
self.assertIn("left:calc(0% + 3px)!important", result)
self.assertIn("width:calc(50% - 6px)!important", result)
@patch("netbox_utilities.topology_views._inject_topology_widths")
@patch("netbox_utilities.topology_views.topology_rack_width_enabled", return_value=True)
def test_response_processor_runs_after_the_topology_view(self, _enabled, inject):
response = HttpResponse("<html></html>")
inject.return_value = response
result = apply_topology_rack_widths(self.request, response)
self.assertIs(result, response)
inject.assert_called_once_with(response)