diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs index 328223c9ce..57bdef72cc 100644 --- a/src/Core/Configurations/RuntimeConfigValidator.cs +++ b/src/Core/Configurations/RuntimeConfigValidator.cs @@ -810,8 +810,24 @@ public void ValidateRelationshipsInConfig(RuntimeConfig runtimeConfig, IMetadata string databaseName = runtimeConfig.GetDataSourceNameFromEntityName(entityName); ISqlMetadataProvider sqlMetadataProvider = sqlMetadataProviderFactory.GetMetadataProvider(databaseName); - foreach (EntityRelationship relationship in entity.Relationships!.Values) + // Dictionary to store mapping from target entity's name to relationship name. Whenever we encounter that we + // are getting more than 1 entry for a target entity, we throw a validation error as it indicates the user has + // defined multiple relationships between the same source and target entities. + Dictionary targetEntityNameToRelationshipName = new(); + foreach ((string relationshipName, EntityRelationship relationship) in entity.Relationships!) { + string targetEntityName = relationship.TargetEntity; + if (targetEntityNameToRelationshipName.TryGetValue(targetEntityName, out string? duplicateRelationshipName)) + { + HandleOrRecordException(new DataApiBuilderException( + message: $"Defining multiple relationships: {duplicateRelationshipName}, {relationshipName} between source entity: {entityName} and target entity: {targetEntityName} is not supported.", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError)); + } + + // Add entry for this relationship to the dictionary tracking all the relationships for this entity. + targetEntityNameToRelationshipName[targetEntityName] = relationshipName; + // Validate if entity referenced in relationship is defined in the config. if (!runtimeConfig.Entities.ContainsKey(relationship.TargetEntity)) { diff --git a/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs b/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs index 6d6065b866..b367131d55 100644 --- a/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs +++ b/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs @@ -336,6 +336,110 @@ public void TestAddingRelationshipWithDisabledGraphQL() Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode); } + /// + /// Testing the RuntimeCOnfigValidator.ValidateRelationshipsInConfig() method to ensure that we throw a validation error + /// when GraphQL is enabled on the source entity and the user defines multiple relationships between the same source and target entities. + /// + [DataTestMethod] + [DataRow(true, DisplayName = "Validate that an exception is thrown when GQL is enabled and user defines multiple relationship between source and target entities.")] + [DataRow(false, DisplayName = "Validate that no exception is thrown when GQL is disabled and user defines multiple relationship between source and target entities.")] + public void TestMultipleRelationshipsBetweenSourceAndTargetEntities(bool isGQLEnabledForSource) + { + string sourceEntityName = "SourceEntity", targetEntityName = "TargetEntity"; + + // Create relationship between source and target entities. + EntityRelationship relationship = new( + Cardinality: Cardinality.One, + TargetEntity: targetEntityName, + SourceFields: new string[] { "abc" }, + TargetFields: new string[] { "xyz" }, + LinkingObject: null, + LinkingSourceFields: null, + LinkingTargetFields: null + ); + + // Add another relationship between the same source and target entities. + EntityRelationship duplicateRelationship = new( + Cardinality: Cardinality.Many, + TargetEntity: targetEntityName, + SourceFields: null, + TargetFields: null, + LinkingObject: null, + LinkingSourceFields: null, + LinkingTargetFields: null + ); + + string relationshipName = "relationship", duplicateRelationshipName = "duplicateRelationship"; + Dictionary relationshipMap = new() + { + { relationshipName, relationship }, + { duplicateRelationshipName, duplicateRelationship } + }; + + // Creating source entity with enabled graphQL + Entity sourceEntity = GetSampleEntityUsingSourceAndRelationshipMap( + source: "TEST_SOURCE1", + relationshipMap: relationshipMap, + graphQLDetails: new(Singular: "", Plural: "", Enabled: isGQLEnabledForSource) + ); + + // Creating target entity. + Entity targetEntity = GetSampleEntityUsingSourceAndRelationshipMap( + source: "TEST_SOURCE2", + relationshipMap: null, + graphQLDetails: new("", "", true) + ); + + Dictionary entityMap = new() + { + { sourceEntityName, sourceEntity }, + { targetEntityName, targetEntity } + }; + + RuntimeConfig runtimeConfig = new( + Schema: "UnitTestSchema", + DataSource: new DataSource(DatabaseType: DatabaseType.MSSQL, "", Options: null), + Runtime: new( + Rest: new(), + GraphQL: new(), + Host: new(null, null) + ), + Entities: new(entityMap) + ); + + RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator(); + Mock _sqlMetadataProvider = new(); + Dictionary mockDictionaryForEntityDatabaseObject = new() + { + { + sourceEntityName, + new DatabaseTable("dbo", "TEST_SOURCE1") + }, + + { + targetEntityName, + new DatabaseTable("dbo", "TEST_SOURCE2") + } + }; + + _sqlMetadataProvider.Setup(x => x.EntityToDatabaseObject).Returns(mockDictionaryForEntityDatabaseObject); + Mock _metadataProviderFactory = new(); + _metadataProviderFactory.Setup(x => x.GetMetadataProvider(It.IsAny())).Returns(_sqlMetadataProvider.Object); + + if (isGQLEnabledForSource) + { + // Assert for expected exception. + DataApiBuilderException ex = Assert.ThrowsException(() => + configValidator.ValidateRelationshipsInConfig(runtimeConfig, _metadataProviderFactory.Object)); + Assert.AreEqual($"Defining multiple relationships: {relationshipName}, {duplicateRelationshipName} between source entity: {sourceEntityName} and target entity: {targetEntityName} is not supported.", ex.Message); + Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode); + } + else + { + configValidator.ValidateRelationshipsInConfig(runtimeConfig, _metadataProviderFactory.Object); + } + } + /// /// Test method to check that an exception is thrown when LinkingObject was provided /// while either LinkingSourceField or SourceField is null, and either targetFields or LinkingTargetField is null.