diff --git a/test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsTest.cs b/test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsTest.cs index a423284b7e..9c8157e647 100644 --- a/test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsTest.cs +++ b/test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsTest.cs @@ -12,8 +12,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; -using Microsoft.WebJobs.Script.Tests; - using WebJobs.Script.Tests; using Xunit; @@ -21,54 +19,91 @@ namespace Microsoft.Azure.WebJobs.Script.Tests.Configuration { public class FunctionsHostingConfigOptionsTest { - [Theory] - [InlineData("FEATURE1", "value1")] - [InlineData("FeAtUrE1", "value1")] - [InlineData("feature1", "value1")] - [InlineData("featuree1", null)] - public async Task Case_Insensitive(string key, string expectedValue) + public static IEnumerable AllProperties { - using (TempDirectory tempDir = new TempDirectory()) + get { - IHost host = GetScriptHostBuilder(Path.Combine(tempDir.Path, "settings.txt"), $"feature1=value1,feature2=value2").Build(); - var testService = host.Services.GetService(); - - _ = Task.Run(async () => + var props = typeof(FunctionsHostingConfigOptions).GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(p => p.Name != nameof(FunctionsHostingConfigOptions.Features)); + foreach (var prop in props) { - await TestHelpers.Await(() => - { - return testService.Options.Value.GetFeature(key) == expectedValue; - }); - await host.StopAsync(); - }); - - await host.RunAsync(); - Assert.Equal(testService.Options.Value.GetFeature(key), expectedValue); + yield return new object[] { prop }; + } } } - [Fact] - public async Task Inject_Succeded() + public static IEnumerable PropertyValues { - using (TempDirectory tempDir = new TempDirectory()) + get { - IHost host = GetScriptHostBuilder(Path.Combine(tempDir.Path, "settings.txt"), $"feature1=value1,feature2=value2").Build(); - var testService = host.Services.GetService(); + // Note: For legacy purposes (we used to call Configuration.Bind() on this object), some properties whose ScmHostingConfig key and + // property name match exactly need to support "True/False". + // It is recommended that new properties only look at "1/0" for their setting. + yield return new object[] { nameof(FunctionsHostingConfigOptions.DisableLinuxAppServiceExecutionDetails), "DisableLinuxExecutionDetails=1", true }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.DisableLinuxAppServiceLogBackoff), "DisableLinuxLogBackoff=1", true }; - _ = Task.Run(async () => - { - await TestHelpers.Await(() => - { - return testService.Options.Value.GetFeature("feature1") == "value1"; - }); - await host.StopAsync(); - }); + // Supports True/False/1/0 + yield return new object[] { nameof(FunctionsHostingConfigOptions.EnableOrderedInvocationMessages), "EnableOrderedInvocationMessages=True", true }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.EnableOrderedInvocationMessages), "EnableOrderedInvocationMessages=1", true }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.EnableOrderedInvocationMessages), "EnableOrderedInvocationMessages=unparseable", true }; // default + yield return new object[] { nameof(FunctionsHostingConfigOptions.EnableOrderedInvocationMessages), string.Empty, true }; // default + + yield return new object[] { nameof(FunctionsHostingConfigOptions.FunctionsWorkerDynamicConcurrencyEnabled), "FUNCTIONS_WORKER_DYNAMIC_CONCURRENCY_ENABLED=1", true }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.MaximumBundleV3Version), "FunctionRuntimeV4MaxBundleV3Version=teststring", "teststring" }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.MaximumBundleV4Version), "FunctionRuntimeV4MaxBundleV4Version=teststring", "teststring" }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.RevertWorkerShutdownBehavior), "REVERT_WORKER_SHUTDOWN_BEHAVIOR=1", true }; - await host.RunAsync(); - Assert.Equal(testService.Options.Value.GetFeature("feature1"), "value1"); + // Supports True/False/1/0 + yield return new object[] { nameof(FunctionsHostingConfigOptions.ShutdownWebhostWorkerChannelsOnHostShutdown), "ShutdownWebhostWorkerChannelsOnHostShutdown=False", false }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.ShutdownWebhostWorkerChannelsOnHostShutdown), "ShutdownWebhostWorkerChannelsOnHostShutdown=True", true }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.ShutdownWebhostWorkerChannelsOnHostShutdown), "ShutdownWebhostWorkerChannelsOnHostShutdown=1", true }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.ShutdownWebhostWorkerChannelsOnHostShutdown), "ShutdownWebhostWorkerChannelsOnHostShutdown=unparseable", true }; // default + yield return new object[] { nameof(FunctionsHostingConfigOptions.ShutdownWebhostWorkerChannelsOnHostShutdown), string.Empty, true }; // default + + // Supports True/False/1/0 + yield return new object[] { nameof(FunctionsHostingConfigOptions.SwtIssuerEnabled), "SwtIssuerEnabled=False", false }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.SwtIssuerEnabled), "SwtIssuerEnabled=True", true }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.SwtIssuerEnabled), "SwtIssuerEnabled=0", false }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.SwtIssuerEnabled), "SwtIssuerEnabled=unparseable", true }; //default + yield return new object[] { nameof(FunctionsHostingConfigOptions.SwtIssuerEnabled), string.Empty, true }; // default + + yield return new object[] { nameof(FunctionsHostingConfigOptions.ThrowOnMissingFunctionsWorkerRuntime), "THROW_ON_MISSING_FUNCTIONS_WORKER_RUNTIME=1", true }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.WorkerIndexingDisabledApps), "WORKER_INDEXING_DISABLED_APPS=teststring", "teststring" }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.WorkerIndexingEnabled), "WORKER_INDEXING_ENABLED=1", true }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.WorkerRuntimeStrictValidationEnabled), "WORKER_RUNTIME_STRICT_VALIDATION_ENABLED=1", true }; + + yield return new object[] { nameof(FunctionsHostingConfigOptions.InternalAuthApisAllowList), "InternalAuthApisAllowList=|", "|" }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.InternalAuthApisAllowList), "InternalAuthApisAllowList=/admin/host/foo|/admin/host/bar", "/admin/host/foo|/admin/host/bar" }; + + yield return new object[] { nameof(FunctionsHostingConfigOptions.IsDotNetInProcDisabled), "DotNetInProcDisabled=False", false }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.IsDotNetInProcDisabled), "DotNetInProcDisabled=True", true }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.IsDotNetInProcDisabled), "DotNetInProcDisabled=1", true }; + yield return new object[] { nameof(FunctionsHostingConfigOptions.IsDotNetInProcDisabled), "DotNetInProcDisabled=0", false }; } } + [Theory] + [InlineData("FEATURE1", "value1")] + [InlineData("FeAtUrE1", "value1")] + [InlineData("feature1", "value1")] + [InlineData("featuree1", null)] + public void Case_Insensitive(string key, string expectedValue) + { + using TempDirectory tempDir = new(); + IHost host = GetScriptHostBuilder(Path.Combine(tempDir.Path, "settings.txt"), $"feature1=value1,feature2=value2").Build(); + var testService = host.Services.GetService(); + Assert.Equal(testService.Options.Value.GetFeature(key), expectedValue); + } + + [Fact] + public void Inject_Succeded() + { + using TempDirectory tempDir = new(); + IHost host = GetScriptHostBuilder(Path.Combine(tempDir.Path, "settings.txt"), $"feature1=value1,feature2=value2").Build(); + var testService = host.Services.GetService(); + Assert.Equal(testService.Options.Value.GetFeature("feature1"), "value1"); + } + [Theory] [InlineData("True", false, true)] [InlineData("False", true, false)] @@ -92,166 +127,89 @@ public void GetFeatureAsBooleanOrDefault(string featureValue, bool defaultValue, Assert.Equal(expected, options.GetFeatureAsBooleanOrDefault(feature, defaultValue)); } - [Fact] - public void Property_Validation() + [Theory] + [MemberData(nameof(AllProperties))] + public void Property_ValidateAccess(PropertyInfo prop) { - // Doing this all in one test case (not using Theory) so that we can ensure we have at least one test for every property. - // Note: For legacy purposes (we used to call Configuration.Bind() on this object), some properties whose ScmHostingConfig key and - // property name match exactly need to support "True/False". - // It is recommended that new properties only look at "1/0" for their setting. - var testCases = new List<(string PropertyName, string ConfigValue, object Expected)> - { - (nameof(FunctionsHostingConfigOptions.DisableLinuxAppServiceExecutionDetails), "DisableLinuxExecutionDetails=1", true), - (nameof(FunctionsHostingConfigOptions.DisableLinuxAppServiceLogBackoff), "DisableLinuxLogBackoff=1", true), + // make sure all props are internal to prevent inadverntent binding in the future + Assert.False( + prop.GetGetMethod() is not null || prop.GetSetMethod() is not null, + $"{prop.Name} is public. All properties on this object should be internal."); - // Supports True/False/1/0 - (nameof(FunctionsHostingConfigOptions.EnableOrderedInvocationMessages), "EnableOrderedInvocationMessages=True", true), - (nameof(FunctionsHostingConfigOptions.EnableOrderedInvocationMessages), "EnableOrderedInvocationMessages=1", true), - (nameof(FunctionsHostingConfigOptions.EnableOrderedInvocationMessages), "EnableOrderedInvocationMessages=unparseable", true), // default - (nameof(FunctionsHostingConfigOptions.EnableOrderedInvocationMessages), string.Empty, true), // default + Assert.True(PropertyValues.Any(x => x[0] as string == prop.Name), $"The property {prop.Name} is not set up to be validated. Please add at least one case in this test."); + } - (nameof(FunctionsHostingConfigOptions.FunctionsWorkerDynamicConcurrencyEnabled), "FUNCTIONS_WORKER_DYNAMIC_CONCURRENCY_ENABLED=1", true), - (nameof(FunctionsHostingConfigOptions.MaximumBundleV3Version), "FunctionRuntimeV4MaxBundleV3Version=teststring", "teststring"), - (nameof(FunctionsHostingConfigOptions.MaximumBundleV4Version), "FunctionRuntimeV4MaxBundleV4Version=teststring", "teststring"), - (nameof(FunctionsHostingConfigOptions.RevertWorkerShutdownBehavior), "REVERT_WORKER_SHUTDOWN_BEHAVIOR=1", true), + [Theory] + [MemberData(nameof(PropertyValues))] + public void Property_Validation(string propertyName, string configValue, object expected) + { + using TempDirectory tempDir = new(); - // Supports True/False/1/0 - (nameof(FunctionsHostingConfigOptions.ShutdownWebhostWorkerChannelsOnHostShutdown), "ShutdownWebhostWorkerChannelsOnHostShutdown=False", false), - (nameof(FunctionsHostingConfigOptions.ShutdownWebhostWorkerChannelsOnHostShutdown), "ShutdownWebhostWorkerChannelsOnHostShutdown=True", true), - (nameof(FunctionsHostingConfigOptions.ShutdownWebhostWorkerChannelsOnHostShutdown), "ShutdownWebhostWorkerChannelsOnHostShutdown=1", true), - (nameof(FunctionsHostingConfigOptions.ShutdownWebhostWorkerChannelsOnHostShutdown), "ShutdownWebhostWorkerChannelsOnHostShutdown=unparseable", true), // default - (nameof(FunctionsHostingConfigOptions.ShutdownWebhostWorkerChannelsOnHostShutdown), string.Empty, true), // default + IHost host = GetScriptHostBuilder(Path.Combine(tempDir.Path, "settings.txt"), configValue).Build(); + var testService = host.Services.GetService(); - // Supports True/False/1/0 - (nameof(FunctionsHostingConfigOptions.SwtIssuerEnabled), "SwtIssuerEnabled=False", false), - (nameof(FunctionsHostingConfigOptions.SwtIssuerEnabled), "SwtIssuerEnabled=True", true), - (nameof(FunctionsHostingConfigOptions.SwtIssuerEnabled), "SwtIssuerEnabled=0", false), - (nameof(FunctionsHostingConfigOptions.SwtIssuerEnabled), "SwtIssuerEnabled=unparseable", true), //default - (nameof(FunctionsHostingConfigOptions.SwtIssuerEnabled), string.Empty, true), // default - - (nameof(FunctionsHostingConfigOptions.ThrowOnMissingFunctionsWorkerRuntime), "THROW_ON_MISSING_FUNCTIONS_WORKER_RUNTIME=1", true), - (nameof(FunctionsHostingConfigOptions.WorkerIndexingDisabledApps), "WORKER_INDEXING_DISABLED_APPS=teststring", "teststring"), - (nameof(FunctionsHostingConfigOptions.WorkerIndexingEnabled), "WORKER_INDEXING_ENABLED=1", true), - (nameof(FunctionsHostingConfigOptions.WorkerRuntimeStrictValidationEnabled), "WORKER_RUNTIME_STRICT_VALIDATION_ENABLED=1", true), - - (nameof(FunctionsHostingConfigOptions.InternalAuthApisAllowList), "InternalAuthApisAllowList=|", "|"), - (nameof(FunctionsHostingConfigOptions.InternalAuthApisAllowList), "InternalAuthApisAllowList=/admin/host/foo|/admin/host/bar", "/admin/host/foo|/admin/host/bar"), - - (nameof(FunctionsHostingConfigOptions.IsDotNetInProcDisabled), "DotNetInProcDisabled=False", false), - (nameof(FunctionsHostingConfigOptions.IsDotNetInProcDisabled), "DotNetInProcDisabled=True", true), - (nameof(FunctionsHostingConfigOptions.IsDotNetInProcDisabled), "DotNetInProcDisabled=1", true), - (nameof(FunctionsHostingConfigOptions.IsDotNetInProcDisabled), "DotNetInProcDisabled=0", false), - }; - - // use reflection to ensure that we have a test that uses every value exposed on FunctionsHostingConfigOptions - // (except for Features, which does not get bound). - var props = typeof(FunctionsHostingConfigOptions).GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - .Where(p => p.Name != nameof(FunctionsHostingConfigOptions.Features)); - - foreach (var prop in props) + var prop = AllProperties.Select(x => x[0] as PropertyInfo).Single(p => p.Name == propertyName); + var actual = prop.GetValue(testService.Options.Value); + try { - // make sure all props are internal to prevent inadverntent binding in the future - if (prop.GetGetMethod() is not null || prop.GetSetMethod() is not null) - { - throw new InvalidOperationException($"{prop.Name} is public. All properties on this object should be internal."); - } - - // just make sure we have at least one test per prop - var expected = testCases.FirstOrDefault(p => p.PropertyName == prop.Name); - if (expected == default) - { - throw new InvalidOperationException($"The property {prop.Name} is not set up to be validated. Please add at least one case in this test."); - } + Assert.Equal(expected, actual); } - - foreach (var (propertyName, configValue, expected) in testCases) + catch (Exception) { - using TempDirectory tempDir = new(); - - IHost host = GetScriptHostBuilder(Path.Combine(tempDir.Path, "settings.txt"), configValue).Build(); - var testService = host.Services.GetService(); - - var prop = props.Single(p => p.Name == propertyName); - var actual = prop.GetValue(testService.Options.Value); - try - { - Assert.Equal(expected, actual); - } - catch (Exception) - { - // provide a better failure message - Assert.True(false, $"{prop.Name} failure ('{configValue}'). Expected: {expected}. Actual: {actual}."); - } + // provide a better failure message + Assert.True(false, $"{prop.Name} failure ('{configValue}'). Expected: {expected}. Actual: {actual}."); } } [Fact] public async Task OnChange_Fires_OnFileChange() { - using (TempDirectory tempDir = new TempDirectory()) - { - string fileName = Path.Combine(tempDir.Path, "settings.txt"); - IHost host = GetScriptHostBuilder(fileName, $"feature1=value1,feature2=value2").Build(); - var testService = host.Services.GetService(); + using TempDirectory tempDir = new(); + string fileName = Path.Combine(tempDir.Path, "settings.txt"); + IHost host = GetScriptHostBuilder(fileName, $"feature1=value1,feature2=value2").Build(); + var testService = host.Services.GetService(); - await host.StartAsync(); - - await Task.Delay(1000); - File.WriteAllText(fileName, $"feature1=value1_updated"); - await TestHelpers.Await(() => - { - return testService.Monitor.CurrentValue.GetFeature("feature1") == "value1_updated"; - }); - } + File.WriteAllText(fileName, $"feature1=value1_updated"); + await TestHelpers.Await(() => + { + return testService.Monitor.CurrentValue.GetFeature("feature1") == "value1_updated"; + }); } [Fact] public async Task OnChange_Fires_OnFileDelete() { - using (TempDirectory tempDir = new TempDirectory()) - { - string fileName = Path.Combine(tempDir.Path, "settings.txt"); - IHost host = GetScriptHostBuilder(fileName, $"feature1=value1,feature2=value2").Build(); - var testService = host.Services.GetService(); + using TempDirectory tempDir = new(); + string fileName = Path.Combine(tempDir.Path, "settings.txt"); + IHost host = GetScriptHostBuilder(fileName, $"feature1=value1,feature2=value2").Build(); + var testService = host.Services.GetService(); - await host.StartAsync(); - - await Task.Delay(1000); - File.Delete(fileName); - - await TestHelpers.Await(() => - { - return testService.Monitor.CurrentValue.GetFeature("feature1") == null; - }); - await host.StopAsync(); - } + File.Delete(fileName); + await TestHelpers.Await(() => + { + return testService.Monitor.CurrentValue.GetFeature("feature1") == null; + }); } [Fact] public async Task OnChange_Fires_OnFileCreate() { - using (TempDirectory tempDir = new TempDirectory()) - { - string fileName = Path.Combine(tempDir.Path, "settings.txt"); - IHost host = GetScriptHostBuilder(fileName, string.Empty).Build(); - var testService = host.Services.GetService(); - - await host.StartAsync(); - await Task.Delay(1000); + using TempDirectory tempDir = new(); + string fileName = Path.Combine(tempDir.Path, "settings.txt"); + IHost host = GetScriptHostBuilder(fileName, string.Empty).Build(); + var testService = host.Services.GetService(); - File.WriteAllText(fileName, $"feature1=value1_updated"); - - await TestHelpers.Await(() => - { - return testService.Monitor.CurrentValue.GetFeature("feature1") == "value1_updated"; - }); - } + File.WriteAllText(fileName, $"feature1=value1_updated"); + await TestHelpers.Await(() => + { + return testService.Monitor.CurrentValue.GetFeature("feature1") == "value1_updated"; + }); } [Fact] public void SwtIssuerEnabled_ReturnsExpectedValue() { - FunctionsHostingConfigOptions options = new FunctionsHostingConfigOptions(); + FunctionsHostingConfigOptions options = new(); // defaults to true Assert.True(options.SwtIssuerEnabled); @@ -268,7 +226,7 @@ public void SwtIssuerEnabled_ReturnsExpectedValue() [Fact] public void InternalAuthApisAllowList_ReturnsExpectedValue() { - FunctionsHostingConfigOptions options = new FunctionsHostingConfigOptions(); + FunctionsHostingConfigOptions options = new(); Assert.Null(options.InternalAuthApisAllowList); @@ -286,10 +244,10 @@ internal static IHostBuilder GetScriptHostBuilder(string fileName, string fileCo File.WriteAllText(fileName, fileContent); } - TestEnvironment environment = new TestEnvironment(); + TestEnvironment environment = new(); environment.SetEnvironmentVariable(EnvironmentSettingNames.FunctionsPlatformConfigFilePath, fileName); - IHost webHost = new HostBuilder() + IHostBuilder builder = new HostBuilder() .ConfigureAppConfiguration((builderContext, config) => { config.Add(new FunctionsHostingConfigSource(environment)); @@ -297,23 +255,15 @@ internal static IHostBuilder GetScriptHostBuilder(string fileName, string fileCo .ConfigureServices((context, services) => { WebHostServiceCollectionExtensions.AddHostingConfigOptions(services, context.Configuration); - }).Build(); - - return new HostBuilder() - .ConfigureServices((context, services) => - { services.AddSingleton(); - }) - .ConfigureDefaultTestWebScriptHost(null, configureRootServices: (services) => - { - services.AddSingleton(webHost.Services.GetService>()); - services.AddSingleton(webHost.Services.GetService>()); }); + + return builder; } public class TestService { - public TestService(IOptions options, IOptionsMonitor monitor) + public TestService(IOptions options, IOptionsMonitor monitor) { Options = options; Monitor = monitor; @@ -323,9 +273,9 @@ public TestService(IOptions options, IOpti }); } - public IOptions Options { get; set; } + public IOptions Options { get; set; } - public IOptionsMonitor Monitor { get; set; } + public IOptionsMonitor Monitor { get; set; } } } }