Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 1 addition & 2 deletions src/Core/Resolvers/SqlMutationEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -467,8 +467,7 @@ await queryExecutor.ExecuteQueryAsync(
/// <returns>IActionResult</returns>
public async Task<IActionResult?> ExecuteAsync(RestRequestContext context)
{
// for REST API scenarios, use the default datasource
string dataSourceName = _runtimeConfigProvider.GetConfig().DefaultDataSourceName;
string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(context.EntityName);

Dictionary<string, object?> parameters = PrepareParameters(context);
ISqlMetadataProvider sqlMetadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName);
Expand Down
101 changes: 101 additions & 0 deletions src/Service.Tests/UnitTests/SqlMutationEngineHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
using System.Threading.Tasks;
using System.Transactions;
using Azure.DataApiBuilder.Auth;
using Azure.DataApiBuilder.Config;
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;
Expand Down Expand Up @@ -479,6 +481,105 @@ public async Task PerformMutationOperation_RejectsInvalidInvocation(EntityAction
}
}

/// <summary>
/// Verifies REST mutations use metadata for the entity's datasource rather than the root datasource.
/// </summary>
[TestMethod]
public async Task ExecuteRestMutation_UsesEntityDataSource()
{
const string rootDataSourceName = "root";
const string linkedDataSourceName = "linked";
const string entityName = "LinkedEntity";

Entity entity = new(
Source: new EntitySource("dbo.LinkedTable", EntitySourceType.Table, null, null),
GraphQL: null,
Fields: null,
Rest: null,
Permissions: Array.Empty<EntityPermission>(),
Mappings: null,
Relationships: null,
Mcp: null);
RuntimeConfig runtimeConfig = new(
Schema: string.Empty,
DataSource: new DataSource(DatabaseType.MSSQL, string.Empty),
Runtime: new RuntimeOptions(null, null, null, null),
Entities: new RuntimeEntities(new Dictionary<string, Entity> { [entityName] = entity }),
DefaultDataSourceName: rootDataSourceName,
DataSourceNameToDataSource: new Dictionary<string, DataSource>
{
[rootDataSourceName] = new(DatabaseType.MSSQL, string.Empty),
[linkedDataSourceName] = new(DatabaseType.MSSQL, string.Empty)
},
EntityNameToDataSourceName: new Dictionary<string, string>
{
[entityName] = linkedDataSourceName
});
Mock<RuntimeConfigLoader> configLoader = new(null, null);
configLoader.Object.RuntimeConfig = runtimeConfig;
RuntimeConfigProvider runtimeConfigProvider = new(configLoader.Object);

Mock<ISqlMetadataProvider> rootMetadataProvider = new();
rootMetadataProvider.Setup(x => x.GetDatabaseType()).Returns(DatabaseType.MSSQL);
rootMetadataProvider.SetupGet(x => x.EntityToDatabaseObject).Returns(new Dictionary<string, DatabaseObject>());

Mock<ISqlMetadataProvider> linkedMetadataProvider = new();
linkedMetadataProvider.Setup(x => x.GetDatabaseType()).Returns(DatabaseType.MSSQL);
linkedMetadataProvider.Setup(x => x.GetSourceDefinition(entityName)).Returns(new SourceDefinition());
linkedMetadataProvider.SetupGet(x => x.EntityToDatabaseObject)
.Returns(new Dictionary<string, DatabaseObject>
{
[entityName] = new DatabaseTable("dbo", "LinkedTable")
});

Mock<IMetadataProviderFactory> metadataProviderFactory = new();
metadataProviderFactory.Setup(x => x.GetMetadataProvider(rootDataSourceName)).Returns(rootMetadataProvider.Object);
metadataProviderFactory.Setup(x => x.GetMetadataProvider(linkedDataSourceName)).Returns(linkedMetadataProvider.Object);

Mock<IQueryBuilder> queryBuilder = new();
queryBuilder.Setup(x => x.Build(It.IsAny<SqlDeleteStructure>())).Returns("DELETE");
Mock<IQueryExecutor> queryExecutor = new();
queryExecutor.Setup(x => x.ExecuteQueryAsync<Dictionary<string, object>>(
It.IsAny<string>(),
It.IsAny<IDictionary<string, DbConnectionParam>>(),
It.IsAny<Func<DbDataReader, List<string>?, Task<Dictionary<string, object>>>>(),
It.IsAny<string>(),
It.IsAny<HttpContext?>(),
It.IsAny<List<string>?>()))
.ReturnsAsync(new Dictionary<string, object> { [nameof(DbDataReader.RecordsAffected)] = 1 });
Mock<IAbstractQueryManagerFactory> queryManagerFactory = new();
queryManagerFactory.Setup(x => x.GetQueryBuilder(DatabaseType.MSSQL)).Returns(queryBuilder.Object);
queryManagerFactory.Setup(x => x.GetQueryExecutor(DatabaseType.MSSQL)).Returns(queryExecutor.Object);

Mock<IHttpContextAccessor> accessor = new();
DefaultHttpContext httpContext = new();
httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER] = "anonymous";
accessor.Setup(x => x.HttpContext).Returns(httpContext);
Mock<IAuthorizationResolver> authorization = new();
authorization.Setup(x => x.ResolveDBPolicy(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<EntityActionOperation>(),
It.IsAny<HttpContext>()))
.Returns(ResolvedDatabasePolicy.Empty);

SqlMutationEngine engine = new(
queryManagerFactory.Object,
metadataProviderFactory.Object,
Mock.Of<IQueryEngineFactory>(),
authorization.Object,
null!,
accessor.Object,
runtimeConfigProvider);

IActionResult? result = await engine.ExecuteAsync(
new DeleteRequestContext(entityName, new DatabaseTable("dbo", "LinkedTable"), isList: false));

Assert.IsInstanceOfType<NoContentResult>(result);
metadataProviderFactory.Verify(x => x.GetMetadataProvider(linkedDataSourceName), Times.Once);
metadataProviderFactory.Verify(x => x.GetMetadataProvider(rootDataSourceName), Times.Never);
}

/// <summary>
/// Verifies empty linking-table and current-entity insert results produce their distinct failure statuses.
/// </summary>
Expand Down
Loading