diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 0614e7688f..9728c27ee7 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -74,7 +74,7 @@ public class ConfigurationTests private const string BROWSER_ACCEPT_HEADER = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"; private const int RETRY_COUNT = 5; - private const int RETRY_WAIT_SECONDS = 1; + private const int RETRY_WAIT_SECONDS = 2; /// /// @@ -1128,7 +1128,7 @@ public async Task TestSqlSettingPostStartupConfigurations(string configurationEn await httpClient.GetAsync($"/{OPENAPI_SWAGGER_ENDPOINT}"); Assert.AreEqual(HttpStatusCode.ServiceUnavailable, preConfigOpenApiSwaggerEndpointAvailability.StatusCode); - HttpStatusCode responseCode = await HydratePostStartupConfiguration(httpClient, content, configurationEndpoint); + HttpStatusCode responseCode = await HydratePostStartupConfiguration(httpClient, content, configurationEndpoint, configuration.Runtime.Rest); // When the authorization resolver is properly configured, authorization will have failed // because no auth headers are present. @@ -2538,26 +2538,37 @@ public async Task TestRuntimeBaseRouteInNextLinkForPaginatedRestResponse() /// Expected HTTP status code code for the GraphQL request [DataTestMethod] [TestCategory(TestCategory.MSSQL)] - [DataRow(true, true, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Both Rest and GraphQL endpoints enabled globally")] - [DataRow(true, false, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest enabled and GraphQL endpoints disabled globally")] - [DataRow(false, true, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest disabled and GraphQL endpoints enabled globally")] - [DataRow(true, true, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Both Rest and GraphQL endpoints enabled globally")] - [DataRow(true, false, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest enabled and GraphQL endpoints disabled globally")] - [DataRow(false, true, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest disabled and GraphQL endpoints enabled globally")] - public async Task TestGlobalFlagToEnableRestAndGraphQLForHostedAndNonHostedEnvironment( + [DataRow(true, true, true, HttpStatusCode.OK, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest, GraphQL, and MCP enabled globally")] + [DataRow(true, true, false, HttpStatusCode.OK, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest and GraphQL enabled, MCP disabled globally")] + [DataRow(true, false, true, HttpStatusCode.OK, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest enabled, GraphQL disabled, and MCP enabled globally")] + [DataRow(true, false, false, HttpStatusCode.OK, HttpStatusCode.NotFound, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest enabled, GraphQL and MCP disabled globally")] + [DataRow(false, true, true, HttpStatusCode.NotFound, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest disabled, GraphQL and MCP enabled globally")] + [DataRow(false, true, false, HttpStatusCode.NotFound, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest disabled, GraphQL enabled, and MCP disabled globally")] + [DataRow(false, false, true, HttpStatusCode.NotFound, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest and GraphQL disabled, MCP enabled globally")] + [DataRow(true, true, true, HttpStatusCode.OK, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest, GraphQL, and MCP enabled globally")] + [DataRow(true, true, false, HttpStatusCode.OK, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest and GraphQL enabled, MCP disabled globally")] + [DataRow(true, false, true, HttpStatusCode.OK, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest enabled, GraphQL disabled, and MCP enabled globally")] + [DataRow(true, false, false, HttpStatusCode.OK, HttpStatusCode.NotFound, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest enabled, GraphQL and MCP disabled globally")] + [DataRow(false, true, true, HttpStatusCode.NotFound, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest disabled, GraphQL and MCP enabled globally")] + [DataRow(false, true, false, HttpStatusCode.NotFound, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest disabled, GraphQL enabled, and MCP disabled globally")] + [DataRow(false, false, true, HttpStatusCode.NotFound, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest and GraphQL disabled, MCP enabled globally")] + public async Task TestGlobalFlagToEnableRestGraphQLAndMcpForHostedAndNonHostedEnvironment( bool isRestEnabled, bool isGraphQLEnabled, + bool isMcpEnabled, HttpStatusCode expectedStatusCodeForREST, HttpStatusCode expectedStatusCodeForGraphQL, + HttpStatusCode expectedStatusCodeForMcp, string configurationEndpoint) { GraphQLRuntimeOptions graphqlOptions = new(Enabled: isGraphQLEnabled); RestRuntimeOptions restRuntimeOptions = new(Enabled: isRestEnabled); + McpRuntimeOptions mcpRuntimeOptions = new(Enabled: isMcpEnabled); DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, null); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, mcpRuntimeOptions); const string CUSTOM_CONFIG = "custom-config.json"; File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); @@ -2580,17 +2591,23 @@ public async Task TestGlobalFlagToEnableRestAndGraphQLForHostedAndNonHostedEnvir object payload = new { query }; - HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql") + // GraphQL request + HttpRequestMessage graphQLRequest = new(HttpMethod.Post, configuration.Runtime.GraphQL.Path) { Content = JsonContent.Create(payload) }; HttpResponseMessage graphQLResponse = await client.SendAsync(graphQLRequest); - Assert.AreEqual(expectedStatusCodeForGraphQL, graphQLResponse.StatusCode); + Assert.AreEqual(expectedStatusCodeForGraphQL, graphQLResponse.StatusCode, "The GraphQL response is different from the expected result."); - HttpRequestMessage restRequest = new(HttpMethod.Get, "/api/Book"); + // REST request + HttpRequestMessage restRequest = new(HttpMethod.Get, $"{configuration.Runtime.Rest.Path}/Book"); HttpResponseMessage restResponse = await client.SendAsync(restRequest); - Assert.AreEqual(expectedStatusCodeForREST, restResponse.StatusCode); + Assert.AreEqual(expectedStatusCodeForREST, restResponse.StatusCode, "The REST response is different from the expected result."); + + // MCP request + HttpStatusCode mcpResponseCode = await GetMcpResponse(client, configuration.Runtime.Mcp); + Assert.AreEqual(expectedStatusCodeForMcp, mcpResponseCode, "The MCP response is different from the expected result."); } // Hosted Scenario @@ -2600,18 +2617,19 @@ public async Task TestGlobalFlagToEnableRestAndGraphQLForHostedAndNonHostedEnvir { JsonContent content = GetPostStartupConfigParams(MSSQL_ENVIRONMENT, configuration, configurationEndpoint); - HttpResponseMessage postResult = - await client.PostAsync(configurationEndpoint, content); - Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); - - HttpStatusCode restResponseCode = await GetRestResponsePostConfigHydration(client); - - Assert.AreEqual(expected: expectedStatusCodeForREST, actual: restResponseCode); + HttpResponseMessage postResult = await client.PostAsync(configurationEndpoint, content); + Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode, "The hydration post-response is different from the expected result."); - HttpStatusCode graphqlResponseCode = await GetGraphQLResponsePostConfigHydration(client); + HttpStatusCode restResponseCode = await GetRestResponsePostConfigHydration(client, configuration.Runtime.Rest); + Assert.AreEqual(expected: expectedStatusCodeForREST, actual: restResponseCode, "The REST hydration post-response is different from the expected result."); - Assert.AreEqual(expected: expectedStatusCodeForGraphQL, actual: graphqlResponseCode); + HttpStatusCode graphqlResponseCode = await GetGraphQLResponsePostConfigHydration(client, configuration.Runtime.GraphQL); + Assert.AreEqual(expected: expectedStatusCodeForGraphQL, actual: graphqlResponseCode, "The GraphQL hydration post-response is different from the expected result."); + // TODO: Issue #3012 - Currently DAB is unable to start MCP with the hydration post-response. + // This needs to be fixed before uncommenting the MCP check + // HttpStatusCode mcpResponseCode = await GetMcpResponse(client, configuration.Runtime.Mcp); + // Assert.AreEqual(expected: expectedStatusCodeForMcp, actual: mcpResponseCode, "The MCP hydration post-response is different from the expected result."); } } @@ -3661,7 +3679,7 @@ public async Task TestSchemaIntrospectionQuery(bool enableIntrospection, bool ex using (HttpClient client = server.CreateClient()) { JsonContent content = GetPostStartupConfigParams(MSSQL_ENVIRONMENT, configuration, configurationEndpoint); - HttpStatusCode responseCode = await HydratePostStartupConfiguration(client, content, configurationEndpoint); + HttpStatusCode responseCode = await HydratePostStartupConfiguration(client, content, configurationEndpoint, configuration.Runtime.Rest); Assert.AreEqual(expected: HttpStatusCode.OK, actual: responseCode, message: "Configuration hydration failed."); @@ -5256,41 +5274,48 @@ private static JsonContent GetPostStartupConfigParams(string environment, Runtim /// by executing HTTP requests against the engine until a non-503 error is received. /// /// Client used for request execution. - /// Post-startup configuration + /// New config file content that will be added to DAB. + /// Endpoint through which content will be sent to DAB." + /// Global settings used at runtime for REST APIs. /// ServiceUnavailable if service is not successfully hydrated with config - private static async Task HydratePostStartupConfiguration(HttpClient httpClient, JsonContent content, string configurationEndpoint) + private static async Task HydratePostStartupConfiguration(HttpClient httpClient, JsonContent content, string configurationEndpoint, RestRuntimeOptions rest) { // Hydrate configuration post-startup HttpResponseMessage postResult = await httpClient.PostAsync(configurationEndpoint, content); Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); - return await GetRestResponsePostConfigHydration(httpClient); + return await GetRestResponsePostConfigHydration(httpClient, rest); } /// /// Executing REST requests against the engine until a non-503 error is received. /// /// Client used for request execution. + /// Global settings used at runtime for REST APIs. /// ServiceUnavailable if service is not successfully hydrated with config, /// else the response code from the REST request - private static async Task GetRestResponsePostConfigHydration(HttpClient httpClient) + private static async Task GetRestResponsePostConfigHydration(HttpClient httpClient, RestRuntimeOptions rest) { - // Retry request RETRY_COUNT times in 1 second increments to allow required services - // time to instantiate and hydrate permissions. - int retryCount = RETRY_COUNT; + // Retry request RETRY_COUNT times in exponential increments to allow + // required services time to instantiate and hydrate permissions because + // the DAB services may take an unpredictable amount of time to become ready. + // + // The service might still fail due to the service not being available yet, + // but it is highly unlikely to be the case. + int retryCount = 0; HttpStatusCode responseCode = HttpStatusCode.ServiceUnavailable; - while (retryCount > 0) + while (retryCount < RETRY_COUNT) { // Spot test authorization resolver utilization to ensure configuration is used. HttpResponseMessage postConfigHydrationResult = - await httpClient.GetAsync($"api/{POST_STARTUP_CONFIG_ENTITY}"); + await httpClient.GetAsync($"{rest.Path}/{POST_STARTUP_CONFIG_ENTITY}"); responseCode = postConfigHydrationResult.StatusCode; if (postConfigHydrationResult.StatusCode == HttpStatusCode.ServiceUnavailable) { - retryCount--; - Thread.Sleep(TimeSpan.FromSeconds(RETRY_WAIT_SECONDS)); + retryCount++; + await Task.Delay(TimeSpan.FromSeconds(Math.Pow(RETRY_WAIT_SECONDS, retryCount))); continue; } @@ -5306,13 +5331,17 @@ private static async Task GetRestResponsePostConfigHydration(Htt /// Client used for request execution. /// ServiceUnavailable if service is not successfully hydrated with config, /// else the response code from the GRAPHQL request - private static async Task GetGraphQLResponsePostConfigHydration(HttpClient httpClient) + private static async Task GetGraphQLResponsePostConfigHydration(HttpClient httpClient, GraphQLRuntimeOptions graphQL) { - // Retry request RETRY_COUNT times in 1 second increments to allow required services - // time to instantiate and hydrate permissions. - int retryCount = RETRY_COUNT; + // Retry request RETRY_COUNT times in exponential increments to allow + // required services time to instantiate and hydrate permissions because + // the DAB services may take an unpredictable amount of time to become ready. + // + // The service might still fail due to the service not being available yet, + // but it is highly unlikely to be the case. + int retryCount = 0; HttpStatusCode responseCode = HttpStatusCode.ServiceUnavailable; - while (retryCount > 0) + while (retryCount < RETRY_COUNT) { string query = @"{ book_by_pk(id: 1) { @@ -5324,7 +5353,7 @@ private static async Task GetGraphQLResponsePostConfigHydration( object payload = new { query }; - HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql") + HttpRequestMessage graphQLRequest = new(HttpMethod.Post, graphQL.Path) { Content = JsonContent.Create(payload) }; @@ -5334,8 +5363,55 @@ private static async Task GetGraphQLResponsePostConfigHydration( if (responseCode == HttpStatusCode.ServiceUnavailable) { - retryCount--; - Thread.Sleep(TimeSpan.FromSeconds(RETRY_WAIT_SECONDS)); + retryCount++; + await Task.Delay(TimeSpan.FromSeconds(Math.Pow(RETRY_WAIT_SECONDS, retryCount))); + continue; + } + + break; + } + + return responseCode; + } + + /// + /// Executing MCP POST requests against the engine until a non-503 error is received. + /// + /// Client used for request execution. + /// ServiceUnavailable if service is not successfully hydrated with config, + /// else the response code from the MCP request + public static async Task GetMcpResponse(HttpClient httpClient, McpRuntimeOptions mcp) + { + // Retry request RETRY_COUNT times in exponential increments to allow + // required services time to instantiate and hydrate permissions because + // the DAB services may take an unpredictable amount of time to become ready. + // + // The service might still fail due to the service not being available yet, + // but it is highly unlikely to be the case. + int retryCount = 0; + HttpStatusCode responseCode = HttpStatusCode.ServiceUnavailable; + while (retryCount < RETRY_COUNT) + { + // Minimal MCP request (list tools) – valid JSON-RPC request + object payload = new + { + jsonrpc = "2.0", + id = 1, + method = "tools/list" + }; + HttpRequestMessage mcpRequest = new(HttpMethod.Post, mcp.Path) + { + Content = JsonContent.Create(payload) + }; + mcpRequest.Headers.Add("Accept", "*/*"); + + HttpResponseMessage mcpResponse = await httpClient.SendAsync(mcpRequest); + responseCode = mcpResponse.StatusCode; + + if (responseCode == HttpStatusCode.ServiceUnavailable || responseCode == HttpStatusCode.NotFound) + { + retryCount++; + await Task.Delay(TimeSpan.FromSeconds(Math.Pow(RETRY_WAIT_SECONDS, retryCount))); continue; }