Update API tests

This commit is contained in:
Jeremy Stretch
2025-11-21 12:41:06 -05:00
parent 5b8d80a371
commit bfff2d7658
6 changed files with 152 additions and 63 deletions

View File

@@ -5,15 +5,15 @@ from rest_framework import serializers
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import ( from dcim.models import (
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PortAssignment,
RearPort, VirtualDeviceContext, PowerOutlet, PowerPort, RearPort, VirtualDeviceContext,
) )
from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer
from ipam.api.serializers_.vrfs import VRFSerializer from ipam.api.serializers_.vrfs import VRFSerializer
from ipam.models import VLAN from ipam.models import VLAN
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.gfk_fields import GFKSerializerField from netbox.api.gfk_fields import GFKSerializerField
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer from netbox.api.serializers import NetBoxModelSerializer
from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
from wireless.api.serializers_.nested import NestedWirelessLinkSerializer from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
from wireless.api.serializers_.wirelesslans import WirelessLANSerializer from wireless.api.serializers_.wirelesslans import WirelessLANSerializer
@@ -294,6 +294,16 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
return super().validate(data) return super().validate(data)
class RearPortAssignmentSerializer(serializers.ModelSerializer):
front_port = serializers.PrimaryKeyRelatedField(
queryset=FrontPort.objects.all(),
)
class Meta:
model = PortAssignment
fields = ('id', 'rear_port_position', 'front_port', 'front_port_position')
class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
module = ModuleSerializer( module = ModuleSerializer(
@@ -303,25 +313,52 @@ class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
allow_null=True allow_null=True
) )
type = ChoiceField(choices=PortTypeChoices) type = ChoiceField(choices=PortTypeChoices)
front_ports = RearPortAssignmentSerializer(
source='assignments',
many=True,
required=False,
)
class Meta: class Meta:
model = RearPort model = RearPort
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', 'front_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'custom_fields', 'created', 'last_updated', '_occupied', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
def create(self, validated_data):
assignments = validated_data.pop('assignments', [])
instance = super().create(validated_data)
class FrontPortRearPortSerializer(WritableNestedSerializer): # Create FrontPort assignments
""" for assignment_data in assignments:
NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device) PortAssignment.objects.create(rear_port=instance, **assignment_data)
"""
return instance
def update(self, instance, validated_data):
assignments = validated_data.pop('assignments', None)
instance = super().update(instance, validated_data)
if assignments is not None:
# Update FrontPort assignments
PortAssignment.objects.filter(rear_port=instance).delete()
for assignment_data in assignments:
PortAssignment.objects.create(rear_port=instance, **assignment_data)
return instance
class FrontPortAssignmentSerializer(serializers.ModelSerializer):
rear_port = serializers.PrimaryKeyRelatedField(
queryset=RearPort.objects.all(),
)
class Meta: class Meta:
model = RearPort model = PortAssignment
fields = ['id', 'url', 'display_url', 'display', 'name', 'label', 'description'] fields = ('id', 'front_port_position', 'rear_port', 'rear_port_position')
class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
@@ -333,7 +370,11 @@ class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
allow_null=True allow_null=True
) )
type = ChoiceField(choices=PortTypeChoices) type = ChoiceField(choices=PortTypeChoices)
rear_ports = FrontPortRearPortSerializer(many=True) rear_ports = FrontPortAssignmentSerializer(
source='assignments',
many=True,
required=False,
)
class Meta: class Meta:
model = FrontPort model = FrontPort
@@ -344,6 +385,28 @@ class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
] ]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
def create(self, validated_data):
assignments = validated_data.pop('assignments', [])
instance = super().create(validated_data)
# Create RearPort assignments
for assignment_data in assignments:
PortAssignment.objects.create(front_port=instance, **assignment_data)
return instance
def update(self, instance, validated_data):
assignments = validated_data.pop('assignments', None)
instance = super().update(instance, validated_data)
if assignments is not None:
# Update RearPort assignments
PortAssignment.objects.filter(front_port=instance).delete()
for assignment_data in assignments:
PortAssignment.objects.create(front_port=instance, **assignment_data)
return instance
class ModuleBaySerializer(NetBoxModelSerializer): class ModuleBaySerializer(NetBoxModelSerializer):
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)

View File

@@ -385,7 +385,7 @@ class DeviceTypeType(PrimaryObjectType):
) )
class FrontPortType(ModularComponentType, CabledObjectMixin): class FrontPortType(ModularComponentType, CabledObjectMixin):
color: str color: str
rear_port: Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')] # rear_port: Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')]
@strawberry_django.type( @strawberry_django.type(
@@ -396,7 +396,7 @@ class FrontPortType(ModularComponentType, CabledObjectMixin):
) )
class FrontPortTemplateType(ModularComponentTemplateType): class FrontPortTemplateType(ModularComponentTemplateType):
color: str color: str
rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')] # rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
@strawberry_django.type( @strawberry_django.type(
@@ -768,7 +768,7 @@ class RackRoleType(OrganizationalObjectType):
class RearPortType(ModularComponentType, CabledObjectMixin): class RearPortType(ModularComponentType, CabledObjectMixin):
color: str color: str
frontports: List[Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]] front_ports: List[Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type( @strawberry_django.type(
@@ -780,7 +780,7 @@ class RearPortType(ModularComponentType, CabledObjectMixin):
class RearPortTemplateType(ModularComponentTemplateType): class RearPortTemplateType(ModularComponentTemplateType):
color: str color: str
frontport_templates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]] front_ports: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type( @strawberry_django.type(

View File

@@ -87,11 +87,19 @@ class Migration(migrations.Migration):
), ),
( (
'front_port', 'front_port',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dcim.frontporttemplate') models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='dcim.frontporttemplate',
related_name='assignments'
)
), ),
( (
'rear_port', 'rear_port',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dcim.rearporttemplate') models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='dcim.rearporttemplate',
related_name='assignments'
)
), ),
], ],
), ),
@@ -146,8 +154,22 @@ class Migration(migrations.Migration):
] ]
), ),
), ),
('front_port', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dcim.frontport')), (
('rear_port', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dcim.rearport')), 'front_port',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='dcim.frontport',
related_name='assignments'
)
),
(
'rear_port',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='dcim.rearport',
related_name='assignments'
)
),
], ],
), ),
migrations.AddConstraint( migrations.AddConstraint(

View File

@@ -527,10 +527,12 @@ class PortAssignmentTemplate(PortAssignmentBase):
front_port = models.ForeignKey( front_port = models.ForeignKey(
to='dcim.FrontPortTemplate', to='dcim.FrontPortTemplate',
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='assignments',
) )
rear_port = models.ForeignKey( rear_port = models.ForeignKey(
to='dcim.RearPortTemplate', to='dcim.RearPortTemplate',
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='assignments',
) )
def clean(self): def clean(self):

View File

@@ -1078,10 +1078,12 @@ class PortAssignment(PortAssignmentBase):
front_port = models.ForeignKey( front_port = models.ForeignKey(
to='dcim.FrontPort', to='dcim.FrontPort',
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='assignments',
) )
rear_port = models.ForeignKey( rear_port = models.ForeignKey(
to='dcim.RearPort', to='dcim.RearPort',
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='assignments',
) )
def clean(self): def clean(self):
@@ -1168,7 +1170,7 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
# Check that positions count is greater than or equal to the number of associated FrontPorts # Check that positions count is greater than or equal to the number of associated FrontPorts
if not self._state.adding: if not self._state.adding:
frontport_count = self.frontports.count() frontport_count = self.front_ports.count()
if self.positions < frontport_count: if self.positions < frontport_count:
raise ValidationError({ raise ValidationError({
"positions": _( "positions": _(

View File

@@ -981,32 +981,18 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
RearPortTemplate.objects.bulk_create(rear_port_templates) RearPortTemplate.objects.bulk_create(rear_port_templates)
front_port_templates = ( front_port_templates = (
FrontPortTemplate( FrontPortTemplate(device_type=devicetype, name='Front Port Template 1', type=PortTypeChoices.TYPE_8P8C),
device_type=devicetype, FrontPortTemplate(device_type=devicetype, name='Front Port Template 2', type=PortTypeChoices.TYPE_8P8C),
name='Front Port Template 1', FrontPortTemplate(module_type=moduletype, name='Front Port Template 5', type=PortTypeChoices.TYPE_8P8C),
type=PortTypeChoices.TYPE_8P8C, FrontPortTemplate(module_type=moduletype, name='Front Port Template 6', type=PortTypeChoices.TYPE_8P8C),
rear_port=rear_port_templates[0]
),
FrontPortTemplate(
device_type=devicetype,
name='Front Port Template 2',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_port_templates[1]
),
FrontPortTemplate(
module_type=moduletype,
name='Front Port Template 5',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_port_templates[4]
),
FrontPortTemplate(
module_type=moduletype,
name='Front Port Template 6',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_port_templates[5]
),
) )
FrontPortTemplate.objects.bulk_create(front_port_templates) FrontPortTemplate.objects.bulk_create(front_port_templates)
PortAssignmentTemplate.objects.bulk_create([
PortAssignmentTemplate(front_port=front_port_templates[0], rear_port=rear_port_templates[0]),
PortAssignmentTemplate(front_port=front_port_templates[1], rear_port=rear_port_templates[1]),
PortAssignmentTemplate(front_port=front_port_templates[2], rear_port=rear_port_templates[4]),
PortAssignmentTemplate(front_port=front_port_templates[3], rear_port=rear_port_templates[5]),
])
cls.create_data = [ cls.create_data = [
{ {
@@ -2017,49 +2003,63 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase):
RearPort.objects.bulk_create(rear_ports) RearPort.objects.bulk_create(rear_ports)
front_ports = ( front_ports = (
FrontPort(device=device, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]), FrontPort(device=device, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C),
FrontPort(device=device, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]), FrontPort(device=device, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C),
FrontPort(device=device, name='Front Port 3', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[2]), FrontPort(device=device, name='Front Port 3', type=PortTypeChoices.TYPE_8P8C),
) )
FrontPort.objects.bulk_create(front_ports) FrontPort.objects.bulk_create(front_ports)
PortAssignment.objects.bulk_create([
PortAssignment(front_port=front_ports[0], rear_port=rear_ports[0]),
PortAssignment(front_port=front_ports[1], rear_port=rear_ports[1]),
PortAssignment(front_port=front_ports[2], rear_port=rear_ports[2]),
])
cls.create_data = [ cls.create_data = [
{ {
'device': device.pk, 'device': device.pk,
'name': 'Front Port 4', 'name': 'Front Port 4',
'type': PortTypeChoices.TYPE_8P8C, 'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_ports[3].pk, 'rear_ports': [
'rear_port_position': 1, {
'front_port_position': 1,
'rear_port': rear_ports[3].pk,
'rear_port_position': 1,
},
],
}, },
{ {
'device': device.pk, 'device': device.pk,
'name': 'Front Port 5', 'name': 'Front Port 5',
'type': PortTypeChoices.TYPE_8P8C, 'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_ports[4].pk, 'rear_ports': [
'rear_port_position': 1, {
'front_port_position': 1,
'rear_port': rear_ports[4].pk,
'rear_port_position': 1,
},
],
}, },
{ {
'device': device.pk, 'device': device.pk,
'name': 'Front Port 6', 'name': 'Front Port 6',
'type': PortTypeChoices.TYPE_8P8C, 'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_ports[5].pk, 'rear_ports': [
'rear_port_position': 1, {
'front_port_position': 1,
'rear_port': rear_ports[5].pk,
'rear_port_position': 1,
},
],
}, },
] ]
@tag('regression') # Issue #18991 @tag('regression') # Issue #18991
def test_front_port_paths(self): def test_front_port_paths(self):
device = Device.objects.first() device = Device.objects.first()
rear_port = RearPort.objects.create(
device=device, name='Rear Port 10', type=PortTypeChoices.TYPE_8P8C
)
interface1 = Interface.objects.create(device=device, name='Interface 1') interface1 = Interface.objects.create(device=device, name='Interface 1')
front_port = FrontPort.objects.create( rear_port = RearPort.objects.create(device=device, name='Rear Port 10', type=PortTypeChoices.TYPE_8P8C)
device=device, front_port = FrontPort.objects.create(device=device, name='Front Port 10', type=PortTypeChoices.TYPE_8P8C)
name='Rear Port 10', PortAssignment.objects.create(front_port=front_port, rear_port=rear_port)
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_port,
)
Cable.objects.create(a_terminations=[interface1], b_terminations=[front_port]) Cable.objects.create(a_terminations=[interface1], b_terminations=[front_port])
self.add_permissions(f'dcim.view_{self.model._meta.model_name}') self.add_permissions(f'dcim.view_{self.model._meta.model_name}')