Skip to content

Commit d9d4bc8

Browse files
author
Samson Gebre
committed
test: Fix to _upsert_multiple: alternate key fields no longer merged into the body; add unit tests for _ODataClient._build_upsert_multiple validation
1 parent fff4185 commit d9d4bc8

2 files changed

Lines changed: 94 additions & 5 deletions

File tree

src/PowerPlatform/Dataverse/data/_odata.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1745,12 +1745,19 @@ def _build_upsert_multiple(
17451745
alt_key_lower = self._lowercase_keys(alt_key)
17461746
record_processed = self._lowercase_keys(record)
17471747
record_processed = self._convert_labels_to_ints(table, record_processed)
1748-
combined: Dict[str, Any] = {**alt_key_lower, **record_processed}
1749-
if "@odata.type" not in combined:
1750-
combined["@odata.type"] = f"Microsoft.Dynamics.CRM.{logical_name}"
1748+
conflicting = {
1749+
k for k in set(alt_key_lower) & set(record_processed) if alt_key_lower[k] != record_processed[k]
1750+
}
1751+
if conflicting:
1752+
raise ValidationError(
1753+
f"record payload conflicts with alternate_key on fields: {sorted(conflicting)!r}",
1754+
subcode="upsert_key_conflict",
1755+
)
1756+
if "@odata.type" not in record_processed:
1757+
record_processed["@odata.type"] = f"Microsoft.Dynamics.CRM.{logical_name}"
17511758
key_str = self._build_alternate_key_str(alt_key)
1752-
combined["@odata.id"] = f"{entity_set}({key_str})"
1753-
targets.append(combined)
1759+
record_processed["@odata.id"] = f"{entity_set}({key_str})"
1760+
targets.append(record_processed)
17541761
return _RawRequest(
17551762
method="POST",
17561763
url=f"{self.api}/{entity_set}/Microsoft.Dynamics.CRM.UpsertMultiple",

tests/unit/data/test_odata_internal.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,5 +341,87 @@ def test_returns_none(self):
341341
self.assertIsNone(result)
342342

343343

344+
class TestBuildUpsertMultiple(unittest.TestCase):
345+
"""Unit tests for _ODataClient._build_upsert_multiple (batch deferred build)."""
346+
347+
def setUp(self):
348+
self.od = _make_odata_client()
349+
350+
def _targets(self, alt_keys, records):
351+
import json
352+
353+
req = self.od._build_upsert_multiple("accounts", "account", alt_keys, records)
354+
return json.loads(req.body)["Targets"]
355+
356+
def test_payload_excludes_alternate_key_fields(self):
357+
"""Alternate key fields must NOT appear in the request body (only in @odata.id)."""
358+
targets = self._targets(
359+
[{"accountnumber": "ACC-001"}],
360+
[{"name": "Contoso"}],
361+
)
362+
self.assertEqual(len(targets), 1)
363+
target = targets[0]
364+
self.assertNotIn("accountnumber", target)
365+
self.assertIn("name", target)
366+
self.assertIn("@odata.id", target)
367+
self.assertIn("accountnumber", target["@odata.id"])
368+
369+
def test_payload_allows_matching_key_field_in_record(self):
370+
"""If user passes matching key field in record with same value, it passes through to body."""
371+
targets = self._targets(
372+
[{"accountnumber": "ACC-001"}],
373+
[{"accountnumber": "ACC-001", "name": "Contoso"}],
374+
)
375+
target = targets[0]
376+
self.assertIn("name", target)
377+
self.assertIn("@odata.id", target)
378+
self.assertIn("accountnumber", target["@odata.id"])
379+
380+
def test_odata_type_added_when_absent(self):
381+
"""@odata.type is injected when not provided by caller."""
382+
targets = self._targets(
383+
[{"accountnumber": "ACC-001"}],
384+
[{"name": "Contoso"}],
385+
)
386+
self.assertIn("@odata.type", targets[0])
387+
self.assertEqual(targets[0]["@odata.type"], "Microsoft.Dynamics.CRM.account")
388+
389+
def test_multiple_targets_all_have_odata_id(self):
390+
"""Each target in a multi-item call gets its own @odata.id."""
391+
targets = self._targets(
392+
[{"accountnumber": "ACC-001"}, {"accountnumber": "ACC-002"}],
393+
[{"name": "Contoso"}, {"name": "Fabrikam"}],
394+
)
395+
self.assertEqual(len(targets), 2)
396+
self.assertIn("ACC-001", targets[0]["@odata.id"])
397+
self.assertIn("ACC-002", targets[1]["@odata.id"])
398+
399+
def test_conflicting_key_field_raises(self):
400+
"""Raises when a record field contradicts its alternate key value."""
401+
with self.assertRaises(Exception) as ctx:
402+
self.od._build_upsert_multiple(
403+
"accounts",
404+
"account",
405+
[{"accountnumber": "ACC-001"}],
406+
[{"accountnumber": "ACC-WRONG", "name": "Contoso"}],
407+
)
408+
self.assertIn("accountnumber", str(ctx.exception))
409+
410+
def test_mismatched_lengths_raises(self):
411+
"""Raises when alternate_keys and records lengths differ."""
412+
with self.assertRaises(Exception):
413+
self.od._build_upsert_multiple(
414+
"accounts", "account", [{"accountnumber": "ACC-001"}], []
415+
)
416+
417+
def test_url_contains_upsert_multiple_action(self):
418+
"""POST URL targets the UpsertMultiple bound action."""
419+
req = self.od._build_upsert_multiple(
420+
"accounts", "account", [{"accountnumber": "ACC-001"}], [{"name": "Contoso"}]
421+
)
422+
self.assertIn("UpsertMultiple", req.url)
423+
self.assertEqual(req.method, "POST")
424+
425+
344426
if __name__ == "__main__":
345427
unittest.main()

0 commit comments

Comments
 (0)