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.constants import *
from dcim.models import (
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
RearPort, VirtualDeviceContext,
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PortAssignment,
PowerOutlet, PowerPort, RearPort, VirtualDeviceContext,
)
from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer
from ipam.api.serializers_.vrfs import VRFSerializer
from ipam.models import VLAN
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
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 wireless.api.serializers_.nested import NestedWirelessLinkSerializer
from wireless.api.serializers_.wirelesslans import WirelessLANSerializer
@@ -294,6 +294,16 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
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):
device = DeviceSerializer(nested=True)
module = ModuleSerializer(
@@ -303,25 +313,52 @@ class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
allow_null=True
)
type = ChoiceField(choices=PortTypeChoices)
front_ports = RearPortAssignmentSerializer(
source='assignments',
many=True,
required=False,
)
class Meta:
model = RearPort
fields = [
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags',
'custom_fields', 'created', 'last_updated', '_occupied',
'front_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'tags', 'custom_fields', 'created', 'last_updated', '_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):
"""
NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device)
"""
# Create FrontPort assignments
for assignment_data in assignments:
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:
model = RearPort
fields = ['id', 'url', 'display_url', 'display', 'name', 'label', 'description']
model = PortAssignment
fields = ('id', 'front_port_position', 'rear_port', 'rear_port_position')
class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
@@ -333,7 +370,11 @@ class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
allow_null=True
)
type = ChoiceField(choices=PortTypeChoices)
rear_ports = FrontPortRearPortSerializer(many=True)
rear_ports = FrontPortAssignmentSerializer(
source='assignments',
many=True,
required=False,
)
class Meta:
model = FrontPort
@@ -344,6 +385,28 @@ class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
]
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):
device = DeviceSerializer(nested=True)

View File

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

View File

@@ -87,11 +87,19 @@ class Migration(migrations.Migration):
),
(
'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',
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(

View File

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

View File

@@ -1078,10 +1078,12 @@ class PortAssignment(PortAssignmentBase):
front_port = models.ForeignKey(
to='dcim.FrontPort',
on_delete=models.CASCADE,
related_name='assignments',
)
rear_port = models.ForeignKey(
to='dcim.RearPort',
on_delete=models.CASCADE,
related_name='assignments',
)
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
if not self._state.adding:
frontport_count = self.frontports.count()
frontport_count = self.front_ports.count()
if self.positions < frontport_count:
raise ValidationError({
"positions": _(

View File

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