diff --git a/src/Digitall.Dataverse.Testing/OrganizationRequests/DeepInsertProcessor.cs b/src/Digitall.Dataverse.Testing/OrganizationRequests/DeepInsertProcessor.cs index 6be3dbf..6f8b99c 100644 --- a/src/Digitall.Dataverse.Testing/OrganizationRequests/DeepInsertProcessor.cs +++ b/src/Digitall.Dataverse.Testing/OrganizationRequests/DeepInsertProcessor.cs @@ -68,21 +68,55 @@ private static void ProcessOneToMany( EntityCollection children, FakeOrganizationService state) { - if (metadata.ReferencedEntity != parentLogicalName) + if (metadata.ReferencedEntity == parentLogicalName) { - ErrorFactory.ThrowFault( - ErrorCodes.InvalidArgument, - $"Relationship '{metadata.SchemaName}' references entity '{metadata.ReferencedEntity}' " + - $"but the parent entity is '{parentLogicalName}'. The relationship metadata does not match the parent."); + // Parent is the "one" side → set FK on each child pointing to parent + var parentRef = new EntityReference(parentLogicalName, parentId); + + foreach (var child in children.Entities) + { + child[metadata.ReferencingAttribute] = parentRef; + state.Create(child); + } } + else if (metadata.ReferencingEntity == parentLogicalName) + { + // Parent is the "many" side (N:1) → lookup can only point to a single record + if (children.Entities.Count > 1) + { + ErrorFactory.ThrowFault( + ErrorCodes.InvalidArgument, + $"Relationship '{metadata.SchemaName}' is used from the referencing (N:1) side. " + + "A lookup attribute can only reference a single entity, but multiple related entities were provided."); + } - var parentRef = new EntityReference(parentLogicalName, parentId); + var child = children.Entities[0]; - foreach (var child in children.Entities) + if (child.LogicalName != metadata.ReferencedEntity) + { + ErrorFactory.ThrowFault( + ErrorCodes.InvalidArgument, + $"Relationship '{metadata.SchemaName}' expects referenced entity '{metadata.ReferencedEntity}' " + + $"but the related entity is '{child.LogicalName}'."); + } + + // Create child first, then set FK on parent pointing to child + var childId = state.Create(child); + var childRef = new EntityReference(metadata.ReferencedEntity, childId); + + var parentUpdate = new Entity(parentLogicalName, parentId) + { + [metadata.ReferencingAttribute] = childRef + }; + state.Update(parentUpdate); + } + else { - // Set the foreign key (lookup) on the child pointing to the parent - child[metadata.ReferencingAttribute] = parentRef; - state.Create(child); + ErrorFactory.ThrowFault( + ErrorCodes.InvalidArgument, + $"Relationship '{metadata.SchemaName}' is between '{metadata.ReferencedEntity}' and " + + $"'{metadata.ReferencingEntity}' but the parent entity is '{parentLogicalName}'. " + + "The relationship metadata does not match the parent."); } } diff --git a/tests/Digitall.Dataverse.Testing.Tests/OrganizationRequests/CreateFakeDeepInsertTests.cs b/tests/Digitall.Dataverse.Testing.Tests/OrganizationRequests/CreateFakeDeepInsertTests.cs index 23fd4bd..3534d16 100644 --- a/tests/Digitall.Dataverse.Testing.Tests/OrganizationRequests/CreateFakeDeepInsertTests.cs +++ b/tests/Digitall.Dataverse.Testing.Tests/OrganizationRequests/CreateFakeDeepInsertTests.cs @@ -5,6 +5,7 @@ using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Messages; using Microsoft.Xrm.Sdk.Metadata; +using Microsoft.Xrm.Sdk.Query; namespace Digitall.Dataverse.Testing.Tests.OrganizationRequests; @@ -216,4 +217,107 @@ account.RelatedEntities[new Relationship("systemuserroles_association")] = await Assert.That(act).Throws(); } + + [Test] + public async Task Create_WithReversedOneToMany_SetsForeignKeyOnParent() + { + // Scenario: calendarrule (Referencing) has a lookup to calendar (Referenced) + // Deep insert from calendarrule side should create the calendar and set FK on calendarrule + _sut.State.Relationships["calendarrule_innercalendar"] = new OneToManyRelationshipMetadata + { + SchemaName = "calendarrule_innercalendar", + ReferencedEntity = "calendar", + ReferencedAttribute = "calendarid", + ReferencingEntity = "calendarrule", + ReferencingAttribute = "innercalendarid" + }; + + // Also register the parent relationship: calendar → calendarrule + _sut.State.Relationships["calendar_calendar_rules"] = new OneToManyRelationshipMetadata + { + SchemaName = "calendar_calendar_rules", + ReferencedEntity = "calendar", + ReferencedAttribute = "calendarid", + ReferencingEntity = "calendarrule", + ReferencingAttribute = "calendarid" + }; + + var calendar = new Entity("calendar") { Id = Guid.NewGuid(), ["name"] = "Business Hours" }; + var innerCalendar = new Entity("calendar") { ["name"] = "Inner Schedule" }; + var calendarRule = new Entity("calendarrule") { ["description"] = "Rule 1" }; + + // Nested deep insert: calendar → calendarrule → innercalendar (reverse direction) + calendarRule.RelatedEntities[new Relationship("calendarrule_innercalendar")] = + new EntityCollection([innerCalendar]); + + calendar.RelatedEntities[new Relationship("calendar_calendar_rules")] = + new EntityCollection([calendarRule]); + + _sut.Execute(new CreateRequest { Target = calendar }); + + // Verify all entities were created + var calendars = _sut.CreateQuery("calendar").ToList(); + var rules = _sut.CreateQuery("calendarrule").ToList(); + + await Assert.That(calendars).Count().IsEqualTo(2); // parent + inner + await Assert.That(rules).Count().IsEqualTo(1); + + // Verify the FK on calendarrule points to the inner calendar + var createdRule = rules.Single(); + var innerCalRef = createdRule.GetAttributeValue("innercalendarid"); + await Assert.That(innerCalRef).IsNotNull(); + await Assert.That(innerCalRef!.LogicalName).IsEqualTo("calendar"); + + // The inner calendar should be the one named "Inner Schedule" + var innerCal = _sut.Retrieve("calendar", innerCalRef.Id, new ColumnSet(true)); + await Assert.That(innerCal.GetAttributeValue("name")).IsEqualTo("Inner Schedule"); + } + + [Test] + public async Task Create_WithReversedOneToMany_MultipleChildren_ThrowsFault() + { + // N:1 direction: a lookup can only point to one record + _sut.State.Relationships["calendarrule_innercalendar"] = new OneToManyRelationshipMetadata + { + SchemaName = "calendarrule_innercalendar", + ReferencedEntity = "calendar", + ReferencedAttribute = "calendarid", + ReferencingEntity = "calendarrule", + ReferencingAttribute = "innercalendarid" + }; + + var rule = new Entity("calendarrule") { Id = Guid.NewGuid(), ["description"] = "Rule" }; + var cal1 = new Entity("calendar") { ["name"] = "Cal 1" }; + var cal2 = new Entity("calendar") { ["name"] = "Cal 2" }; + + rule.RelatedEntities[new Relationship("calendarrule_innercalendar")] = + new EntityCollection([cal1, cal2]); // Two children on N:1 → invalid + + var act = () => _sut.Execute(new CreateRequest { Target = rule }); + + await Assert.That(act).Throws(); + } + + [Test] + public async Task Create_WithReversedOneToMany_WrongChildLogicalName_ThrowsFault() + { + _sut.State.Relationships["calendarrule_innercalendar"] = new OneToManyRelationshipMetadata + { + SchemaName = "calendarrule_innercalendar", + ReferencedEntity = "calendar", + ReferencedAttribute = "calendarid", + ReferencingEntity = "calendarrule", + ReferencingAttribute = "innercalendarid" + }; + + var rule = new Entity("calendarrule") { Id = Guid.NewGuid(), ["description"] = "Rule" }; + var wrongEntity = new Entity("account") { ["name"] = "Not a calendar" }; + + rule.RelatedEntities[new Relationship("calendarrule_innercalendar")] = + new EntityCollection([wrongEntity]); + + var act = () => _sut.Execute(new CreateRequest { Target = rule }); + + await Assert.That(act).Throws(); + } }