ipfabric_netbox 4.2.2b1__py3-none-any.whl → 4.2.2b3__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/forms.py +9 -9
- ipfabric_netbox/models.py +13 -0
- ipfabric_netbox/template_content.py +8 -5
- ipfabric_netbox/templates/ipfabric_netbox/ipfabric_table.html +1 -1
- ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_restore.html +1 -1
- ipfabric_netbox/tests/test_views.py +2153 -0
- ipfabric_netbox/urls.py +15 -14
- ipfabric_netbox/views.py +100 -70
- {ipfabric_netbox-4.2.2b1.dist-info → ipfabric_netbox-4.2.2b3.dist-info}/METADATA +1 -1
- {ipfabric_netbox-4.2.2b1.dist-info → ipfabric_netbox-4.2.2b3.dist-info}/RECORD +12 -11
- {ipfabric_netbox-4.2.2b1.dist-info → ipfabric_netbox-4.2.2b3.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,2153 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from core.choices import DataSourceStatusChoices
|
|
6
|
+
from core.choices import JobStatusChoices
|
|
7
|
+
from core.models import Job
|
|
8
|
+
from dcim.models import Device
|
|
9
|
+
from dcim.models import DeviceRole
|
|
10
|
+
from dcim.models import DeviceType
|
|
11
|
+
from dcim.models import Manufacturer
|
|
12
|
+
from dcim.models import Site
|
|
13
|
+
from django.contrib.contenttypes.models import ContentType
|
|
14
|
+
from django.core.cache import cache
|
|
15
|
+
from django.core.exceptions import ValidationError
|
|
16
|
+
from django.db.models import Model
|
|
17
|
+
from django.forms.models import model_to_dict
|
|
18
|
+
from django.test import override_settings
|
|
19
|
+
from django.utils import timezone
|
|
20
|
+
from netbox_branching.models import Branch
|
|
21
|
+
from netbox_branching.models import ChangeDiff
|
|
22
|
+
from utilities.testing import ModelTestCase
|
|
23
|
+
from utilities.testing import ViewTestCases
|
|
24
|
+
|
|
25
|
+
from ipfabric_netbox.choices import IPFabricSnapshotStatusModelChoices
|
|
26
|
+
from ipfabric_netbox.choices import IPFabricSourceTypeChoices
|
|
27
|
+
from ipfabric_netbox.forms import dcim_parameters
|
|
28
|
+
from ipfabric_netbox.forms import ipam_parameters
|
|
29
|
+
from ipfabric_netbox.forms import tableChoices
|
|
30
|
+
from ipfabric_netbox.jobs import merge_ipfabric_ingestion
|
|
31
|
+
from ipfabric_netbox.models import IPFabricData
|
|
32
|
+
from ipfabric_netbox.models import IPFabricIngestion
|
|
33
|
+
from ipfabric_netbox.models import IPFabricRelationshipField
|
|
34
|
+
from ipfabric_netbox.models import IPFabricSnapshot
|
|
35
|
+
from ipfabric_netbox.models import IPFabricSource
|
|
36
|
+
from ipfabric_netbox.models import IPFabricSync
|
|
37
|
+
from ipfabric_netbox.models import IPFabricTransformField
|
|
38
|
+
from ipfabric_netbox.models import IPFabricTransformMap
|
|
39
|
+
from ipfabric_netbox.models import IPFabricTransformMapGroup
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PluginPathMixin:
|
|
43
|
+
"""Mixin to correct URL Paths for plugin test."""
|
|
44
|
+
|
|
45
|
+
maxDiff = 1000
|
|
46
|
+
|
|
47
|
+
model: Model # To avoid unresolved attribute warning
|
|
48
|
+
|
|
49
|
+
def _get_model_name(self):
|
|
50
|
+
return self.model._meta.model_name
|
|
51
|
+
|
|
52
|
+
def _get_base_url(self):
|
|
53
|
+
return f"plugins:ipfabric_netbox:{self._get_model_name()}_{{}}" # noqa: E231
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class IPFabricSourceTestCase(
|
|
57
|
+
PluginPathMixin,
|
|
58
|
+
ViewTestCases.GetObjectViewTestCase,
|
|
59
|
+
ViewTestCases.GetObjectChangelogViewTestCase,
|
|
60
|
+
ViewTestCases.CreateObjectViewTestCase,
|
|
61
|
+
ViewTestCases.EditObjectViewTestCase,
|
|
62
|
+
ViewTestCases.DeleteObjectViewTestCase,
|
|
63
|
+
ViewTestCases.ListObjectsViewTestCase,
|
|
64
|
+
ViewTestCases.BulkDeleteObjectsViewTestCase,
|
|
65
|
+
# ViewTestCases.BulkRenameObjectsViewTestCase,
|
|
66
|
+
# ViewTestCases.BulkEditObjectsViewTestCase,
|
|
67
|
+
# ViewTestCases.BulkImportObjectsViewTestCase,
|
|
68
|
+
):
|
|
69
|
+
model = IPFabricSource
|
|
70
|
+
user_permissions = ("ipfabric_netbox.sync_ipfabricsource",)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def setUpTestData(cls):
|
|
74
|
+
# Create three IPFabricSource instances for testing
|
|
75
|
+
sources = (
|
|
76
|
+
IPFabricSource(
|
|
77
|
+
name="IP Fabric Source 1",
|
|
78
|
+
type=IPFabricSourceTypeChoices.LOCAL,
|
|
79
|
+
url="https://ipfabric1.example.com",
|
|
80
|
+
status=DataSourceStatusChoices.NEW,
|
|
81
|
+
parameters={"auth": "token1", "verify": True, "timeout": 30},
|
|
82
|
+
last_synced=timezone.now(),
|
|
83
|
+
),
|
|
84
|
+
IPFabricSource(
|
|
85
|
+
name="IP Fabric Source 2",
|
|
86
|
+
type=IPFabricSourceTypeChoices.REMOTE,
|
|
87
|
+
url="https://ipfabric2.example.com",
|
|
88
|
+
status=DataSourceStatusChoices.COMPLETED,
|
|
89
|
+
parameters={"auth": "token2", "verify": False, "timeout": 60},
|
|
90
|
+
last_synced=timezone.now(),
|
|
91
|
+
),
|
|
92
|
+
IPFabricSource(
|
|
93
|
+
name="IP Fabric Source 3",
|
|
94
|
+
type=IPFabricSourceTypeChoices.LOCAL,
|
|
95
|
+
url="https://ipfabric3.example.com",
|
|
96
|
+
status=DataSourceStatusChoices.FAILED,
|
|
97
|
+
parameters={"auth": "token3", "verify": True, "timeout": 45},
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
for source in sources:
|
|
101
|
+
source.save()
|
|
102
|
+
Job.objects.create(
|
|
103
|
+
job_id=uuid4(),
|
|
104
|
+
object_id=source.pk,
|
|
105
|
+
object_type=ContentType.objects.get_for_model(IPFabricSource),
|
|
106
|
+
name=f"Test Sync Job {source.pk}",
|
|
107
|
+
status=JobStatusChoices.STATUS_COMPLETED,
|
|
108
|
+
completed=timezone.now(),
|
|
109
|
+
created=timezone.now(),
|
|
110
|
+
)
|
|
111
|
+
IPFabricSnapshot.objects.create(
|
|
112
|
+
source=IPFabricSource.objects.first(),
|
|
113
|
+
name="Snapshot 1",
|
|
114
|
+
snapshot_id="$last",
|
|
115
|
+
data={
|
|
116
|
+
"version": "6.0.0",
|
|
117
|
+
"sites": ["Site A", "Site B", "Site C"],
|
|
118
|
+
"total_dev_count": 100,
|
|
119
|
+
"interface_count": 500,
|
|
120
|
+
"note": "Test snapshot 1",
|
|
121
|
+
},
|
|
122
|
+
date=timezone.now(),
|
|
123
|
+
status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
cls.site = Site.objects.create(name="Test Site", slug="test-site")
|
|
127
|
+
|
|
128
|
+
cls.form_data = {
|
|
129
|
+
"name": "IP Fabric Source X",
|
|
130
|
+
"type": IPFabricSourceTypeChoices.LOCAL,
|
|
131
|
+
"url": "https://ipfabricx.example.com",
|
|
132
|
+
"auth": "tokenX",
|
|
133
|
+
"verify": True,
|
|
134
|
+
"timeout": 30,
|
|
135
|
+
"comments": "This is a test IP Fabric source",
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
cls.csv_data = (
|
|
139
|
+
"name,type,url,parameters",
|
|
140
|
+
'IP Fabric Source 4,local,https://ipfabric4.example.com,"{""auth"": ""token4"", ""verify"": true}"',
|
|
141
|
+
'IP Fabric Source 5,remote,https://ipfabric5.example.com,"{""auth"": ""token5"", ""verify"": false}"',
|
|
142
|
+
'IP Fabric Source 6,local,https://ipfabric6.example.com,"{""auth"": ""token6"", ""verify"": true}"',
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
cls.csv_update_data = (
|
|
146
|
+
"id,name,url",
|
|
147
|
+
f"{sources[0].pk},IP Fabric Source 7,https://ipfabric7.example.com", # noqa: E231
|
|
148
|
+
f"{sources[1].pk},IP Fabric Source 8,https://ipfabric8.example.com", # noqa: E231
|
|
149
|
+
f"{sources[2].pk},IP Fabric Source 9,https://ipfabric9.example.com", # noqa: E231
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
cls.bulk_edit_data = {
|
|
153
|
+
"type": IPFabricSourceTypeChoices.REMOTE,
|
|
154
|
+
"comments": "Bulk updated comment",
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
158
|
+
def test_get_topology(self):
|
|
159
|
+
response = self.client.get(
|
|
160
|
+
self._get_queryset().first().get_absolute_url()
|
|
161
|
+
+ f"topology/{self.site.pk}/"
|
|
162
|
+
)
|
|
163
|
+
self.assertHttpStatus(response, 200)
|
|
164
|
+
# Verify the response contains expected modal structure
|
|
165
|
+
self.assertContains(response, "modal-body")
|
|
166
|
+
# Check that the context contains the site object
|
|
167
|
+
self.assertIn("site", response.context)
|
|
168
|
+
self.assertEqual(response.context["site"], self.site.id)
|
|
169
|
+
|
|
170
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
171
|
+
@patch("ipfabric_netbox.utilities.ipfutils.IPFClient")
|
|
172
|
+
def test_get_topology_htmx(self, mock_ipfclient_class):
|
|
173
|
+
# Mock the IPFClient instance that gets created inside IPFabric.__init__
|
|
174
|
+
mock_ipfclient_instance = mock_ipfclient_class.return_value
|
|
175
|
+
mock_ipfclient_instance._client.headers = {"user-agent": "test-user-agent"}
|
|
176
|
+
|
|
177
|
+
# Mock snapshot data - this is what ipf.ipf.snapshots.get(snapshot) returns
|
|
178
|
+
mock_snapshot_data = {
|
|
179
|
+
"id": "$last",
|
|
180
|
+
"name": "Test Snapshot",
|
|
181
|
+
"status": "done",
|
|
182
|
+
"finish_status": "done",
|
|
183
|
+
"end": "2024-01-15T10:30:00Z",
|
|
184
|
+
"snapshot_id": "snapshot123",
|
|
185
|
+
"version": "6.0.0",
|
|
186
|
+
"sites": ["Test Site"],
|
|
187
|
+
"total_dev_count": 10,
|
|
188
|
+
"interface_count": 50,
|
|
189
|
+
}
|
|
190
|
+
mock_ipfclient_instance.snapshots.get.return_value = mock_snapshot_data
|
|
191
|
+
|
|
192
|
+
# Mock site data - this is what ipf.ipf.inventory.sites.all() returns
|
|
193
|
+
mock_sites_data = [
|
|
194
|
+
{
|
|
195
|
+
"siteName": "Test Site",
|
|
196
|
+
"siteKey": "site123",
|
|
197
|
+
"location": "Test Location",
|
|
198
|
+
"deviceCount": 5,
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
mock_ipfclient_instance.inventory.sites.all.return_value = mock_sites_data
|
|
202
|
+
|
|
203
|
+
# Mock diagram methods to avoid actual diagram generation
|
|
204
|
+
mock_ipfclient_instance.diagram.share_link.return_value = (
|
|
205
|
+
"https://ipfabric.example.com/diagram/share/123"
|
|
206
|
+
)
|
|
207
|
+
mock_ipfclient_instance.diagram.svg.return_value = (
|
|
208
|
+
b'<svg><rect width="100" height="100" fill="blue"/></svg>'
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
response = self.client.get(
|
|
212
|
+
self._get_queryset().first().get_absolute_url()
|
|
213
|
+
+ f"topology/{self.site.pk}/",
|
|
214
|
+
**{"HTTP_HX-Request": "true"},
|
|
215
|
+
query_params={
|
|
216
|
+
"source": IPFabricSource.objects.first().pk,
|
|
217
|
+
"snapshot": "$last",
|
|
218
|
+
},
|
|
219
|
+
)
|
|
220
|
+
self.assertHttpStatus(response, 200)
|
|
221
|
+
|
|
222
|
+
# Verify that the API calls were made with correct parameters
|
|
223
|
+
mock_ipfclient_instance.snapshots.get.assert_called_once_with("$last")
|
|
224
|
+
mock_ipfclient_instance.inventory.sites.all.assert_called_once_with(
|
|
225
|
+
filters={"siteName": ["eq", "Test Site"]}
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
229
|
+
@patch("ipfabric_netbox.utilities.ipfutils.IPFClient")
|
|
230
|
+
def test_get_topology_htmx_empty_snapshot_data(self, mock_ipfclient_class):
|
|
231
|
+
# Mock the IPFClient instance that gets created inside IPFabric.__init__
|
|
232
|
+
mock_ipfclient_instance = mock_ipfclient_class.return_value
|
|
233
|
+
mock_ipfclient_instance._client.headers = {"user-agent": "test-user-agent"}
|
|
234
|
+
|
|
235
|
+
# Mock empty snapshot data - this is what ipf.ipf.snapshots.get(snapshot) returns when snapshot doesn't exist
|
|
236
|
+
mock_ipfclient_instance.snapshots.get.return_value = None
|
|
237
|
+
|
|
238
|
+
response = self.client.get(
|
|
239
|
+
self._get_queryset().first().get_absolute_url()
|
|
240
|
+
+ f"topology/{self.site.pk}/",
|
|
241
|
+
**{"HTTP_HX-Request": "true"},
|
|
242
|
+
query_params={
|
|
243
|
+
"source": IPFabricSource.objects.first().pk,
|
|
244
|
+
"snapshot": "$last",
|
|
245
|
+
},
|
|
246
|
+
)
|
|
247
|
+
self.assertHttpStatus(response, 200)
|
|
248
|
+
|
|
249
|
+
# Verify that the snapshot API was called but sites API was not called due to early exit
|
|
250
|
+
mock_ipfclient_instance.snapshots.get.assert_called_once_with("$last")
|
|
251
|
+
mock_ipfclient_instance.inventory.sites.all.assert_not_called()
|
|
252
|
+
|
|
253
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
254
|
+
@patch("ipfabric_netbox.utilities.ipfutils.IPFClient")
|
|
255
|
+
def test_get_topology_htmx_empty_sites_data(self, mock_ipfclient_class):
|
|
256
|
+
# Mock the IPFClient instance that gets created inside IPFabric.__init__
|
|
257
|
+
mock_ipfclient_instance = mock_ipfclient_class.return_value
|
|
258
|
+
mock_ipfclient_instance._client.headers = {"user-agent": "test-user-agent"}
|
|
259
|
+
|
|
260
|
+
# Mock valid snapshot data
|
|
261
|
+
mock_snapshot_data = {
|
|
262
|
+
"id": "$last",
|
|
263
|
+
"name": "Test Snapshot",
|
|
264
|
+
"status": "done",
|
|
265
|
+
"finish_status": "done",
|
|
266
|
+
"end": "2024-01-15T10:30:00Z",
|
|
267
|
+
"snapshot_id": "snapshot123",
|
|
268
|
+
"version": "6.0.0",
|
|
269
|
+
"sites": ["Test Site"],
|
|
270
|
+
"total_dev_count": 10,
|
|
271
|
+
"interface_count": 50,
|
|
272
|
+
}
|
|
273
|
+
mock_ipfclient_instance.snapshots.get.return_value = mock_snapshot_data
|
|
274
|
+
|
|
275
|
+
# Mock empty site data - this is what ipf.ipf.inventory.sites.all() returns when site doesn't exist in snapshot
|
|
276
|
+
mock_ipfclient_instance.inventory.sites.all.return_value = []
|
|
277
|
+
|
|
278
|
+
response = self.client.get(
|
|
279
|
+
self._get_queryset().first().get_absolute_url()
|
|
280
|
+
+ f"topology/{self.site.pk}/",
|
|
281
|
+
**{"HTTP_HX-Request": "true"},
|
|
282
|
+
query_params={
|
|
283
|
+
"source": IPFabricSource.objects.first().pk,
|
|
284
|
+
"snapshot": "$last",
|
|
285
|
+
},
|
|
286
|
+
)
|
|
287
|
+
self.assertHttpStatus(response, 200)
|
|
288
|
+
|
|
289
|
+
# Verify that both API calls were made
|
|
290
|
+
mock_ipfclient_instance.snapshots.get.assert_called_once_with("$last")
|
|
291
|
+
mock_ipfclient_instance.inventory.sites.all.assert_called_once_with(
|
|
292
|
+
filters={"siteName": ["eq", "Test Site"]}
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
296
|
+
def test_get_topology_htmx_no_source(self):
|
|
297
|
+
response = self.client.get(
|
|
298
|
+
self._get_queryset().first().get_absolute_url()
|
|
299
|
+
+ f"topology/{self.site.pk}/",
|
|
300
|
+
**{"HTTP_HX-Request": "true"},
|
|
301
|
+
)
|
|
302
|
+
self.assertHttpStatus(response, 200)
|
|
303
|
+
# Verify response contains HTMX content for no source scenario
|
|
304
|
+
self.assertContains(response, "Source ID not available in request")
|
|
305
|
+
# Check that context indicates no source selected
|
|
306
|
+
self.assertIn("source", response.context)
|
|
307
|
+
self.assertIsNone(response.context.get("source"))
|
|
308
|
+
|
|
309
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
310
|
+
def test_get_topology_htmx_no_snapshot(self):
|
|
311
|
+
response = self.client.get(
|
|
312
|
+
self._get_queryset().first().get_absolute_url()
|
|
313
|
+
+ f"topology/{self.site.pk}/",
|
|
314
|
+
**{"HTTP_HX-Request": "true"},
|
|
315
|
+
query_params={"source": IPFabricSource.objects.first().pk},
|
|
316
|
+
)
|
|
317
|
+
self.assertHttpStatus(response, 200)
|
|
318
|
+
# Verify response contains HTMX content for no snapshot scenario
|
|
319
|
+
self.assertContains(response, "Snapshot ID not available in request.")
|
|
320
|
+
# Verify response indicates no snapshot selected
|
|
321
|
+
self.assertIn("snapshot", response.context)
|
|
322
|
+
self.assertIsNone(response.context.get("snapshot"))
|
|
323
|
+
|
|
324
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
325
|
+
def test_sync_view_get_redirect(self):
|
|
326
|
+
"""Test that GET request to sync view redirects to source detail page."""
|
|
327
|
+
source = self._get_queryset().first()
|
|
328
|
+
response = self.client.get(source.get_absolute_url() + "sync/")
|
|
329
|
+
self.assertHttpStatus(response, 302)
|
|
330
|
+
self.assertRedirects(response, source.get_absolute_url())
|
|
331
|
+
|
|
332
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
333
|
+
@patch("ipfabric_netbox.models.IPFabricSource.enqueue_sync_job")
|
|
334
|
+
def test_sync_view_post_valid(self, mock_enqueue_sync_job):
|
|
335
|
+
"""Test POST request to sync view successfully enqueues sync job."""
|
|
336
|
+
|
|
337
|
+
# Set up mock job
|
|
338
|
+
mock_job = Job(pk=123, name="Test Source Sync Job")
|
|
339
|
+
mock_enqueue_sync_job.return_value = mock_job
|
|
340
|
+
|
|
341
|
+
source = self._get_queryset().first()
|
|
342
|
+
|
|
343
|
+
response = self.client.post(source.get_absolute_url() + "sync/", follow=True)
|
|
344
|
+
|
|
345
|
+
# Should redirect to source detail page
|
|
346
|
+
self.assertHttpStatus(response, 200)
|
|
347
|
+
self.assertRedirects(response, source.get_absolute_url())
|
|
348
|
+
|
|
349
|
+
# Should have called enqueue_sync_job with correct parameters
|
|
350
|
+
mock_enqueue_sync_job.assert_called_once()
|
|
351
|
+
call_args = mock_enqueue_sync_job.call_args
|
|
352
|
+
self.assertIn("request", call_args[1])
|
|
353
|
+
|
|
354
|
+
# Should show success message
|
|
355
|
+
messages = list(response.context["messages"])
|
|
356
|
+
self.assertEqual(len(messages), 1)
|
|
357
|
+
self.assertIn(f"Queued job #{mock_job.pk}", str(messages[0]))
|
|
358
|
+
|
|
359
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
360
|
+
def test_sync_view_nonexistent_source(self):
|
|
361
|
+
"""Test sync view with non-existent source returns 404."""
|
|
362
|
+
nonexistent_pk = 99999
|
|
363
|
+
response = self.client.post(
|
|
364
|
+
f"/plugins/ipfabric-netbox/ipfabricsource/{nonexistent_pk}/sync/"
|
|
365
|
+
)
|
|
366
|
+
self.assertHttpStatus(response, 404)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class IPFabricSnapshotTestCase(
|
|
370
|
+
PluginPathMixin,
|
|
371
|
+
ViewTestCases.GetObjectViewTestCase,
|
|
372
|
+
ViewTestCases.GetObjectChangelogViewTestCase,
|
|
373
|
+
# ViewTestCases.CreateObjectViewTestCase,
|
|
374
|
+
# ViewTestCases.EditObjectViewTestCase,
|
|
375
|
+
ViewTestCases.DeleteObjectViewTestCase,
|
|
376
|
+
ViewTestCases.ListObjectsViewTestCase,
|
|
377
|
+
ViewTestCases.BulkDeleteObjectsViewTestCase,
|
|
378
|
+
# ViewTestCases.BulkRenameObjectsViewTestCase,
|
|
379
|
+
# ViewTestCases.BulkEditObjectsViewTestCase,
|
|
380
|
+
# ViewTestCases.BulkImportObjectsViewTestCase,
|
|
381
|
+
):
|
|
382
|
+
model = IPFabricSnapshot
|
|
383
|
+
|
|
384
|
+
@classmethod
|
|
385
|
+
def setUpTestData(cls):
|
|
386
|
+
# Create IPFabricSource instances needed for snapshots
|
|
387
|
+
source1 = IPFabricSource.objects.create(
|
|
388
|
+
name="Test Source 1",
|
|
389
|
+
type=IPFabricSourceTypeChoices.LOCAL,
|
|
390
|
+
url="https://ipfabric1.example.com",
|
|
391
|
+
status=DataSourceStatusChoices.NEW,
|
|
392
|
+
)
|
|
393
|
+
source2 = IPFabricSource.objects.create(
|
|
394
|
+
name="Test Source 2",
|
|
395
|
+
type=IPFabricSourceTypeChoices.REMOTE,
|
|
396
|
+
url="https://ipfabric2.example.com",
|
|
397
|
+
status=DataSourceStatusChoices.COMPLETED,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Create three IPFabricSnapshot instances for testing
|
|
401
|
+
snapshots = (
|
|
402
|
+
IPFabricSnapshot(
|
|
403
|
+
source=source1,
|
|
404
|
+
name="Snapshot 1",
|
|
405
|
+
snapshot_id="snap001",
|
|
406
|
+
data={
|
|
407
|
+
"version": "6.0.0",
|
|
408
|
+
"sites": ["Site A", "Site B", "Site C"],
|
|
409
|
+
"total_dev_count": 100,
|
|
410
|
+
"interface_count": 500,
|
|
411
|
+
"note": "Test snapshot 1",
|
|
412
|
+
},
|
|
413
|
+
date=timezone.now(),
|
|
414
|
+
status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
|
|
415
|
+
),
|
|
416
|
+
IPFabricSnapshot(
|
|
417
|
+
source=source1,
|
|
418
|
+
name="Snapshot 2",
|
|
419
|
+
snapshot_id="snap002",
|
|
420
|
+
data={
|
|
421
|
+
"version": "6.0.1",
|
|
422
|
+
"sites": ["Site D", "Site E"],
|
|
423
|
+
"total_dev_count": 150,
|
|
424
|
+
"interface_count": 750,
|
|
425
|
+
"note": "Test snapshot 2",
|
|
426
|
+
},
|
|
427
|
+
date=timezone.now(),
|
|
428
|
+
status=IPFabricSnapshotStatusModelChoices.STATUS_UNLOADED,
|
|
429
|
+
),
|
|
430
|
+
IPFabricSnapshot(
|
|
431
|
+
source=source2,
|
|
432
|
+
name="Snapshot 3",
|
|
433
|
+
snapshot_id="snap003",
|
|
434
|
+
data={
|
|
435
|
+
"version": "6.1.0",
|
|
436
|
+
"sites": ["Site F", "Site G", "Site H", "Site I"],
|
|
437
|
+
"total_dev_count": 200,
|
|
438
|
+
"interface_count": 1000,
|
|
439
|
+
"note": "Test snapshot 3",
|
|
440
|
+
},
|
|
441
|
+
date=timezone.now(),
|
|
442
|
+
status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
|
|
443
|
+
),
|
|
444
|
+
)
|
|
445
|
+
for snapshot in snapshots:
|
|
446
|
+
snapshot.save()
|
|
447
|
+
IPFabricData.objects.create(snapshot_data=snapshot, type="device", data={})
|
|
448
|
+
|
|
449
|
+
cls.form_data = {
|
|
450
|
+
"source": source1.pk,
|
|
451
|
+
"name": "Test Snapshot X",
|
|
452
|
+
"snapshot_id": "snapX",
|
|
453
|
+
"data": '{"version": "6.0.0", "sites": ["Site X"], "total_dev_count": 75, "interface_count": 375, "note": "Test snapshot X"}',
|
|
454
|
+
"status": IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
cls.csv_data = (
|
|
458
|
+
"source,name,snapshot_id,status",
|
|
459
|
+
f"{source1.pk},Snapshot CSV 1,snapcsv001,{IPFabricSnapshotStatusModelChoices.STATUS_LOADED}", # noqa: E231
|
|
460
|
+
f"{source1.pk},Snapshot CSV 2,snapcsv002,{IPFabricSnapshotStatusModelChoices.STATUS_UNLOADED}", # noqa: E231
|
|
461
|
+
f"{source2.pk},Snapshot CSV 3,snapcsv003,{IPFabricSnapshotStatusModelChoices.STATUS_LOADED}", # noqa: E231
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
cls.csv_update_data = (
|
|
465
|
+
"id,name,snapshot_id",
|
|
466
|
+
f"{snapshots[0].pk},Updated Snapshot 1,updsnap001", # noqa: E231
|
|
467
|
+
f"{snapshots[1].pk},Updated Snapshot 2,updsnap002", # noqa: E231
|
|
468
|
+
f"{snapshots[2].pk},Updated Snapshot 3,updsnap003", # noqa: E231
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
cls.bulk_edit_data = {
|
|
472
|
+
"status": IPFabricSnapshotStatusModelChoices.STATUS_UNLOADED,
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
476
|
+
def test_get_data(self):
|
|
477
|
+
snapshot = self._get_queryset().first()
|
|
478
|
+
response = self.client.get(snapshot.get_absolute_url() + "data/")
|
|
479
|
+
self.assertHttpStatus(response, 200)
|
|
480
|
+
# Verify the response contains expected data view elements
|
|
481
|
+
self.assertContains(response, "Raw Data")
|
|
482
|
+
# Check that context contains the snapshot object
|
|
483
|
+
self.assertIn("object", response.context)
|
|
484
|
+
response_snapshot = response.context["object"]
|
|
485
|
+
self.assertEqual(response_snapshot.name, snapshot.name)
|
|
486
|
+
# Verify related data is accessible
|
|
487
|
+
self.assertTrue(response_snapshot.ipf_data.exists())
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# TODO: Get back to these tests
|
|
491
|
+
class IPFabricDataTestCase(
|
|
492
|
+
PluginPathMixin,
|
|
493
|
+
# ViewTestCases.GetObjectViewTestCase,
|
|
494
|
+
# ViewTestCases.GetObjectChangelogViewTestCase,
|
|
495
|
+
# ViewTestCases.CreateObjectViewTestCase,
|
|
496
|
+
# ViewTestCases.EditObjectViewTestCase,
|
|
497
|
+
ViewTestCases.DeleteObjectViewTestCase,
|
|
498
|
+
# ViewTestCases.ListObjectsViewTestCase,
|
|
499
|
+
ViewTestCases.BulkDeleteObjectsViewTestCase,
|
|
500
|
+
# ViewTestCases.BulkRenameObjectsViewTestCase,
|
|
501
|
+
# ViewTestCases.BulkEditObjectsViewTestCase,
|
|
502
|
+
# ViewTestCases.BulkImportObjectsViewTestCase,
|
|
503
|
+
):
|
|
504
|
+
model = IPFabricData
|
|
505
|
+
|
|
506
|
+
@classmethod
|
|
507
|
+
def setUpTestData(cls):
|
|
508
|
+
# Create required dependencies
|
|
509
|
+
source = IPFabricSource.objects.create(
|
|
510
|
+
name="Test Source",
|
|
511
|
+
type=IPFabricSourceTypeChoices.LOCAL,
|
|
512
|
+
url="https://ipfabric.example.com",
|
|
513
|
+
status=DataSourceStatusChoices.NEW,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
snapshot = IPFabricSnapshot.objects.create(
|
|
517
|
+
source=source,
|
|
518
|
+
name="Test Snapshot",
|
|
519
|
+
snapshot_id="data_snap001",
|
|
520
|
+
data={"version": "6.0.0", "sites": ["Site A"], "total_dev_count": 100},
|
|
521
|
+
date=timezone.now(),
|
|
522
|
+
status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
# Create three IPFabricData instances for testing
|
|
526
|
+
data_instances = (
|
|
527
|
+
IPFabricData(
|
|
528
|
+
snapshot_data=snapshot,
|
|
529
|
+
type="devices",
|
|
530
|
+
data={"hostname": "device1", "vendor": "cisco", "model": "ISR4331"},
|
|
531
|
+
),
|
|
532
|
+
IPFabricData(
|
|
533
|
+
snapshot_data=snapshot,
|
|
534
|
+
type="interfaces",
|
|
535
|
+
data={"name": "GigabitEthernet0/0/0", "type": "ethernet"},
|
|
536
|
+
),
|
|
537
|
+
IPFabricData(
|
|
538
|
+
snapshot_data=snapshot,
|
|
539
|
+
type="sites",
|
|
540
|
+
data={"name": "Main Site", "location": "New York"},
|
|
541
|
+
),
|
|
542
|
+
)
|
|
543
|
+
for data_instance in data_instances:
|
|
544
|
+
data_instance.save()
|
|
545
|
+
|
|
546
|
+
cls.form_data = {
|
|
547
|
+
"snapshot_data": snapshot.pk,
|
|
548
|
+
"type": "devices",
|
|
549
|
+
"data": '{"hostname": "test-device", "vendor": "juniper"}',
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
cls.csv_data = (
|
|
553
|
+
"snapshot_data,type,data",
|
|
554
|
+
f'{snapshot.pk},devices,"{{\\"hostname\\": \\"csv-device1\\", \\"vendor\\": \\"cisco\\"}}"', # noqa: E231
|
|
555
|
+
f'{snapshot.pk},interfaces,"{{\\"name\\": \\"Eth0/0\\", \\"type\\": \\"ethernet\\"}}"', # noqa: E231
|
|
556
|
+
f'{snapshot.pk},sites,"{{\\"name\\": \\"CSV Site\\", \\"location\\": \\"Boston\\"}}"', # noqa: E231
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
cls.csv_update_data = (
|
|
560
|
+
"id,type,data",
|
|
561
|
+
f'{data_instances[0].pk},devices,"{{\\"hostname\\": \\"updated-device1\\", \\"vendor\\": \\"juniper\\"}}"', # noqa: E231
|
|
562
|
+
f'{data_instances[1].pk},interfaces,"{{\\"name\\": \\"Updated-Eth0/0\\", \\"type\\": \\"ethernet\\"}}"', # noqa: E231
|
|
563
|
+
f'{data_instances[2].pk},sites,"{{\\"name\\": \\"Updated Site\\", \\"location\\": \\"Chicago\\"}}"', # noqa: E231
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
cls.bulk_edit_data = {
|
|
567
|
+
"type": "devices",
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
571
|
+
def test_get_json(self):
|
|
572
|
+
data = self._get_queryset().first()
|
|
573
|
+
response = self.client.get(
|
|
574
|
+
# No need to add +"json/" thanks to path="json" in @register_model_view
|
|
575
|
+
data.get_absolute_url()
|
|
576
|
+
)
|
|
577
|
+
self.assertHttpStatus(response, 200)
|
|
578
|
+
# Verify response contains expected content
|
|
579
|
+
self.assertContains(response, "JSON Output")
|
|
580
|
+
# Verify the data contains expected device information
|
|
581
|
+
for value in data.data.values():
|
|
582
|
+
self.assertContains(response, value)
|
|
583
|
+
|
|
584
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
585
|
+
def test_get_json_htmx(self):
|
|
586
|
+
data = self._get_queryset().first()
|
|
587
|
+
response = self.client.get(
|
|
588
|
+
# No need to add +"json/" thanks to path="json" in @register_model_view
|
|
589
|
+
data.get_absolute_url(),
|
|
590
|
+
**{"HTTP_HX-Request": "true"},
|
|
591
|
+
)
|
|
592
|
+
self.assertHttpStatus(response, 200)
|
|
593
|
+
# Verify HTMX response contains expected content
|
|
594
|
+
self.assertContains(response, "JSON Output")
|
|
595
|
+
# Verify the data contains expected device information
|
|
596
|
+
for value in data.data.values():
|
|
597
|
+
self.assertContains(response, value)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
class IPFabricSyncTestCase(
|
|
601
|
+
PluginPathMixin,
|
|
602
|
+
ViewTestCases.GetObjectViewTestCase,
|
|
603
|
+
ViewTestCases.GetObjectChangelogViewTestCase,
|
|
604
|
+
ViewTestCases.CreateObjectViewTestCase,
|
|
605
|
+
ViewTestCases.EditObjectViewTestCase,
|
|
606
|
+
ViewTestCases.DeleteObjectViewTestCase,
|
|
607
|
+
ViewTestCases.ListObjectsViewTestCase,
|
|
608
|
+
ViewTestCases.BulkDeleteObjectsViewTestCase,
|
|
609
|
+
# ViewTestCases.BulkRenameObjectsViewTestCase,
|
|
610
|
+
# ViewTestCases.BulkEditObjectsViewTestCase,
|
|
611
|
+
# ViewTestCases.BulkImportObjectsViewTestCase,
|
|
612
|
+
):
|
|
613
|
+
model = IPFabricSync
|
|
614
|
+
user_permissions = ("ipfabric_netbox.start_ipfabricsync",)
|
|
615
|
+
|
|
616
|
+
@classmethod
|
|
617
|
+
def setUpTestData(cls):
|
|
618
|
+
def get_parameters() -> dict:
|
|
619
|
+
"""Create dict of randomized but expected parameters for testing."""
|
|
620
|
+
parameters = {}
|
|
621
|
+
for param in list(ipam_parameters.keys()) + list(dcim_parameters.keys()):
|
|
622
|
+
parameters[param] = bool(random.getrandbits(1))
|
|
623
|
+
return parameters
|
|
624
|
+
|
|
625
|
+
# Create required dependencies
|
|
626
|
+
source = IPFabricSource.objects.create(
|
|
627
|
+
name="Test Source",
|
|
628
|
+
type=IPFabricSourceTypeChoices.LOCAL,
|
|
629
|
+
url="https://ipfabric.example.com",
|
|
630
|
+
status=DataSourceStatusChoices.NEW,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
snapshot1 = IPFabricSnapshot.objects.create(
|
|
634
|
+
source=source,
|
|
635
|
+
name="Test Snapshot 1",
|
|
636
|
+
snapshot_id="sync_snap001",
|
|
637
|
+
data={"version": "6.0.0", "sites": ["Site A"], "devices": 100},
|
|
638
|
+
date=timezone.now(),
|
|
639
|
+
status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
snapshot2 = IPFabricSnapshot.objects.create(
|
|
643
|
+
source=source,
|
|
644
|
+
name="Test Snapshot 2",
|
|
645
|
+
snapshot_id="sync_snap002",
|
|
646
|
+
data={"version": "6.0.1", "sites": ["Site B"], "devices": 120},
|
|
647
|
+
date=timezone.now(),
|
|
648
|
+
status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
# Create three IPFabricSync instances for testing
|
|
652
|
+
cls.syncs = (
|
|
653
|
+
IPFabricSync(
|
|
654
|
+
name="Sync Job 1",
|
|
655
|
+
snapshot_data=snapshot1,
|
|
656
|
+
status=DataSourceStatusChoices.NEW,
|
|
657
|
+
parameters=get_parameters(),
|
|
658
|
+
last_synced=timezone.now(),
|
|
659
|
+
),
|
|
660
|
+
IPFabricSync(
|
|
661
|
+
name="Sync Job 2",
|
|
662
|
+
snapshot_data=snapshot1,
|
|
663
|
+
status=DataSourceStatusChoices.COMPLETED,
|
|
664
|
+
parameters=get_parameters(),
|
|
665
|
+
last_synced=timezone.now(),
|
|
666
|
+
),
|
|
667
|
+
IPFabricSync(
|
|
668
|
+
name="Sync Job 3",
|
|
669
|
+
snapshot_data=snapshot2,
|
|
670
|
+
status=DataSourceStatusChoices.FAILED,
|
|
671
|
+
parameters=get_parameters(),
|
|
672
|
+
),
|
|
673
|
+
)
|
|
674
|
+
for sync in cls.syncs:
|
|
675
|
+
sync.save()
|
|
676
|
+
job = Job.objects.create(
|
|
677
|
+
job_id=uuid4(),
|
|
678
|
+
name="Test Ingestion Job 1",
|
|
679
|
+
object_id=sync.pk,
|
|
680
|
+
object_type=ContentType.objects.get_for_model(IPFabricSync),
|
|
681
|
+
status=JobStatusChoices.STATUS_COMPLETED,
|
|
682
|
+
completed=timezone.now(),
|
|
683
|
+
created=timezone.now(),
|
|
684
|
+
)
|
|
685
|
+
IPFabricIngestion.objects.create(
|
|
686
|
+
sync=sync,
|
|
687
|
+
job=job,
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
cls.form_data = {
|
|
691
|
+
"name": "Test Sync X",
|
|
692
|
+
"source": source.pk,
|
|
693
|
+
"snapshot_data": snapshot1.pk,
|
|
694
|
+
"auto_merge": False,
|
|
695
|
+
"update_custom_fields": True,
|
|
696
|
+
**{f"ipf_{k}": v for k, v in get_parameters().items()},
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
cls.csv_data = (
|
|
700
|
+
"name,snapshot_data,status,parameters",
|
|
701
|
+
f'Sync CSV 1,{snapshot1.pk},{DataSourceStatusChoices.NEW},"{{\\"auto_merge\\": true}}"', # noqa: E231
|
|
702
|
+
f'Sync CSV 2,{snapshot1.pk},{DataSourceStatusChoices.COMPLETED},"{{\\"auto_merge\\": false}}"', # noqa: E231
|
|
703
|
+
f'Sync CSV 3,{snapshot2.pk},{DataSourceStatusChoices.NEW},"{{\\"auto_merge\\": true}}"', # noqa: E231
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
cls.csv_update_data = (
|
|
707
|
+
"id,name,status",
|
|
708
|
+
f"{cls.syncs[0].pk},Updated Sync 1,{DataSourceStatusChoices.COMPLETED}", # noqa: E231
|
|
709
|
+
f"{cls.syncs[1].pk},Updated Sync 2,{DataSourceStatusChoices.FAILED}", # noqa: E231
|
|
710
|
+
f"{cls.syncs[2].pk},Updated Sync 3,{DataSourceStatusChoices.COMPLETED}", # noqa: E231
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
cls.bulk_edit_data = {
|
|
714
|
+
"auto_merge": True,
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
718
|
+
def test_get_htmx_request(self):
|
|
719
|
+
instance = self._get_queryset().first()
|
|
720
|
+
# Try GET with HTMX
|
|
721
|
+
response = self.client.get(
|
|
722
|
+
instance.get_absolute_url(), **{"HTTP_HX-Request": "true"}
|
|
723
|
+
)
|
|
724
|
+
self.assertHttpStatus(response, 200)
|
|
725
|
+
|
|
726
|
+
# Verify HTMX response doesn't include full page structure
|
|
727
|
+
self.assertNotContains(response, "<html>")
|
|
728
|
+
self.assertNotContains(response, "<head>")
|
|
729
|
+
|
|
730
|
+
# Verify the response contains the sync instance data
|
|
731
|
+
self.assertContains(response, instance.name)
|
|
732
|
+
self.assertContains(response, instance.last_ingestion.name)
|
|
733
|
+
self.assertContains(response, instance.last_ingestion.get_absolute_url())
|
|
734
|
+
|
|
735
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
736
|
+
def test_get_yaml_format(self):
|
|
737
|
+
self.assertIsNone(self.user.config.get("data_format"))
|
|
738
|
+
|
|
739
|
+
instance = self._get_queryset().first()
|
|
740
|
+
|
|
741
|
+
# Try GET with yaml format
|
|
742
|
+
self.assertHttpStatus(
|
|
743
|
+
self.client.get(
|
|
744
|
+
instance.get_absolute_url(), query_params={"format": "yaml"}
|
|
745
|
+
),
|
|
746
|
+
200,
|
|
747
|
+
)
|
|
748
|
+
self.user.refresh_from_db()
|
|
749
|
+
self.assertTrue(self.user.is_authenticated)
|
|
750
|
+
self.assertEqual(self.user.config.get("data_format"), "yaml")
|
|
751
|
+
|
|
752
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
753
|
+
def test_get_transformmaps(self):
|
|
754
|
+
sync = self._get_queryset().first()
|
|
755
|
+
response = self.client.get(sync.get_absolute_url() + "transformmaps/")
|
|
756
|
+
self.assertHttpStatus(response, 200)
|
|
757
|
+
|
|
758
|
+
# Check that context contains the sync object
|
|
759
|
+
self.assertIn("object", response.context)
|
|
760
|
+
self.assertEqual(response.context["object"], sync)
|
|
761
|
+
|
|
762
|
+
# Check if transform maps are displayed if they exist
|
|
763
|
+
if hasattr(sync, "transform_maps") and sync.transform_maps.exists():
|
|
764
|
+
for transform_map in sync.transform_maps.all():
|
|
765
|
+
self.assertContains(response, transform_map.name)
|
|
766
|
+
|
|
767
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
768
|
+
def test_get_ingestions(self):
|
|
769
|
+
sync = self._get_queryset().first()
|
|
770
|
+
response = self.client.get(sync.get_absolute_url() + "ingestion/")
|
|
771
|
+
self.assertHttpStatus(response, 200)
|
|
772
|
+
|
|
773
|
+
# Verify the response contains expected elements
|
|
774
|
+
self.assertContains(response, "Ingestions")
|
|
775
|
+
|
|
776
|
+
# Check that context contains the sync object
|
|
777
|
+
self.assertIn("object", response.context)
|
|
778
|
+
self.assertEqual(response.context["object"], sync)
|
|
779
|
+
|
|
780
|
+
# Check if ingestions are displayed if they exist
|
|
781
|
+
ingestions = sync.ipfabricingestion_set.all()
|
|
782
|
+
if ingestions.exists():
|
|
783
|
+
for ingestion in ingestions:
|
|
784
|
+
self.assertContains(response, str(ingestion))
|
|
785
|
+
|
|
786
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
787
|
+
def test_sync_view_get_redirect(self):
|
|
788
|
+
"""Test that GET request to sync view redirects to sync detail page."""
|
|
789
|
+
sync = self._get_queryset().first()
|
|
790
|
+
response = self.client.get(sync.get_absolute_url() + "sync/")
|
|
791
|
+
self.assertHttpStatus(response, 302)
|
|
792
|
+
self.assertRedirects(response, sync.get_absolute_url())
|
|
793
|
+
|
|
794
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
795
|
+
@patch("ipfabric_netbox.models.IPFabricSync.enqueue_sync_job")
|
|
796
|
+
def test_sync_view_post_valid(self, mock_enqueue_sync_job):
|
|
797
|
+
"""Test POST request to sync view successfully enqueues sync job."""
|
|
798
|
+
|
|
799
|
+
# Set up mock job
|
|
800
|
+
mock_job = Job(pk=123, name="Test Sync Job")
|
|
801
|
+
mock_enqueue_sync_job.return_value = mock_job
|
|
802
|
+
|
|
803
|
+
sync = self._get_queryset().first()
|
|
804
|
+
|
|
805
|
+
response = self.client.post(sync.get_absolute_url() + "sync/", follow=True)
|
|
806
|
+
|
|
807
|
+
# Should redirect to sync detail page
|
|
808
|
+
self.assertHttpStatus(response, 200)
|
|
809
|
+
self.assertRedirects(response, sync.get_absolute_url())
|
|
810
|
+
|
|
811
|
+
# Should have called enqueue_sync_job with correct parameters
|
|
812
|
+
mock_enqueue_sync_job.assert_called_once()
|
|
813
|
+
call_args = mock_enqueue_sync_job.call_args
|
|
814
|
+
self.assertEqual(call_args[1]["user"], self.user)
|
|
815
|
+
self.assertEqual(call_args[1]["adhoc"], True)
|
|
816
|
+
|
|
817
|
+
# Should show success message
|
|
818
|
+
messages = list(response.context["messages"])
|
|
819
|
+
self.assertEqual(len(messages), 1)
|
|
820
|
+
self.assertIn(f"Queued job #{mock_job.pk}", str(messages[0]))
|
|
821
|
+
|
|
822
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
823
|
+
def test_sync_view_nonexistent_sync(self):
|
|
824
|
+
"""Test sync view with non-existent sync returns 404."""
|
|
825
|
+
nonexistent_pk = 99999
|
|
826
|
+
response = self.client.post(
|
|
827
|
+
f"/plugins/ipfabric-netbox/ipfabricsync/{nonexistent_pk}/sync/"
|
|
828
|
+
)
|
|
829
|
+
self.assertHttpStatus(response, 404)
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
class IPFabricTransformMapGroupTestCase(
|
|
833
|
+
PluginPathMixin,
|
|
834
|
+
ViewTestCases.GetObjectViewTestCase,
|
|
835
|
+
ViewTestCases.GetObjectChangelogViewTestCase,
|
|
836
|
+
ViewTestCases.CreateObjectViewTestCase,
|
|
837
|
+
ViewTestCases.EditObjectViewTestCase,
|
|
838
|
+
ViewTestCases.DeleteObjectViewTestCase,
|
|
839
|
+
ViewTestCases.ListObjectsViewTestCase,
|
|
840
|
+
ViewTestCases.BulkDeleteObjectsViewTestCase,
|
|
841
|
+
# ViewTestCases.BulkRenameObjectsViewTestCase,
|
|
842
|
+
# ViewTestCases.BulkEditObjectsViewTestCase,
|
|
843
|
+
# ViewTestCases.BulkImportObjectsViewTestCase,
|
|
844
|
+
):
|
|
845
|
+
model = IPFabricTransformMapGroup
|
|
846
|
+
|
|
847
|
+
@classmethod
|
|
848
|
+
def setUpTestData(cls):
|
|
849
|
+
# Create three IPFabricTransformMapGroup instances for testing
|
|
850
|
+
groups = (
|
|
851
|
+
IPFabricTransformMapGroup(
|
|
852
|
+
name="Device Transform Group",
|
|
853
|
+
description="Group for device transformations",
|
|
854
|
+
),
|
|
855
|
+
IPFabricTransformMapGroup(
|
|
856
|
+
name="Interface Transform Group",
|
|
857
|
+
description="Group for interface transformations",
|
|
858
|
+
),
|
|
859
|
+
IPFabricTransformMapGroup(
|
|
860
|
+
name="IP Address Transform Group",
|
|
861
|
+
description="Group for IP address transformations",
|
|
862
|
+
),
|
|
863
|
+
)
|
|
864
|
+
for group in groups:
|
|
865
|
+
group.save()
|
|
866
|
+
|
|
867
|
+
cls.form_data = {
|
|
868
|
+
"name": "Test Transform Group X",
|
|
869
|
+
"description": "Test group description",
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
class IPFabricTransformMapTestCase(
|
|
874
|
+
PluginPathMixin,
|
|
875
|
+
ViewTestCases.GetObjectViewTestCase,
|
|
876
|
+
ViewTestCases.GetObjectChangelogViewTestCase,
|
|
877
|
+
ViewTestCases.CreateObjectViewTestCase,
|
|
878
|
+
ViewTestCases.EditObjectViewTestCase,
|
|
879
|
+
ViewTestCases.DeleteObjectViewTestCase,
|
|
880
|
+
ViewTestCases.ListObjectsViewTestCase,
|
|
881
|
+
ViewTestCases.BulkDeleteObjectsViewTestCase,
|
|
882
|
+
# ViewTestCases.BulkRenameObjectsViewTestCase,
|
|
883
|
+
# ViewTestCases.BulkEditObjectsViewTestCase,
|
|
884
|
+
# ViewTestCases.BulkImportObjectsViewTestCase,
|
|
885
|
+
):
|
|
886
|
+
model = IPFabricTransformMap
|
|
887
|
+
user_permissions = (
|
|
888
|
+
"ipfabric_netbox.clone_ipfabrictransformmap",
|
|
889
|
+
"ipfabric_netbox.restore_ipfabrictransformmap",
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
@classmethod
|
|
893
|
+
def setUpTestData(cls):
|
|
894
|
+
# Number of IPFabricTransformMaps created during migration
|
|
895
|
+
cls.default_maps_count = 14
|
|
896
|
+
|
|
897
|
+
# Create required dependencies
|
|
898
|
+
group = IPFabricTransformMapGroup.objects.create(
|
|
899
|
+
name="Test Group",
|
|
900
|
+
description="Test group for transform maps",
|
|
901
|
+
)
|
|
902
|
+
cls.clone_group = IPFabricTransformMapGroup.objects.create(
|
|
903
|
+
name="Test Cloning Group",
|
|
904
|
+
description="Test group for cloning transform maps",
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
# Create IPFabricTransformMap instances with groups for testing
|
|
908
|
+
maps = (
|
|
909
|
+
IPFabricTransformMap(
|
|
910
|
+
name="Device Transform Map",
|
|
911
|
+
source_model="device",
|
|
912
|
+
target_model=ContentType.objects.get(app_label="dcim", model="device"),
|
|
913
|
+
group=group,
|
|
914
|
+
),
|
|
915
|
+
IPFabricTransformMap(
|
|
916
|
+
name="Site Transform Map",
|
|
917
|
+
source_model="site",
|
|
918
|
+
target_model=ContentType.objects.get(app_label="dcim", model="site"),
|
|
919
|
+
group=group,
|
|
920
|
+
),
|
|
921
|
+
)
|
|
922
|
+
for map_obj in maps:
|
|
923
|
+
map_obj.save()
|
|
924
|
+
|
|
925
|
+
cls.form_data = {
|
|
926
|
+
"name": "Test Transform Map X",
|
|
927
|
+
"source_model": "device",
|
|
928
|
+
"target_model": ContentType.objects.get(
|
|
929
|
+
app_label="dcim", model="manufacturer"
|
|
930
|
+
).pk,
|
|
931
|
+
"group": group.pk,
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
935
|
+
def test_get_relationships(self):
|
|
936
|
+
transform_map = self._get_queryset().first()
|
|
937
|
+
response = self.client.get(transform_map.get_absolute_url() + "relationships/")
|
|
938
|
+
self.assertHttpStatus(response, 200)
|
|
939
|
+
|
|
940
|
+
# Check that context contains the transform map object
|
|
941
|
+
self.assertIn("object", response.context)
|
|
942
|
+
self.assertEqual(response.context["object"], transform_map)
|
|
943
|
+
|
|
944
|
+
# Verify the template used is correct (using the actual template)
|
|
945
|
+
self.assertTemplateUsed(
|
|
946
|
+
response, "ipfabric_netbox/inc/transform_map_relationship_map.html"
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
# Check if relationship fields are displayed if they exist
|
|
950
|
+
if transform_map.relationship_maps.exists():
|
|
951
|
+
for relationship in transform_map.relationship_maps.all():
|
|
952
|
+
self.assertContains(response, relationship.target_field)
|
|
953
|
+
|
|
954
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
955
|
+
def test_get_fields(self):
|
|
956
|
+
transform_map = self._get_queryset().first()
|
|
957
|
+
response = self.client.get(transform_map.get_absolute_url() + "fields/")
|
|
958
|
+
self.assertHttpStatus(response, 200)
|
|
959
|
+
|
|
960
|
+
# Check that context contains the transform map object
|
|
961
|
+
self.assertIn("object", response.context)
|
|
962
|
+
self.assertEqual(response.context["object"], transform_map)
|
|
963
|
+
|
|
964
|
+
# Verify the template used is correct (using the actual template)
|
|
965
|
+
self.assertTemplateUsed(
|
|
966
|
+
response, "ipfabric_netbox/inc/transform_map_field_map.html"
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
# Check if transform fields are displayed if they exist
|
|
970
|
+
if transform_map.field_maps.exists():
|
|
971
|
+
for field in transform_map.field_maps.all():
|
|
972
|
+
self.assertContains(response, field.source_field)
|
|
973
|
+
self.assertContains(response, field.target_field)
|
|
974
|
+
|
|
975
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
976
|
+
def test_clone_view_get_redirect(self):
|
|
977
|
+
"""Test that GET request to clone view redirects to transform map detail page."""
|
|
978
|
+
transform_map = self._get_queryset().first()
|
|
979
|
+
response = self.client.get(transform_map.get_absolute_url() + "clone/")
|
|
980
|
+
self.assertHttpStatus(response, 302)
|
|
981
|
+
self.assertRedirects(response, transform_map.get_absolute_url())
|
|
982
|
+
|
|
983
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
984
|
+
def test_clone_view_get_htmx(self):
|
|
985
|
+
"""Test that HTMX GET request to clone view returns form."""
|
|
986
|
+
transform_map = self._get_queryset().first()
|
|
987
|
+
response = self.client.get(
|
|
988
|
+
transform_map.get_absolute_url() + "clone/",
|
|
989
|
+
**{"HTTP_HX-Request": "true"},
|
|
990
|
+
)
|
|
991
|
+
self.assertHttpStatus(response, 200)
|
|
992
|
+
self.assertContains(response, f"Clone of {transform_map.name}")
|
|
993
|
+
|
|
994
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
995
|
+
def test_clone_view_post_valid_form(self):
|
|
996
|
+
"""Test POST request with valid form data successfully clones transform map."""
|
|
997
|
+
# Get a TransformMap with at least one field and one relationship
|
|
998
|
+
transform_map = None
|
|
999
|
+
for transform_map in self._get_queryset():
|
|
1000
|
+
if (
|
|
1001
|
+
transform_map.field_maps.count() > 0
|
|
1002
|
+
and transform_map.relationship_maps.count() > 0
|
|
1003
|
+
):
|
|
1004
|
+
break
|
|
1005
|
+
transform_map = None
|
|
1006
|
+
self.assertIsNotNone(transform_map)
|
|
1007
|
+
|
|
1008
|
+
# Valid form data
|
|
1009
|
+
form_data = {
|
|
1010
|
+
"name": "Cloned Transform Map",
|
|
1011
|
+
"group": self.clone_group.pk,
|
|
1012
|
+
"clone_fields": True,
|
|
1013
|
+
"clone_relationships": True,
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
response = self.client.post(
|
|
1017
|
+
transform_map.get_absolute_url() + "clone/",
|
|
1018
|
+
data=form_data,
|
|
1019
|
+
follow=True,
|
|
1020
|
+
**{"HTTP_HX-Request": "true"},
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
# Should redirect to old transform map detail page since it's HTMX
|
|
1024
|
+
self.assertHttpStatus(response, 200)
|
|
1025
|
+
self.assertIn("HX-Redirect", response)
|
|
1026
|
+
|
|
1027
|
+
# Verify new transform map was created
|
|
1028
|
+
cloned_map = IPFabricTransformMap.objects.get(name="Cloned Transform Map")
|
|
1029
|
+
self.assertEqual(cloned_map.source_model, transform_map.source_model)
|
|
1030
|
+
self.assertEqual(cloned_map.target_model, transform_map.target_model)
|
|
1031
|
+
self.assertNotEqual(cloned_map.group, transform_map.group)
|
|
1032
|
+
|
|
1033
|
+
# Verify fields were cloned
|
|
1034
|
+
self.assertEqual(
|
|
1035
|
+
IPFabricTransformField.objects.filter(transform_map=cloned_map).count(),
|
|
1036
|
+
transform_map.field_maps.count(),
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
# Verify relationships were cloned
|
|
1040
|
+
self.assertEqual(
|
|
1041
|
+
IPFabricRelationshipField.objects.filter(transform_map=cloned_map).count(),
|
|
1042
|
+
transform_map.relationship_maps.count(),
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1046
|
+
def test_clone_view_post_without_fields_and_relationships(self):
|
|
1047
|
+
"""Test POST request with clone_fields=False and clone_relationships=False."""
|
|
1048
|
+
transform_map = self._get_queryset().first()
|
|
1049
|
+
|
|
1050
|
+
# Create some fields and relationships
|
|
1051
|
+
IPFabricTransformField.objects.create(
|
|
1052
|
+
transform_map=transform_map,
|
|
1053
|
+
source_field="vendor",
|
|
1054
|
+
target_field="manufacturer",
|
|
1055
|
+
template="{{ object.vendor }}",
|
|
1056
|
+
)
|
|
1057
|
+
IPFabricRelationshipField.objects.create(
|
|
1058
|
+
transform_map=transform_map,
|
|
1059
|
+
source_model=ContentType.objects.get(app_label="dcim", model="site"),
|
|
1060
|
+
target_field="site",
|
|
1061
|
+
template="{{ object.site_id }}",
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
# Form data without cloning fields and relationships
|
|
1065
|
+
form_data = {
|
|
1066
|
+
"name": "Cloned Map No Fields",
|
|
1067
|
+
"clone_fields": False,
|
|
1068
|
+
"clone_relationships": False,
|
|
1069
|
+
"group": self.clone_group.pk,
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
response = self.client.post(
|
|
1073
|
+
transform_map.get_absolute_url() + "clone/",
|
|
1074
|
+
data=form_data,
|
|
1075
|
+
follow=True,
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
cloned_map = IPFabricTransformMap.objects.get(name="Cloned Map No Fields")
|
|
1079
|
+
|
|
1080
|
+
# Should redirect to new transform map detail page
|
|
1081
|
+
self.assertHttpStatus(response, 200)
|
|
1082
|
+
self.assertRedirects(response, cloned_map.get_absolute_url())
|
|
1083
|
+
|
|
1084
|
+
# Verify new transform map was created but without fields/relationships
|
|
1085
|
+
self.assertEqual(cloned_map.source_model, transform_map.source_model)
|
|
1086
|
+
self.assertEqual(cloned_map.target_model, transform_map.target_model)
|
|
1087
|
+
|
|
1088
|
+
# Verify fields were not cloned
|
|
1089
|
+
self.assertEqual(
|
|
1090
|
+
IPFabricTransformField.objects.filter(transform_map=cloned_map).count(), 0
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
# Verify relationships were not cloned
|
|
1094
|
+
self.assertEqual(
|
|
1095
|
+
IPFabricRelationshipField.objects.filter(transform_map=cloned_map).count(),
|
|
1096
|
+
0,
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1099
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1100
|
+
def test_clone_view_post_htmx_valid(self):
|
|
1101
|
+
"""Test HTMX POST request with valid form data."""
|
|
1102
|
+
transform_map = self._get_queryset().first()
|
|
1103
|
+
|
|
1104
|
+
form_data = {
|
|
1105
|
+
"name": "HTMX Cloned Map",
|
|
1106
|
+
"clone_fields": False,
|
|
1107
|
+
"clone_relationships": False,
|
|
1108
|
+
"group": self.clone_group.pk,
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
response = self.client.post(
|
|
1112
|
+
transform_map.get_absolute_url() + "clone/",
|
|
1113
|
+
data=form_data,
|
|
1114
|
+
**{"HTTP_HX-Request": "true"},
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
# Should return HX-Redirect header
|
|
1118
|
+
self.assertHttpStatus(response, 200)
|
|
1119
|
+
self.assertIn("HX-Redirect", response)
|
|
1120
|
+
|
|
1121
|
+
# Verify new transform map was created
|
|
1122
|
+
cloned_map = IPFabricTransformMap.objects.get(name="HTMX Cloned Map")
|
|
1123
|
+
self.assertEqual(cloned_map.source_model, transform_map.source_model)
|
|
1124
|
+
|
|
1125
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1126
|
+
def test_clone_view_post_invalid_form(self):
|
|
1127
|
+
"""Test POST request with invalid form data."""
|
|
1128
|
+
transform_map = self._get_queryset().first()
|
|
1129
|
+
|
|
1130
|
+
# Invalid form data - missing name
|
|
1131
|
+
form_data = {
|
|
1132
|
+
"group": self.clone_group.pk,
|
|
1133
|
+
"clone_fields": True,
|
|
1134
|
+
"clone_relationships": True,
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
response = self.client.post(
|
|
1138
|
+
transform_map.get_absolute_url() + "clone/",
|
|
1139
|
+
data=form_data,
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1142
|
+
# Should show form errors
|
|
1143
|
+
self.assertHttpStatus(response, 200)
|
|
1144
|
+
self.assertContains(response, "This field is required")
|
|
1145
|
+
|
|
1146
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1147
|
+
def test_clone_view_post_invalid_form_htmx(self):
|
|
1148
|
+
"""Test POST request with invalid form data."""
|
|
1149
|
+
transform_map = IPFabricTransformMap.objects.filter(group__isnull=False).first()
|
|
1150
|
+
|
|
1151
|
+
# Invalid form data - same group as original
|
|
1152
|
+
form_data = {
|
|
1153
|
+
"name": "HTMX Cloned Map",
|
|
1154
|
+
"group": transform_map.group.pk,
|
|
1155
|
+
"clone_fields": True,
|
|
1156
|
+
"clone_relationships": True,
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
response = self.client.post(
|
|
1160
|
+
transform_map.get_absolute_url() + "clone/",
|
|
1161
|
+
data=form_data,
|
|
1162
|
+
**{"HTTP_HX-Request": "true"},
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1165
|
+
# Should show form errors
|
|
1166
|
+
self.assertHttpStatus(response, 200)
|
|
1167
|
+
self.assertContains(
|
|
1168
|
+
response, "A transform map with this group and target model already exists."
|
|
1169
|
+
)
|
|
1170
|
+
self.assertIn("X-Debug-HTMX-Partial", response)
|
|
1171
|
+
|
|
1172
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1173
|
+
def test_clone_view_nonexistent_transform_map(self):
|
|
1174
|
+
"""Test clone view with non-existent transform map returns 404."""
|
|
1175
|
+
nonexistent_pk = 99999
|
|
1176
|
+
response = self.client.get(
|
|
1177
|
+
f"/plugins/ipfabric-netbox/ipfabrictransformmap/{nonexistent_pk}/clone/"
|
|
1178
|
+
)
|
|
1179
|
+
self.assertHttpStatus(response, 404)
|
|
1180
|
+
|
|
1181
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1182
|
+
def test_clone_view_post_general_validation_error(self):
|
|
1183
|
+
transform_map = self._get_queryset().first()
|
|
1184
|
+
|
|
1185
|
+
# Use valid form data but patch full_clean to raise ValidationError without error_dict
|
|
1186
|
+
form_data = {
|
|
1187
|
+
"name": "Test Clone General Error",
|
|
1188
|
+
"clone_fields": False,
|
|
1189
|
+
"clone_relationships": False,
|
|
1190
|
+
"group": self.clone_group.pk,
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
# Patch full_clean to raise ValidationError without error_dict
|
|
1194
|
+
with patch.object(
|
|
1195
|
+
IPFabricTransformMap,
|
|
1196
|
+
"full_clean",
|
|
1197
|
+
side_effect=ValidationError("Test error"),
|
|
1198
|
+
):
|
|
1199
|
+
response = self.client.post(
|
|
1200
|
+
transform_map.get_absolute_url() + "clone/",
|
|
1201
|
+
data=form_data,
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
# Should show form with general error
|
|
1205
|
+
self.assertHttpStatus(response, 200)
|
|
1206
|
+
self.assertContains(response, "Test error")
|
|
1207
|
+
|
|
1208
|
+
# Verify no new transform map was created due to validation error
|
|
1209
|
+
self.assertFalse(
|
|
1210
|
+
IPFabricTransformMap.objects.filter(
|
|
1211
|
+
name="Test Clone General Error"
|
|
1212
|
+
).exists()
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1216
|
+
def test_restore_view_get_non_htmx(self):
|
|
1217
|
+
"""Test that GET request to restore view without HTMX returns empty response."""
|
|
1218
|
+
response = self.client.get(self._get_url(action="restore"))
|
|
1219
|
+
self.assertHttpStatus(response, 302)
|
|
1220
|
+
|
|
1221
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1222
|
+
def test_restore_view_get_htmx(self):
|
|
1223
|
+
"""Test that HTMX GET request to restore view returns confirmation form."""
|
|
1224
|
+
response = self.client.get(
|
|
1225
|
+
self._get_url(action="restore"),
|
|
1226
|
+
**{"HTTP_HX-Request": "true"},
|
|
1227
|
+
)
|
|
1228
|
+
|
|
1229
|
+
self.assertHttpStatus(response, 200)
|
|
1230
|
+
self.assertContains(response, self._get_url(action="restore"))
|
|
1231
|
+
# Check that dependent objects are included in context
|
|
1232
|
+
self.assertIn("dependent_objects", response.context)
|
|
1233
|
+
self.assertIn("form", response.context)
|
|
1234
|
+
self.assertIn("form_url", response.context)
|
|
1235
|
+
|
|
1236
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1237
|
+
def test_restore_view_post_success(self):
|
|
1238
|
+
"""Test POST request to restore view successfully deletes ungrouped maps and rebuilds them."""
|
|
1239
|
+
default_maps = list(IPFabricTransformMap.objects.filter(group__isnull=True))
|
|
1240
|
+
self.assertEqual(len(default_maps), self.default_maps_count)
|
|
1241
|
+
|
|
1242
|
+
# Remove all transform maps created in migrations
|
|
1243
|
+
IPFabricTransformMap.objects.filter(group__isnull=True).delete()
|
|
1244
|
+
self.assertEqual(
|
|
1245
|
+
IPFabricTransformMap.objects.filter(group__isnull=True).count(), 0
|
|
1246
|
+
)
|
|
1247
|
+
|
|
1248
|
+
response = self.client.post(
|
|
1249
|
+
self._get_url(action="restore"),
|
|
1250
|
+
follow=True,
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
# Should redirect to transform map list
|
|
1254
|
+
self.assertHttpStatus(response, 200)
|
|
1255
|
+
self.assertRedirects(response, "/plugins/ipfabric/transform-map/")
|
|
1256
|
+
|
|
1257
|
+
# Verify ungrouped transform maps were restored
|
|
1258
|
+
self.assertEqual(
|
|
1259
|
+
IPFabricTransformMap.objects.filter(group__isnull=True).count(),
|
|
1260
|
+
self.default_maps_count,
|
|
1261
|
+
)
|
|
1262
|
+
for map in default_maps:
|
|
1263
|
+
self.assertTrue(
|
|
1264
|
+
IPFabricTransformMap.objects.filter(
|
|
1265
|
+
name=map.name,
|
|
1266
|
+
source_model=map.source_model,
|
|
1267
|
+
target_model=map.target_model,
|
|
1268
|
+
group__isnull=True,
|
|
1269
|
+
).exists()
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
# Verify grouped transform map still exists
|
|
1273
|
+
self.assertGreater(
|
|
1274
|
+
IPFabricTransformMap.objects.filter(group__isnull=True).count(), 0
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
def test_restore_view_requires_permission(self):
|
|
1278
|
+
"""Test that restore view requires 'ipfabric_netbox.tm_restore' permission."""
|
|
1279
|
+
# Test without required permission
|
|
1280
|
+
response = self.client.get(self._get_url(action="restore"))
|
|
1281
|
+
# Should get permission denied (403) or redirect to login depending on settings
|
|
1282
|
+
self.assertIn(response.status_code, [302, 403])
|
|
1283
|
+
|
|
1284
|
+
# Test POST without required permission
|
|
1285
|
+
response = self.client.post(self._get_url(action="restore"))
|
|
1286
|
+
# Should get permission denied (403) or redirect to login depending on settings
|
|
1287
|
+
self.assertIn(response.status_code, [302, 403])
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
# TODO: These have no detail view, so it's failing of finding reverse URL
|
|
1291
|
+
# class IPFabricTransformFieldTestCase(
|
|
1292
|
+
# PluginPathMixin,
|
|
1293
|
+
# # ViewTestCases.GetObjectViewTestCase,
|
|
1294
|
+
# # ViewTestCases.GetObjectChangelogViewTestCase,
|
|
1295
|
+
# ViewTestCases.CreateObjectViewTestCase,
|
|
1296
|
+
# ViewTestCases.EditObjectViewTestCase,
|
|
1297
|
+
# ViewTestCases.DeleteObjectViewTestCase,
|
|
1298
|
+
# ViewTestCases.ListObjectsViewTestCase,
|
|
1299
|
+
# ViewTestCases.BulkDeleteObjectsViewTestCase,
|
|
1300
|
+
# # ViewTestCases.BulkRenameObjectsViewTestCase,
|
|
1301
|
+
# # ViewTestCases.BulkEditObjectsViewTestCase,
|
|
1302
|
+
# # ViewTestCases.BulkImportObjectsViewTestCase,
|
|
1303
|
+
# ):
|
|
1304
|
+
# model = IPFabricTransformField
|
|
1305
|
+
#
|
|
1306
|
+
# @classmethod
|
|
1307
|
+
# def setUpTestData(cls):
|
|
1308
|
+
# # Create required dependencies
|
|
1309
|
+
# group = IPFabricTransformMapGroup.objects.create(
|
|
1310
|
+
# name="Test Group",
|
|
1311
|
+
# description="Test group for transform fields",
|
|
1312
|
+
# )
|
|
1313
|
+
#
|
|
1314
|
+
# transform_map = IPFabricTransformMap.objects.create(
|
|
1315
|
+
# name="Test Transform Map",
|
|
1316
|
+
# source_model="devices",
|
|
1317
|
+
# target_model=ContentType.objects.get(app_label="dcim", model="device"),
|
|
1318
|
+
# group=group,
|
|
1319
|
+
# )
|
|
1320
|
+
#
|
|
1321
|
+
# # Create three IPFabricTransformField instances for testing
|
|
1322
|
+
# fields = (
|
|
1323
|
+
# IPFabricTransformField(
|
|
1324
|
+
# transform_map=transform_map,
|
|
1325
|
+
# source_field="hostname",
|
|
1326
|
+
# target_field="name",
|
|
1327
|
+
# coalesce=False,
|
|
1328
|
+
# template="{{ object.hostname }}",
|
|
1329
|
+
# ),
|
|
1330
|
+
# IPFabricTransformField(
|
|
1331
|
+
# transform_map=transform_map,
|
|
1332
|
+
# source_field="vendor",
|
|
1333
|
+
# target_field="manufacturer",
|
|
1334
|
+
# coalesce=True,
|
|
1335
|
+
# template="{{ object.vendor }}",
|
|
1336
|
+
# ),
|
|
1337
|
+
# IPFabricTransformField(
|
|
1338
|
+
# transform_map=transform_map,
|
|
1339
|
+
# source_field="model",
|
|
1340
|
+
# target_field="device_type",
|
|
1341
|
+
# coalesce=False,
|
|
1342
|
+
# template="{{ object.model }}",
|
|
1343
|
+
# ),
|
|
1344
|
+
# )
|
|
1345
|
+
# for field in fields:
|
|
1346
|
+
# field.save()
|
|
1347
|
+
#
|
|
1348
|
+
# cls.form_data = {
|
|
1349
|
+
# "transform_map": transform_map.pk,
|
|
1350
|
+
# "source_field": "serial_number",
|
|
1351
|
+
# "target_field": "serial",
|
|
1352
|
+
# "coalesce": False,
|
|
1353
|
+
# "template": "{{ object.serial_number }}",
|
|
1354
|
+
# }
|
|
1355
|
+
#
|
|
1356
|
+
#
|
|
1357
|
+
# class IPFabricRelationshipFieldTestCase(
|
|
1358
|
+
# PluginPathMixin,
|
|
1359
|
+
# # ViewTestCases.GetObjectViewTestCase,
|
|
1360
|
+
# # ViewTestCases.GetObjectChangelogViewTestCase,
|
|
1361
|
+
# ViewTestCases.CreateObjectViewTestCase,
|
|
1362
|
+
# ViewTestCases.EditObjectViewTestCase,
|
|
1363
|
+
# ViewTestCases.DeleteObjectViewTestCase,
|
|
1364
|
+
# ViewTestCases.ListObjectsViewTestCase,
|
|
1365
|
+
# ViewTestCases.BulkDeleteObjectsViewTestCase,
|
|
1366
|
+
# # ViewTestCases.BulkRenameObjectsViewTestCase,
|
|
1367
|
+
# # ViewTestCases.BulkEditObjectsViewTestCase,
|
|
1368
|
+
# # ViewTestCases.BulkImportObjectsViewTestCase,
|
|
1369
|
+
# ):
|
|
1370
|
+
# model = IPFabricRelationshipField
|
|
1371
|
+
#
|
|
1372
|
+
# @classmethod
|
|
1373
|
+
# def setUpTestData(cls):
|
|
1374
|
+
# # Create required dependencies
|
|
1375
|
+
# group = IPFabricTransformMapGroup.objects.create(
|
|
1376
|
+
# name="Test Group",
|
|
1377
|
+
# description="Test group for relationship fields",
|
|
1378
|
+
# )
|
|
1379
|
+
#
|
|
1380
|
+
# device_ct = ContentType.objects.get(app_label="dcim", model="device")
|
|
1381
|
+
# site_ct = ContentType.objects.get(app_label="dcim", model="site")
|
|
1382
|
+
#
|
|
1383
|
+
# transform_map = IPFabricTransformMap.objects.create(
|
|
1384
|
+
# name="Test Transform Map",
|
|
1385
|
+
# source_model="devices",
|
|
1386
|
+
# target_model=device_ct,
|
|
1387
|
+
# group=group,
|
|
1388
|
+
# )
|
|
1389
|
+
#
|
|
1390
|
+
# # Create three IPFabricRelationshipField instances for testing
|
|
1391
|
+
# fields = (
|
|
1392
|
+
# IPFabricRelationshipField(
|
|
1393
|
+
# transform_map=transform_map,
|
|
1394
|
+
# source_model=site_ct,
|
|
1395
|
+
# target_field="site",
|
|
1396
|
+
# coalesce=False,
|
|
1397
|
+
# template="{{ object.site_id }}",
|
|
1398
|
+
# ),
|
|
1399
|
+
# IPFabricRelationshipField(
|
|
1400
|
+
# transform_map=transform_map,
|
|
1401
|
+
# source_model=device_ct,
|
|
1402
|
+
# target_field="parent_device",
|
|
1403
|
+
# coalesce=True,
|
|
1404
|
+
# template="{{ object.parent_id }}",
|
|
1405
|
+
# ),
|
|
1406
|
+
# IPFabricRelationshipField(
|
|
1407
|
+
# transform_map=transform_map,
|
|
1408
|
+
# source_model=site_ct,
|
|
1409
|
+
# target_field="location",
|
|
1410
|
+
# coalesce=False,
|
|
1411
|
+
# template="{{ object.location_id }}",
|
|
1412
|
+
# ),
|
|
1413
|
+
# )
|
|
1414
|
+
# for field in fields:
|
|
1415
|
+
# field.save()
|
|
1416
|
+
#
|
|
1417
|
+
# cls.form_data = {
|
|
1418
|
+
# "transform_map": transform_map.pk,
|
|
1419
|
+
# "source_model": site_ct.pk,
|
|
1420
|
+
# "target_field": "site",
|
|
1421
|
+
# "coalesce": False,
|
|
1422
|
+
# "template": "{{ object.site_id }}",
|
|
1423
|
+
# }
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
class IPFabricIngestionTestCase(
|
|
1427
|
+
PluginPathMixin,
|
|
1428
|
+
ViewTestCases.GetObjectViewTestCase,
|
|
1429
|
+
# ViewTestCases.GetObjectChangelogViewTestCase,
|
|
1430
|
+
# ViewTestCases.CreateObjectViewTestCase,
|
|
1431
|
+
# ViewTestCases.EditObjectViewTestCase,
|
|
1432
|
+
# ViewTestCases.DeleteObjectViewTestCase,
|
|
1433
|
+
ViewTestCases.ListObjectsViewTestCase,
|
|
1434
|
+
# ViewTestCases.BulkDeleteObjectsViewTestCase,
|
|
1435
|
+
# ViewTestCases.BulkRenameObjectsViewTestCase,
|
|
1436
|
+
# ViewTestCases.BulkEditObjectsViewTestCase,
|
|
1437
|
+
# ViewTestCases.BulkImportObjectsViewTestCase,
|
|
1438
|
+
):
|
|
1439
|
+
model = IPFabricIngestion
|
|
1440
|
+
user_permissions = ("ipfabric_netbox.merge_ipfabricingestion",)
|
|
1441
|
+
|
|
1442
|
+
@classmethod
|
|
1443
|
+
def setUpTestData(cls):
|
|
1444
|
+
# Create required dependencies
|
|
1445
|
+
source = IPFabricSource.objects.create(
|
|
1446
|
+
name="Test Source",
|
|
1447
|
+
type=IPFabricSourceTypeChoices.LOCAL,
|
|
1448
|
+
url="https://ipfabric.example.com",
|
|
1449
|
+
status=DataSourceStatusChoices.NEW,
|
|
1450
|
+
)
|
|
1451
|
+
|
|
1452
|
+
snapshot = IPFabricSnapshot.objects.create(
|
|
1453
|
+
source=source,
|
|
1454
|
+
name="Test Snapshot",
|
|
1455
|
+
snapshot_id="ingest_snap001",
|
|
1456
|
+
data={"devices": 100},
|
|
1457
|
+
date=timezone.now(),
|
|
1458
|
+
status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
|
|
1459
|
+
)
|
|
1460
|
+
|
|
1461
|
+
# Create Sync instances for each ingestion
|
|
1462
|
+
sync1 = IPFabricSync.objects.create(
|
|
1463
|
+
name="Test Sync 1",
|
|
1464
|
+
snapshot_data=snapshot,
|
|
1465
|
+
status=DataSourceStatusChoices.NEW,
|
|
1466
|
+
parameters={"batch_size": 100},
|
|
1467
|
+
last_synced=timezone.now(),
|
|
1468
|
+
)
|
|
1469
|
+
|
|
1470
|
+
sync2 = IPFabricSync.objects.create(
|
|
1471
|
+
name="Test Sync 2",
|
|
1472
|
+
snapshot_data=snapshot,
|
|
1473
|
+
status=DataSourceStatusChoices.COMPLETED,
|
|
1474
|
+
parameters={"batch_size": 200},
|
|
1475
|
+
last_synced=timezone.now(),
|
|
1476
|
+
)
|
|
1477
|
+
|
|
1478
|
+
sync3 = IPFabricSync.objects.create(
|
|
1479
|
+
name="Test Sync 3",
|
|
1480
|
+
snapshot_data=snapshot,
|
|
1481
|
+
status=DataSourceStatusChoices.FAILED,
|
|
1482
|
+
parameters={"batch_size": 50},
|
|
1483
|
+
)
|
|
1484
|
+
|
|
1485
|
+
# Create Job instances for each ingestion
|
|
1486
|
+
job1 = Job.objects.create(
|
|
1487
|
+
job_id=uuid4(),
|
|
1488
|
+
name="Test Ingestion Job 1",
|
|
1489
|
+
object_id=sync1.pk,
|
|
1490
|
+
object_type=ContentType.objects.get_for_model(IPFabricSync),
|
|
1491
|
+
status=JobStatusChoices.STATUS_COMPLETED,
|
|
1492
|
+
completed=timezone.now(),
|
|
1493
|
+
created=timezone.now(),
|
|
1494
|
+
)
|
|
1495
|
+
|
|
1496
|
+
job2 = Job.objects.create(
|
|
1497
|
+
job_id=uuid4(),
|
|
1498
|
+
name="Test Ingestion Job 2",
|
|
1499
|
+
object_id=sync2.pk,
|
|
1500
|
+
object_type=ContentType.objects.get_for_model(IPFabricSync),
|
|
1501
|
+
status=JobStatusChoices.STATUS_RUNNING,
|
|
1502
|
+
created=timezone.now(),
|
|
1503
|
+
)
|
|
1504
|
+
|
|
1505
|
+
job3 = Job.objects.create(
|
|
1506
|
+
job_id=uuid4(),
|
|
1507
|
+
name="Test Ingestion Job 3",
|
|
1508
|
+
object_id=sync3.pk,
|
|
1509
|
+
object_type=ContentType.objects.get_for_model(IPFabricSync),
|
|
1510
|
+
status=JobStatusChoices.STATUS_FAILED,
|
|
1511
|
+
created=timezone.now(),
|
|
1512
|
+
)
|
|
1513
|
+
|
|
1514
|
+
branch1 = Branch.objects.create(name="Test Branch 1")
|
|
1515
|
+
branch2 = Branch.objects.create(name="Test Branch 2")
|
|
1516
|
+
branch3 = Branch.objects.create(name="Test Branch 3")
|
|
1517
|
+
|
|
1518
|
+
site = Site.objects.create(name="Default Site", slug="default-site")
|
|
1519
|
+
modified = model_to_dict(site)
|
|
1520
|
+
modified["name"] = "Updated Site Name"
|
|
1521
|
+
for branch in (branch1, branch2, branch3):
|
|
1522
|
+
ChangeDiff.objects.create(
|
|
1523
|
+
branch=branch,
|
|
1524
|
+
object=site,
|
|
1525
|
+
object_type=ContentType.objects.get_for_model(site),
|
|
1526
|
+
object_repr=repr(site),
|
|
1527
|
+
original=model_to_dict(site),
|
|
1528
|
+
modified=modified,
|
|
1529
|
+
)
|
|
1530
|
+
|
|
1531
|
+
# Create three IPFabricIngestion instances for testing (linked to sync and job instances)
|
|
1532
|
+
ingestions = (
|
|
1533
|
+
IPFabricIngestion(sync=sync1, job=job1, branch=branch1),
|
|
1534
|
+
IPFabricIngestion(sync=sync2, job=job2, branch=branch2),
|
|
1535
|
+
IPFabricIngestion(sync=sync3, job=job3, branch=branch3),
|
|
1536
|
+
)
|
|
1537
|
+
for ingestion in ingestions:
|
|
1538
|
+
ingestion.save()
|
|
1539
|
+
|
|
1540
|
+
cls.form_data = {
|
|
1541
|
+
"snapshot": snapshot.pk,
|
|
1542
|
+
"status": DataSourceStatusChoices.NEW,
|
|
1543
|
+
"parameters": '{"batch_size": 150}',
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1547
|
+
def test_get_ingestion_issues(self):
|
|
1548
|
+
ingestion = self._get_queryset().first()
|
|
1549
|
+
response = self.client.get(ingestion.get_absolute_url() + "ingestion_issues/")
|
|
1550
|
+
self.assertHttpStatus(response, 200)
|
|
1551
|
+
|
|
1552
|
+
# Verify the response contains expected elements
|
|
1553
|
+
self.assertContains(response, "Ingestion Issues")
|
|
1554
|
+
|
|
1555
|
+
# Check that context contains the ingestion object
|
|
1556
|
+
self.assertIn("object", response.context)
|
|
1557
|
+
self.assertEqual(response.context["object"], ingestion)
|
|
1558
|
+
|
|
1559
|
+
# Check if issues table or empty state is displayed
|
|
1560
|
+
if hasattr(ingestion, "issues") and ingestion.issues.exists():
|
|
1561
|
+
for issue in ingestion.issues.all():
|
|
1562
|
+
self.assertContains(response, issue.model)
|
|
1563
|
+
else:
|
|
1564
|
+
# Should contain table structure even if empty
|
|
1565
|
+
self.assertContains(response, "table")
|
|
1566
|
+
|
|
1567
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1568
|
+
def test_get_logs(self):
|
|
1569
|
+
ingestion = self._get_queryset().first()
|
|
1570
|
+
response = self.client.get(ingestion.get_absolute_url() + "logs/")
|
|
1571
|
+
self.assertHttpStatus(response, 200)
|
|
1572
|
+
|
|
1573
|
+
# Verify the response contains expected elements
|
|
1574
|
+
self.assertContains(response, "Logs pending...")
|
|
1575
|
+
|
|
1576
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1577
|
+
def test_get_logs_htmx(self):
|
|
1578
|
+
ingestion = self._get_queryset().first()
|
|
1579
|
+
response = self.client.get(
|
|
1580
|
+
ingestion.get_absolute_url() + "logs/",
|
|
1581
|
+
**{"HTTP_HX-Request": "true"},
|
|
1582
|
+
)
|
|
1583
|
+
self.assertHttpStatus(response, 200)
|
|
1584
|
+
|
|
1585
|
+
# Verify HTMX-specific response characteristics
|
|
1586
|
+
self.assertIn("object", response.context)
|
|
1587
|
+
self.assertEqual(response.context["object"], ingestion)
|
|
1588
|
+
|
|
1589
|
+
# Verify HTMX response doesn't include full page structure
|
|
1590
|
+
self.assertNotContains(response, "<html>")
|
|
1591
|
+
self.assertNotContains(response, "<head>")
|
|
1592
|
+
|
|
1593
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1594
|
+
def test_get_changes(self):
|
|
1595
|
+
ingestion = self._get_queryset().first()
|
|
1596
|
+
response = self.client.get(ingestion.get_absolute_url() + "change/")
|
|
1597
|
+
self.assertHttpStatus(response, 200)
|
|
1598
|
+
|
|
1599
|
+
# Verify the response contains expected elements
|
|
1600
|
+
self.assertContains(response, "Changes")
|
|
1601
|
+
|
|
1602
|
+
# Check that context contains the ingestion object
|
|
1603
|
+
self.assertIn("object", response.context)
|
|
1604
|
+
self.assertEqual(response.context["object"], ingestion)
|
|
1605
|
+
|
|
1606
|
+
# Check if branch changes are displayed
|
|
1607
|
+
if (
|
|
1608
|
+
ingestion.branch
|
|
1609
|
+
and hasattr(ingestion.branch, "changes")
|
|
1610
|
+
and ingestion.branch.changes.exists()
|
|
1611
|
+
):
|
|
1612
|
+
for change in ingestion.branch.changes.all():
|
|
1613
|
+
self.assertContains(response, str(change.id))
|
|
1614
|
+
|
|
1615
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1616
|
+
def test_get_change(self):
|
|
1617
|
+
ingestion = self._get_queryset().first()
|
|
1618
|
+
change = ChangeDiff.objects.get(branch=ingestion.branch)
|
|
1619
|
+
response = self.client.get(
|
|
1620
|
+
ingestion.get_absolute_url() + f"change/{change.pk}/"
|
|
1621
|
+
)
|
|
1622
|
+
self.assertHttpStatus(response, 200)
|
|
1623
|
+
|
|
1624
|
+
# Verify we remove empty change diff since it's not HTMX
|
|
1625
|
+
self.assertNotContains(response, str(change))
|
|
1626
|
+
self.assertContains(response, "Change Diff None")
|
|
1627
|
+
|
|
1628
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1629
|
+
def test_get_change_htmx(self):
|
|
1630
|
+
ingestion = self._get_queryset().first()
|
|
1631
|
+
change = ChangeDiff.objects.get(branch=ingestion.branch)
|
|
1632
|
+
response = self.client.get(
|
|
1633
|
+
ingestion.get_absolute_url() + f"change/{change.pk}/",
|
|
1634
|
+
**{"HTTP_HX-Request": "true"},
|
|
1635
|
+
)
|
|
1636
|
+
self.assertHttpStatus(response, 200)
|
|
1637
|
+
|
|
1638
|
+
# Verify HTMX response doesn't include full page structure
|
|
1639
|
+
self.assertNotContains(response, "<html>")
|
|
1640
|
+
self.assertNotContains(response, "<head>")
|
|
1641
|
+
|
|
1642
|
+
# Check that change diff content is displayed
|
|
1643
|
+
self.assertContains(response, str(change))
|
|
1644
|
+
if change.modified:
|
|
1645
|
+
for key, value in change.modified.items():
|
|
1646
|
+
if isinstance(value, str):
|
|
1647
|
+
self.assertContains(response, value)
|
|
1648
|
+
|
|
1649
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1650
|
+
def test_get_change_htmx_empty_diff(self):
|
|
1651
|
+
ingestion = self._get_queryset().first()
|
|
1652
|
+
change = ChangeDiff.objects.get(branch=ingestion.branch)
|
|
1653
|
+
change.original = {}
|
|
1654
|
+
change.modified = {}
|
|
1655
|
+
change.save()
|
|
1656
|
+
response = self.client.get(
|
|
1657
|
+
ingestion.get_absolute_url() + f"change/{change.pk}/",
|
|
1658
|
+
**{"HTTP_HX-Request": "true"},
|
|
1659
|
+
)
|
|
1660
|
+
self.assertHttpStatus(response, 200)
|
|
1661
|
+
|
|
1662
|
+
# Verify HTMX response handles empty diff gracefully
|
|
1663
|
+
self.assertContains(response, str(change))
|
|
1664
|
+
|
|
1665
|
+
# Should contain some indication of empty changes
|
|
1666
|
+
self.assertContains(response, "No Changes")
|
|
1667
|
+
|
|
1668
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1669
|
+
def test_get_change_htmx_no_change(self):
|
|
1670
|
+
ingestion = self._get_queryset().first()
|
|
1671
|
+
response = self.client.get(
|
|
1672
|
+
ingestion.get_absolute_url() + "change/0/",
|
|
1673
|
+
**{"HTTP_HX-Request": "true"},
|
|
1674
|
+
)
|
|
1675
|
+
self.assertHttpStatus(response, 200)
|
|
1676
|
+
|
|
1677
|
+
# Should contain some indication that change was not found
|
|
1678
|
+
self.assertContains(response, "Change Diff None")
|
|
1679
|
+
self.assertContains(response, "No Changes")
|
|
1680
|
+
|
|
1681
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1682
|
+
def test_merge_view_get_redirect(self):
|
|
1683
|
+
"""Test that GET request to merge view redirects to ingestion detail page."""
|
|
1684
|
+
ingestion = self._get_queryset().first()
|
|
1685
|
+
response = self.client.get(ingestion.get_absolute_url() + "merge/")
|
|
1686
|
+
self.assertHttpStatus(response, 302)
|
|
1687
|
+
self.assertRedirects(response, ingestion.get_absolute_url())
|
|
1688
|
+
|
|
1689
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1690
|
+
def test_merge_view_get_htmx(self):
|
|
1691
|
+
"""Test that HTMX GET request to merge view returns form."""
|
|
1692
|
+
|
|
1693
|
+
ingestion = self._get_queryset().first()
|
|
1694
|
+
response = self.client.get(
|
|
1695
|
+
ingestion.get_absolute_url() + "merge/",
|
|
1696
|
+
**{"HTTP_HX-Request": "true"},
|
|
1697
|
+
)
|
|
1698
|
+
self.assertHttpStatus(response, 200)
|
|
1699
|
+
|
|
1700
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1701
|
+
@patch("ipfabric_netbox.forms.IPFabricIngestionMergeForm.clean")
|
|
1702
|
+
def test_merge_view_post_invalid_form(self, mock_clean):
|
|
1703
|
+
"""Test POST request with invalid form data."""
|
|
1704
|
+
# Mock the clean method to raise ValidationError to make form invalid
|
|
1705
|
+
mock_clean.side_effect = ValidationError("Mocked validation error")
|
|
1706
|
+
|
|
1707
|
+
ingestion = self._get_queryset().first()
|
|
1708
|
+
|
|
1709
|
+
# Valid form data (but form will be invalid due to mocked clean method)
|
|
1710
|
+
form_data = {"confirm": True, "remove_branch": True}
|
|
1711
|
+
|
|
1712
|
+
# The view should handle invalid form gracefully and redirect back
|
|
1713
|
+
response = self.client.post(
|
|
1714
|
+
ingestion.get_absolute_url() + "merge/", data=form_data, follow=True
|
|
1715
|
+
)
|
|
1716
|
+
|
|
1717
|
+
# Should redirect to ingestion detail page
|
|
1718
|
+
self.assertHttpStatus(response, 200)
|
|
1719
|
+
self.assertRedirects(response, ingestion.get_absolute_url())
|
|
1720
|
+
|
|
1721
|
+
# Should show error message for the validation error, 1 per field
|
|
1722
|
+
messages = list(response.context["messages"])
|
|
1723
|
+
self.assertEqual(len(messages), 2)
|
|
1724
|
+
self.assertIn("Mocked validation error", str(messages[0]))
|
|
1725
|
+
|
|
1726
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1727
|
+
@patch("ipfabric_netbox.models.Job.enqueue")
|
|
1728
|
+
def test_merge_view_post_valid_form(self, mock_enqueue):
|
|
1729
|
+
"""Test POST request with valid form data successfully enqueues merge job."""
|
|
1730
|
+
|
|
1731
|
+
# Set up mock job
|
|
1732
|
+
mock_job = Job(pk=123, name="Test Merge Job")
|
|
1733
|
+
mock_enqueue.return_value = mock_job
|
|
1734
|
+
|
|
1735
|
+
ingestion = self._get_queryset().first()
|
|
1736
|
+
|
|
1737
|
+
# Valid form data
|
|
1738
|
+
form_data = {"confirm": True, "remove_branch": True}
|
|
1739
|
+
|
|
1740
|
+
response = self.client.post(
|
|
1741
|
+
ingestion.get_absolute_url() + "merge/", data=form_data, follow=True
|
|
1742
|
+
)
|
|
1743
|
+
|
|
1744
|
+
# Should redirect to ingestion detail page
|
|
1745
|
+
self.assertHttpStatus(response, 200)
|
|
1746
|
+
self.assertRedirects(response, ingestion.get_absolute_url())
|
|
1747
|
+
|
|
1748
|
+
# Should have called Job.enqueue with correct parameters
|
|
1749
|
+
mock_enqueue.assert_called_once()
|
|
1750
|
+
call_args = mock_enqueue.call_args
|
|
1751
|
+
self.assertEqual(call_args[1]["instance"], ingestion)
|
|
1752
|
+
self.assertEqual(call_args[1]["remove_branch"], True)
|
|
1753
|
+
self.assertIn("user", call_args[1])
|
|
1754
|
+
|
|
1755
|
+
# Should show success message
|
|
1756
|
+
messages = list(response.context["messages"])
|
|
1757
|
+
self.assertEqual(len(messages), 1)
|
|
1758
|
+
self.assertIn(f"Queued job #{mock_job.pk}", str(messages[0]))
|
|
1759
|
+
|
|
1760
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1761
|
+
@patch("ipfabric_netbox.models.Job.enqueue")
|
|
1762
|
+
def test_merge_view_post_valid_form_keep_branch(self, mock_enqueue):
|
|
1763
|
+
"""Test POST request with remove_branch=False."""
|
|
1764
|
+
|
|
1765
|
+
# Set up mock job
|
|
1766
|
+
mock_job = Job(pk=124, name="Test Merge Job Keep Branch")
|
|
1767
|
+
mock_enqueue.return_value = mock_job
|
|
1768
|
+
|
|
1769
|
+
ingestion = self._get_queryset().first()
|
|
1770
|
+
|
|
1771
|
+
# Valid form data with remove_branch=False
|
|
1772
|
+
form_data = {"confirm": True, "remove_branch": False}
|
|
1773
|
+
|
|
1774
|
+
response = self.client.post(
|
|
1775
|
+
ingestion.get_absolute_url() + "merge/", data=form_data, follow=True
|
|
1776
|
+
)
|
|
1777
|
+
|
|
1778
|
+
# Should redirect to ingestion detail page
|
|
1779
|
+
self.assertHttpStatus(response, 200)
|
|
1780
|
+
|
|
1781
|
+
# Should have called Job.enqueue with remove_branch=False
|
|
1782
|
+
mock_enqueue.assert_called_once()
|
|
1783
|
+
call_args = mock_enqueue.call_args
|
|
1784
|
+
self.assertEqual(call_args[1]["remove_branch"], False)
|
|
1785
|
+
|
|
1786
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1787
|
+
def test_merge_view_nonexistent_ingestion(self):
|
|
1788
|
+
"""Test merge view with non-existent ingestion returns 404."""
|
|
1789
|
+
response = self.client.get(
|
|
1790
|
+
"/plugins/ipfabric-netbox/ipfabricingestion/99999/merge/"
|
|
1791
|
+
)
|
|
1792
|
+
self.assertHttpStatus(response, 404)
|
|
1793
|
+
|
|
1794
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1795
|
+
@patch("ipfabric_netbox.models.Job.enqueue")
|
|
1796
|
+
def test_merge_view_job_enqueue_parameters(self, mock_enqueue):
|
|
1797
|
+
"""Test that Job.enqueue is called with correct parameters."""
|
|
1798
|
+
|
|
1799
|
+
# Set up mock job with all expected attributes
|
|
1800
|
+
mock_job = Job(pk=125, name="Test Merge Job Parameters", job_id=uuid4())
|
|
1801
|
+
mock_enqueue.return_value = mock_job
|
|
1802
|
+
|
|
1803
|
+
ingestion = self._get_queryset().first()
|
|
1804
|
+
|
|
1805
|
+
form_data = {
|
|
1806
|
+
"confirm": True,
|
|
1807
|
+
"remove_branch": True,
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
self.client.post(
|
|
1811
|
+
ingestion.get_absolute_url() + "merge/", data=form_data, follow=True
|
|
1812
|
+
)
|
|
1813
|
+
|
|
1814
|
+
# Verify Job.enqueue was called exactly once
|
|
1815
|
+
mock_enqueue.assert_called_once()
|
|
1816
|
+
|
|
1817
|
+
# Get the call arguments
|
|
1818
|
+
call_args, call_kwargs = mock_enqueue.call_args
|
|
1819
|
+
|
|
1820
|
+
# Check that the first argument is the correct job function
|
|
1821
|
+
self.assertEqual(call_args[0], merge_ipfabric_ingestion)
|
|
1822
|
+
|
|
1823
|
+
# Check keyword arguments
|
|
1824
|
+
self.assertEqual(call_kwargs["name"], f"{ingestion.name} Merge")
|
|
1825
|
+
self.assertEqual(call_kwargs["instance"], ingestion)
|
|
1826
|
+
self.assertEqual(call_kwargs["remove_branch"], True)
|
|
1827
|
+
self.assertIsNotNone(call_kwargs["user"])
|
|
1828
|
+
|
|
1829
|
+
|
|
1830
|
+
class IPFabricTableViewTestCase(PluginPathMixin, ModelTestCase):
|
|
1831
|
+
model = Device
|
|
1832
|
+
|
|
1833
|
+
@classmethod
|
|
1834
|
+
def setUpTestData(cls):
|
|
1835
|
+
"""Prepare a single Device with all required data filled."""
|
|
1836
|
+
|
|
1837
|
+
manufacturer = Manufacturer.objects.create(
|
|
1838
|
+
name="Test Manufacturer", slug="test-manufacturer"
|
|
1839
|
+
)
|
|
1840
|
+
|
|
1841
|
+
device_role = DeviceRole.objects.create(
|
|
1842
|
+
name="Test Device Role",
|
|
1843
|
+
slug="test-device-role",
|
|
1844
|
+
color="ff0000", # Red color
|
|
1845
|
+
)
|
|
1846
|
+
|
|
1847
|
+
site = Site.objects.create(name="Test Site", slug="test-site")
|
|
1848
|
+
|
|
1849
|
+
device_type = DeviceType.objects.create(
|
|
1850
|
+
model="Test Device Model",
|
|
1851
|
+
slug="test-device-model",
|
|
1852
|
+
manufacturer=manufacturer,
|
|
1853
|
+
u_height=1,
|
|
1854
|
+
)
|
|
1855
|
+
|
|
1856
|
+
cls.source = IPFabricSource.objects.create(
|
|
1857
|
+
name="IP Fabric Source 1",
|
|
1858
|
+
type=IPFabricSourceTypeChoices.LOCAL,
|
|
1859
|
+
url="https://ipfabric1.example.com",
|
|
1860
|
+
status=DataSourceStatusChoices.NEW,
|
|
1861
|
+
parameters={"auth": "token1", "verify": True, "timeout": 30},
|
|
1862
|
+
last_synced=timezone.now(),
|
|
1863
|
+
)
|
|
1864
|
+
cls.snapshot = IPFabricSnapshot.objects.create(
|
|
1865
|
+
source=cls.source,
|
|
1866
|
+
name="Snapshot 1",
|
|
1867
|
+
snapshot_id="snap001",
|
|
1868
|
+
data={
|
|
1869
|
+
"version": "6.0.0",
|
|
1870
|
+
"sites": [site.name, "Site B", "Site C"],
|
|
1871
|
+
"total_dev_count": 100,
|
|
1872
|
+
"interface_count": 500,
|
|
1873
|
+
"note": "Test snapshot 1",
|
|
1874
|
+
},
|
|
1875
|
+
date=timezone.now(),
|
|
1876
|
+
status=IPFabricSnapshotStatusModelChoices.STATUS_LOADED,
|
|
1877
|
+
)
|
|
1878
|
+
cls.device = Device.objects.create(
|
|
1879
|
+
name="test-device-001",
|
|
1880
|
+
device_type=device_type,
|
|
1881
|
+
role=device_role,
|
|
1882
|
+
site=site,
|
|
1883
|
+
serial="TST123456789",
|
|
1884
|
+
asset_tag="ASSET-001",
|
|
1885
|
+
custom_field_data={"ipfabric_source": cls.source.pk},
|
|
1886
|
+
)
|
|
1887
|
+
|
|
1888
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1889
|
+
def test_get_table(self):
|
|
1890
|
+
response = self.client.get(
|
|
1891
|
+
self.device.get_absolute_url() + "ipfabric/",
|
|
1892
|
+
)
|
|
1893
|
+
self.assertHttpStatus(response, 200)
|
|
1894
|
+
|
|
1895
|
+
# Validate template used
|
|
1896
|
+
self.assertTemplateUsed(response, "ipfabric_netbox/ipfabric_table.html")
|
|
1897
|
+
|
|
1898
|
+
# Validate context variables
|
|
1899
|
+
self.assertEqual(response.context["object"], self.device)
|
|
1900
|
+
self.assertIsNotNone(response.context["form"])
|
|
1901
|
+
self.assertIsNotNone(response.context["table"])
|
|
1902
|
+
self.assertIn("tab", response.context)
|
|
1903
|
+
|
|
1904
|
+
# Validate that the source is retrieved from custom field
|
|
1905
|
+
expected_source = IPFabricSource.objects.filter(
|
|
1906
|
+
pk=self.device.custom_field_data.get("ipfabric_source")
|
|
1907
|
+
).first()
|
|
1908
|
+
self.assertEqual(response.context["source"], expected_source)
|
|
1909
|
+
|
|
1910
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1911
|
+
def test_get_table_without_cf(self):
|
|
1912
|
+
self.device.custom_field_data = {}
|
|
1913
|
+
self.device.save()
|
|
1914
|
+
response = self.client.get(
|
|
1915
|
+
self.device.get_absolute_url() + "ipfabric/",
|
|
1916
|
+
)
|
|
1917
|
+
self.assertHttpStatus(response, 200)
|
|
1918
|
+
|
|
1919
|
+
# Validate template used
|
|
1920
|
+
self.assertTemplateUsed(response, "ipfabric_netbox/ipfabric_table.html")
|
|
1921
|
+
|
|
1922
|
+
# Validate context variables
|
|
1923
|
+
self.assertEqual(response.context["object"], self.device)
|
|
1924
|
+
self.assertIsNotNone(response.context["form"])
|
|
1925
|
+
self.assertIsNotNone(response.context["table"])
|
|
1926
|
+
|
|
1927
|
+
# When no custom field is set, source should be retrieved from site
|
|
1928
|
+
expected_source = IPFabricSource.get_for_site(self.device.site).first()
|
|
1929
|
+
self.assertEqual(response.context["source"], expected_source)
|
|
1930
|
+
|
|
1931
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1932
|
+
def test_get_table_with_table_param(self):
|
|
1933
|
+
response = self.client.get(
|
|
1934
|
+
self.device.get_absolute_url() + "ipfabric/",
|
|
1935
|
+
query_params={"table": tableChoices[0][0]},
|
|
1936
|
+
)
|
|
1937
|
+
self.assertHttpStatus(response, 200)
|
|
1938
|
+
|
|
1939
|
+
# Validate template used
|
|
1940
|
+
self.assertTemplateUsed(response, "ipfabric_netbox/ipfabric_table.html")
|
|
1941
|
+
|
|
1942
|
+
# Validate context variables
|
|
1943
|
+
self.assertEqual(response.context["object"], self.device)
|
|
1944
|
+
self.assertIsNotNone(response.context["form"])
|
|
1945
|
+
self.assertIsNotNone(response.context["table"])
|
|
1946
|
+
|
|
1947
|
+
# Validate that form has the correct initial table value
|
|
1948
|
+
form = response.context["form"]
|
|
1949
|
+
self.assertEqual(form.initial.get("table"), None)
|
|
1950
|
+
|
|
1951
|
+
# Validate that the source is retrieved from custom field
|
|
1952
|
+
expected_source = IPFabricSource.objects.filter(
|
|
1953
|
+
pk=self.device.custom_field_data.get("ipfabric_source")
|
|
1954
|
+
).first()
|
|
1955
|
+
self.assertEqual(response.context["source"], expected_source)
|
|
1956
|
+
|
|
1957
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1958
|
+
def test_get_table_without_source(self):
|
|
1959
|
+
self.device.custom_field_data = {}
|
|
1960
|
+
self.device.save()
|
|
1961
|
+
self.source.delete()
|
|
1962
|
+
response = self.client.get(
|
|
1963
|
+
self.device.get_absolute_url() + "ipfabric/",
|
|
1964
|
+
query_params={"table": tableChoices[0][0]},
|
|
1965
|
+
)
|
|
1966
|
+
self.assertHttpStatus(response, 200)
|
|
1967
|
+
|
|
1968
|
+
# Validate template used
|
|
1969
|
+
self.assertTemplateUsed(response, "ipfabric_netbox/ipfabric_table.html")
|
|
1970
|
+
|
|
1971
|
+
# Validate context variables
|
|
1972
|
+
self.assertEqual(response.context["object"], self.device)
|
|
1973
|
+
self.assertIsNotNone(response.context["form"])
|
|
1974
|
+
self.assertEqual(response.context["table"], tableChoices[0][0])
|
|
1975
|
+
|
|
1976
|
+
# When no source is available, source should be None
|
|
1977
|
+
self.assertIsNone(response.context["source"])
|
|
1978
|
+
|
|
1979
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1980
|
+
def test_get_table_with_snapshot_data(self):
|
|
1981
|
+
response = self.client.get(
|
|
1982
|
+
self.device.get_absolute_url() + "ipfabric/",
|
|
1983
|
+
query_params={
|
|
1984
|
+
"table": tableChoices[0][0],
|
|
1985
|
+
"source": self.source.pk,
|
|
1986
|
+
"snapshot_data": self.snapshot.pk,
|
|
1987
|
+
},
|
|
1988
|
+
)
|
|
1989
|
+
self.assertHttpStatus(response, 200)
|
|
1990
|
+
|
|
1991
|
+
# Validate template used
|
|
1992
|
+
self.assertTemplateUsed(response, "ipfabric_netbox/ipfabric_table.html")
|
|
1993
|
+
|
|
1994
|
+
# Validate context variables
|
|
1995
|
+
self.assertEqual(response.context["object"], self.device)
|
|
1996
|
+
self.assertIsNotNone(response.context["form"])
|
|
1997
|
+
self.assertIsNotNone(response.context["table"])
|
|
1998
|
+
self.assertEqual(response.context["source"], self.source)
|
|
1999
|
+
|
|
2000
|
+
# Note: The actual implementation doesn't set form initial values,
|
|
2001
|
+
# it validates the form and uses cleaned_data instead
|
|
2002
|
+
form = response.context["form"]
|
|
2003
|
+
self.assertTrue(form.is_valid())
|
|
2004
|
+
self.assertEqual(form.cleaned_data.get("source"), self.source)
|
|
2005
|
+
self.assertEqual(form.cleaned_data.get("snapshot_data"), self.snapshot)
|
|
2006
|
+
|
|
2007
|
+
# Validate that the response contains expected elements
|
|
2008
|
+
self.assertContains(response, self.device.name)
|
|
2009
|
+
self.assertContains(response, self.source.name)
|
|
2010
|
+
|
|
2011
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
2012
|
+
@patch("ipfabric_netbox.utilities.ipfutils.IPFClient")
|
|
2013
|
+
def test_get_table_with_cache(self, mock_ipfclient_class):
|
|
2014
|
+
# Clear cache before the test
|
|
2015
|
+
cache.clear()
|
|
2016
|
+
|
|
2017
|
+
table_name = tableChoices[0][0]
|
|
2018
|
+
|
|
2019
|
+
# Mock the IPFClient instance that gets created inside IPFabric.__init__
|
|
2020
|
+
mock_ipfclient_instance = mock_ipfclient_class.return_value
|
|
2021
|
+
mock_ipfclient_instance._client.headers = {"user-agent": "test-user-agent"}
|
|
2022
|
+
mock_ipfclient_instance.get_columns.return_value = [
|
|
2023
|
+
"id",
|
|
2024
|
+
"hostname",
|
|
2025
|
+
"vendor",
|
|
2026
|
+
"model",
|
|
2027
|
+
]
|
|
2028
|
+
|
|
2029
|
+
mock_table = getattr(mock_ipfclient_instance.inventory, table_name)
|
|
2030
|
+
mock_table.all.return_value = [
|
|
2031
|
+
{"hostname": "mock-device-1", "vendor": "cisco", "model": "ISR4331"},
|
|
2032
|
+
]
|
|
2033
|
+
mock_table.endpoint = f"inventory/{table_name}"
|
|
2034
|
+
|
|
2035
|
+
response = self.client.get(
|
|
2036
|
+
self.device.get_absolute_url() + "ipfabric/",
|
|
2037
|
+
query_params={
|
|
2038
|
+
"table": table_name,
|
|
2039
|
+
"cache_enable": "True",
|
|
2040
|
+
},
|
|
2041
|
+
)
|
|
2042
|
+
self.assertHttpStatus(response, 200)
|
|
2043
|
+
|
|
2044
|
+
mock_ipfclient_class.assert_called_once()
|
|
2045
|
+
mock_table.all.assert_called_once()
|
|
2046
|
+
|
|
2047
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
2048
|
+
def test_get_table_htmx(self):
|
|
2049
|
+
response = self.client.get(
|
|
2050
|
+
self.device.get_absolute_url() + "ipfabric/",
|
|
2051
|
+
query_params={"table": tableChoices[0][0]},
|
|
2052
|
+
**{"HTTP_HX-Request": "true"},
|
|
2053
|
+
)
|
|
2054
|
+
self.assertHttpStatus(response, 200)
|
|
2055
|
+
|
|
2056
|
+
# Validate HTMX-specific behavior - should not include full page structure
|
|
2057
|
+
self.assertNotContains(response, "<html>")
|
|
2058
|
+
self.assertNotContains(response, "<head>")
|
|
2059
|
+
|
|
2060
|
+
# Validate that response contains the htmx form elements
|
|
2061
|
+
self.assertContains(response, "hx-target") # HTMX attributes should be present
|
|
2062
|
+
|
|
2063
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
2064
|
+
@patch("ipfabric_netbox.utilities.ipfutils.IPFClient")
|
|
2065
|
+
def test_get_table_with_snapshot_data_and_api_call(self, mock_ipfclient_class):
|
|
2066
|
+
"""Test that snapshot data properly triggers API calls when needed."""
|
|
2067
|
+
# Mock the IPFClient instance
|
|
2068
|
+
mock_ipfclient_instance = mock_ipfclient_class.return_value
|
|
2069
|
+
mock_ipfclient_instance._client.headers = {"user-agent": "test-user-agent"}
|
|
2070
|
+
mock_ipfclient_instance.get_columns.return_value = ["id", "hostname", "vendor"]
|
|
2071
|
+
|
|
2072
|
+
table_name = tableChoices[0][0]
|
|
2073
|
+
mock_table = getattr(mock_ipfclient_instance.inventory, table_name)
|
|
2074
|
+
mock_table.all.return_value = [
|
|
2075
|
+
{"hostname": "test-device-001", "vendor": "cisco", "model": "ISR4331"},
|
|
2076
|
+
]
|
|
2077
|
+
mock_table.endpoint = f"inventory/{table_name}"
|
|
2078
|
+
|
|
2079
|
+
response = self.client.get(
|
|
2080
|
+
self.device.get_absolute_url() + "ipfabric/",
|
|
2081
|
+
query_params={
|
|
2082
|
+
"table": table_name,
|
|
2083
|
+
"source": self.source.pk,
|
|
2084
|
+
"snapshot_data": self.snapshot.pk,
|
|
2085
|
+
"cache_enable": "False", # Disable cache to force API call
|
|
2086
|
+
},
|
|
2087
|
+
)
|
|
2088
|
+
self.assertHttpStatus(response, 200)
|
|
2089
|
+
|
|
2090
|
+
# Validate that API was called
|
|
2091
|
+
mock_ipfclient_class.assert_called_once()
|
|
2092
|
+
mock_table.all.assert_called_once()
|
|
2093
|
+
|
|
2094
|
+
# Validate that response contains the mocked data
|
|
2095
|
+
self.assertContains(response, "test-device-001")
|
|
2096
|
+
self.assertContains(response, "cisco")
|
|
2097
|
+
|
|
2098
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
2099
|
+
def test_get_table_htmx_with_empty_table_param(self):
|
|
2100
|
+
"""Test HTMX request with empty table parameter."""
|
|
2101
|
+
response = self.client.get(
|
|
2102
|
+
self.device.get_absolute_url() + "ipfabric/",
|
|
2103
|
+
query_params={"table": ""},
|
|
2104
|
+
**{"HTTP_HX-Request": "true"},
|
|
2105
|
+
)
|
|
2106
|
+
self.assertHttpStatus(response, 200)
|
|
2107
|
+
|
|
2108
|
+
# For HTMX requests with empty table, it still returns a table context
|
|
2109
|
+
self.assertIn("table", response.context)
|
|
2110
|
+
self.assertIsNotNone(response.context["table"])
|
|
2111
|
+
|
|
2112
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
2113
|
+
def test_get_table_with_invalid_snapshot_data(self):
|
|
2114
|
+
"""Test behavior with invalid snapshot data parameter."""
|
|
2115
|
+
response = self.client.get(
|
|
2116
|
+
self.device.get_absolute_url() + "ipfabric/",
|
|
2117
|
+
query_params={
|
|
2118
|
+
"table": tableChoices[0][0],
|
|
2119
|
+
"source": self.source.pk,
|
|
2120
|
+
"snapshot_data": 99999, # Non-existent snapshot ID
|
|
2121
|
+
},
|
|
2122
|
+
)
|
|
2123
|
+
self.assertHttpStatus(response, 200)
|
|
2124
|
+
|
|
2125
|
+
# Should handle invalid snapshot gracefully
|
|
2126
|
+
self.assertEqual(response.context["object"], self.device)
|
|
2127
|
+
self.assertEqual(response.context["source"], self.source)
|
|
2128
|
+
|
|
2129
|
+
# The form should be invalid due to invalid snapshot_data
|
|
2130
|
+
form = response.context["form"]
|
|
2131
|
+
self.assertFalse(form.is_valid())
|
|
2132
|
+
|
|
2133
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
2134
|
+
def test_get_table_htmx_form_validation(self):
|
|
2135
|
+
"""Test HTMX request form validation and data handling."""
|
|
2136
|
+
response = self.client.get(
|
|
2137
|
+
self.device.get_absolute_url() + "ipfabric/",
|
|
2138
|
+
query_params={
|
|
2139
|
+
"table": tableChoices[0][0],
|
|
2140
|
+
"source": self.source.pk,
|
|
2141
|
+
"cache_enable": "True",
|
|
2142
|
+
},
|
|
2143
|
+
**{"HTTP_HX-Request": "true"},
|
|
2144
|
+
)
|
|
2145
|
+
self.assertHttpStatus(response, 200)
|
|
2146
|
+
|
|
2147
|
+
# For HTMX requests, context only contains table object
|
|
2148
|
+
self.assertIn("table", response.context)
|
|
2149
|
+
self.assertIsNotNone(response.context["table"])
|
|
2150
|
+
|
|
2151
|
+
# HTMX requests don't include full page structure
|
|
2152
|
+
self.assertNotContains(response, "<html>")
|
|
2153
|
+
self.assertNotContains(response, "<body>")
|