ipfabric_netbox 4.3.2b9__py3-none-any.whl → 4.3.2b10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ipfabric_netbox might be problematic. Click here for more details.
- ipfabric_netbox/__init__.py +2 -2
- ipfabric_netbox/api/serializers.py +112 -7
- ipfabric_netbox/api/urls.py +6 -0
- ipfabric_netbox/api/views.py +23 -0
- ipfabric_netbox/choices.py +72 -40
- ipfabric_netbox/data/endpoint.json +47 -0
- ipfabric_netbox/data/filters.json +51 -0
- ipfabric_netbox/data/transform_map.json +188 -174
- ipfabric_netbox/exceptions.py +7 -5
- ipfabric_netbox/filtersets.py +310 -41
- ipfabric_netbox/forms.py +324 -79
- ipfabric_netbox/graphql/__init__.py +6 -0
- ipfabric_netbox/graphql/enums.py +5 -5
- ipfabric_netbox/graphql/filters.py +56 -4
- ipfabric_netbox/graphql/schema.py +28 -0
- ipfabric_netbox/graphql/types.py +61 -1
- ipfabric_netbox/jobs.py +5 -1
- ipfabric_netbox/migrations/0022_prepare_for_filters.py +182 -0
- ipfabric_netbox/migrations/0023_populate_filters_data.py +279 -0
- ipfabric_netbox/migrations/0024_finish_filters.py +29 -0
- ipfabric_netbox/models.py +384 -12
- ipfabric_netbox/navigation.py +98 -24
- ipfabric_netbox/tables.py +194 -9
- ipfabric_netbox/templates/ipfabric_netbox/htmx_list.html +5 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/combined_expressions.html +59 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/combined_expressions_content.html +39 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/endpoint_filters_with_selector.html +54 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricendpoint.html +39 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilter.html +51 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilterexpression.html +39 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilterexpression_edit.html +150 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +1 -1
- ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +16 -2
- ipfabric_netbox/templatetags/ipfabric_netbox_helpers.py +65 -0
- ipfabric_netbox/tests/api/test_api.py +333 -13
- ipfabric_netbox/tests/test_filtersets.py +2592 -0
- ipfabric_netbox/tests/test_forms.py +1256 -74
- ipfabric_netbox/tests/test_models.py +242 -34
- ipfabric_netbox/tests/test_views.py +2030 -25
- ipfabric_netbox/urls.py +35 -0
- ipfabric_netbox/utilities/endpoint.py +30 -0
- ipfabric_netbox/utilities/filters.py +88 -0
- ipfabric_netbox/utilities/ipfutils.py +254 -316
- ipfabric_netbox/utilities/logging.py +7 -7
- ipfabric_netbox/utilities/transform_map.py +126 -0
- ipfabric_netbox/views.py +719 -5
- {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/METADATA +3 -2
- {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/RECORD +49 -33
- {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/WHEEL +1 -1
|
@@ -1,16 +1,25 @@
|
|
|
1
|
+
import json
|
|
1
2
|
from datetime import timedelta
|
|
2
3
|
from unittest.mock import patch
|
|
3
4
|
|
|
4
5
|
from dcim.models import Device
|
|
6
|
+
from dcim.models import Interface
|
|
5
7
|
from dcim.models import Site
|
|
8
|
+
from django import forms
|
|
6
9
|
from django.contrib.contenttypes.models import ContentType
|
|
7
10
|
from django.test import TestCase
|
|
11
|
+
from ipam.models import IPAddress
|
|
12
|
+
from ipam.models import VRF
|
|
8
13
|
from utilities.datetime import local_now
|
|
14
|
+
from utilities.forms.rendering import FieldSet
|
|
9
15
|
|
|
16
|
+
from ipfabric_netbox.choices import IPFabricFilterTypeChoices
|
|
10
17
|
from ipfabric_netbox.choices import IPFabricSnapshotStatusModelChoices
|
|
11
18
|
from ipfabric_netbox.choices import IPFabricSourceStatusChoices
|
|
12
19
|
from ipfabric_netbox.choices import IPFabricSourceTypeChoices
|
|
13
20
|
from ipfabric_netbox.choices import IPFabricSyncStatusChoices
|
|
21
|
+
from ipfabric_netbox.forms import IPFabricFilterExpressionForm
|
|
22
|
+
from ipfabric_netbox.forms import IPFabricFilterForm
|
|
14
23
|
from ipfabric_netbox.forms import IPFabricIngestionFilterForm
|
|
15
24
|
from ipfabric_netbox.forms import IPFabricIngestionMergeForm
|
|
16
25
|
from ipfabric_netbox.forms import IPFabricRelationshipFieldForm
|
|
@@ -22,6 +31,9 @@ from ipfabric_netbox.forms import IPFabricTransformFieldForm
|
|
|
22
31
|
from ipfabric_netbox.forms import IPFabricTransformMapCloneForm
|
|
23
32
|
from ipfabric_netbox.forms import IPFabricTransformMapForm
|
|
24
33
|
from ipfabric_netbox.forms import IPFabricTransformMapGroupForm
|
|
34
|
+
from ipfabric_netbox.models import IPFabricEndpoint
|
|
35
|
+
from ipfabric_netbox.models import IPFabricFilter
|
|
36
|
+
from ipfabric_netbox.models import IPFabricFilterExpression
|
|
25
37
|
from ipfabric_netbox.models import IPFabricRelationshipField
|
|
26
38
|
from ipfabric_netbox.models import IPFabricSnapshot
|
|
27
39
|
from ipfabric_netbox.models import IPFabricSource
|
|
@@ -148,7 +160,6 @@ class IPFabricSourceFormTestCase(TestCase):
|
|
|
148
160
|
|
|
149
161
|
def test_remote_source_creates_last_snapshot(self):
|
|
150
162
|
"""Check that $last snapshot is created for remote sources"""
|
|
151
|
-
from ipfabric_netbox.models import IPFabricSnapshot
|
|
152
163
|
|
|
153
164
|
self.assertEqual(IPFabricSnapshot.objects.count(), 0)
|
|
154
165
|
|
|
@@ -350,10 +361,14 @@ class IPFabricRelationshipFieldFormTestCase(TestCase):
|
|
|
350
361
|
cls.device_content_type = ContentType.objects.get_for_model(Device)
|
|
351
362
|
cls.site_content_type = ContentType.objects.get_for_model(Site)
|
|
352
363
|
|
|
364
|
+
cls.device_endpoint = IPFabricEndpoint.objects.get(
|
|
365
|
+
endpoint="/inventory/devices"
|
|
366
|
+
)
|
|
367
|
+
|
|
353
368
|
cls.transform_map = IPFabricTransformMap.objects.create(
|
|
354
369
|
name="Test Transform Map",
|
|
355
370
|
group=cls.transform_map_group,
|
|
356
|
-
|
|
371
|
+
source_endpoint=cls.device_endpoint,
|
|
357
372
|
target_model=cls.device_content_type,
|
|
358
373
|
)
|
|
359
374
|
|
|
@@ -463,10 +478,14 @@ class IPFabricTransformFieldFormTestCase(TestCase):
|
|
|
463
478
|
|
|
464
479
|
cls.device_content_type = ContentType.objects.get_for_model(Device)
|
|
465
480
|
|
|
481
|
+
cls.device_endpoint = IPFabricEndpoint.objects.get(
|
|
482
|
+
endpoint="/inventory/devices"
|
|
483
|
+
)
|
|
484
|
+
|
|
466
485
|
cls.transform_map = IPFabricTransformMap.objects.create(
|
|
467
486
|
name="Test Transform Map",
|
|
468
487
|
group=cls.transform_map_group,
|
|
469
|
-
|
|
488
|
+
source_endpoint=cls.device_endpoint,
|
|
470
489
|
target_model=cls.device_content_type,
|
|
471
490
|
)
|
|
472
491
|
|
|
@@ -587,12 +606,15 @@ class IPFabricTransformMapFormTestCase(TestCase):
|
|
|
587
606
|
name="Test Group", description="Test group description"
|
|
588
607
|
)
|
|
589
608
|
cls.device_content_type = ContentType.objects.get_for_model(Device)
|
|
609
|
+
cls.device_endpoint = IPFabricEndpoint.objects.get(
|
|
610
|
+
endpoint="/inventory/devices"
|
|
611
|
+
)
|
|
590
612
|
|
|
591
613
|
def test_fields_are_required(self):
|
|
592
614
|
form = IPFabricTransformMapForm(data={})
|
|
593
615
|
self.assertFalse(form.is_valid(), form.errors)
|
|
594
616
|
self.assertIn("name", form.errors)
|
|
595
|
-
self.assertIn("
|
|
617
|
+
self.assertIn("source_endpoint", form.errors)
|
|
596
618
|
self.assertIn("target_model", form.errors)
|
|
597
619
|
|
|
598
620
|
def test_group_is_optional(self):
|
|
@@ -603,7 +625,7 @@ class IPFabricTransformMapFormTestCase(TestCase):
|
|
|
603
625
|
form = IPFabricTransformMapForm(
|
|
604
626
|
data={
|
|
605
627
|
"name": "Test Transform Map",
|
|
606
|
-
"
|
|
628
|
+
"source_endpoint": self.device_endpoint.pk,
|
|
607
629
|
"target_model": self.device_content_type.pk,
|
|
608
630
|
}
|
|
609
631
|
)
|
|
@@ -614,7 +636,7 @@ class IPFabricTransformMapFormTestCase(TestCase):
|
|
|
614
636
|
data={
|
|
615
637
|
"name": "Test Transform Map",
|
|
616
638
|
"group": self.transform_map_group.pk,
|
|
617
|
-
"
|
|
639
|
+
"source_endpoint": self.device_endpoint.pk,
|
|
618
640
|
"target_model": self.device_content_type.pk,
|
|
619
641
|
}
|
|
620
642
|
)
|
|
@@ -623,6 +645,301 @@ class IPFabricTransformMapFormTestCase(TestCase):
|
|
|
623
645
|
self.assertEqual(instance.name, "Test Transform Map")
|
|
624
646
|
self.assertEqual(instance.group, self.transform_map_group)
|
|
625
647
|
|
|
648
|
+
def test_existing_instance_excludes_self_from_parents_choices(self):
|
|
649
|
+
"""Test that when editing an existing transform map, it excludes itself from parents choices"""
|
|
650
|
+
# Create a transform map instance
|
|
651
|
+
transform_map = IPFabricTransformMap.objects.create(
|
|
652
|
+
name="Test Transform Map",
|
|
653
|
+
group=self.transform_map_group,
|
|
654
|
+
source_endpoint=self.device_endpoint,
|
|
655
|
+
target_model=self.device_content_type,
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
# Create another transform map that could be a parent
|
|
659
|
+
other_transform_map = IPFabricTransformMap.objects.create(
|
|
660
|
+
name="Other Transform Map",
|
|
661
|
+
group=self.transform_map_group,
|
|
662
|
+
source_endpoint=self.device_endpoint,
|
|
663
|
+
target_model=ContentType.objects.get_for_model(Site),
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
# Initialize form with the existing instance
|
|
667
|
+
form = IPFabricTransformMapForm(instance=transform_map)
|
|
668
|
+
|
|
669
|
+
# Verify that the instance itself is excluded from parents choices
|
|
670
|
+
parents_queryset = form.fields["parents"].queryset
|
|
671
|
+
self.assertNotIn(transform_map, parents_queryset)
|
|
672
|
+
|
|
673
|
+
# Verify that other transform maps are still available as parent options
|
|
674
|
+
self.assertIn(other_transform_map, parents_queryset)
|
|
675
|
+
|
|
676
|
+
# Verify help text is set
|
|
677
|
+
self.assertIn(
|
|
678
|
+
"must be processed before this one", form.fields["parents"].help_text
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
def test_circular_dependency_direct(self):
|
|
682
|
+
"""Test that direct circular dependencies are detected (A → B, B → A)"""
|
|
683
|
+
# Create two transform maps
|
|
684
|
+
site_ct = ContentType.objects.get_for_model(Site)
|
|
685
|
+
|
|
686
|
+
tm_a = IPFabricTransformMap.objects.create(
|
|
687
|
+
name="Transform Map A",
|
|
688
|
+
group=self.transform_map_group,
|
|
689
|
+
source_endpoint=self.device_endpoint,
|
|
690
|
+
target_model=self.device_content_type,
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
tm_b = IPFabricTransformMap.objects.create(
|
|
694
|
+
name="Transform Map B",
|
|
695
|
+
group=self.transform_map_group,
|
|
696
|
+
source_endpoint=self.device_endpoint,
|
|
697
|
+
target_model=site_ct,
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
# Set B as parent of A
|
|
701
|
+
tm_a.parents.add(tm_b)
|
|
702
|
+
|
|
703
|
+
# Try to set A as parent of B (should fail)
|
|
704
|
+
form = IPFabricTransformMapForm(
|
|
705
|
+
instance=tm_b,
|
|
706
|
+
data={
|
|
707
|
+
"name": tm_b.name,
|
|
708
|
+
"group": self.transform_map_group.pk,
|
|
709
|
+
"source_endpoint": self.device_endpoint.pk,
|
|
710
|
+
"target_model": site_ct.pk,
|
|
711
|
+
"parents": [tm_a.pk],
|
|
712
|
+
},
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
self.assertFalse(form.is_valid())
|
|
716
|
+
self.assertIn("parents", form.errors)
|
|
717
|
+
self.assertIn("circular dependency", str(form.errors["parents"]).lower())
|
|
718
|
+
|
|
719
|
+
def test_circular_dependency_indirect(self):
|
|
720
|
+
"""Test that indirect circular dependencies are detected (A → B → C → A)"""
|
|
721
|
+
# Create three transform maps with different content types
|
|
722
|
+
|
|
723
|
+
site_ct = ContentType.objects.get_for_model(Site)
|
|
724
|
+
vrf_ct = ContentType.objects.get_for_model(VRF)
|
|
725
|
+
interface_ct = ContentType.objects.get_for_model(Interface)
|
|
726
|
+
|
|
727
|
+
vrf_endpoint = IPFabricEndpoint.objects.get(
|
|
728
|
+
endpoint="/technology/routing/vrf/detail"
|
|
729
|
+
)
|
|
730
|
+
interface_endpoint = IPFabricEndpoint.objects.get(
|
|
731
|
+
endpoint="/inventory/interfaces"
|
|
732
|
+
)
|
|
733
|
+
site_endpoint = IPFabricEndpoint.objects.get(
|
|
734
|
+
endpoint="/inventory/sites/overview"
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
tm_a = IPFabricTransformMap.objects.create(
|
|
738
|
+
name="Transform Map A",
|
|
739
|
+
group=self.transform_map_group,
|
|
740
|
+
source_endpoint=site_endpoint,
|
|
741
|
+
target_model=site_ct,
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
tm_b = IPFabricTransformMap.objects.create(
|
|
745
|
+
name="Transform Map B",
|
|
746
|
+
group=self.transform_map_group,
|
|
747
|
+
source_endpoint=vrf_endpoint,
|
|
748
|
+
target_model=vrf_ct,
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
tm_c = IPFabricTransformMap.objects.create(
|
|
752
|
+
name="Transform Map C",
|
|
753
|
+
group=self.transform_map_group,
|
|
754
|
+
source_endpoint=interface_endpoint,
|
|
755
|
+
target_model=interface_ct,
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
# Create chain: A → B → C
|
|
759
|
+
tm_b.parents.add(tm_a)
|
|
760
|
+
tm_c.parents.add(tm_b)
|
|
761
|
+
|
|
762
|
+
# Try to add C as parent of A (completing the cycle: A → B → C → A)
|
|
763
|
+
form = IPFabricTransformMapForm(
|
|
764
|
+
instance=tm_a,
|
|
765
|
+
data={
|
|
766
|
+
"name": tm_a.name,
|
|
767
|
+
"group": self.transform_map_group.pk,
|
|
768
|
+
"source_endpoint": site_endpoint.pk,
|
|
769
|
+
"target_model": site_ct.pk,
|
|
770
|
+
"parents": [tm_c.pk],
|
|
771
|
+
},
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
self.assertFalse(form.is_valid())
|
|
775
|
+
self.assertIn("parents", form.errors)
|
|
776
|
+
self.assertIn("circular dependency", str(form.errors["parents"]).lower())
|
|
777
|
+
|
|
778
|
+
def test_valid_parent_assignment_no_cycle(self):
|
|
779
|
+
"""Test that valid parent assignments without cycles are allowed"""
|
|
780
|
+
site_ct = ContentType.objects.get_for_model(Site)
|
|
781
|
+
vrf_ct = ContentType.objects.get_for_model(VRF)
|
|
782
|
+
|
|
783
|
+
vrf_endpoint = IPFabricEndpoint.objects.get(
|
|
784
|
+
endpoint="/technology/routing/vrf/detail"
|
|
785
|
+
)
|
|
786
|
+
site_endpoint = IPFabricEndpoint.objects.get(
|
|
787
|
+
endpoint="/inventory/sites/overview"
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
tm_site = IPFabricTransformMap.objects.create(
|
|
791
|
+
name="Site Transform Map",
|
|
792
|
+
group=self.transform_map_group,
|
|
793
|
+
source_endpoint=site_endpoint,
|
|
794
|
+
target_model=site_ct,
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
tm_vrf = IPFabricTransformMap.objects.create(
|
|
798
|
+
name="VRF Transform Map",
|
|
799
|
+
group=self.transform_map_group,
|
|
800
|
+
source_endpoint=vrf_endpoint,
|
|
801
|
+
target_model=vrf_ct,
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
# Set Site as parent of VRF (valid - no cycle)
|
|
805
|
+
form = IPFabricTransformMapForm(
|
|
806
|
+
instance=tm_vrf,
|
|
807
|
+
data={
|
|
808
|
+
"name": tm_vrf.name,
|
|
809
|
+
"group": self.transform_map_group.pk,
|
|
810
|
+
"source_endpoint": vrf_endpoint.pk,
|
|
811
|
+
"target_model": vrf_ct.pk,
|
|
812
|
+
"parents": [tm_site.pk],
|
|
813
|
+
},
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
817
|
+
form.save()
|
|
818
|
+
self.assertIn(tm_site, tm_vrf.parents.all())
|
|
819
|
+
|
|
820
|
+
def test_multiple_parents_no_cycle(self):
|
|
821
|
+
"""Test that multiple parents can be assigned without creating cycles"""
|
|
822
|
+
|
|
823
|
+
site_ct = ContentType.objects.get_for_model(Site)
|
|
824
|
+
vrf_ct = ContentType.objects.get_for_model(VRF)
|
|
825
|
+
interface_ct = ContentType.objects.get_for_model(Interface)
|
|
826
|
+
ipaddress_ct = ContentType.objects.get_for_model(IPAddress)
|
|
827
|
+
|
|
828
|
+
vrf_endpoint = IPFabricEndpoint.objects.get(
|
|
829
|
+
endpoint="/technology/routing/vrf/detail"
|
|
830
|
+
)
|
|
831
|
+
interface_endpoint = IPFabricEndpoint.objects.get(
|
|
832
|
+
endpoint="/inventory/interfaces"
|
|
833
|
+
)
|
|
834
|
+
site_endpoint = IPFabricEndpoint.objects.get(
|
|
835
|
+
endpoint="/inventory/sites/overview"
|
|
836
|
+
)
|
|
837
|
+
ipaddress_endpoint = IPFabricEndpoint.objects.get(
|
|
838
|
+
endpoint="/technology/addressing/managed-ip/ipv4"
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
tm_site = IPFabricTransformMap.objects.create(
|
|
842
|
+
name="Site Transform Map",
|
|
843
|
+
group=self.transform_map_group,
|
|
844
|
+
source_endpoint=site_endpoint,
|
|
845
|
+
target_model=site_ct,
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
tm_vrf = IPFabricTransformMap.objects.create(
|
|
849
|
+
name="VRF Transform Map",
|
|
850
|
+
group=self.transform_map_group,
|
|
851
|
+
source_endpoint=vrf_endpoint,
|
|
852
|
+
target_model=vrf_ct,
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
tm_interface = IPFabricTransformMap.objects.create(
|
|
856
|
+
name="Interface Transform Map",
|
|
857
|
+
group=self.transform_map_group,
|
|
858
|
+
source_endpoint=interface_endpoint,
|
|
859
|
+
target_model=interface_ct,
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
tm_ipaddress = IPFabricTransformMap.objects.create(
|
|
863
|
+
name="IP Address Transform Map",
|
|
864
|
+
group=self.transform_map_group,
|
|
865
|
+
source_endpoint=ipaddress_endpoint,
|
|
866
|
+
target_model=ipaddress_ct,
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
# Set Site as parent of both VRF and Interface
|
|
870
|
+
tm_vrf.parents.add(tm_site)
|
|
871
|
+
tm_interface.parents.add(tm_site)
|
|
872
|
+
|
|
873
|
+
# Set both VRF and Interface as parents of IP Address (valid - no cycle)
|
|
874
|
+
form = IPFabricTransformMapForm(
|
|
875
|
+
instance=tm_ipaddress,
|
|
876
|
+
data={
|
|
877
|
+
"name": tm_ipaddress.name,
|
|
878
|
+
"group": self.transform_map_group.pk,
|
|
879
|
+
"source_endpoint": ipaddress_endpoint.pk,
|
|
880
|
+
"target_model": ipaddress_ct.pk,
|
|
881
|
+
"parents": [tm_vrf.pk, tm_interface.pk],
|
|
882
|
+
},
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
886
|
+
form.save()
|
|
887
|
+
self.assertEqual(tm_ipaddress.parents.count(), 2)
|
|
888
|
+
self.assertIn(tm_vrf, tm_ipaddress.parents.all())
|
|
889
|
+
self.assertIn(tm_interface, tm_ipaddress.parents.all())
|
|
890
|
+
|
|
891
|
+
def test_self_as_parent_prevented_by_form_init(self):
|
|
892
|
+
"""Test that a transform map cannot be set as its own parent (prevented by form __init__)"""
|
|
893
|
+
tm = IPFabricTransformMap.objects.create(
|
|
894
|
+
name="Test Transform Map",
|
|
895
|
+
group=self.transform_map_group,
|
|
896
|
+
source_endpoint=self.device_endpoint,
|
|
897
|
+
target_model=self.device_content_type,
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
# The form should exclude self from parents queryset
|
|
901
|
+
form = IPFabricTransformMapForm(instance=tm)
|
|
902
|
+
parents_queryset = form.fields["parents"].queryset
|
|
903
|
+
self.assertNotIn(tm, parents_queryset)
|
|
904
|
+
|
|
905
|
+
def test_no_validation_for_new_instances(self):
|
|
906
|
+
"""Test that circular dependency validation is skipped for new instances"""
|
|
907
|
+
# New instance (no pk) should not trigger circular dependency validation
|
|
908
|
+
form = IPFabricTransformMapForm(
|
|
909
|
+
data={
|
|
910
|
+
"name": "New Transform Map",
|
|
911
|
+
"group": self.transform_map_group.pk,
|
|
912
|
+
"source_endpoint": self.device_endpoint.pk,
|
|
913
|
+
"target_model": self.device_content_type.pk,
|
|
914
|
+
# parents field is optional and empty for new instances
|
|
915
|
+
}
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
919
|
+
|
|
920
|
+
def test_no_validation_when_parents_empty(self):
|
|
921
|
+
"""Test that circular dependency validation is skipped when no parents are selected"""
|
|
922
|
+
tm = IPFabricTransformMap.objects.create(
|
|
923
|
+
name="Test Transform Map",
|
|
924
|
+
group=self.transform_map_group,
|
|
925
|
+
source_endpoint=self.device_endpoint,
|
|
926
|
+
target_model=self.device_content_type,
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
# Edit without setting any parents
|
|
930
|
+
form = IPFabricTransformMapForm(
|
|
931
|
+
instance=tm,
|
|
932
|
+
data={
|
|
933
|
+
"name": "Updated Transform Map",
|
|
934
|
+
"group": self.transform_map_group.pk,
|
|
935
|
+
"source_endpoint": self.device_endpoint.pk,
|
|
936
|
+
"target_model": self.device_content_type.pk,
|
|
937
|
+
"parents": [], # Empty parents
|
|
938
|
+
},
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
942
|
+
|
|
626
943
|
|
|
627
944
|
class IPFabricTransformMapCloneFormTestCase(TestCase):
|
|
628
945
|
@classmethod
|
|
@@ -771,9 +1088,7 @@ class IPFabricSyncFormTestCase(TestCase):
|
|
|
771
1088
|
source=cls.source,
|
|
772
1089
|
snapshot_id="test-snapshot-id",
|
|
773
1090
|
status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
|
|
774
|
-
data={
|
|
775
|
-
"sites": ["site1", "site2", "site3"]
|
|
776
|
-
}, # Store as list instead of comma-separated string
|
|
1091
|
+
data={"sites": ["site1", "site2", "site3"]},
|
|
777
1092
|
)
|
|
778
1093
|
|
|
779
1094
|
cls.transform_map_group = IPFabricTransformMapGroup.objects.create(
|
|
@@ -931,6 +1246,31 @@ class IPFabricSyncFormTestCase(TestCase):
|
|
|
931
1246
|
# But the provided initial value should be present
|
|
932
1247
|
self.assertEqual(form.initial.get("name"), "Override Name")
|
|
933
1248
|
|
|
1249
|
+
def test_htmx_boolean_field_list_values_handled(self):
|
|
1250
|
+
"""Test sanitizing HTMX BooleanField list values like ['', 'on']"""
|
|
1251
|
+
# Simulate HTMX request where BooleanField values become lists
|
|
1252
|
+
# This happens when `source` field value is changed and form is re-drawn via HTMX
|
|
1253
|
+
form = IPFabricSyncForm(
|
|
1254
|
+
initial={
|
|
1255
|
+
"auto_merge": ["", "on"], # HTMX sends BooleanField as list
|
|
1256
|
+
"update_custom_fields": ["", "on"], # Another BooleanField as list
|
|
1257
|
+
"name": "Test Sync", # Normal field (not affected)
|
|
1258
|
+
},
|
|
1259
|
+
data={
|
|
1260
|
+
"name": "Test Sync HTMX",
|
|
1261
|
+
"source": self.source.pk,
|
|
1262
|
+
"snapshot_data": self.snapshot.pk,
|
|
1263
|
+
},
|
|
1264
|
+
)
|
|
1265
|
+
|
|
1266
|
+
# The last value from ['', 'on'] should be 'on' which evaluates to True for BooleanFields
|
|
1267
|
+
self.assertEqual(form.initial["auto_merge"], "on")
|
|
1268
|
+
self.assertEqual(form.initial["update_custom_fields"], "on")
|
|
1269
|
+
self.assertEqual(form.initial["name"], "Test Sync") # Normal field unchanged
|
|
1270
|
+
|
|
1271
|
+
# Verify the form is still valid and processes correctly
|
|
1272
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
1273
|
+
|
|
934
1274
|
def test_sites_initial_value_set_from_form_initial(self):
|
|
935
1275
|
"""Test that sites field initial is set from self.initial["sites"]"""
|
|
936
1276
|
# Create an existing sync instance with sites in parameters
|
|
@@ -958,33 +1298,6 @@ class IPFabricSyncFormTestCase(TestCase):
|
|
|
958
1298
|
# Also verify that self.initial contains the expected sites
|
|
959
1299
|
self.assertEqual(form.initial["sites"], ["override_site1", "override_site2"])
|
|
960
1300
|
|
|
961
|
-
def test_htmx_boolean_field_list_values_handled(self):
|
|
962
|
-
"""Test sanitizing HTMX BooleanField list values like ['', 'on']"""
|
|
963
|
-
# Simulate HTMX request where BooleanField values become lists
|
|
964
|
-
# This happens when `source` field value is changed and form is re-drawn via HTMX
|
|
965
|
-
form = IPFabricSyncForm(
|
|
966
|
-
initial={
|
|
967
|
-
"auto_merge": ["", "on"], # HTMX sends BooleanField as list
|
|
968
|
-
"update_custom_fields": ["", "on"], # Another BooleanField as list
|
|
969
|
-
"ipf_site": ["", "on"], # ipf_ prefixed field as list
|
|
970
|
-
"name": "Test Sync", # Normal field (not affected)
|
|
971
|
-
},
|
|
972
|
-
data={
|
|
973
|
-
"name": "Test Sync HTMX",
|
|
974
|
-
"source": self.source.pk,
|
|
975
|
-
"snapshot_data": self.snapshot.pk,
|
|
976
|
-
},
|
|
977
|
-
)
|
|
978
|
-
|
|
979
|
-
# The last value from ['', 'on'] should be 'on' which evaluates to True for BooleanFields
|
|
980
|
-
self.assertEqual(form.initial["auto_merge"], "on")
|
|
981
|
-
self.assertEqual(form.initial["update_custom_fields"], "on")
|
|
982
|
-
self.assertEqual(form.initial["ipf_site"], "on")
|
|
983
|
-
self.assertEqual(form.initial["name"], "Test Sync") # Normal field unchanged
|
|
984
|
-
|
|
985
|
-
# Verify the form is still valid and processes correctly
|
|
986
|
-
self.assertTrue(form.is_valid(), form.errors)
|
|
987
|
-
|
|
988
1301
|
def test_htmx_boolean_field_single_values_unchanged(self):
|
|
989
1302
|
"""Test that normal single values are not affected by the HTMX list handling"""
|
|
990
1303
|
# Test with normal single values (not lists)
|
|
@@ -992,7 +1305,6 @@ class IPFabricSyncFormTestCase(TestCase):
|
|
|
992
1305
|
initial={
|
|
993
1306
|
"auto_merge": True, # Normal boolean value
|
|
994
1307
|
"update_custom_fields": False, # Normal boolean value
|
|
995
|
-
"ipf_site": "on", # Normal string value
|
|
996
1308
|
"name": "Test Sync", # Normal string value
|
|
997
1309
|
},
|
|
998
1310
|
data={
|
|
@@ -1005,7 +1317,6 @@ class IPFabricSyncFormTestCase(TestCase):
|
|
|
1005
1317
|
# Verify that single values are not processed by value sanitization
|
|
1006
1318
|
self.assertEqual(form.initial["auto_merge"], True)
|
|
1007
1319
|
self.assertEqual(form.initial["update_custom_fields"], False)
|
|
1008
|
-
self.assertEqual(form.initial["ipf_site"], "on")
|
|
1009
1320
|
self.assertEqual(form.initial["name"], "Test Sync")
|
|
1010
1321
|
|
|
1011
1322
|
# Verify the form is still valid
|
|
@@ -1161,9 +1472,9 @@ class IPFabricSyncFormTestCase(TestCase):
|
|
|
1161
1472
|
form = IPFabricSyncForm(
|
|
1162
1473
|
data={
|
|
1163
1474
|
"name": "Test Sync Save",
|
|
1475
|
+
"sites": ["site1", "site2"],
|
|
1164
1476
|
"source": self.source.pk,
|
|
1165
1477
|
"snapshot_data": self.snapshot.pk,
|
|
1166
|
-
"sites": ["site1", "site2"],
|
|
1167
1478
|
"groups": [self.transform_map_group.pk],
|
|
1168
1479
|
"auto_merge": True,
|
|
1169
1480
|
"update_custom_fields": True,
|
|
@@ -1189,33 +1500,33 @@ class IPFabricSyncFormTestCase(TestCase):
|
|
|
1189
1500
|
expected_parameters = {
|
|
1190
1501
|
"sites": ["site1", "site2"],
|
|
1191
1502
|
"groups": [self.transform_map_group.pk],
|
|
1192
|
-
"site": False,
|
|
1193
|
-
"manufacturer": False,
|
|
1194
|
-
"devicetype": False,
|
|
1195
|
-
"devicerole": False,
|
|
1196
|
-
"platform": False,
|
|
1197
|
-
"device": False,
|
|
1198
|
-
"virtualchassis": False,
|
|
1199
|
-
"interface": False,
|
|
1200
|
-
"macaddress": False,
|
|
1201
|
-
"inventoryitem": False,
|
|
1202
|
-
"vlan": False,
|
|
1203
|
-
"vrf": False,
|
|
1204
|
-
"prefix": False,
|
|
1205
|
-
"ipaddress": False,
|
|
1503
|
+
"dcim.site": False,
|
|
1504
|
+
"dcim.manufacturer": False,
|
|
1505
|
+
"dcim.devicetype": False,
|
|
1506
|
+
"dcim.devicerole": False,
|
|
1507
|
+
"dcim.platform": False,
|
|
1508
|
+
"dcim.device": False,
|
|
1509
|
+
"dcim.virtualchassis": False,
|
|
1510
|
+
"dcim.interface": False,
|
|
1511
|
+
"dcim.macaddress": False,
|
|
1512
|
+
"dcim.inventoryitem": False,
|
|
1513
|
+
"ipam.vlan": False,
|
|
1514
|
+
"ipam.vrf": False,
|
|
1515
|
+
"ipam.prefix": False,
|
|
1516
|
+
"ipam.ipaddress": False,
|
|
1206
1517
|
}
|
|
1207
1518
|
self.assertEqual(sync_instance.parameters, expected_parameters)
|
|
1208
1519
|
|
|
1209
|
-
def
|
|
1210
|
-
"""Test save method properly handles
|
|
1520
|
+
def test_save_method_with_model_parameters(self):
|
|
1521
|
+
"""Test save method properly handles model parameters form fields"""
|
|
1211
1522
|
form = IPFabricSyncForm(
|
|
1212
1523
|
data={
|
|
1213
1524
|
"name": "Test Sync IPF Params",
|
|
1214
1525
|
"source": self.source.pk,
|
|
1215
1526
|
"snapshot_data": self.snapshot.pk,
|
|
1216
|
-
"
|
|
1217
|
-
"
|
|
1218
|
-
"
|
|
1527
|
+
"dcim.site": True,
|
|
1528
|
+
"dcim.interface": True,
|
|
1529
|
+
"ipam.prefix": True,
|
|
1219
1530
|
}
|
|
1220
1531
|
)
|
|
1221
1532
|
|
|
@@ -1223,25 +1534,25 @@ class IPFabricSyncFormTestCase(TestCase):
|
|
|
1223
1534
|
|
|
1224
1535
|
sync_instance = form.save()
|
|
1225
1536
|
|
|
1226
|
-
# Verify
|
|
1537
|
+
# Verify parameters were stripped and stored correctly
|
|
1227
1538
|
# All models are `False` since checkboxes must always default to False
|
|
1228
1539
|
expected_parameters = {
|
|
1229
1540
|
"sites": [],
|
|
1230
1541
|
"groups": [],
|
|
1231
|
-
"site": True, # Explicitly set
|
|
1232
|
-
"manufacturer": False,
|
|
1233
|
-
"devicetype": False,
|
|
1234
|
-
"devicerole": False,
|
|
1235
|
-
"platform": False,
|
|
1236
|
-
"device": False,
|
|
1237
|
-
"virtualchassis": False,
|
|
1238
|
-
"interface": True, # Explicitly set
|
|
1239
|
-
"macaddress": False,
|
|
1240
|
-
"inventoryitem": False,
|
|
1241
|
-
"ipaddress": False,
|
|
1242
|
-
"vlan": False,
|
|
1243
|
-
"vrf": False,
|
|
1244
|
-
"prefix": True, # Explicitly set
|
|
1542
|
+
"dcim.site": True, # Explicitly set
|
|
1543
|
+
"dcim.manufacturer": False,
|
|
1544
|
+
"dcim.devicetype": False,
|
|
1545
|
+
"dcim.devicerole": False,
|
|
1546
|
+
"dcim.platform": False,
|
|
1547
|
+
"dcim.device": False,
|
|
1548
|
+
"dcim.virtualchassis": False,
|
|
1549
|
+
"dcim.interface": True, # Explicitly set
|
|
1550
|
+
"dcim.macaddress": False,
|
|
1551
|
+
"dcim.inventoryitem": False,
|
|
1552
|
+
"ipam.ipaddress": False,
|
|
1553
|
+
"ipam.vlan": False,
|
|
1554
|
+
"ipam.vrf": False,
|
|
1555
|
+
"ipam.prefix": True, # Explicitly set
|
|
1245
1556
|
}
|
|
1246
1557
|
self.assertEqual(sync_instance.parameters, expected_parameters)
|
|
1247
1558
|
|
|
@@ -1487,7 +1798,6 @@ class IPFabricSyncFormTestCase(TestCase):
|
|
|
1487
1798
|
|
|
1488
1799
|
def test_fieldsets_property_returns_correct_field_types(self):
|
|
1489
1800
|
"""Test that fieldsets property returns FieldSet objects with correct structure"""
|
|
1490
|
-
from utilities.forms.rendering import FieldSet
|
|
1491
1801
|
|
|
1492
1802
|
form = IPFabricSyncForm(
|
|
1493
1803
|
data={
|
|
@@ -1532,3 +1842,875 @@ class IPFabricSyncFormTestCase(TestCase):
|
|
|
1532
1842
|
fieldset_names1 = [fs.name for fs in fieldsets1]
|
|
1533
1843
|
fieldset_names2 = [fs.name for fs in fieldsets2]
|
|
1534
1844
|
self.assertEqual(fieldset_names1, fieldset_names2)
|
|
1845
|
+
|
|
1846
|
+
|
|
1847
|
+
class IPFabricFilterFormTestCase(TestCase):
|
|
1848
|
+
"""Test cases for IPFabricFilterForm"""
|
|
1849
|
+
|
|
1850
|
+
@classmethod
|
|
1851
|
+
def setUpTestData(cls):
|
|
1852
|
+
# Create endpoints for testing
|
|
1853
|
+
cls.endpoint1 = IPFabricEndpoint.objects.get(endpoint="/inventory/devices")
|
|
1854
|
+
cls.endpoint2 = IPFabricEndpoint.objects.get(endpoint="/inventory/interfaces")
|
|
1855
|
+
|
|
1856
|
+
# Create filter expressions for testing
|
|
1857
|
+
cls.expression1 = IPFabricFilterExpression.objects.create(
|
|
1858
|
+
name="Test Expression 1",
|
|
1859
|
+
description="First test expression",
|
|
1860
|
+
expression=[{"siteName": ["eq", "Site1"]}],
|
|
1861
|
+
)
|
|
1862
|
+
cls.expression2 = IPFabricFilterExpression.objects.create(
|
|
1863
|
+
name="Test Expression 2",
|
|
1864
|
+
description="Second test expression",
|
|
1865
|
+
expression=[{"hostname": ["like", "router"]}],
|
|
1866
|
+
)
|
|
1867
|
+
|
|
1868
|
+
# Create a sync object for testing
|
|
1869
|
+
cls.source = IPFabricSource.objects.create(
|
|
1870
|
+
name="Test Source",
|
|
1871
|
+
type=IPFabricSourceTypeChoices.LOCAL,
|
|
1872
|
+
url="https://test.ipfabric.local",
|
|
1873
|
+
status=IPFabricSourceStatusChoices.NEW,
|
|
1874
|
+
)
|
|
1875
|
+
cls.snapshot = IPFabricSnapshot.objects.create(
|
|
1876
|
+
name="Test Snapshot",
|
|
1877
|
+
source=cls.source,
|
|
1878
|
+
snapshot_id="test-snapshot-id",
|
|
1879
|
+
status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
|
|
1880
|
+
data={"sites": ["site1", "site2"]},
|
|
1881
|
+
)
|
|
1882
|
+
cls.sync = IPFabricSync.objects.create(
|
|
1883
|
+
name="Test Sync",
|
|
1884
|
+
snapshot_data=cls.snapshot,
|
|
1885
|
+
)
|
|
1886
|
+
|
|
1887
|
+
def test_fields_are_required(self):
|
|
1888
|
+
"""Test that required fields are validated"""
|
|
1889
|
+
form = IPFabricFilterForm(data={})
|
|
1890
|
+
self.assertFalse(form.is_valid(), form.errors)
|
|
1891
|
+
self.assertIn("name", form.errors)
|
|
1892
|
+
self.assertIn("filter_type", form.errors)
|
|
1893
|
+
|
|
1894
|
+
def test_fields_are_optional(self):
|
|
1895
|
+
"""Test that optional fields work correctly"""
|
|
1896
|
+
form = IPFabricFilterForm(
|
|
1897
|
+
data={
|
|
1898
|
+
"name": "Test Filter",
|
|
1899
|
+
"filter_type": IPFabricFilterTypeChoices.AND,
|
|
1900
|
+
}
|
|
1901
|
+
)
|
|
1902
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
1903
|
+
|
|
1904
|
+
def test_valid_filter_form_with_all_fields(self):
|
|
1905
|
+
"""Test valid form submission with all fields"""
|
|
1906
|
+
form = IPFabricFilterForm(
|
|
1907
|
+
data={
|
|
1908
|
+
"name": "Test Filter Complete",
|
|
1909
|
+
"description": "A complete test filter",
|
|
1910
|
+
"filter_type": IPFabricFilterTypeChoices.OR,
|
|
1911
|
+
"endpoints": [self.endpoint1.pk, self.endpoint2.pk],
|
|
1912
|
+
"syncs": [self.sync.pk],
|
|
1913
|
+
"expressions": [self.expression1.pk, self.expression2.pk],
|
|
1914
|
+
}
|
|
1915
|
+
)
|
|
1916
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
1917
|
+
instance = form.save()
|
|
1918
|
+
|
|
1919
|
+
# Verify the instance was created correctly
|
|
1920
|
+
self.assertEqual(instance.name, "Test Filter Complete")
|
|
1921
|
+
self.assertEqual(instance.description, "A complete test filter")
|
|
1922
|
+
self.assertEqual(instance.filter_type, IPFabricFilterTypeChoices.OR)
|
|
1923
|
+
|
|
1924
|
+
# Verify many-to-many relationships
|
|
1925
|
+
self.assertEqual(
|
|
1926
|
+
list(instance.endpoints.all()), [self.endpoint1, self.endpoint2]
|
|
1927
|
+
)
|
|
1928
|
+
self.assertEqual(list(instance.syncs.all()), [self.sync])
|
|
1929
|
+
self.assertEqual(
|
|
1930
|
+
list(instance.expressions.all().order_by("pk")),
|
|
1931
|
+
[self.expression1, self.expression2],
|
|
1932
|
+
)
|
|
1933
|
+
|
|
1934
|
+
def test_valid_filter_form_with_and_type(self):
|
|
1935
|
+
"""Test form with AND filter type"""
|
|
1936
|
+
form = IPFabricFilterForm(
|
|
1937
|
+
data={
|
|
1938
|
+
"name": "AND Filter",
|
|
1939
|
+
"filter_type": IPFabricFilterTypeChoices.AND,
|
|
1940
|
+
"expressions": [self.expression1.pk],
|
|
1941
|
+
}
|
|
1942
|
+
)
|
|
1943
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
1944
|
+
instance = form.save()
|
|
1945
|
+
self.assertEqual(instance.filter_type, IPFabricFilterTypeChoices.AND)
|
|
1946
|
+
|
|
1947
|
+
def test_valid_filter_form_with_or_type(self):
|
|
1948
|
+
"""Test form with OR filter type"""
|
|
1949
|
+
form = IPFabricFilterForm(
|
|
1950
|
+
data={
|
|
1951
|
+
"name": "OR Filter",
|
|
1952
|
+
"filter_type": IPFabricFilterTypeChoices.OR,
|
|
1953
|
+
"expressions": [self.expression2.pk],
|
|
1954
|
+
}
|
|
1955
|
+
)
|
|
1956
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
1957
|
+
instance = form.save()
|
|
1958
|
+
self.assertEqual(instance.filter_type, IPFabricFilterTypeChoices.OR)
|
|
1959
|
+
|
|
1960
|
+
def test_filter_type_must_be_valid_choice(self):
|
|
1961
|
+
"""Test that filter_type must be a valid choice"""
|
|
1962
|
+
form = IPFabricFilterForm(
|
|
1963
|
+
data={
|
|
1964
|
+
"name": "Invalid Filter",
|
|
1965
|
+
"filter_type": "invalid_type",
|
|
1966
|
+
}
|
|
1967
|
+
)
|
|
1968
|
+
self.assertFalse(form.is_valid(), form.errors)
|
|
1969
|
+
self.assertIn("filter_type", form.errors)
|
|
1970
|
+
|
|
1971
|
+
def test_form_initialization_with_existing_instance(self):
|
|
1972
|
+
"""Test that when editing an existing filter, expressions are properly initialized"""
|
|
1973
|
+
# Create an existing filter with expressions
|
|
1974
|
+
existing_filter = IPFabricFilter.objects.create(
|
|
1975
|
+
name="Existing Filter",
|
|
1976
|
+
filter_type=IPFabricFilterTypeChoices.AND,
|
|
1977
|
+
)
|
|
1978
|
+
existing_filter.expressions.set([self.expression1, self.expression2])
|
|
1979
|
+
|
|
1980
|
+
# Initialize form with the existing instance
|
|
1981
|
+
form = IPFabricFilterForm(instance=existing_filter)
|
|
1982
|
+
|
|
1983
|
+
# Verify that expressions initial is set correctly
|
|
1984
|
+
expected_ids = list(
|
|
1985
|
+
existing_filter.expressions.all().values_list("id", flat=True)
|
|
1986
|
+
)
|
|
1987
|
+
actual_initial = (
|
|
1988
|
+
list(form.fields["expressions"].initial)
|
|
1989
|
+
if form.fields["expressions"].initial
|
|
1990
|
+
else []
|
|
1991
|
+
)
|
|
1992
|
+
self.assertEqual(actual_initial, expected_ids)
|
|
1993
|
+
|
|
1994
|
+
def test_form_initialization_with_new_instance(self):
|
|
1995
|
+
"""Test form initialization for a new instance (no pk)"""
|
|
1996
|
+
# Create a new instance without saving
|
|
1997
|
+
new_filter = IPFabricFilter(
|
|
1998
|
+
name="New Filter",
|
|
1999
|
+
filter_type=IPFabricFilterTypeChoices.OR,
|
|
2000
|
+
)
|
|
2001
|
+
|
|
2002
|
+
# Initialize form with new instance (no pk)
|
|
2003
|
+
form = IPFabricFilterForm(instance=new_filter)
|
|
2004
|
+
|
|
2005
|
+
# Initial should not be set since instance.pk is None
|
|
2006
|
+
self.assertIsNone(form.fields["expressions"].initial)
|
|
2007
|
+
|
|
2008
|
+
def test_save_method_sets_expressions(self):
|
|
2009
|
+
"""Test that save method properly sets the expressions relationship"""
|
|
2010
|
+
form = IPFabricFilterForm(
|
|
2011
|
+
data={
|
|
2012
|
+
"name": "Filter with Expressions",
|
|
2013
|
+
"filter_type": IPFabricFilterTypeChoices.AND,
|
|
2014
|
+
"expressions": [self.expression1.pk, self.expression2.pk],
|
|
2015
|
+
}
|
|
2016
|
+
)
|
|
2017
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2018
|
+
|
|
2019
|
+
instance = form.save()
|
|
2020
|
+
|
|
2021
|
+
# Verify expressions were set correctly
|
|
2022
|
+
expression_ids = list(instance.expressions.all().values_list("id", flat=True))
|
|
2023
|
+
self.assertIn(self.expression1.pk, expression_ids)
|
|
2024
|
+
self.assertIn(self.expression2.pk, expression_ids)
|
|
2025
|
+
self.assertEqual(len(expression_ids), 2)
|
|
2026
|
+
|
|
2027
|
+
def test_save_method_updates_expressions(self):
|
|
2028
|
+
"""Test that save method properly updates expressions on existing instance"""
|
|
2029
|
+
# Create an existing filter with one expression
|
|
2030
|
+
existing_filter = IPFabricFilter.objects.create(
|
|
2031
|
+
name="Existing Filter",
|
|
2032
|
+
filter_type=IPFabricFilterTypeChoices.AND,
|
|
2033
|
+
)
|
|
2034
|
+
existing_filter.expressions.set([self.expression1])
|
|
2035
|
+
|
|
2036
|
+
# Update the filter to use different expressions
|
|
2037
|
+
form = IPFabricFilterForm(
|
|
2038
|
+
data={
|
|
2039
|
+
"name": "Updated Filter",
|
|
2040
|
+
"filter_type": IPFabricFilterTypeChoices.OR,
|
|
2041
|
+
"expressions": [self.expression2.pk],
|
|
2042
|
+
},
|
|
2043
|
+
instance=existing_filter,
|
|
2044
|
+
)
|
|
2045
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2046
|
+
|
|
2047
|
+
updated_instance = form.save()
|
|
2048
|
+
|
|
2049
|
+
# Verify expressions were updated
|
|
2050
|
+
expression_ids = list(
|
|
2051
|
+
updated_instance.expressions.all().values_list("id", flat=True)
|
|
2052
|
+
)
|
|
2053
|
+
self.assertEqual(expression_ids, [self.expression2.pk])
|
|
2054
|
+
self.assertEqual(updated_instance.filter_type, IPFabricFilterTypeChoices.OR)
|
|
2055
|
+
|
|
2056
|
+
def test_save_method_clears_expressions(self):
|
|
2057
|
+
"""Test that save method can clear all expressions"""
|
|
2058
|
+
# Create an existing filter with expressions
|
|
2059
|
+
existing_filter = IPFabricFilter.objects.create(
|
|
2060
|
+
name="Filter with Expressions",
|
|
2061
|
+
filter_type=IPFabricFilterTypeChoices.AND,
|
|
2062
|
+
)
|
|
2063
|
+
existing_filter.expressions.set([self.expression1, self.expression2])
|
|
2064
|
+
|
|
2065
|
+
# Update the filter to have no expressions
|
|
2066
|
+
form = IPFabricFilterForm(
|
|
2067
|
+
data={
|
|
2068
|
+
"name": "Filter without Expressions",
|
|
2069
|
+
"filter_type": IPFabricFilterTypeChoices.AND,
|
|
2070
|
+
"expressions": [],
|
|
2071
|
+
},
|
|
2072
|
+
instance=existing_filter,
|
|
2073
|
+
)
|
|
2074
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2075
|
+
|
|
2076
|
+
updated_instance = form.save()
|
|
2077
|
+
|
|
2078
|
+
# Verify expressions were cleared
|
|
2079
|
+
self.assertEqual(updated_instance.expressions.count(), 0)
|
|
2080
|
+
|
|
2081
|
+
def test_form_with_multiple_endpoints(self):
|
|
2082
|
+
"""Test form with multiple endpoints"""
|
|
2083
|
+
form = IPFabricFilterForm(
|
|
2084
|
+
data={
|
|
2085
|
+
"name": "Multi-Endpoint Filter",
|
|
2086
|
+
"filter_type": IPFabricFilterTypeChoices.AND,
|
|
2087
|
+
"endpoints": [self.endpoint1.pk, self.endpoint2.pk],
|
|
2088
|
+
}
|
|
2089
|
+
)
|
|
2090
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2091
|
+
instance = form.save()
|
|
2092
|
+
|
|
2093
|
+
self.assertEqual(instance.endpoints.count(), 2)
|
|
2094
|
+
self.assertIn(self.endpoint1, instance.endpoints.all())
|
|
2095
|
+
self.assertIn(self.endpoint2, instance.endpoints.all())
|
|
2096
|
+
|
|
2097
|
+
def test_form_with_multiple_syncs(self):
|
|
2098
|
+
"""Test form with multiple syncs"""
|
|
2099
|
+
# Create another sync
|
|
2100
|
+
sync2 = IPFabricSync.objects.create(
|
|
2101
|
+
name="Test Sync 2",
|
|
2102
|
+
snapshot_data=self.snapshot,
|
|
2103
|
+
)
|
|
2104
|
+
|
|
2105
|
+
form = IPFabricFilterForm(
|
|
2106
|
+
data={
|
|
2107
|
+
"name": "Multi-Sync Filter",
|
|
2108
|
+
"filter_type": IPFabricFilterTypeChoices.OR,
|
|
2109
|
+
"syncs": [self.sync.pk, sync2.pk],
|
|
2110
|
+
}
|
|
2111
|
+
)
|
|
2112
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2113
|
+
instance = form.save()
|
|
2114
|
+
|
|
2115
|
+
self.assertEqual(instance.syncs.count(), 2)
|
|
2116
|
+
self.assertIn(self.sync, instance.syncs.all())
|
|
2117
|
+
self.assertIn(sync2, instance.syncs.all())
|
|
2118
|
+
|
|
2119
|
+
def test_form_without_optional_fields(self):
|
|
2120
|
+
"""Test that form works without any optional fields"""
|
|
2121
|
+
form = IPFabricFilterForm(
|
|
2122
|
+
data={
|
|
2123
|
+
"name": "Minimal Filter",
|
|
2124
|
+
"filter_type": IPFabricFilterTypeChoices.AND,
|
|
2125
|
+
}
|
|
2126
|
+
)
|
|
2127
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2128
|
+
instance = form.save()
|
|
2129
|
+
|
|
2130
|
+
self.assertEqual(instance.name, "Minimal Filter")
|
|
2131
|
+
self.assertEqual(instance.endpoints.count(), 0)
|
|
2132
|
+
self.assertEqual(instance.syncs.count(), 0)
|
|
2133
|
+
self.assertEqual(instance.expressions.count(), 0)
|
|
2134
|
+
|
|
2135
|
+
|
|
2136
|
+
class IPFabricFilterExpressionFormTestCase(TestCase):
|
|
2137
|
+
"""Test cases for IPFabricFilterExpressionForm"""
|
|
2138
|
+
|
|
2139
|
+
@classmethod
|
|
2140
|
+
def setUpTestData(cls):
|
|
2141
|
+
# Create filters for testing
|
|
2142
|
+
cls.filter1 = IPFabricFilter.objects.create(
|
|
2143
|
+
name="Test Filter 1",
|
|
2144
|
+
filter_type=IPFabricFilterTypeChoices.AND,
|
|
2145
|
+
)
|
|
2146
|
+
cls.filter2 = IPFabricFilter.objects.create(
|
|
2147
|
+
name="Test Filter 2",
|
|
2148
|
+
filter_type=IPFabricFilterTypeChoices.OR,
|
|
2149
|
+
)
|
|
2150
|
+
|
|
2151
|
+
def test_fields_are_required(self):
|
|
2152
|
+
"""Test that required fields are validated"""
|
|
2153
|
+
form = IPFabricFilterExpressionForm(data={})
|
|
2154
|
+
self.assertFalse(form.is_valid(), form.errors)
|
|
2155
|
+
self.assertIn("name", form.errors)
|
|
2156
|
+
self.assertIn("expression", form.errors)
|
|
2157
|
+
|
|
2158
|
+
def test_fields_are_optional(self):
|
|
2159
|
+
"""Test that optional fields work correctly"""
|
|
2160
|
+
form = IPFabricFilterExpressionForm(
|
|
2161
|
+
data={
|
|
2162
|
+
"name": "Test Expression",
|
|
2163
|
+
"expression": [{"siteName": ["eq", "Site1"]}],
|
|
2164
|
+
}
|
|
2165
|
+
)
|
|
2166
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2167
|
+
|
|
2168
|
+
def test_valid_expression_form_with_all_fields(self):
|
|
2169
|
+
"""Test valid form submission with all fields"""
|
|
2170
|
+
expression_data = [{"siteName": ["eq", "Site1"]}]
|
|
2171
|
+
form = IPFabricFilterExpressionForm(
|
|
2172
|
+
data={
|
|
2173
|
+
"name": "Complete Expression",
|
|
2174
|
+
"description": "A complete test expression",
|
|
2175
|
+
"expression": expression_data,
|
|
2176
|
+
"filters": [self.filter1.pk, self.filter2.pk],
|
|
2177
|
+
}
|
|
2178
|
+
)
|
|
2179
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2180
|
+
instance = form.save()
|
|
2181
|
+
|
|
2182
|
+
# Verify the instance was created correctly
|
|
2183
|
+
self.assertEqual(instance.name, "Complete Expression")
|
|
2184
|
+
self.assertEqual(instance.description, "A complete test expression")
|
|
2185
|
+
self.assertEqual(instance.expression, expression_data)
|
|
2186
|
+
|
|
2187
|
+
# Verify many-to-many relationships
|
|
2188
|
+
self.assertEqual(instance.filters.count(), 2)
|
|
2189
|
+
self.assertIn(self.filter1, instance.filters.all())
|
|
2190
|
+
self.assertIn(self.filter2, instance.filters.all())
|
|
2191
|
+
|
|
2192
|
+
def test_valid_expression_with_simple_filter(self):
|
|
2193
|
+
"""Test form with a simple site name filter"""
|
|
2194
|
+
form = IPFabricFilterExpressionForm(
|
|
2195
|
+
data={
|
|
2196
|
+
"name": "Site Filter",
|
|
2197
|
+
"expression": [{"siteName": ["eq", "Site1"]}],
|
|
2198
|
+
}
|
|
2199
|
+
)
|
|
2200
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2201
|
+
instance = form.save()
|
|
2202
|
+
self.assertEqual(instance.expression, [{"siteName": ["eq", "Site1"]}])
|
|
2203
|
+
|
|
2204
|
+
def test_valid_expression_with_hostname_filter(self):
|
|
2205
|
+
"""Test form with hostname like filter"""
|
|
2206
|
+
form = IPFabricFilterExpressionForm(
|
|
2207
|
+
data={
|
|
2208
|
+
"name": "Hostname Filter",
|
|
2209
|
+
"expression": [{"hostname": ["like", "router"]}],
|
|
2210
|
+
}
|
|
2211
|
+
)
|
|
2212
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2213
|
+
instance = form.save()
|
|
2214
|
+
self.assertEqual(instance.expression, [{"hostname": ["like", "router"]}])
|
|
2215
|
+
|
|
2216
|
+
def test_valid_expression_with_complex_filter(self):
|
|
2217
|
+
"""Test form with complex nested filter expression"""
|
|
2218
|
+
complex_expression = [
|
|
2219
|
+
{
|
|
2220
|
+
"or": [
|
|
2221
|
+
{"siteName": ["eq", "Site1"]},
|
|
2222
|
+
{"siteName": ["eq", "Site2"]},
|
|
2223
|
+
]
|
|
2224
|
+
}
|
|
2225
|
+
]
|
|
2226
|
+
form = IPFabricFilterExpressionForm(
|
|
2227
|
+
data={
|
|
2228
|
+
"name": "Complex Expression",
|
|
2229
|
+
"expression": complex_expression,
|
|
2230
|
+
}
|
|
2231
|
+
)
|
|
2232
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2233
|
+
instance = form.save()
|
|
2234
|
+
self.assertEqual(instance.expression, complex_expression)
|
|
2235
|
+
|
|
2236
|
+
def test_valid_expression_with_multiple_conditions(self):
|
|
2237
|
+
"""Test form with multiple filter conditions"""
|
|
2238
|
+
multi_condition_expression = [
|
|
2239
|
+
{"siteName": ["eq", "Site1"]},
|
|
2240
|
+
{"hostname": ["like", "switch"]},
|
|
2241
|
+
]
|
|
2242
|
+
form = IPFabricFilterExpressionForm(
|
|
2243
|
+
data={
|
|
2244
|
+
"name": "Multi Condition",
|
|
2245
|
+
"expression": multi_condition_expression,
|
|
2246
|
+
}
|
|
2247
|
+
)
|
|
2248
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2249
|
+
instance = form.save()
|
|
2250
|
+
self.assertEqual(instance.expression, multi_condition_expression)
|
|
2251
|
+
|
|
2252
|
+
def test_expression_must_be_valid_json(self):
|
|
2253
|
+
"""Test that expression field accepts valid JSON"""
|
|
2254
|
+
|
|
2255
|
+
# Valid JSON that will be parsed
|
|
2256
|
+
valid_json_str = json.dumps([{"siteName": ["eq", "Site1"]}])
|
|
2257
|
+
form = IPFabricFilterExpressionForm(
|
|
2258
|
+
data={
|
|
2259
|
+
"name": "JSON Expression",
|
|
2260
|
+
"expression": json.loads(valid_json_str), # Pass as Python object
|
|
2261
|
+
}
|
|
2262
|
+
)
|
|
2263
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2264
|
+
|
|
2265
|
+
def test_form_with_single_filter(self):
|
|
2266
|
+
"""Test form with a single filter"""
|
|
2267
|
+
form = IPFabricFilterExpressionForm(
|
|
2268
|
+
data={
|
|
2269
|
+
"name": "Single Filter Expression",
|
|
2270
|
+
"expression": [{"siteName": ["eq", "Site1"]}],
|
|
2271
|
+
"filters": [self.filter1.pk],
|
|
2272
|
+
}
|
|
2273
|
+
)
|
|
2274
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2275
|
+
instance = form.save()
|
|
2276
|
+
|
|
2277
|
+
self.assertEqual(instance.filters.count(), 1)
|
|
2278
|
+
self.assertIn(self.filter1, instance.filters.all())
|
|
2279
|
+
|
|
2280
|
+
def test_form_with_multiple_filters(self):
|
|
2281
|
+
"""Test form with multiple filters"""
|
|
2282
|
+
form = IPFabricFilterExpressionForm(
|
|
2283
|
+
data={
|
|
2284
|
+
"name": "Multi Filter Expression",
|
|
2285
|
+
"expression": [{"hostname": ["like", "core"]}],
|
|
2286
|
+
"filters": [self.filter1.pk, self.filter2.pk],
|
|
2287
|
+
}
|
|
2288
|
+
)
|
|
2289
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2290
|
+
instance = form.save()
|
|
2291
|
+
|
|
2292
|
+
self.assertEqual(instance.filters.count(), 2)
|
|
2293
|
+
self.assertIn(self.filter1, instance.filters.all())
|
|
2294
|
+
self.assertIn(self.filter2, instance.filters.all())
|
|
2295
|
+
|
|
2296
|
+
def test_form_without_filters(self):
|
|
2297
|
+
"""Test that form works without any filters"""
|
|
2298
|
+
form = IPFabricFilterExpressionForm(
|
|
2299
|
+
data={
|
|
2300
|
+
"name": "No Filter Expression",
|
|
2301
|
+
"expression": [{"siteName": ["eq", "Site1"]}],
|
|
2302
|
+
}
|
|
2303
|
+
)
|
|
2304
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2305
|
+
instance = form.save()
|
|
2306
|
+
|
|
2307
|
+
self.assertEqual(instance.filters.count(), 0)
|
|
2308
|
+
|
|
2309
|
+
def test_form_without_description(self):
|
|
2310
|
+
"""Test that description is optional"""
|
|
2311
|
+
form = IPFabricFilterExpressionForm(
|
|
2312
|
+
data={
|
|
2313
|
+
"name": "No Description",
|
|
2314
|
+
"expression": [{"siteName": ["eq", "Site1"]}],
|
|
2315
|
+
}
|
|
2316
|
+
)
|
|
2317
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2318
|
+
instance = form.save()
|
|
2319
|
+
self.assertEqual(instance.description, "")
|
|
2320
|
+
|
|
2321
|
+
def test_form_with_description(self):
|
|
2322
|
+
"""Test form with description field"""
|
|
2323
|
+
description = "This is a detailed description of the filter expression"
|
|
2324
|
+
form = IPFabricFilterExpressionForm(
|
|
2325
|
+
data={
|
|
2326
|
+
"name": "With Description",
|
|
2327
|
+
"description": description,
|
|
2328
|
+
"expression": [{"siteName": ["eq", "Site1"]}],
|
|
2329
|
+
}
|
|
2330
|
+
)
|
|
2331
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2332
|
+
instance = form.save()
|
|
2333
|
+
self.assertEqual(instance.description, description)
|
|
2334
|
+
|
|
2335
|
+
def test_form_update_existing_expression(self):
|
|
2336
|
+
"""Test updating an existing expression"""
|
|
2337
|
+
# Create an existing expression
|
|
2338
|
+
existing_expression = IPFabricFilterExpression.objects.create(
|
|
2339
|
+
name="Existing Expression",
|
|
2340
|
+
expression=[{"siteName": ["eq", "OldSite"]}],
|
|
2341
|
+
)
|
|
2342
|
+
existing_expression.filters.set([self.filter1])
|
|
2343
|
+
|
|
2344
|
+
# Update it
|
|
2345
|
+
new_expression_data = [{"siteName": ["eq", "NewSite"]}]
|
|
2346
|
+
form = IPFabricFilterExpressionForm(
|
|
2347
|
+
data={
|
|
2348
|
+
"name": "Updated Expression",
|
|
2349
|
+
"description": "Updated description",
|
|
2350
|
+
"expression": new_expression_data,
|
|
2351
|
+
"filters": [self.filter2.pk],
|
|
2352
|
+
},
|
|
2353
|
+
instance=existing_expression,
|
|
2354
|
+
)
|
|
2355
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2356
|
+
updated_instance = form.save()
|
|
2357
|
+
|
|
2358
|
+
# Verify updates
|
|
2359
|
+
self.assertEqual(updated_instance.name, "Updated Expression")
|
|
2360
|
+
self.assertEqual(updated_instance.description, "Updated description")
|
|
2361
|
+
self.assertEqual(updated_instance.expression, new_expression_data)
|
|
2362
|
+
self.assertEqual(list(updated_instance.filters.all()), [self.filter2])
|
|
2363
|
+
|
|
2364
|
+
def test_form_clear_filters(self):
|
|
2365
|
+
"""Test clearing all filters from an existing expression"""
|
|
2366
|
+
# Create an existing expression with filters
|
|
2367
|
+
existing_expression = IPFabricFilterExpression.objects.create(
|
|
2368
|
+
name="Expression with Filters",
|
|
2369
|
+
expression=[{"siteName": ["eq", "Site1"]}],
|
|
2370
|
+
)
|
|
2371
|
+
existing_expression.filters.set([self.filter1, self.filter2])
|
|
2372
|
+
|
|
2373
|
+
# Clear filters
|
|
2374
|
+
form = IPFabricFilterExpressionForm(
|
|
2375
|
+
data={
|
|
2376
|
+
"name": "Expression without Filters",
|
|
2377
|
+
"expression": [{"siteName": ["eq", "Site1"]}],
|
|
2378
|
+
"filters": [],
|
|
2379
|
+
},
|
|
2380
|
+
instance=existing_expression,
|
|
2381
|
+
)
|
|
2382
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2383
|
+
updated_instance = form.save()
|
|
2384
|
+
|
|
2385
|
+
# Verify filters were cleared
|
|
2386
|
+
self.assertEqual(updated_instance.filters.count(), 0)
|
|
2387
|
+
|
|
2388
|
+
def test_name_must_be_unique(self):
|
|
2389
|
+
"""Test that name field must be unique"""
|
|
2390
|
+
# Create first expression
|
|
2391
|
+
IPFabricFilterExpression.objects.create(
|
|
2392
|
+
name="Unique Name",
|
|
2393
|
+
expression=[{"siteName": ["eq", "Site1"]}],
|
|
2394
|
+
)
|
|
2395
|
+
|
|
2396
|
+
# Try to create another with same name
|
|
2397
|
+
form = IPFabricFilterExpressionForm(
|
|
2398
|
+
data={
|
|
2399
|
+
"name": "Unique Name",
|
|
2400
|
+
"expression": [{"siteName": ["eq", "Site2"]}],
|
|
2401
|
+
}
|
|
2402
|
+
)
|
|
2403
|
+
self.assertFalse(form.is_valid())
|
|
2404
|
+
self.assertIn("name", form.errors)
|
|
2405
|
+
|
|
2406
|
+
def test_textarea_widget_for_expression(self):
|
|
2407
|
+
"""Test that expression field uses Textarea widget with monospace class"""
|
|
2408
|
+
form = IPFabricFilterExpressionForm()
|
|
2409
|
+
expression_widget = form.fields["expression"].widget
|
|
2410
|
+
|
|
2411
|
+
# Check widget type
|
|
2412
|
+
self.assertIsInstance(expression_widget, forms.Textarea)
|
|
2413
|
+
|
|
2414
|
+
# Check that it has the monospace class
|
|
2415
|
+
self.assertIn("class", expression_widget.attrs)
|
|
2416
|
+
self.assertIn("font-monospace", expression_widget.attrs["class"])
|
|
2417
|
+
|
|
2418
|
+
def test_form_with_various_operator_types(self):
|
|
2419
|
+
"""Test expressions with various operator types"""
|
|
2420
|
+
operators = [
|
|
2421
|
+
[{"siteName": ["eq", "Site1"]}], # equals
|
|
2422
|
+
[{"hostname": ["like", "%router%"]}], # like
|
|
2423
|
+
[{"hostname": ["reg", "^switch.*"]}], # regex
|
|
2424
|
+
[{"vlan": ["gt", 100]}], # greater than
|
|
2425
|
+
[{"vlan": ["lt", 200]}], # less than
|
|
2426
|
+
]
|
|
2427
|
+
|
|
2428
|
+
for i, expression_data in enumerate(operators):
|
|
2429
|
+
form = IPFabricFilterExpressionForm(
|
|
2430
|
+
data={
|
|
2431
|
+
"name": f"Operator Test {i}",
|
|
2432
|
+
"expression": expression_data,
|
|
2433
|
+
}
|
|
2434
|
+
)
|
|
2435
|
+
self.assertTrue(
|
|
2436
|
+
form.is_valid(), f"Form invalid for {expression_data}: {form.errors}"
|
|
2437
|
+
)
|
|
2438
|
+
instance = form.save()
|
|
2439
|
+
self.assertEqual(instance.expression, expression_data)
|
|
2440
|
+
|
|
2441
|
+
def test_form_minimal_required_fields(self):
|
|
2442
|
+
"""Test form with only minimal required fields"""
|
|
2443
|
+
form = IPFabricFilterExpressionForm(
|
|
2444
|
+
data={
|
|
2445
|
+
"name": "Minimal Expression",
|
|
2446
|
+
"expression": [{"siteName": ["eq", "Site1"]}],
|
|
2447
|
+
}
|
|
2448
|
+
)
|
|
2449
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2450
|
+
instance = form.save()
|
|
2451
|
+
|
|
2452
|
+
self.assertEqual(instance.name, "Minimal Expression")
|
|
2453
|
+
self.assertEqual(instance.expression, [{"siteName": ["eq", "Site1"]}])
|
|
2454
|
+
self.assertEqual(instance.description, "")
|
|
2455
|
+
self.assertEqual(instance.filters.count(), 0)
|
|
2456
|
+
|
|
2457
|
+
def test_expression_must_be_list(self):
|
|
2458
|
+
"""Test that expression must be a list (validation at model level)"""
|
|
2459
|
+
# Try to save with a string instead of list
|
|
2460
|
+
form = IPFabricFilterExpressionForm(
|
|
2461
|
+
data={
|
|
2462
|
+
"name": "Invalid String Expression",
|
|
2463
|
+
"expression": "not a list",
|
|
2464
|
+
}
|
|
2465
|
+
)
|
|
2466
|
+
self.assertFalse(form.is_valid())
|
|
2467
|
+
self.assertIn("expression", form.errors)
|
|
2468
|
+
|
|
2469
|
+
def test_expression_cannot_be_empty_list(self):
|
|
2470
|
+
"""Test that expression cannot be an empty list (validation at model level)"""
|
|
2471
|
+
form = IPFabricFilterExpressionForm(
|
|
2472
|
+
data={
|
|
2473
|
+
"name": "Empty List Expression",
|
|
2474
|
+
"expression": [],
|
|
2475
|
+
}
|
|
2476
|
+
)
|
|
2477
|
+
self.assertFalse(form.is_valid())
|
|
2478
|
+
self.assertIn("expression", form.errors)
|
|
2479
|
+
|
|
2480
|
+
def test_expression_must_contain_dictionaries(self):
|
|
2481
|
+
"""Test that expression items must be dictionaries (validation at model level)"""
|
|
2482
|
+
# Test with list containing strings
|
|
2483
|
+
form = IPFabricFilterExpressionForm(
|
|
2484
|
+
data={
|
|
2485
|
+
"name": "Invalid List Items",
|
|
2486
|
+
"expression": ["string1", "string2"],
|
|
2487
|
+
}
|
|
2488
|
+
)
|
|
2489
|
+
self.assertFalse(form.is_valid())
|
|
2490
|
+
self.assertIn("expression", form.errors)
|
|
2491
|
+
|
|
2492
|
+
def test_expression_with_mixed_types(self):
|
|
2493
|
+
"""Test that expression rejects mixed types in list (validation at model level)"""
|
|
2494
|
+
form = IPFabricFilterExpressionForm(
|
|
2495
|
+
data={
|
|
2496
|
+
"name": "Mixed Types Expression",
|
|
2497
|
+
"expression": [{"siteName": ["eq", "Site1"]}, "not a dict", 123],
|
|
2498
|
+
}
|
|
2499
|
+
)
|
|
2500
|
+
self.assertFalse(form.is_valid())
|
|
2501
|
+
self.assertIn("expression", form.errors)
|
|
2502
|
+
|
|
2503
|
+
def test_expression_with_list_of_integers(self):
|
|
2504
|
+
"""Test that expression rejects list of integers (validation at model level)"""
|
|
2505
|
+
form = IPFabricFilterExpressionForm(
|
|
2506
|
+
data={
|
|
2507
|
+
"name": "Integer List Expression",
|
|
2508
|
+
"expression": [1, 2, 3],
|
|
2509
|
+
}
|
|
2510
|
+
)
|
|
2511
|
+
self.assertFalse(form.is_valid())
|
|
2512
|
+
self.assertIn("expression", form.errors)
|
|
2513
|
+
|
|
2514
|
+
def test_expression_with_nested_structure(self):
|
|
2515
|
+
"""Test that expression accepts complex nested dictionary structures"""
|
|
2516
|
+
complex_expr = [
|
|
2517
|
+
{
|
|
2518
|
+
"and": [
|
|
2519
|
+
{"siteName": ["eq", "Site1"]},
|
|
2520
|
+
{
|
|
2521
|
+
"or": [
|
|
2522
|
+
{"hostname": ["like", "router%"]},
|
|
2523
|
+
{"hostname": ["like", "switch%"]},
|
|
2524
|
+
]
|
|
2525
|
+
},
|
|
2526
|
+
]
|
|
2527
|
+
}
|
|
2528
|
+
]
|
|
2529
|
+
form = IPFabricFilterExpressionForm(
|
|
2530
|
+
data={
|
|
2531
|
+
"name": "Nested Structure Expression",
|
|
2532
|
+
"expression": complex_expr,
|
|
2533
|
+
}
|
|
2534
|
+
)
|
|
2535
|
+
self.assertTrue(form.is_valid(), form.errors)
|
|
2536
|
+
instance = form.save()
|
|
2537
|
+
self.assertEqual(instance.expression, complex_expr)
|
|
2538
|
+
|
|
2539
|
+
def test_form_init_sets_test_source_initial_when_single_source(self):
|
|
2540
|
+
"""Test that form __init__ sets test_source initial value when only one LOCAL source exists"""
|
|
2541
|
+
|
|
2542
|
+
# Create a LOCAL source
|
|
2543
|
+
source = IPFabricSource.objects.create(
|
|
2544
|
+
name="Test Local Source",
|
|
2545
|
+
type=IPFabricSourceTypeChoices.LOCAL,
|
|
2546
|
+
url="https://test.local",
|
|
2547
|
+
)
|
|
2548
|
+
|
|
2549
|
+
# Create snapshot and sync with LOCAL source
|
|
2550
|
+
snapshot = IPFabricSnapshot.objects.create(
|
|
2551
|
+
name="Test Snapshot",
|
|
2552
|
+
source=source,
|
|
2553
|
+
)
|
|
2554
|
+
sync = IPFabricSync.objects.create(
|
|
2555
|
+
name="Test Sync",
|
|
2556
|
+
snapshot_data=snapshot,
|
|
2557
|
+
)
|
|
2558
|
+
|
|
2559
|
+
# Create filter with endpoint
|
|
2560
|
+
filter_obj = IPFabricFilter.objects.create(
|
|
2561
|
+
name="Test Filter with Source",
|
|
2562
|
+
filter_type=IPFabricFilterTypeChoices.AND,
|
|
2563
|
+
)
|
|
2564
|
+
filter_obj.syncs.add(sync)
|
|
2565
|
+
|
|
2566
|
+
# Create expression and associate with filter
|
|
2567
|
+
expression = IPFabricFilterExpression.objects.create(
|
|
2568
|
+
name="Expression with Single Source",
|
|
2569
|
+
expression=[{"siteName": ["eq", "Site1"]}],
|
|
2570
|
+
)
|
|
2571
|
+
expression.filters.add(filter_obj)
|
|
2572
|
+
|
|
2573
|
+
# Create form with instance - should set test_source initial value
|
|
2574
|
+
form = IPFabricFilterExpressionForm(instance=expression)
|
|
2575
|
+
|
|
2576
|
+
# Verify test_source initial value is set
|
|
2577
|
+
self.assertEqual(form.fields["test_source"].initial, source)
|
|
2578
|
+
|
|
2579
|
+
def test_form_init_sets_test_endpoint_initial_when_single_endpoint(self):
|
|
2580
|
+
"""Test that form __init__ sets test_endpoint initial value when only one endpoint exists"""
|
|
2581
|
+
|
|
2582
|
+
# Get or create an endpoint
|
|
2583
|
+
endpoint = IPFabricEndpoint.objects.first()
|
|
2584
|
+
if not endpoint:
|
|
2585
|
+
endpoint = IPFabricEndpoint.objects.create(
|
|
2586
|
+
name="Test Endpoint", endpoint="/tables/devices"
|
|
2587
|
+
)
|
|
2588
|
+
|
|
2589
|
+
# Create filter with endpoint
|
|
2590
|
+
filter_obj = IPFabricFilter.objects.create(
|
|
2591
|
+
name="Test Filter with Endpoint",
|
|
2592
|
+
filter_type=IPFabricFilterTypeChoices.AND,
|
|
2593
|
+
)
|
|
2594
|
+
filter_obj.endpoints.add(endpoint)
|
|
2595
|
+
|
|
2596
|
+
# Create expression and associate with filter
|
|
2597
|
+
expression = IPFabricFilterExpression.objects.create(
|
|
2598
|
+
name="Expression with Single Endpoint",
|
|
2599
|
+
expression=[{"siteName": ["eq", "Site1"]}],
|
|
2600
|
+
)
|
|
2601
|
+
expression.filters.add(filter_obj)
|
|
2602
|
+
|
|
2603
|
+
# Create form with instance - should set test_endpoint initial value
|
|
2604
|
+
form = IPFabricFilterExpressionForm(instance=expression)
|
|
2605
|
+
|
|
2606
|
+
# Verify test_endpoint initial value is set
|
|
2607
|
+
self.assertEqual(form.fields["test_endpoint"].initial, endpoint)
|
|
2608
|
+
|
|
2609
|
+
def test_form_init_does_not_set_initial_when_multiple_sources(self):
|
|
2610
|
+
"""Test that form __init__ does not set test_source when multiple LOCAL sources exist"""
|
|
2611
|
+
|
|
2612
|
+
# Create two LOCAL sources
|
|
2613
|
+
source1 = IPFabricSource.objects.create(
|
|
2614
|
+
name="Test Local Source 1",
|
|
2615
|
+
type=IPFabricSourceTypeChoices.LOCAL,
|
|
2616
|
+
url="https://test1.local",
|
|
2617
|
+
)
|
|
2618
|
+
source2 = IPFabricSource.objects.create(
|
|
2619
|
+
name="Test Local Source 2",
|
|
2620
|
+
type=IPFabricSourceTypeChoices.LOCAL,
|
|
2621
|
+
url="https://test2.local",
|
|
2622
|
+
)
|
|
2623
|
+
|
|
2624
|
+
# Create snapshots and syncs
|
|
2625
|
+
snapshot1 = IPFabricSnapshot.objects.create(
|
|
2626
|
+
name="Test Snapshot 1", source=source1
|
|
2627
|
+
)
|
|
2628
|
+
snapshot2 = IPFabricSnapshot.objects.create(
|
|
2629
|
+
name="Test Snapshot 2", source=source2
|
|
2630
|
+
)
|
|
2631
|
+
|
|
2632
|
+
sync1 = IPFabricSync.objects.create(
|
|
2633
|
+
name="Test Sync 1",
|
|
2634
|
+
snapshot_data=snapshot1,
|
|
2635
|
+
)
|
|
2636
|
+
sync2 = IPFabricSync.objects.create(
|
|
2637
|
+
name="Test Sync 2",
|
|
2638
|
+
snapshot_data=snapshot2,
|
|
2639
|
+
)
|
|
2640
|
+
|
|
2641
|
+
# Create two filters with different syncs
|
|
2642
|
+
filter1 = IPFabricFilter.objects.create(
|
|
2643
|
+
name="Test Filter for Source 1",
|
|
2644
|
+
filter_type=IPFabricFilterTypeChoices.AND,
|
|
2645
|
+
)
|
|
2646
|
+
filter1.syncs.add(sync1)
|
|
2647
|
+
|
|
2648
|
+
filter2 = IPFabricFilter.objects.create(
|
|
2649
|
+
name="Test Filter for Source 2",
|
|
2650
|
+
filter_type=IPFabricFilterTypeChoices.AND,
|
|
2651
|
+
)
|
|
2652
|
+
filter2.syncs.add(sync2)
|
|
2653
|
+
|
|
2654
|
+
# Create expression and associate with both filters
|
|
2655
|
+
expression = IPFabricFilterExpression.objects.create(
|
|
2656
|
+
name="Expression with Multiple Sources",
|
|
2657
|
+
expression=[{"siteName": ["eq", "Site1"]}],
|
|
2658
|
+
)
|
|
2659
|
+
expression.filters.add(filter1, filter2)
|
|
2660
|
+
|
|
2661
|
+
# Create form with instance - should NOT set test_source initial value
|
|
2662
|
+
form = IPFabricFilterExpressionForm(instance=expression)
|
|
2663
|
+
|
|
2664
|
+
# Verify test_source initial value is NOT set
|
|
2665
|
+
self.assertIsNone(form.fields["test_source"].initial)
|
|
2666
|
+
|
|
2667
|
+
def test_form_init_does_not_set_initial_when_multiple_endpoints(self):
|
|
2668
|
+
"""Test that form __init__ does not set test_endpoint when multiple endpoints exist"""
|
|
2669
|
+
|
|
2670
|
+
# Get or create two endpoints
|
|
2671
|
+
endpoint1 = IPFabricEndpoint.objects.filter(endpoint="/tables/devices").first()
|
|
2672
|
+
endpoint2 = IPFabricEndpoint.objects.filter(endpoint="/tables/sites").first()
|
|
2673
|
+
|
|
2674
|
+
if not endpoint1:
|
|
2675
|
+
endpoint1 = IPFabricEndpoint.objects.create(
|
|
2676
|
+
name="Devices Endpoint", endpoint="/tables/devices"
|
|
2677
|
+
)
|
|
2678
|
+
if not endpoint2:
|
|
2679
|
+
endpoint2 = IPFabricEndpoint.objects.create(
|
|
2680
|
+
name="Sites Endpoint", endpoint="/tables/sites"
|
|
2681
|
+
)
|
|
2682
|
+
|
|
2683
|
+
# Create two filters with different endpoints
|
|
2684
|
+
filter1 = IPFabricFilter.objects.create(
|
|
2685
|
+
name="Test Filter Devices",
|
|
2686
|
+
filter_type=IPFabricFilterTypeChoices.AND,
|
|
2687
|
+
)
|
|
2688
|
+
filter1.endpoints.add(endpoint1)
|
|
2689
|
+
|
|
2690
|
+
filter2 = IPFabricFilter.objects.create(
|
|
2691
|
+
name="Test Filter Sites",
|
|
2692
|
+
filter_type=IPFabricFilterTypeChoices.AND,
|
|
2693
|
+
)
|
|
2694
|
+
filter2.endpoints.add(endpoint2)
|
|
2695
|
+
|
|
2696
|
+
# Create expression and associate with both filters
|
|
2697
|
+
expression = IPFabricFilterExpression.objects.create(
|
|
2698
|
+
name="Expression with Multiple Endpoints",
|
|
2699
|
+
expression=[{"siteName": ["eq", "Site1"]}],
|
|
2700
|
+
)
|
|
2701
|
+
expression.filters.add(filter1, filter2)
|
|
2702
|
+
|
|
2703
|
+
# Create form with instance - should NOT set test_endpoint initial value
|
|
2704
|
+
form = IPFabricFilterExpressionForm(instance=expression)
|
|
2705
|
+
|
|
2706
|
+
# Verify test_endpoint initial value is NOT set
|
|
2707
|
+
self.assertIsNone(form.fields["test_endpoint"].initial)
|
|
2708
|
+
|
|
2709
|
+
def test_form_init_with_new_instance_does_not_set_initial(self):
|
|
2710
|
+
"""Test that form __init__ does not set initial values for new instances"""
|
|
2711
|
+
# Create form without instance (new expression)
|
|
2712
|
+
form = IPFabricFilterExpressionForm()
|
|
2713
|
+
|
|
2714
|
+
# Verify no initial values are set
|
|
2715
|
+
self.assertIsNone(form.fields["test_source"].initial)
|
|
2716
|
+
self.assertIsNone(form.fields["test_endpoint"].initial)
|