feat: add tenant groups and required tenancy

This commit is contained in:
2026-07-28 16:57:52 +02:00
parent 54f6a3f6c9
commit 35e06ee004
17 changed files with 427 additions and 75 deletions
+40 -3
View File
@@ -4,6 +4,7 @@ from unittest.mock import patch
from django.test import RequestFactory, SimpleTestCase
from netbox_utilities.middleware import GlobalTenantFilterMiddleware
from netbox_utilities.tenant_scope import ActiveTenantScope
class TenantFilterSet:
@@ -32,7 +33,8 @@ class GlobalTenantFilterMiddlewareTest(SimpleTestCase):
mocked_resolve.return_value = resolver_match(TenantFilterSet)
request = self.factory.get("/dcim/devices/?tenant_id=7&status=active")
GlobalTenantFilterMiddleware._inject_filter_parameter(request, 42)
scope = ActiveTenantScope("tenant", 42, frozenset({42}))
GlobalTenantFilterMiddleware._inject_filter_parameter(request, scope)
self.assertEqual(request.GET.getlist("tenant_id"), ["42"])
self.assertEqual(request.GET["status"], "active")
@@ -42,7 +44,8 @@ class GlobalTenantFilterMiddlewareTest(SimpleTestCase):
mocked_resolve.return_value = resolver_match(SharedFilterSet)
request = self.factory.get("/dcim/manufacturers/?status=active")
GlobalTenantFilterMiddleware._inject_filter_parameter(request, 42)
scope = ActiveTenantScope("tenant", 42, frozenset({42}))
GlobalTenantFilterMiddleware._inject_filter_parameter(request, scope)
self.assertNotIn("tenant_id", request.GET)
self.assertEqual(request.GET["status"], "active")
@@ -52,6 +55,40 @@ class GlobalTenantFilterMiddlewareTest(SimpleTestCase):
mocked_resolve.return_value = resolver_match(None, "tenancy:tenant_list")
request = self.factory.get("/tenancy/tenants/")
GlobalTenantFilterMiddleware._inject_filter_parameter(request, 42)
scope = ActiveTenantScope("tenant", 42, frozenset({42}))
GlobalTenantFilterMiddleware._inject_filter_parameter(request, scope)
self.assertEqual(request.GET.getlist("id"), ["42"])
@patch("netbox_utilities.middleware.resolve")
def test_injects_tenant_group(self, mocked_resolve):
class TenantGroupFilterSet:
base_filters = {"tenant_id": object(), "tenant_group_id": object()}
mocked_resolve.return_value = resolver_match(TenantGroupFilterSet)
request = self.factory.get("/dcim/devices/")
scope = ActiveTenantScope("group", 8, frozenset({42, 43}))
GlobalTenantFilterMiddleware._inject_filter_parameter(request, scope)
self.assertEqual(request.GET.getlist("tenant_group_id"), ["8"])
@patch("netbox_utilities.middleware.resolve")
def test_group_falls_back_to_tenant_ids(self, mocked_resolve):
mocked_resolve.return_value = resolver_match(TenantFilterSet)
request = self.factory.get("/dcim/virtual-chassis/")
scope = ActiveTenantScope("group", 8, frozenset({42, 43}))
GlobalTenantFilterMiddleware._inject_filter_parameter(request, scope)
self.assertCountEqual(request.GET.getlist("tenant_id"), ["42", "43"])
@patch("netbox_utilities.middleware.resolve")
def test_tenant_group_list_includes_descendant_groups(self, mocked_resolve):
mocked_resolve.return_value = resolver_match(None, "tenancy:tenantgroup_list")
request = self.factory.get("/tenancy/tenant-groups/")
scope = ActiveTenantScope("group", 8, frozenset({42}), frozenset({8, 9}))
GlobalTenantFilterMiddleware._inject_filter_parameter(request, scope)
self.assertCountEqual(request.GET.getlist("id"), ["8", "9"])
+6 -1
View File
@@ -2,7 +2,7 @@ from types import SimpleNamespace
from django.test import SimpleTestCase
from netbox_utilities.tenant_scope import object_matches_tenant
from netbox_utilities.tenant_scope import object_matches_tenant, object_matches_tenant_ids
class FakeMeta:
@@ -32,3 +32,8 @@ class TenantScopeTest(SimpleTestCase):
def test_unassigned_tenant_object_is_hidden(self):
obj = SimpleNamespace(_meta=FakeMeta("tenant"), tenant_id=None)
self.assertFalse(object_matches_tenant(obj, 42))
def test_object_matches_any_tenant_in_group_scope(self):
obj = SimpleNamespace(_meta=FakeMeta("tenant"), tenant_id=43)
self.assertTrue(object_matches_tenant_ids(obj, frozenset({42, 43})))
self.assertFalse(object_matches_tenant_ids(obj, frozenset({7, 8})))
@@ -0,0 +1,50 @@
from types import SimpleNamespace
from unittest.mock import patch
from django.core.exceptions import FieldDoesNotExist, ValidationError
from django.test import SimpleTestCase
from tenancy.models import Tenant
from netbox_utilities.tenant_validation import model_supports_tenant, validate_tenant_assignment
class FakeMeta:
def __init__(self, supports_tenant=True):
self.supports_tenant = supports_tenant
def get_field(self, name):
if name != "tenant" or not self.supports_tenant:
raise FieldDoesNotExist(name)
return SimpleNamespace(concrete=True, is_relation=True, related_model=Tenant)
class TenantModel:
_meta = FakeMeta()
def __init__(self, tenant_id=None):
self.tenant_id = tenant_id
class SharedModel:
_meta = FakeMeta(supports_tenant=False)
class TenantValidationTest(SimpleTestCase):
def test_detects_tenant_capable_model(self):
self.assertTrue(model_supports_tenant(TenantModel))
self.assertFalse(model_supports_tenant(SharedModel))
@patch("netbox_utilities.tenant_validation.tenant_required", return_value=True)
def test_rejects_missing_tenant_when_required(self, _tenant_required):
with self.assertRaises(ValidationError) as context:
validate_tenant_assignment(TenantModel())
self.assertEqual(context.exception.messages, ["Für dieses Objekt muss ein Mandant angegeben werden."])
@patch("netbox_utilities.tenant_validation.tenant_required", return_value=True)
def test_accepts_assigned_tenant(self, _tenant_required):
validate_tenant_assignment(TenantModel(tenant_id=42))
@patch("netbox_utilities.tenant_validation.tenant_required", return_value=False)
def test_allows_missing_tenant_when_disabled(self, _tenant_required):
validate_tenant_assignment(TenantModel())