Skip to content

Commit 4fa6c7b

Browse files
committed
fix: fix errors attempting value harmonization
cleanup error messages
1 parent 905e2c8 commit 4fa6c7b

File tree

5 files changed

+122
-17
lines changed

5 files changed

+122
-17
lines changed

netbox_diode_plugin/api/applier.py

+13-6
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def apply_changeset(change_set: ChangeSet, request) -> ChangeSetResult:
2323
_validate_change_set(change_set)
2424

2525
created = {}
26-
for i, change in enumerate(change_set.changes):
26+
for change in change_set.changes:
2727
change_type = change.change_type
2828
object_type = change.object_type
2929

@@ -35,9 +35,14 @@ def apply_changeset(change_set: ChangeSet, request) -> ChangeSetResult:
3535
data = _pre_apply(model_class, change, created)
3636
_apply_change(data, model_class, change, created, request)
3737
except ValidationError as e:
38-
raise _err_from_validation_error(e, f"changes[{i}]")
38+
raise _err_from_validation_error(e, object_type)
3939
except ObjectDoesNotExist:
40-
raise _err(f"{object_type} with id {change.object_id} does not exist", f"changes[{i}]", "object_id")
40+
raise _err(f"{object_type} with id {change.object_id} does not exist", object_type, "object_id")
41+
except TypeError as e:
42+
# this indicates a problem in model validation (should raise ValidationError)
43+
# but raised non-validation error (TypeError) -- we don't know which field trigged it.
44+
logger.error(f"invalid data type for unspecified field (validation raised non-validation error): {data}: {e}")
45+
raise _err("invalid data type for field", object_type, "__all__")
4146
# ConstraintViolationError ?
4247
# ...
4348

@@ -113,13 +118,15 @@ def _validate_change_set(change_set: ChangeSet):
113118
if not change_set.changes:
114119
raise _err("Changes are required", "changeset", "changes")
115120

116-
for i, change in enumerate(change_set.changes):
121+
for change in change_set.changes:
117122
if change.object_id is None and change.ref_id is None:
118-
raise _err("Object ID or Ref ID must be provided", f"changes[{i}]", NON_FIELD_ERRORS)
123+
raise _err("Object ID or Ref ID must be provided", change.object_type, NON_FIELD_ERRORS)
119124
if change.change_type not in ChangeType:
120-
raise _err(f"Unsupported change type '{change.change_type}'", f"changes[{i}]", "change_type")
125+
raise _err(f"Unsupported change type '{change.change_type}'", change.object_type, "change_type")
121126

122127
def _err(message, object_name, field):
128+
if not object_name:
129+
object_name = "__all__"
123130
return ChangeSetException(message, errors={object_name: {field: [message]}})
124131

125132
def _err_from_validation_error(e, object_name):

netbox_diode_plugin/api/differ.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from django.core.exceptions import ValidationError
1111
from utilities.data import shallow_compare_dict
1212

13-
from .common import Change, ChangeSet, ChangeSetException, ChangeSetResult, ChangeType
13+
from .common import Change, ChangeSet, ChangeSetException, ChangeSetResult, ChangeType, UnresolvedReference
1414
from .plugin_utils import get_primary_value, legal_fields
1515
from .supported_models import extract_supported_models
1616
from .transformer import cleanup_unresolved_references, set_custom_field_defaults, transform_proto_json
@@ -84,12 +84,20 @@ def prechange_data_from_instance(instance) -> dict: # noqa: C901
8484

8585
def _harmonize_formats(prechange_data: dict, postchange_data: dict):
8686
for k, v in prechange_data.items():
87+
if k.startswith('_'):
88+
continue
8789
if isinstance(v, datetime.datetime):
8890
prechange_data[k] = v.strftime("%Y-%m-%dT%H:%M:%SZ")
8991
elif isinstance(v, datetime.date):
9092
prechange_data[k] = v.strftime("%Y-%m-%d")
9193
elif isinstance(v, int) and k in postchange_data:
92-
postchange_data[k] = int(postchange_data[k])
94+
val = postchange_data[k]
95+
if isinstance(val, UnresolvedReference):
96+
continue
97+
try:
98+
postchange_data[k] = int(val)
99+
except Exception:
100+
continue
93101
elif isinstance(v, dict):
94102
_harmonize_formats(v, postchange_data.get(k, {}))
95103

netbox_diode_plugin/api/transformer.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,10 @@ def _prepare_custom_fields(object_type: str, custom_fields: dict) -> tuple[dict,
452452
out[key] = value
453453
elif value_type == "date":
454454
# truncate to YYYY-MM-DD
455-
out[key] = datetime.datetime.fromisoformat(value).strftime("%Y-%m-%d")
455+
try:
456+
out[key] = datetime.datetime.fromisoformat(value).strftime("%Y-%m-%d")
457+
except Exception:
458+
out[key] = value
456459
elif value_type == "integer":
457460
out[key] = int(value)
458461
elif value_type == "json":

netbox_diode_plugin/tests/test_api_apply_change_set.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ def test_change_type_create_with_error_return_400(self):
300300

301301
self.assertIn(
302302
'Expected a list of items but got type "int".',
303-
_get_error(response, "changes[0]", "asns"),
303+
_get_error(response, "dcim.site", "asns"),
304304
)
305305
self.assertFalse(site_created.exists())
306306

@@ -334,7 +334,7 @@ def test_change_type_update_with_error_return_400(self):
334334
site_updated = Site.objects.get(id=20)
335335
self.assertIn(
336336
'Expected a list of items but got type "int".',
337-
_get_error(response, "changes[0]", "asns")
337+
_get_error(response, "dcim.site", "asns")
338338
)
339339
self.assertEqual(site_updated.name, "Site 2")
340340

@@ -478,7 +478,7 @@ def test_change_type_create_and_update_with_error_in_one_object_return_400(self)
478478

479479
self.assertIn(
480480
"Related object not found using the provided numeric ID: 3",
481-
_get_error(response, "changes[1]", "device_type"),
481+
_get_error(response, "dcim.device", "device_type"),
482482
)
483483
self.assertFalse(site_created.exists())
484484
self.assertFalse(device_created.exists())
@@ -548,7 +548,7 @@ def test_multiples_create_type_error_in_two_objects_return_400(self):
548548

549549
self.assertIn(
550550
"Related object not found using the provided numeric ID: 3",
551-
_get_error(response, "changes[1]", "device_type"),
551+
_get_error(response, "dcim.device", "device_type"),
552552
)
553553

554554
self.assertFalse(site_created.exists())
@@ -587,7 +587,7 @@ def test_change_type_update_with_object_id_not_exist_return_400(self):
587587

588588
self.assertIn(
589589
"dcim.site with id 30 does not exist",
590-
_get_error(response, "changes[0]", "object_id"),
590+
_get_error(response, "dcim.site", "object_id"),
591591
)
592592
self.assertEqual(site_updated.name, "Site 2")
593593

@@ -655,7 +655,7 @@ def test_change_type_field_not_provided_return_400(
655655

656656
self.assertIn(
657657
"Unsupported change type ''",
658-
_get_error(response, "changes[0]", "change_type"),
658+
_get_error(response, "dcim.site", "change_type"),
659659
)
660660

661661
def test_change_set_id_field_and_change_set_not_provided_return_400(self):
@@ -720,7 +720,7 @@ def test_change_type_and_object_type_provided_return_400(
720720

721721
self.assertIn(
722722
"Unsupported change type 'None'",
723-
_get_error(response, "changes[0]", "change_type"),
723+
_get_error(response, "__all__", "change_type"),
724724
)
725725
# self.assertEqual(
726726
# response.json().get("errors")[0].get("change_type"),
@@ -992,7 +992,7 @@ def test_create_prefix_with_unknown_site_fails(self):
992992
response = self.send_request(payload, status_code=status.HTTP_400_BAD_REQUEST)
993993
self.assertIn(
994994
'Please select a site.',
995-
_get_error(response, "changes[0]", "scope"),
995+
_get_error(response, "ipam.prefix", "scope"),
996996
)
997997
self.assertFalse(Prefix.objects.filter(prefix="192.168.0.0/24").exists())
998998

netbox_diode_plugin/tests/test_api_diff_and_apply.py

+87
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,64 @@ def test_generate_diff_and_apply_create_interface_with_tags(self):
113113
self.assertEqual(new_interface.tags.first().name, "tag 1")
114114

115115

116+
def test_generate_diff_and_apply_create_and_update_device_role(self):
117+
"""Test generate diff and apply create and update device role."""
118+
device_uuid = str(uuid4())
119+
role_1_uuid = str(uuid4())
120+
role_2_uuid = str(uuid4())
121+
site_uuid = str(uuid4())
122+
payload = {
123+
"timestamp": 1,
124+
"object_type": "dcim.device",
125+
"entity": {
126+
"device": {
127+
"name": f"Device {device_uuid}",
128+
"deviceType": {
129+
"model": f"Device Type {uuid4()}",
130+
"manufacturer": {
131+
"name": f"Manufacturer {uuid4()}"
132+
}
133+
},
134+
"role": {
135+
"name": f"Role {role_1_uuid}"
136+
},
137+
"site": {
138+
"name": f"Site {site_uuid}"
139+
}
140+
},
141+
}
142+
}
143+
_, response = self.diff_and_apply(payload)
144+
new_device = Device.objects.get(name=f"Device {device_uuid}")
145+
self.assertEqual(new_device.site.name, f"Site {site_uuid}")
146+
self.assertEqual(new_device.role.name, f"Role {role_1_uuid}")
147+
payload = {
148+
"timestamp": 1,
149+
"object_type": "dcim.device",
150+
"entity": {
151+
"device": {
152+
"name": f"Device {device_uuid}",
153+
"deviceType": {
154+
"model": f"Device Type {uuid4()}",
155+
"manufacturer": {
156+
"name": f"Manufacturer {uuid4()}"
157+
}
158+
},
159+
"role": {
160+
"name": f"Role {role_2_uuid}"
161+
},
162+
"site": {
163+
"name": f"Site {site_uuid}"
164+
}
165+
},
166+
}
167+
}
168+
_, response = self.diff_and_apply(payload)
169+
device = Device.objects.get(name=f"Device {device_uuid}")
170+
self.assertEqual(device.site.name, f"Site {site_uuid}")
171+
self.assertEqual(device.role.name, f"Role {role_2_uuid}")
172+
173+
116174
def test_generate_diff_and_apply_create_site_autoslug(self):
117175
"""Test generate diff and apply create site."""
118176
"""Test generate diff create site."""
@@ -321,6 +379,35 @@ def test_generate_diff_and_apply_create_and_update_site_with_custom_field(self):
321379
diff = response1.json().get("change_set", {})
322380
self.assertEqual(diff.get("changes", []), [])
323381

382+
def test_generate_diff_wrong_type_date(self):
383+
"""Test generate diff wrong type date."""
384+
payload = {
385+
"timestamp": 1,
386+
"object_type": "dcim.site",
387+
"entity": {
388+
"site": {
389+
"name": "Site Generate Diff 1",
390+
"slug": "site-generate-diff-1",
391+
"customFields": {
392+
"mydate": {
393+
"date": 12,
394+
},
395+
},
396+
},
397+
}
398+
}
399+
response1 = self.client.post(
400+
self.diff_url, data=payload, format="json", **self.user_header
401+
)
402+
self.assertEqual(response1.status_code, status.HTTP_200_OK)
403+
404+
diff = response1.json().get("change_set", {})
405+
406+
response2 = self.client.post(
407+
self.apply_url, data=diff, format="json", **self.user_header
408+
)
409+
self.assertEqual(response2.status_code, status.HTTP_400_BAD_REQUEST)
410+
324411

325412
def diff_and_apply(self, payload):
326413
"""Diff and apply the payload."""

0 commit comments

Comments
 (0)