ipfabric_netbox 4.2.0b7__py3-none-any.whl → 4.2.0b9__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.

@@ -0,0 +1,1440 @@
1
+ from datetime import timedelta
2
+ from unittest.mock import patch
3
+
4
+ from core.choices import DataSourceStatusChoices
5
+ from dcim.models import Device
6
+ from dcim.models import Site
7
+ from django.contrib.contenttypes.models import ContentType
8
+ from django.test import TestCase
9
+ from utilities.datetime import local_now
10
+
11
+ from ipfabric_netbox.choices import IPFabricSnapshotStatusModelChoices
12
+ from ipfabric_netbox.choices import IPFabricSourceTypeChoices
13
+ from ipfabric_netbox.forms import IPFabricIngestionFilterForm
14
+ from ipfabric_netbox.forms import IPFabricIngestionMergeForm
15
+ from ipfabric_netbox.forms import IPFabricRelationshipFieldForm
16
+ from ipfabric_netbox.forms import IPFabricSnapshotFilterForm
17
+ from ipfabric_netbox.forms import IPFabricSourceFilterForm
18
+ from ipfabric_netbox.forms import IPFabricSourceForm
19
+ from ipfabric_netbox.forms import IPFabricSyncForm
20
+ from ipfabric_netbox.forms import IPFabricTransformFieldForm
21
+ from ipfabric_netbox.forms import IPFabricTransformMapCloneForm
22
+ from ipfabric_netbox.forms import IPFabricTransformMapForm
23
+ from ipfabric_netbox.forms import IPFabricTransformMapGroupForm
24
+ from ipfabric_netbox.models import IPFabricRelationshipField
25
+ from ipfabric_netbox.models import IPFabricSnapshot
26
+ from ipfabric_netbox.models import IPFabricSource
27
+ from ipfabric_netbox.models import IPFabricSync
28
+ from ipfabric_netbox.models import IPFabricTransformField
29
+ from ipfabric_netbox.models import IPFabricTransformMap
30
+ from ipfabric_netbox.models import IPFabricTransformMapGroup
31
+
32
+
33
+ class IPFabricSourceFormTestCase(TestCase):
34
+ @classmethod
35
+ def setUpTestData(cls):
36
+ # Create a test IPFabricSource instance for form tests
37
+ cls.ipfabric_source = IPFabricSource.objects.create(
38
+ name="Test IP Fabric Source",
39
+ type=IPFabricSourceTypeChoices.LOCAL,
40
+ url="https://test.ipfabric.local",
41
+ status=DataSourceStatusChoices.NEW,
42
+ parameters={"auth": "test_token", "verify": True, "timeout": 30},
43
+ )
44
+
45
+ def test_fields_are_required(self):
46
+ form = IPFabricSourceForm(data={})
47
+ self.assertFalse(form.is_valid(), form.errors)
48
+ self.assertIn("name", form.errors)
49
+ self.assertIn("type", form.errors)
50
+ self.assertIn("url", form.errors)
51
+
52
+ def test_fields_are_optional(self):
53
+ form = IPFabricSourceForm(
54
+ data={
55
+ "name": "Test No Comments Source",
56
+ "type": IPFabricSourceTypeChoices.REMOTE,
57
+ "url": "https://test.ipfabric.local",
58
+ }
59
+ )
60
+ self.assertTrue(form.is_valid(), form.errors)
61
+
62
+ def test_type_must_be_defined_choice(self):
63
+ form = IPFabricSourceForm(
64
+ data={
65
+ "name": "Test Source",
66
+ "type": "invalid_type",
67
+ "url": "https://test.ipfabric.local",
68
+ }
69
+ )
70
+ self.assertFalse(form.is_valid(), form.errors)
71
+ self.assertIn("type", form.errors)
72
+ self.assertTrue(form.errors["type"][-1].startswith("Select a valid choice."))
73
+
74
+ def test_valid_local_source_form(self):
75
+ form = IPFabricSourceForm(
76
+ data={
77
+ "name": "Test Local Source",
78
+ "type": IPFabricSourceTypeChoices.LOCAL,
79
+ "url": "https://test.ipfabric.local",
80
+ "auth": "test_api_token",
81
+ "verify": False,
82
+ "timeout": 45,
83
+ "description": "Test local IP Fabric source",
84
+ "comments": "Test comments",
85
+ }
86
+ )
87
+ self.assertTrue(form.is_valid(), form.errors)
88
+ instance = form.save()
89
+
90
+ # Check that parameters are properly stored
91
+ self.assertEqual(instance.parameters["auth"], "test_api_token")
92
+ self.assertEqual(instance.parameters["verify"], False)
93
+ self.assertEqual(instance.parameters["timeout"], 45)
94
+
95
+ def test_valid_remote_source_form(self):
96
+ form = IPFabricSourceForm(
97
+ data={
98
+ "name": "Test Remote Source",
99
+ "type": IPFabricSourceTypeChoices.REMOTE,
100
+ "url": "https://remote.ipfabric.local",
101
+ "timeout": 60,
102
+ "description": "Test remote IP Fabric source",
103
+ "comments": "Test comments",
104
+ }
105
+ )
106
+ self.assertTrue(form.is_valid(), form.errors)
107
+
108
+ def test_local_source_requires_auth_token(self):
109
+ # Test that when type is 'local', auth field becomes required
110
+ form = IPFabricSourceForm(
111
+ data={
112
+ "name": "Test Local Source",
113
+ "type": IPFabricSourceTypeChoices.LOCAL,
114
+ "url": "https://test.ipfabric.local",
115
+ "verify": True,
116
+ "timeout": 30,
117
+ }
118
+ )
119
+ # Since auth is dynamically added as required for local sources
120
+ # we need to check if the form properly handles this validation
121
+ self.assertFalse(form.is_valid(), form.errors)
122
+ self.assertIn("auth", form.errors)
123
+
124
+ def test_form_save_sets_status_to_new(self):
125
+ form = IPFabricSourceForm(
126
+ data={
127
+ "name": "Test Save Source",
128
+ "type": IPFabricSourceTypeChoices.LOCAL,
129
+ "url": "https://test.ipfabric.local",
130
+ "auth": "test_api_token",
131
+ "verify": True,
132
+ "timeout": 30,
133
+ }
134
+ )
135
+ self.assertTrue(form.is_valid(), form.errors)
136
+ instance = form.save()
137
+ self.assertEqual(instance.status, DataSourceStatusChoices.NEW)
138
+
139
+ def test_form_initializes_existing_parameters(self):
140
+ # Test that form properly initializes with existing instance parameters
141
+ form = IPFabricSourceForm(instance=self.ipfabric_source)
142
+
143
+ # Check that the form fields are initialized with the instance's parameters
144
+ self.assertEqual(form.fields["auth"].initial, "test_token")
145
+ self.assertEqual(form.fields["verify"].initial, True)
146
+ self.assertEqual(form.fields["timeout"].initial, 30)
147
+
148
+ def test_remote_source_creates_last_snapshot(self):
149
+ """Check that $last snapshot is created for remote sources"""
150
+ from ipfabric_netbox.models import IPFabricSnapshot
151
+
152
+ self.assertEqual(IPFabricSnapshot.objects.count(), 0)
153
+
154
+ form = IPFabricSourceForm(
155
+ data={
156
+ "name": "Test Remote Snapshot Source",
157
+ "type": IPFabricSourceTypeChoices.REMOTE,
158
+ "url": "https://remote.ipfabric.local",
159
+ "timeout": 30,
160
+ }
161
+ )
162
+ self.assertTrue(form.is_valid(), form.errors)
163
+ instance = form.save()
164
+
165
+ last_snapshot = IPFabricSnapshot.objects.filter(
166
+ source=instance, snapshot_id="$last"
167
+ ).first()
168
+ self.assertIsNotNone(last_snapshot)
169
+ self.assertEqual(last_snapshot.name, "$last")
170
+
171
+ def test_fieldsets_for_remote_source_type(self):
172
+ """Test that fieldsets property returns correct structure for remote source type"""
173
+ form = IPFabricSourceForm(
174
+ data={
175
+ "name": "Test Remote Source",
176
+ "type": IPFabricSourceTypeChoices.REMOTE,
177
+ "url": "https://remote.ipfabric.local",
178
+ }
179
+ )
180
+
181
+ fieldsets = form.fieldsets
182
+
183
+ # Should have 2 fieldsets for remote type
184
+ self.assertEqual(len(fieldsets), 2)
185
+
186
+ # First fieldset should be for Source
187
+ self.assertEqual(fieldsets[0].name, "Source")
188
+
189
+ # Second fieldset should be for Parameters
190
+ self.assertEqual(fieldsets[1].name, "Parameters")
191
+
192
+ # Verify the remote type fieldsets match the expected structure from forms.py
193
+ # For remote type: FieldSet("timeout", name=_("Parameters"))
194
+ # This means the Parameters fieldset should only contain timeout field
195
+ self.assertEqual(len(form.fieldsets), 2)
196
+ self.assertEqual(form.fieldsets[0].name, "Source")
197
+ self.assertEqual(form.fieldsets[1].name, "Parameters")
198
+
199
+ # For remote sources, verify that auth and verify fields are NOT in the form
200
+ # (they are only added for local sources in the __init__ method)
201
+ self.assertNotIn("auth", form.fields)
202
+ self.assertNotIn("verify", form.fields)
203
+ # But timeout should be present for all source types
204
+ self.assertIn("timeout", form.fields)
205
+
206
+ def test_fieldsets_for_local_source_type(self):
207
+ """Test that fieldsets property returns correct structure for local source type"""
208
+ form = IPFabricSourceForm(
209
+ data={
210
+ "name": "Test Local Source",
211
+ "type": IPFabricSourceTypeChoices.LOCAL,
212
+ "url": "https://local.ipfabric.local",
213
+ "auth": "test_token",
214
+ "verify": True,
215
+ "timeout": 30,
216
+ }
217
+ )
218
+
219
+ fieldsets = form.fieldsets
220
+
221
+ # Should have 2 fieldsets for local type as well
222
+ self.assertEqual(len(fieldsets), 2)
223
+
224
+ # First fieldset should be for Source
225
+ self.assertEqual(fieldsets[0].name, "Source")
226
+
227
+ # Second fieldset should be for Parameters
228
+ self.assertEqual(fieldsets[1].name, "Parameters")
229
+
230
+ # Verify the local type fieldsets match the expected structure from forms.py
231
+ # For local type: FieldSet("auth", "verify", "timeout", name=_("Parameters"))
232
+ # This means the Parameters fieldset should contain auth, verify, and timeout fields
233
+ self.assertEqual(len(form.fieldsets), 2)
234
+ self.assertEqual(form.fieldsets[0].name, "Source")
235
+ self.assertEqual(form.fieldsets[1].name, "Parameters")
236
+
237
+ # For local sources, verify that auth, verify, and timeout fields ARE in the form
238
+ # (they are dynamically added for local sources in the __init__ method)
239
+ self.assertIn("auth", form.fields)
240
+ self.assertIn("verify", form.fields)
241
+ self.assertIn("timeout", form.fields)
242
+
243
+ # Verify that the auth field is required for local sources
244
+ self.assertTrue(form.fields["auth"].required)
245
+ # Verify that verify field is optional (BooleanField with required=False)
246
+ self.assertFalse(form.fields["verify"].required)
247
+ # Verify that timeout field is optional
248
+ self.assertFalse(form.fields["timeout"].required)
249
+
250
+ def test_fieldsets_with_no_source_type_set(self):
251
+ """Test fieldsets behavior when source_type is None or not set"""
252
+ form = IPFabricSourceForm()
253
+
254
+ # When no source_type is set, should default to basic fieldsets (non-local behavior)
255
+ fieldsets = form.fieldsets
256
+
257
+ self.assertEqual(len(fieldsets), 2)
258
+ self.assertEqual(fieldsets[0].name, "Source")
259
+ self.assertEqual(fieldsets[1].name, "Parameters")
260
+
261
+ def test_fieldsets_with_existing_instance_local_type(self):
262
+ """Test fieldsets behavior with an existing local source instance"""
263
+ form = IPFabricSourceForm(instance=self.ipfabric_source)
264
+
265
+ fieldsets = form.fieldsets
266
+
267
+ # Should have extended fieldsets for local type since test instance is local
268
+ self.assertEqual(len(fieldsets), 2)
269
+ self.assertEqual(fieldsets[1].name, "Parameters")
270
+
271
+ def test_fieldsets_dynamic_behavior_consistency(self):
272
+ """Test that fieldsets method consistently returns the same structure for same source_type"""
273
+ # Test local type consistency
274
+ form_local_1 = IPFabricSourceForm(
275
+ data={"type": IPFabricSourceTypeChoices.LOCAL}
276
+ )
277
+ form_local_2 = IPFabricSourceForm(
278
+ data={"type": IPFabricSourceTypeChoices.LOCAL}
279
+ )
280
+
281
+ fieldsets_1 = form_local_1.fieldsets
282
+ fieldsets_2 = form_local_2.fieldsets
283
+
284
+ # Both should have the same structure
285
+ self.assertEqual(len(fieldsets_1), len(fieldsets_2))
286
+ self.assertEqual(fieldsets_1[0].name, fieldsets_2[0].name)
287
+ self.assertEqual(fieldsets_1[1].name, fieldsets_2[1].name)
288
+
289
+ # Test remote type consistency
290
+ form_remote_1 = IPFabricSourceForm(
291
+ data={"type": IPFabricSourceTypeChoices.REMOTE}
292
+ )
293
+ form_remote_2 = IPFabricSourceForm(
294
+ data={"type": IPFabricSourceTypeChoices.REMOTE}
295
+ )
296
+
297
+ fieldsets_remote_1 = form_remote_1.fieldsets
298
+ fieldsets_remote_2 = form_remote_2.fieldsets
299
+
300
+ # Both should have the same structure
301
+ self.assertEqual(len(fieldsets_remote_1), len(fieldsets_remote_2))
302
+ self.assertEqual(fieldsets_remote_1[0].name, fieldsets_remote_2[0].name)
303
+ self.assertEqual(fieldsets_remote_1[1].name, fieldsets_remote_2[1].name)
304
+
305
+ def test_fieldsets_source_type_changes_parameters_fieldset(self):
306
+ """Test that changing source_type results in different parameters fieldset"""
307
+ # Create forms with different source types
308
+ form_local = IPFabricSourceForm(data={"type": IPFabricSourceTypeChoices.LOCAL})
309
+ form_remote = IPFabricSourceForm(
310
+ data={"type": IPFabricSourceTypeChoices.REMOTE}
311
+ )
312
+
313
+ fieldsets_local = form_local.fieldsets
314
+ fieldsets_remote = form_remote.fieldsets
315
+
316
+ # Both should have same number of fieldsets
317
+ self.assertEqual(len(fieldsets_local), 2)
318
+ self.assertEqual(len(fieldsets_remote), 2)
319
+
320
+ # Both should have same Source fieldset name
321
+ self.assertEqual(fieldsets_local[0].name, fieldsets_remote[0].name)
322
+ self.assertEqual(fieldsets_local[0].name, "Source")
323
+
324
+ # Both should have Parameters fieldset, but they should be different objects
325
+ # (one with basic timeout, one with auth, verify, timeout)
326
+ self.assertEqual(fieldsets_local[1].name, "Parameters")
327
+ self.assertEqual(fieldsets_remote[1].name, "Parameters")
328
+
329
+ # The fieldsets should be different objects since they contain different fields
330
+ # We can't easily test field contents without knowing FieldSet internals,
331
+ # but we can verify the method creates new objects as expected
332
+ self.assertIsInstance(fieldsets_local[1], type(fieldsets_remote[1]))
333
+
334
+
335
+ class IPFabricRelationshipFieldFormTestCase(TestCase):
336
+ @classmethod
337
+ def setUpTestData(cls):
338
+ cls.source = IPFabricSource.objects.create(
339
+ name="Test Source",
340
+ type=IPFabricSourceTypeChoices.LOCAL,
341
+ url="https://test.ipfabric.local",
342
+ status=DataSourceStatusChoices.NEW,
343
+ )
344
+
345
+ cls.transform_map_group = IPFabricTransformMapGroup.objects.create(
346
+ name="Test Group", description="Test group description"
347
+ )
348
+
349
+ cls.device_content_type = ContentType.objects.get_for_model(Device)
350
+ cls.site_content_type = ContentType.objects.get_for_model(Site)
351
+
352
+ cls.transform_map = IPFabricTransformMap.objects.create(
353
+ name="Test Transform Map",
354
+ group=cls.transform_map_group,
355
+ source_model="device",
356
+ target_model=cls.device_content_type,
357
+ )
358
+
359
+ def test_fields_are_required(self):
360
+ form = IPFabricRelationshipFieldForm(data={})
361
+ self.assertFalse(form.is_valid(), form.errors)
362
+ self.assertIn("transform_map", form.errors)
363
+ self.assertIn("source_model", form.errors)
364
+ self.assertIn("target_field", form.errors)
365
+
366
+ def test_fields_are_optional(self):
367
+ form = IPFabricRelationshipFieldForm(
368
+ data={
369
+ "transform_map": self.transform_map.pk,
370
+ "source_model": self.device_content_type.pk,
371
+ "target_field": "site",
372
+ }
373
+ )
374
+ self.assertTrue(form.is_valid(), form.errors)
375
+
376
+ def test_valid_relationship_field_form(self):
377
+ # Initialize form with transform_map to set up field choices
378
+ form = IPFabricRelationshipFieldForm(
379
+ initial={"transform_map": self.transform_map.pk},
380
+ data={
381
+ "transform_map": self.transform_map.pk,
382
+ "source_model": self.device_content_type.pk,
383
+ "target_field": "site",
384
+ "coalesce": True,
385
+ "template": "{{ object.siteName }}",
386
+ },
387
+ )
388
+ self.assertTrue(form.is_valid(), form.errors)
389
+
390
+ def test_coalesce_field_defaults_to_false(self):
391
+ # Initialize form with transform_map to set up field choices
392
+ form = IPFabricRelationshipFieldForm(
393
+ initial={"transform_map": self.transform_map.pk},
394
+ data={
395
+ "transform_map": self.transform_map.pk,
396
+ "source_model": self.device_content_type.pk, # Use ContentType pk instead of string
397
+ "target_field": "site",
398
+ },
399
+ )
400
+ # Since the form requires dynamic field setup, let's manually set the choices
401
+ form.fields["target_field"].widget.choices = [("site", "Site")]
402
+ self.assertTrue(form.is_valid(), form.errors)
403
+ instance = form.save()
404
+ self.assertFalse(instance.coalesce)
405
+
406
+ def test_form_initialization_with_existing_instance_no_data(self):
407
+ """Test no self.data with existing instance"""
408
+ # Create an existing IPFabricRelationshipField instance
409
+ relationship_field = IPFabricRelationshipField.objects.create(
410
+ transform_map=self.transform_map,
411
+ source_model=self.device_content_type,
412
+ target_field="site",
413
+ coalesce=True,
414
+ )
415
+
416
+ # Initialize form with existing instance but no data
417
+ form = IPFabricRelationshipFieldForm(instance=relationship_field)
418
+
419
+ # Verify that the form sets up field choices based on the existing instance
420
+ self.assertIsNotNone(form.fields["target_field"].widget.choices)
421
+ self.assertEqual(form.fields["target_field"].widget.initial, "site")
422
+
423
+ def test_form_initialization_with_initial_transform_map_no_data(self):
424
+ """Test no self.data with initial transform_map"""
425
+ # Initialize form with initial transform_map but no data
426
+ form = IPFabricRelationshipFieldForm(
427
+ initial={"transform_map": self.transform_map.pk}
428
+ )
429
+
430
+ # Verify that the form sets up field choices based on the transform_map
431
+ self.assertIsNotNone(form.fields["target_field"].widget.choices)
432
+ # Verify choices contain relation fields (excluding exclude_fields)
433
+ target_choices = form.fields["target_field"].widget.choices
434
+ self.assertTrue(len(target_choices) > 0)
435
+
436
+ def test_form_initialization_without_initial_data_no_data(self):
437
+ """Test no self.data without initial transform_map"""
438
+ # Initialize form without initial data and no data
439
+ form = IPFabricRelationshipFieldForm()
440
+
441
+ # Verify that the form doesn't crash and has default field setup
442
+ self.assertIsNotNone(form.fields["source_model"])
443
+ self.assertIsNotNone(form.fields["target_field"])
444
+ # Widget choices should be empty or default since no transform_map is provided
445
+ self.assertTrue(hasattr(form.fields["target_field"], "widget"))
446
+ self.assertTrue(hasattr(form.fields["source_model"], "widget"))
447
+
448
+
449
+ class IPFabricTransformFieldFormTestCase(TestCase):
450
+ @classmethod
451
+ def setUpTestData(cls):
452
+ cls.source = IPFabricSource.objects.create(
453
+ name="Test Source",
454
+ type=IPFabricSourceTypeChoices.LOCAL,
455
+ url="https://test.ipfabric.local",
456
+ status=DataSourceStatusChoices.NEW,
457
+ )
458
+
459
+ cls.transform_map_group = IPFabricTransformMapGroup.objects.create(
460
+ name="Test Group", description="Test group description"
461
+ )
462
+
463
+ cls.device_content_type = ContentType.objects.get_for_model(Device)
464
+
465
+ cls.transform_map = IPFabricTransformMap.objects.create(
466
+ name="Test Transform Map",
467
+ group=cls.transform_map_group,
468
+ source_model="device",
469
+ target_model=cls.device_content_type,
470
+ )
471
+
472
+ def test_fields_are_required(self):
473
+ form = IPFabricTransformFieldForm(data={})
474
+ self.assertFalse(form.is_valid(), form.errors)
475
+ self.assertIn("source_field", form.errors)
476
+ self.assertIn("target_field", form.errors)
477
+ self.assertIn("transform_map", form.errors)
478
+
479
+ def test_fields_are_optional(self):
480
+ form = IPFabricTransformFieldForm(
481
+ data={
482
+ "transform_map": self.transform_map.pk,
483
+ "source_field": "hostname",
484
+ "target_field": "name",
485
+ },
486
+ )
487
+ self.assertTrue(form.is_valid(), form.errors)
488
+
489
+ def test_valid_transform_field_form(self):
490
+ # Initialize form with transform_map to set up field choices
491
+ form = IPFabricTransformFieldForm(
492
+ initial={"transform_map": self.transform_map.pk},
493
+ data={
494
+ "transform_map": self.transform_map.pk,
495
+ "source_field": "hostname",
496
+ "target_field": "name",
497
+ "coalesce": True,
498
+ "template": "{{ object.hostname }}",
499
+ },
500
+ )
501
+ self.assertTrue(form.is_valid(), form.errors)
502
+
503
+ def test_coalesce_field_defaults_to_false(self):
504
+ # Initialize form with transform_map to set up field choices
505
+ form = IPFabricTransformFieldForm(
506
+ initial={"transform_map": self.transform_map.pk},
507
+ data={
508
+ "transform_map": self.transform_map.pk,
509
+ "source_field": "hostname",
510
+ "target_field": "name",
511
+ },
512
+ )
513
+ self.assertTrue(form.is_valid(), form.errors)
514
+ instance = form.save()
515
+ self.assertFalse(instance.coalesce)
516
+
517
+ def test_form_initialization_with_existing_instance_no_data(self):
518
+ """Test no data with existing instance"""
519
+ # Create an existing IPFabricTransformField instance
520
+ transform_field = IPFabricTransformField.objects.create(
521
+ transform_map=self.transform_map,
522
+ source_field="hostname",
523
+ target_field="name",
524
+ coalesce=True,
525
+ )
526
+
527
+ # Initialize form with existing instance but no data
528
+ form = IPFabricTransformFieldForm(instance=transform_field)
529
+
530
+ # Verify that the form sets up field choices based on the existing instance
531
+ self.assertIsNotNone(form.fields["target_field"].widget.choices)
532
+ self.assertIsNotNone(form.fields["source_field"].widget.choices)
533
+ self.assertEqual(form.fields["target_field"].widget.initial, "name")
534
+
535
+ def test_form_initialization_with_initial_transform_map_no_data(self):
536
+ """Test no data with initial transform_map"""
537
+ # Initialize form with initial transform_map but no data
538
+ form = IPFabricTransformFieldForm(
539
+ initial={"transform_map": self.transform_map.pk}
540
+ )
541
+
542
+ # Verify that the form sets up field choices based on the transform_map
543
+ self.assertIsNotNone(form.fields["target_field"].widget.choices)
544
+ self.assertIsNotNone(form.fields["source_field"].widget.choices)
545
+ # Verify choices contain non-relation fields (excluding exclude_fields)
546
+ target_choices = form.fields["target_field"].widget.choices
547
+ self.assertTrue(len(target_choices) > 0)
548
+
549
+ def test_form_initialization_without_initial_data_no_data(self):
550
+ """Test no data without initial transform_map"""
551
+ # Initialize form without initial data and no data
552
+ form = IPFabricTransformFieldForm()
553
+
554
+ # Verify that the form doesn't crash and has default field setup
555
+ self.assertIsNotNone(form.fields["source_field"])
556
+ self.assertIsNotNone(form.fields["target_field"])
557
+ # Widget choices should be empty or default since no transform_map is provided
558
+ self.assertTrue(hasattr(form.fields["target_field"], "widget"))
559
+ self.assertTrue(hasattr(form.fields["source_field"], "widget"))
560
+
561
+
562
+ class IPFabricTransformMapGroupFormTestCase(TestCase):
563
+ def test_fields_are_required(self):
564
+ form = IPFabricTransformMapGroupForm(data={})
565
+ self.assertFalse(form.is_valid(), form.errors)
566
+ self.assertIn("name", form.errors)
567
+
568
+ def test_fields_are_optional(self):
569
+ form = IPFabricTransformMapGroupForm(data={"name": "Test Group"})
570
+ self.assertTrue(form.is_valid(), form.errors)
571
+
572
+ def test_valid_transform_map_group_form(self):
573
+ form = IPFabricTransformMapGroupForm(
574
+ data={"name": "Test Group", "description": "Test group description"}
575
+ )
576
+ self.assertTrue(form.is_valid(), form.errors)
577
+ instance = form.save()
578
+ self.assertEqual(instance.name, "Test Group")
579
+ self.assertEqual(instance.description, "Test group description")
580
+
581
+
582
+ class IPFabricTransformMapFormTestCase(TestCase):
583
+ @classmethod
584
+ def setUpTestData(cls):
585
+ cls.transform_map_group = IPFabricTransformMapGroup.objects.create(
586
+ name="Test Group", description="Test group description"
587
+ )
588
+ cls.device_content_type = ContentType.objects.get_for_model(Device)
589
+
590
+ def test_fields_are_required(self):
591
+ form = IPFabricTransformMapForm(data={})
592
+ self.assertFalse(form.is_valid(), form.errors)
593
+ self.assertIn("name", form.errors)
594
+ self.assertIn("source_model", form.errors)
595
+ self.assertIn("target_model", form.errors)
596
+
597
+ def test_group_is_optional(self):
598
+ # Need to avoid unique_together constraint violation
599
+ IPFabricTransformMap.objects.get(
600
+ group=None, target_model=self.device_content_type
601
+ ).delete()
602
+ form = IPFabricTransformMapForm(
603
+ data={
604
+ "name": "Test Transform Map",
605
+ "source_model": "device",
606
+ "target_model": self.device_content_type.pk,
607
+ }
608
+ )
609
+ self.assertTrue(form.is_valid(), form.errors)
610
+
611
+ def test_valid_transform_map_form(self):
612
+ form = IPFabricTransformMapForm(
613
+ data={
614
+ "name": "Test Transform Map",
615
+ "group": self.transform_map_group.pk,
616
+ "source_model": "device",
617
+ "target_model": self.device_content_type.pk,
618
+ }
619
+ )
620
+ self.assertTrue(form.is_valid(), form.errors)
621
+ instance = form.save()
622
+ self.assertEqual(instance.name, "Test Transform Map")
623
+ self.assertEqual(instance.group, self.transform_map_group)
624
+
625
+
626
+ class IPFabricTransformMapCloneFormTestCase(TestCase):
627
+ @classmethod
628
+ def setUpTestData(cls):
629
+ cls.transform_map_group = IPFabricTransformMapGroup.objects.create(
630
+ name="Test Group", description="Test group description"
631
+ )
632
+
633
+ def fields_are_required(self):
634
+ form = IPFabricTransformMapCloneForm(data={})
635
+ self.assertFalse(form.is_valid(), form.errors)
636
+ self.assertIn("name", form.errors)
637
+
638
+ def test_fields_are_optional(self):
639
+ form = IPFabricTransformMapCloneForm(
640
+ data={
641
+ "name": "Cloned Transform Map",
642
+ }
643
+ )
644
+ self.assertTrue(form.is_valid(), form.errors)
645
+
646
+ def test_clone_options_default_to_true(self):
647
+ form = IPFabricTransformMapCloneForm(
648
+ data={"name": "Cloned Transform Map", "group": self.transform_map_group.pk}
649
+ )
650
+ self.assertTrue(form.is_valid(), form.errors)
651
+ # Check initial values
652
+ self.assertTrue(form.fields["clone_fields"].initial)
653
+ self.assertTrue(form.fields["clone_relationships"].initial)
654
+
655
+ def test_valid_clone_form(self):
656
+ form = IPFabricTransformMapCloneForm(
657
+ data={
658
+ "name": "Cloned Transform Map",
659
+ "group": self.transform_map_group.pk,
660
+ "clone_fields": False,
661
+ "clone_relationships": True,
662
+ }
663
+ )
664
+ self.assertTrue(form.is_valid(), form.errors)
665
+
666
+
667
+ class IPFabricSnapshotFilterFormTestCase(TestCase):
668
+ @classmethod
669
+ def setUpTestData(cls):
670
+ cls.source = IPFabricSource.objects.create(
671
+ name="Test Source",
672
+ type=IPFabricSourceTypeChoices.LOCAL,
673
+ url="https://test.ipfabric.local",
674
+ status=DataSourceStatusChoices.NEW,
675
+ )
676
+
677
+ def test_all_fields_are_optional(self):
678
+ form = IPFabricSnapshotFilterForm(data={})
679
+ self.assertTrue(form.is_valid(), form.errors)
680
+
681
+ def test_valid_filter_form_with_all_fields(self):
682
+ form = IPFabricSnapshotFilterForm(
683
+ data={
684
+ "name": "Test Snapshot",
685
+ "status": "loaded",
686
+ "source_id": [self.source.pk],
687
+ "snapshot_id": "test-snapshot-id",
688
+ }
689
+ )
690
+ self.assertTrue(form.is_valid(), form.errors)
691
+
692
+
693
+ class IPFabricSourceFilterFormTestCase(TestCase):
694
+ def test_all_fields_are_optional(self):
695
+ form = IPFabricSourceFilterForm(data={})
696
+ self.assertTrue(form.is_valid(), form.errors)
697
+
698
+ def test_valid_filter_form_with_status(self):
699
+ form = IPFabricSourceFilterForm(
700
+ data={
701
+ "status": [
702
+ DataSourceStatusChoices.NEW,
703
+ DataSourceStatusChoices.COMPLETED,
704
+ ]
705
+ }
706
+ )
707
+ self.assertTrue(form.is_valid(), form.errors)
708
+
709
+
710
+ class IPFabricIngestionFilterFormTestCase(TestCase):
711
+ @classmethod
712
+ def setUpTestData(cls):
713
+ cls.source = IPFabricSource.objects.create(
714
+ name="Test Source",
715
+ type=IPFabricSourceTypeChoices.LOCAL,
716
+ url="https://test.ipfabric.local",
717
+ status=DataSourceStatusChoices.NEW,
718
+ )
719
+
720
+ cls.snapshot = IPFabricSnapshot.objects.create(
721
+ name="Test Snapshot",
722
+ source=cls.source,
723
+ snapshot_id="test-snapshot-id",
724
+ status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
725
+ )
726
+
727
+ cls.sync = IPFabricSync.objects.create(
728
+ name="Test Sync",
729
+ snapshot_data=cls.snapshot,
730
+ )
731
+
732
+ def test_all_fields_are_optional(self):
733
+ form = IPFabricIngestionFilterForm(data={})
734
+ self.assertTrue(form.is_valid(), form.errors)
735
+
736
+ def test_valid_filter_form_with_sync(self):
737
+ form = IPFabricIngestionFilterForm(data={"sync_id": [self.sync.pk]})
738
+ self.assertTrue(form.is_valid(), form.errors)
739
+
740
+
741
+ class IPFabricIngestionMergeFormTestCase(TestCase):
742
+ def test_remove_branch_defaults_to_true(self):
743
+ form = IPFabricIngestionMergeForm(data={"confirm": True})
744
+ self.assertTrue(form.is_valid(), form.errors)
745
+ self.assertTrue(form.fields["remove_branch"].initial)
746
+
747
+ def test_remove_branch_is_optional(self):
748
+ form = IPFabricIngestionMergeForm(data={"confirm": True})
749
+ self.assertTrue(form.is_valid(), form.errors)
750
+
751
+ def test_valid_merge_form(self):
752
+ form = IPFabricIngestionMergeForm(data={"confirm": True, "remove_branch": True})
753
+ self.assertTrue(form.is_valid(), form.errors)
754
+
755
+
756
+ class IPFabricSyncFormTestCase(TestCase):
757
+ maxDiff = 1500
758
+
759
+ @classmethod
760
+ def setUpTestData(cls):
761
+ cls.source = IPFabricSource.objects.create(
762
+ name="Test Source",
763
+ type=IPFabricSourceTypeChoices.LOCAL,
764
+ url="https://test.ipfabric.local",
765
+ status=DataSourceStatusChoices.NEW,
766
+ )
767
+
768
+ cls.snapshot = IPFabricSnapshot.objects.create(
769
+ name="Test Snapshot",
770
+ source=cls.source,
771
+ snapshot_id="test-snapshot-id",
772
+ status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
773
+ data={
774
+ "sites": ["site1", "site2", "site3"]
775
+ }, # Store as list instead of comma-separated string
776
+ )
777
+
778
+ cls.transform_map_group = IPFabricTransformMapGroup.objects.create(
779
+ name="Test Group", description="Test group description"
780
+ )
781
+
782
+ def test_fields_are_required(self):
783
+ form = IPFabricSyncForm(data={})
784
+ self.assertFalse(form.is_valid(), form.errors)
785
+ self.assertIn("name", form.errors)
786
+ self.assertIn("source", form.errors)
787
+ self.assertIn("snapshot_data", form.errors)
788
+
789
+ def test_fields_are_optional(self):
790
+ form = IPFabricSyncForm(
791
+ data={
792
+ "name": "Test Sync",
793
+ "source": self.source.pk,
794
+ "snapshot_data": self.snapshot.pk,
795
+ }
796
+ )
797
+ self.assertTrue(form.is_valid(), form.errors)
798
+
799
+ def test_valid_sync_form(self):
800
+ form = IPFabricSyncForm(
801
+ data={
802
+ "name": "Test Sync",
803
+ "source": self.source.pk,
804
+ "snapshot_data": self.snapshot.pk,
805
+ "auto_merge": True,
806
+ "update_custom_fields": True,
807
+ }
808
+ )
809
+ self.assertTrue(form.is_valid(), form.errors)
810
+
811
+ def test_form_initialization_with_source_no_data(self):
812
+ """Test source handling without data"""
813
+ form = IPFabricSyncForm(initial={"source": self.source.pk})
814
+
815
+ # Verify that source_type is set when there's a source and no data
816
+ self.assertEqual(form.source_type, IPFabricSourceTypeChoices.LOCAL)
817
+
818
+ def test_form_initialization_with_sites_no_data(self):
819
+ """Test sites handling without data"""
820
+ form = IPFabricSyncForm(initial={"sites": ["site1", "site2"]})
821
+
822
+ # Verify that sites choices and initial values are set
823
+ # Convert to list for comparison since form returns list, not tuple
824
+ expected_choices = [("site1", "site1"), ("site2", "site2")]
825
+ self.assertEqual(form.fields["sites"].choices, expected_choices)
826
+ self.assertEqual(form.fields["sites"].initial, tuple(expected_choices))
827
+
828
+ def test_form_initialization_with_snapshot_data_in_form_data(self):
829
+ """Test form with data containing snapshot_data"""
830
+ form = IPFabricSyncForm(
831
+ data={
832
+ "name": "Test Sync",
833
+ "source": self.source.pk,
834
+ "snapshot_data": self.snapshot.pk,
835
+ }
836
+ )
837
+
838
+ # Verify that site choices are set based on snapshot's sites when data exists
839
+ expected_choices = [("site1", "site1"), ("site2", "site2"), ("site3", "site3")]
840
+ self.assertEqual(form.fields["sites"].choices, expected_choices)
841
+ self.assertEqual(self.snapshot.sites, ["site1", "site2", "site3"])
842
+
843
+ def test_form_initialization_with_different_snapshot_sites(self):
844
+ """Verify different snapshot sites are properly handled"""
845
+ # Create another snapshot with different sites
846
+ snapshot2 = IPFabricSnapshot.objects.create(
847
+ name="Test Snapshot 2",
848
+ source=self.source,
849
+ snapshot_id="test-snapshot-id-2",
850
+ status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
851
+ data={"sites": ["siteA", "siteB"]},
852
+ )
853
+
854
+ # Test form with the second snapshot
855
+ form = IPFabricSyncForm(
856
+ data={
857
+ "name": "Test Sync 2",
858
+ "source": self.source.pk,
859
+ "snapshot_data": snapshot2.pk,
860
+ }
861
+ )
862
+
863
+ # Verify that the correct snapshot's sites are used
864
+ # Convert to list for comparison since form returns list, not tuple
865
+ expected_choices = [("siteA", "siteA"), ("siteB", "siteB")]
866
+ self.assertEqual(form.fields["sites"].choices, expected_choices)
867
+
868
+ def test_form_initialization_with_snapshot_no_sites_data(self):
869
+ """Verify handling when snapshot has no sites data"""
870
+ # Create a snapshot with no sites data
871
+ snapshot_no_sites = IPFabricSnapshot.objects.create(
872
+ name="Test Snapshot No Sites",
873
+ source=self.source,
874
+ snapshot_id="test-snapshot-no-sites",
875
+ status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
876
+ data={}, # No sites data
877
+ )
878
+
879
+ # Test form with snapshot that has no sites
880
+ form = IPFabricSyncForm(
881
+ data={
882
+ "name": "Test Sync No Sites",
883
+ "source": self.source.pk,
884
+ "snapshot_data": snapshot_no_sites.pk,
885
+ }
886
+ )
887
+
888
+ # Verify that sites choices are empty when snapshot has no sites
889
+ sites_choices = form.fields["sites"].choices
890
+ self.assertTrue(len(sites_choices) == 0)
891
+ self.assertEqual(snapshot_no_sites.sites, [])
892
+
893
+ def test_form_initialization_with_existing_instance_no_data(self):
894
+ """Test existing instance initialization when not self.data"""
895
+ # Create an existing sync instance
896
+ sync_instance = IPFabricSync.objects.create(
897
+ name="Existing Sync",
898
+ snapshot_data=self.snapshot,
899
+ parameters={
900
+ "sites": ["site1", "site2"],
901
+ "groups": [self.transform_map_group.pk],
902
+ },
903
+ )
904
+
905
+ # Test form initialization with existing instance but no data
906
+ form = IPFabricSyncForm(instance=sync_instance)
907
+
908
+ # Verify that source_type is set from the instance
909
+ self.assertEqual(form.source_type, IPFabricSourceTypeChoices.LOCAL)
910
+
911
+ # Verify that initial values are set from instance parameters
912
+ self.assertEqual(form.initial["source"], self.source)
913
+ self.assertEqual(form.initial["sites"], ["site1", "site2"])
914
+ self.assertEqual(form.initial["groups"], [self.transform_map_group.pk])
915
+
916
+ # Verify that sites choices are set from instance's snapshot when no data
917
+ # Convert to list for comparison since form returns list, not tuple
918
+ expected_choices = [("site1", "site1"), ("site2", "site2"), ("site3", "site3")]
919
+ self.assertEqual(form.fields["sites"].choices, expected_choices)
920
+
921
+ def test_form_initialization_with_existing_instance_and_initial_kwargs(self):
922
+ """Test existing instance initialization with initial kwargs"""
923
+ # Create an existing sync instance
924
+ sync_instance = IPFabricSync.objects.create(
925
+ name="Existing Sync",
926
+ snapshot_data=self.snapshot,
927
+ parameters={"sites": ["site1"], "groups": []},
928
+ )
929
+
930
+ # Test form initialization with existing instance and initial kwargs
931
+ form = IPFabricSyncForm(
932
+ instance=sync_instance, initial={"name": "Override Name"}
933
+ )
934
+
935
+ # When initial kwargs are provided, the form skips the instance initialization block
936
+ # This means source_type remains None as intended by the form logic
937
+ self.assertIsNone(form.source_type)
938
+
939
+ # initial should not be set from instance when initial kwargs are provided
940
+ self.assertNotIn("source", form.initial)
941
+ self.assertNotIn("sites", form.initial)
942
+ self.assertNotIn("groups", form.initial)
943
+
944
+ # But the provided initial value should be present
945
+ self.assertEqual(form.initial.get("name"), "Override Name")
946
+
947
+ def test_clean_sites_not_part_of_snapshot(self):
948
+ """Test form validation when selected sites are not part of the snapshot"""
949
+ form = IPFabricSyncForm(
950
+ data={
951
+ "name": "Test Sync Invalid Sites",
952
+ "source": self.source.pk,
953
+ "snapshot_data": self.snapshot.pk,
954
+ "sites": ["invalid_site1", "invalid_site2"], # Sites not in snapshot
955
+ }
956
+ )
957
+
958
+ # Form should be invalid due to sites validation
959
+ self.assertFalse(form.is_valid(), form.errors)
960
+ self.assertIn("sites", form.errors)
961
+ self.assertTrue("not part of the snapshot" in str(form.errors["sites"]))
962
+
963
+ def test_clean_sites_validation_with_valid_sites(self):
964
+ """Test form validation when selected sites are valid (part of the snapshot)"""
965
+ form = IPFabricSyncForm(
966
+ data={
967
+ "name": "Test Sync Valid Sites",
968
+ "source": self.source.pk,
969
+ "snapshot_data": self.snapshot.pk,
970
+ "sites": ["site1", "site2"], # Valid sites that are in snapshot
971
+ }
972
+ )
973
+
974
+ # Form should be valid since sites are part of the snapshot
975
+ self.assertTrue(form.is_valid(), form.errors)
976
+
977
+ def test_clean_sites_validation_with_partial_match(self):
978
+ """Test form validation when some sites are valid and some are not"""
979
+ form = IPFabricSyncForm(
980
+ data={
981
+ "name": "Test Sync Partial Sites",
982
+ "source": self.source.pk,
983
+ "snapshot_data": self.snapshot.pk,
984
+ "sites": ["site1", "invalid_site"], # Mix of valid and invalid sites
985
+ }
986
+ )
987
+
988
+ # Form should be invalid since not all sites are part of the snapshot
989
+ self.assertFalse(form.is_valid(), form.errors)
990
+ self.assertIn("sites", form.errors)
991
+ self.assertTrue("not part of the snapshot" in str(form.errors["sites"]))
992
+
993
+ def test_clean_sites_validation_without_sites(self):
994
+ """Test form validation when no sites are selected (sites is None/empty)"""
995
+ form = IPFabricSyncForm(
996
+ data={
997
+ "name": "Test Sync No Sites",
998
+ "source": self.source.pk,
999
+ "snapshot_data": self.snapshot.pk,
1000
+ # No sites specified
1001
+ }
1002
+ )
1003
+
1004
+ # Form should be valid since the condition only triggers when sites exist
1005
+ self.assertTrue(form.is_valid(), form.errors)
1006
+
1007
+ def test_clean_scheduled_time_in_past(self):
1008
+ """Test form validation when scheduled time is in the past"""
1009
+ past_time = local_now() - timedelta(hours=1)
1010
+ form = IPFabricSyncForm(
1011
+ data={
1012
+ "name": "Test Sync Past Schedule",
1013
+ "source": self.source.pk,
1014
+ "snapshot_data": self.snapshot.pk,
1015
+ "scheduled": past_time,
1016
+ }
1017
+ )
1018
+
1019
+ # Form should be invalid due to scheduled time validation
1020
+ self.assertFalse(form.is_valid(), form.errors)
1021
+ self.assertTrue("Scheduled time must be in the future" in str(form.errors))
1022
+
1023
+ def test_clean_interval_without_scheduled_time(self):
1024
+ """Test interval is provided without scheduled time"""
1025
+ form = IPFabricSyncForm(
1026
+ data={
1027
+ "name": "Test Sync No Schedule",
1028
+ "source": self.source.pk,
1029
+ "snapshot_data": self.snapshot.pk,
1030
+ "interval": 60,
1031
+ # No scheduled time specified
1032
+ }
1033
+ )
1034
+
1035
+ self.assertTrue(form.is_valid(), form.errors)
1036
+ self.assertIsNotNone(form.cleaned_data["scheduled"])
1037
+
1038
+ def test_clean_groups_missing_required_transform_maps(self):
1039
+ """Test form validation when transform map groups are missing required maps"""
1040
+ # Delete a required default transform map to trigger validation failure
1041
+ # This ensures that the missing map cannot be covered by default maps
1042
+ manufacturer_content_type = ContentType.objects.get(
1043
+ app_label="dcim", model="manufacturer"
1044
+ )
1045
+ IPFabricTransformMap.objects.filter(
1046
+ target_model=manufacturer_content_type, group__isnull=True
1047
+ ).delete()
1048
+
1049
+ form = IPFabricSyncForm(
1050
+ data={
1051
+ "name": "Test Sync Missing Maps",
1052
+ "source": self.source.pk,
1053
+ "snapshot_data": self.snapshot.pk,
1054
+ }
1055
+ )
1056
+
1057
+ # Form should be invalid due to missing required transform maps
1058
+ self.assertFalse(form.is_valid(), form.errors)
1059
+ self.assertIn("groups", form.errors)
1060
+ self.assertTrue("Missing maps:" in str(form.errors["groups"]))
1061
+ # Check that it mentions some of the missing required maps
1062
+ error_message = str(form.errors["groups"])
1063
+ self.assertTrue("dcim.manufacturer" in error_message, error_message)
1064
+
1065
+ def test_save_method_basic_functionality(self):
1066
+ """Test basic save functionality without scheduling"""
1067
+ form = IPFabricSyncForm(
1068
+ data={
1069
+ "name": "Test Sync Save",
1070
+ "source": self.source.pk,
1071
+ "snapshot_data": self.snapshot.pk,
1072
+ "sites": ["site1", "site2"],
1073
+ "groups": [self.transform_map_group.pk],
1074
+ "auto_merge": True,
1075
+ "update_custom_fields": True,
1076
+ }
1077
+ )
1078
+
1079
+ self.assertTrue(form.is_valid(), form.errors)
1080
+
1081
+ # Save the form
1082
+ sync_instance = form.save()
1083
+
1084
+ # Verify the instance was created correctly
1085
+ self.assertIsInstance(sync_instance, IPFabricSync)
1086
+ self.assertEqual(sync_instance.name, "Test Sync Save")
1087
+ self.assertEqual(sync_instance.snapshot_data.source, self.source)
1088
+ self.assertEqual(sync_instance.snapshot_data, self.snapshot)
1089
+ self.assertEqual(sync_instance.status, DataSourceStatusChoices.NEW)
1090
+ self.assertTrue(sync_instance.auto_merge)
1091
+ self.assertTrue(sync_instance.update_custom_fields)
1092
+
1093
+ # Verify parameters were stored correctly
1094
+ # All models are `False` since checkboxes must always default to False
1095
+ expected_parameters = {
1096
+ "sites": ["site1", "site2"],
1097
+ "groups": [self.transform_map_group.pk],
1098
+ "site": False,
1099
+ "manufacturer": False,
1100
+ "devicetype": False,
1101
+ "devicerole": False,
1102
+ "platform": False,
1103
+ "device": False,
1104
+ "virtualchassis": False,
1105
+ "interface": False,
1106
+ "macaddress": False,
1107
+ "inventoryitem": False,
1108
+ "vlan": False,
1109
+ "vrf": False,
1110
+ "prefix": False,
1111
+ "ipaddress": False,
1112
+ }
1113
+ self.assertEqual(sync_instance.parameters, expected_parameters)
1114
+
1115
+ def test_save_method_with_ipf_parameters(self):
1116
+ """Test save method properly handles ipf_ prefixed form fields"""
1117
+ form = IPFabricSyncForm(
1118
+ data={
1119
+ "name": "Test Sync IPF Params",
1120
+ "source": self.source.pk,
1121
+ "snapshot_data": self.snapshot.pk,
1122
+ "ipf_site": True,
1123
+ "ipf_interface": True,
1124
+ "ipf_prefix": True,
1125
+ }
1126
+ )
1127
+
1128
+ self.assertTrue(form.is_valid(), form.errors)
1129
+
1130
+ sync_instance = form.save()
1131
+
1132
+ # Verify ipf_ parameters were stripped and stored correctly
1133
+ # All models are `False` since checkboxes must always default to False
1134
+ expected_parameters = {
1135
+ "sites": [],
1136
+ "groups": [],
1137
+ "site": True, # Explicitly set via ipf_site
1138
+ "manufacturer": False,
1139
+ "devicetype": False,
1140
+ "devicerole": False,
1141
+ "platform": False,
1142
+ "device": False,
1143
+ "virtualchassis": False,
1144
+ "interface": True, # Explicitly set via ipf_interface
1145
+ "macaddress": False,
1146
+ "inventoryitem": False,
1147
+ "ipaddress": False,
1148
+ "vlan": False,
1149
+ "vrf": False,
1150
+ "prefix": True, # Explicitly set via ipf_prefix
1151
+ }
1152
+ self.assertEqual(sync_instance.parameters, expected_parameters)
1153
+
1154
+ @patch("ipfabric_netbox.models.IPFabricSync.enqueue_sync_job")
1155
+ def test_save_method_with_scheduling_no_interval(self, mock_enqueue):
1156
+ """Test save method with scheduled time but no interval"""
1157
+ future_time = local_now() + timedelta(hours=1)
1158
+
1159
+ form = IPFabricSyncForm(
1160
+ data={
1161
+ "name": "Test Sync Scheduled",
1162
+ "source": self.source.pk,
1163
+ "snapshot_data": self.snapshot.pk,
1164
+ "scheduled": future_time,
1165
+ }
1166
+ )
1167
+
1168
+ self.assertTrue(form.is_valid(), form.errors)
1169
+
1170
+ # Capture the time before save to compare - with more tolerance
1171
+ sync_instance = form.save()
1172
+
1173
+ # Verify the instance was created correctly
1174
+ self.assertEqual(sync_instance.scheduled, future_time)
1175
+ self.assertIsNone(sync_instance.interval)
1176
+
1177
+ # Verify the instance exists in database
1178
+ saved_instance = IPFabricSync.objects.get(pk=sync_instance.pk)
1179
+ self.assertEqual(saved_instance.scheduled, future_time)
1180
+
1181
+ # Verify that enqueue_sync_job was called when object.scheduled is set
1182
+ mock_enqueue.assert_called_once()
1183
+
1184
+ @patch("ipfabric_netbox.models.IPFabricSync.enqueue_sync_job")
1185
+ def test_save_method_with_interval_auto_schedule(self, mock_enqueue):
1186
+ """Test save method with interval automatically schedules for current time"""
1187
+ form = IPFabricSyncForm(
1188
+ data={
1189
+ "name": "Test Sync Interval",
1190
+ "source": self.source.pk,
1191
+ "snapshot_data": self.snapshot.pk,
1192
+ "interval": 60,
1193
+ # No scheduled time specified
1194
+ }
1195
+ )
1196
+
1197
+ self.assertTrue(form.is_valid(), form.errors)
1198
+
1199
+ # Capture the time before save to compare - with more tolerance
1200
+ before_save = local_now() - timedelta(seconds=1)
1201
+ sync_instance = form.save()
1202
+ after_save = local_now() + timedelta(seconds=1)
1203
+
1204
+ # Verify interval was set and scheduled time was auto-generated
1205
+ self.assertEqual(sync_instance.interval, 60)
1206
+ self.assertIsNotNone(sync_instance.scheduled)
1207
+
1208
+ # Scheduled time should be close to current time (within the test execution window)
1209
+ self.assertTrue(before_save <= sync_instance.scheduled <= after_save)
1210
+
1211
+ # Verify that enqueue_sync_job was called when object.scheduled is set
1212
+ mock_enqueue.assert_called_once()
1213
+
1214
+ @patch("ipfabric_netbox.models.IPFabricSync.enqueue_sync_job")
1215
+ def test_save_method_with_both_scheduled_and_interval(self, mock_enqueue):
1216
+ """Test save method with both scheduled time and interval"""
1217
+ future_time = local_now() + timedelta(hours=2)
1218
+
1219
+ form = IPFabricSyncForm(
1220
+ data={
1221
+ "name": "Test Sync Both",
1222
+ "source": self.source.pk,
1223
+ "snapshot_data": self.snapshot.pk,
1224
+ "scheduled": future_time,
1225
+ "interval": 120,
1226
+ }
1227
+ )
1228
+
1229
+ self.assertTrue(form.is_valid(), form.errors)
1230
+
1231
+ sync_instance = form.save()
1232
+
1233
+ # Verify both values were set correctly
1234
+ self.assertEqual(sync_instance.scheduled, future_time)
1235
+ self.assertEqual(sync_instance.interval, 120)
1236
+
1237
+ # Verify that enqueue_sync_job was called when object.scheduled is set
1238
+ mock_enqueue.assert_called_once()
1239
+
1240
+ def test_save_method_empty_sites_and_groups(self):
1241
+ """Test save method handles empty sites and groups correctly"""
1242
+ form = IPFabricSyncForm(
1243
+ data={
1244
+ "name": "Test Sync Empty",
1245
+ "source": self.source.pk,
1246
+ "snapshot_data": self.snapshot.pk,
1247
+ # No sites or groups specified
1248
+ }
1249
+ )
1250
+
1251
+ self.assertTrue(form.is_valid(), form.errors)
1252
+
1253
+ sync_instance = form.save()
1254
+
1255
+ # Verify empty collections are handled correctly
1256
+ self.assertEqual(sync_instance.parameters["sites"], [])
1257
+ self.assertEqual(sync_instance.parameters["groups"], [])
1258
+
1259
+ def test_save_method_status_always_set_to_new(self):
1260
+ """Test that save method always sets status to NEW regardless of input"""
1261
+ form = IPFabricSyncForm(
1262
+ data={
1263
+ "name": "Test Sync Status",
1264
+ "source": self.source.pk,
1265
+ "snapshot_data": self.snapshot.pk,
1266
+ }
1267
+ )
1268
+
1269
+ self.assertTrue(form.is_valid(), form.errors)
1270
+
1271
+ sync_instance = form.save()
1272
+
1273
+ # Status should always be NEW after save
1274
+ self.assertEqual(sync_instance.status, DataSourceStatusChoices.NEW)
1275
+
1276
+ # Verify in database as well
1277
+ saved_instance = IPFabricSync.objects.get(pk=sync_instance.pk)
1278
+ self.assertEqual(saved_instance.status, DataSourceStatusChoices.NEW)
1279
+
1280
+ def test_fieldsets_for_local_source_type(self):
1281
+ """Test that fieldsets returns correct structure for local source type"""
1282
+ form = IPFabricSyncForm(
1283
+ data={
1284
+ "name": "Test Sync",
1285
+ "source": self.source.pk,
1286
+ "snapshot_data": self.snapshot.pk,
1287
+ }
1288
+ )
1289
+
1290
+ fieldsets = form.fieldsets
1291
+
1292
+ # Should have multiple fieldsets
1293
+ self.assertGreater(len(fieldsets), 5)
1294
+
1295
+ # First fieldset should be IP Fabric Source
1296
+ self.assertEqual(fieldsets[0].name, "IP Fabric Source")
1297
+
1298
+ # Second fieldset should be Snapshot Information with sites for local source
1299
+ self.assertEqual(fieldsets[1].name, "Snapshot Information")
1300
+
1301
+ # Should contain Ingestion Execution Parameters fieldset
1302
+ exec_params_fieldset = next(
1303
+ (fs for fs in fieldsets if fs.name == "Ingestion Execution Parameters"),
1304
+ None,
1305
+ )
1306
+ self.assertIsNotNone(exec_params_fieldset)
1307
+
1308
+ # Should contain Extras fieldset
1309
+ extras_fieldset = next((fs for fs in fieldsets if fs.name == "Extras"), None)
1310
+ self.assertIsNotNone(extras_fieldset)
1311
+
1312
+ # Should contain Tags fieldset
1313
+ tags_fieldset = next((fs for fs in fieldsets if fs.name == "Tags"), None)
1314
+ self.assertIsNotNone(tags_fieldset)
1315
+
1316
+ def test_fieldsets_for_remote_source_type(self):
1317
+ """Test that fieldsets returns correct structure for remote source type"""
1318
+ # Create a remote source
1319
+ remote_source = IPFabricSource.objects.create(
1320
+ name="Test Remote Source",
1321
+ type=IPFabricSourceTypeChoices.REMOTE,
1322
+ url="https://remote.ipfabric.local",
1323
+ status=DataSourceStatusChoices.NEW,
1324
+ )
1325
+
1326
+ remote_snapshot = IPFabricSnapshot.objects.create(
1327
+ name="Test Remote Snapshot",
1328
+ source=remote_source,
1329
+ snapshot_id="test-remote-snapshot-id",
1330
+ status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
1331
+ data={"sites": ["remote_site1", "remote_site2"]},
1332
+ )
1333
+
1334
+ form = IPFabricSyncForm(
1335
+ data={
1336
+ "name": "Test Remote Sync",
1337
+ "source": remote_source.pk,
1338
+ "snapshot_data": remote_snapshot.pk,
1339
+ }
1340
+ )
1341
+
1342
+ fieldsets = form.fieldsets
1343
+
1344
+ # Should have multiple fieldsets
1345
+ self.assertGreater(len(fieldsets), 5)
1346
+
1347
+ # First fieldset should be IP Fabric Source
1348
+ self.assertEqual(fieldsets[0].name, "IP Fabric Source")
1349
+
1350
+ # Second fieldset should be Snapshot Information without sites for remote source
1351
+ self.assertEqual(fieldsets[1].name, "Snapshot Information")
1352
+
1353
+ # Verify the fieldsets structure is consistent
1354
+ fieldset_names = [fs.name for fs in fieldsets]
1355
+ expected_names = [
1356
+ "IP Fabric Source",
1357
+ "Snapshot Information",
1358
+ "Extras",
1359
+ "Tags",
1360
+ ]
1361
+
1362
+ for expected_name in expected_names:
1363
+ self.assertIn(expected_name, fieldset_names)
1364
+
1365
+ def test_fieldsets_with_existing_instance_local_source(self):
1366
+ """Test fieldsets behavior with an existing sync instance from local source"""
1367
+ # Create an existing sync instance
1368
+ sync_instance = IPFabricSync.objects.create(
1369
+ name="Existing Sync",
1370
+ snapshot_data=self.snapshot,
1371
+ parameters={
1372
+ "sites": ["site1", "site2"],
1373
+ "groups": [self.transform_map_group.pk],
1374
+ },
1375
+ )
1376
+
1377
+ form = IPFabricSyncForm(instance=sync_instance)
1378
+ fieldsets = form.fieldsets
1379
+
1380
+ # Should have multiple fieldsets
1381
+ self.assertGreater(len(fieldsets), 5)
1382
+
1383
+ # First fieldset should be IP Fabric Source
1384
+ self.assertEqual(fieldsets[0].name, "IP Fabric Source")
1385
+
1386
+ # Second fieldset should be Snapshot Information with sites (local source)
1387
+ self.assertEqual(fieldsets[1].name, "Snapshot Information")
1388
+
1389
+ # Should contain parameter fieldsets for ALL type
1390
+ fieldset_names = [fs.name for fs in fieldsets]
1391
+ self.assertIn("DCIM Parameters", fieldset_names)
1392
+ self.assertIn("IPAM Parameters", fieldset_names)
1393
+
1394
+ def test_fieldsets_property_returns_correct_field_types(self):
1395
+ """Test that fieldsets property returns FieldSet objects with correct structure"""
1396
+ from utilities.forms.rendering import FieldSet
1397
+
1398
+ form = IPFabricSyncForm(
1399
+ data={
1400
+ "name": "Test Sync",
1401
+ "source": self.source.pk,
1402
+ "snapshot_data": self.snapshot.pk,
1403
+ }
1404
+ )
1405
+
1406
+ fieldsets = form.fieldsets
1407
+
1408
+ # Each item should be a FieldSet instance
1409
+ for fieldset in fieldsets:
1410
+ self.assertIsInstance(fieldset, FieldSet)
1411
+ # Each fieldset should have a name
1412
+ self.assertIsNotNone(fieldset.name)
1413
+
1414
+ def test_fieldsets_dynamic_behavior_consistency(self):
1415
+ """Test that fieldsets method consistently returns the same structure for same parameters"""
1416
+ # Test consistency for same parameters
1417
+ form1 = IPFabricSyncForm(
1418
+ data={
1419
+ "name": "Test Sync 1",
1420
+ "source": self.source.pk,
1421
+ "snapshot_data": self.snapshot.pk,
1422
+ }
1423
+ )
1424
+ form2 = IPFabricSyncForm(
1425
+ data={
1426
+ "name": "Test Sync 2",
1427
+ "source": self.source.pk,
1428
+ "snapshot_data": self.snapshot.pk,
1429
+ }
1430
+ )
1431
+
1432
+ fieldsets1 = form1.fieldsets
1433
+ fieldsets2 = form2.fieldsets
1434
+
1435
+ # Both should have the same structure
1436
+ self.assertEqual(len(fieldsets1), len(fieldsets2))
1437
+
1438
+ fieldset_names1 = [fs.name for fs in fieldsets1]
1439
+ fieldset_names2 = [fs.name for fs in fieldsets2]
1440
+ self.assertEqual(fieldset_names1, fieldset_names2)