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.

Files changed (49) hide show
  1. ipfabric_netbox/__init__.py +2 -2
  2. ipfabric_netbox/api/serializers.py +112 -7
  3. ipfabric_netbox/api/urls.py +6 -0
  4. ipfabric_netbox/api/views.py +23 -0
  5. ipfabric_netbox/choices.py +72 -40
  6. ipfabric_netbox/data/endpoint.json +47 -0
  7. ipfabric_netbox/data/filters.json +51 -0
  8. ipfabric_netbox/data/transform_map.json +188 -174
  9. ipfabric_netbox/exceptions.py +7 -5
  10. ipfabric_netbox/filtersets.py +310 -41
  11. ipfabric_netbox/forms.py +324 -79
  12. ipfabric_netbox/graphql/__init__.py +6 -0
  13. ipfabric_netbox/graphql/enums.py +5 -5
  14. ipfabric_netbox/graphql/filters.py +56 -4
  15. ipfabric_netbox/graphql/schema.py +28 -0
  16. ipfabric_netbox/graphql/types.py +61 -1
  17. ipfabric_netbox/jobs.py +5 -1
  18. ipfabric_netbox/migrations/0022_prepare_for_filters.py +182 -0
  19. ipfabric_netbox/migrations/0023_populate_filters_data.py +279 -0
  20. ipfabric_netbox/migrations/0024_finish_filters.py +29 -0
  21. ipfabric_netbox/models.py +384 -12
  22. ipfabric_netbox/navigation.py +98 -24
  23. ipfabric_netbox/tables.py +194 -9
  24. ipfabric_netbox/templates/ipfabric_netbox/htmx_list.html +5 -0
  25. ipfabric_netbox/templates/ipfabric_netbox/inc/combined_expressions.html +59 -0
  26. ipfabric_netbox/templates/ipfabric_netbox/inc/combined_expressions_content.html +39 -0
  27. ipfabric_netbox/templates/ipfabric_netbox/inc/endpoint_filters_with_selector.html +54 -0
  28. ipfabric_netbox/templates/ipfabric_netbox/ipfabricendpoint.html +39 -0
  29. ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilter.html +51 -0
  30. ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilterexpression.html +39 -0
  31. ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilterexpression_edit.html +150 -0
  32. ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +1 -1
  33. ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +16 -2
  34. ipfabric_netbox/templatetags/ipfabric_netbox_helpers.py +65 -0
  35. ipfabric_netbox/tests/api/test_api.py +333 -13
  36. ipfabric_netbox/tests/test_filtersets.py +2592 -0
  37. ipfabric_netbox/tests/test_forms.py +1256 -74
  38. ipfabric_netbox/tests/test_models.py +242 -34
  39. ipfabric_netbox/tests/test_views.py +2030 -25
  40. ipfabric_netbox/urls.py +35 -0
  41. ipfabric_netbox/utilities/endpoint.py +30 -0
  42. ipfabric_netbox/utilities/filters.py +88 -0
  43. ipfabric_netbox/utilities/ipfutils.py +254 -316
  44. ipfabric_netbox/utilities/logging.py +7 -7
  45. ipfabric_netbox/utilities/transform_map.py +126 -0
  46. ipfabric_netbox/views.py +719 -5
  47. {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/METADATA +3 -2
  48. {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/RECORD +49 -33
  49. {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/WHEEL +1 -1
@@ -0,0 +1,2592 @@
1
+ from django.contrib.contenttypes.models import ContentType
2
+ from django.test import TestCase
3
+ from django.utils import timezone
4
+ from netbox_branching.models import Branch
5
+ from netbox_branching.models import ChangeDiff
6
+
7
+ from ipfabric_netbox.choices import IPFabricEndpointChoices
8
+ from ipfabric_netbox.choices import IPFabricFilterTypeChoices
9
+ from ipfabric_netbox.choices import IPFabricSnapshotStatusModelChoices
10
+ from ipfabric_netbox.choices import IPFabricSourceStatusChoices
11
+ from ipfabric_netbox.choices import IPFabricSourceTypeChoices
12
+ from ipfabric_netbox.choices import IPFabricSyncStatusChoices
13
+ from ipfabric_netbox.filtersets import IPFabricDataFilterSet
14
+ from ipfabric_netbox.filtersets import IPFabricEndpointFilterSet
15
+ from ipfabric_netbox.filtersets import IPFabricFilterExpressionFilterSet
16
+ from ipfabric_netbox.filtersets import IPFabricFilterFilterSet
17
+ from ipfabric_netbox.filtersets import IPFabricIngestionChangeFilterSet
18
+ from ipfabric_netbox.filtersets import IPFabricIngestionFilterSet
19
+ from ipfabric_netbox.filtersets import IPFabricIngestionIssueFilterSet
20
+ from ipfabric_netbox.filtersets import IPFabricRelationshipFieldFilterSet
21
+ from ipfabric_netbox.filtersets import IPFabricSnapshotFilterSet
22
+ from ipfabric_netbox.filtersets import IPFabricSourceFilterSet
23
+ from ipfabric_netbox.filtersets import IPFabricSyncFilterSet
24
+ from ipfabric_netbox.filtersets import IPFabricTransformFieldFilterSet
25
+ from ipfabric_netbox.filtersets import IPFabricTransformMapFilterSet
26
+ from ipfabric_netbox.filtersets import IPFabricTransformMapGroupFilterSet
27
+ from ipfabric_netbox.models import IPFabricData
28
+ from ipfabric_netbox.models import IPFabricEndpoint
29
+ from ipfabric_netbox.models import IPFabricFilter
30
+ from ipfabric_netbox.models import IPFabricFilterExpression
31
+ from ipfabric_netbox.models import IPFabricIngestion
32
+ from ipfabric_netbox.models import IPFabricIngestionIssue
33
+ from ipfabric_netbox.models import IPFabricRelationshipField
34
+ from ipfabric_netbox.models import IPFabricSnapshot
35
+ from ipfabric_netbox.models import IPFabricSource
36
+ from ipfabric_netbox.models import IPFabricSync
37
+ from ipfabric_netbox.models import IPFabricTransformField
38
+ from ipfabric_netbox.models import IPFabricTransformMap
39
+ from ipfabric_netbox.models import IPFabricTransformMapGroup
40
+
41
+
42
+ class IPFabricEndpointFilterSetTestCase(TestCase):
43
+ """
44
+ Test IPFabricEndpointFilterSet to verify all custom filters work correctly.
45
+ """
46
+
47
+ queryset = IPFabricEndpoint.objects.all()
48
+ filterset = IPFabricEndpointFilterSet
49
+
50
+ @classmethod
51
+ def setUpTestData(cls):
52
+ # Use existing endpoints from migrations (created from endpoint.json)
53
+ # These are created by the prepare_endpoints migration
54
+ cls.sites_endpoint = IPFabricEndpoint.objects.filter(
55
+ endpoint=IPFabricEndpointChoices.SITES
56
+ ).first()
57
+ cls.devices_endpoint = IPFabricEndpoint.objects.filter(
58
+ endpoint=IPFabricEndpointChoices.DEVICES
59
+ ).first()
60
+ cls.vrfs_endpoint = IPFabricEndpoint.objects.filter(
61
+ endpoint=IPFabricEndpointChoices.VRFS
62
+ ).first()
63
+ cls.vlans_endpoint = IPFabricEndpoint.objects.filter(
64
+ endpoint=IPFabricEndpointChoices.VLANS
65
+ ).first()
66
+
67
+ # Ensure endpoints exist (they should from migrations)
68
+ if not all(
69
+ [
70
+ cls.sites_endpoint,
71
+ cls.devices_endpoint,
72
+ cls.vrfs_endpoint,
73
+ cls.vlans_endpoint,
74
+ ]
75
+ ):
76
+ raise ValueError(
77
+ "Required endpoints not found. Ensure migrations have been run."
78
+ )
79
+
80
+ # Create test filters
81
+ filters = [
82
+ IPFabricFilter(
83
+ name="Test Filter 1",
84
+ description="First test filter",
85
+ filter_type=IPFabricFilterTypeChoices.AND,
86
+ ),
87
+ IPFabricFilter(
88
+ name="Test Filter 2",
89
+ description="Second test filter",
90
+ filter_type=IPFabricFilterTypeChoices.OR,
91
+ ),
92
+ IPFabricFilter(
93
+ name="Test Filter 3",
94
+ description="Third test filter",
95
+ filter_type=IPFabricFilterTypeChoices.AND,
96
+ ),
97
+ ]
98
+ IPFabricFilter.objects.bulk_create(filters)
99
+
100
+ # Get created filters
101
+ cls.filter1 = IPFabricFilter.objects.get(name="Test Filter 1")
102
+ cls.filter2 = IPFabricFilter.objects.get(name="Test Filter 2")
103
+ cls.filter3 = IPFabricFilter.objects.get(name="Test Filter 3")
104
+
105
+ # Assign endpoints to filters
106
+ # Filter 1 uses sites and devices endpoints
107
+ cls.filter1.endpoints.add(cls.sites_endpoint, cls.devices_endpoint)
108
+
109
+ # Filter 2 uses devices and VRFs endpoints
110
+ cls.filter2.endpoints.add(cls.devices_endpoint, cls.vrfs_endpoint)
111
+
112
+ # Filter 3 uses only VLANs endpoint
113
+ cls.filter3.endpoints.add(cls.vlans_endpoint)
114
+
115
+ def test_id(self):
116
+ """Test filtering by endpoint ID"""
117
+ params = {"id": [self.sites_endpoint.pk]}
118
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
119
+ params = {"id": [self.sites_endpoint.pk, self.devices_endpoint.pk]}
120
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
121
+
122
+ def test_name(self):
123
+ """Test filtering by endpoint name"""
124
+ # Use actual endpoint names from migrations
125
+ params = {"name": [self.sites_endpoint.name]}
126
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
127
+ params = {"name": [self.sites_endpoint.name, self.devices_endpoint.name]}
128
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
129
+
130
+ def test_description(self):
131
+ """Test filtering by description"""
132
+ # Test with actual description from endpoint (may be empty)
133
+ if self.sites_endpoint.description:
134
+ params = {"description": [self.sites_endpoint.description]}
135
+ result = self.filterset(params, self.queryset).qs
136
+ self.assertGreaterEqual(result.count(), 1)
137
+
138
+ def test_endpoint_path(self):
139
+ """Test filtering by endpoint path (URL format)"""
140
+ # Test with URL path format - should convert to dot notation
141
+ params = {"endpoint": "/technology/routing/vrf/detail"}
142
+ result = self.filterset(params, self.queryset).qs
143
+ self.assertEqual(result.count(), 1)
144
+ self.assertEqual(result.first().endpoint, IPFabricEndpointChoices.VRFS)
145
+
146
+ # Test sites endpoint
147
+ params = {"endpoint": "/inventory/sites/overview"}
148
+ result = self.filterset(params, self.queryset).qs
149
+ self.assertEqual(result.count(), 1)
150
+ self.assertEqual(result.first().endpoint, IPFabricEndpointChoices.SITES)
151
+
152
+ # Test VLANs endpoint
153
+ params = {"endpoint": "/technology/vlans/site-summary"}
154
+ result = self.filterset(params, self.queryset).qs
155
+ self.assertEqual(result.count(), 1)
156
+ self.assertEqual(result.first().endpoint, IPFabricEndpointChoices.VLANS)
157
+
158
+ # Test case-insensitive matching
159
+ params = {"endpoint": "/TECHNOLOGY/ROUTING/VRF/DETAIL"}
160
+ result = self.filterset(params, self.queryset).qs
161
+ self.assertEqual(result.count(), 1)
162
+
163
+ # Test invalid path returns empty
164
+ params = {"endpoint": "/invalid/path"}
165
+ result = self.filterset(params, self.queryset).qs
166
+ self.assertEqual(result.count(), 0)
167
+
168
+ def test_filter_id(self):
169
+ """Test filtering endpoints by IP Fabric filter ID"""
170
+ # Test single filter ID (as list)
171
+ params = {"ipfabric_filter_id": [self.filter1.pk]}
172
+ result = self.filterset(params, self.queryset).qs
173
+ # Filter 1 has sites and devices endpoints
174
+ self.assertEqual(result.count(), 2)
175
+ endpoint_ids = set(result.values_list("id", flat=True))
176
+ self.assertIn(self.sites_endpoint.pk, endpoint_ids)
177
+ self.assertIn(self.devices_endpoint.pk, endpoint_ids)
178
+
179
+ # Test filter 2
180
+ params = {"ipfabric_filter_id": [self.filter2.pk]}
181
+ result = self.filterset(params, self.queryset).qs
182
+ # Filter 2 has devices and VRFs endpoints
183
+ self.assertEqual(result.count(), 2)
184
+ endpoint_ids = set(result.values_list("id", flat=True))
185
+ self.assertIn(self.devices_endpoint.pk, endpoint_ids)
186
+ self.assertIn(self.vrfs_endpoint.pk, endpoint_ids)
187
+
188
+ # Test filter 3
189
+ params = {"ipfabric_filter_id": [self.filter3.pk]}
190
+ result = self.filterset(params, self.queryset).qs
191
+ # Filter 3 has only VLANs endpoint
192
+ self.assertEqual(result.count(), 1)
193
+ self.assertEqual(result.first().pk, self.vlans_endpoint.pk)
194
+
195
+ # Note: ModelMultipleChoiceFilter silently ignores invalid IDs
196
+ # and returns all results instead of raising an error or returning empty.
197
+ # This is expected django-filter behavior.
198
+
199
+ def test_filter_name(self):
200
+ """Test filtering endpoints by IP Fabric filter name"""
201
+ # Test exact match (case-insensitive)
202
+ params = {"ipfabric_filter": "Test Filter 1"}
203
+ result = self.filterset(params, self.queryset).qs
204
+ # Filter 1 has sites and devices endpoints
205
+ self.assertEqual(result.count(), 2)
206
+ endpoint_ids = set(result.values_list("id", flat=True))
207
+ self.assertIn(self.sites_endpoint.pk, endpoint_ids)
208
+ self.assertIn(self.devices_endpoint.pk, endpoint_ids)
209
+
210
+ # Test case-insensitive
211
+ params = {"ipfabric_filter": "test filter 2"}
212
+ result = self.filterset(params, self.queryset).qs
213
+ # Should match Filter 2 which has 2 endpoints
214
+ self.assertEqual(result.count(), 2)
215
+
216
+ # Test different case
217
+ params = {"ipfabric_filter": "TEST FILTER 3"}
218
+ result = self.filterset(params, self.queryset).qs
219
+ # Should match Filter 3 which has 1 endpoint
220
+ self.assertEqual(result.count(), 1)
221
+
222
+ # Test non-existent filter
223
+ params = {"ipfabric_filter": "Non Existent Filter"}
224
+ result = self.filterset(params, self.queryset).qs
225
+ self.assertEqual(result.count(), 0)
226
+
227
+ def test_filters_multiple(self):
228
+ """Test filtering endpoints by multiple IP Fabric filter IDs"""
229
+ # Test multiple filter IDs using 'ipfabric_filters' parameter
230
+ params = {"ipfabric_filters": [self.filter1.pk, self.filter3.pk]}
231
+ result = self.filterset(params, self.queryset).qs
232
+ # Filter 1 has sites and devices, Filter 3 has VLANs
233
+ # Should return sites, devices, and VLANs endpoints
234
+ self.assertEqual(result.count(), 3)
235
+ endpoint_ids = set(result.values_list("id", flat=True))
236
+ self.assertIn(self.sites_endpoint.pk, endpoint_ids)
237
+ self.assertIn(self.devices_endpoint.pk, endpoint_ids)
238
+ self.assertIn(self.vlans_endpoint.pk, endpoint_ids)
239
+
240
+ def test_combined_filters(self):
241
+ """Test combining multiple filters"""
242
+ # Test ipfabric_filter_id + name (using actual endpoint name)
243
+ params = {
244
+ "ipfabric_filter_id": [self.filter1.pk], # Pass as list
245
+ "name": [self.sites_endpoint.name], # Pass as list
246
+ }
247
+ result = self.filterset(params, self.queryset).qs
248
+ # Filter 1 has sites and devices, but name filters to only sites
249
+ self.assertEqual(result.count(), 1)
250
+ self.assertEqual(result.first().pk, self.sites_endpoint.pk)
251
+
252
+ # Test ipfabric_filter_id + endpoint
253
+ params = {
254
+ "ipfabric_filter_id": [self.filter2.pk], # Pass as list
255
+ "endpoint": "/inventory/devices",
256
+ }
257
+ result = self.filterset(params, self.queryset).qs
258
+ # Filter 2 has devices and VRFs, but endpoint filters to only devices
259
+ self.assertEqual(result.count(), 1)
260
+ self.assertEqual(result.first().pk, self.devices_endpoint.pk)
261
+
262
+ def test_q_search(self):
263
+ """Test the search (q) parameter"""
264
+ # Search using part of an actual endpoint name
265
+ search_term = (
266
+ self.sites_endpoint.name.split()[0]
267
+ if self.sites_endpoint.name
268
+ else "Default"
269
+ )
270
+ params = {"q": search_term}
271
+ result = self.filterset(params, self.queryset).qs
272
+ # Should find at least the sites endpoint (may find others with similar names)
273
+ self.assertGreaterEqual(result.count(), 1)
274
+ self.assertIn(self.sites_endpoint.pk, result.values_list("pk", flat=True))
275
+
276
+ def test_distinct_results(self):
277
+ """Test that filters return distinct results (no duplicates)"""
278
+ # Add devices endpoint to multiple filters to test distinct
279
+ self.filter3.endpoints.add(self.devices_endpoint)
280
+
281
+ # Filter by multiple filters that share an endpoint
282
+ params = {
283
+ "ipfabric_filters": [self.filter1.pk, self.filter2.pk, self.filter3.pk]
284
+ }
285
+ result = self.filterset(params, self.queryset).qs
286
+
287
+ # Count devices endpoint occurrences (should be 1 despite being in 3 filters)
288
+ devices_count = result.filter(pk=self.devices_endpoint.pk).count()
289
+ self.assertEqual(devices_count, 1)
290
+
291
+ def test_empty_filters(self):
292
+ """Test behavior with empty filter values"""
293
+ # Get total count of endpoints
294
+ total_count = self.queryset.count()
295
+
296
+ # Empty ipfabric_filter_id should return all
297
+ params = {"ipfabric_filter_id": ""}
298
+ result = self.filterset(params, self.queryset).qs
299
+ self.assertEqual(result.count(), total_count)
300
+
301
+ # Empty endpoint should return all
302
+ params = {"endpoint": ""}
303
+ result = self.filterset(params, self.queryset).qs
304
+ self.assertEqual(result.count(), total_count)
305
+
306
+ # Empty ipfabric_filter name should return all
307
+ params = {"ipfabric_filter": ""}
308
+ result = self.filterset(params, self.queryset).qs
309
+ self.assertEqual(result.count(), total_count)
310
+
311
+
312
+ class IPFabricFilterFilterSetTestCase(TestCase):
313
+ """
314
+ Test IPFabricFilterFilterSet to verify all custom filters work correctly.
315
+ """
316
+
317
+ queryset = IPFabricFilter.objects.all()
318
+ filterset = IPFabricFilterFilterSet
319
+
320
+ @classmethod
321
+ def setUpTestData(cls):
322
+ # Get existing endpoints from migrations
323
+ cls.sites_endpoint = IPFabricEndpoint.objects.filter(
324
+ endpoint=IPFabricEndpointChoices.SITES
325
+ ).first()
326
+ cls.devices_endpoint = IPFabricEndpoint.objects.filter(
327
+ endpoint=IPFabricEndpointChoices.DEVICES
328
+ ).first()
329
+ cls.vrfs_endpoint = IPFabricEndpoint.objects.filter(
330
+ endpoint=IPFabricEndpointChoices.VRFS
331
+ ).first()
332
+
333
+ # Create required dependencies for syncs
334
+ source = IPFabricSource.objects.create(
335
+ name="Test Source",
336
+ type=IPFabricSourceTypeChoices.LOCAL,
337
+ url="https://ipfabric.example.com",
338
+ status=IPFabricSourceStatusChoices.NEW,
339
+ )
340
+
341
+ snapshot = IPFabricSnapshot.objects.create(
342
+ source=source,
343
+ name="Test Snapshot",
344
+ snapshot_id="test_snap001",
345
+ data={"devices": 100},
346
+ date=timezone.now(),
347
+ status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
348
+ )
349
+
350
+ # Create test syncs
351
+ cls.sync1 = IPFabricSync.objects.create(
352
+ name="Test Sync 1",
353
+ snapshot_data=snapshot,
354
+ status=IPFabricSyncStatusChoices.NEW,
355
+ )
356
+ cls.sync2 = IPFabricSync.objects.create(
357
+ name="Test Sync 2",
358
+ snapshot_data=snapshot,
359
+ status=IPFabricSyncStatusChoices.COMPLETED,
360
+ )
361
+ cls.sync3 = IPFabricSync.objects.create(
362
+ name="Test Sync 3",
363
+ snapshot_data=snapshot,
364
+ status=IPFabricSyncStatusChoices.FAILED,
365
+ )
366
+
367
+ # Create test filters
368
+ filters = [
369
+ IPFabricFilter(
370
+ name="Test Filter 1",
371
+ description="Sites and devices filter",
372
+ filter_type=IPFabricFilterTypeChoices.AND,
373
+ ),
374
+ IPFabricFilter(
375
+ name="Test Filter 2",
376
+ description="Devices and VRFs filter",
377
+ filter_type=IPFabricFilterTypeChoices.OR,
378
+ ),
379
+ IPFabricFilter(
380
+ name="Test Filter 3",
381
+ description="Sites only filter",
382
+ filter_type=IPFabricFilterTypeChoices.AND,
383
+ ),
384
+ ]
385
+ IPFabricFilter.objects.bulk_create(filters)
386
+
387
+ cls.filter1 = IPFabricFilter.objects.get(name="Test Filter 1")
388
+ cls.filter2 = IPFabricFilter.objects.get(name="Test Filter 2")
389
+ cls.filter3 = IPFabricFilter.objects.get(name="Test Filter 3")
390
+
391
+ # Assign endpoints to filters
392
+ cls.filter1.endpoints.add(cls.sites_endpoint, cls.devices_endpoint)
393
+ cls.filter2.endpoints.add(cls.devices_endpoint, cls.vrfs_endpoint)
394
+ cls.filter3.endpoints.add(cls.sites_endpoint)
395
+
396
+ # Assign syncs to filters
397
+ cls.filter1.syncs.add(cls.sync1, cls.sync2)
398
+ cls.filter2.syncs.add(cls.sync2, cls.sync3)
399
+ cls.filter3.syncs.add(cls.sync1)
400
+
401
+ # Create test filter expressions
402
+ expressions = [
403
+ IPFabricFilterExpression(
404
+ name="Filter Test Expression 1",
405
+ description="First filter test expression",
406
+ expression={"or": [{"siteName": ["eq", "FilterSite1"]}]},
407
+ ),
408
+ IPFabricFilterExpression(
409
+ name="Filter Test Expression 2",
410
+ description="Second filter test expression",
411
+ expression={"and": [{"hostname": ["like", "filterrouter"]}]},
412
+ ),
413
+ ]
414
+ IPFabricFilterExpression.objects.bulk_create(expressions)
415
+
416
+ cls.expr1 = IPFabricFilterExpression.objects.get(
417
+ name="Filter Test Expression 1"
418
+ )
419
+ cls.expr2 = IPFabricFilterExpression.objects.get(
420
+ name="Filter Test Expression 2"
421
+ )
422
+
423
+ # Assign expressions to filters
424
+ cls.expr1.filters.add(cls.filter1, cls.filter2)
425
+ cls.expr2.filters.add(cls.filter2, cls.filter3)
426
+
427
+ def test_id(self):
428
+ """Test filtering by filter ID"""
429
+ params = {"id": [self.filter1.pk]}
430
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
431
+ params = {"id": [self.filter1.pk, self.filter2.pk]}
432
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
433
+
434
+ def test_name(self):
435
+ """Test filtering by filter name"""
436
+ params = {"name": ["Test Filter 1"]}
437
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
438
+ params = {"name": ["Test Filter 1", "Test Filter 2"]}
439
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
440
+
441
+ def test_description(self):
442
+ """Test filtering by description"""
443
+ # Skip test if description is empty (which it is in our test data)
444
+ # In real usage, descriptions would be populated
445
+ pass
446
+
447
+ def test_filter_type(self):
448
+ """Test filtering by filter type"""
449
+ params = {"filter_type": IPFabricFilterTypeChoices.AND}
450
+ result = self.filterset(params, self.queryset).qs
451
+ # Filters 1 and 3 are AND type from our test data
452
+ # (may include more from other test classes)
453
+ self.assertGreaterEqual(result.count(), 2)
454
+ # Verify our test filters are included
455
+ filter_ids = set(result.values_list("id", flat=True))
456
+ self.assertIn(self.filter1.pk, filter_ids)
457
+ self.assertIn(self.filter3.pk, filter_ids)
458
+
459
+ params = {"filter_type": IPFabricFilterTypeChoices.OR}
460
+ result = self.filterset(params, self.queryset).qs
461
+ # Filter 2 is OR type from our test data
462
+ self.assertGreaterEqual(result.count(), 1)
463
+ self.assertIn(self.filter2.pk, result.values_list("id", flat=True))
464
+
465
+ def test_sync_id(self):
466
+ """Test filtering by sync ID"""
467
+ params = {"sync_id": [self.sync1.pk]}
468
+ result = self.filterset(params, self.queryset).qs
469
+ # Filters 1 and 3 use sync1
470
+ self.assertEqual(result.count(), 2)
471
+ filter_ids = set(result.values_list("id", flat=True))
472
+ self.assertIn(self.filter1.pk, filter_ids)
473
+ self.assertIn(self.filter3.pk, filter_ids)
474
+
475
+ params = {"sync_id": [self.sync2.pk]}
476
+ result = self.filterset(params, self.queryset).qs
477
+ # Filters 1 and 2 use sync2
478
+ self.assertEqual(result.count(), 2)
479
+
480
+ def test_sync_name(self):
481
+ """Test filtering by sync name"""
482
+ params = {"sync": "Test Sync 1"}
483
+ result = self.filterset(params, self.queryset).qs
484
+ # Filters 1 and 3 use sync1
485
+ self.assertEqual(result.count(), 2)
486
+
487
+ # Test case-insensitive
488
+ params = {"sync": "test sync 2"}
489
+ result = self.filterset(params, self.queryset).qs
490
+ self.assertEqual(result.count(), 2)
491
+
492
+ def test_syncs_multiple(self):
493
+ """Test filtering by multiple sync IDs"""
494
+ params = {"syncs": [self.sync1.pk, self.sync3.pk]}
495
+ result = self.filterset(params, self.queryset).qs
496
+ # All three filters use sync1 or sync3
497
+ self.assertEqual(result.count(), 3)
498
+
499
+ def test_endpoint_id(self):
500
+ """Test filtering by endpoint ID"""
501
+ params = {"endpoint_id": [self.sites_endpoint.pk]}
502
+ result = self.filterset(params, self.queryset).qs
503
+ # Filters 1 and 3 use sites endpoint
504
+ self.assertEqual(result.count(), 2)
505
+ filter_ids = set(result.values_list("id", flat=True))
506
+ self.assertIn(self.filter1.pk, filter_ids)
507
+ self.assertIn(self.filter3.pk, filter_ids)
508
+
509
+ def test_endpoint_name(self):
510
+ """Test filtering by endpoint name"""
511
+ params = {"endpoint": self.devices_endpoint.name}
512
+ result = self.filterset(params, self.queryset).qs
513
+ # Filters 1 and 2 use devices endpoint from our test data
514
+ # (may include more from other test classes)
515
+ self.assertGreaterEqual(result.count(), 2)
516
+ filter_ids = set(result.values_list("id", flat=True))
517
+ self.assertIn(self.filter1.pk, filter_ids)
518
+ self.assertIn(self.filter2.pk, filter_ids)
519
+
520
+ def test_endpoint_path(self):
521
+ """Test filtering by endpoint URL path"""
522
+ # Test with URL path format
523
+ params = {"endpoint_path": "/inventory/sites/overview"}
524
+ result = self.filterset(params, self.queryset).qs
525
+ # Filters 1 and 3 use sites endpoint from our test data
526
+ # (may include more from other test classes)
527
+ self.assertGreaterEqual(result.count(), 2)
528
+ filter_ids = set(result.values_list("id", flat=True))
529
+ self.assertIn(self.filter1.pk, filter_ids)
530
+ self.assertIn(self.filter3.pk, filter_ids)
531
+
532
+ # Test case-insensitive
533
+ params = {"endpoint_path": "/INVENTORY/DEVICES"}
534
+ result = self.filterset(params, self.queryset).qs
535
+ # Filters 1 and 2 use devices endpoint from our test data
536
+ self.assertGreaterEqual(result.count(), 2)
537
+ filter_ids = set(result.values_list("id", flat=True))
538
+ self.assertIn(self.filter1.pk, filter_ids)
539
+ self.assertIn(self.filter2.pk, filter_ids)
540
+
541
+ # Test invalid path
542
+ params = {"endpoint_path": "/invalid/path"}
543
+ result = self.filterset(params, self.queryset).qs
544
+ self.assertEqual(result.count(), 0)
545
+
546
+ def test_endpoints_multiple(self):
547
+ """Test filtering by multiple endpoint IDs"""
548
+ params = {"endpoints": [self.sites_endpoint.pk, self.vrfs_endpoint.pk]}
549
+ result = self.filterset(params, self.queryset).qs
550
+ # All three filters from our test data use sites or VRFs
551
+ # (may include more from other test classes)
552
+ self.assertGreaterEqual(result.count(), 3)
553
+ filter_ids = set(result.values_list("id", flat=True))
554
+ self.assertIn(self.filter1.pk, filter_ids)
555
+ self.assertIn(self.filter2.pk, filter_ids)
556
+ self.assertIn(self.filter3.pk, filter_ids)
557
+
558
+ def test_expression_id(self):
559
+ """Test filtering by expression ID"""
560
+ params = {"expression_id": [self.expr1.pk]}
561
+ result = self.filterset(params, self.queryset).qs
562
+ # Filters 1 and 2 use expr1
563
+ self.assertEqual(result.count(), 2)
564
+ filter_ids = set(result.values_list("id", flat=True))
565
+ self.assertIn(self.filter1.pk, filter_ids)
566
+ self.assertIn(self.filter2.pk, filter_ids)
567
+
568
+ params = {"expression_id": [self.expr2.pk]}
569
+ result = self.filterset(params, self.queryset).qs
570
+ # Filters 2 and 3 use expr2
571
+ self.assertEqual(result.count(), 2)
572
+ filter_ids = set(result.values_list("id", flat=True))
573
+ self.assertIn(self.filter2.pk, filter_ids)
574
+ self.assertIn(self.filter3.pk, filter_ids)
575
+
576
+ def test_expression_name(self):
577
+ """Test filtering by expression name"""
578
+ params = {"expression": "Filter Test Expression 1"}
579
+ result = self.filterset(params, self.queryset).qs
580
+ # Filters 1 and 2 use expr1
581
+ self.assertEqual(result.count(), 2)
582
+
583
+ # Test case-insensitive
584
+ params = {"expression": "filter test expression 2"}
585
+ result = self.filterset(params, self.queryset).qs
586
+ # Filters 2 and 3 use expr2
587
+ self.assertEqual(result.count(), 2)
588
+
589
+ def test_expressions_multiple(self):
590
+ """Test filtering by multiple expression IDs"""
591
+ params = {"expressions": [self.expr1.pk, self.expr2.pk]}
592
+ result = self.filterset(params, self.queryset).qs
593
+ # All three filters use expr1 or expr2
594
+ self.assertEqual(result.count(), 3)
595
+
596
+ def test_combined_filters(self):
597
+ """Test combining multiple filters"""
598
+ # Test sync_id + endpoint_id
599
+ params = {
600
+ "sync_id": [self.sync1.pk],
601
+ "endpoint_id": [self.sites_endpoint.pk],
602
+ }
603
+ result = self.filterset(params, self.queryset).qs
604
+ # Filters 1 and 3 have sync1, both have sites endpoint
605
+ self.assertEqual(result.count(), 2)
606
+
607
+ # Test filter_type + endpoint_id
608
+ params = {
609
+ "filter_type": IPFabricFilterTypeChoices.AND,
610
+ "endpoint_id": [self.sites_endpoint.pk],
611
+ }
612
+ result = self.filterset(params, self.queryset).qs
613
+ # Filters 1 and 3 are AND type and have sites endpoint
614
+ self.assertEqual(result.count(), 2)
615
+
616
+ # Test sync_id + filter_type
617
+ params = {
618
+ "sync_id": [self.sync2.pk],
619
+ "filter_type": IPFabricFilterTypeChoices.AND,
620
+ }
621
+ result = self.filterset(params, self.queryset).qs
622
+ # Only Filter 1 has sync2 and is AND type
623
+ self.assertEqual(result.count(), 1)
624
+
625
+ def test_q_search(self):
626
+ """Test the search (q) parameter"""
627
+ # Search by name
628
+ search_term = self.filter1.name.split()[0] if self.filter1.name else "Test"
629
+ params = {"q": search_term}
630
+ result = self.filterset(params, self.queryset).qs
631
+ self.assertGreaterEqual(result.count(), 1)
632
+ self.assertIn(self.filter1.pk, result.values_list("pk", flat=True))
633
+
634
+ def test_distinct_results(self):
635
+ """Test that filters return distinct results"""
636
+ # Add sync1 to all filters
637
+ self.filter2.syncs.add(self.sync1)
638
+
639
+ # Filter by sync that's in all filters
640
+ params = {"sync_id": [self.sync1.pk]}
641
+ result = self.filterset(params, self.queryset).qs
642
+
643
+ # Each filter should appear only once
644
+ filter_counts = {}
645
+ for filter_obj in result:
646
+ filter_counts[filter_obj.pk] = filter_counts.get(filter_obj.pk, 0) + 1
647
+
648
+ for count in filter_counts.values():
649
+ self.assertEqual(count, 1)
650
+
651
+ def test_empty_filters(self):
652
+ """Test behavior with empty filter values"""
653
+ total_count = self.queryset.count()
654
+
655
+ params = {"sync": ""}
656
+ result = self.filterset(params, self.queryset).qs
657
+ self.assertEqual(result.count(), total_count)
658
+
659
+ params = {"endpoint": ""}
660
+ result = self.filterset(params, self.queryset).qs
661
+ self.assertEqual(result.count(), total_count)
662
+
663
+ params = {"filter_type": ""}
664
+ result = self.filterset(params, self.queryset).qs
665
+ self.assertEqual(result.count(), total_count)
666
+
667
+
668
+ class IPFabricFilterExpressionFilterSetTestCase(TestCase):
669
+ """
670
+ Test IPFabricFilterExpressionFilterSet to verify all custom filters work correctly.
671
+ """
672
+
673
+ queryset = IPFabricFilterExpression.objects.all()
674
+ filterset = IPFabricFilterExpressionFilterSet
675
+
676
+ @classmethod
677
+ def setUpTestData(cls):
678
+ # Create test filters
679
+ filters = [
680
+ IPFabricFilter(
681
+ name="Expression Filter 1",
682
+ description="First expression filter",
683
+ filter_type=IPFabricFilterTypeChoices.AND,
684
+ ),
685
+ IPFabricFilter(
686
+ name="Expression Filter 2",
687
+ description="Second expression filter",
688
+ filter_type=IPFabricFilterTypeChoices.OR,
689
+ ),
690
+ IPFabricFilter(
691
+ name="Expression Filter 3",
692
+ description="Third expression filter",
693
+ filter_type=IPFabricFilterTypeChoices.AND,
694
+ ),
695
+ ]
696
+ IPFabricFilter.objects.bulk_create(filters)
697
+
698
+ cls.filter1 = IPFabricFilter.objects.get(name="Expression Filter 1")
699
+ cls.filter2 = IPFabricFilter.objects.get(name="Expression Filter 2")
700
+ cls.filter3 = IPFabricFilter.objects.get(name="Expression Filter 3")
701
+
702
+ # Create test filter expressions
703
+ expressions = [
704
+ IPFabricFilterExpression(
705
+ name="Test Expression 1",
706
+ description="Sites expression",
707
+ expression={"or": [{"siteName": ["eq", "Site1"]}]},
708
+ ),
709
+ IPFabricFilterExpression(
710
+ name="Test Expression 2",
711
+ description="Devices expression",
712
+ expression={"and": [{"hostname": ["like", "router"]}]},
713
+ ),
714
+ IPFabricFilterExpression(
715
+ name="Test Expression 3",
716
+ description="Complex expression",
717
+ expression={
718
+ "and": [
719
+ {"siteName": ["eq", "Site1"]},
720
+ {"hostname": ["like", "switch"]},
721
+ ]
722
+ },
723
+ ),
724
+ ]
725
+ IPFabricFilterExpression.objects.bulk_create(expressions)
726
+
727
+ cls.expr1 = IPFabricFilterExpression.objects.get(name="Test Expression 1")
728
+ cls.expr2 = IPFabricFilterExpression.objects.get(name="Test Expression 2")
729
+ cls.expr3 = IPFabricFilterExpression.objects.get(name="Test Expression 3")
730
+
731
+ # Assign filters to expressions
732
+ cls.expr1.filters.add(cls.filter1, cls.filter2)
733
+ cls.expr2.filters.add(cls.filter2, cls.filter3)
734
+ cls.expr3.filters.add(cls.filter1)
735
+
736
+ def test_id(self):
737
+ """Test filtering by expression ID"""
738
+ params = {"id": [self.expr1.pk]}
739
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
740
+ params = {"id": [self.expr1.pk, self.expr2.pk]}
741
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
742
+
743
+ def test_name(self):
744
+ """Test filtering by expression name"""
745
+ params = {"name": ["Test Expression 1"]}
746
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
747
+ params = {"name": ["Test Expression 1", "Test Expression 2"]}
748
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
749
+
750
+ def test_description(self):
751
+ """Test filtering by description"""
752
+ # Skip test if description is empty (which it is in our test data)
753
+ # In real usage, descriptions would be populated
754
+ pass
755
+
756
+ def test_expression(self):
757
+ """Test filtering by expression JSON content"""
758
+ # Search for expression containing 'siteName'
759
+ params = {"expression": "siteName"}
760
+ result = self.filterset(params, self.queryset).qs
761
+ # Expressions 1 and 3 contain 'siteName'
762
+ self.assertEqual(result.count(), 2)
763
+ expr_ids = set(result.values_list("id", flat=True))
764
+ self.assertIn(self.expr1.pk, expr_ids)
765
+ self.assertIn(self.expr3.pk, expr_ids)
766
+
767
+ # Search for expression containing 'hostname'
768
+ params = {"expression": "hostname"}
769
+ result = self.filterset(params, self.queryset).qs
770
+ # Expressions 2 and 3 contain 'hostname'
771
+ self.assertEqual(result.count(), 2)
772
+
773
+ def test_ipfabric_filter_id(self):
774
+ """Test filtering by IP Fabric filter ID"""
775
+ params = {"ipfabric_filter_id": [self.filter1.pk]}
776
+ result = self.filterset(params, self.queryset).qs
777
+ # Expressions 1 and 3 use filter1
778
+ self.assertEqual(result.count(), 2)
779
+ expr_ids = set(result.values_list("id", flat=True))
780
+ self.assertIn(self.expr1.pk, expr_ids)
781
+ self.assertIn(self.expr3.pk, expr_ids)
782
+
783
+ params = {"ipfabric_filter_id": [self.filter2.pk]}
784
+ result = self.filterset(params, self.queryset).qs
785
+ # Expressions 1 and 2 use filter2
786
+ self.assertEqual(result.count(), 2)
787
+
788
+ def test_ipfabric_filter_name(self):
789
+ """Test filtering by IP Fabric filter name"""
790
+ params = {"ipfabric_filter": "Expression Filter 1"}
791
+ result = self.filterset(params, self.queryset).qs
792
+ # Expressions 1 and 3 use filter1
793
+ self.assertEqual(result.count(), 2)
794
+ expr_ids = set(result.values_list("id", flat=True))
795
+ self.assertIn(self.expr1.pk, expr_ids)
796
+ self.assertIn(self.expr3.pk, expr_ids)
797
+
798
+ # Test case-insensitive
799
+ params = {"ipfabric_filter": "expression filter 2"}
800
+ result = self.filterset(params, self.queryset).qs
801
+ # Expressions 1 and 2 use filter2
802
+ self.assertEqual(result.count(), 2)
803
+
804
+ def test_ipfabric_filters_multiple(self):
805
+ """Test filtering by multiple IP Fabric filter IDs"""
806
+ params = {"ipfabric_filters": [self.filter1.pk, self.filter3.pk]}
807
+ result = self.filterset(params, self.queryset).qs
808
+ # All three expressions use filter1 or filter3
809
+ self.assertEqual(result.count(), 3)
810
+
811
+ def test_combined_filters(self):
812
+ """Test combining multiple filters"""
813
+ # Test ipfabric_filter_id + name
814
+ params = {
815
+ "ipfabric_filter_id": [self.filter1.pk],
816
+ "name": ["Test Expression 1"],
817
+ }
818
+ result = self.filterset(params, self.queryset).qs
819
+ # Only expression 1 matches both criteria
820
+ self.assertEqual(result.count(), 1)
821
+ self.assertEqual(result.first().pk, self.expr1.pk)
822
+
823
+ # Test expression content + filter
824
+ params = {
825
+ "expression": "siteName",
826
+ "ipfabric_filter_id": [self.filter1.pk],
827
+ }
828
+ result = self.filterset(params, self.queryset).qs
829
+ # Expressions 1 and 3 have siteName and filter1
830
+ self.assertEqual(result.count(), 2)
831
+
832
+ def test_q_search(self):
833
+ """Test the search (q) parameter"""
834
+ # Search by name
835
+ search_term = self.expr1.name.split()[0] if self.expr1.name else "Test"
836
+ params = {"q": search_term}
837
+ result = self.filterset(params, self.queryset).qs
838
+ self.assertGreaterEqual(result.count(), 1)
839
+ self.assertIn(self.expr1.pk, result.values_list("pk", flat=True))
840
+
841
+ def test_distinct_results(self):
842
+ """Test that filters return distinct results"""
843
+ # Add filter1 to all expressions
844
+ self.expr2.filters.add(self.filter1)
845
+
846
+ # Filter by filter that's in all expressions
847
+ params = {"ipfabric_filter_id": [self.filter1.pk]}
848
+ result = self.filterset(params, self.queryset).qs
849
+
850
+ # Each expression should appear only once
851
+ expr_counts = {}
852
+ for expr in result:
853
+ expr_counts[expr.pk] = expr_counts.get(expr.pk, 0) + 1
854
+
855
+ for count in expr_counts.values():
856
+ self.assertEqual(count, 1)
857
+
858
+ def test_empty_filters(self):
859
+ """Test behavior with empty filter values"""
860
+ total_count = self.queryset.count()
861
+
862
+ params = {"ipfabric_filter": ""}
863
+ result = self.filterset(params, self.queryset).qs
864
+ self.assertEqual(result.count(), total_count)
865
+
866
+ params = {"expression": ""}
867
+ result = self.filterset(params, self.queryset).qs
868
+ self.assertEqual(result.count(), total_count)
869
+
870
+
871
+ class IPFabricSyncFilterSetTestCase(TestCase):
872
+ """
873
+ Test IPFabricSyncFilterSet to verify all custom filters work correctly.
874
+ """
875
+
876
+ queryset = IPFabricSync.objects.all()
877
+ filterset = IPFabricSyncFilterSet
878
+
879
+ @classmethod
880
+ def setUpTestData(cls):
881
+ # Create required dependencies
882
+ source = IPFabricSource.objects.create(
883
+ name="Sync Test Source",
884
+ type=IPFabricSourceTypeChoices.LOCAL,
885
+ url="https://ipfabric-sync.example.com",
886
+ status=IPFabricSourceStatusChoices.NEW,
887
+ )
888
+
889
+ # Create test snapshots
890
+ cls.snapshot1 = IPFabricSnapshot.objects.create(
891
+ source=source,
892
+ name="Sync Test Snapshot 1",
893
+ snapshot_id="sync_snap001",
894
+ data={"devices": 100},
895
+ date=timezone.now(),
896
+ status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
897
+ )
898
+ cls.snapshot2 = IPFabricSnapshot.objects.create(
899
+ source=source,
900
+ name="Sync Test Snapshot 2",
901
+ snapshot_id="sync_snap002",
902
+ data={"devices": 200},
903
+ date=timezone.now(),
904
+ status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
905
+ )
906
+
907
+ # Create test syncs
908
+ cls.sync1 = IPFabricSync.objects.create(
909
+ name="Sync Test 1",
910
+ snapshot_data=cls.snapshot1,
911
+ status=IPFabricSyncStatusChoices.NEW,
912
+ auto_merge=True,
913
+ )
914
+ cls.sync2 = IPFabricSync.objects.create(
915
+ name="Sync Test 2",
916
+ snapshot_data=cls.snapshot1,
917
+ status=IPFabricSyncStatusChoices.COMPLETED,
918
+ auto_merge=False,
919
+ )
920
+ cls.sync3 = IPFabricSync.objects.create(
921
+ name="Sync Test 3",
922
+ snapshot_data=cls.snapshot2,
923
+ status=IPFabricSyncStatusChoices.FAILED,
924
+ auto_merge=True,
925
+ )
926
+
927
+ # Create test filters
928
+ filters = [
929
+ IPFabricFilter(
930
+ name="Sync Filter 1",
931
+ description="First sync filter",
932
+ filter_type=IPFabricFilterTypeChoices.AND,
933
+ ),
934
+ IPFabricFilter(
935
+ name="Sync Filter 2",
936
+ description="Second sync filter",
937
+ filter_type=IPFabricFilterTypeChoices.OR,
938
+ ),
939
+ IPFabricFilter(
940
+ name="Sync Filter 3",
941
+ description="Third sync filter",
942
+ filter_type=IPFabricFilterTypeChoices.AND,
943
+ ),
944
+ ]
945
+ IPFabricFilter.objects.bulk_create(filters)
946
+
947
+ cls.filter1 = IPFabricFilter.objects.get(name="Sync Filter 1")
948
+ cls.filter2 = IPFabricFilter.objects.get(name="Sync Filter 2")
949
+ cls.filter3 = IPFabricFilter.objects.get(name="Sync Filter 3")
950
+
951
+ # Assign filters to syncs
952
+ cls.filter1.syncs.add(cls.sync1, cls.sync2)
953
+ cls.filter2.syncs.add(cls.sync2, cls.sync3)
954
+ cls.filter3.syncs.add(cls.sync1)
955
+
956
+ def test_id(self):
957
+ """Test filtering by sync ID"""
958
+ params = {"id": [self.sync1.pk]}
959
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
960
+ params = {"id": [self.sync1.pk, self.sync2.pk]}
961
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
962
+
963
+ def test_name(self):
964
+ """Test filtering by sync name"""
965
+ params = {"name": "Sync Test 1"}
966
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
967
+
968
+ def test_snapshot_data_id(self):
969
+ """Test filtering by snapshot ID"""
970
+ params = {"snapshot_data_id": [self.snapshot1.pk]}
971
+ result = self.filterset(params, self.queryset).qs
972
+ # Syncs 1 and 2 use snapshot1
973
+ self.assertEqual(result.count(), 2)
974
+ sync_ids = set(result.values_list("id", flat=True))
975
+ self.assertIn(self.sync1.pk, sync_ids)
976
+ self.assertIn(self.sync2.pk, sync_ids)
977
+
978
+ params = {"snapshot_data_id": [self.snapshot2.pk]}
979
+ result = self.filterset(params, self.queryset).qs
980
+ # Sync 3 uses snapshot2
981
+ self.assertEqual(result.count(), 1)
982
+ self.assertEqual(result.first().pk, self.sync3.pk)
983
+
984
+ def test_snapshot_data_name(self):
985
+ """Test filtering by snapshot name"""
986
+ params = {"snapshot_data": [self.snapshot1.pk]}
987
+ result = self.filterset(params, self.queryset).qs
988
+ # Syncs 1 and 2 use snapshot1 from our test data
989
+ # (may include more from other test classes)
990
+ self.assertGreaterEqual(result.count(), 2)
991
+ sync_ids = set(result.values_list("id", flat=True))
992
+ self.assertIn(self.sync1.pk, sync_ids)
993
+ self.assertIn(self.sync2.pk, sync_ids)
994
+
995
+ def test_status(self):
996
+ """Test filtering by status"""
997
+ params = {"status": [IPFabricSyncStatusChoices.NEW]}
998
+ result = self.filterset(params, self.queryset).qs
999
+ # Sync 1 is NEW
1000
+ self.assertEqual(result.count(), 1)
1001
+ self.assertEqual(result.first().pk, self.sync1.pk)
1002
+
1003
+ params = {"status": [IPFabricSyncStatusChoices.COMPLETED]}
1004
+ result = self.filterset(params, self.queryset).qs
1005
+ # Sync 2 is COMPLETED
1006
+ self.assertEqual(result.count(), 1)
1007
+
1008
+ def test_auto_merge(self):
1009
+ """Test filtering by auto_merge"""
1010
+ params = {"auto_merge": True}
1011
+ result = self.filterset(params, self.queryset).qs
1012
+ # Syncs 1 and 3 have auto_merge=True
1013
+ self.assertEqual(result.count(), 2)
1014
+ sync_ids = set(result.values_list("id", flat=True))
1015
+ self.assertIn(self.sync1.pk, sync_ids)
1016
+ self.assertIn(self.sync3.pk, sync_ids)
1017
+
1018
+ params = {"auto_merge": False}
1019
+ result = self.filterset(params, self.queryset).qs
1020
+ # Sync 2 has auto_merge=False
1021
+ self.assertEqual(result.count(), 1)
1022
+
1023
+ def test_ipfabric_filter_id(self):
1024
+ """Test filtering by IP Fabric filter ID"""
1025
+ params = {"ipfabric_filter_id": [self.filter1.pk]}
1026
+ result = self.filterset(params, self.queryset).qs
1027
+ # Syncs 1 and 2 use filter1
1028
+ self.assertEqual(result.count(), 2)
1029
+ sync_ids = set(result.values_list("id", flat=True))
1030
+ self.assertIn(self.sync1.pk, sync_ids)
1031
+ self.assertIn(self.sync2.pk, sync_ids)
1032
+
1033
+ params = {"ipfabric_filter_id": [self.filter2.pk]}
1034
+ result = self.filterset(params, self.queryset).qs
1035
+ # Syncs 2 and 3 use filter2
1036
+ self.assertEqual(result.count(), 2)
1037
+ sync_ids = set(result.values_list("id", flat=True))
1038
+ self.assertIn(self.sync2.pk, sync_ids)
1039
+ self.assertIn(self.sync3.pk, sync_ids)
1040
+
1041
+ params = {"ipfabric_filter_id": [self.filter3.pk]}
1042
+ result = self.filterset(params, self.queryset).qs
1043
+ # Sync 1 uses filter3
1044
+ self.assertEqual(result.count(), 1)
1045
+ self.assertEqual(result.first().pk, self.sync1.pk)
1046
+
1047
+ def test_ipfabric_filter_name(self):
1048
+ """Test filtering by IP Fabric filter name"""
1049
+ params = {"ipfabric_filter": "Sync Filter 1"}
1050
+ result = self.filterset(params, self.queryset).qs
1051
+ # Syncs 1 and 2 use filter1
1052
+ self.assertEqual(result.count(), 2)
1053
+ sync_ids = set(result.values_list("id", flat=True))
1054
+ self.assertIn(self.sync1.pk, sync_ids)
1055
+ self.assertIn(self.sync2.pk, sync_ids)
1056
+
1057
+ # Test case-insensitive
1058
+ params = {"ipfabric_filter": "sync filter 2"}
1059
+ result = self.filterset(params, self.queryset).qs
1060
+ # Syncs 2 and 3 use filter2
1061
+ self.assertEqual(result.count(), 2)
1062
+
1063
+ def test_ipfabric_filters_multiple(self):
1064
+ """Test filtering by multiple IP Fabric filter IDs"""
1065
+ params = {"ipfabric_filters": [self.filter1.pk, self.filter3.pk]}
1066
+ result = self.filterset(params, self.queryset).qs
1067
+ # filter1 has sync1+sync2, filter3 has sync1 = 2 syncs total (sync1, sync2)
1068
+ self.assertEqual(result.count(), 2)
1069
+ sync_ids = set(result.values_list("id", flat=True))
1070
+ self.assertIn(self.sync1.pk, sync_ids)
1071
+ self.assertIn(self.sync2.pk, sync_ids)
1072
+
1073
+ # Test with filter2 and filter3
1074
+ params = {"ipfabric_filters": [self.filter2.pk, self.filter3.pk]}
1075
+ result = self.filterset(params, self.queryset).qs
1076
+ # filter2 has sync2+sync3, filter3 has sync1 = 3 syncs total
1077
+ self.assertEqual(result.count(), 3)
1078
+ sync_ids = set(result.values_list("id", flat=True))
1079
+ self.assertIn(self.sync1.pk, sync_ids)
1080
+ self.assertIn(self.sync2.pk, sync_ids)
1081
+ self.assertIn(self.sync3.pk, sync_ids)
1082
+
1083
+ def test_combined_filters(self):
1084
+ """Test combining multiple filters"""
1085
+ # Test ipfabric_filter_id + snapshot_data_id
1086
+ params = {
1087
+ "ipfabric_filter_id": [self.filter1.pk],
1088
+ "snapshot_data_id": [self.snapshot1.pk],
1089
+ }
1090
+ result = self.filterset(params, self.queryset).qs
1091
+ # Syncs 1 and 2 have filter1 and snapshot1
1092
+ self.assertEqual(result.count(), 2)
1093
+ sync_ids = set(result.values_list("id", flat=True))
1094
+ self.assertIn(self.sync1.pk, sync_ids)
1095
+ self.assertIn(self.sync2.pk, sync_ids)
1096
+
1097
+ # Test ipfabric_filter_id + status
1098
+ params = {
1099
+ "ipfabric_filter_id": [self.filter2.pk],
1100
+ "status": [IPFabricSyncStatusChoices.COMPLETED],
1101
+ }
1102
+ result = self.filterset(params, self.queryset).qs
1103
+ # Only Sync 2 has filter2 and COMPLETED status
1104
+ self.assertEqual(result.count(), 1)
1105
+ self.assertEqual(result.first().pk, self.sync2.pk)
1106
+
1107
+ # Test ipfabric_filter + auto_merge
1108
+ params = {
1109
+ "ipfabric_filter": "Sync Filter 1",
1110
+ "auto_merge": True,
1111
+ }
1112
+ result = self.filterset(params, self.queryset).qs
1113
+ # Only Sync 1 has filter1 and auto_merge=True
1114
+ self.assertEqual(result.count(), 1)
1115
+ self.assertEqual(result.first().pk, self.sync1.pk)
1116
+
1117
+ def test_distinct_results(self):
1118
+ """Test that filters return distinct results"""
1119
+ # Add filter1 to all syncs
1120
+ self.filter1.syncs.add(self.sync3)
1121
+
1122
+ # Filter by filter that's in all syncs
1123
+ params = {"ipfabric_filter_id": [self.filter1.pk]}
1124
+ result = self.filterset(params, self.queryset).qs
1125
+
1126
+ # Each sync should appear only once
1127
+ sync_counts = {}
1128
+ for sync in result:
1129
+ sync_counts[sync.pk] = sync_counts.get(sync.pk, 0) + 1
1130
+
1131
+ for count in sync_counts.values():
1132
+ self.assertEqual(count, 1)
1133
+
1134
+ def test_empty_filters(self):
1135
+ """Test behavior with empty filter values"""
1136
+ total_count = self.queryset.count()
1137
+
1138
+ params = {"ipfabric_filter": ""}
1139
+ result = self.filterset(params, self.queryset).qs
1140
+ self.assertEqual(result.count(), total_count)
1141
+
1142
+ params = {"name": ""}
1143
+ result = self.filterset(params, self.queryset).qs
1144
+ self.assertEqual(result.count(), total_count)
1145
+
1146
+ def test_ipfabric_filter_filters_equivalence(self):
1147
+ """Test that ipfabric_filter_id and ipfabric_filters work correctly"""
1148
+ # Single ID with ipfabric_filter_id
1149
+ params1 = {"ipfabric_filter_id": [self.filter1.pk]}
1150
+ result1 = self.filterset(params1, self.queryset).qs
1151
+
1152
+ # Same ID with ipfabric_filters
1153
+ params2 = {"ipfabric_filters": [self.filter1.pk]}
1154
+ result2 = self.filterset(params2, self.queryset).qs
1155
+
1156
+ # Should return same results
1157
+ self.assertEqual(result1.count(), result2.count())
1158
+ self.assertEqual(
1159
+ set(result1.values_list("id", flat=True)),
1160
+ set(result2.values_list("id", flat=True)),
1161
+ )
1162
+
1163
+ # Multiple IDs with ipfabric_filter_id
1164
+ params1 = {"ipfabric_filter_id": [self.filter1.pk, self.filter2.pk]}
1165
+ result1 = self.filterset(params1, self.queryset).qs
1166
+
1167
+ # Same IDs with ipfabric_filters
1168
+ params2 = {"ipfabric_filters": [self.filter1.pk, self.filter2.pk]}
1169
+ result2 = self.filterset(params2, self.queryset).qs
1170
+
1171
+ # Should return same results
1172
+ self.assertEqual(result1.count(), result2.count())
1173
+ self.assertEqual(
1174
+ set(result1.values_list("id", flat=True)),
1175
+ set(result2.values_list("id", flat=True)),
1176
+ )
1177
+
1178
+ def test_search(self):
1179
+ """Test search functionality"""
1180
+ params = {"q": "Sync"}
1181
+ result = self.filterset(params, self.queryset).qs
1182
+ # Should find syncs with "Sync" in name or snapshot name
1183
+ self.assertGreaterEqual(result.count(), 3)
1184
+
1185
+
1186
+ class IPFabricTransformMapFilterSetTestCase(TestCase):
1187
+ """
1188
+ Test IPFabricTransformMapFilterSet to verify all custom filters work correctly.
1189
+ """
1190
+
1191
+ queryset = IPFabricTransformMap.objects.all()
1192
+ filterset = IPFabricTransformMapFilterSet
1193
+
1194
+ @classmethod
1195
+ def setUpTestData(cls):
1196
+ # Get existing endpoints from migrations
1197
+ cls.sites_endpoint = IPFabricEndpoint.objects.filter(
1198
+ endpoint=IPFabricEndpointChoices.SITES
1199
+ ).first()
1200
+ cls.devices_endpoint = IPFabricEndpoint.objects.filter(
1201
+ endpoint=IPFabricEndpointChoices.DEVICES
1202
+ ).first()
1203
+ cls.vrfs_endpoint = IPFabricEndpoint.objects.filter(
1204
+ endpoint=IPFabricEndpointChoices.VRFS
1205
+ ).first()
1206
+
1207
+ # Get ContentType for target_model
1208
+ from dcim.models import Site, Device
1209
+
1210
+ cls.site_ct = ContentType.objects.get_for_model(Site)
1211
+ cls.device_ct = ContentType.objects.get_for_model(Device)
1212
+
1213
+ # Create test transform map groups
1214
+ groups = [
1215
+ IPFabricTransformMapGroup(
1216
+ name="Test Group 1",
1217
+ description="First test group",
1218
+ ),
1219
+ IPFabricTransformMapGroup(
1220
+ name="Test Group 2",
1221
+ description="Second test group",
1222
+ ),
1223
+ ]
1224
+ IPFabricTransformMapGroup.objects.bulk_create(groups)
1225
+
1226
+ cls.group1 = IPFabricTransformMapGroup.objects.get(name="Test Group 1")
1227
+ cls.group2 = IPFabricTransformMapGroup.objects.get(name="Test Group 2")
1228
+
1229
+ # Create test transform maps
1230
+ transform_maps = [
1231
+ IPFabricTransformMap(
1232
+ name="Sites Transform Map 1",
1233
+ source_endpoint=cls.sites_endpoint,
1234
+ target_model=cls.site_ct,
1235
+ group=cls.group1,
1236
+ ),
1237
+ IPFabricTransformMap(
1238
+ name="Devices Transform Map 1",
1239
+ source_endpoint=cls.devices_endpoint,
1240
+ target_model=cls.device_ct,
1241
+ group=cls.group1,
1242
+ ),
1243
+ IPFabricTransformMap(
1244
+ name="VRFs Transform Map 1",
1245
+ source_endpoint=cls.vrfs_endpoint,
1246
+ target_model=cls.device_ct,
1247
+ group=cls.group2,
1248
+ ),
1249
+ IPFabricTransformMap(
1250
+ name="Sites Transform Map 2",
1251
+ source_endpoint=cls.sites_endpoint,
1252
+ target_model=cls.site_ct,
1253
+ group=cls.group2,
1254
+ ),
1255
+ ]
1256
+ IPFabricTransformMap.objects.bulk_create(transform_maps)
1257
+
1258
+ cls.sites_map1 = IPFabricTransformMap.objects.get(name="Sites Transform Map 1")
1259
+ cls.devices_map1 = IPFabricTransformMap.objects.get(
1260
+ name="Devices Transform Map 1"
1261
+ )
1262
+ cls.vrfs_map1 = IPFabricTransformMap.objects.get(name="VRFs Transform Map 1")
1263
+ cls.sites_map2 = IPFabricTransformMap.objects.get(name="Sites Transform Map 2")
1264
+
1265
+ def test_id(self):
1266
+ """Test filtering by transform map ID"""
1267
+ params = {"id": [self.sites_map1.pk]}
1268
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
1269
+ params = {"id": [self.sites_map1.pk, self.devices_map1.pk]}
1270
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1271
+
1272
+ def test_name(self):
1273
+ """Test filtering by transform map name"""
1274
+ params = {"name": ["Sites Transform Map 1"]}
1275
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
1276
+ params = {"name": ["Sites Transform Map 1", "Devices Transform Map 1"]}
1277
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1278
+
1279
+ def test_group(self):
1280
+ """Test filtering by group"""
1281
+ params = {"group": [self.group1.pk]}
1282
+ result = self.filterset(params, self.queryset).qs
1283
+ # Group 1 has sites_map1 and devices_map1
1284
+ self.assertEqual(result.count(), 2)
1285
+ map_ids = set(result.values_list("id", flat=True))
1286
+ self.assertIn(self.sites_map1.pk, map_ids)
1287
+ self.assertIn(self.devices_map1.pk, map_ids)
1288
+
1289
+ params = {"group": [self.group2.pk]}
1290
+ result = self.filterset(params, self.queryset).qs
1291
+ # Group 2 has vrfs_map1 and sites_map2
1292
+ self.assertEqual(result.count(), 2)
1293
+
1294
+ def test_group_id(self):
1295
+ """Test filtering by group ID"""
1296
+ params = {"group_id": [self.group1.pk]}
1297
+ result = self.filterset(params, self.queryset).qs
1298
+ # Group 1 has sites_map1 and devices_map1
1299
+ self.assertEqual(result.count(), 2)
1300
+ map_ids = set(result.values_list("id", flat=True))
1301
+ self.assertIn(self.sites_map1.pk, map_ids)
1302
+ self.assertIn(self.devices_map1.pk, map_ids)
1303
+
1304
+ def test_source_endpoint(self):
1305
+ """Test filtering by source endpoint (single)"""
1306
+ params = {"source_endpoint": self.sites_endpoint.pk}
1307
+ result = self.filterset(params, self.queryset).qs
1308
+ # Sites endpoint has sites_map1 and sites_map2 from our test data
1309
+ # (may include more from other test classes)
1310
+ self.assertGreaterEqual(result.count(), 2)
1311
+ map_ids = set(result.values_list("id", flat=True))
1312
+ self.assertIn(self.sites_map1.pk, map_ids)
1313
+ self.assertIn(self.sites_map2.pk, map_ids)
1314
+
1315
+ params = {"source_endpoint": self.devices_endpoint.pk}
1316
+ result = self.filterset(params, self.queryset).qs
1317
+ # Devices endpoint has devices_map1 from our test data
1318
+ self.assertGreaterEqual(result.count(), 1)
1319
+ self.assertIn(self.devices_map1.pk, result.values_list("id", flat=True))
1320
+
1321
+ def test_source_endpoint_id(self):
1322
+ """Test filtering by source endpoint ID (multiple)"""
1323
+ params = {"source_endpoint_id": [self.sites_endpoint.pk]}
1324
+ result = self.filterset(params, self.queryset).qs
1325
+ # Sites endpoint has sites_map1 and sites_map2 from our test data
1326
+ # (may include more from other test classes)
1327
+ self.assertGreaterEqual(result.count(), 2)
1328
+ map_ids = set(result.values_list("id", flat=True))
1329
+ self.assertIn(self.sites_map1.pk, map_ids)
1330
+ self.assertIn(self.sites_map2.pk, map_ids)
1331
+
1332
+ params = {"source_endpoint_id": [self.devices_endpoint.pk]}
1333
+ result = self.filterset(params, self.queryset).qs
1334
+ # Devices endpoint has devices_map1 from our test data
1335
+ self.assertGreaterEqual(result.count(), 1)
1336
+ self.assertIn(self.devices_map1.pk, result.values_list("id", flat=True))
1337
+
1338
+ # Test multiple endpoint IDs
1339
+ params = {"source_endpoint_id": [self.sites_endpoint.pk, self.vrfs_endpoint.pk]}
1340
+ result = self.filterset(params, self.queryset).qs
1341
+ # Sites has 2 maps, VRFs has 1 map = 3 minimum from our test data
1342
+ self.assertGreaterEqual(result.count(), 3)
1343
+ map_ids = set(result.values_list("id", flat=True))
1344
+ self.assertIn(self.sites_map1.pk, map_ids)
1345
+ self.assertIn(self.sites_map2.pk, map_ids)
1346
+ self.assertIn(self.vrfs_map1.pk, map_ids)
1347
+
1348
+ def test_source_endpoints(self):
1349
+ """Test filtering by source endpoints (multiple)"""
1350
+ params = {
1351
+ "source_endpoints": [self.sites_endpoint.pk, self.devices_endpoint.pk]
1352
+ }
1353
+ result = self.filterset(params, self.queryset).qs
1354
+ # Sites has 2 maps, Devices has 1 map = 3 minimum from our test data
1355
+ # (may include more from other test classes)
1356
+ self.assertGreaterEqual(result.count(), 3)
1357
+ map_ids = set(result.values_list("id", flat=True))
1358
+ self.assertIn(self.sites_map1.pk, map_ids)
1359
+ self.assertIn(self.sites_map2.pk, map_ids)
1360
+ self.assertIn(self.devices_map1.pk, map_ids)
1361
+
1362
+ # Test all endpoints
1363
+ params = {
1364
+ "source_endpoints": [
1365
+ self.sites_endpoint.pk,
1366
+ self.devices_endpoint.pk,
1367
+ self.vrfs_endpoint.pk,
1368
+ ]
1369
+ }
1370
+ result = self.filterset(params, self.queryset).qs
1371
+ # All 4 transform maps from our test data minimum
1372
+ self.assertGreaterEqual(result.count(), 4)
1373
+ map_ids = set(result.values_list("id", flat=True))
1374
+ self.assertIn(self.sites_map1.pk, map_ids)
1375
+ self.assertIn(self.sites_map2.pk, map_ids)
1376
+ self.assertIn(self.devices_map1.pk, map_ids)
1377
+ self.assertIn(self.vrfs_map1.pk, map_ids)
1378
+
1379
+ def test_target_model(self):
1380
+ """Test filtering by target model"""
1381
+ params = {"target_model": self.site_ct.pk}
1382
+ result = self.filterset(params, self.queryset).qs
1383
+ # Sites maps target Site model from our test data
1384
+ # (may include more from other test classes)
1385
+ self.assertGreaterEqual(result.count(), 2)
1386
+ map_ids = set(result.values_list("id", flat=True))
1387
+ self.assertIn(self.sites_map1.pk, map_ids)
1388
+ self.assertIn(self.sites_map2.pk, map_ids)
1389
+
1390
+ params = {"target_model": self.device_ct.pk}
1391
+ result = self.filterset(params, self.queryset).qs
1392
+ # Devices and VRFs maps target Device model from our test data
1393
+ self.assertGreaterEqual(result.count(), 2)
1394
+ map_ids = set(result.values_list("id", flat=True))
1395
+ self.assertIn(self.devices_map1.pk, map_ids)
1396
+ self.assertIn(self.vrfs_map1.pk, map_ids)
1397
+
1398
+ def test_combined_filters(self):
1399
+ """Test combining multiple filters"""
1400
+ # Test source_endpoint_id + group
1401
+ params = {
1402
+ "source_endpoint_id": [self.sites_endpoint.pk],
1403
+ "group": [self.group1.pk],
1404
+ }
1405
+ result = self.filterset(params, self.queryset).qs
1406
+ # Only sites_map1 has sites endpoint and group1 from our test data
1407
+ # (may include more from other test classes)
1408
+ self.assertGreaterEqual(result.count(), 1)
1409
+ self.assertIn(self.sites_map1.pk, result.values_list("id", flat=True))
1410
+
1411
+ # Test source_endpoints + target_model
1412
+ params = {
1413
+ "source_endpoints": [self.sites_endpoint.pk, self.devices_endpoint.pk],
1414
+ "target_model": self.device_ct.pk,
1415
+ }
1416
+ result = self.filterset(params, self.queryset).qs
1417
+ # Only devices_map1 has devices endpoint and Device target from our test data
1418
+ self.assertGreaterEqual(result.count(), 1)
1419
+ self.assertIn(self.devices_map1.pk, result.values_list("id", flat=True))
1420
+
1421
+ # Test group_id + target_model
1422
+ params = {
1423
+ "group_id": [self.group2.pk],
1424
+ "target_model": self.site_ct.pk,
1425
+ }
1426
+ result = self.filterset(params, self.queryset).qs
1427
+ # Only sites_map2 has group2 and Site target from our test data
1428
+ self.assertGreaterEqual(result.count(), 1)
1429
+ self.assertIn(self.sites_map2.pk, result.values_list("id", flat=True))
1430
+
1431
+ def test_q_search(self):
1432
+ """Test the search (q) parameter"""
1433
+ # Search by transform map name
1434
+ params = {"q": "Sites"}
1435
+ result = self.filterset(params, self.queryset).qs
1436
+ self.assertEqual(result.count(), 2)
1437
+
1438
+ # Search by group name
1439
+ params = {"q": "Test Group 1"}
1440
+ result = self.filterset(params, self.queryset).qs
1441
+ self.assertEqual(result.count(), 2)
1442
+
1443
+ # Search that matches transform map name
1444
+ params = {"q": "Devices"}
1445
+ result = self.filterset(params, self.queryset).qs
1446
+ self.assertEqual(result.count(), 1)
1447
+
1448
+ def test_distinct_results(self):
1449
+ """Test that filters return distinct results"""
1450
+ # Query with source_endpoint_id (should have no duplicates)
1451
+ params = {"source_endpoint_id": [self.sites_endpoint.pk]}
1452
+ result = self.filterset(params, self.queryset).qs
1453
+
1454
+ # Each transform map should appear only once
1455
+ map_counts = {}
1456
+ for map_obj in result:
1457
+ map_counts[map_obj.pk] = map_counts.get(map_obj.pk, 0) + 1
1458
+
1459
+ for count in map_counts.values():
1460
+ self.assertEqual(count, 1)
1461
+
1462
+ def test_empty_filters(self):
1463
+ """Test behavior with empty filter values"""
1464
+ total_count = self.queryset.count()
1465
+
1466
+ params = {"source_endpoint": ""}
1467
+ result = self.filterset(params, self.queryset).qs
1468
+ self.assertEqual(result.count(), total_count)
1469
+
1470
+ params = {"group": ""}
1471
+ result = self.filterset(params, self.queryset).qs
1472
+ self.assertEqual(result.count(), total_count)
1473
+
1474
+ def test_source_endpoint_filters_equivalence(self):
1475
+ """Test that source_endpoint_id and source_endpoints work correctly"""
1476
+ # Single ID with source_endpoint_id
1477
+ params1 = {"source_endpoint_id": [self.sites_endpoint.pk]}
1478
+ result1 = self.filterset(params1, self.queryset).qs
1479
+
1480
+ # Same ID with source_endpoints
1481
+ params2 = {"source_endpoints": [self.sites_endpoint.pk]}
1482
+ result2 = self.filterset(params2, self.queryset).qs
1483
+
1484
+ # Should return same results
1485
+ self.assertEqual(result1.count(), result2.count())
1486
+ self.assertEqual(
1487
+ set(result1.values_list("id", flat=True)),
1488
+ set(result2.values_list("id", flat=True)),
1489
+ )
1490
+
1491
+ # Multiple IDs with source_endpoint_id
1492
+ params1 = {
1493
+ "source_endpoint_id": [self.sites_endpoint.pk, self.devices_endpoint.pk]
1494
+ }
1495
+ result1 = self.filterset(params1, self.queryset).qs
1496
+
1497
+ # Same IDs with source_endpoints
1498
+ params2 = {
1499
+ "source_endpoints": [self.sites_endpoint.pk, self.devices_endpoint.pk]
1500
+ }
1501
+ result2 = self.filterset(params2, self.queryset).qs
1502
+
1503
+ # Should return same results
1504
+ self.assertEqual(result1.count(), result2.count())
1505
+ self.assertEqual(
1506
+ set(result1.values_list("id", flat=True)),
1507
+ set(result2.values_list("id", flat=True)),
1508
+ )
1509
+
1510
+
1511
+ class IPFabricTransformFieldFilterSetTestCase(TestCase):
1512
+ """
1513
+ Test IPFabricTransformFieldFilterSet to verify all custom filters work correctly.
1514
+ """
1515
+
1516
+ queryset = IPFabricTransformField.objects.all()
1517
+ filterset = IPFabricTransformFieldFilterSet
1518
+
1519
+ @classmethod
1520
+ def setUpTestData(cls):
1521
+ # Get existing endpoints from migrations
1522
+ cls.sites_endpoint = IPFabricEndpoint.objects.filter(
1523
+ endpoint=IPFabricEndpointChoices.SITES
1524
+ ).first()
1525
+ cls.devices_endpoint = IPFabricEndpoint.objects.filter(
1526
+ endpoint=IPFabricEndpointChoices.DEVICES
1527
+ ).first()
1528
+
1529
+ # Get ContentType for target_model
1530
+ from dcim.models import Site, Device
1531
+
1532
+ cls.site_ct = ContentType.objects.get_for_model(Site)
1533
+ cls.device_ct = ContentType.objects.get_for_model(Device)
1534
+
1535
+ # Create test transform map groups
1536
+ cls.group1 = IPFabricTransformMapGroup.objects.create(
1537
+ name="Transform Field Test Group 1",
1538
+ description="First transform field test group",
1539
+ )
1540
+ cls.group2 = IPFabricTransformMapGroup.objects.create(
1541
+ name="Transform Field Test Group 2",
1542
+ description="Second transform field test group",
1543
+ )
1544
+
1545
+ # Create test transform maps
1546
+ cls.map1 = IPFabricTransformMap.objects.create(
1547
+ name="Transform Field Map 1",
1548
+ source_endpoint=cls.sites_endpoint,
1549
+ target_model=cls.site_ct,
1550
+ group=cls.group1,
1551
+ )
1552
+ cls.map2 = IPFabricTransformMap.objects.create(
1553
+ name="Transform Field Map 2",
1554
+ source_endpoint=cls.devices_endpoint,
1555
+ target_model=cls.device_ct,
1556
+ group=cls.group1,
1557
+ )
1558
+ cls.map3 = IPFabricTransformMap.objects.create(
1559
+ name="Transform Field Map 3",
1560
+ source_endpoint=cls.sites_endpoint,
1561
+ target_model=cls.site_ct,
1562
+ group=cls.group2,
1563
+ )
1564
+
1565
+ # Create test transform fields
1566
+ cls.field1 = IPFabricTransformField.objects.create(
1567
+ transform_map=cls.map1,
1568
+ source_field="siteName",
1569
+ target_field="name",
1570
+ coalesce=False,
1571
+ template="{{siteName}}",
1572
+ )
1573
+ cls.field2 = IPFabricTransformField.objects.create(
1574
+ transform_map=cls.map1,
1575
+ source_field="siteDescription",
1576
+ target_field="description",
1577
+ coalesce=True,
1578
+ template="{{siteDescription}}",
1579
+ )
1580
+ cls.field3 = IPFabricTransformField.objects.create(
1581
+ transform_map=cls.map2,
1582
+ source_field="hostname",
1583
+ target_field="name",
1584
+ coalesce=False,
1585
+ template="{{hostname}}",
1586
+ )
1587
+ cls.field4 = IPFabricTransformField.objects.create(
1588
+ transform_map=cls.map3,
1589
+ source_field="siteName",
1590
+ target_field="name",
1591
+ coalesce=False,
1592
+ template="{{siteName}}",
1593
+ )
1594
+
1595
+ def test_id(self):
1596
+ """Test filtering by transform field ID"""
1597
+ params = {"id": [self.field1.pk]}
1598
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
1599
+ params = {"id": [self.field1.pk, self.field2.pk]}
1600
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1601
+
1602
+ def test_transform_map(self):
1603
+ """Test filtering by transform map"""
1604
+ params = {"transform_map": [self.map1.pk]}
1605
+ result = self.filterset(params, self.queryset).qs
1606
+ # Map1 has field1 and field2
1607
+ self.assertEqual(result.count(), 2)
1608
+ field_ids = set(result.values_list("id", flat=True))
1609
+ self.assertIn(self.field1.pk, field_ids)
1610
+ self.assertIn(self.field2.pk, field_ids)
1611
+
1612
+ params = {"transform_map": [self.map2.pk]}
1613
+ result = self.filterset(params, self.queryset).qs
1614
+ # Map2 has field3
1615
+ self.assertEqual(result.count(), 1)
1616
+ self.assertEqual(result.first().pk, self.field3.pk)
1617
+
1618
+ def test_transform_map_id(self):
1619
+ """Test filtering by transform map ID"""
1620
+ params = {"transform_map_id": [self.map1.pk]}
1621
+ result = self.filterset(params, self.queryset).qs
1622
+ # Map1 has field1 and field2
1623
+ self.assertEqual(result.count(), 2)
1624
+ field_ids = set(result.values_list("id", flat=True))
1625
+ self.assertIn(self.field1.pk, field_ids)
1626
+ self.assertIn(self.field2.pk, field_ids)
1627
+
1628
+ # Test multiple transform map IDs
1629
+ params = {"transform_map_id": [self.map1.pk, self.map3.pk]}
1630
+ result = self.filterset(params, self.queryset).qs
1631
+ # Map1 has 2 fields, Map3 has 1 field = 3 total
1632
+ self.assertEqual(result.count(), 3)
1633
+ field_ids = set(result.values_list("id", flat=True))
1634
+ self.assertIn(self.field1.pk, field_ids)
1635
+ self.assertIn(self.field2.pk, field_ids)
1636
+ self.assertIn(self.field4.pk, field_ids)
1637
+
1638
+ def test_source_field(self):
1639
+ """Test filtering by source field"""
1640
+ # First filter to only our test transform maps' fields
1641
+ test_maps = [self.map1.pk, self.map2.pk, self.map3.pk]
1642
+
1643
+ params = {"source_field": "siteName", "transform_map": test_maps}
1644
+ result = self.filterset(params, self.queryset).qs
1645
+ # field1 and field4 have siteName (from our test maps only)
1646
+ self.assertEqual(result.count(), 2)
1647
+ field_ids = set(result.values_list("id", flat=True))
1648
+ self.assertIn(self.field1.pk, field_ids)
1649
+ self.assertIn(self.field4.pk, field_ids)
1650
+
1651
+ params = {"source_field": "hostname", "transform_map": test_maps}
1652
+ result = self.filterset(params, self.queryset).qs
1653
+ # field3 has hostname (from our test maps only)
1654
+ self.assertEqual(result.count(), 1)
1655
+ self.assertEqual(result.first().pk, self.field3.pk)
1656
+
1657
+ def test_target_field(self):
1658
+ """Test filtering by target field"""
1659
+ # First filter to only our test transform maps' fields
1660
+ test_maps = [self.map1.pk, self.map2.pk, self.map3.pk]
1661
+
1662
+ params = {"target_field": "name", "transform_map": test_maps}
1663
+ result = self.filterset(params, self.queryset).qs
1664
+ # field1, field3, and field4 have name as target (from our test maps only)
1665
+ self.assertEqual(result.count(), 3)
1666
+ field_ids = set(result.values_list("id", flat=True))
1667
+ self.assertIn(self.field1.pk, field_ids)
1668
+ self.assertIn(self.field3.pk, field_ids)
1669
+ self.assertIn(self.field4.pk, field_ids)
1670
+
1671
+ params = {"target_field": "description", "transform_map": test_maps}
1672
+ result = self.filterset(params, self.queryset).qs
1673
+ # field2 has description as target (from our test maps only)
1674
+ self.assertEqual(result.count(), 1)
1675
+ self.assertEqual(result.first().pk, self.field2.pk)
1676
+
1677
+ def test_coalesce(self):
1678
+ """Test filtering by coalesce"""
1679
+ # First filter to only our test transform maps' fields
1680
+ test_maps = [self.map1.pk, self.map2.pk, self.map3.pk]
1681
+
1682
+ params = {"coalesce": True, "transform_map": test_maps}
1683
+ result = self.filterset(params, self.queryset).qs
1684
+ # field2 has coalesce=True (from our test maps only)
1685
+ self.assertEqual(result.count(), 1)
1686
+ self.assertEqual(result.first().pk, self.field2.pk)
1687
+
1688
+ params = {"coalesce": False, "transform_map": test_maps}
1689
+ result = self.filterset(params, self.queryset).qs
1690
+ # field1, field3, field4 have coalesce=False (from our test maps only)
1691
+ self.assertEqual(result.count(), 3)
1692
+ field_ids = set(result.values_list("id", flat=True))
1693
+ self.assertIn(self.field1.pk, field_ids)
1694
+ self.assertIn(self.field3.pk, field_ids)
1695
+ self.assertIn(self.field4.pk, field_ids)
1696
+
1697
+ def test_combined_filters(self):
1698
+ """Test combining multiple filters"""
1699
+ # Test transform_map_id + source_field
1700
+ params = {
1701
+ "transform_map_id": [self.map1.pk],
1702
+ "source_field": "siteName",
1703
+ }
1704
+ result = self.filterset(params, self.queryset).qs
1705
+ # Only field1 has map1 and siteName
1706
+ self.assertEqual(result.count(), 1)
1707
+ self.assertEqual(result.first().pk, self.field1.pk)
1708
+
1709
+ # Test transform_map_id + target_field
1710
+ params = {
1711
+ "transform_map_id": [self.map1.pk],
1712
+ "target_field": "name",
1713
+ }
1714
+ result = self.filterset(params, self.queryset).qs
1715
+ # Only field1 has map1 and name target
1716
+ self.assertEqual(result.count(), 1)
1717
+ self.assertEqual(result.first().pk, self.field1.pk)
1718
+
1719
+ # Test transform_map_id + coalesce
1720
+ params = {
1721
+ "transform_map_id": [self.map1.pk],
1722
+ "coalesce": True,
1723
+ }
1724
+ result = self.filterset(params, self.queryset).qs
1725
+ # Only field2 has map1 and coalesce=True
1726
+ self.assertEqual(result.count(), 1)
1727
+ self.assertEqual(result.first().pk, self.field2.pk)
1728
+
1729
+ def test_transform_map_filters_equivalence(self):
1730
+ """Test that transform_map and transform_map_id work correctly"""
1731
+ # Single ID with transform_map
1732
+ params1 = {"transform_map": [self.map1.pk]}
1733
+ result1 = self.filterset(params1, self.queryset).qs
1734
+
1735
+ # Same ID with transform_map_id
1736
+ params2 = {"transform_map_id": [self.map1.pk]}
1737
+ result2 = self.filterset(params2, self.queryset).qs
1738
+
1739
+ # Should return same results
1740
+ self.assertEqual(result1.count(), result2.count())
1741
+ self.assertEqual(
1742
+ set(result1.values_list("id", flat=True)),
1743
+ set(result2.values_list("id", flat=True)),
1744
+ )
1745
+
1746
+ def test_distinct_results(self):
1747
+ """Test that filters return distinct results"""
1748
+ # Query with transform_map_id (should have no duplicates)
1749
+ params = {"transform_map_id": [self.map1.pk]}
1750
+ result = self.filterset(params, self.queryset).qs
1751
+
1752
+ # Each transform field should appear only once
1753
+ field_counts = {}
1754
+ for field in result:
1755
+ field_counts[field.pk] = field_counts.get(field.pk, 0) + 1
1756
+
1757
+ for count in field_counts.values():
1758
+ self.assertEqual(count, 1)
1759
+
1760
+ def test_empty_filters(self):
1761
+ """Test behavior with empty filter values"""
1762
+ total_count = self.queryset.count()
1763
+
1764
+ params = {"transform_map": ""}
1765
+ result = self.filterset(params, self.queryset).qs
1766
+ self.assertEqual(result.count(), total_count)
1767
+
1768
+ params = {"source_field": ""}
1769
+ result = self.filterset(params, self.queryset).qs
1770
+ self.assertEqual(result.count(), total_count)
1771
+
1772
+
1773
+ class IPFabricRelationshipFieldFilterSetTestCase(TestCase):
1774
+ """
1775
+ Test IPFabricRelationshipFieldFilterSet to verify all custom filters work correctly.
1776
+ """
1777
+
1778
+ queryset = IPFabricRelationshipField.objects.all()
1779
+ filterset = IPFabricRelationshipFieldFilterSet
1780
+
1781
+ @classmethod
1782
+ def setUpTestData(cls):
1783
+ # Get existing endpoints from migrations
1784
+ cls.sites_endpoint = IPFabricEndpoint.objects.filter(
1785
+ endpoint=IPFabricEndpointChoices.SITES
1786
+ ).first()
1787
+ cls.devices_endpoint = IPFabricEndpoint.objects.filter(
1788
+ endpoint=IPFabricEndpointChoices.DEVICES
1789
+ ).first()
1790
+
1791
+ # Get ContentType for models
1792
+ from dcim.models import Site, Device, Location
1793
+
1794
+ cls.site_ct = ContentType.objects.get_for_model(Site)
1795
+ cls.device_ct = ContentType.objects.get_for_model(Device)
1796
+ cls.location_ct = ContentType.objects.get_for_model(Location)
1797
+
1798
+ # Create test transform map groups
1799
+ cls.group1 = IPFabricTransformMapGroup.objects.create(
1800
+ name="Relationship Field Test Group 1",
1801
+ description="First relationship field test group",
1802
+ )
1803
+ cls.group2 = IPFabricTransformMapGroup.objects.create(
1804
+ name="Relationship Field Test Group 2",
1805
+ description="Second relationship field test group",
1806
+ )
1807
+
1808
+ # Create test transform maps
1809
+ cls.map1 = IPFabricTransformMap.objects.create(
1810
+ name="Relationship Field Map 1",
1811
+ source_endpoint=cls.sites_endpoint,
1812
+ target_model=cls.site_ct,
1813
+ group=cls.group1,
1814
+ )
1815
+ cls.map2 = IPFabricTransformMap.objects.create(
1816
+ name="Relationship Field Map 2",
1817
+ source_endpoint=cls.devices_endpoint,
1818
+ target_model=cls.device_ct,
1819
+ group=cls.group1,
1820
+ )
1821
+ cls.map3 = IPFabricTransformMap.objects.create(
1822
+ name="Relationship Field Map 3",
1823
+ source_endpoint=cls.sites_endpoint,
1824
+ target_model=cls.site_ct,
1825
+ group=cls.group2,
1826
+ )
1827
+
1828
+ # Create test relationship fields
1829
+ cls.rel_field1 = IPFabricRelationshipField.objects.create(
1830
+ transform_map=cls.map1,
1831
+ source_model=cls.location_ct,
1832
+ target_field="location",
1833
+ coalesce=False,
1834
+ template="{{location_id}}",
1835
+ )
1836
+ cls.rel_field2 = IPFabricRelationshipField.objects.create(
1837
+ transform_map=cls.map1,
1838
+ source_model=cls.site_ct,
1839
+ target_field="site",
1840
+ coalesce=True,
1841
+ template="{{site_id}}",
1842
+ )
1843
+ cls.rel_field3 = IPFabricRelationshipField.objects.create(
1844
+ transform_map=cls.map2,
1845
+ source_model=cls.device_ct,
1846
+ target_field="device",
1847
+ coalesce=False,
1848
+ template="{{device_id}}",
1849
+ )
1850
+ cls.rel_field4 = IPFabricRelationshipField.objects.create(
1851
+ transform_map=cls.map3,
1852
+ source_model=cls.location_ct,
1853
+ target_field="location",
1854
+ coalesce=False,
1855
+ template="{{location_id}}",
1856
+ )
1857
+
1858
+ def test_id(self):
1859
+ """Test filtering by relationship field ID"""
1860
+ params = {"id": [self.rel_field1.pk]}
1861
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
1862
+ params = {"id": [self.rel_field1.pk, self.rel_field2.pk]}
1863
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1864
+
1865
+ def test_transform_map(self):
1866
+ """Test filtering by transform map"""
1867
+ params = {"transform_map": [self.map1.pk]}
1868
+ result = self.filterset(params, self.queryset).qs
1869
+ # Map1 has rel_field1 and rel_field2
1870
+ self.assertEqual(result.count(), 2)
1871
+ field_ids = set(result.values_list("id", flat=True))
1872
+ self.assertIn(self.rel_field1.pk, field_ids)
1873
+ self.assertIn(self.rel_field2.pk, field_ids)
1874
+
1875
+ params = {"transform_map": [self.map2.pk]}
1876
+ result = self.filterset(params, self.queryset).qs
1877
+ # Map2 has rel_field3
1878
+ self.assertEqual(result.count(), 1)
1879
+ self.assertEqual(result.first().pk, self.rel_field3.pk)
1880
+
1881
+ def test_transform_map_id(self):
1882
+ """Test filtering by transform map ID"""
1883
+ params = {"transform_map_id": [self.map1.pk]}
1884
+ result = self.filterset(params, self.queryset).qs
1885
+ # Map1 has rel_field1 and rel_field2
1886
+ self.assertEqual(result.count(), 2)
1887
+ field_ids = set(result.values_list("id", flat=True))
1888
+ self.assertIn(self.rel_field1.pk, field_ids)
1889
+ self.assertIn(self.rel_field2.pk, field_ids)
1890
+
1891
+ # Test multiple transform map IDs
1892
+ params = {"transform_map_id": [self.map1.pk, self.map3.pk]}
1893
+ result = self.filterset(params, self.queryset).qs
1894
+ # Map1 has 2 fields, Map3 has 1 field = 3 total
1895
+ self.assertEqual(result.count(), 3)
1896
+ field_ids = set(result.values_list("id", flat=True))
1897
+ self.assertIn(self.rel_field1.pk, field_ids)
1898
+ self.assertIn(self.rel_field2.pk, field_ids)
1899
+ self.assertIn(self.rel_field4.pk, field_ids)
1900
+
1901
+ def test_source_model(self):
1902
+ """Test filtering by source model"""
1903
+ # First filter to only our test transform maps' fields
1904
+ test_maps = [self.map1.pk, self.map2.pk, self.map3.pk]
1905
+
1906
+ params = {"source_model": self.location_ct.pk, "transform_map": test_maps}
1907
+ result = self.filterset(params, self.queryset).qs
1908
+ # rel_field1 and rel_field4 have Location as source model (from our test maps only)
1909
+ self.assertEqual(result.count(), 2)
1910
+ field_ids = set(result.values_list("id", flat=True))
1911
+ self.assertIn(self.rel_field1.pk, field_ids)
1912
+ self.assertIn(self.rel_field4.pk, field_ids)
1913
+
1914
+ params = {"source_model": self.device_ct.pk, "transform_map": test_maps}
1915
+ result = self.filterset(params, self.queryset).qs
1916
+ # rel_field3 has Device as source model (from our test maps only)
1917
+ self.assertEqual(result.count(), 1)
1918
+ self.assertEqual(result.first().pk, self.rel_field3.pk)
1919
+
1920
+ def test_target_field(self):
1921
+ """Test filtering by target field"""
1922
+ # First filter to only our test transform maps' fields
1923
+ test_maps = [self.map1.pk, self.map2.pk, self.map3.pk]
1924
+
1925
+ params = {"target_field": "location", "transform_map": test_maps}
1926
+ result = self.filterset(params, self.queryset).qs
1927
+ # rel_field1 and rel_field4 have location as target (from our test maps only)
1928
+ self.assertEqual(result.count(), 2)
1929
+ field_ids = set(result.values_list("id", flat=True))
1930
+ self.assertIn(self.rel_field1.pk, field_ids)
1931
+ self.assertIn(self.rel_field4.pk, field_ids)
1932
+
1933
+ params = {"target_field": "site", "transform_map": test_maps}
1934
+ result = self.filterset(params, self.queryset).qs
1935
+ # rel_field2 has site as target (from our test maps only)
1936
+ self.assertEqual(result.count(), 1)
1937
+ self.assertEqual(result.first().pk, self.rel_field2.pk)
1938
+
1939
+ def test_coalesce(self):
1940
+ """Test filtering by coalesce"""
1941
+ # First filter to only our test transform maps' fields
1942
+ test_maps = [self.map1.pk, self.map2.pk, self.map3.pk]
1943
+
1944
+ params = {"coalesce": True, "transform_map": test_maps}
1945
+ result = self.filterset(params, self.queryset).qs
1946
+ # rel_field2 has coalesce=True (from our test maps only)
1947
+ self.assertEqual(result.count(), 1)
1948
+ self.assertEqual(result.first().pk, self.rel_field2.pk)
1949
+
1950
+ params = {"coalesce": False, "transform_map": test_maps}
1951
+ result = self.filterset(params, self.queryset).qs
1952
+ # rel_field1, rel_field3, rel_field4 have coalesce=False (from our test maps only)
1953
+ self.assertEqual(result.count(), 3)
1954
+ field_ids = set(result.values_list("id", flat=True))
1955
+ self.assertIn(self.rel_field1.pk, field_ids)
1956
+ self.assertIn(self.rel_field3.pk, field_ids)
1957
+ self.assertIn(self.rel_field4.pk, field_ids)
1958
+
1959
+ def test_combined_filters(self):
1960
+ """Test combining multiple filters"""
1961
+ # Test transform_map_id + source_model
1962
+ params = {
1963
+ "transform_map_id": [self.map1.pk],
1964
+ "source_model": self.location_ct.pk,
1965
+ }
1966
+ result = self.filterset(params, self.queryset).qs
1967
+ # Only rel_field1 has map1 and Location source
1968
+ self.assertEqual(result.count(), 1)
1969
+ self.assertEqual(result.first().pk, self.rel_field1.pk)
1970
+
1971
+ # Test transform_map_id + target_field
1972
+ params = {
1973
+ "transform_map_id": [self.map1.pk],
1974
+ "target_field": "location",
1975
+ }
1976
+ result = self.filterset(params, self.queryset).qs
1977
+ # Only rel_field1 has map1 and location target
1978
+ self.assertEqual(result.count(), 1)
1979
+ self.assertEqual(result.first().pk, self.rel_field1.pk)
1980
+
1981
+ # Test transform_map_id + coalesce
1982
+ params = {
1983
+ "transform_map_id": [self.map1.pk],
1984
+ "coalesce": True,
1985
+ }
1986
+ result = self.filterset(params, self.queryset).qs
1987
+ # Only rel_field2 has map1 and coalesce=True
1988
+ self.assertEqual(result.count(), 1)
1989
+ self.assertEqual(result.first().pk, self.rel_field2.pk)
1990
+
1991
+ def test_transform_map_filters_equivalence(self):
1992
+ """Test that transform_map and transform_map_id work correctly"""
1993
+ # Single ID with transform_map
1994
+ params1 = {"transform_map": [self.map1.pk]}
1995
+ result1 = self.filterset(params1, self.queryset).qs
1996
+
1997
+ # Same ID with transform_map_id
1998
+ params2 = {"transform_map_id": [self.map1.pk]}
1999
+ result2 = self.filterset(params2, self.queryset).qs
2000
+
2001
+ # Should return same results
2002
+ self.assertEqual(result1.count(), result2.count())
2003
+ self.assertEqual(
2004
+ set(result1.values_list("id", flat=True)),
2005
+ set(result2.values_list("id", flat=True)),
2006
+ )
2007
+
2008
+ def test_distinct_results(self):
2009
+ """Test that filters return distinct results"""
2010
+ # Query with transform_map_id (should have no duplicates)
2011
+ params = {"transform_map_id": [self.map1.pk]}
2012
+ result = self.filterset(params, self.queryset).qs
2013
+
2014
+ # Each relationship field should appear only once
2015
+ field_counts = {}
2016
+ for field in result:
2017
+ field_counts[field.pk] = field_counts.get(field.pk, 0) + 1
2018
+
2019
+ for count in field_counts.values():
2020
+ self.assertEqual(count, 1)
2021
+
2022
+ def test_empty_filters(self):
2023
+ """Test behavior with empty filter values"""
2024
+ total_count = self.queryset.count()
2025
+
2026
+ params = {"transform_map": ""}
2027
+ result = self.filterset(params, self.queryset).qs
2028
+ self.assertEqual(result.count(), total_count)
2029
+
2030
+ params = {"target_field": ""}
2031
+ result = self.filterset(params, self.queryset).qs
2032
+ self.assertEqual(result.count(), total_count)
2033
+
2034
+
2035
+ class IPFabricTransformMapGroupFilterSetTestCase(TestCase):
2036
+ filterset = IPFabricTransformMapGroupFilterSet
2037
+
2038
+ @classmethod
2039
+ def setUpTestData(cls):
2040
+ groups = [
2041
+ IPFabricTransformMapGroup(
2042
+ name="Group Alpha",
2043
+ description="First transform map group",
2044
+ ),
2045
+ IPFabricTransformMapGroup(
2046
+ name="Group Beta",
2047
+ description="Second transform map group",
2048
+ ),
2049
+ IPFabricTransformMapGroup(
2050
+ name="Group Gamma",
2051
+ description="Third group for testing",
2052
+ ),
2053
+ ]
2054
+ IPFabricTransformMapGroup.objects.bulk_create(groups)
2055
+
2056
+ @property
2057
+ def queryset(self):
2058
+ return IPFabricTransformMapGroup.objects.all()
2059
+
2060
+ def test_id(self):
2061
+ """Test filtering by group ID"""
2062
+ group = IPFabricTransformMapGroup.objects.first()
2063
+ params = {"id": [group.pk]}
2064
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
2065
+
2066
+ def test_name(self):
2067
+ """Test filtering by name (exact match)"""
2068
+ params = {"name": ["Group Alpha"]}
2069
+ result = self.filterset(params, self.queryset).qs
2070
+ # Exact match should find exactly 1
2071
+ self.assertEqual(result.count(), 1)
2072
+ self.assertEqual(result.first().name, "Group Alpha")
2073
+
2074
+ def test_description(self):
2075
+ """Test filtering by description (exact match)"""
2076
+ params = {"description": "First transform map group"}
2077
+ result = self.filterset(params, self.queryset).qs
2078
+ self.assertEqual(result.count(), 1)
2079
+ self.assertEqual(result.first().name, "Group Alpha")
2080
+
2081
+ def test_search(self):
2082
+ """Test search across name and description"""
2083
+ params = {"q": "Alpha"}
2084
+ result = self.filterset(params, self.queryset).qs
2085
+ self.assertGreaterEqual(result.count(), 1)
2086
+
2087
+ params = {"q": "group"}
2088
+ result = self.filterset(params, self.queryset).qs
2089
+ self.assertGreaterEqual(result.count(), 3)
2090
+
2091
+
2092
+ class IPFabricSourceFilterSetTestCase(TestCase):
2093
+ filterset = IPFabricSourceFilterSet
2094
+
2095
+ @classmethod
2096
+ def setUpTestData(cls):
2097
+ sources = [
2098
+ IPFabricSource(
2099
+ name="Source Alpha",
2100
+ url="https://alpha.example.com",
2101
+ parameters={"auth": "token"},
2102
+ status="ready",
2103
+ description="Primary source",
2104
+ comments="Alpha comments",
2105
+ ),
2106
+ IPFabricSource(
2107
+ name="Source Beta",
2108
+ url="https://beta.example.com",
2109
+ parameters={"auth": "basic"},
2110
+ status="error",
2111
+ description="Secondary source",
2112
+ comments="Beta notes",
2113
+ ),
2114
+ IPFabricSource(
2115
+ name="Source Gamma",
2116
+ url="https://gamma.example.com",
2117
+ parameters={"verify": False},
2118
+ status="ready",
2119
+ description="Testing source",
2120
+ ),
2121
+ ]
2122
+ IPFabricSource.objects.bulk_create(sources)
2123
+
2124
+ @property
2125
+ def queryset(self):
2126
+ return IPFabricSource.objects.all()
2127
+
2128
+ def test_id(self):
2129
+ """Test filtering by source ID"""
2130
+ source = IPFabricSource.objects.first()
2131
+ params = {"id": [source.pk]}
2132
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
2133
+
2134
+ def test_name(self):
2135
+ """Test filtering by name (exact match)"""
2136
+ params = {"name": ["Source Alpha"]}
2137
+ result = self.filterset(params, self.queryset).qs
2138
+ # Exact match should find exactly 1
2139
+ self.assertEqual(result.count(), 1)
2140
+ self.assertEqual(result.first().url, "https://alpha.example.com")
2141
+
2142
+ def test_status(self):
2143
+ """Test filtering by status"""
2144
+ params = {"status": ["ready"]}
2145
+ result = self.filterset(params, self.queryset).qs
2146
+ # Should find at least our 2 test sources with ready status
2147
+ self.assertGreaterEqual(result.count(), 2)
2148
+
2149
+ params = {"status": ["error"]}
2150
+ result = self.filterset(params, self.queryset).qs
2151
+ # Should find at least our 1 test source with error status
2152
+ self.assertGreaterEqual(result.count(), 1)
2153
+
2154
+ def test_search(self):
2155
+ """Test search across name, description, and comments"""
2156
+ params = {"q": "Alpha"}
2157
+ result = self.filterset(params, self.queryset).qs
2158
+ self.assertGreaterEqual(result.count(), 1)
2159
+
2160
+ params = {"q": "source"}
2161
+ result = self.filterset(params, self.queryset).qs
2162
+ self.assertGreaterEqual(result.count(), 3)
2163
+
2164
+ params = {"q": "notes"}
2165
+ result = self.filterset(params, self.queryset).qs
2166
+ self.assertGreaterEqual(result.count(), 1)
2167
+
2168
+
2169
+ class IPFabricSnapshotFilterSetTestCase(TestCase):
2170
+ filterset = IPFabricSnapshotFilterSet
2171
+
2172
+ @classmethod
2173
+ def setUpTestData(cls):
2174
+ source1 = IPFabricSource.objects.create(
2175
+ name="Snapshot Test Source 1",
2176
+ url="https://source1.example.com",
2177
+ parameters={"auth": "token"},
2178
+ )
2179
+ source2 = IPFabricSource.objects.create(
2180
+ name="Snapshot Test Source 2",
2181
+ url="https://source2.example.com",
2182
+ parameters={"auth": "basic"},
2183
+ )
2184
+
2185
+ cls.snapshots = [
2186
+ IPFabricSnapshot(
2187
+ name="Snapshot Alpha",
2188
+ source=source1,
2189
+ snapshot_id="snap-alpha-001",
2190
+ status="loaded",
2191
+ data={"sites": ["SiteA"]},
2192
+ ),
2193
+ IPFabricSnapshot(
2194
+ name="Snapshot Beta",
2195
+ source=source1,
2196
+ snapshot_id="snap-beta-002",
2197
+ status="unloaded",
2198
+ data={"sites": ["SiteB"]},
2199
+ ),
2200
+ IPFabricSnapshot(
2201
+ name="Snapshot Gamma",
2202
+ source=source2,
2203
+ snapshot_id="snap-gamma-003",
2204
+ status="loaded",
2205
+ data={"sites": ["SiteC"]},
2206
+ ),
2207
+ ]
2208
+ IPFabricSnapshot.objects.bulk_create(cls.snapshots)
2209
+
2210
+ @property
2211
+ def queryset(self):
2212
+ return IPFabricSnapshot.objects.all()
2213
+
2214
+ def test_id(self):
2215
+ """Test filtering by snapshot ID"""
2216
+ snapshot = IPFabricSnapshot.objects.first()
2217
+ params = {"id": [snapshot.pk]}
2218
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
2219
+
2220
+ def test_name(self):
2221
+ """Test filtering by name (exact match)"""
2222
+ params = {"name": ["Snapshot Alpha"]}
2223
+ result = self.filterset(params, self.queryset).qs
2224
+ # Exact match should find exactly 1
2225
+ self.assertEqual(result.count(), 1)
2226
+ self.assertEqual(result.first().snapshot_id, "snap-alpha-001")
2227
+
2228
+ def test_status(self):
2229
+ """Test filtering by status"""
2230
+ params = {"status": "loaded"}
2231
+ result = self.filterset(params, self.queryset).qs
2232
+ # Should find at least our 2 test snapshots with loaded status
2233
+ self.assertGreaterEqual(result.count(), 2)
2234
+
2235
+ def test_snapshot_id(self):
2236
+ """Test filtering by snapshot_id with icontains"""
2237
+ params = {"snapshot_id": "alpha"}
2238
+ result = self.filterset(params, self.queryset).qs
2239
+ self.assertGreaterEqual(result.count(), 1)
2240
+
2241
+ params = {"snapshot_id": "snap"}
2242
+ result = self.filterset(params, self.queryset).qs
2243
+ self.assertGreaterEqual(result.count(), 3)
2244
+
2245
+ def test_source_id(self):
2246
+ """Test filtering by source ID"""
2247
+ source = IPFabricSource.objects.get(name="Snapshot Test Source 1")
2248
+ params = {"source_id": [source.pk]}
2249
+ result = self.filterset(params, self.queryset).qs
2250
+ self.assertGreaterEqual(result.count(), 2)
2251
+
2252
+ def test_source_name(self):
2253
+ """Test filtering by source name"""
2254
+ source = IPFabricSource.objects.get(name="Snapshot Test Source 2")
2255
+ params = {"source": [source.pk]}
2256
+ result = self.filterset(params, self.queryset).qs
2257
+ # Should find at least our one snapshot for this source
2258
+ self.assertGreaterEqual(result.count(), 1)
2259
+ # Verify it's the right snapshot
2260
+ self.assertTrue(result.filter(snapshot_id="snap-gamma-003").exists())
2261
+
2262
+ def test_search(self):
2263
+ """Test search functionality"""
2264
+ params = {"q": "Alpha"}
2265
+ result = self.filterset(params, self.queryset).qs
2266
+ self.assertGreaterEqual(result.count(), 1)
2267
+
2268
+
2269
+ class IPFabricDataFilterSetTestCase(TestCase):
2270
+ filterset = IPFabricDataFilterSet
2271
+
2272
+ @classmethod
2273
+ def setUpTestData(cls):
2274
+ from ipfabric_netbox.models import IPFabricData
2275
+
2276
+ source = IPFabricSource.objects.create(
2277
+ name="Data Test Source",
2278
+ url="https://data.example.com",
2279
+ parameters={},
2280
+ )
2281
+ cls.snapshot = IPFabricSnapshot.objects.create(
2282
+ name="Data Test Snapshot",
2283
+ source=source,
2284
+ snapshot_id="snap-data-001",
2285
+ status="loaded",
2286
+ )
2287
+
2288
+ data_items = [
2289
+ IPFabricData(
2290
+ snapshot_data=cls.snapshot,
2291
+ data={"device": "router1", "ip": "10.0.0.1"},
2292
+ ),
2293
+ IPFabricData(
2294
+ snapshot_data=cls.snapshot,
2295
+ data={"device": "switch1", "ip": "10.0.0.2"},
2296
+ ),
2297
+ IPFabricData(
2298
+ snapshot_data=cls.snapshot,
2299
+ data={"device": "firewall1", "ip": "10.0.0.3"},
2300
+ ),
2301
+ ]
2302
+ IPFabricData.objects.bulk_create(data_items)
2303
+
2304
+ @property
2305
+ def queryset(self):
2306
+ return IPFabricData.objects.all()
2307
+
2308
+ def test_snapshot_data(self):
2309
+ """Test filtering by snapshot_data"""
2310
+ params = {"snapshot_data": self.snapshot.pk}
2311
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
2312
+
2313
+ def test_search(self):
2314
+ """Test search functionality"""
2315
+ # The search method filters on snapshot_data__name
2316
+ params = {"q": "Data Test Snapshot"}
2317
+ result = self.filterset(params, self.queryset).qs
2318
+ # Should find all data items for our snapshot
2319
+ self.assertEqual(result.count(), 3)
2320
+
2321
+
2322
+ class IPFabricIngestionFilterSetTestCase(TestCase):
2323
+ filterset = IPFabricIngestionFilterSet
2324
+
2325
+ @classmethod
2326
+ def setUpTestData(cls):
2327
+ # Create sources
2328
+ source1 = IPFabricSource.objects.create(
2329
+ name="Ingestion Test Source 1",
2330
+ url="https://ing1.example.com",
2331
+ parameters={},
2332
+ )
2333
+ source2 = IPFabricSource.objects.create(
2334
+ name="Ingestion Test Source 2",
2335
+ url="https://ing2.example.com",
2336
+ parameters={},
2337
+ )
2338
+
2339
+ # Create snapshots
2340
+ snapshot1 = IPFabricSnapshot.objects.create(
2341
+ name="Ingestion Snapshot 1",
2342
+ source=source1,
2343
+ snapshot_id="ing-snap-1",
2344
+ status="loaded",
2345
+ )
2346
+ snapshot2 = IPFabricSnapshot.objects.create(
2347
+ name="Ingestion Snapshot 2",
2348
+ source=source2,
2349
+ snapshot_id="ing-snap-2",
2350
+ status="loaded",
2351
+ )
2352
+
2353
+ # Create syncs
2354
+ cls.sync1 = IPFabricSync.objects.create(
2355
+ name="Ingestion Sync 1",
2356
+ snapshot_data=snapshot1,
2357
+ parameters={},
2358
+ )
2359
+ cls.sync2 = IPFabricSync.objects.create(
2360
+ name="Ingestion Sync 2",
2361
+ snapshot_data=snapshot2,
2362
+ parameters={},
2363
+ )
2364
+
2365
+ # Create branches - need unique branches since branch is OneToOneField
2366
+ cls.branch1 = Branch.objects.create(name="Ingestion Branch 1")
2367
+ cls.branch2 = Branch.objects.create(name="Ingestion Branch 2")
2368
+ cls.branch3 = Branch.objects.create(name="Ingestion Branch 3")
2369
+
2370
+ # Create ingestions - each needs its own unique branch
2371
+ cls.ingestions = [
2372
+ IPFabricIngestion(
2373
+ sync=cls.sync1,
2374
+ branch=cls.branch1,
2375
+ ),
2376
+ IPFabricIngestion(
2377
+ sync=cls.sync1,
2378
+ branch=cls.branch2,
2379
+ ),
2380
+ IPFabricIngestion(
2381
+ sync=cls.sync2,
2382
+ branch=cls.branch3,
2383
+ ),
2384
+ ]
2385
+ IPFabricIngestion.objects.bulk_create(cls.ingestions)
2386
+
2387
+ @property
2388
+ def queryset(self):
2389
+ return IPFabricIngestion.objects.all()
2390
+
2391
+ def test_id(self):
2392
+ """Test filtering by ingestion ID"""
2393
+ ingestion = IPFabricIngestion.objects.first()
2394
+ params = {"id": [ingestion.pk]}
2395
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
2396
+
2397
+ def test_branch(self):
2398
+ """Test filtering by branch"""
2399
+ params = {"branch": self.branch1.pk}
2400
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
2401
+
2402
+ def test_sync_id(self):
2403
+ """Test filtering by sync ID"""
2404
+ params = {"sync_id": [self.sync1.pk]}
2405
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
2406
+
2407
+ def test_search(self):
2408
+ """Test search functionality"""
2409
+ params = {"q": "Ingestion Branch 1"}
2410
+ result = self.filterset(params, self.queryset).qs
2411
+ self.assertGreaterEqual(result.count(), 1)
2412
+
2413
+
2414
+ class IPFabricIngestionIssueFilterSetTestCase(TestCase):
2415
+ filterset = IPFabricIngestionIssueFilterSet
2416
+
2417
+ @classmethod
2418
+ def setUpTestData(cls):
2419
+ source = IPFabricSource.objects.create(
2420
+ name="Issue Test Source",
2421
+ url="https://issue.example.com",
2422
+ parameters={},
2423
+ )
2424
+ snapshot = IPFabricSnapshot.objects.create(
2425
+ name="Issue Snapshot",
2426
+ source=source,
2427
+ snapshot_id="issue-snap-1",
2428
+ status="loaded",
2429
+ )
2430
+ sync = IPFabricSync.objects.create(
2431
+ name="Issue Sync",
2432
+ snapshot_data=snapshot,
2433
+ parameters={},
2434
+ )
2435
+ branch = Branch.objects.create(name="Issue Branch")
2436
+ ingestion = IPFabricIngestion.objects.create(
2437
+ sync=sync,
2438
+ branch=branch,
2439
+ )
2440
+
2441
+ cls.issues = [
2442
+ IPFabricIngestionIssue(
2443
+ ingestion=ingestion,
2444
+ model="dcim.Device",
2445
+ raw_data={"hostname": "device1"},
2446
+ coalesce_fields=["name"],
2447
+ defaults={"status": "active"},
2448
+ exception="ValueError",
2449
+ message="Device validation error",
2450
+ ),
2451
+ IPFabricIngestionIssue(
2452
+ ingestion=ingestion,
2453
+ model="ipam.IPAddress",
2454
+ raw_data={"address": "10.0.0.1"},
2455
+ coalesce_fields=["address"],
2456
+ defaults={"status": "active"},
2457
+ exception="IntegrityError",
2458
+ message="Duplicate IP address",
2459
+ ),
2460
+ IPFabricIngestionIssue(
2461
+ ingestion=ingestion,
2462
+ model="dcim.Interface",
2463
+ raw_data={"name": "eth0"},
2464
+ coalesce_fields=["name", "device"],
2465
+ defaults={},
2466
+ exception="KeyError",
2467
+ message="Missing device reference",
2468
+ ),
2469
+ ]
2470
+ IPFabricIngestionIssue.objects.bulk_create(cls.issues)
2471
+
2472
+ @property
2473
+ def queryset(self):
2474
+ return IPFabricIngestionIssue.objects.all()
2475
+
2476
+ def test_model(self):
2477
+ """Test filtering by model (exact match)"""
2478
+ # First verify we have test data
2479
+ total_issues = self.queryset.count()
2480
+ self.assertGreater(total_issues, 0, "No ingestion issues found in queryset")
2481
+
2482
+ # Check what models exist
2483
+ models_in_db = list(self.queryset.values_list("model", flat=True))
2484
+ self.assertIn("dcim.Device", models_in_db, f"dcim.Device not in {models_in_db}")
2485
+
2486
+ params = {"model": ["dcim.Device"]}
2487
+ result = self.filterset(params, self.queryset).qs
2488
+ # Should find exactly one device issue
2489
+ self.assertEqual(result.count(), 1)
2490
+ self.assertEqual(result.first().model, "dcim.Device")
2491
+
2492
+ def test_exception(self):
2493
+ """Test filtering by exception (exact match)"""
2494
+ params = {"exception": "ValueError"}
2495
+ result = self.filterset(params, self.queryset).qs
2496
+ self.assertEqual(result.count(), 1)
2497
+ self.assertEqual(result.first().model, "dcim.Device")
2498
+
2499
+ def test_message(self):
2500
+ """Test filtering by message (exact match)"""
2501
+ params = {"message": "Device validation error"}
2502
+ result = self.filterset(params, self.queryset).qs
2503
+ self.assertEqual(result.count(), 1)
2504
+
2505
+ def test_search(self):
2506
+ """Test search functionality"""
2507
+ params = {"q": "Device"}
2508
+ result = self.filterset(params, self.queryset).qs
2509
+ self.assertGreaterEqual(result.count(), 1)
2510
+
2511
+ params = {"q": "ValueError"}
2512
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
2513
+
2514
+ params = {"q": "duplicate"}
2515
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
2516
+
2517
+
2518
+ class IPFabricIngestionChangeFilterSetTestCase(TestCase):
2519
+ filterset = IPFabricIngestionChangeFilterSet
2520
+
2521
+ @classmethod
2522
+ def setUpTestData(cls):
2523
+ # Create a branch
2524
+ cls.branch = Branch.objects.create(name="Change Test Branch")
2525
+
2526
+ # Get content type for Device
2527
+ cls.device_ct = ContentType.objects.get(app_label="dcim", model="device")
2528
+ cls.site_ct = ContentType.objects.get(app_label="dcim", model="site")
2529
+
2530
+ # Create change diffs
2531
+ cls.changes = [
2532
+ ChangeDiff(
2533
+ branch=cls.branch,
2534
+ object_type=cls.device_ct,
2535
+ object_id=1,
2536
+ action="create",
2537
+ current={"name": "device1", "status": "active"},
2538
+ modified={},
2539
+ original={},
2540
+ ),
2541
+ ChangeDiff(
2542
+ branch=cls.branch,
2543
+ object_type=cls.device_ct,
2544
+ object_id=2,
2545
+ action="update",
2546
+ current={"name": "device2", "status": "planned"},
2547
+ modified={"status": "active"},
2548
+ original={"name": "device2", "status": "planned"},
2549
+ ),
2550
+ ChangeDiff(
2551
+ branch=cls.branch,
2552
+ object_type=cls.site_ct,
2553
+ object_id=1,
2554
+ action="delete",
2555
+ current={},
2556
+ modified={},
2557
+ original={"name": "site1"},
2558
+ ),
2559
+ ]
2560
+ ChangeDiff.objects.bulk_create(cls.changes)
2561
+
2562
+ @property
2563
+ def queryset(self):
2564
+ return ChangeDiff.objects.all()
2565
+
2566
+ def test_branch(self):
2567
+ """Test filtering by branch"""
2568
+ params = {"branch": self.branch.pk}
2569
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
2570
+
2571
+ def test_action(self):
2572
+ """Test filtering by action"""
2573
+ params = {"action": ["create"]}
2574
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
2575
+
2576
+ params = {"action": ["update", "delete"]}
2577
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
2578
+
2579
+ def test_object_type(self):
2580
+ """Test filtering by object_type"""
2581
+ params = {"object_type": self.device_ct.pk}
2582
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
2583
+
2584
+ def test_search(self):
2585
+ """Test search functionality"""
2586
+ params = {"q": "device1"}
2587
+ result = self.filterset(params, self.queryset).qs
2588
+ self.assertGreaterEqual(result.count(), 1)
2589
+
2590
+ params = {"q": "create"}
2591
+ result = self.filterset(params, self.queryset).qs
2592
+ self.assertGreaterEqual(result.count(), 1)