ipfabric_netbox 4.2.2__py3-none-any.whl → 4.2.2b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ipfabric_netbox might be problematic. Click here for more details.
- ipfabric_netbox/__init__.py +1 -1
- ipfabric_netbox/api/__init__.py +0 -1
- ipfabric_netbox/api/serializers.py +147 -90
- ipfabric_netbox/api/urls.py +4 -4
- ipfabric_netbox/api/views.py +18 -19
- ipfabric_netbox/choices.py +0 -12
- ipfabric_netbox/filtersets.py +67 -4
- ipfabric_netbox/forms.py +99 -140
- ipfabric_netbox/graphql/__init__.py +23 -0
- ipfabric_netbox/graphql/enums.py +35 -0
- ipfabric_netbox/graphql/filters.py +317 -0
- ipfabric_netbox/graphql/schema.py +101 -0
- ipfabric_netbox/graphql/types.py +216 -0
- ipfabric_netbox/migrations/0016_tags_and_changelog_for_snapshots.py +31 -0
- ipfabric_netbox/migrations/0017_ipfabricsync_update_custom_fields.py +17 -0
- ipfabric_netbox/migrations/0018_remove_type_field.py +17 -0
- ipfabric_netbox/models.py +10 -10
- ipfabric_netbox/tables.py +30 -9
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricsource.html +1 -1
- ipfabric_netbox/tests/api/__init__.py +0 -0
- ipfabric_netbox/tests/api/test_api.py +879 -0
- ipfabric_netbox/tests/test_forms.py +1440 -0
- ipfabric_netbox/tests/test_models.py +47 -11
- ipfabric_netbox/utilities/ipfutils.py +43 -23
- ipfabric_netbox/views.py +6 -8
- {ipfabric_netbox-4.2.2.dist-info → ipfabric_netbox-4.2.2b1.dist-info}/METADATA +8 -7
- {ipfabric_netbox-4.2.2.dist-info → ipfabric_netbox-4.2.2b1.dist-info}/RECORD +28 -19
- {ipfabric_netbox-4.2.2.dist-info → ipfabric_netbox-4.2.2b1.dist-info}/WHEEL +1 -1
- ipfabric_netbox/api/nested_serializers.py +0 -78
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync_list.html +0 -71
|
@@ -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)
|