Skip to content
Open
291 changes: 291 additions & 0 deletions src/Cli.Tests/AutoentitiesConfigureTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Cli.Tests;

/// <summary>
/// Tests for the autoentities-configure CLI command.
/// </summary>
[TestClass]
public class AutoentitiesConfigureTests
{
private IFileSystem? _fileSystem;
private FileSystemRuntimeConfigLoader? _runtimeConfigLoader;

[TestInitialize]
public void TestInitialize()
{
_fileSystem = FileSystemUtils.ProvisionMockFileSystem();
_runtimeConfigLoader = new FileSystemRuntimeConfigLoader(_fileSystem);

ILoggerFactory loggerFactory = TestLoggerSupport.ProvisionLoggerFactory();
ConfigGenerator.SetLoggerForCliConfigGenerator(loggerFactory.CreateLogger<ConfigGenerator>());
SetCliUtilsLogger(loggerFactory.CreateLogger<Utils>());
}

[TestCleanup]
public void TestCleanup()
{
_fileSystem = null;
_runtimeConfigLoader = null;
}

/// <summary>
/// Tests that a new autoentities definition is successfully created with patterns.
/// </summary>
[TestMethod]
public void TestCreateAutoentitiesDefinition_WithPatterns()
{
// Arrange
InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE);
Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!));

AutoentitiesConfigureOptions options = new(
definitionName: "test-def",
patternsInclude: new[] { "dbo.%", "sys.%" },
patternsExclude: new[] { "dbo.internal%" },
patternsName: "{schema}_{table}",
config: TEST_RUNTIME_CONFIG_FILE
);

// Act
bool success = ConfigGenerator.TryConfigureAutoentities(options, _runtimeConfigLoader!, _fileSystem!);

// Assert
Assert.IsTrue(success);
Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? config));
Assert.IsNotNull(config.Autoentities);
Assert.IsTrue(config.Autoentities.AutoEntities.ContainsKey("test-def"));

Autoentity autoentity = config.Autoentities.AutoEntities["test-def"];
Assert.AreEqual(2, autoentity.Patterns.Include.Length);
Assert.AreEqual("dbo.%", autoentity.Patterns.Include[0]);
Assert.AreEqual("sys.%", autoentity.Patterns.Include[1]);
Assert.AreEqual(1, autoentity.Patterns.Exclude.Length);
Assert.AreEqual("dbo.internal%", autoentity.Patterns.Exclude[0]);
Assert.AreEqual("{schema}_{table}", autoentity.Patterns.Name);
}

/// <summary>
/// Tests that template options are correctly configured for an autoentities definition.
/// </summary>
[TestMethod]
public void TestConfigureAutoentitiesDefinition_WithTemplateOptions()
{
// Arrange
InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE);
Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!));

AutoentitiesConfigureOptions options = new(
definitionName: "test-def",
templateRestEnabled: true,
templateGraphqlEnabled: false,
templateMcpDmlTool: "true",
templateCacheEnabled: true,
templateCacheTtlSeconds: 30,
templateCacheLevel: "L1",
templateHealthEnabled: true,
config: TEST_RUNTIME_CONFIG_FILE
);

// Act
bool success = ConfigGenerator.TryConfigureAutoentities(options, _runtimeConfigLoader!, _fileSystem!);

// Assert
Assert.IsTrue(success);
Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? config));

Autoentity autoentity = config.Autoentities!.AutoEntities["test-def"];
Assert.IsTrue(autoentity.Template.Rest.Enabled);
Assert.IsFalse(autoentity.Template.GraphQL.Enabled);
Assert.IsTrue(autoentity.Template.Mcp!.DmlToolEnabled);
Assert.AreEqual(true, autoentity.Template.Cache.Enabled);
Assert.AreEqual(30, autoentity.Template.Cache.TtlSeconds);
Assert.AreEqual(EntityCacheLevel.L1, autoentity.Template.Cache.Level);
Assert.IsTrue(autoentity.Template.Health.Enabled);
}
Comment on lines 73 to 106
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unit tests validate Template.Mcp.DmlToolEnabled on the in-memory object, but they don’t assert the actual JSON persisted to disk. Given EntityMcpOptions can serialize as a boolean shorthand, it would be good to add an assertion that the written config contains autoentities.<def>.template.mcp as an object with a dml-tools property (matching the schema), and that GraphQL template serialization only writes enabled (no type). This would catch regressions in the converters.

Copilot uses AI. Check for mistakes.

/// <summary>
/// Tests that an existing autoentities definition is successfully updated.
/// </summary>
[TestMethod]
public void TestUpdateExistingAutoentitiesDefinition()
{
// Arrange
InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE);
Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!));

// Create initial definition
AutoentitiesConfigureOptions initialOptions = new(
definitionName: "test-def",
patternsInclude: new[] { "dbo.%" },
templateCacheTtlSeconds: 10,
permissions: new[] { "anonymous", "read" },
config: TEST_RUNTIME_CONFIG_FILE
);
Assert.IsTrue(ConfigGenerator.TryConfigureAutoentities(initialOptions, _runtimeConfigLoader!, _fileSystem!));

// Update definition
AutoentitiesConfigureOptions updateOptions = new(
definitionName: "test-def",
patternsExclude: new[] { "dbo.internal%" },
templateCacheTtlSeconds: 60,
permissions: new[] { "authenticated", "create,read,update,delete" },
config: TEST_RUNTIME_CONFIG_FILE
);

// Act
bool success = ConfigGenerator.TryConfigureAutoentities(updateOptions, _runtimeConfigLoader!, _fileSystem!);

// Assert
Assert.IsTrue(success);
Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? config));

Autoentity autoentity = config.Autoentities!.AutoEntities["test-def"];
// Include should remain from initial setup
Assert.AreEqual(1, autoentity.Patterns.Include.Length);
Assert.AreEqual("dbo.%", autoentity.Patterns.Include[0]);
// Exclude should be added
Assert.AreEqual(1, autoentity.Patterns.Exclude.Length);
Assert.AreEqual("dbo.internal%", autoentity.Patterns.Exclude[0]);
// Cache TTL should be updated
Assert.AreEqual(60, autoentity.Template.Cache.TtlSeconds);
// Permissions should be replaced
Assert.AreEqual(1, autoentity.Permissions.Length);
Assert.AreEqual("authenticated", autoentity.Permissions[0].Role);
}

/// <summary>
/// Tests that permissions are correctly parsed and applied.
/// </summary>
[TestMethod]
public void TestConfigureAutoentitiesDefinition_WithMultipleActions()
{
// Arrange
InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE);
Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!));

AutoentitiesConfigureOptions options = new(
definitionName: "test-def",
permissions: new[] { "authenticated", "create,read,update,delete" },
config: TEST_RUNTIME_CONFIG_FILE
);

// Act
bool success = ConfigGenerator.TryConfigureAutoentities(options, _runtimeConfigLoader!, _fileSystem!);

// Assert
Assert.IsTrue(success);
Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? config));

Autoentity autoentity = config.Autoentities!.AutoEntities["test-def"];
Assert.AreEqual(1, autoentity.Permissions.Length);
Assert.AreEqual("authenticated", autoentity.Permissions[0].Role);
Assert.AreEqual(4, autoentity.Permissions[0].Actions.Length);
}

/// <summary>
/// Tests that invalid MCP dml-tool value is handled correctly.
/// </summary>
[TestMethod]
public void TestConfigureAutoentitiesDefinition_InvalidMcpDmlTool()
{
// Arrange
InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE);
Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!));

AutoentitiesConfigureOptions options = new(
definitionName: "test-def",
templateMcpDmlTool: "invalid-value",
permissions: new[] { "anonymous", "read" },
config: TEST_RUNTIME_CONFIG_FILE
);

// Act
bool success = ConfigGenerator.TryConfigureAutoentities(options, _runtimeConfigLoader!, _fileSystem!);

// Assert - Should fail due to invalid MCP value
Assert.IsFalse(success);
}

/// <summary>
/// Tests that invalid cache level value is handled correctly.
/// </summary>
[TestMethod]
public void TestConfigureAutoentitiesDefinition_InvalidCacheLevel()
{
// Arrange
InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE);
Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!));

AutoentitiesConfigureOptions options = new(
definitionName: "test-def",
templateCacheLevel: "InvalidLevel",
permissions: new[] { "anonymous", "read" },
config: TEST_RUNTIME_CONFIG_FILE
);

// Act
bool success = ConfigGenerator.TryConfigureAutoentities(options, _runtimeConfigLoader!, _fileSystem!);

// Assert - Should fail due to invalid cache level
Assert.IsFalse(success);
}

/// <summary>
/// Tests that multiple autoentities definitions can coexist.
/// </summary>
[TestMethod]
public void TestMultipleAutoentitiesDefinitions()
{
// Arrange
InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE);
Assert.IsTrue(ConfigGenerator.TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!));

// Create first definition
AutoentitiesConfigureOptions options1 = new(
definitionName: "def-1",
patternsInclude: new[] { "dbo.%" },
permissions: new[] { "anonymous", "read" },
config: TEST_RUNTIME_CONFIG_FILE
);
Assert.IsTrue(ConfigGenerator.TryConfigureAutoentities(options1, _runtimeConfigLoader!, _fileSystem!));

// Create second definition
AutoentitiesConfigureOptions options2 = new(
definitionName: "def-2",
patternsInclude: new[] { "sys.%" },
permissions: new[] { "authenticated", "*" },
config: TEST_RUNTIME_CONFIG_FILE
);

// Act
bool success = ConfigGenerator.TryConfigureAutoentities(options2, _runtimeConfigLoader!, _fileSystem!);

// Assert
Assert.IsTrue(success);
Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? config));
Assert.AreEqual(2, config.Autoentities!.AutoEntities.Count);
Assert.IsTrue(config.Autoentities.AutoEntities.ContainsKey("def-1"));
Assert.IsTrue(config.Autoentities.AutoEntities.ContainsKey("def-2"));
}

/// <summary>
/// Tests that attempting to configure autoentities without a config file fails.
/// </summary>
[TestMethod]
public void TestConfigureAutoentitiesDefinition_NoConfigFile()
{
// Arrange
AutoentitiesConfigureOptions options = new(
definitionName: "test-def",
permissions: new[] { "anonymous", "read" }
);

// Act
bool success = ConfigGenerator.TryConfigureAutoentities(options, _runtimeConfigLoader!, _fileSystem!);

// Assert
Assert.IsFalse(success);
}
}
104 changes: 104 additions & 0 deletions src/Cli/Commands/AutoentitiesConfigureOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.IO.Abstractions;
using Azure.DataApiBuilder.Config;
using Azure.DataApiBuilder.Product;
using Cli.Constants;
using CommandLine;
using Microsoft.Extensions.Logging;
using static Cli.Utils;
using ILogger = Microsoft.Extensions.Logging.ILogger;

namespace Cli.Commands
{
/// <summary>
/// AutoentitiesConfigureOptions command options
/// This command will be used to configure autoentities definitions in the config file.
/// </summary>
[Verb("autoentities-configure", isDefault: false, HelpText = "Configure autoentities definitions", Hidden = false)]
public class AutoentitiesConfigureOptions : Options
{
public AutoentitiesConfigureOptions(
string definitionName,
IEnumerable<string>? patternsInclude = null,
IEnumerable<string>? patternsExclude = null,
string? patternsName = null,
string? templateMcpDmlTool = null,
bool? templateRestEnabled = null,
bool? templateGraphqlEnabled = null,
bool? templateCacheEnabled = null,
int? templateCacheTtlSeconds = null,
string? templateCacheLevel = null,
bool? templateHealthEnabled = null,
IEnumerable<string>? permissions = null,
string? config = null)
: base(config)
{
DefinitionName = definitionName;
PatternsInclude = patternsInclude;
PatternsExclude = patternsExclude;
PatternsName = patternsName;
TemplateMcpDmlTool = templateMcpDmlTool;
TemplateRestEnabled = templateRestEnabled;
TemplateGraphqlEnabled = templateGraphqlEnabled;
TemplateCacheEnabled = templateCacheEnabled;
TemplateCacheTtlSeconds = templateCacheTtlSeconds;
TemplateCacheLevel = templateCacheLevel;
TemplateHealthEnabled = templateHealthEnabled;
Permissions = permissions;
}

[Value(0, Required = true, HelpText = "Name of the autoentities definition to configure.")]
public string DefinitionName { get; }

[Option("patterns.include", Required = false, HelpText = "T-SQL LIKE pattern(s) to include database objects. Space-separated array of patterns.")]
public IEnumerable<string>? PatternsInclude { get; }

[Option("patterns.exclude", Required = false, HelpText = "T-SQL LIKE pattern(s) to exclude database objects. Space-separated array of patterns.")]
public IEnumerable<string>? PatternsExclude { get; }

[Option("patterns.name", Required = false, HelpText = "Interpolation syntax for entity naming (must be unique for each generated entity).")]
public string? PatternsName { get; }

[Option("template.mcp.dml-tool", Required = false, HelpText = "Enable/disable DML tools for generated entities. Allowed values: true, false.")]
public string? TemplateMcpDmlTool { get; }

[Option("template.rest.enabled", Required = false, HelpText = "Enable/disable REST endpoint for generated entities. Allowed values: true, false.")]
public bool? TemplateRestEnabled { get; }

[Option("template.graphql.enabled", Required = false, HelpText = "Enable/disable GraphQL endpoint for generated entities. Allowed values: true, false.")]
public bool? TemplateGraphqlEnabled { get; }

[Option("template.cache.enabled", Required = false, HelpText = "Enable/disable cache for generated entities. Allowed values: true, false.")]
public bool? TemplateCacheEnabled { get; }

[Option("template.cache.ttl-seconds", Required = false, HelpText = "Cache time-to-live in seconds for generated entities.")]
public int? TemplateCacheTtlSeconds { get; }

[Option("template.cache.level", Required = false, HelpText = "Cache level for generated entities. Allowed values: L1, L1L2.")]
public string? TemplateCacheLevel { get; }

[Option("template.health.enabled", Required = false, HelpText = "Enable/disable health check for generated entities. Allowed values: true, false.")]
public bool? TemplateHealthEnabled { get; }

[Option("permissions", Required = false, Separator = ':', HelpText = "Permissions for generated entities in the format role:actions (e.g., anonymous:read).")]
public IEnumerable<string>? Permissions { get; }

public int Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
{
logger.LogInformation("{productName} {version}", PRODUCT_NAME, ProductInfo.GetProductVersion());
bool isSuccess = ConfigGenerator.TryConfigureAutoentities(this, loader, fileSystem);
if (isSuccess)
{
logger.LogInformation("Successfully configured autoentities definition: {DefinitionName}.", DefinitionName);
return CliReturnCode.SUCCESS;
}
else
{
logger.LogError("Failed to configure autoentities definition: {DefinitionName}.", DefinitionName);
return CliReturnCode.GENERAL_ERROR;
}
}
}
}
Loading