@@ -1298,6 +1298,73 @@ public async Task TestConfigIsValid()
12981298 }
12991299 }
13001300
1301+ /// <summary>
1302+ /// Test to verify that provided invalid value of depth-limit in the config file should
1303+ /// result in validation failure during `dab validate` and `dab start`.
1304+ /// </summary>
1305+ [ DataTestMethod ]
1306+ [ DataRow ( 0 , DisplayName = "[FAIL]: Invalid Value: 0 for depth-limit." ) ]
1307+ [ DataRow ( - 2 , DisplayName = "[FAIL]: Invalid Value: -2 for depth-limit." ) ]
1308+ [ TestCategory ( TestCategory . MSSQL ) ]
1309+ public async Task TestValidateConfigForInvalidDepthLimit ( int ? depthLimit )
1310+ {
1311+ await ValidateConfigWithDepthLimit ( depthLimit , expectedSuccess : false ) ;
1312+ }
1313+
1314+ /// <summary>
1315+ /// Test to verify that provided valid value of depth-limit in the config file should not
1316+ /// result in any validation failure during `dab validate` and `dab start`.
1317+ /// -1 and null are special values.
1318+ /// -1 can be set to remove the depth limit, while `null` is the default value which means no depth limit check.
1319+ /// </summary>
1320+ [ DataTestMethod ]
1321+ [ DataRow ( - 1 , DisplayName = "[PASS]: Valid Value: -1 to disable depth limit" ) ]
1322+ [ DataRow ( 2 , DisplayName = "[PASS]: Valid Value: 2 for depth-limit." ) ]
1323+ [ DataRow ( 2147483647 , DisplayName = "[PASS]: Valid Value: Using Int32.MaxValue(2147483647) for depth-limit." ) ]
1324+ [ DataRow ( null , DisplayName = "[PASS]: Default Value: null for depth-limit." ) ]
1325+ [ TestCategory ( TestCategory . MSSQL ) ]
1326+ public async Task TestValidateConfigForValidDepthLimit ( int ? depthLimit )
1327+ {
1328+ await ValidateConfigWithDepthLimit ( depthLimit , expectedSuccess : true ) ;
1329+ }
1330+
1331+ /// <summary>
1332+ /// This method validates that depth-limit outside the valid range should fail validation
1333+ /// during `dab validate` and `dab start`.
1334+ /// </summary>
1335+ /// <param name="depthLimit"></param>
1336+ /// <param name="expectedSuccess"></param>
1337+ private static async Task ValidateConfigWithDepthLimit ( int ? depthLimit , bool expectedSuccess )
1338+ {
1339+ // Arrange: Common setup logic
1340+ TestHelper . SetupDatabaseEnvironment ( MSSQL_ENVIRONMENT ) ;
1341+ const string CUSTOM_CONFIG = "custom-config.json" ;
1342+ FileSystemRuntimeConfigLoader testConfigPath = TestHelper . GetRuntimeConfigLoader ( ) ;
1343+ RuntimeConfig configuration = TestHelper . GetRuntimeConfigProvider ( testConfigPath ) . GetConfig ( ) ;
1344+ configuration = configuration with
1345+ {
1346+ Runtime = configuration . Runtime with
1347+ {
1348+ GraphQL = configuration . Runtime . GraphQL with { DepthLimit = depthLimit , UserProvidedDepthLimit = true }
1349+ }
1350+ } ;
1351+
1352+ MockFileSystem fileSystem = new ( ) ;
1353+ fileSystem . AddFile ( CUSTOM_CONFIG , new MockFileData ( configuration . ToJson ( ) ) ) ;
1354+ FileSystemRuntimeConfigLoader configLoader = new ( fileSystem ) ;
1355+ configLoader . UpdateConfigFilePath ( CUSTOM_CONFIG ) ;
1356+ RuntimeConfigProvider configProvider = TestHelper . GetRuntimeConfigProvider ( configLoader ) ;
1357+
1358+ Mock < ILogger < RuntimeConfigValidator > > configValidatorLogger = new ( ) ;
1359+ RuntimeConfigValidator configValidator = new ( configProvider , fileSystem , configValidatorLogger . Object , true ) ;
1360+
1361+ // Act
1362+ bool isSuccess = await configValidator . TryValidateConfig ( CUSTOM_CONFIG , TestHelper . ProvisionLoggerFactory ( ) ) ;
1363+
1364+ // Assert based on expected success
1365+ Assert . AreEqual ( expectedSuccess , isSuccess ) ;
1366+ }
1367+
13011368 /// <summary>
13021369 /// This test method checks a valid config's entities against
13031370 /// the database and ensures they are valid.
@@ -3698,6 +3765,273 @@ public async Task ValidateNextLinkUsage()
36983765 }
36993766 }
37003767
3768+ /// <summary>
3769+ /// Tests the enforcement of depth limit restrictions on GraphQL queries and mutations in non-hosted mode.
3770+ /// Verifies that requests exceeding the specified depth limit result in a BadRequest,
3771+ /// while requests within the limit succeed with the expected status code.
3772+ /// Also verifies that the error message contains the current and allowed max depth limit value.
3773+ /// Example:
3774+ /// Query:
3775+ /// query book_by_pk{
3776+ /// book_by_pk(id: 1) { // depth: 1
3777+ /// id, // depth: 2
3778+ /// title, // depth: 2
3779+ /// publisher_id // depth: 2
3780+ /// }
3781+ /// }
3782+ /// Mutation:
3783+ /// mutation createbook {
3784+ /// createbook(item: { title: ""Book #1"", publisher_id: 1234 }) { // depth: 1
3785+ /// title, // depth: 2
3786+ /// publisher_id // depth: 2
3787+ /// }
3788+ /// </summary>
3789+ /// <param name="depthLimit">The maximum allowed depth for GraphQL queries and mutations.</param>
3790+ /// <param name="operationType">Indicates whether the operation is a mutation or a query.</param>
3791+ /// <param name="expectedStatusCodeForGraphQL">The expected HTTP status code for the operation.</param>
3792+ [ DataTestMethod ]
3793+ [ DataRow ( 1 , GraphQLOperation . Query , HttpStatusCode . BadRequest , DisplayName = "Failed Query execution when max depth limit is set to 1" ) ]
3794+ [ DataRow ( 2 , GraphQLOperation . Query , HttpStatusCode . OK , DisplayName = "Query execution successful when max depth limit is set to 2" ) ]
3795+ [ DataRow ( 1 , GraphQLOperation . Mutation , HttpStatusCode . BadRequest , DisplayName = "Failed Mutation execution when max depth limit is set to 1" ) ]
3796+ [ DataRow ( 2 , GraphQLOperation . Mutation , HttpStatusCode . OK , DisplayName = "Mutation execution successful when max depth limit is set to 2" ) ]
3797+ [ TestCategory ( TestCategory . MSSQL ) ]
3798+ public async Task TestDepthLimitRestrictionOnGraphQLInNonHostedMode (
3799+ int depthLimit ,
3800+ GraphQLOperation operationType ,
3801+ HttpStatusCode expectedStatusCodeForGraphQL )
3802+ {
3803+ // Arrange
3804+ GraphQLRuntimeOptions graphqlOptions = new ( DepthLimit : depthLimit ) ;
3805+ graphqlOptions = graphqlOptions with { UserProvidedDepthLimit = true } ;
3806+
3807+ DataSource dataSource = new ( DatabaseType . MSSQL ,
3808+ GetConnectionStringFromEnvironmentConfig ( environment : TestCategory . MSSQL ) , Options : null ) ;
3809+
3810+ RuntimeConfig configuration = InitMinimalRuntimeConfig ( dataSource , graphqlOptions , restOptions : new ( ) ) ;
3811+ const string CUSTOM_CONFIG = "custom-config.json" ;
3812+ File . WriteAllText ( CUSTOM_CONFIG , configuration . ToJson ( ) ) ;
3813+
3814+ string [ ] args = new [ ]
3815+ {
3816+ $ "--ConfigFileName={ CUSTOM_CONFIG } "
3817+ } ;
3818+
3819+ using ( TestServer server = new ( Program . CreateWebHostBuilder ( args ) ) )
3820+ using ( HttpClient client = server . CreateClient ( ) )
3821+ {
3822+ string query ;
3823+ if ( operationType is GraphQLOperation . Mutation )
3824+ {
3825+ // requested mutation operation has depth of 2
3826+ query = @"mutation createbook{
3827+ createbook(item: { title: ""Book #1"", publisher_id: 1234 }) {
3828+ title
3829+ publisher_id
3830+ }
3831+ }" ;
3832+ }
3833+ else
3834+ {
3835+ // requested query operation has depth of 2
3836+ query = @"query book_by_pk{
3837+ book_by_pk(id: 1) {
3838+ id,
3839+ title,
3840+ publisher_id
3841+ }
3842+ }" ;
3843+ }
3844+
3845+ object payload = new { query } ;
3846+
3847+ HttpRequestMessage graphQLRequest = new ( HttpMethod . Post , "/graphql" )
3848+ {
3849+ Content = JsonContent . Create ( payload )
3850+ } ;
3851+
3852+ // Act
3853+ HttpResponseMessage graphQLResponse = await client . SendAsync ( graphQLRequest ) ;
3854+
3855+ // Assert
3856+ Assert . AreEqual ( expectedStatusCodeForGraphQL , graphQLResponse . StatusCode ) ;
3857+ string body = await graphQLResponse . Content . ReadAsStringAsync ( ) ;
3858+ JsonElement responseJson = JsonSerializer . Deserialize < JsonElement > ( body ) ;
3859+ if ( graphQLResponse . StatusCode == HttpStatusCode . OK )
3860+ {
3861+ Assert . IsTrue ( responseJson . TryGetProperty ( "data" , out JsonElement data ) , "The response should contain data." ) ;
3862+ Assert . IsFalse ( data . TryGetProperty ( "errors" , out _ ) , "The response should not contain any errors." ) ;
3863+ }
3864+ else
3865+ {
3866+ Assert . IsTrue ( responseJson . TryGetProperty ( "errors" , out JsonElement data ) , "The response should contain errors." ) ;
3867+ Assert . IsTrue ( data . EnumerateArray ( ) . Any ( ) , "The response should contain at least one error." ) ;
3868+ Assert . IsTrue ( data . EnumerateArray ( ) . FirstOrDefault ( ) . TryGetProperty ( "message" , out JsonElement message ) , "The error should contain a message." ) ;
3869+ string errorMessage = message . GetString ( ) ;
3870+ string expectedErrorMessage = $ "The GraphQL document has an execution depth of 2 which exceeds the max allowed execution depth of { depthLimit } .";
3871+ Assert . AreEqual ( expectedErrorMessage , errorMessage , "The error message should contain the current and allowed max depth limit value." ) ;
3872+ }
3873+ }
3874+ }
3875+
3876+ /// <summary>
3877+ /// This test verifies that the depth-limit specified for GraphQL does not affect introspection queries.
3878+ /// In this test, we have specified the depth limit as 2 and we are sending introspection query with depth 6.
3879+ /// The expected result is that the query should be successful and should not return any errors.
3880+ /// Example:
3881+ /// {
3882+ /// __schema { // depth: 1
3883+ /// types { // depth: 2
3884+ /// name // depth: 3
3885+ /// fields { // depth: 3
3886+ /// name // depth: 4
3887+ /// type { // depth: 4
3888+ /// name // depth: 5
3889+ /// kind // depth: 5
3890+ /// ofType { // depth: 5
3891+ /// name // depth: 6
3892+ /// kind // depth: 6
3893+ /// }
3894+ /// }
3895+ /// }
3896+ /// }
3897+ /// </summary>
3898+ [ TestCategory ( TestCategory . MSSQL ) ]
3899+ [ TestMethod ]
3900+ public async Task TestGraphQLIntrospectionQueriesAreNotImpactedByDepthLimit ( )
3901+ {
3902+ // Arrange
3903+ GraphQLRuntimeOptions graphqlOptions = new ( DepthLimit : 2 ) ;
3904+ graphqlOptions = graphqlOptions with { UserProvidedDepthLimit = true } ;
3905+
3906+ DataSource dataSource = new ( DatabaseType . MSSQL ,
3907+ GetConnectionStringFromEnvironmentConfig ( environment : TestCategory . MSSQL ) , Options : null ) ;
3908+
3909+ RuntimeConfig configuration = InitMinimalRuntimeConfig ( dataSource , graphqlOptions , restOptions : new ( ) ) ;
3910+ const string CUSTOM_CONFIG = "custom-config.json" ;
3911+ File . WriteAllText ( CUSTOM_CONFIG , configuration . ToJson ( ) ) ;
3912+
3913+ string [ ] args = new [ ]
3914+ {
3915+ $ "--ConfigFileName={ CUSTOM_CONFIG } "
3916+ } ;
3917+
3918+ using ( TestServer server = new ( Program . CreateWebHostBuilder ( args ) ) )
3919+ using ( HttpClient client = server . CreateClient ( ) )
3920+ {
3921+ // nested depth:6
3922+ string query = @"{
3923+ __schema {
3924+ types {
3925+ name
3926+ fields {
3927+ name
3928+ type {
3929+ name
3930+ kind
3931+ ofType {
3932+ name
3933+ kind
3934+ }
3935+ }
3936+ }
3937+ }
3938+ }
3939+ }" ;
3940+
3941+ object payload = new { query } ;
3942+
3943+ HttpRequestMessage graphQLRequest = new ( HttpMethod . Post , "/graphql" )
3944+ {
3945+ Content = JsonContent . Create ( payload )
3946+ } ;
3947+
3948+ // Act
3949+ HttpResponseMessage graphQLResponse = await client . SendAsync ( graphQLRequest ) ;
3950+
3951+ // Assert
3952+ Assert . AreEqual ( HttpStatusCode . OK , graphQLResponse . StatusCode ) ;
3953+ string body = await graphQLResponse . Content . ReadAsStringAsync ( ) ;
3954+
3955+ JsonElement responseJson = JsonSerializer . Deserialize < JsonElement > ( body ) ;
3956+ Assert . IsNotNull ( responseJson , "The response should be a valid JSON." ) ;
3957+ Assert . IsTrue ( responseJson . TryGetProperty ( "data" , out JsonElement data ) , "The response should contain data." ) ;
3958+ Assert . IsFalse ( data . TryGetProperty ( "errors" , out _ ) , "The response should not contain any errors." ) ;
3959+ Assert . IsTrue ( responseJson . GetProperty ( "data" ) . TryGetProperty ( "__schema" , out JsonElement schema ) ) ;
3960+ Assert . IsNotNull ( schema , "The response should contain schema information." ) ;
3961+ }
3962+ }
3963+
3964+ /// <summary>
3965+ /// Tests the behavior of GraphQL queries in non-hosted mode when the depth limit is explicitly set to -1 or null.
3966+ /// Setting the depth limit to -1 is intended to disable the depth limit check, allowing queries of any depth.
3967+ /// Using null as default value of dab which also disables the depth limit check.
3968+ /// This test verifies that queries are processed successfully without any errors under these configurations.
3969+ /// Example Query:
3970+ /// {
3971+ /// book_by_pk(id: 1) { // depth: 1
3972+ /// id, // depth: 2
3973+ /// title, // depth: 2
3974+ /// publisher_id // depth: 2
3975+ /// }
3976+ /// }
3977+ /// </summary>
3978+ /// <param name="depthLimit"> </param>
3979+ [ DataTestMethod ]
3980+ [ DataRow ( - 1 , DisplayName = "Setting -1 for depth-limit will disable the depth limit" ) ]
3981+ [ DataRow ( null , DisplayName = "Using default value: null for depth-limit which also disables the depth limit check" ) ]
3982+ [ TestCategory ( TestCategory . MSSQL ) ]
3983+ public async Task TestNoDepthLimitOnGrahQLInNonHostedMode ( int ? depthLimit )
3984+ {
3985+ // Arrange
3986+ GraphQLRuntimeOptions graphqlOptions = new ( DepthLimit : depthLimit ) ;
3987+ graphqlOptions = graphqlOptions with { UserProvidedDepthLimit = true } ;
3988+
3989+ DataSource dataSource = new ( DatabaseType . MSSQL ,
3990+ GetConnectionStringFromEnvironmentConfig ( environment : TestCategory . MSSQL ) , Options : null ) ;
3991+
3992+ RuntimeConfig configuration = InitMinimalRuntimeConfig ( dataSource , graphqlOptions , restOptions : new ( ) ) ;
3993+ const string CUSTOM_CONFIG = "custom-config.json" ;
3994+ File . WriteAllText ( CUSTOM_CONFIG , configuration . ToJson ( ) ) ;
3995+
3996+ string [ ] args = new [ ]
3997+ {
3998+ $ "--ConfigFileName={ CUSTOM_CONFIG } "
3999+ } ;
4000+
4001+ using ( TestServer server = new ( Program . CreateWebHostBuilder ( args ) ) )
4002+ using ( HttpClient client = server . CreateClient ( ) )
4003+ {
4004+ // requested query operation has depth of 2
4005+ string query = @"{
4006+ book_by_pk(id: 1) {
4007+ id,
4008+ title,
4009+ publisher_id
4010+ }
4011+ }" ;
4012+
4013+ object payload = new { query } ;
4014+
4015+ HttpRequestMessage graphQLRequest = new ( HttpMethod . Post , "/graphql" )
4016+ {
4017+ Content = JsonContent . Create ( payload )
4018+ } ;
4019+
4020+ // Act
4021+ HttpResponseMessage graphQLResponse = await client . SendAsync ( graphQLRequest ) ;
4022+
4023+ // Assert
4024+ Assert . AreEqual ( HttpStatusCode . OK , graphQLResponse . StatusCode ) ;
4025+ string body = await graphQLResponse . Content . ReadAsStringAsync ( ) ;
4026+
4027+ JsonElement responseJson = JsonSerializer . Deserialize < JsonElement > ( body ) ;
4028+ Assert . IsNotNull ( responseJson , "The response should be a valid JSON." ) ;
4029+ Assert . IsTrue ( responseJson . TryGetProperty ( "data" , out JsonElement data ) , "The response should contain data." ) ;
4030+ Assert . IsFalse ( data . TryGetProperty ( "errors" , out _ ) , "The response should not contain any errors." ) ;
4031+ Assert . IsTrue ( data . TryGetProperty ( "book_by_pk" , out _ ) , "The response data should contain book_by_pk data." ) ;
4032+ }
4033+ }
4034+
37014035 /// <summary>
37024036 /// Helper function to write custom configuration file. with minimal REST/GraphQL global settings
37034037 /// using the supplied entities.
0 commit comments