Skip to content

Commit 4218260

Browse files
committed
add code coverage to easy to cover branches
1 parent 8028a93 commit 4218260

23 files changed

Lines changed: 3059 additions & 0 deletions
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Text.Json;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Azure.DataApiBuilder.Auth;
10+
using Azure.DataApiBuilder.Config.ObjectModel;
11+
using Azure.DataApiBuilder.Core.Authorization;
12+
using Azure.DataApiBuilder.Core.Configurations;
13+
using Azure.DataApiBuilder.Mcp.BuiltInTools;
14+
using Azure.DataApiBuilder.Mcp.Model;
15+
using Microsoft.AspNetCore.Http;
16+
using Microsoft.Extensions.DependencyInjection;
17+
using Microsoft.VisualStudio.TestTools.UnitTesting;
18+
using ModelContextProtocol.Protocol;
19+
using Moq;
20+
21+
namespace Azure.DataApiBuilder.Service.Tests.Mcp
22+
{
23+
/// <summary>
24+
/// Non-database unit tests for the built-in DML MCP tools (create/read/update/delete_record).
25+
/// Covers the early-return branches that execute before any database access:
26+
/// tool-disabled (runtime and entity level), null/invalid arguments, and metadata-resolution
27+
/// failures. No test server or database is required.
28+
/// </summary>
29+
[TestClass]
30+
public class BuiltInDmlToolValidationTests
31+
{
32+
#region Tool metadata & IsEnabled
33+
34+
[TestMethod]
35+
public void ToolMetadata_HasExpectedNames()
36+
{
37+
Assert.AreEqual("create_record", new CreateRecordTool().GetToolMetadata().Name);
38+
Assert.AreEqual("read_records", new ReadRecordsTool().GetToolMetadata().Name);
39+
Assert.AreEqual("update_record", new UpdateRecordTool().GetToolMetadata().Name);
40+
Assert.AreEqual("delete_record", new DeleteRecordTool().GetToolMetadata().Name);
41+
}
42+
43+
[DataTestMethod]
44+
[DataRow(true)]
45+
[DataRow(false)]
46+
public void IsEnabled_ReflectsDmlToolsConfig(bool enabled)
47+
{
48+
RuntimeConfig config = CreateConfig(
49+
createEnabled: enabled, readEnabled: enabled, updateEnabled: enabled, deleteEnabled: enabled);
50+
51+
Assert.AreEqual(enabled, new CreateRecordTool().IsEnabled(config));
52+
Assert.AreEqual(enabled, new ReadRecordsTool().IsEnabled(config));
53+
Assert.AreEqual(enabled, new UpdateRecordTool().IsEnabled(config));
54+
Assert.AreEqual(enabled, new DeleteRecordTool().IsEnabled(config));
55+
}
56+
57+
#endregion
58+
59+
#region Null arguments
60+
61+
[TestMethod]
62+
public async Task CreateRecord_NullArguments_ReturnsInvalidArguments()
63+
{
64+
CallToolResult result = await ExecuteAsync(new CreateRecordTool(), CreateServiceProvider(CreateConfig()), arguments: null);
65+
AssertErrorType(result, "InvalidArguments");
66+
}
67+
68+
[TestMethod]
69+
public async Task ReadRecords_NullArguments_ReturnsInvalidArguments()
70+
{
71+
CallToolResult result = await ExecuteAsync(new ReadRecordsTool(), CreateServiceProvider(CreateConfig()), arguments: null);
72+
AssertErrorType(result, "InvalidArguments");
73+
}
74+
75+
[TestMethod]
76+
public async Task UpdateRecord_NullArguments_ReturnsInvalidArguments()
77+
{
78+
CallToolResult result = await ExecuteAsync(new UpdateRecordTool(), CreateServiceProvider(CreateConfig()), arguments: null);
79+
AssertErrorType(result, "InvalidArguments");
80+
}
81+
82+
[TestMethod]
83+
public async Task DeleteRecord_NullArguments_ReturnsInvalidArguments()
84+
{
85+
CallToolResult result = await ExecuteAsync(new DeleteRecordTool(), CreateServiceProvider(CreateConfig()), arguments: null);
86+
AssertErrorType(result, "InvalidArguments");
87+
}
88+
89+
#endregion
90+
91+
#region Runtime-level tool disabled
92+
93+
[TestMethod]
94+
public async Task CreateRecord_RuntimeDisabled_ReturnsToolDisabled()
95+
{
96+
IServiceProvider sp = CreateServiceProvider(CreateConfig(createEnabled: false));
97+
CallToolResult result = await ExecuteAsync(new CreateRecordTool(), sp, "{\"entity\":\"Book\",\"data\":{\"title\":\"x\"}}");
98+
AssertErrorType(result, "ToolDisabled");
99+
}
100+
101+
[TestMethod]
102+
public async Task ReadRecords_RuntimeDisabled_ReturnsToolDisabled()
103+
{
104+
IServiceProvider sp = CreateServiceProvider(CreateConfig(readEnabled: false));
105+
CallToolResult result = await ExecuteAsync(new ReadRecordsTool(), sp, "{\"entity\":\"Book\"}");
106+
AssertErrorType(result, "ToolDisabled");
107+
}
108+
109+
[TestMethod]
110+
public async Task UpdateRecord_RuntimeDisabled_ReturnsToolDisabled()
111+
{
112+
IServiceProvider sp = CreateServiceProvider(CreateConfig(updateEnabled: false));
113+
CallToolResult result = await ExecuteAsync(new UpdateRecordTool(), sp, "{\"entity\":\"Book\",\"keys\":{\"id\":1},\"fields\":{\"title\":\"x\"}}");
114+
AssertErrorType(result, "ToolDisabled");
115+
}
116+
117+
[TestMethod]
118+
public async Task DeleteRecord_RuntimeDisabled_ReturnsToolDisabled()
119+
{
120+
IServiceProvider sp = CreateServiceProvider(CreateConfig(deleteEnabled: false));
121+
CallToolResult result = await ExecuteAsync(new DeleteRecordTool(), sp, "{\"entity\":\"Book\",\"keys\":{\"id\":1}}");
122+
AssertErrorType(result, "ToolDisabled");
123+
}
124+
125+
#endregion
126+
127+
#region Invalid arguments
128+
129+
[TestMethod]
130+
public async Task CreateRecord_MissingData_ReturnsInvalidArguments()
131+
{
132+
IServiceProvider sp = CreateServiceProvider(CreateConfig());
133+
CallToolResult result = await ExecuteAsync(new CreateRecordTool(), sp, "{\"entity\":\"Book\"}");
134+
AssertErrorType(result, "InvalidArguments");
135+
}
136+
137+
[TestMethod]
138+
public async Task ReadRecords_MissingEntity_ReturnsInvalidArguments()
139+
{
140+
IServiceProvider sp = CreateServiceProvider(CreateConfig());
141+
CallToolResult result = await ExecuteAsync(new ReadRecordsTool(), sp, "{\"select\":\"id\"}");
142+
AssertErrorType(result, "InvalidArguments");
143+
}
144+
145+
[TestMethod]
146+
public async Task UpdateRecord_MissingFields_ReturnsInvalidArguments()
147+
{
148+
IServiceProvider sp = CreateServiceProvider(CreateConfig());
149+
CallToolResult result = await ExecuteAsync(new UpdateRecordTool(), sp, "{\"entity\":\"Book\",\"keys\":{\"id\":1}}");
150+
AssertErrorType(result, "InvalidArguments");
151+
}
152+
153+
[TestMethod]
154+
public async Task DeleteRecord_MissingKeys_ReturnsInvalidArguments()
155+
{
156+
IServiceProvider sp = CreateServiceProvider(CreateConfig());
157+
CallToolResult result = await ExecuteAsync(new DeleteRecordTool(), sp, "{\"entity\":\"Book\"}");
158+
AssertErrorType(result, "InvalidArguments");
159+
}
160+
161+
#endregion
162+
163+
#region Entity-level DML disabled
164+
165+
[TestMethod]
166+
public async Task CreateRecord_EntityDmlDisabled_ReturnsToolDisabled()
167+
{
168+
IServiceProvider sp = CreateServiceProvider(CreateConfig(entityDmlEnabled: false));
169+
CallToolResult result = await ExecuteAsync(new CreateRecordTool(), sp, "{\"entity\":\"Book\",\"data\":{\"title\":\"x\"}}");
170+
AssertErrorType(result, "ToolDisabled");
171+
}
172+
173+
[TestMethod]
174+
public async Task DeleteRecord_EntityDmlDisabled_ReturnsToolDisabled()
175+
{
176+
IServiceProvider sp = CreateServiceProvider(CreateConfig(entityDmlEnabled: false));
177+
CallToolResult result = await ExecuteAsync(new DeleteRecordTool(), sp, "{\"entity\":\"Book\",\"keys\":{\"id\":1}}");
178+
AssertErrorType(result, "ToolDisabled");
179+
}
180+
181+
[TestMethod]
182+
public async Task UpdateRecord_EntityDmlDisabled_ReturnsToolDisabled()
183+
{
184+
IServiceProvider sp = CreateServiceProvider(CreateConfig(entityDmlEnabled: false));
185+
CallToolResult result = await ExecuteAsync(new UpdateRecordTool(), sp, "{\"entity\":\"Book\",\"keys\":{\"id\":1},\"fields\":{\"title\":\"x\"}}");
186+
AssertErrorType(result, "ToolDisabled");
187+
}
188+
189+
#endregion
190+
191+
#region Metadata resolution failure (no metadata provider registered)
192+
193+
[TestMethod]
194+
public async Task CreateRecord_UnresolvableMetadata_ReturnsError()
195+
{
196+
IServiceProvider sp = CreateServiceProvider(CreateConfig());
197+
CallToolResult result = await ExecuteAsync(new CreateRecordTool(), sp, "{\"entity\":\"Book\",\"data\":{\"title\":\"x\"}}");
198+
Assert.IsTrue(result.IsError == true);
199+
}
200+
201+
[TestMethod]
202+
public async Task ReadRecords_UnresolvableMetadata_ReturnsError()
203+
{
204+
IServiceProvider sp = CreateServiceProvider(CreateConfig());
205+
CallToolResult result = await ExecuteAsync(new ReadRecordsTool(), sp, "{\"entity\":\"Book\"}");
206+
Assert.IsTrue(result.IsError == true);
207+
}
208+
209+
[TestMethod]
210+
public async Task DeleteRecord_UnresolvableMetadata_ReturnsError()
211+
{
212+
IServiceProvider sp = CreateServiceProvider(CreateConfig());
213+
CallToolResult result = await ExecuteAsync(new DeleteRecordTool(), sp, "{\"entity\":\"Book\",\"keys\":{\"id\":1}}");
214+
Assert.IsTrue(result.IsError == true);
215+
}
216+
217+
#endregion
218+
219+
#region Helpers
220+
221+
private static async Task<CallToolResult> ExecuteAsync(IMcpTool tool, IServiceProvider sp, string? arguments)
222+
{
223+
if (arguments is null)
224+
{
225+
return await tool.ExecuteAsync(null, sp, CancellationToken.None);
226+
}
227+
228+
using JsonDocument args = JsonDocument.Parse(arguments);
229+
return await tool.ExecuteAsync(args, sp, CancellationToken.None);
230+
}
231+
232+
private static void AssertErrorType(CallToolResult result, string expectedType)
233+
{
234+
Assert.IsTrue(result.IsError == true, "Expected an error result.");
235+
TextContentBlock block = (TextContentBlock)result.Content[0];
236+
JsonElement root = JsonDocument.Parse(block.Text).RootElement;
237+
Assert.AreEqual(expectedType, root.GetProperty("error").GetProperty("type").GetString());
238+
}
239+
240+
private static RuntimeConfig CreateConfig(
241+
bool createEnabled = true,
242+
bool readEnabled = true,
243+
bool updateEnabled = true,
244+
bool deleteEnabled = true,
245+
bool entityDmlEnabled = true)
246+
{
247+
Dictionary<string, Entity> entities = new()
248+
{
249+
["Book"] = new Entity(
250+
Source: new("books", EntitySourceType.Table, null, null),
251+
GraphQL: new("Book", "Books"),
252+
Fields: null,
253+
Rest: new(Enabled: true),
254+
Permissions: new[]
255+
{
256+
new EntityPermission(Role: "anonymous", Actions: new[]
257+
{
258+
new EntityAction(Action: EntityActionOperation.All, Fields: null, Policy: null)
259+
})
260+
},
261+
Mappings: null,
262+
Relationships: null,
263+
Mcp: entityDmlEnabled ? null : new EntityMcpOptions(customToolEnabled: false, dmlToolsEnabled: false))
264+
};
265+
266+
return new RuntimeConfig(
267+
Schema: "test-schema",
268+
DataSource: new DataSource(DatabaseType: DatabaseType.MSSQL, ConnectionString: "", Options: null),
269+
Runtime: new(
270+
Rest: new(),
271+
GraphQL: new(),
272+
Mcp: new(
273+
Enabled: true,
274+
Path: "/mcp",
275+
DmlTools: new(
276+
describeEntities: true,
277+
readRecords: readEnabled,
278+
createRecord: createEnabled,
279+
updateRecord: updateEnabled,
280+
deleteRecord: deleteEnabled,
281+
executeEntity: true,
282+
aggregateRecords: true)),
283+
Host: new(Cors: null, Authentication: null, Mode: HostMode.Development)),
284+
Entities: new(entities));
285+
}
286+
287+
private static IServiceProvider CreateServiceProvider(RuntimeConfig config)
288+
{
289+
ServiceCollection services = new();
290+
291+
RuntimeConfigProvider configProvider = TestHelper.GenerateInMemoryRuntimeConfigProvider(config);
292+
services.AddSingleton(configProvider);
293+
294+
Mock<IAuthorizationResolver> authResolver = new();
295+
authResolver.Setup(x => x.IsValidRoleContext(It.IsAny<HttpContext>())).Returns(true);
296+
services.AddSingleton(authResolver.Object);
297+
298+
Mock<HttpContext> httpContext = new();
299+
Mock<HttpRequest> request = new();
300+
request.Setup(x => x.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER]).Returns("anonymous");
301+
httpContext.Setup(x => x.Request).Returns(request.Object);
302+
303+
Mock<IHttpContextAccessor> httpContextAccessor = new();
304+
httpContextAccessor.Setup(x => x.HttpContext).Returns(httpContext.Object);
305+
services.AddSingleton(httpContextAccessor.Object);
306+
307+
services.AddLogging();
308+
309+
return services.BuildServiceProvider();
310+
}
311+
312+
#endregion
313+
}
314+
}

0 commit comments

Comments
 (0)