Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
7ad10d2
add checks for validation, missing exceptions still
aaronburtle Mar 26, 2024
5ca3f37
add exceptions
aaronburtle Mar 27, 2024
ba13dc8
add comments and clarify exception messages
aaronburtle Mar 27, 2024
56a0d15
added unit tests
aaronburtle Mar 28, 2024
a8460a8
white space
aaronburtle Mar 28, 2024
551dac3
substatus codes
aaronburtle Mar 28, 2024
9c221e3
test language matching engine language
aaronburtle Mar 28, 2024
2621590
old tests failing new validation, adjusting
aaronburtle Mar 28, 2024
8498232
old tests failing new validation, adjusting
aaronburtle Mar 28, 2024
afb698e
remove redundant cases
aaronburtle Mar 28, 2024
6211c5c
Merge branch 'main' into dev/aaronburtle/ValidateSourceAndTargetField…
aaronburtle Mar 28, 2024
a1f5c41
readability
aaronburtle Mar 28, 2024
c7366f3
removed case that doesn't make sense
aaronburtle Mar 29, 2024
7dcd9cb
typo in test
aaronburtle Mar 29, 2024
d46925b
Merge branch 'main' into dev/aaronburtle/ValidateSourceAndTargetField…
aaronburtle Mar 29, 2024
0625fbc
typo in test
aaronburtle Mar 29, 2024
3e1a919
revert
aaronburtle Apr 2, 2024
b357e19
Merge branch 'main' of github.com:Azure/data-api-builder
aaronburtle Apr 3, 2024
19d9665
...
aaronburtle Apr 30, 2024
bb4ceba
try catch backing column validation
aaronburtle May 2, 2024
a701e6a
add regression test, custom exception
aaronburtle May 3, 2024
1447d2c
remove permissions from test config string
aaronburtle May 3, 2024
10eac89
exposed name function added, comments added
aaronburtle May 3, 2024
6dc7888
removed unused variable fix some white space
aaronburtle May 3, 2024
6d7823c
empty permission section
aaronburtle May 3, 2024
83abb02
revert unwanted change
aaronburtle May 3, 2024
c85a3fb
unused variable
aaronburtle May 3, 2024
e407ccc
add flag for unit test
aaronburtle May 3, 2024
5e5d011
deconflict exceptions by using keynotfound, adjust tests
aaronburtle May 3, 2024
6830d86
addressing comments, clarify language in comments
aaronburtle May 4, 2024
6f24635
fix schema to placeholder
aaronburtle May 4, 2024
a7d9b3e
change test name
aaronburtle May 4, 2024
24f1ada
clarify variable name
aaronburtle May 4, 2024
40577c4
cleanup
aaronburtle May 6, 2024
f7e8fb1
Merge branch 'main' into dev/aaronburtle/DabValidateUnhandledException
seantleonard May 6, 2024
d5bd316
bypass validation on backend when conncetion unavailable at init
aaronburtle May 7, 2024
a844520
Merge branch 'dev/aaronburtle/DabValidateUnhandledException' of githu…
aaronburtle May 7, 2024
809aa4c
refactor to split entity relationship validation into no DB meta, pro…
aaronburtle May 8, 2024
41bc215
whitespace
aaronburtle May 8, 2024
b343ff9
whitespace
aaronburtle May 8, 2024
df27061
change names, adjust tests
aaronburtle May 9, 2024
a1d8786
cleanup tests
aaronburtle May 9, 2024
e895f35
clarify comment
aaronburtle May 9, 2024
d6ff789
more effecient lookup
aaronburtle May 9, 2024
8a4e874
Merge branch 'main' into dev/aaronburtle/DabValidateUnhandledException
seantleonard May 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions src/Cli.Tests/TestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,104 @@ public static Process ExecuteDabCommand(string command, string flags)
}
}";

public const string COMPLETE_CONFIG_WITH_RELATIONSHIPS_NON_WORKING_CONN_STRING = @"
{
""$schema"": ""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json"",
""data-source"": {
""database-type"": ""mssql"",
""options"": {
""set-session-context"": false
},
""connection-string"": ""Server=XXXXX;Persist Security Info=False;User ID=<USERHERE>;Password=<PWD HERE> ;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=5;""
},
""runtime"": {
""rest"": {
""enabled"": true,
""path"": ""/api""
},
""graphql"": {
""allow-introspection"": true,
""enabled"": true,
""path"": ""/graphql""
},
""host"": {
""mode"": ""development"",
""cors"": {
""origins"": [],
""allow-credentials"": false
},
""authentication"": {
""provider"": ""StaticWebApps""
}
}
},
""entities"": {
""Publisher"": {
""source"": {
""object"": ""publishers"",
""type"": ""table"",
""key-fields"": [ ""id"" ]
},
""graphql"": {
""enabled"": true,
""type"": {
""singular"": ""Publisher"",
""plural"": ""Publishers""
}
},
""rest"": {
""enabled"": true
},
""permissions"": [
],
""relationships"": {
""books"": {
""cardinality"": ""many"",
""target.entity"": ""Book"",
""source.fields"": [ ""id"" ],
""target.fields"": [ ""publisher_id"" ],
""linking.source.fields"": [],
""linking.target.fields"": []
}
}
},
""Book"": {
""source"": {
""object"": ""books"",
""type"": ""table"",
""key-fields"": [ ""id"" ]
},
""graphql"": {
""enabled"": true,
""type"": {
""singular"": ""book"",
""plural"": ""books""
}
},
""rest"": {
""enabled"": true
},
""permissions"": [
],
""mappings"": {
""id"": ""id"",
""title"": ""title""
},
""relationships"": {
""publishers"": {
""cardinality"": ""one"",
""target.entity"": ""Publisher"",
""source.fields"": [ ""publisher_id"" ],
""target.fields"": [ ""id"" ],
""linking.source.fields"": [],
""linking.target.fields"": []
}
}
}
}
}
";

/// <summary>
/// Creates basic initialization options for MS SQL config.
/// </summary>
Expand Down
25 changes: 25 additions & 0 deletions src/Cli.Tests/ValidateConfigTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,31 @@ public void TestConfigWithCustomPropertyAsInvalid()
Assert.IsFalse(isConfigValid);
}

/// <summary>
/// This method verifies that the relationship validation does not cause unhandled
/// exceptions, and that the errors generated include the expected messaging.
/// This case is a regression test due to the metadata needed not always being
/// populated in the SqlMetadataProvider if for example a bad connection string
/// is given.
/// </summary>
[TestMethod]
public void TestErrorHandlingForRelationshipValidationWithNonWorkingConnectionString()
{
// Arrange
((MockFileSystem)_fileSystem!).AddFile(TEST_RUNTIME_CONFIG_FILE, COMPLETE_CONFIG_WITH_RELATIONSHIPS_NON_WORKING_CONN_STRING);
ValidateOptions validateOptions = new(TEST_RUNTIME_CONFIG_FILE);
StringWriter writer = new();
// Capture console output to get error messaging.
Console.SetOut(writer);

// Act
ConfigGenerator.IsConfigValid(validateOptions, _runtimeConfigLoader!, _fileSystem!);
string errorMessage = writer.ToString();

// Assert
Assert.IsTrue(errorMessage.Contains(DataApiBuilderException.CONNECTION_STRING_ERROR_MESSAGE));
}

/// <summary>
/// Validates that the IsConfigValid method returns false when a config is passed with
/// both rest and graphQL disabled globally.
Expand Down
321 changes: 185 additions & 136 deletions src/Core/Configurations/RuntimeConfigValidator.cs

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,23 +92,31 @@ bool VerifyForeignKeyExistsInDB(
/// try to get the exposed name associated
/// with the provided field, if it exists, save in out
/// parameter, and return true, otherwise return false.
/// If an entity name is provided that does not exist
/// as metadata in this metadata provider, a KeyNotFoundException
/// is thrown.
/// </summary>
/// <param name="entityName">The entity whose mapping we lookup.</param>
/// <param name="backingFieldName">The field used for the lookup in the mapping.</param>
/// <param name="name">Out parameter in which we will save exposed name.</param>
/// <returns>True if exists, false otherwise.</returns>
/// <throws>KeyNotFoundException if entity name not found.</throws>
bool TryGetExposedColumnName(string entityName, string backingFieldName, [NotNullWhen(true)] out string? name);

/// <summary>
/// For the entity that is provided as an argument,
/// try to get the underlying backing column name associated
/// with the provided field, if it exists, save in out
/// parameter, and return true, otherwise return false.
/// If an entity name is provided that does not exist
/// as metadata in this metadata provider, a KeyNotFoundException
/// is thrown.
/// </summary>
/// <param name="entityName">The entity whose mapping we lookup.</param>
/// <param name="field">The field used for the lookup in the mapping.</param>
/// <param name="name"/>Out parameter in which we will save backing column name.<param>
/// <returns>True if exists, false otherwise.</returns>
/// <throws>KeyNotFoundException if entity name not found.</throws>
bool TryGetBackingColumn(string entityName, string field, [NotNullWhen(true)] out string? name);

/// <summary>
Expand Down
16 changes: 14 additions & 2 deletions src/Core/Services/MetadataProviders/SqlMetadataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,13 +209,25 @@ public StoredProcedureDefinition GetStoredProcedureDefinition(string entityName)
/// <inheritdoc />
public bool TryGetExposedColumnName(string entityName, string backingFieldName, [NotNullWhen(true)] out string? name)
{
return EntityBackingColumnsToExposedNames[entityName].TryGetValue(backingFieldName, out name);
Dictionary<string, string>? backingColumnsToExposedNamesMap;
if (!EntityBackingColumnsToExposedNames.TryGetValue(entityName, out backingColumnsToExposedNamesMap))
{
throw new KeyNotFoundException($"Initialization of metadata incomplete for entity: {entityName}");
}

return backingColumnsToExposedNamesMap.TryGetValue(backingFieldName, out name);
}

/// <inheritdoc />
public bool TryGetBackingColumn(string entityName, string field, [NotNullWhen(true)] out string? name)
{
return EntityExposedNamesToBackingColumnNames[entityName].TryGetValue(field, out name);
Dictionary<string, string>? exposedNamesToBackingColumnsMap;
if (!EntityExposedNamesToBackingColumnNames.TryGetValue(entityName, out exposedNamesToBackingColumnsMap))
{
throw new KeyNotFoundException($"Initialization of metadata incomplete for entity: {entityName}");
}

return exposedNamesToBackingColumnsMap.TryGetValue(field, out name);
}

/// <inheritdoc />
Expand Down
3 changes: 3 additions & 0 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1271,6 +1271,7 @@ public async Task TestSqlMetadataForValidConfigEntities()
configValidatorLogger.Object,
isValidateOnly: true);

configValidator.ValidateRelationshipConfigCorrectness(configProvider.GetConfig());
await configValidator.ValidateEntitiesMetadata(configProvider.GetConfig(), mockLoggerFactory);
Assert.IsTrue(configValidator.ConfigValidationExceptions.IsNullOrEmpty());
}
Expand Down Expand Up @@ -1337,6 +1338,7 @@ public async Task TestSqlMetadataForInvalidConfigEntities()

ILoggerFactory mockLoggerFactory = TestHelper.ProvisionLoggerFactory();

configValidator.ValidateRelationshipConfigCorrectness(configProvider.GetConfig());
await configValidator.ValidateEntitiesMetadata(configProvider.GetConfig(), mockLoggerFactory);

Assert.IsTrue(configValidator.ConfigValidationExceptions.Any());
Expand Down Expand Up @@ -1418,6 +1420,7 @@ public async Task TestSqlMetadataValidationForEntitiesWithInvalidSource()

try
{
configValidator.ValidateRelationshipConfigCorrectness(configProvider.GetConfig());
await configValidator.ValidateEntitiesMetadata(configProvider.GetConfig(), mockLoggerFactory);
}
catch
Expand Down
41 changes: 9 additions & 32 deletions src/Service.Tests/Unittests/ConfigValidationUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,13 +262,10 @@ public void TestAddingRelationshipWithInvalidTargetEntity()
);

RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator();
Mock<ISqlMetadataProvider> _sqlMetadataProvider = new();
Mock<IMetadataProviderFactory> _metadataProviderFactory = new();
_metadataProviderFactory.Setup(x => x.GetMetadataProvider(It.IsAny<string>())).Returns(_sqlMetadataProvider.Object);

// Assert that expected exception is thrown. Entity used in relationship is Invalid
DataApiBuilderException ex = Assert.ThrowsException<DataApiBuilderException>(() =>
configValidator.ValidateRelationshipsInConfig(runtimeConfig, _metadataProviderFactory.Object));
configValidator.ValidateRelationshipConfigCorrectness(runtimeConfig));
Assert.AreEqual($"Entity: {sampleRelationship.TargetEntity} used for relationship is not defined in the config.", ex.Message);
Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode);
}
Expand Down Expand Up @@ -325,13 +322,10 @@ public void TestAddingRelationshipWithDisabledGraphQL()
);

RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator();
Mock<ISqlMetadataProvider> _sqlMetadataProvider = new();
Mock<IMetadataProviderFactory> _metadataProviderFactory = new();
_metadataProviderFactory.Setup(x => x.GetMetadataProvider(It.IsAny<string>())).Returns(_sqlMetadataProvider.Object);

// Exception should be thrown as we cannot use an entity (with graphQL disabled) in a relationship.
DataApiBuilderException ex = Assert.ThrowsException<DataApiBuilderException>(() =>
configValidator.ValidateRelationshipsInConfig(runtimeConfig, _metadataProviderFactory.Object));
configValidator.ValidateRelationshipConfigCorrectness(runtimeConfig));
Assert.AreEqual($"Entity: {sampleRelationship.TargetEntity} is disabled for GraphQL.", ex.Message);
Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode);
}
Expand Down Expand Up @@ -417,7 +411,7 @@ string relationshipEntity

// Exception thrown as foreignKeyPair not found in the DB.
DataApiBuilderException ex = Assert.ThrowsException<DataApiBuilderException>(() =>
configValidator.ValidateRelationshipsInConfig(runtimeConfig, _metadataProviderFactory.Object));
configValidator.ValidateRelationships(runtimeConfig, _metadataProviderFactory.Object));
Assert.AreEqual($"Could not find relationship between Linking Object: TEST_SOURCE_LINK"
+ $" and entity: {relationshipEntity}.", ex.Message);
Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode);
Expand All @@ -435,7 +429,7 @@ string relationshipEntity

// Since, we have defined the relationship in Database,
// the engine was able to find foreign key relation and validation will pass.
configValidator.ValidateRelationshipsInConfig(runtimeConfig, _metadataProviderFactory.Object);
configValidator.ValidateRelationships(runtimeConfig, _metadataProviderFactory.Object);
}

/// <summary>
Expand Down Expand Up @@ -498,7 +492,7 @@ public void TestRelationshipWithNoLinkingObjectAndEitherSourceOrTargetFieldIsNul
// Exception is thrown as foreignKey pair is not specified in the config, nor defined
// in the database.
DataApiBuilderException ex = Assert.ThrowsException<DataApiBuilderException>(() =>
configValidator.ValidateRelationshipsInConfig(runtimeConfig, _metadataProviderFactory.Object));
configValidator.ValidateRelationships(runtimeConfig, _metadataProviderFactory.Object));
Assert.AreEqual($"Could not find relationship between entities:"
+ $" SampleEntity1 and SampleEntity2.", ex.Message);
Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode);
Expand All @@ -518,7 +512,7 @@ public void TestRelationshipWithNoLinkingObjectAndEitherSourceOrTargetFieldIsNul

// No Exception is thrown as foreignKey Pair was found in the DB between
// source and target entity.
configValidator.ValidateRelationshipsInConfig(runtimeConfig, _metadataProviderFactory.Object);
configValidator.ValidateRelationships(runtimeConfig, _metadataProviderFactory.Object);
}

/// <summary>
Expand Down Expand Up @@ -566,7 +560,6 @@ public void TestRelationshipWithoutSourceAndTargetFieldsMatching(
FileSystemRuntimeConfigLoader loader = new(fileSystem);
RuntimeConfigProvider provider = new(loader) { IsLateConfigured = true };
RuntimeConfigValidator configValidator = new(provider, fileSystem, new Mock<ILogger<RuntimeConfigValidator>>().Object);
Mock<ISqlMetadataProvider> _sqlMetadataProvider = new();

Dictionary<string, DatabaseObject> mockDictionaryForEntityDatabaseObject = new()
{
Expand All @@ -581,16 +574,10 @@ public void TestRelationshipWithoutSourceAndTargetFieldsMatching(
}
};

_sqlMetadataProvider.Setup<Dictionary<string, DatabaseObject>>(x =>
x.EntityToDatabaseObject).Returns(mockDictionaryForEntityDatabaseObject);

Mock<IMetadataProviderFactory> _metadataProviderFactory = new();
_metadataProviderFactory.Setup(x => x.GetMetadataProvider(It.IsAny<string>())).Returns(_sqlMetadataProvider.Object);

// Exception is thrown since sourceFields and targetFields do not match in either their existence,
// or their length.
DataApiBuilderException ex = Assert.ThrowsException<DataApiBuilderException>(() =>
configValidator.ValidateRelationshipsInConfig(runtimeConfig, _metadataProviderFactory.Object));
configValidator.ValidateRelationshipConfigCorrectness(runtimeConfig));
Assert.AreEqual(expectedExceptionMessage, ex.Message);
Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode);
Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ConfigValidationError, ex.SubStatusCode);
Expand Down Expand Up @@ -672,7 +659,7 @@ public void TestRelationshipWithoutSourceAndTargetFieldsAsValidBackingColumns(

// Exception is thrown since either source or target field does not exist as a valid backing column in their respective entity.
DataApiBuilderException ex = Assert.ThrowsException<DataApiBuilderException>(() =>
configValidator.ValidateRelationshipsInConfig(runtimeConfig, _metadataProviderFactory.Object));
configValidator.ValidateRelationships(runtimeConfig, _metadataProviderFactory.Object));
Assert.AreEqual(expectedExceptionMessage, ex.Message);
Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode);
Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ConfigValidationError, ex.SubStatusCode);
Expand Down Expand Up @@ -776,7 +763,6 @@ public void TestRelationshipWithoutLinkingSourceAndTargetFieldsMatching(
FileSystemRuntimeConfigLoader loader = new(fileSystem);
RuntimeConfigProvider provider = new(loader) { IsLateConfigured = true };
RuntimeConfigValidator configValidator = new(provider, fileSystem, new Mock<ILogger<RuntimeConfigValidator>>().Object);
Mock<ISqlMetadataProvider> _sqlMetadataProvider = new();

Dictionary<string, DatabaseObject> mockDictionaryForEntityDatabaseObject = new()
{
Expand All @@ -791,19 +777,10 @@ public void TestRelationshipWithoutLinkingSourceAndTargetFieldsMatching(
}
};

_sqlMetadataProvider.Setup<Dictionary<string, DatabaseObject>>(x =>
x.EntityToDatabaseObject).Returns(mockDictionaryForEntityDatabaseObject);

string discard;
_sqlMetadataProvider.Setup(x => x.TryGetExposedColumnName(It.IsAny<string>(), It.IsAny<string>(), out discard)).Returns(true);

Mock<IMetadataProviderFactory> _metadataProviderFactory = new();
_metadataProviderFactory.Setup(x => x.GetMetadataProvider(It.IsAny<string>())).Returns(_sqlMetadataProvider.Object);

// Exception is thrown since linkingSourceFields and linkingTargetFields do not match in either their existence,
// or their length.
DataApiBuilderException ex = Assert.ThrowsException<DataApiBuilderException>(() =>
configValidator.ValidateRelationshipsInConfig(runtimeConfig, _metadataProviderFactory.Object));
configValidator.ValidateRelationshipConfigCorrectness(runtimeConfig));
Assert.AreEqual(expectedExceptionMessage, ex.Message);
Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode);
Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ConfigValidationError, ex.SubStatusCode);
Expand Down
3 changes: 2 additions & 1 deletion src/Service/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,8 @@ private async Task<bool> PerformOnConfigChangeAsync(IApplicationBuilder app)
if (runtimeConfig.IsDevelopmentMode())
{
// Running only in developer mode to ensure fast and smooth startup in production.
runtimeConfigValidator.ValidateRelationshipsInConfig(runtimeConfig, sqlMetadataProviderFactory!);
runtimeConfigValidator.ValidateRelationshipConfigCorrectness(runtimeConfig);
runtimeConfigValidator.ValidateRelationships(runtimeConfig, sqlMetadataProviderFactory!);
}

// OpenAPI document creation is only attempted for REST supporting database types.
Expand Down