From d56f0859859aa1c47bf9d8bcf4fe89d3e120a308 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Tue, 19 May 2026 13:26:48 +0530 Subject: [PATCH 1/6] ExecuteEntityTool: DB metadata based parameter support --- .../BuiltInTools/ExecuteEntityTool.cs | 23 +- .../ExecuteEntityToolMsSqlIntegrationTests.cs | 191 ++++++++ .../Mcp/ExecuteEntityToolTests.cs | 447 ++++++++++++++++++ 3 files changed, 650 insertions(+), 11 deletions(-) create mode 100644 src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs create mode 100644 src/Service.Tests/Mcp/ExecuteEntityToolTests.cs diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs index 8989680f9e..2ec5712eb2 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs @@ -162,13 +162,15 @@ public async Task ExecuteAsync( return McpErrorHelpers.PermissionDenied(toolName, entity, "execute", authError, logger); } - // 7) Validate parameters against metadata - if (parameters != null && entityConfig.Source.Parameters != null) + // 7) Validate parameters against DB metadata (StoredProcedureDefinition.Parameters), + // which is the source of truth for parameter names. The upstream merge performed by + // FillSchemaForStoredProcedureAsync ensures this dictionary contains all valid parameters. + StoredProcedureDefinition spDefinition = ((DatabaseStoredProcedure)dbObject).StoredProcedureDefinition; + if (parameters != null) { - // Validate all provided parameters exist in metadata foreach (KeyValuePair param in parameters) { - if (!entityConfig.Source.Parameters.Any(p => p.Name == param.Key)) + if (!spDefinition.Parameters.ContainsKey(param.Key)) { return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", $"Invalid parameter: {param.Key}", logger); } @@ -201,15 +203,14 @@ public async Task ExecuteAsync( } } - // Then, add default parameters from configuration (only if not already provided by user) - if ((parameters == null || parameters.Count == 0) && entityConfig.Source.Parameters != null) + // Apply config-declared defaults from the merged ParameterDefinitions. + // This covers all parameters (including DB-discovered ones with config defaults) + // and applies them when the user didn't supply a value. + foreach ((string paramName, ParameterDefinition paramDef) in spDefinition.Parameters) { - foreach (ParameterMetadata param in entityConfig.Source.Parameters) + if (!context.FieldValuePairsInBody.ContainsKey(paramName) && paramDef.HasConfigDefault) { - if (!context.FieldValuePairsInBody.ContainsKey(param.Name)) - { - context.FieldValuePairsInBody[param.Name] = param.Default; - } + context.FieldValuePairsInBody[paramName] = paramDef.ConfigDefaultValue; } } diff --git a/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs b/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs new file mode 100644 index 0000000000..dbbb75281f --- /dev/null +++ b/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core.Authorization; +using Azure.DataApiBuilder.Core.Configurations; +using Azure.DataApiBuilder.Core.Resolvers; +using Azure.DataApiBuilder.Core.Resolvers.Factories; +using Azure.DataApiBuilder.Core.Services.Cache; +using Azure.DataApiBuilder.Mcp.BuiltInTools; +using Azure.DataApiBuilder.Service.Tests.SqlTests; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using ModelContextProtocol.Protocol; +using Moq; +using ZiggyCreatures.Caching.Fusion; + +namespace Azure.DataApiBuilder.Service.Tests.Mcp +{ + /// + /// Integration tests for ExecuteEntityTool's parameter validation and default application. + /// Verifies the end-to-end behavior after the fix: + /// - Parameters are validated against StoredProcedureDefinition.Parameters (DB metadata), + /// not config-side parameters alone. + /// - Config defaults are applied from ParameterDefinition.HasConfigDefault/ConfigDefaultValue + /// for any parameter the user did not supply. + /// + /// Scenarios (reuse SPs already defined in DatabaseSchema-MsSql.sql / dab-config.MsSql.json): + /// - GetBook -> SP get_book_by_id(@id int), no config params. + /// - InsertBook -> SP insert_book(@title, @publisher_id), config defaults applied. + /// - GetBooks -> SP get_books, zero params. + /// + [TestClass, TestCategory(TestCategory.MSSQL)] + public class ExecuteEntityToolMsSqlIntegrationTests : SqlTestBase + { + private static RuntimeConfig _baseConfig; + + [ClassInitialize] + public static async Task SetupAsync(TestContext context) + { + DatabaseEngine = TestCategory.MSSQL; + await InitializeTestFixture(); + _baseConfig = SqlTestHelper.SetupRuntimeConfig(); + } + + /// + /// Data-driven test validating successful SP execution across multiple parameter scenarios. + /// Each row exercises a distinct code path in ExecuteEntityTool: + /// - DB-discovered param with no config entry (validates the fix for param validation). + /// - Config defaults applied when user omits params. + /// - User-supplied params override config defaults. + /// - Zero-param SP succeeds with no parameters. + /// + [DataTestMethod] + [DataRow("GetBook", "{\"id\": 1}", DisplayName = "DB-discovered param accepted (no config entry)")] + [DataRow("InsertBook", null, DisplayName = "Config defaults applied when no params supplied")] + [DataRow("InsertBook", "{\"title\": \"Integration Test Book\", \"publisher_id\": 2345}", DisplayName = "User-supplied params override defaults")] + [DataRow("GetBooks", null, DisplayName = "Zero-param SP succeeds")] + public async Task ExecuteEntity_SuccessfulExecution(string entityName, string parametersJson) + { + Dictionary parameters = parametersJson != null + ? JsonSerializer.Deserialize>(parametersJson) + : null; + + CallToolResult result = await ExecuteEntityAsync(entityName, parameters); + + Assert.IsTrue(result.IsError == false || result.IsError == null, + $"execute_entity failed for entity '{entityName}' with params '{parametersJson}'. Content: {SerializeFirstContent(result)}"); + + string content = GetFirstTextContent(result); + Assert.IsFalse(string.IsNullOrWhiteSpace(content), $"Expected non-empty result for entity '{entityName}'."); + } + + /// + /// Reject a parameter name that does not exist in the DB metadata. + /// Validation against StoredProcedureDefinition.Parameters should catch this. + /// + [DataTestMethod] + [DataRow("GetBook", "nonexistent_param", "value", DisplayName = "Rejects unknown param on single-param SP")] + [DataRow("GetBooks", "bogus", "123", DisplayName = "Rejects any param on zero-param SP")] + public async Task ExecuteEntity_InvalidParamName_ReturnsError(string entityName, string paramName, string paramValue) + { + Dictionary parameters = new() { { paramName, paramValue } }; + CallToolResult result = await ExecuteEntityAsync(entityName, parameters); + + Assert.IsTrue(result.IsError == true, + $"execute_entity should reject parameter '{paramName}' not in DB metadata for '{entityName}'."); + string content = GetFirstTextContent(result); + StringAssert.Contains(content, paramName); + } + + private static async Task ExecuteEntityAsync(string entityName, Dictionary parameters) + { + IServiceProvider serviceProvider = BuildExecuteEntityServiceProvider(_baseConfig); + ExecuteEntityTool tool = new(); + + var args = new Dictionary { { "entity", entityName } }; + if (parameters != null) + { + args["parameters"] = parameters; + } + + string argsJson = JsonSerializer.Serialize(args); + using JsonDocument arguments = JsonDocument.Parse(argsJson); + + return await tool.ExecuteAsync(arguments, serviceProvider, CancellationToken.None); + } + + /// + /// Builds a service provider that wires ExecuteEntityTool to the shared fixture's + /// real ISqlMetadataProvider, real IQueryEngine (SqlQueryEngine), and real + /// authorization resolver, with a DefaultHttpContext carrying the anonymous role header. + /// + private static IServiceProvider BuildExecuteEntityServiceProvider(RuntimeConfig config) + { + ServiceCollection services = new(); + + // Real RuntimeConfigProvider populated from the provided config snapshot. + RuntimeConfigProvider configProvider = TestHelper.GenerateInMemoryRuntimeConfigProvider(config); + services.AddSingleton(configProvider); + + // Real metadata-provider factory backed by the shared fixture's live provider. + services.AddSingleton(_metadataProviderFactory.Object); + + // Real authorization resolver wired by SqlTestBase against the live config + provider. + services.AddSingleton(_authorizationResolver); + + // Real HttpContext carrying the anonymous role header. + DefaultHttpContext httpContext = new(); + httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER] = AuthorizationResolver.ROLE_ANONYMOUS; + IHttpContextAccessor httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext }; + services.AddSingleton(httpContextAccessor); + + // Build a real SqlQueryEngine using the shared fixtures. + Mock cache = new(); + DabCacheService cacheService = new(cache.Object, logger: null, httpContextAccessor); + + SqlQueryEngine queryEngine = new( + _queryManagerFactory.Object, + _metadataProviderFactory.Object, + httpContextAccessor, + _authorizationResolver, + _gqlFilterParser, + new Mock>().Object, + configProvider, + cacheService); + + // Wrap in a mock IQueryEngineFactory that returns the real engine. + Mock queryEngineFactory = new(); + queryEngineFactory + .Setup(f => f.GetQueryEngine(It.IsAny())) + .Returns(queryEngine); + services.AddSingleton(queryEngineFactory.Object); + + services.AddLogging(); + + return services.BuildServiceProvider(); + } + + private static string GetFirstTextContent(CallToolResult result) + { + if (result.Content is null || result.Content.Count == 0) + { + return string.Empty; + } + + return result.Content[0] is TextContentBlock textBlock + ? textBlock.Text ?? string.Empty + : string.Empty; + } + + private static string SerializeFirstContent(CallToolResult result) + { + if (result.Content is null || result.Content.Count == 0) + { + return ""; + } + + return result.Content[0] is TextContentBlock textBlock + ? textBlock.Text ?? string.Empty + : result.Content[0].GetType().Name; + } + } +} diff --git a/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs b/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs new file mode 100644 index 0000000000..0d55cd296b --- /dev/null +++ b/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs @@ -0,0 +1,447 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure.DataApiBuilder.Auth; +using Azure.DataApiBuilder.Config.DatabasePrimitives; +using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core.Authorization; +using Azure.DataApiBuilder.Core.Configurations; +using Azure.DataApiBuilder.Core.Models; +using Azure.DataApiBuilder.Core.Resolvers; +using Azure.DataApiBuilder.Core.Resolvers.Factories; +using Azure.DataApiBuilder.Core.Services; +using Azure.DataApiBuilder.Core.Services.MetadataProviders; +using Azure.DataApiBuilder.Mcp.BuiltInTools; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using ModelContextProtocol.Protocol; +using Moq; + +namespace Azure.DataApiBuilder.Service.Tests.Mcp +{ + /// + /// Unit tests for ExecuteEntityTool parameter validation and default application. + /// Uses mocked metadata and query engine to isolate the tool's logic from real DB. + /// + /// Key behaviors tested: + /// - Parameters validated against StoredProcedureDefinition.Parameters (DB metadata). + /// - Config defaults (HasConfigDefault/ConfigDefaultValue) applied for missing params. + /// - Invalid parameter names rejected. + /// - Entity-level and runtime-level gating. + /// + [TestClass] + public class ExecuteEntityToolTests + { + private const string TEST_ENTITY = "GetBook"; + private const string SP_SOURCE_OBJECT = "get_book"; + + #region Parameter Validation Tests + + /// + /// A parameter that exists in DB metadata (StoredProcedureDefinition.Parameters) + /// is accepted even if it has no config-side entry. + /// + [TestMethod] + public async Task ExecuteEntity_AcceptsDbDiscoveredParam_NotInConfig() + { + Dictionary dbParams = new() + { + ["id"] = new() + }; + + CallToolResult result = await ExecuteWithMockedEngineAsync( + entityName: TEST_ENTITY, + dbParameters: dbParams, + userParameters: new() { { "id", 1 } }); + + Assert.IsTrue(result.IsError == false || result.IsError == null, + $"Should accept DB-discovered param 'id'. Content: {GetFirstText(result)}"); + } + + /// + /// A parameter name NOT in StoredProcedureDefinition.Parameters is rejected + /// with an InvalidArguments error. + /// + [DataTestMethod] + [DataRow("nonexistent", DisplayName = "Completely unknown param")] + [DataRow("ID", DisplayName = "Case-sensitive mismatch")] + public async Task ExecuteEntity_RejectsInvalidParamName(string invalidParamName) + { + Dictionary dbParams = new() + { + ["id"] = new() + }; + + CallToolResult result = await ExecuteWithMockedEngineAsync( + entityName: TEST_ENTITY, + dbParameters: dbParams, + userParameters: new() { { invalidParamName, "value" } }); + + Assert.IsTrue(result.IsError == true, + $"Should reject param '{invalidParamName}' not in DB metadata."); + string content = GetFirstText(result); + StringAssert.Contains(content, invalidParamName); + StringAssert.Contains(content, "InvalidArguments"); + } + + /// + /// Multiple parameters can be provided when all exist in DB metadata. + /// + [TestMethod] + public async Task ExecuteEntity_AcceptsMultipleValidParams() + { + Dictionary dbParams = new() + { + ["title"] = new(), + ["publisher_id"] = new() + }; + + CallToolResult result = await ExecuteWithMockedEngineAsync( + entityName: TEST_ENTITY, + dbParameters: dbParams, + userParameters: new() { { "title", "Test" }, { "publisher_id", 123 } }); + + Assert.IsTrue(result.IsError == false || result.IsError == null, + $"Should accept all valid params. Content: {GetFirstText(result)}"); + } + + /// + /// If one param in a multi-param request is invalid, the entire request is rejected. + /// + [TestMethod] + public async Task ExecuteEntity_RejectsRequest_WhenAnyParamInvalid() + { + Dictionary dbParams = new() + { + ["id"] = new() + }; + + CallToolResult result = await ExecuteWithMockedEngineAsync( + entityName: TEST_ENTITY, + dbParameters: dbParams, + userParameters: new() { { "id", 1 }, { "bogus", "x" } }); + + Assert.IsTrue(result.IsError == true, + "Should reject request when any param is invalid."); + StringAssert.Contains(GetFirstText(result), "bogus"); + } + + #endregion + + #region Default Application Tests + + /// + /// Config defaults are applied for parameters the user did not supply. + /// Verifies that the context passed to the query engine includes the default values. + /// + [TestMethod] + public async Task ExecuteEntity_AppliesConfigDefaults_ForMissingParams() + { + Dictionary dbParams = new() + { + ["title"] = new() { HasConfigDefault = true, ConfigDefaultValue = "defaultTitle" }, + ["publisher_id"] = new() { HasConfigDefault = true, ConfigDefaultValue = "999" } + }; + + StoredProcedureRequestContext capturedContext = null; + CallToolResult result = await ExecuteWithMockedEngineAsync( + entityName: TEST_ENTITY, + dbParameters: dbParams, + userParameters: null, + captureContext: ctx => capturedContext = ctx); + + Assert.IsTrue(result.IsError == false || result.IsError == null, + $"Should succeed with config defaults. Content: {GetFirstText(result)}"); + Assert.IsNotNull(capturedContext, "Query engine should have been called."); + Assert.IsTrue(capturedContext.ResolvedParameters.ContainsKey("title")); + Assert.IsTrue(capturedContext.ResolvedParameters.ContainsKey("publisher_id")); + Assert.AreEqual("defaultTitle", capturedContext.ResolvedParameters["title"]); + Assert.AreEqual("999", capturedContext.ResolvedParameters["publisher_id"]); + } + + /// + /// User-supplied parameters override config defaults. + /// + [TestMethod] + public async Task ExecuteEntity_UserParams_OverrideConfigDefaults() + { + Dictionary dbParams = new() + { + ["title"] = new() { HasConfigDefault = true, ConfigDefaultValue = "defaultTitle" }, + ["publisher_id"] = new() { HasConfigDefault = true, ConfigDefaultValue = "999" } + }; + + StoredProcedureRequestContext capturedContext = null; + CallToolResult result = await ExecuteWithMockedEngineAsync( + entityName: TEST_ENTITY, + dbParameters: dbParams, + userParameters: new() { { "title", "UserTitle" } }, + captureContext: ctx => capturedContext = ctx); + + Assert.IsTrue(result.IsError == false || result.IsError == null, + $"Should succeed. Content: {GetFirstText(result)}"); + Assert.IsNotNull(capturedContext); + Assert.AreEqual("UserTitle", capturedContext.ResolvedParameters["title"]); + // publisher_id should get the config default since user didn't supply it + Assert.AreEqual("999", capturedContext.ResolvedParameters["publisher_id"]); + } + + /// + /// Parameters without config defaults are NOT injected into the request. + /// Only params with HasConfigDefault=true get applied. + /// + [TestMethod] + public async Task ExecuteEntity_DoesNotInjectParams_WithoutConfigDefault() + { + Dictionary dbParams = new() + { + ["id"] = new(), // No config default + ["tenant"] = new() { HasConfigDefault = true, ConfigDefaultValue = "default_tenant" } + }; + + StoredProcedureRequestContext capturedContext = null; + CallToolResult result = await ExecuteWithMockedEngineAsync( + entityName: TEST_ENTITY, + dbParameters: dbParams, + userParameters: new() { { "id", 42 } }, + captureContext: ctx => capturedContext = ctx); + + Assert.IsTrue(result.IsError == false || result.IsError == null, + $"Should succeed. Content: {GetFirstText(result)}"); + Assert.IsNotNull(capturedContext); + Assert.IsTrue(capturedContext.ResolvedParameters.ContainsKey("id")); + Assert.IsTrue(capturedContext.ResolvedParameters.ContainsKey("tenant")); + Assert.AreEqual("default_tenant", capturedContext.ResolvedParameters["tenant"]); + } + + /// + /// Zero-parameter SP with no user params and no config defaults: no parameters + /// are passed to the query engine. + /// + [TestMethod] + public async Task ExecuteEntity_ZeroParamSP_PassesEmptyParams() + { + Dictionary dbParams = new(); + + StoredProcedureRequestContext capturedContext = null; + CallToolResult result = await ExecuteWithMockedEngineAsync( + entityName: TEST_ENTITY, + dbParameters: dbParams, + userParameters: null, + captureContext: ctx => capturedContext = ctx); + + Assert.IsTrue(result.IsError == false || result.IsError == null, + $"Should succeed for zero-param SP. Content: {GetFirstText(result)}"); + Assert.IsNotNull(capturedContext); + Assert.AreEqual(0, capturedContext.ResolvedParameters.Count); + } + + #endregion + + #region Gating Tests + + /// + /// When the entity is not a stored procedure, ExecuteEntityTool returns InvalidEntity. + /// + [TestMethod] + public async Task ExecuteEntity_RejectsNonStoredProcedureEntity() + { + IServiceProvider sp = BuildServiceProvider( + entityName: "Book", + sourceObject: "books", + sourceType: EntitySourceType.Table, + dbParameters: new()); + + ExecuteEntityTool tool = new(); + using JsonDocument args = JsonDocument.Parse("{\"entity\": \"Book\"}"); + CallToolResult result = await tool.ExecuteAsync(args, sp, CancellationToken.None); + + Assert.IsTrue(result.IsError == true); + StringAssert.Contains(GetFirstText(result), "InvalidEntity"); + } + + /// + /// When the entity does not exist in config, returns EntityNotFound. + /// + [TestMethod] + public async Task ExecuteEntity_ReturnsError_WhenEntityNotFound() + { + IServiceProvider sp = BuildServiceProvider( + entityName: TEST_ENTITY, + sourceObject: SP_SOURCE_OBJECT, + sourceType: EntitySourceType.StoredProcedure, + dbParameters: new() { ["id"] = new() }); + + ExecuteEntityTool tool = new(); + using JsonDocument args = JsonDocument.Parse("{\"entity\": \"NonExistent\"}"); + CallToolResult result = await tool.ExecuteAsync(args, sp, CancellationToken.None); + + Assert.IsTrue(result.IsError == true); + StringAssert.Contains(GetFirstText(result), "EntityNotFound"); + } + + #endregion + + #region Helpers + + /// + /// Runs ExecuteEntityTool with a mocked query engine that captures the + /// StoredProcedureRequestContext and returns an empty result. + /// + private static async Task ExecuteWithMockedEngineAsync( + string entityName, + Dictionary dbParameters, + Dictionary userParameters, + Action captureContext = null) + { + IServiceProvider sp = BuildServiceProvider( + entityName: entityName, + sourceObject: SP_SOURCE_OBJECT, + sourceType: EntitySourceType.StoredProcedure, + dbParameters: dbParameters, + captureContext: captureContext); + + ExecuteEntityTool tool = new(); + + var args = new Dictionary { { "entity", entityName } }; + if (userParameters != null) + { + args["parameters"] = userParameters; + } + + string argsJson = JsonSerializer.Serialize(args); + using JsonDocument arguments = JsonDocument.Parse(argsJson); + + return await tool.ExecuteAsync(arguments, sp, CancellationToken.None); + } + + /// + /// Builds a fully mocked service provider for ExecuteEntityTool. + /// + private static IServiceProvider BuildServiceProvider( + string entityName, + string sourceObject, + EntitySourceType sourceType, + Dictionary dbParameters, + Action captureContext = null) + { + Entity entity = new( + Source: new(sourceObject, sourceType, Parameters: null, KeyFields: null), + GraphQL: new(entityName, entityName), + Rest: new(Enabled: true), + Fields: null, + Permissions: new[] + { + new EntityPermission( + Role: "anonymous", + Actions: new[] + { + new EntityAction(Action: EntityActionOperation.Execute, Fields: null, Policy: null) + }) + }, + Relationships: null, + Mappings: null, + Mcp: null); + + Dictionary entities = new() { [entityName] = entity }; + + RuntimeConfig config = new( + Schema: "test-schema", + DataSource: new DataSource(DatabaseType: DatabaseType.MSSQL, ConnectionString: "", Options: null), + Runtime: new( + Rest: new(), + GraphQL: new(), + Mcp: new(Enabled: true, Path: "/mcp", DmlTools: null), + Host: new(Cors: null, Authentication: null, Mode: HostMode.Development) + ), + Entities: new(entities)); + + ServiceCollection services = new(); + + RuntimeConfigProvider configProvider = TestHelper.GenerateInMemoryRuntimeConfigProvider(config); + services.AddSingleton(configProvider); + + // Mock authorization resolver + Mock mockAuthResolver = new(); + mockAuthResolver.Setup(x => x.IsValidRoleContext(It.IsAny())).Returns(true); + mockAuthResolver + .Setup(x => x.AreRoleAndOperationDefinedForEntity( + It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(true); + services.AddSingleton(mockAuthResolver.Object); + + // Mock HttpContext with anonymous role header + DefaultHttpContext httpContext = new(); + httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER] = "anonymous"; + IHttpContextAccessor httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext }; + services.AddSingleton(httpContextAccessor); + + // Mock metadata provider with DB object + DatabaseObject dbObject = sourceType == EntitySourceType.StoredProcedure + ? new DatabaseStoredProcedure("dbo", sourceObject) + { + SourceType = EntitySourceType.StoredProcedure, + StoredProcedureDefinition = new StoredProcedureDefinition + { + Parameters = dbParameters + } + } + : new DatabaseTable("dbo", sourceObject) { SourceType = EntitySourceType.Table }; + + Mock mockSqlMetadataProvider = new(); + mockSqlMetadataProvider + .Setup(x => x.EntityToDatabaseObject) + .Returns(new Dictionary { [entityName] = dbObject }); + mockSqlMetadataProvider.Setup(x => x.GetDatabaseType()).Returns(DatabaseType.MSSQL); + + Mock mockMetadataProviderFactory = new(); + mockMetadataProviderFactory + .Setup(x => x.GetMetadataProvider(It.IsAny())) + .Returns(mockSqlMetadataProvider.Object); + services.AddSingleton(mockMetadataProviderFactory.Object); + + // Mock query engine factory + Mock mockQueryEngine = new(); + mockQueryEngine + .Setup(x => x.ExecuteAsync(It.IsAny(), It.IsAny())) + .Returns((StoredProcedureRequestContext ctx, string ds) => + { + captureContext?.Invoke(ctx); + // Return empty JSON array result + using JsonDocument doc = JsonDocument.Parse("[]"); + return Task.FromResult(new OkObjectResult(doc.RootElement.Clone())); + }); + + Mock mockQueryEngineFactory = new(); + mockQueryEngineFactory + .Setup(x => x.GetQueryEngine(It.IsAny())) + .Returns(mockQueryEngine.Object); + services.AddSingleton(mockQueryEngineFactory.Object); + + services.AddLogging(); + + return services.BuildServiceProvider(); + } + + private static string GetFirstText(CallToolResult result) + { + if (result.Content is null || result.Content.Count == 0) + { + return string.Empty; + } + + return result.Content[0] is TextContentBlock textBlock + ? textBlock.Text ?? string.Empty + : string.Empty; + } + + #endregion + } +} From c0a6f578df8ffac9f8df4611d655e583c0c87b61 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Tue, 19 May 2026 14:06:14 +0530 Subject: [PATCH 2/6] Fix role permission and copilot suggesstions --- .../BuiltInTools/ExecuteEntityTool.cs | 7 +- .../ExecuteEntityToolMsSqlIntegrationTests.cs | 65 ++++++++++++++++++- .../Mcp/ExecuteEntityToolTests.cs | 32 ++++----- 3 files changed, 84 insertions(+), 20 deletions(-) diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs index 2ec5712eb2..1ade144d6a 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs @@ -165,7 +165,12 @@ public async Task ExecuteAsync( // 7) Validate parameters against DB metadata (StoredProcedureDefinition.Parameters), // which is the source of truth for parameter names. The upstream merge performed by // FillSchemaForStoredProcedureAsync ensures this dictionary contains all valid parameters. - StoredProcedureDefinition spDefinition = ((DatabaseStoredProcedure)dbObject).StoredProcedureDefinition; + if (dbObject is not DatabaseStoredProcedure storedProcedure) + { + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidEntity", $"Entity '{entity}' is not a stored procedure.", logger); + } + + StoredProcedureDefinition spDefinition = storedProcedure.StoredProcedureDefinition; if (parameters != null) { foreach (KeyValuePair param in parameters) diff --git a/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs b/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs index dbbb75281f..b4e95b26f0 100644 --- a/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs +++ b/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Security.Claims; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -71,11 +72,55 @@ public async Task ExecuteEntity_SuccessfulExecution(string entityName, string pa CallToolResult result = await ExecuteEntityAsync(entityName, parameters); - Assert.IsTrue(result.IsError == false || result.IsError == null, - $"execute_entity failed for entity '{entityName}' with params '{parametersJson}'. Content: {SerializeFirstContent(result)}"); + AssertSuccess(result, + $"execute_entity failed for entity '{entityName}' with params '{parametersJson}'."); + // Parse response and verify structure string content = GetFirstTextContent(result); Assert.IsFalse(string.IsNullOrWhiteSpace(content), $"Expected non-empty result for entity '{entityName}'."); + + using JsonDocument doc = JsonDocument.Parse(content); + JsonElement root = doc.RootElement; + Assert.AreEqual(entityName, root.GetProperty("entity").GetString()); + Assert.AreEqual("Stored procedure executed successfully", root.GetProperty("message").GetString()); + } + + /// + /// Verify that GetBook with id=1 returns the actual book record from the database. + /// This ensures the parameter value is correctly passed to the stored procedure. + /// + [TestMethod] + public async Task ExecuteEntity_GetBookById_ReturnsMatchingRecord() + { + Dictionary parameters = new() { { "id", 1 } }; + CallToolResult result = await ExecuteEntityAsync("GetBook", parameters); + + AssertSuccess(result, "GetBook with id=1 should succeed."); + + using JsonDocument doc = JsonDocument.Parse(GetFirstTextContent(result)); + JsonElement root = doc.RootElement; + + // Verify the value array contains at least one record with id=1 + Assert.IsTrue(root.TryGetProperty("value", out JsonElement value), "Response should contain 'value' property."); + Assert.AreEqual(JsonValueKind.Array, value.ValueKind); + Assert.IsTrue(value.GetArrayLength() > 0, "Expected at least one book record."); + Assert.AreEqual(1, value[0].GetProperty("id").GetInt32()); + } + + /// + /// Verify that InsertBook with no user params applies config defaults (title="randomX", publisher_id="1234"). + /// The SP inserts using those defaults. We verify the tool reports success (the SP executed without error). + /// + [TestMethod] + public async Task ExecuteEntity_InsertBookWithDefaults_ExecutesSuccessfully() + { + CallToolResult result = await ExecuteEntityAsync("InsertBook", parameters: null); + + AssertSuccess(result, "InsertBook with config defaults should succeed."); + + using JsonDocument doc = JsonDocument.Parse(GetFirstTextContent(result)); + JsonElement root = doc.RootElement; + Assert.AreEqual("InsertBook", root.GetProperty("entity").GetString()); } /// @@ -132,9 +177,17 @@ private static IServiceProvider BuildExecuteEntityServiceProvider(RuntimeConfig // Real authorization resolver wired by SqlTestBase against the live config + provider. services.AddSingleton(_authorizationResolver); - // Real HttpContext carrying the anonymous role header. + // Real HttpContext carrying the anonymous role header and a ClaimsPrincipal + // with the anonymous role claim so that AuthorizationResolver.IsValidRoleContext + // (which calls httpContext.User.IsInRole) returns true. DefaultHttpContext httpContext = new(); httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER] = AuthorizationResolver.ROLE_ANONYMOUS; + ClaimsIdentity identity = new( + authenticationType: "TestAuth", + nameType: null, + roleType: AuthenticationOptions.ROLE_CLAIM_TYPE); + identity.AddClaim(new Claim(AuthenticationOptions.ROLE_CLAIM_TYPE, AuthorizationResolver.ROLE_ANONYMOUS)); + httpContext.User = new ClaimsPrincipal(identity); IHttpContextAccessor httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext }; services.AddSingleton(httpContextAccessor); @@ -176,6 +229,12 @@ private static string GetFirstTextContent(CallToolResult result) : string.Empty; } + private static void AssertSuccess(CallToolResult result, string message) + { + Assert.IsTrue(result.IsError != true, + $"{message} Content: {GetFirstTextContent(result)}"); + } + private static string SerializeFirstContent(CallToolResult result) { if (result.Content is null || result.Content.Count == 0) diff --git a/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs b/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs index 0d55cd296b..e391f3be06 100644 --- a/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs +++ b/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs @@ -61,8 +61,7 @@ public async Task ExecuteEntity_AcceptsDbDiscoveredParam_NotInConfig() dbParameters: dbParams, userParameters: new() { { "id", 1 } }); - Assert.IsTrue(result.IsError == false || result.IsError == null, - $"Should accept DB-discovered param 'id'. Content: {GetFirstText(result)}"); + AssertSuccess(result, "Should accept DB-discovered param 'id'."); } /// @@ -108,8 +107,7 @@ public async Task ExecuteEntity_AcceptsMultipleValidParams() dbParameters: dbParams, userParameters: new() { { "title", "Test" }, { "publisher_id", 123 } }); - Assert.IsTrue(result.IsError == false || result.IsError == null, - $"Should accept all valid params. Content: {GetFirstText(result)}"); + AssertSuccess(result, "Should accept all valid params."); } /// @@ -150,15 +148,14 @@ public async Task ExecuteEntity_AppliesConfigDefaults_ForMissingParams() ["publisher_id"] = new() { HasConfigDefault = true, ConfigDefaultValue = "999" } }; - StoredProcedureRequestContext capturedContext = null; + StoredProcedureRequestContext? capturedContext = null; CallToolResult result = await ExecuteWithMockedEngineAsync( entityName: TEST_ENTITY, dbParameters: dbParams, userParameters: null, captureContext: ctx => capturedContext = ctx); - Assert.IsTrue(result.IsError == false || result.IsError == null, - $"Should succeed with config defaults. Content: {GetFirstText(result)}"); + AssertSuccess(result, "Should succeed with config defaults."); Assert.IsNotNull(capturedContext, "Query engine should have been called."); Assert.IsTrue(capturedContext.ResolvedParameters.ContainsKey("title")); Assert.IsTrue(capturedContext.ResolvedParameters.ContainsKey("publisher_id")); @@ -178,15 +175,14 @@ public async Task ExecuteEntity_UserParams_OverrideConfigDefaults() ["publisher_id"] = new() { HasConfigDefault = true, ConfigDefaultValue = "999" } }; - StoredProcedureRequestContext capturedContext = null; + StoredProcedureRequestContext? capturedContext = null; CallToolResult result = await ExecuteWithMockedEngineAsync( entityName: TEST_ENTITY, dbParameters: dbParams, userParameters: new() { { "title", "UserTitle" } }, captureContext: ctx => capturedContext = ctx); - Assert.IsTrue(result.IsError == false || result.IsError == null, - $"Should succeed. Content: {GetFirstText(result)}"); + AssertSuccess(result, "Should succeed with user-supplied params."); Assert.IsNotNull(capturedContext); Assert.AreEqual("UserTitle", capturedContext.ResolvedParameters["title"]); // publisher_id should get the config default since user didn't supply it @@ -206,15 +202,14 @@ public async Task ExecuteEntity_DoesNotInjectParams_WithoutConfigDefault() ["tenant"] = new() { HasConfigDefault = true, ConfigDefaultValue = "default_tenant" } }; - StoredProcedureRequestContext capturedContext = null; + StoredProcedureRequestContext? capturedContext = null; CallToolResult result = await ExecuteWithMockedEngineAsync( entityName: TEST_ENTITY, dbParameters: dbParams, userParameters: new() { { "id", 42 } }, captureContext: ctx => capturedContext = ctx); - Assert.IsTrue(result.IsError == false || result.IsError == null, - $"Should succeed. Content: {GetFirstText(result)}"); + AssertSuccess(result, "Should succeed with partial params."); Assert.IsNotNull(capturedContext); Assert.IsTrue(capturedContext.ResolvedParameters.ContainsKey("id")); Assert.IsTrue(capturedContext.ResolvedParameters.ContainsKey("tenant")); @@ -230,15 +225,14 @@ public async Task ExecuteEntity_ZeroParamSP_PassesEmptyParams() { Dictionary dbParams = new(); - StoredProcedureRequestContext capturedContext = null; + StoredProcedureRequestContext? capturedContext = null; CallToolResult result = await ExecuteWithMockedEngineAsync( entityName: TEST_ENTITY, dbParameters: dbParams, userParameters: null, captureContext: ctx => capturedContext = ctx); - Assert.IsTrue(result.IsError == false || result.IsError == null, - $"Should succeed for zero-param SP. Content: {GetFirstText(result)}"); + AssertSuccess(result, "Should succeed for zero-param SP."); Assert.IsNotNull(capturedContext); Assert.AreEqual(0, capturedContext.ResolvedParameters.Count); } @@ -430,6 +424,12 @@ private static IServiceProvider BuildServiceProvider( return services.BuildServiceProvider(); } + private static void AssertSuccess(CallToolResult result, string message) + { + Assert.IsTrue(result.IsError != true, + $"{message} Content: {GetFirstText(result)}"); + } + private static string GetFirstText(CallToolResult result) { if (result.Content is null || result.Content.Count == 0) From c8dc52f592ae65640723c0660e2e44edff268353 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Tue, 19 May 2026 14:16:47 +0530 Subject: [PATCH 3/6] Removed unused code --- .../Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs b/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs index b4e95b26f0..260b91cd74 100644 --- a/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs +++ b/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs @@ -234,17 +234,5 @@ private static void AssertSuccess(CallToolResult result, string message) Assert.IsTrue(result.IsError != true, $"{message} Content: {GetFirstTextContent(result)}"); } - - private static string SerializeFirstContent(CallToolResult result) - { - if (result.Content is null || result.Content.Count == 0) - { - return ""; - } - - return result.Content[0] is TextContentBlock textBlock - ? textBlock.Text ?? string.Empty - : result.Content[0].GetType().Name; - } } } From 8fc98dbcfd73a281184a6a1fe5c5806c5f9166d0 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Tue, 19 May 2026 15:35:38 +0530 Subject: [PATCH 4/6] fix datasource issue in test --- .../Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs b/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs index 260b91cd74..8960619280 100644 --- a/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs +++ b/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs @@ -41,14 +41,11 @@ namespace Azure.DataApiBuilder.Service.Tests.Mcp [TestClass, TestCategory(TestCategory.MSSQL)] public class ExecuteEntityToolMsSqlIntegrationTests : SqlTestBase { - private static RuntimeConfig _baseConfig; - [ClassInitialize] public static async Task SetupAsync(TestContext context) { DatabaseEngine = TestCategory.MSSQL; await InitializeTestFixture(); - _baseConfig = SqlTestHelper.SetupRuntimeConfig(); } /// @@ -143,7 +140,7 @@ public async Task ExecuteEntity_InvalidParamName_ReturnsError(string entityName, private static async Task ExecuteEntityAsync(string entityName, Dictionary parameters) { - IServiceProvider serviceProvider = BuildExecuteEntityServiceProvider(_baseConfig); + IServiceProvider serviceProvider = BuildExecuteEntityServiceProvider(); ExecuteEntityTool tool = new(); var args = new Dictionary { { "entity", entityName } }; @@ -162,13 +159,17 @@ private static async Task ExecuteEntityAsync(string entityName, /// Builds a service provider that wires ExecuteEntityTool to the shared fixture's /// real ISqlMetadataProvider, real IQueryEngine (SqlQueryEngine), and real /// authorization resolver, with a DefaultHttpContext carrying the anonymous role header. + /// Uses the RuntimeConfigProvider from the WebApplicationFactory so that the datasource + /// name matches what the real MsSqlQueryExecutor was initialized with. /// - private static IServiceProvider BuildExecuteEntityServiceProvider(RuntimeConfig config) + private static IServiceProvider BuildExecuteEntityServiceProvider() { ServiceCollection services = new(); - // Real RuntimeConfigProvider populated from the provided config snapshot. - RuntimeConfigProvider configProvider = TestHelper.GenerateInMemoryRuntimeConfigProvider(config); + // Use the RuntimeConfigProvider from the WebApplicationFactory — this is the same + // provider that initialized _queryExecutor, so its DefaultDataSourceName matches + // the key in _queryExecutor.ConnectionStringBuilders. + RuntimeConfigProvider configProvider = _application.Services.GetRequiredService(); services.AddSingleton(configProvider); // Real metadata-provider factory backed by the shared fixture's live provider. From be7a99f55f6642c8dec40667555421e7a1b1028f Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Tue, 19 May 2026 16:07:42 +0530 Subject: [PATCH 5/6] Fix test failing with response type --- .../ExecuteEntityToolMsSqlIntegrationTests.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs b/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs index 8960619280..2ca01a7951 100644 --- a/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs +++ b/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs @@ -97,11 +97,19 @@ public async Task ExecuteEntity_GetBookById_ReturnsMatchingRecord() using JsonDocument doc = JsonDocument.Parse(GetFirstTextContent(result)); JsonElement root = doc.RootElement; - // Verify the value array contains at least one record with id=1 - Assert.IsTrue(root.TryGetProperty("value", out JsonElement value), "Response should contain 'value' property."); - Assert.AreEqual(JsonValueKind.Array, value.ValueKind); - Assert.IsTrue(value.GetArrayLength() > 0, "Expected at least one book record."); - Assert.AreEqual(1, value[0].GetProperty("id").GetInt32()); + // Verify the value property contains the SP result with at least one record with id=1. + // SqlResponseHelpers.OkResponse wraps results in { value: [...] }, and + // BuildExecuteSuccessResponse serializes that as-is into the "value" field. + Assert.IsTrue(root.TryGetProperty("value", out JsonElement valueWrapper), "Response should contain 'value' property."); + + // The value may be the wrapper object { "value": [...] } or directly an array. + JsonElement records = valueWrapper.ValueKind == JsonValueKind.Object + ? valueWrapper.GetProperty("value") + : valueWrapper; + + Assert.AreEqual(JsonValueKind.Array, records.ValueKind); + Assert.IsTrue(records.GetArrayLength() > 0, "Expected at least one book record."); + Assert.AreEqual(1, records[0].GetProperty("id").GetInt32()); } /// From a46afe9dba9deeb6dd4926f6ffb8efd5c9780b98 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Wed, 20 May 2026 12:22:54 +0530 Subject: [PATCH 6/6] Address review: null guards, case-sensitivity comment, and nullable fixes --- .../BuiltInTools/ExecuteEntityTool.cs | 13 +++++++++---- .../Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs | 8 +++++--- src/Service.Tests/Mcp/ExecuteEntityToolTests.cs | 8 +++++--- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs index 1ade144d6a..8d37287544 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs @@ -165,13 +165,15 @@ public async Task ExecuteAsync( // 7) Validate parameters against DB metadata (StoredProcedureDefinition.Parameters), // which is the source of truth for parameter names. The upstream merge performed by // FillSchemaForStoredProcedureAsync ensures this dictionary contains all valid parameters. + // Note: Comparison is case-sensitive (default Dictionary comparer), + // consistent with the existing REST/GraphQL SP execution path. if (dbObject is not DatabaseStoredProcedure storedProcedure) { return McpResponseBuilder.BuildErrorResult(toolName, "InvalidEntity", $"Entity '{entity}' is not a stored procedure.", logger); } StoredProcedureDefinition spDefinition = storedProcedure.StoredProcedureDefinition; - if (parameters != null) + if (parameters != null && spDefinition.Parameters is not null) { foreach (KeyValuePair param in parameters) { @@ -211,11 +213,14 @@ public async Task ExecuteAsync( // Apply config-declared defaults from the merged ParameterDefinitions. // This covers all parameters (including DB-discovered ones with config defaults) // and applies them when the user didn't supply a value. - foreach ((string paramName, ParameterDefinition paramDef) in spDefinition.Parameters) + if (spDefinition.Parameters is not null) { - if (!context.FieldValuePairsInBody.ContainsKey(paramName) && paramDef.HasConfigDefault) + foreach ((string paramName, ParameterDefinition paramDef) in spDefinition.Parameters) { - context.FieldValuePairsInBody[paramName] = paramDef.ConfigDefaultValue; + if (!context.FieldValuePairsInBody.ContainsKey(paramName) && paramDef.HasConfigDefault) + { + context.FieldValuePairsInBody[paramName] = paramDef.ConfigDefaultValue; + } } } diff --git a/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs b/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs index 2ca01a7951..624990dc38 100644 --- a/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs +++ b/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#nullable enable + using System; using System.Collections.Generic; using System.Security.Claims; @@ -61,9 +63,9 @@ public static async Task SetupAsync(TestContext context) [DataRow("InsertBook", null, DisplayName = "Config defaults applied when no params supplied")] [DataRow("InsertBook", "{\"title\": \"Integration Test Book\", \"publisher_id\": 2345}", DisplayName = "User-supplied params override defaults")] [DataRow("GetBooks", null, DisplayName = "Zero-param SP succeeds")] - public async Task ExecuteEntity_SuccessfulExecution(string entityName, string parametersJson) + public async Task ExecuteEntity_SuccessfulExecution(string entityName, string? parametersJson) { - Dictionary parameters = parametersJson != null + Dictionary? parameters = parametersJson != null ? JsonSerializer.Deserialize>(parametersJson) : null; @@ -146,7 +148,7 @@ public async Task ExecuteEntity_InvalidParamName_ReturnsError(string entityName, StringAssert.Contains(content, paramName); } - private static async Task ExecuteEntityAsync(string entityName, Dictionary parameters) + private static async Task ExecuteEntityAsync(string entityName, Dictionary? parameters) { IServiceProvider serviceProvider = BuildExecuteEntityServiceProvider(); ExecuteEntityTool tool = new(); diff --git a/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs b/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs index e391f3be06..6d87da9eb4 100644 --- a/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs +++ b/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#nullable enable + using System; using System.Collections.Generic; using System.Text.Json; @@ -292,8 +294,8 @@ public async Task ExecuteEntity_ReturnsError_WhenEntityNotFound() private static async Task ExecuteWithMockedEngineAsync( string entityName, Dictionary dbParameters, - Dictionary userParameters, - Action captureContext = null) + Dictionary? userParameters, + Action? captureContext = null) { IServiceProvider sp = BuildServiceProvider( entityName: entityName, @@ -324,7 +326,7 @@ private static IServiceProvider BuildServiceProvider( string sourceObject, EntitySourceType sourceType, Dictionary dbParameters, - Action captureContext = null) + Action? captureContext = null) { Entity entity = new( Source: new(sourceObject, sourceType, Parameters: null, KeyFields: null),