diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 70d5c4be29..84f8e5cfbd 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -819,15 +819,23 @@ "additionalProperties": false, "properties": { "mcp": { - "type": "object", - "description": "MCP endpoint configuration", - "additionalProperties": false, - "properties": { - "dml-tools": { + "oneOf": [ + { "$ref": "#/$defs/boolean-or-string", - "description": "Enable/disable all DML tools with default settings." + "description": "Boolean shorthand: true enables dml-tools only (custom-tool remains false), false disables all MCP functionality." + }, + { + "type": "object", + "description": "MCP endpoint configuration", + "additionalProperties": false, + "properties": { + "dml-tools": { + "$ref": "#/$defs/boolean-or-string", + "description": "Enable/disable all DML tools with default settings." + } + } } - } + ] }, "rest": { "type": "object", diff --git a/src/Cli.Tests/AutoConfigTests.cs b/src/Cli.Tests/AutoConfigTests.cs index 40e3a461f7..7bd5487305 100644 --- a/src/Cli.Tests/AutoConfigTests.cs +++ b/src/Cli.Tests/AutoConfigTests.cs @@ -80,7 +80,7 @@ public void TestConfigureAutoentitiesDefinition_WithTemplateOptions() definitionName: "test-def", templateRestEnabled: true, templateGraphqlEnabled: false, - templateMcpDmlTool: "true", + templateMcpDmlTools: "true", templateCacheEnabled: true, templateCacheTtlSeconds: 30, templateCacheLevel: "L1", @@ -196,7 +196,7 @@ public void TestConfigureAutoentitiesDefinition_InvalidMcpDmlTool() AutoConfigOptions options = new( definitionName: "test-def", - templateMcpDmlTool: "invalid-value", + templateMcpDmlTools: "invalid-value", permissions: new[] { "anonymous", "read" }, config: TEST_RUNTIME_CONFIG_FILE ); diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs index 4ae27f790f..d5bd937fe9 100644 --- a/src/Cli.Tests/EndToEndTests.cs +++ b/src/Cli.Tests/EndToEndTests.cs @@ -3,6 +3,7 @@ using Azure.DataApiBuilder.Config.Converters; using Azure.DataApiBuilder.Product; +using Azure.DataApiBuilder.Service; using Cli.Constants; using Microsoft.Data.SqlClient; @@ -857,6 +858,83 @@ public void TestEngineStartUpWithVerboseAndLogLevelOptions(string logLevelOption StringAssert.Contains(output, $"User provided config file: {TEST_RUNTIME_CONFIG_FILE}", StringComparison.Ordinal); } + /// + /// Validates that `dab start` correctly sets + /// based on whether the --LogLevel CLI flag is provided. + /// + /// When the --LogLevel flag is provided, IsLogLevelOverriddenByCli should be true. + /// When the --LogLevel flag is omitted (log level comes from the config file), IsLogLevelOverriddenByCli should be false. + /// + /// The --LogLevel CLI flag value, or null to omit the flag. + /// Expected value of Startup.IsLogLevelOverriddenByCli. + [DataTestMethod] + [DataRow(null, false, DisplayName = "IsLogLevelOverriddenByCli is false")] + [DataRow(LogLevel.Error, true, DisplayName = "IsLogLevelOverriddenByCli is true")] + public async Task TestStartCommandResolvesLogLevelFromConfigOrFlag( + LogLevel? cliLogLevel, + bool expectedIsOverridden) + { + string baseConfig = @" + { + ""$schema"": """ + DAB_DRAFT_SCHEMA_TEST_PATH + @""", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": """ + SAMPLE_TEST_CONN_STRING + @""" + }, + ""runtime"": { + ""rest"": { + ""path"": ""/api"", + ""enabled"": true + }, + ""graphql"": { + ""path"": ""/graphql"", + ""enabled"": true, + ""allow-introspection"": true + }, + ""host"": { + ""mode"": ""development"", + ""cors"": { + ""origins"": [], + ""allow-credentials"": false + }, + ""authentication"": { + ""provider"": ""Unauthenticated"" + } + }, + ""telemetry"": { + ""log-level"": { + ""Azure.DataApiBuilder.Core.Services.ISqlMetadataProvider"": ""Information"", + ""Azure.DataApiBuilder.Core"": ""Debug"", + ""Azure.DataApiBuilder.Service.Controllers.RestController"": ""Error"", + ""default"": ""Warning"" + } + } + }, + ""entities"": {} + }"; + + // Merge in an entity so the config is not rejected for having an empty entities section. + string configWithLogLevel = AddPropertiesToJson(baseConfig, BASIC_ENTITY_WITH_ANONYMOUS_ROLE); + _fileSystem!.File.WriteAllText(TEST_RUNTIME_CONFIG_FILE, configWithLogLevel); + + StartOptions options = new( + verbose: false, + logLevel: cliLogLevel, + isHttpsRedirectionDisabled: false, + mcpStdio: false, + mcpRole: null, + config: TEST_RUNTIME_CONFIG_FILE); + + // Run TryStartEngineWithOptions on a background task because StartEngine blocks until the host shuts down. + Task engineTask = Task.Run(() => + TryStartEngineWithOptions(options, _runtimeConfigLoader!, _fileSystem!)); + + // Wait for the engine to finish loading the config. + await Task.Delay(TimeSpan.FromSeconds(5)); + + Assert.AreEqual(expectedIsOverridden, Startup.IsLogLevelOverriddenByCli); + } + /// /// Validates that valid usage of verbs and associated options produce exit code 0 (CliReturnCode.SUCCESS). /// Verifies that explicitly implemented verbs (add, update, init, start) and appropriately diff --git a/src/Cli.Tests/ModuleInitializer.cs b/src/Cli.Tests/ModuleInitializer.cs index 0bbb5b3eab..3a10eeffde 100644 --- a/src/Cli.Tests/ModuleInitializer.cs +++ b/src/Cli.Tests/ModuleInitializer.cs @@ -65,6 +65,8 @@ public static void Init() VerifierSettings.IgnoreMember(entity => entity.EntityFirst); // Ignore the entity IsLinkingEntity as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(entity => entity.IsLinkingEntity); + // Ignore the entity IsAutoentity as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(entity => entity.IsAutoentity); // Ignore the UserProvidedTtlOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter. VerifierSettings.IgnoreMember(cacheOptions => cacheOptions.UserProvidedTtlOptions); // Ignore the UserProvidedEnabledOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter. diff --git a/src/Cli/Commands/AutoConfigOptions.cs b/src/Cli/Commands/AutoConfigOptions.cs index 41227cd03a..41be943303 100644 --- a/src/Cli/Commands/AutoConfigOptions.cs +++ b/src/Cli/Commands/AutoConfigOptions.cs @@ -24,7 +24,7 @@ public AutoConfigOptions( IEnumerable? patternsInclude = null, IEnumerable? patternsExclude = null, string? patternsName = null, - string? templateMcpDmlTool = null, + string? templateMcpDmlTools = null, bool? templateRestEnabled = null, bool? templateGraphqlEnabled = null, bool? templateCacheEnabled = null, @@ -39,7 +39,7 @@ public AutoConfigOptions( PatternsInclude = patternsInclude; PatternsExclude = patternsExclude; PatternsName = patternsName; - TemplateMcpDmlTool = templateMcpDmlTool; + TemplateMcpDmlTools = templateMcpDmlTools; TemplateRestEnabled = templateRestEnabled; TemplateGraphqlEnabled = templateGraphqlEnabled; TemplateCacheEnabled = templateCacheEnabled; @@ -61,8 +61,8 @@ public AutoConfigOptions( [Option("patterns.name", Required = false, HelpText = "Interpolation syntax for entity naming (must be unique for each generated entity). Default: '{object}'")] public string? PatternsName { get; } - [Option("template.mcp.dml-tool", Required = false, HelpText = "Enable/disable DML tools for generated entities. Allowed values: true, false. Default: true")] - public string? TemplateMcpDmlTool { get; } + [Option("template.mcp.dml-tools", Required = false, HelpText = "Enable/disable DML tools for generated entities. Allowed values: true, false. Default: true")] + public string? TemplateMcpDmlTools { get; } [Option("template.rest.enabled", Required = false, HelpText = "Enable/disable REST endpoint for generated entities. Allowed values: true, false. Default: true")] public bool? TemplateRestEnabled { get; } diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index 8cec9dd239..af98be05ff 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -2596,18 +2596,10 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun } minimumLogLevel = (LogLevel)options.LogLevel; + args.Add("--LogLevel"); + args.Add(minimumLogLevel.ToString()); _logger.LogInformation("Setting minimum LogLevel: {minimumLogLevel}.", minimumLogLevel); } - else - { - minimumLogLevel = deserializedRuntimeConfig.GetConfiguredLogLevel(); - HostMode hostModeType = deserializedRuntimeConfig.IsDevelopmentMode() ? HostMode.Development : HostMode.Production; - - _logger.LogInformation($"Setting default minimum LogLevel: {minimumLogLevel} for {hostModeType} mode.", minimumLogLevel, hostModeType); - } - - args.Add("--LogLevel"); - args.Add(minimumLogLevel.ToString()); // This will add args to disable automatic redirects to https if specified by user if (options.IsHttpsRedirectionDisabled) @@ -3109,11 +3101,11 @@ private static AutoentityPatterns BuildAutoentityPatterns(AutoConfigOptions opti bool userProvidedCache = existingAutoentity?.Template.UserProvidedCacheOptions ?? false; // Update MCP options - if (!string.IsNullOrWhiteSpace(options.TemplateMcpDmlTool)) + if (!string.IsNullOrWhiteSpace(options.TemplateMcpDmlTools)) { - if (!bool.TryParse(options.TemplateMcpDmlTool, out bool mcpDmlToolValue)) + if (!bool.TryParse(options.TemplateMcpDmlTools, out bool mcpDmlToolValue)) { - _logger.LogError("Invalid value for template.mcp.dml-tool: {value}. Valid values are: true, false", options.TemplateMcpDmlTool); + _logger.LogError("Invalid value for template.mcp.dml-tools: {value}. Valid values are: true, false", options.TemplateMcpDmlTools); return null; } @@ -3122,7 +3114,7 @@ private static AutoentityPatterns BuildAutoentityPatterns(AutoConfigOptions opti bool? dmlToolValue = mcpDmlToolValue; mcp = new EntityMcpOptions(customToolEnabled: customToolEnabled, dmlToolsEnabled: dmlToolValue); userProvidedMcp = true; - _logger.LogInformation("Updated template.mcp.dml-tool for definition '{DefinitionName}'", options.DefinitionName); + _logger.LogInformation("Updated template.mcp.dml-tools for definition '{DefinitionName}'", options.DefinitionName); } // Update REST options @@ -3268,7 +3260,7 @@ public static bool TrySimulateAutoentities(AutoConfigSimulateOptions options, Fi if (runtimeConfig.DataSource.DatabaseType != DatabaseType.MSSQL) { - _logger.LogError("Autoentities simulation is only supported for MSSQL databases. Current database type: {DatabaseType}.", runtimeConfig.DataSource.DatabaseType); + _logger.LogError("The autoentities simulation is only supported for MSSQL databases. Current database type: {DatabaseType}.", runtimeConfig.DataSource.DatabaseType); return false; } @@ -3360,7 +3352,7 @@ public static bool TrySimulateAutoentities(AutoConfigSimulateOptions options, Fi /// The simulation results keyed by filter (definition) name. private static void WriteSimulationResultsToConsole(Dictionary> results) { - Console.WriteLine("AutoEntities Simulation Results"); + Console.WriteLine("Autoentities Simulation Results"); Console.WriteLine(); foreach ((string filterName, List<(string EntityName, string SchemaName, string ObjectName)> matches) in results) diff --git a/src/Config/ObjectModel/Entity.cs b/src/Config/ObjectModel/Entity.cs index d67a1f9f28..9c9ba17f2c 100644 --- a/src/Config/ObjectModel/Entity.cs +++ b/src/Config/ObjectModel/Entity.cs @@ -45,6 +45,9 @@ public record Entity [JsonIgnore] public bool IsLinkingEntity { get; init; } + [JsonIgnore] + public bool IsAutoentity { get; init; } + [JsonConstructor] public Entity( EntitySource Source, @@ -58,7 +61,8 @@ public Entity( bool IsLinkingEntity = false, EntityHealthCheckConfig? Health = null, string? Description = null, - EntityMcpOptions? Mcp = null) + EntityMcpOptions? Mcp = null, + bool IsAutoentity = false) { this.Health = Health; this.Source = Source; @@ -72,6 +76,7 @@ public Entity( this.IsLinkingEntity = IsLinkingEntity; this.Description = Description; this.Mcp = Mcp; + this.IsAutoentity = IsAutoentity; } /// diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 89cc7413d1..54c3e77556 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -269,6 +269,11 @@ public bool TryAddGeneratedAutoentityNameToDataSourceName(string entityName, str return false; } + public bool RemoveGeneratedAutoentityNameFromDataSourceName(string entityName) + { + return _entityNameToDataSourceName.Remove(entityName); + } + /// /// Constructor for runtimeConfig. /// To be used when setting up from cli json scenario. @@ -502,7 +507,7 @@ public string GetDataSourceNameFromAutoentityName(string autoentityName) if (!_autoentityNameToDataSourceName.TryGetValue(autoentityName, out string? autoentityDataSource)) { throw new DataApiBuilderException( - message: $"{autoentityName} is not a valid autoentity.", + message: $"'{autoentityName}' is not a valid autoentities definition.", statusCode: HttpStatusCode.NotFound, subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); } diff --git a/src/Core/Configurations/RuntimeConfigProvider.cs b/src/Core/Configurations/RuntimeConfigProvider.cs index 644782e2cc..70327326ba 100644 --- a/src/Core/Configurations/RuntimeConfigProvider.cs +++ b/src/Core/Configurations/RuntimeConfigProvider.cs @@ -426,4 +426,32 @@ public void AddMergedEntitiesToConfig(Dictionary newEntities) }; _configLoader.EditRuntimeConfig(newRuntimeConfig); } + + public void RemoveGeneratedAutoentitiesFromConfig() + { + Dictionary entities = new(_configLoader.RuntimeConfig!.Entities); + List removingEntities = new(); + + // Add entities that will be removed to a list first to avoid modifying the collection while iterating over it. + foreach ((string name, Entity entity) in entities) + { + if (entity.IsAutoentity) + { + removingEntities.Add(name); + } + } + + // Remove all autoentities from the config. + foreach (string name in removingEntities) + { + entities.Remove(name); + _configLoader.RuntimeConfig!.RemoveGeneratedAutoentityNameFromDataSourceName(name); + } + + RuntimeConfig newRuntimeConfig = _configLoader.RuntimeConfig! with + { + Entities = new(entities) + }; + _configLoader.EditRuntimeConfig(newRuntimeConfig); + } } diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index 96fa47dcfd..23d12ec31f 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -305,7 +305,7 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona foreach ((string autoentityName, Autoentity autoentity) in autoentities) { int addedEntities = 0; - JsonArray? resultArray = await QueryAutoentitiesAsync(autoentity); + JsonArray? resultArray = await QueryAutoentitiesAsync(autoentityName, autoentity); if (resultArray is null) { continue; @@ -316,7 +316,7 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona if (resultObject is null) { throw new DataApiBuilderException( - message: $"Cannot create new entity from autoentity pattern due to an internal error.", + message: $"Cannot create new entity from autoentities definition '{autoentityName}' due to an internal error.", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); } @@ -329,7 +329,7 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona if (string.IsNullOrWhiteSpace(entityName) || string.IsNullOrWhiteSpace(objectName) || string.IsNullOrWhiteSpace(schemaName)) { - _logger.LogError("Skipping autoentity generation: entity_name or object is null or empty for autoentity pattern '{AutoentityName}'.", autoentityName); + _logger.LogError("Skipping autoentity generation: 'entity_name', 'object', or 'schema' is null or empty for autoentities definition '{autoentityName}'.", autoentityName); continue; } @@ -349,14 +349,15 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona Health: autoentity.Template.Health, Fields: null, Relationships: null, - Mappings: new()); + Mappings: new(), + IsAutoentity: true); // Add the generated entity to the linking entities dictionary. // This allows the entity to be processed later during metadata population. if (!entities.TryAdd(entityName, generatedEntity) || !runtimeConfig.TryAddGeneratedAutoentityNameToDataSourceName(entityName, autoentityName)) { throw new DataApiBuilderException( - message: $"Entity with name '{entityName}' already exists. Cannot create new entity from autoentity pattern with definition-name '{autoentityName}'.", + message: $"Entity '{entityName}' conflicts with autoentity pattern '{autoentityName}'. Use --patterns.exclude to skip it.", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); } @@ -375,14 +376,14 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona if (addedEntities == 0) { - _logger.LogWarning("No new entities were generated from the autoentity {autoentityName} defined in the configuration.", autoentityName); + _logger.LogWarning("No new entities were generated from the autoentities definition '{autoentityName}'.", autoentityName); } } _runtimeConfigProvider.AddMergedEntitiesToConfig(entities); } - public async Task QueryAutoentitiesAsync(Autoentity autoentity) + public async Task QueryAutoentitiesAsync(string autoentityName, Autoentity autoentity) { string include = string.Join(",", autoentity.Patterns.Include); string exclude = string.Join(",", autoentity.Patterns.Exclude); @@ -395,10 +396,10 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona { $"{BaseQueryStructure.PARAM_NAME_PREFIX}name_pattern", new(namePattern, null, SqlDbType.NVarChar) } }; - _logger.LogInformation("Query for Autoentities is being executed with the following parameters."); - _logger.LogInformation($"Autoentities include pattern: {include}"); - _logger.LogInformation($"Autoentities exclude pattern: {exclude}"); - _logger.LogInformation($"Autoentities name pattern: {namePattern}"); + _logger.LogDebug("Query for autoentities is being executed with the following parameters."); + _logger.LogDebug("The autoentities definition '{autoentityName}' include pattern: {include}", autoentityName, include); + _logger.LogDebug("The autoentities definition '{autoentityName}' exclude pattern: {exclude}", autoentityName, exclude); + _logger.LogDebug("The autoentities definition '{autoentityName}' name pattern: {namePattern}", autoentityName, namePattern); JsonArray? resultArray = await QueryExecutor.ExecuteQueryAsync( sqltext: getAutoentitiesQuery, diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 6aa2712468..951b5984e4 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -353,6 +353,12 @@ public async Task InitializeAsync() GenerateRestPathToEntityMap(); InitODataParser(); + + if (_isValidateOnly) + { + RemoveGeneratedAutoentities(); + } + timer.Stop(); _logger.LogTrace($"Done inferring Sql database schema in {timer.ElapsedMilliseconds}ms."); } @@ -711,7 +717,16 @@ private void GenerateDatabaseObjectForEntities() /// protected virtual Task GenerateAutoentitiesIntoEntities(IReadOnlyDictionary? autoentities) { - throw new NotSupportedException($"{GetType().Name} does not support Autoentities yet."); + throw new NotSupportedException($"{GetType().Name} does not support autoentities yet."); + } + + /// + /// Removes the entities that were generated from the autoentities property. + /// This should only be done when we only want to validate the entities. + /// + private void RemoveGeneratedAutoentities() + { + _runtimeConfigProvider.RemoveGeneratedAutoentitiesFromConfig(); } protected void PopulateDatabaseObjectForEntity( diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 53a97ae722..e37b0920e2 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -5590,7 +5590,7 @@ public async Task TestAutoentitiesAreGeneratedIntoEntities(bool useEntities, int /// [TestCategory(TestCategory.MSSQL)] [DataTestMethod] - [DataRow("publishers", "uniqueSingularPublisher", "uniquePluralPublishers", "/unique/publisher", "Entity with name 'publishers' already exists. Cannot create new entity from autoentity pattern with definition-name 'PublisherAutoEntity'.", DisplayName = "Autoentities fail due to entity name")] + [DataRow("publishers", "uniqueSingularPublisher", "uniquePluralPublishers", "/unique/publisher", "Entity 'publishers' conflicts with autoentity pattern 'PublisherAutoEntity'. Use --patterns.exclude to skip it.", DisplayName = "Autoentities fail due to entity name")] [DataRow("UniquePublisher", "publishers", "uniquePluralPublishers", "/unique/publisher", "Entity publishers generates queries/mutation that already exist", DisplayName = "Autoentities fail due to graphql singular type")] [DataRow("UniquePublisher", "uniqueSingularPublisher", "publishers", "/unique/publisher", "Entity publishers generates queries/mutation that already exist", DisplayName = "Autoentities fail due to graphql plural type")] [DataRow("UniquePublisher", "uniqueSingularPublisher", "uniquePluralPublishers", "/publishers", "The rest path: publishers specified for entity: publishers is already used by another entity.", DisplayName = "Autoentities fail due to rest path")] diff --git a/src/Service.Tests/Configuration/HotReload/ConfigurationHotReloadTests.cs b/src/Service.Tests/Configuration/HotReload/ConfigurationHotReloadTests.cs index 0f9ff6c1b8..b1770f504d 100644 --- a/src/Service.Tests/Configuration/HotReload/ConfigurationHotReloadTests.cs +++ b/src/Service.Tests/Configuration/HotReload/ConfigurationHotReloadTests.cs @@ -64,6 +64,7 @@ private static void GenerateConfigFile( string entityBackingColumn = "title", string entityExposedName = "title", string mcpEnabled = "true", + string autoentityName = "autoentity_{object}", string configFileName = CONFIG_FILE_NAME) { File.WriteAllText(configFileName, @" @@ -180,6 +181,29 @@ private static void GenerateConfigFile( } ] } + }, + ""autoentities"": { + ""BooksAutoentities"": { + ""patterns"": { + ""include"": [ ""%book%"" ], + ""name"": """ + autoentityName + @""" + }, + ""template"": { + ""rest"": { + ""enabled"": true + } + }, + ""permissions"": [ + { + ""role"": ""anonymous"", + ""actions"": [ + { + ""action"": ""*"" + } + ] + } + ] + } } }"); } @@ -768,6 +792,41 @@ await WaitForConditionAsync( Assert.AreEqual(HttpStatusCode.OK, restResult.StatusCode); } + /// + /// Hot reload the configuration file so that it changes the name of the autoentity properties. + /// Then we assert that the hot reload is successful by sending a request to the newly created autoentity. + /// + [TestCategory(MSSQL_ENVIRONMENT)] + [TestMethod] + public async Task HotReloadAutoentities() + { + // Arrange + _writer = new StringWriter(); + Console.SetOut(_writer); + + // Act + HttpResponseMessage restResult = await _testClient.GetAsync($"rest/autoentity_books"); + + GenerateConfigFile( + connectionString: $"{ConfigurationTests.GetConnectionStringFromEnvironmentConfig(TestCategory.MSSQL).Replace("\\", "\\\\")}", + autoentityName: "HotReload_{object}"); + await WaitForConditionAsync( + () => WriterContains(HOT_RELOAD_SUCCESS_MESSAGE), + TimeSpan.FromSeconds(HOT_RELOAD_TIMEOUT_SECONDS), + TimeSpan.FromMilliseconds(500)); + + HttpResponseMessage failRestResult = await _testClient.GetAsync($"rest/autoentity_books"); + HttpResponseMessage hotReloadRestResult = await _testClient.GetAsync($"rest/HotReload_books"); + + // Assert + Assert.AreEqual(HttpStatusCode.OK, restResult.StatusCode, + $"REST request before hot-reload failed when it was expected to succeed. Response: {await restResult.Content.ReadAsStringAsync()}"); + Assert.AreEqual(HttpStatusCode.NotFound, failRestResult.StatusCode, + $"REST request after hot-reload succeeded when it was expected to fail. Response: {await failRestResult.Content.ReadAsStringAsync()}"); + Assert.AreEqual(HttpStatusCode.OK, hotReloadRestResult.StatusCode, + $"REST request after hot-reload failed when it was expected to succeed. Response: {await hotReloadRestResult.Content.ReadAsStringAsync()}"); + } + /// /// /// (Warning: This test only currently works in the pipeline due to constrains of not /// being able to change from one database type to another, under normal circumstances diff --git a/src/Service.Tests/ModuleInitializer.cs b/src/Service.Tests/ModuleInitializer.cs index 637280b45d..9d8c213b7c 100644 --- a/src/Service.Tests/ModuleInitializer.cs +++ b/src/Service.Tests/ModuleInitializer.cs @@ -69,6 +69,8 @@ public static void Init() VerifierSettings.IgnoreMember(entity => entity.EntityFirst); // Ignore the entity IsLinkingEntity as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(entity => entity.IsLinkingEntity); + // Ignore the entity IsAutoentity as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(entity => entity.IsAutoentity); // Ignore the UserProvidedTtlOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter. VerifierSettings.IgnoreMember(cacheOptions => cacheOptions.UserProvidedTtlOptions); // Ignore the UserProvidedEnabledOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter. diff --git a/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs b/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs index 4c5782b4ca..7242716847 100644 --- a/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs +++ b/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs @@ -634,7 +634,7 @@ public async Task CheckAutoentitiesQuery(string[] include, string[] exclude, str // Act MsSqlMetadataProvider metadataProvider = (MsSqlMetadataProvider)_sqlMetadataProvider; - JsonArray resultArray = await metadataProvider.QueryAutoentitiesAsync(autoentity); + JsonArray resultArray = await metadataProvider.QueryAutoentitiesAsync("autoentity", autoentity); // Assert Assert.IsNotNull(resultArray);