From df290c22122fbe266f5531144239367cc9684d96 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 3 Mar 2025 12:40:13 -0800 Subject: [PATCH 01/11] add initial integration test file --- tests/IntegrationTests.cs | 336 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 tests/IntegrationTests.cs diff --git a/tests/IntegrationTests.cs b/tests/IntegrationTests.cs new file mode 100644 index 00000000..b056cef9 --- /dev/null +++ b/tests/IntegrationTests.cs @@ -0,0 +1,336 @@ +using Azure; +using Azure.Data.AppConfiguration; +using Azure.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.AzureAppConfiguration; +using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Tests.AzureAppConfiguration.IntegrationTests +{ + /// + /// Integration tests for Azure App Configuration that connect to a real service. + /// Requires valid connection details to be provided through environment variables or other secure methods. + /// + [Trait("Category", "Integration")] + public class AzureAppConfigurationIntegrationTests : IAsyncLifetime + { + // Test constants + private const string TestKeyPrefix = "IntegrationTest"; + private const string SentinelKey = TestKeyPrefix + ":Sentinel"; + private const string FeatureFlagKey = ".appconfig.featureflag/" + TestKeyPrefix + "Feature"; + + // Keys to create for testing + private readonly List _testSettings = new List + { + new ConfigurationSetting($"{TestKeyPrefix}:Setting1", "InitialValue1"), + new ConfigurationSetting($"{TestKeyPrefix}:Setting2", "InitialValue2"), + new ConfigurationSetting(SentinelKey, "Initial"), + new ConfigurationSetting(FeatureFlagKey, + @"{""id"":""" + TestKeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":false}", + contentType: FeatureManagementConstants.ContentType) + }; + + // Client for direct manipulation of the store + private ConfigurationClient _configClient; + + /// + /// Gets a DefaultAzureCredential for authentication (alternative to connection string). + /// + private DefaultAzureCredential GetCredential() + { + return new DefaultAzureCredential(); + } + + /// + /// Creates test data in the Azure App Configuration store before running tests. + /// + public async Task InitializeAsync() + { + try + { + // Get endpoint from environment variable + string endpoint = Environment.GetEnvironmentVariable("AZURE_APPCONFIG_ENDPOINT"); + + if (string.IsNullOrEmpty(endpoint)) + { + throw new InvalidOperationException("AZURE_APPCONFIG_ENDPOINT environment variable is required when using managed identity"); + } + + _configClient = new ConfigurationClient(new Uri(endpoint), GetCredential()); + + // Add test settings to the store + foreach (var setting in _testSettings) + { + await _configClient.SetConfigurationSettingAsync(setting); + } + } + catch (Exception ex) + { + Console.WriteLine($"Test initialization failed: {ex}"); + throw; + } + } + + /// + /// Cleans up test data after tests are complete. + /// + public async Task DisposeAsync() + { + try + { + // Remove test settings from the store + foreach (var setting in _testSettings) + { + await _configClient.DeleteConfigurationSettingAsync(setting.Key, setting.Label); + } + } + catch (Exception ex) + { + Console.WriteLine($"Test cleanup failed: {ex}"); + } + } + + [Fact] + public void LoadConfiguration_RetrievesValuesFromAppConfiguration() + { + // Arrange & Act + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{TestKeyPrefix}:*"); + }) + .Build(); + + // Assert + Assert.Equal("InitialValue1", config[$"{TestKeyPrefix}:Setting1"]); + Assert.Equal("InitialValue2", config[$"{TestKeyPrefix}:Setting2"]); + } + + [Fact] + public async Task TryRefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() + { + // Arrange + IConfigurationRefresher refresher = null; + + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{TestKeyPrefix}:*"); + options.ConfigureRefresh(refresh => + { + refresh.Register(SentinelKey, refreshAll: true) + .SetCacheExpiration(TimeSpan.FromSeconds(1)); + }); + + refresher = options.GetRefresher(); + }) + .Build(); + + // Verify initial values + Assert.Equal("InitialValue1", config[$"{TestKeyPrefix}:Setting1"]); + + // Update values in the store + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting($"{TestKeyPrefix}:Setting1", "UpdatedValue1")); + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting(SentinelKey, "Updated")); + + // Wait for cache to expire + await Task.Delay(TimeSpan.FromSeconds(2)); + + // Act + var result = await refresher.TryRefreshAsync(); + + // Assert + Assert.True(result); + Assert.Equal("UpdatedValue1", config[$"{TestKeyPrefix}:Setting1"]); + } + + [Fact] + public async Task TryRefreshAsync_RefreshesOnlySelectedKeys_WhenUsingKeyFilter() + { + // Arrange + IConfigurationRefresher refresher = null; + + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{TestKeyPrefix}:*"); + + // Only refresh Setting1 when sentinel changes + options.ConfigureRefresh(refresh => + { + refresh.Register(SentinelKey, $"{TestKeyPrefix}:Setting1", refreshAll: false) + .SetCacheExpiration(TimeSpan.FromSeconds(1)); + }); + + refresher = options.GetRefresher(); + }) + .Build(); + + // Verify initial values + Assert.Equal("InitialValue1", config[$"{TestKeyPrefix}:Setting1"]); + Assert.Equal("InitialValue2", config[$"{TestKeyPrefix}:Setting2"]); + + // Update values in the store + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting($"{TestKeyPrefix}:Setting1", "UpdatedValue1")); + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting($"{TestKeyPrefix}:Setting2", "UpdatedValue2")); + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting(SentinelKey, "Updated")); + + // Wait for cache to expire + await Task.Delay(TimeSpan.FromSeconds(2)); + + // Act + var result = await refresher.TryRefreshAsync(); + + // Assert + Assert.True(result); + Assert.Equal("UpdatedValue1", config[$"{TestKeyPrefix}:Setting1"]); + Assert.Equal("InitialValue2", config[$"{TestKeyPrefix}:Setting2"]); // This value shouldn't change + } + + [Fact] + public async Task TryRefreshAsync_RefreshesFeatureFlags_WhenConfigured() + { + // Arrange + IConfigurationRefresher refresher = null; + + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{TestKeyPrefix}:*"); + options.UseFeatureFlags(); + + options.ConfigureRefresh(refresh => + { + refresh.Register(SentinelKey) + .SetCacheExpiration(TimeSpan.FromSeconds(1)); + }); + + refresher = options.GetRefresher(); + }) + .Build(); + + // Verify initial feature flag state + Assert.Equal("False", config[$"FeatureManagement:{TestKeyPrefix}Feature:Enabled"]); + + // Update feature flag in the store + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting( + FeatureFlagKey, + @"{""id"":""" + TestKeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":true}", + contentType: FeatureManagementConstants.ContentType)); + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting(SentinelKey, "Updated")); + + // Wait for cache to expire + await Task.Delay(TimeSpan.FromSeconds(2)); + + // Act + var result = await refresher.TryRefreshAsync(); + + // Assert + Assert.True(result); + Assert.Equal("True", config[$"FeatureManagement:{TestKeyPrefix}Feature:Enabled"]); + } + + [Fact] + public async Task RegisterAll_RefreshesAllKeys_WhenSentinelChanged() + { + // Arrange + IConfigurationRefresher refresher = null; + + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{TestKeyPrefix}:*"); + + // Use RegisterAll to refresh everything when sentinel changes + options.ConfigureRefresh(refresh => + { + refresh.RegisterAll(SentinelKey) + .SetCacheExpiration(TimeSpan.FromSeconds(1)); + }); + + refresher = options.GetRefresher(); + }) + .Build(); + + // Verify initial values + Assert.Equal("InitialValue1", config[$"{TestKeyPrefix}:Setting1"]); + Assert.Equal("InitialValue2", config[$"{TestKeyPrefix}:Setting2"]); + + // Update all values in the store + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting($"{TestKeyPrefix}:Setting1", "UpdatedValue1")); + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting($"{TestKeyPrefix}:Setting2", "UpdatedValue2")); + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting(SentinelKey, "Updated")); + + // Wait for cache to expire + await Task.Delay(TimeSpan.FromSeconds(2)); + + // Act + var result = await refresher.TryRefreshAsync(); + + // Assert + Assert.True(result); + Assert.Equal("UpdatedValue1", config[$"{TestKeyPrefix}:Setting1"]); + Assert.Equal("UpdatedValue2", config[$"{TestKeyPrefix}:Setting2"]); + } + + [Fact] + public async Task TryRefreshAsync_ReturnsFalse_WhenSentinelKeyUnchanged() + { + // Arrange + IConfigurationRefresher refresher = null; + + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{TestKeyPrefix}:*"); + + options.ConfigureRefresh(refresh => + { + refresh.Register(SentinelKey) + .SetCacheExpiration(TimeSpan.FromSeconds(1)); + }); + + refresher = options.GetRefresher(); + }) + .Build(); + + // Verify initial values + Assert.Equal("InitialValue1", config[$"{TestKeyPrefix}:Setting1"]); + + // Update data but not sentinel + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting($"{TestKeyPrefix}:Setting1", "UpdatedValue1")); + + // Wait for cache to expire + await Task.Delay(TimeSpan.FromSeconds(2)); + + // Act + var result = await refresher.TryRefreshAsync(); + + // Assert + Assert.False(result); // Should return false as sentinel hasn't changed + Assert.Equal("InitialValue1", config[$"{TestKeyPrefix}:Setting1"]); // Should not update + } + } +} \ No newline at end of file From e99a65937106d1deac03ebc5599e138ee7b9864d Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 3 Mar 2025 12:57:31 -0800 Subject: [PATCH 02/11] update auth --- .../IntegrationTests.cs | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) rename tests/{ => Tests.AzureAppConfiguration}/IntegrationTests.cs (92%) diff --git a/tests/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/IntegrationTests.cs similarity index 92% rename from tests/IntegrationTests.cs rename to tests/Tests.AzureAppConfiguration/IntegrationTests.cs index b056cef9..5b50f240 100644 --- a/tests/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/IntegrationTests.cs @@ -10,31 +10,32 @@ using System.Threading.Tasks; using Xunit; -namespace Tests.AzureAppConfiguration.IntegrationTests +namespace Tests.AzureAppConfiguration { /// /// Integration tests for Azure App Configuration that connect to a real service. /// Requires valid connection details to be provided through environment variables or other secure methods. /// [Trait("Category", "Integration")] - public class AzureAppConfigurationIntegrationTests : IAsyncLifetime + public class IntegrationTests : IAsyncLifetime { // Test constants private const string TestKeyPrefix = "IntegrationTest"; private const string SentinelKey = TestKeyPrefix + ":Sentinel"; private const string FeatureFlagKey = ".appconfig.featureflag/" + TestKeyPrefix + "Feature"; - + // Keys to create for testing private readonly List _testSettings = new List { new ConfigurationSetting($"{TestKeyPrefix}:Setting1", "InitialValue1"), new ConfigurationSetting($"{TestKeyPrefix}:Setting2", "InitialValue2"), new ConfigurationSetting(SentinelKey, "Initial"), - new ConfigurationSetting(FeatureFlagKey, - @"{""id"":""" + TestKeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":false}", + ConfigurationModelFactory.ConfigurationSetting( + FeatureFlagKey, + @"{""id"":""" + TestKeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":false}", contentType: FeatureManagementConstants.ContentType) }; - + // Client for direct manipulation of the store private ConfigurationClient _configClient; @@ -46,6 +47,14 @@ private DefaultAzureCredential GetCredential() return new DefaultAzureCredential(); } + /// + /// Gets the endpoint for the App Configuration store. + /// + private Uri GetEndpoint() + { + return new Uri(Environment.GetEnvironmentVariable("AZURE_APPCONFIG_INTEGRATIONTEST_ENDPOINT")); + } + /// /// Creates test data in the Azure App Configuration store before running tests. /// @@ -54,15 +63,15 @@ public async Task InitializeAsync() try { // Get endpoint from environment variable - string endpoint = Environment.GetEnvironmentVariable("AZURE_APPCONFIG_ENDPOINT"); - + string endpoint = Environment.GetEnvironmentVariable("AZURE_APPCONFIG_INTEGRATIONTEST_ENDPOINT"); + if (string.IsNullOrEmpty(endpoint)) { - throw new InvalidOperationException("AZURE_APPCONFIG_ENDPOINT environment variable is required when using managed identity"); + throw new InvalidOperationException("AZURE_APPCONFIG_INTEGRATIONTEST_ENDPOINT environment variable is required when using managed identity"); } - + _configClient = new ConfigurationClient(new Uri(endpoint), GetCredential()); - + // Add test settings to the store foreach (var setting in _testSettings) { @@ -102,7 +111,7 @@ public void LoadConfiguration_RetrievesValuesFromAppConfiguration() var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(GetEndpoint(), GetCredential()); options.Select($"{TestKeyPrefix}:*"); }) .Build(); @@ -121,7 +130,7 @@ public async Task TryRefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(GetEndpoint(), GetCredential()); options.Select($"{TestKeyPrefix}:*"); options.ConfigureRefresh(refresh => { @@ -162,7 +171,7 @@ public async Task TryRefreshAsync_RefreshesOnlySelectedKeys_WhenUsingKeyFilter() var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(GetEndpoint(), GetCredential()); options.Select($"{TestKeyPrefix}:*"); // Only refresh Setting1 when sentinel changes @@ -209,7 +218,7 @@ public async Task TryRefreshAsync_RefreshesFeatureFlags_WhenConfigured() var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(GetEndpoint(), GetCredential()); options.Select($"{TestKeyPrefix}:*"); options.UseFeatureFlags(); @@ -255,7 +264,7 @@ public async Task RegisterAll_RefreshesAllKeys_WhenSentinelChanged() var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(GetEndpoint(), GetCredential()); options.Select($"{TestKeyPrefix}:*"); // Use RegisterAll to refresh everything when sentinel changes @@ -280,7 +289,7 @@ await _configClient.SetConfigurationSettingAsync( new ConfigurationSetting($"{TestKeyPrefix}:Setting2", "UpdatedValue2")); await _configClient.SetConfigurationSettingAsync( new ConfigurationSetting(SentinelKey, "Updated")); - + // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); @@ -302,7 +311,7 @@ public async Task TryRefreshAsync_ReturnsFalse_WhenSentinelKeyUnchanged() var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(GetEndpoint(), GetCredential()); options.Select($"{TestKeyPrefix}:*"); options.ConfigureRefresh(refresh => @@ -333,4 +342,4 @@ await _configClient.SetConfigurationSettingAsync( Assert.Equal("InitialValue1", config[$"{TestKeyPrefix}:Setting1"]); // Should not update } } -} \ No newline at end of file +} From ba1d8f0470c5895aa7c647eeb3f9696b66586950 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 3 Mar 2025 13:08:59 -0800 Subject: [PATCH 03/11] add skipping --- .../IntegrationTests.cs | 49 ++++++++++++++++--- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/IntegrationTests.cs index 5b50f240..e6f838cb 100644 --- a/tests/Tests.AzureAppConfiguration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/IntegrationTests.cs @@ -38,6 +38,10 @@ public class IntegrationTests : IAsyncLifetime // Client for direct manipulation of the store private ConfigurationClient _configClient; + + // Flag indicating whether tests should run + private bool _skipTests = false; + private string _skipReason = null; /// /// Gets a DefaultAzureCredential for authentication (alternative to connection string). @@ -52,7 +56,16 @@ private DefaultAzureCredential GetCredential() /// private Uri GetEndpoint() { - return new Uri(Environment.GetEnvironmentVariable("AZURE_APPCONFIG_INTEGRATIONTEST_ENDPOINT")); + string endpoint = Environment.GetEnvironmentVariable("AZURE_APPCONFIG_INTEGRATIONTEST_ENDPOINT"); + + if (string.IsNullOrEmpty(endpoint)) + { + _skipTests = true; + _skipReason = "AZURE_APPCONFIG_INTEGRATIONTEST_ENDPOINT environment variable is missing"; + return null; + } + + return new Uri(endpoint); } /// @@ -62,15 +75,16 @@ public async Task InitializeAsync() { try { - // Get endpoint from environment variable - string endpoint = Environment.GetEnvironmentVariable("AZURE_APPCONFIG_INTEGRATIONTEST_ENDPOINT"); - - if (string.IsNullOrEmpty(endpoint)) + Uri endpoint = GetEndpoint(); + + // If endpoint is null, skip all tests + if (_skipTests) { - throw new InvalidOperationException("AZURE_APPCONFIG_INTEGRATIONTEST_ENDPOINT environment variable is required when using managed identity"); + Console.WriteLine($"Integration tests will be skipped: {_skipReason}"); + return; } - _configClient = new ConfigurationClient(new Uri(endpoint), GetCredential()); + _configClient = new ConfigurationClient(endpoint, GetCredential()); // Add test settings to the store foreach (var setting in _testSettings) @@ -81,7 +95,8 @@ public async Task InitializeAsync() catch (Exception ex) { Console.WriteLine($"Test initialization failed: {ex}"); - throw; + _skipTests = true; + _skipReason = $"Failed to initialize integration tests: {ex.Message}"; } } @@ -90,6 +105,12 @@ public async Task InitializeAsync() /// public async Task DisposeAsync() { + // Don't attempt cleanup if we're skipping tests + if (_skipTests || _configClient == null) + { + return; + } + try { // Remove test settings from the store @@ -107,6 +128,8 @@ public async Task DisposeAsync() [Fact] public void LoadConfiguration_RetrievesValuesFromAppConfiguration() { + Skip.If(_skipTests, _skipReason); + // Arrange & Act var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => @@ -124,6 +147,8 @@ public void LoadConfiguration_RetrievesValuesFromAppConfiguration() [Fact] public async Task TryRefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() { + Skip.If(_skipTests, _skipReason); + // Arrange IConfigurationRefresher refresher = null; @@ -165,6 +190,8 @@ await _configClient.SetConfigurationSettingAsync( [Fact] public async Task TryRefreshAsync_RefreshesOnlySelectedKeys_WhenUsingKeyFilter() { + Skip.If(_skipTests, _skipReason); + // Arrange IConfigurationRefresher refresher = null; @@ -212,6 +239,8 @@ await _configClient.SetConfigurationSettingAsync( [Fact] public async Task TryRefreshAsync_RefreshesFeatureFlags_WhenConfigured() { + Skip.If(_skipTests, _skipReason); + // Arrange IConfigurationRefresher refresher = null; @@ -258,6 +287,8 @@ await _configClient.SetConfigurationSettingAsync( [Fact] public async Task RegisterAll_RefreshesAllKeys_WhenSentinelChanged() { + Skip.If(_skipTests, _skipReason); + // Arrange IConfigurationRefresher refresher = null; @@ -305,6 +336,8 @@ await _configClient.SetConfigurationSettingAsync( [Fact] public async Task TryRefreshAsync_ReturnsFalse_WhenSentinelKeyUnchanged() { + Skip.If(_skipTests, _skipReason); + // Arrange IConfigurationRefresher refresher = null; From 366687c1bbb0b10afe1dc661571b30f8aa9bf679 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 3 Mar 2025 14:20:06 -0800 Subject: [PATCH 04/11] organize tests into unit and integration --- .../{ => Integration}/IntegrationTests.cs | 8 ++++---- .../{ => Unit}/CallbackMessageHandler.cs | 0 .../{ => Unit}/ClientOptionsTests.cs | 0 .../{ => Unit}/ConnectTests.cs | 0 .../{ => Unit}/FailoverTests.cs | 0 .../{ => Unit}/FeatureManagementTests.cs | 0 .../{ => Unit}/HttpRequestCountPipelinePolicy.cs | 0 .../{ => Unit}/JsonContentTypeTests.cs | 0 .../{ => Unit}/KeyVaultReferenceTests.cs | 0 .../{ => Unit}/LoadBalancingTests.cs | 0 .../{ => Unit}/LoggingTests.cs | 0 tests/Tests.AzureAppConfiguration/{ => Unit}/MapTests.cs | 0 .../{ => Unit}/MockedConfigurationClientManager.cs | 0 .../{ => Unit}/PushRefreshTests.cs | 0 .../{ => Unit}/RefreshTests.cs | 0 .../Tests.AzureAppConfiguration/{ => Unit}/TestHelper.cs | 0 tests/Tests.AzureAppConfiguration/{ => Unit}/Tests.cs | 0 17 files changed, 4 insertions(+), 4 deletions(-) rename tests/Tests.AzureAppConfiguration/{ => Integration}/IntegrationTests.cs (98%) rename tests/Tests.AzureAppConfiguration/{ => Unit}/CallbackMessageHandler.cs (100%) rename tests/Tests.AzureAppConfiguration/{ => Unit}/ClientOptionsTests.cs (100%) rename tests/Tests.AzureAppConfiguration/{ => Unit}/ConnectTests.cs (100%) rename tests/Tests.AzureAppConfiguration/{ => Unit}/FailoverTests.cs (100%) rename tests/Tests.AzureAppConfiguration/{ => Unit}/FeatureManagementTests.cs (100%) rename tests/Tests.AzureAppConfiguration/{ => Unit}/HttpRequestCountPipelinePolicy.cs (100%) rename tests/Tests.AzureAppConfiguration/{ => Unit}/JsonContentTypeTests.cs (100%) rename tests/Tests.AzureAppConfiguration/{ => Unit}/KeyVaultReferenceTests.cs (100%) rename tests/Tests.AzureAppConfiguration/{ => Unit}/LoadBalancingTests.cs (100%) rename tests/Tests.AzureAppConfiguration/{ => Unit}/LoggingTests.cs (100%) rename tests/Tests.AzureAppConfiguration/{ => Unit}/MapTests.cs (100%) rename tests/Tests.AzureAppConfiguration/{ => Unit}/MockedConfigurationClientManager.cs (100%) rename tests/Tests.AzureAppConfiguration/{ => Unit}/PushRefreshTests.cs (100%) rename tests/Tests.AzureAppConfiguration/{ => Unit}/RefreshTests.cs (100%) rename tests/Tests.AzureAppConfiguration/{ => Unit}/TestHelper.cs (100%) rename tests/Tests.AzureAppConfiguration/{ => Unit}/Tests.cs (100%) diff --git a/tests/Tests.AzureAppConfiguration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs similarity index 98% rename from tests/Tests.AzureAppConfiguration/IntegrationTests.cs rename to tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index e6f838cb..9b45601b 100644 --- a/tests/Tests.AzureAppConfiguration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -263,10 +263,10 @@ public async Task TryRefreshAsync_RefreshesFeatureFlags_WhenConfigured() // Verify initial feature flag state Assert.Equal("False", config[$"FeatureManagement:{TestKeyPrefix}Feature:Enabled"]); - + // Update feature flag in the store await _configClient.SetConfigurationSettingAsync( - new ConfigurationSetting( + ConfigurationModelFactory.ConfigurationSetting( FeatureFlagKey, @"{""id"":""" + TestKeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":true}", contentType: FeatureManagementConstants.ContentType)); @@ -301,8 +301,8 @@ public async Task RegisterAll_RefreshesAllKeys_WhenSentinelChanged() // Use RegisterAll to refresh everything when sentinel changes options.ConfigureRefresh(refresh => { - refresh.RegisterAll(SentinelKey) - .SetCacheExpiration(TimeSpan.FromSeconds(1)); + refresh.RegisterAll() + .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); refresher = options.GetRefresher(); diff --git a/tests/Tests.AzureAppConfiguration/CallbackMessageHandler.cs b/tests/Tests.AzureAppConfiguration/Unit/CallbackMessageHandler.cs similarity index 100% rename from tests/Tests.AzureAppConfiguration/CallbackMessageHandler.cs rename to tests/Tests.AzureAppConfiguration/Unit/CallbackMessageHandler.cs diff --git a/tests/Tests.AzureAppConfiguration/ClientOptionsTests.cs b/tests/Tests.AzureAppConfiguration/Unit/ClientOptionsTests.cs similarity index 100% rename from tests/Tests.AzureAppConfiguration/ClientOptionsTests.cs rename to tests/Tests.AzureAppConfiguration/Unit/ClientOptionsTests.cs diff --git a/tests/Tests.AzureAppConfiguration/ConnectTests.cs b/tests/Tests.AzureAppConfiguration/Unit/ConnectTests.cs similarity index 100% rename from tests/Tests.AzureAppConfiguration/ConnectTests.cs rename to tests/Tests.AzureAppConfiguration/Unit/ConnectTests.cs diff --git a/tests/Tests.AzureAppConfiguration/FailoverTests.cs b/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs similarity index 100% rename from tests/Tests.AzureAppConfiguration/FailoverTests.cs rename to tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs diff --git a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs b/tests/Tests.AzureAppConfiguration/Unit/FeatureManagementTests.cs similarity index 100% rename from tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs rename to tests/Tests.AzureAppConfiguration/Unit/FeatureManagementTests.cs diff --git a/tests/Tests.AzureAppConfiguration/HttpRequestCountPipelinePolicy.cs b/tests/Tests.AzureAppConfiguration/Unit/HttpRequestCountPipelinePolicy.cs similarity index 100% rename from tests/Tests.AzureAppConfiguration/HttpRequestCountPipelinePolicy.cs rename to tests/Tests.AzureAppConfiguration/Unit/HttpRequestCountPipelinePolicy.cs diff --git a/tests/Tests.AzureAppConfiguration/JsonContentTypeTests.cs b/tests/Tests.AzureAppConfiguration/Unit/JsonContentTypeTests.cs similarity index 100% rename from tests/Tests.AzureAppConfiguration/JsonContentTypeTests.cs rename to tests/Tests.AzureAppConfiguration/Unit/JsonContentTypeTests.cs diff --git a/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs b/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs similarity index 100% rename from tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs rename to tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs diff --git a/tests/Tests.AzureAppConfiguration/LoadBalancingTests.cs b/tests/Tests.AzureAppConfiguration/Unit/LoadBalancingTests.cs similarity index 100% rename from tests/Tests.AzureAppConfiguration/LoadBalancingTests.cs rename to tests/Tests.AzureAppConfiguration/Unit/LoadBalancingTests.cs diff --git a/tests/Tests.AzureAppConfiguration/LoggingTests.cs b/tests/Tests.AzureAppConfiguration/Unit/LoggingTests.cs similarity index 100% rename from tests/Tests.AzureAppConfiguration/LoggingTests.cs rename to tests/Tests.AzureAppConfiguration/Unit/LoggingTests.cs diff --git a/tests/Tests.AzureAppConfiguration/MapTests.cs b/tests/Tests.AzureAppConfiguration/Unit/MapTests.cs similarity index 100% rename from tests/Tests.AzureAppConfiguration/MapTests.cs rename to tests/Tests.AzureAppConfiguration/Unit/MapTests.cs diff --git a/tests/Tests.AzureAppConfiguration/MockedConfigurationClientManager.cs b/tests/Tests.AzureAppConfiguration/Unit/MockedConfigurationClientManager.cs similarity index 100% rename from tests/Tests.AzureAppConfiguration/MockedConfigurationClientManager.cs rename to tests/Tests.AzureAppConfiguration/Unit/MockedConfigurationClientManager.cs diff --git a/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs b/tests/Tests.AzureAppConfiguration/Unit/PushRefreshTests.cs similarity index 100% rename from tests/Tests.AzureAppConfiguration/PushRefreshTests.cs rename to tests/Tests.AzureAppConfiguration/Unit/PushRefreshTests.cs diff --git a/tests/Tests.AzureAppConfiguration/RefreshTests.cs b/tests/Tests.AzureAppConfiguration/Unit/RefreshTests.cs similarity index 100% rename from tests/Tests.AzureAppConfiguration/RefreshTests.cs rename to tests/Tests.AzureAppConfiguration/Unit/RefreshTests.cs diff --git a/tests/Tests.AzureAppConfiguration/TestHelper.cs b/tests/Tests.AzureAppConfiguration/Unit/TestHelper.cs similarity index 100% rename from tests/Tests.AzureAppConfiguration/TestHelper.cs rename to tests/Tests.AzureAppConfiguration/Unit/TestHelper.cs diff --git a/tests/Tests.AzureAppConfiguration/Tests.cs b/tests/Tests.AzureAppConfiguration/Unit/Tests.cs similarity index 100% rename from tests/Tests.AzureAppConfiguration/Tests.cs rename to tests/Tests.AzureAppConfiguration/Unit/Tests.cs From 421862abccf469a3f66e5690a19a298f1da0a726 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 3 Mar 2025 14:37:38 -0800 Subject: [PATCH 05/11] update formatting --- .../ConfigurationSettingPageExtensions.cs | 4 +- .../IConfigurationSettingPageIterator.cs | 4 +- .../Integration/IntegrationTests.cs | 104 +++++++++--------- .../Tests.AzureAppConfiguration.csproj | 1 + .../Unit/FeatureManagementTests.cs | 3 + 5 files changed, 60 insertions(+), 56 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ConfigurationSettingPageExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ConfigurationSettingPageExtensions.cs index aba7684b..ad69330d 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ConfigurationSettingPageExtensions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ConfigurationSettingPageExtensions.cs @@ -1,5 +1,5 @@ -using Azure.Data.AppConfiguration; -using Azure; +using Azure; +using Azure.Data.AppConfiguration; using System.Collections.Generic; namespace Microsoft.Extensions.Configuration.AzureAppConfiguration diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/IConfigurationSettingPageIterator.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/IConfigurationSettingPageIterator.cs index 08c95751..7bbc3ded 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/IConfigurationSettingPageIterator.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/IConfigurationSettingPageIterator.cs @@ -1,5 +1,5 @@ -using Azure.Data.AppConfiguration; -using Azure; +using Azure; +using Azure.Data.AppConfiguration; using System.Collections.Generic; namespace Microsoft.Extensions.Configuration.AzureAppConfiguration diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 9b45601b..418bfcc8 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -38,7 +38,7 @@ public class IntegrationTests : IAsyncLifetime // Client for direct manipulation of the store private ConfigurationClient _configClient; - + // Flag indicating whether tests should run private bool _skipTests = false; private string _skipReason = null; @@ -57,14 +57,14 @@ private DefaultAzureCredential GetCredential() private Uri GetEndpoint() { string endpoint = Environment.GetEnvironmentVariable("AZURE_APPCONFIG_INTEGRATIONTEST_ENDPOINT"); - + if (string.IsNullOrEmpty(endpoint)) { _skipTests = true; _skipReason = "AZURE_APPCONFIG_INTEGRATIONTEST_ENDPOINT environment variable is missing"; return null; } - + return new Uri(endpoint); } @@ -76,7 +76,7 @@ public async Task InitializeAsync() try { Uri endpoint = GetEndpoint(); - + // If endpoint is null, skip all tests if (_skipTests) { @@ -110,7 +110,7 @@ public async Task DisposeAsync() { return; } - + try { // Remove test settings from the store @@ -129,7 +129,7 @@ public async Task DisposeAsync() public void LoadConfiguration_RetrievesValuesFromAppConfiguration() { Skip.If(_skipTests, _skipReason); - + // Arrange & Act var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => @@ -148,7 +148,7 @@ public void LoadConfiguration_RetrievesValuesFromAppConfiguration() public async Task TryRefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() { Skip.If(_skipTests, _skipReason); - + // Arrange IConfigurationRefresher refresher = null; @@ -160,38 +160,38 @@ public async Task TryRefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() options.ConfigureRefresh(refresh => { refresh.Register(SentinelKey, refreshAll: true) - .SetCacheExpiration(TimeSpan.FromSeconds(1)); + .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); - + refresher = options.GetRefresher(); }) .Build(); - + // Verify initial values Assert.Equal("InitialValue1", config[$"{TestKeyPrefix}:Setting1"]); - + // Update values in the store await _configClient.SetConfigurationSettingAsync( new ConfigurationSetting($"{TestKeyPrefix}:Setting1", "UpdatedValue1")); await _configClient.SetConfigurationSettingAsync( new ConfigurationSetting(SentinelKey, "Updated")); - + // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); - + // Act var result = await refresher.TryRefreshAsync(); - + // Assert Assert.True(result); Assert.Equal("UpdatedValue1", config[$"{TestKeyPrefix}:Setting1"]); } - - [Fact] + + [SkippableFact] public async Task TryRefreshAsync_RefreshesOnlySelectedKeys_WhenUsingKeyFilter() { Skip.If(_skipTests, _skipReason); - + // Arrange IConfigurationRefresher refresher = null; @@ -200,22 +200,22 @@ public async Task TryRefreshAsync_RefreshesOnlySelectedKeys_WhenUsingKeyFilter() { options.Connect(GetEndpoint(), GetCredential()); options.Select($"{TestKeyPrefix}:*"); - + // Only refresh Setting1 when sentinel changes options.ConfigureRefresh(refresh => { refresh.Register(SentinelKey, $"{TestKeyPrefix}:Setting1", refreshAll: false) - .SetCacheExpiration(TimeSpan.FromSeconds(1)); + .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); - + refresher = options.GetRefresher(); }) .Build(); - + // Verify initial values Assert.Equal("InitialValue1", config[$"{TestKeyPrefix}:Setting1"]); Assert.Equal("InitialValue2", config[$"{TestKeyPrefix}:Setting2"]); - + // Update values in the store await _configClient.SetConfigurationSettingAsync( new ConfigurationSetting($"{TestKeyPrefix}:Setting1", "UpdatedValue1")); @@ -223,24 +223,24 @@ await _configClient.SetConfigurationSettingAsync( new ConfigurationSetting($"{TestKeyPrefix}:Setting2", "UpdatedValue2")); await _configClient.SetConfigurationSettingAsync( new ConfigurationSetting(SentinelKey, "Updated")); - + // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); - + // Act var result = await refresher.TryRefreshAsync(); - + // Assert Assert.True(result); Assert.Equal("UpdatedValue1", config[$"{TestKeyPrefix}:Setting1"]); Assert.Equal("InitialValue2", config[$"{TestKeyPrefix}:Setting2"]); // This value shouldn't change } - + [Fact] public async Task TryRefreshAsync_RefreshesFeatureFlags_WhenConfigured() { Skip.If(_skipTests, _skipReason); - + // Arrange IConfigurationRefresher refresher = null; @@ -250,17 +250,17 @@ public async Task TryRefreshAsync_RefreshesFeatureFlags_WhenConfigured() options.Connect(GetEndpoint(), GetCredential()); options.Select($"{TestKeyPrefix}:*"); options.UseFeatureFlags(); - + options.ConfigureRefresh(refresh => { refresh.Register(SentinelKey) - .SetCacheExpiration(TimeSpan.FromSeconds(1)); + .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); - + refresher = options.GetRefresher(); }) .Build(); - + // Verify initial feature flag state Assert.Equal("False", config[$"FeatureManagement:{TestKeyPrefix}Feature:Enabled"]); @@ -272,23 +272,23 @@ await _configClient.SetConfigurationSettingAsync( contentType: FeatureManagementConstants.ContentType)); await _configClient.SetConfigurationSettingAsync( new ConfigurationSetting(SentinelKey, "Updated")); - + // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); - + // Act var result = await refresher.TryRefreshAsync(); - + // Assert Assert.True(result); Assert.Equal("True", config[$"FeatureManagement:{TestKeyPrefix}Feature:Enabled"]); } - + [Fact] public async Task RegisterAll_RefreshesAllKeys_WhenSentinelChanged() { Skip.If(_skipTests, _skipReason); - + // Arrange IConfigurationRefresher refresher = null; @@ -297,22 +297,22 @@ public async Task RegisterAll_RefreshesAllKeys_WhenSentinelChanged() { options.Connect(GetEndpoint(), GetCredential()); options.Select($"{TestKeyPrefix}:*"); - + // Use RegisterAll to refresh everything when sentinel changes options.ConfigureRefresh(refresh => { refresh.RegisterAll() .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); - + refresher = options.GetRefresher(); }) .Build(); - + // Verify initial values Assert.Equal("InitialValue1", config[$"{TestKeyPrefix}:Setting1"]); Assert.Equal("InitialValue2", config[$"{TestKeyPrefix}:Setting2"]); - + // Update all values in the store await _configClient.SetConfigurationSettingAsync( new ConfigurationSetting($"{TestKeyPrefix}:Setting1", "UpdatedValue1")); @@ -323,21 +323,21 @@ await _configClient.SetConfigurationSettingAsync( // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); - + // Act var result = await refresher.TryRefreshAsync(); - + // Assert Assert.True(result); Assert.Equal("UpdatedValue1", config[$"{TestKeyPrefix}:Setting1"]); Assert.Equal("UpdatedValue2", config[$"{TestKeyPrefix}:Setting2"]); } - + [Fact] public async Task TryRefreshAsync_ReturnsFalse_WhenSentinelKeyUnchanged() { Skip.If(_skipTests, _skipReason); - + // Arrange IConfigurationRefresher refresher = null; @@ -346,30 +346,30 @@ public async Task TryRefreshAsync_ReturnsFalse_WhenSentinelKeyUnchanged() { options.Connect(GetEndpoint(), GetCredential()); options.Select($"{TestKeyPrefix}:*"); - + options.ConfigureRefresh(refresh => { refresh.Register(SentinelKey) - .SetCacheExpiration(TimeSpan.FromSeconds(1)); + .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); - + refresher = options.GetRefresher(); }) .Build(); - + // Verify initial values Assert.Equal("InitialValue1", config[$"{TestKeyPrefix}:Setting1"]); - + // Update data but not sentinel await _configClient.SetConfigurationSettingAsync( new ConfigurationSetting($"{TestKeyPrefix}:Setting1", "UpdatedValue1")); - + // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); - + // Act var result = await refresher.TryRefreshAsync(); - + // Assert Assert.False(result); // Should return false as sentinel hasn't changed Assert.Equal("InitialValue1", config[$"{TestKeyPrefix}:Setting1"]); // Should not update diff --git a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj index 49c58ec6..eb1d27e6 100644 --- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj +++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj @@ -21,6 +21,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/Tests.AzureAppConfiguration/Unit/FeatureManagementTests.cs b/tests/Tests.AzureAppConfiguration/Unit/FeatureManagementTests.cs index d96e9e39..075a8ef4 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/FeatureManagementTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/FeatureManagementTests.cs @@ -731,6 +731,7 @@ public async Task WatchesFeatureFlags() } [Fact] + [Obsolete] public async Task WatchesFeatureFlagsUsingCacheExpirationInterval() { var featureFlags = new List { _kv }; @@ -874,6 +875,7 @@ public async Task SkipRefreshIfRefreshIntervalHasNotElapsed() } [Fact] + [Obsolete] public async Task SkipRefreshIfCacheNotExpired() { var featureFlags = new List { _kv }; @@ -1199,6 +1201,7 @@ MockAsyncPageable GetTestKeys(SettingSelector selector, CancellationToken ct) } [Fact] + [Obsolete] public void AlternateValidFeatureFlagFormats() { var mockResponse = new Mock(); From 8d6db8c5c33cf16ae30cae455f403e2f0c8ae423 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 3 Mar 2025 16:36:30 -0800 Subject: [PATCH 06/11] WIP create config store --- .../Integration/IntegrationTests.cs | 177 +++++++++++++++--- .../Tests.AzureAppConfiguration.csproj | 2 + .../local.settings.json.template | 6 + 3 files changed, 160 insertions(+), 25 deletions(-) create mode 100644 tests/Tests.AzureAppConfiguration/local.settings.json.template diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 418bfcc8..70cb26cb 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -1,11 +1,17 @@ using Azure; +using Azure.Core; using Azure.Data.AppConfiguration; using Azure.Identity; +using Azure.ResourceManager; +using Azure.ResourceManager.AppConfiguration; +using Azure.ResourceManager.AppConfiguration.Models; +using Azure.ResourceManager.Resources; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.AzureAppConfiguration; using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement; using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -14,7 +20,8 @@ namespace Tests.AzureAppConfiguration { /// /// Integration tests for Azure App Configuration that connect to a real service. - /// Requires valid connection details to be provided through environment variables or other secure methods. + /// Creates a temporary App Configuration store for testing and deletes it after the tests are complete. + /// Requires Azure credentials with appropriate permissions. /// [Trait("Category", "Integration")] public class IntegrationTests : IAsyncLifetime @@ -24,6 +31,14 @@ public class IntegrationTests : IAsyncLifetime private const string SentinelKey = TestKeyPrefix + ":Sentinel"; private const string FeatureFlagKey = ".appconfig.featureflag/" + TestKeyPrefix + "Feature"; + // Azure Resource Management constants + private const string ResourceGroupEnvVar = "AZURE_APPCONFIG_RESOURCE_GROUP"; + private const string SubscriptionIdEnvVar = "AZURE_SUBSCRIPTION_ID"; + private const string LocationEnvVar = "AZURE_LOCATION"; + private const string CreateResourceGroupEnvVar = "AZURE_CREATE_RESOURCE_GROUP"; + private const string DefaultLocation = "westus"; + private const string LocalSettingsFile = "local.settings.json"; + // Keys to create for testing private readonly List _testSettings = new List { @@ -39,89 +54,201 @@ public class IntegrationTests : IAsyncLifetime // Client for direct manipulation of the store private ConfigurationClient _configClient; + // Store management resources + private ArmClient _armClient; + private string _testStoreName; + private string _testResourceGroupName; + private bool _shouldDeleteResourceGroup = false; + private AppConfigurationStoreResource _appConfigStore; + private Uri _appConfigEndpoint; + private ResourceGroupResource _resourceGroup; + // Flag indicating whether tests should run private bool _skipTests = false; private string _skipReason = null; /// - /// Gets a DefaultAzureCredential for authentication (alternative to connection string). + /// Loads environment variables from a local settings file if it exists. /// - private DefaultAzureCredential GetCredential() + private void LoadEnvironmentVariablesFromFile() { - return new DefaultAzureCredential(); + string localSettingsPath = Path.Combine(AppContext.BaseDirectory, LocalSettingsFile); + + if (File.Exists(localSettingsPath)) + { + Console.WriteLine($"Loading settings from {localSettingsPath}"); + try + { + var config = new ConfigurationBuilder() + .AddJsonFile(localSettingsPath, optional: true) + .Build(); + + foreach (var setting in config.AsEnumerable()) + { + if (!string.IsNullOrEmpty(setting.Value) && + Environment.GetEnvironmentVariable(setting.Key) == null) + { + Environment.SetEnvironmentVariable(setting.Key, setting.Value); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error loading local settings: {ex.Message}"); + } + } } /// - /// Gets the endpoint for the App Configuration store. + /// Gets a DefaultAzureCredential for authentication. /// - private Uri GetEndpoint() + private DefaultAzureCredential GetCredential() { - string endpoint = Environment.GetEnvironmentVariable("AZURE_APPCONFIG_INTEGRATIONTEST_ENDPOINT"); - - if (string.IsNullOrEmpty(endpoint)) + try + { + return new DefaultAzureCredential(new DefaultAzureCredentialOptions + { + ExcludeSharedTokenCacheCredential = true + }); + } + catch (CredentialUnavailableException ex) { _skipTests = true; - _skipReason = "AZURE_APPCONFIG_INTEGRATIONTEST_ENDPOINT environment variable is missing"; + _skipReason = $"Azure credentials unavailable: {ex.Message}"; return null; } + } - return new Uri(endpoint); + /// + /// Gets the endpoint for the App Configuration store. + /// + private Uri GetEndpoint() + { + return _appConfigEndpoint; } /// - /// Creates test data in the Azure App Configuration store before running tests. + /// Creates a temporary Azure App Configuration store and adds test data. /// public async Task InitializeAsync() { try { - Uri endpoint = GetEndpoint(); + // Load environment variables from local.settings.json if present + LoadEnvironmentVariablesFromFile(); + + var credential = GetCredential(); + if (_skipTests) return; - // If endpoint is null, skip all tests - if (_skipTests) + // Get required Azure information from environment variables + string resourceGroupName = Environment.GetEnvironmentVariable(ResourceGroupEnvVar); + string subscriptionIdStr = Environment.GetEnvironmentVariable(SubscriptionIdEnvVar); + string location = Environment.GetEnvironmentVariable(LocationEnvVar) ?? DefaultLocation; + bool createResourceGroup = string.Equals(Environment.GetEnvironmentVariable(CreateResourceGroupEnvVar), "true", StringComparison.OrdinalIgnoreCase); + + if (string.IsNullOrEmpty(subscriptionIdStr)) { - Console.WriteLine($"Integration tests will be skipped: {_skipReason}"); + _skipTests = true; + _skipReason = $"Missing required environment variable: {SubscriptionIdEnvVar}"; return; } - _configClient = new ConfigurationClient(endpoint, GetCredential()); + // Initialize Azure Resource Manager client + _armClient = new ArmClient(credential); + SubscriptionResource subscription = _armClient.GetDefaultSubscription(); + + // Create resource group if requested or use existing one + if (createResourceGroup) + { + _testResourceGroupName = $"appconfig-test-{Guid.NewGuid():N}".Substring(0, 20); + Console.WriteLine($"Creating temporary resource group: {_testResourceGroupName}"); + + var rgData = new ResourceGroupData(AzureLocation.Parse(location)); + var rgLro = await subscription.GetResourceGroups().CreateOrUpdateAsync(WaitUntil.Completed, _testResourceGroupName, rgData); + _resourceGroup = rgLro.Value; + _shouldDeleteResourceGroup = true; + } + else + { + if (string.IsNullOrEmpty(resourceGroupName)) + { + _skipTests = true; + _skipReason = $"Missing required environment variable: {ResourceGroupEnvVar}"; + return; + } + + _testResourceGroupName = resourceGroupName; + _resourceGroup = await subscription.GetResourceGroups().GetAsync(resourceGroupName); + } + + // Create unique store name for this test run + _testStoreName = $"integration-{Guid.NewGuid():N}".Substring(0, 20); + Console.WriteLine($"Creating test App Configuration store: {_testStoreName}"); + + // Create the App Configuration store + var storeData = new AppConfigurationStoreData(AzureLocation.Parse(location), new AppConfigurationSku("free")); + var createOperation = await _resourceGroup.GetAppConfigurationStores().CreateOrUpdateAsync( + WaitUntil.Completed, + _testStoreName, + storeData); + + _appConfigStore = createOperation.Value; + _appConfigEndpoint = new Uri(_appConfigStore.Data.Endpoint); + + Console.WriteLine($"Store created: {_appConfigEndpoint}"); + + // Initialize the configuration client for the store + _configClient = new ConfigurationClient(_appConfigEndpoint, credential); // Add test settings to the store foreach (var setting in _testSettings) { await _configClient.SetConfigurationSettingAsync(setting); } + + Console.WriteLine("Test data initialized successfully"); } catch (Exception ex) { Console.WriteLine($"Test initialization failed: {ex}"); _skipTests = true; _skipReason = $"Failed to initialize integration tests: {ex.Message}"; + + // Clean up any partially created resources + await DisposeAsync(); } } /// - /// Cleans up test data after tests are complete. + /// Cleans up the temporary App Configuration store after tests are complete. /// public async Task DisposeAsync() { - // Don't attempt cleanup if we're skipping tests - if (_skipTests || _configClient == null) + // Don't attempt cleanup if we don't have a store to delete + if (_appConfigStore == null && !_shouldDeleteResourceGroup) { return; } try { - // Remove test settings from the store - foreach (var setting in _testSettings) + if (_appConfigStore != null) + { + Console.WriteLine($"Deleting test App Configuration store: {_testStoreName}"); + await _appConfigStore.DeleteAsync(WaitUntil.Completed); + Console.WriteLine("Store deleted successfully"); + } + + if (_shouldDeleteResourceGroup && _resourceGroup != null) { - await _configClient.DeleteConfigurationSettingAsync(setting.Key, setting.Label); + Console.WriteLine($"Deleting temporary resource group: {_testResourceGroupName}"); + await _resourceGroup.DeleteAsync(WaitUntil.Completed); + Console.WriteLine("Resource group deleted successfully"); } } catch (Exception ex) { - Console.WriteLine($"Test cleanup failed: {ex}"); + Console.WriteLine($"Test cleanup failed: {ex}. You may need to manually delete the resources: Store={_testStoreName}, ResourceGroup={(_shouldDeleteResourceGroup ? _testResourceGroupName : "N/A")}"); } } @@ -187,7 +314,7 @@ await _configClient.SetConfigurationSettingAsync( Assert.Equal("UpdatedValue1", config[$"{TestKeyPrefix}:Setting1"]); } - [SkippableFact] + [Fact] public async Task TryRefreshAsync_RefreshesOnlySelectedKeys_WhenUsingKeyFilter() { Skip.If(_skipTests, _skipReason); diff --git a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj index eb1d27e6..ec29d616 100644 --- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj +++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj @@ -12,6 +12,8 @@ + + diff --git a/tests/Tests.AzureAppConfiguration/local.settings.json.template b/tests/Tests.AzureAppConfiguration/local.settings.json.template new file mode 100644 index 00000000..f180a652 --- /dev/null +++ b/tests/Tests.AzureAppConfiguration/local.settings.json.template @@ -0,0 +1,6 @@ +{ + "AZURE_SUBSCRIPTION_ID": "", + "AZURE_APPCONFIG_RESOURCE_GROUP": "", + "AZURE_LOCATION": "", + "AZURE_CREATE_RESOURCE_GROUP": "true" +} \ No newline at end of file From 10ccabc94de97ee0ea211542ce9e6fccc05079b5 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 4 Mar 2025 12:14:11 -0800 Subject: [PATCH 07/11] edit --- .../Integration/IntegrationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 70cb26cb..a39f1808 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -163,7 +163,7 @@ public async Task InitializeAsync() _testResourceGroupName = $"appconfig-test-{Guid.NewGuid():N}".Substring(0, 20); Console.WriteLine($"Creating temporary resource group: {_testResourceGroupName}"); - var rgData = new ResourceGroupData(AzureLocation.Parse(location)); + var rgData = new ResourceGroupData(new AzureLocation(location)); var rgLro = await subscription.GetResourceGroups().CreateOrUpdateAsync(WaitUntil.Completed, _testResourceGroupName, rgData); _resourceGroup = rgLro.Value; _shouldDeleteResourceGroup = true; @@ -186,7 +186,7 @@ public async Task InitializeAsync() Console.WriteLine($"Creating test App Configuration store: {_testStoreName}"); // Create the App Configuration store - var storeData = new AppConfigurationStoreData(AzureLocation.Parse(location), new AppConfigurationSku("free")); + var storeData = new AppConfigurationStoreData(new AzureLocation(location), new AppConfigurationSku("free")); var createOperation = await _resourceGroup.GetAppConfigurationStores().CreateOrUpdateAsync( WaitUntil.Completed, _testStoreName, From 6dd421ac568751a9a3453c90295ce33c723b5f53 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 4 Mar 2025 14:29:41 -0800 Subject: [PATCH 08/11] update integration setup --- .../Integration/IntegrationTests.cs | 232 +++++++++++++----- .../Tests.AzureAppConfiguration.csproj | 8 +- 2 files changed, 179 insertions(+), 61 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index a39f1808..788ce5a6 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -24,6 +24,7 @@ namespace Tests.AzureAppConfiguration /// Requires Azure credentials with appropriate permissions. /// [Trait("Category", "Integration")] + [CollectionDefinition(nameof(IntegrationTests), DisableParallelization = true)] public class IntegrationTests : IAsyncLifetime { // Test constants @@ -36,9 +37,14 @@ public class IntegrationTests : IAsyncLifetime private const string SubscriptionIdEnvVar = "AZURE_SUBSCRIPTION_ID"; private const string LocationEnvVar = "AZURE_LOCATION"; private const string CreateResourceGroupEnvVar = "AZURE_CREATE_RESOURCE_GROUP"; - private const string DefaultLocation = "westus"; + private const string DefaultLocation = "eastus"; private const string LocalSettingsFile = "local.settings.json"; + // Retry configuration for RBAC permission propagation + private const int MaxRetryAttempts = 5; + private static readonly TimeSpan InitialBackoff = TimeSpan.FromSeconds(1); + private static readonly TimeSpan MaxBackoff = TimeSpan.FromSeconds(15); + // Keys to create for testing private readonly List _testSettings = new List { @@ -127,6 +133,66 @@ private Uri GetEndpoint() return _appConfigEndpoint; } + /// + /// Sets a configuration setting with retries to handle RBAC permission propagation delay + /// + /// The configuration setting to add or update + private async Task SetConfigurationSettingWithRetryAsync(ConfigurationSetting setting) + { + int attempt = 0; + TimeSpan backoff = InitialBackoff; + + while (true) + { + try + { + attempt++; + + await _configClient.SetConfigurationSettingAsync(setting); + + // Successfully set the setting, exit the loop + return; + } + catch (RequestFailedException ex) when ( + (ex.Status == 403 || ex.Status == 401) && // Permission/authorization issues + attempt < MaxRetryAttempts) + { + // Calculate exponential backoff with jitter + Random jitter = new Random(); + double jitterFactor = 0.8 + (jitter.NextDouble() * 0.4); // 0.8-1.2 jitter factor + TimeSpan delay = TimeSpan.FromMilliseconds(backoff.TotalMilliseconds * jitterFactor); + + // Don't exceed max backoff + if (delay > MaxBackoff) delay = MaxBackoff; + + Console.WriteLine($"RBAC permissions not propagated yet (attempt {attempt}/{MaxRetryAttempts}). Retrying in {delay.TotalSeconds:0.##}s..."); + + await Task.Delay(delay); + + // Exponential backoff: double the delay for next attempt + backoff = TimeSpan.FromMilliseconds(backoff.TotalMilliseconds * 2); + } + catch (Exception ex) when (attempt < MaxRetryAttempts) + { + // Handle other transient errors with retry + Console.WriteLine($"Error setting configuration (attempt {attempt}/{MaxRetryAttempts}): {ex.Message}"); + + await Task.Delay(backoff); + + // Exponential backoff: double the delay for next attempt + backoff = TimeSpan.FromMilliseconds(backoff.TotalMilliseconds * 2); + if (backoff > MaxBackoff) backoff = MaxBackoff; + } + + // If we've reached max attempts, let the final exception propagate + if (attempt >= MaxRetryAttempts) + { + await _configClient.SetConfigurationSettingAsync(setting); // Let any exception propagate + break; + } + } + } + /// /// Creates a temporary Azure App Configuration store and adds test data. /// @@ -155,7 +221,8 @@ public async Task InitializeAsync() // Initialize Azure Resource Manager client _armClient = new ArmClient(credential); - SubscriptionResource subscription = _armClient.GetDefaultSubscription(); + + SubscriptionResource subscription = _armClient.GetSubscriptions().Get(subscriptionIdStr); // Create resource group if requested or use existing one if (createResourceGroup) @@ -200,10 +267,11 @@ public async Task InitializeAsync() // Initialize the configuration client for the store _configClient = new ConfigurationClient(_appConfigEndpoint, credential); - // Add test settings to the store + // Add test settings to the store with retry logic to handle RBAC permission propagation delays + Console.WriteLine("Setting up initial test data..."); foreach (var setting in _testSettings) { - await _configClient.SetConfigurationSettingAsync(setting); + await SetConfigurationSettingWithRetryAsync(setting); } Console.WriteLine("Test data initialized successfully"); @@ -252,6 +320,45 @@ public async Task DisposeAsync() } } + /// + /// Creates a unique prefix for test keys to ensure test isolation + /// + private string GetUniqueKeyPrefix(string testName) + { + // Use a combination of the test prefix and test method name to ensure uniqueness + return $"{TestKeyPrefix}_{testName}_{Guid.NewGuid().ToString("N").Substring(0, 8)}"; + } + + /// + /// Setup test-specific keys and settings + /// + private async Task<(string keyPrefix, string sentinelKey, string featureFlagKey)> SetupTestKeys(string testName) + { + string keyPrefix = GetUniqueKeyPrefix(testName); + string sentinelKey = $"{keyPrefix}:Sentinel"; + string featureFlagKey = $".appconfig.featureflag/{keyPrefix}Feature"; + + // Create test-specific settings + var testSettings = new List + { + new ConfigurationSetting($"{keyPrefix}:Setting1", "InitialValue1"), + new ConfigurationSetting($"{keyPrefix}:Setting2", "InitialValue2"), + new ConfigurationSetting(sentinelKey, "Initial"), + ConfigurationModelFactory.ConfigurationSetting( + featureFlagKey, + @"{""id"":""" + keyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":false}", + contentType: FeatureManagementConstants.ContentType) + }; + + // Add test-specific settings to the store with retry logic + foreach (var setting in testSettings) + { + await SetConfigurationSettingWithRetryAsync(setting); + } + + return (keyPrefix, sentinelKey, featureFlagKey); + } + [Fact] public void LoadConfiguration_RetrievesValuesFromAppConfiguration() { @@ -276,17 +383,18 @@ public async Task TryRefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() { Skip.If(_skipTests, _skipReason); - // Arrange + // Arrange - Setup test-specific keys + var (keyPrefix, sentinelKey, _) = await SetupTestKeys("UpdatesConfig"); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { options.Connect(GetEndpoint(), GetCredential()); - options.Select($"{TestKeyPrefix}:*"); + options.Select($"{keyPrefix}:*"); options.ConfigureRefresh(refresh => { - refresh.Register(SentinelKey, refreshAll: true) + refresh.Register(sentinelKey, refreshAll: true) .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); @@ -295,13 +403,13 @@ public async Task TryRefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() .Build(); // Verify initial values - Assert.Equal("InitialValue1", config[$"{TestKeyPrefix}:Setting1"]); + Assert.Equal("InitialValue1", config[$"{keyPrefix}:Setting1"]); - // Update values in the store - await _configClient.SetConfigurationSettingAsync( - new ConfigurationSetting($"{TestKeyPrefix}:Setting1", "UpdatedValue1")); - await _configClient.SetConfigurationSettingAsync( - new ConfigurationSetting(SentinelKey, "Updated")); + // Update values in the store with retry logic + await SetConfigurationSettingWithRetryAsync( + new ConfigurationSetting($"{keyPrefix}:Setting1", "UpdatedValue1")); + await SetConfigurationSettingWithRetryAsync( + new ConfigurationSetting(sentinelKey, "Updated")); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); @@ -311,7 +419,7 @@ await _configClient.SetConfigurationSettingAsync( // Assert Assert.True(result); - Assert.Equal("UpdatedValue1", config[$"{TestKeyPrefix}:Setting1"]); + Assert.Equal("UpdatedValue1", config[$"{keyPrefix}:Setting1"]); } [Fact] @@ -319,19 +427,20 @@ public async Task TryRefreshAsync_RefreshesOnlySelectedKeys_WhenUsingKeyFilter() { Skip.If(_skipTests, _skipReason); - // Arrange + // Arrange - Setup test-specific keys + var (keyPrefix, sentinelKey, _) = await SetupTestKeys("RefreshesSelectedKeys"); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { options.Connect(GetEndpoint(), GetCredential()); - options.Select($"{TestKeyPrefix}:*"); + options.Select($"{keyPrefix}:*"); // Only refresh Setting1 when sentinel changes options.ConfigureRefresh(refresh => { - refresh.Register(SentinelKey, $"{TestKeyPrefix}:Setting1", refreshAll: false) + refresh.Register(sentinelKey, $"{keyPrefix}:Setting1", refreshAll: false) .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); @@ -340,16 +449,16 @@ public async Task TryRefreshAsync_RefreshesOnlySelectedKeys_WhenUsingKeyFilter() .Build(); // Verify initial values - Assert.Equal("InitialValue1", config[$"{TestKeyPrefix}:Setting1"]); - Assert.Equal("InitialValue2", config[$"{TestKeyPrefix}:Setting2"]); + Assert.Equal("InitialValue1", config[$"{keyPrefix}:Setting1"]); + Assert.Equal("InitialValue2", config[$"{keyPrefix}:Setting2"]); - // Update values in the store - await _configClient.SetConfigurationSettingAsync( - new ConfigurationSetting($"{TestKeyPrefix}:Setting1", "UpdatedValue1")); - await _configClient.SetConfigurationSettingAsync( - new ConfigurationSetting($"{TestKeyPrefix}:Setting2", "UpdatedValue2")); - await _configClient.SetConfigurationSettingAsync( - new ConfigurationSetting(SentinelKey, "Updated")); + // Update values in the store with retry logic + await SetConfigurationSettingWithRetryAsync( + new ConfigurationSetting($"{keyPrefix}:Setting1", "UpdatedValue1")); + await SetConfigurationSettingWithRetryAsync( + new ConfigurationSetting($"{keyPrefix}:Setting2", "UpdatedValue2")); + await SetConfigurationSettingWithRetryAsync( + new ConfigurationSetting(sentinelKey, "Updated")); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); @@ -359,8 +468,8 @@ await _configClient.SetConfigurationSettingAsync( // Assert Assert.True(result); - Assert.Equal("UpdatedValue1", config[$"{TestKeyPrefix}:Setting1"]); - Assert.Equal("InitialValue2", config[$"{TestKeyPrefix}:Setting2"]); // This value shouldn't change + Assert.Equal("UpdatedValue1", config[$"{keyPrefix}:Setting1"]); + Assert.Equal("InitialValue2", config[$"{keyPrefix}:Setting2"]); // This value shouldn't change } [Fact] @@ -368,19 +477,20 @@ public async Task TryRefreshAsync_RefreshesFeatureFlags_WhenConfigured() { Skip.If(_skipTests, _skipReason); - // Arrange + // Arrange - Setup test-specific keys + var (keyPrefix, sentinelKey, featureFlagKey) = await SetupTestKeys("RefreshesFeatureFlags"); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { options.Connect(GetEndpoint(), GetCredential()); - options.Select($"{TestKeyPrefix}:*"); + options.Select($"{keyPrefix}:*"); options.UseFeatureFlags(); options.ConfigureRefresh(refresh => { - refresh.Register(SentinelKey) + refresh.Register(sentinelKey) .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); @@ -389,16 +499,16 @@ public async Task TryRefreshAsync_RefreshesFeatureFlags_WhenConfigured() .Build(); // Verify initial feature flag state - Assert.Equal("False", config[$"FeatureManagement:{TestKeyPrefix}Feature:Enabled"]); + Assert.Equal("False", config[$"FeatureManagement:{keyPrefix}Feature:Enabled"]); - // Update feature flag in the store - await _configClient.SetConfigurationSettingAsync( + // Update feature flag in the store with retry logic + await SetConfigurationSettingWithRetryAsync( ConfigurationModelFactory.ConfigurationSetting( - FeatureFlagKey, - @"{""id"":""" + TestKeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":true}", + featureFlagKey, + @"{""id"":""" + keyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":true}", contentType: FeatureManagementConstants.ContentType)); - await _configClient.SetConfigurationSettingAsync( - new ConfigurationSetting(SentinelKey, "Updated")); + await SetConfigurationSettingWithRetryAsync( + new ConfigurationSetting(sentinelKey, "Updated")); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); @@ -408,7 +518,7 @@ await _configClient.SetConfigurationSettingAsync( // Assert Assert.True(result); - Assert.Equal("True", config[$"FeatureManagement:{TestKeyPrefix}Feature:Enabled"]); + Assert.Equal("True", config[$"FeatureManagement:{keyPrefix}Feature:Enabled"]); } [Fact] @@ -416,14 +526,15 @@ public async Task RegisterAll_RefreshesAllKeys_WhenSentinelChanged() { Skip.If(_skipTests, _skipReason); - // Arrange + // Arrange - Setup test-specific keys + var (keyPrefix, sentinelKey, _) = await SetupTestKeys("RefreshesAllKeys"); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { options.Connect(GetEndpoint(), GetCredential()); - options.Select($"{TestKeyPrefix}:*"); + options.Select($"{keyPrefix}:*"); // Use RegisterAll to refresh everything when sentinel changes options.ConfigureRefresh(refresh => @@ -437,16 +548,16 @@ public async Task RegisterAll_RefreshesAllKeys_WhenSentinelChanged() .Build(); // Verify initial values - Assert.Equal("InitialValue1", config[$"{TestKeyPrefix}:Setting1"]); - Assert.Equal("InitialValue2", config[$"{TestKeyPrefix}:Setting2"]); + Assert.Equal("InitialValue1", config[$"{keyPrefix}:Setting1"]); + Assert.Equal("InitialValue2", config[$"{keyPrefix}:Setting2"]); - // Update all values in the store - await _configClient.SetConfigurationSettingAsync( - new ConfigurationSetting($"{TestKeyPrefix}:Setting1", "UpdatedValue1")); - await _configClient.SetConfigurationSettingAsync( - new ConfigurationSetting($"{TestKeyPrefix}:Setting2", "UpdatedValue2")); - await _configClient.SetConfigurationSettingAsync( - new ConfigurationSetting(SentinelKey, "Updated")); + // Update all values in the store with retry logic + await SetConfigurationSettingWithRetryAsync( + new ConfigurationSetting($"{keyPrefix}:Setting1", "UpdatedValue1")); + await SetConfigurationSettingWithRetryAsync( + new ConfigurationSetting($"{keyPrefix}:Setting2", "UpdatedValue2")); + await SetConfigurationSettingWithRetryAsync( + new ConfigurationSetting(sentinelKey, "Updated")); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); @@ -456,8 +567,8 @@ await _configClient.SetConfigurationSettingAsync( // Assert Assert.True(result); - Assert.Equal("UpdatedValue1", config[$"{TestKeyPrefix}:Setting1"]); - Assert.Equal("UpdatedValue2", config[$"{TestKeyPrefix}:Setting2"]); + Assert.Equal("UpdatedValue1", config[$"{keyPrefix}:Setting1"]); + Assert.Equal("UpdatedValue2", config[$"{keyPrefix}:Setting2"]); } [Fact] @@ -465,18 +576,19 @@ public async Task TryRefreshAsync_ReturnsFalse_WhenSentinelKeyUnchanged() { Skip.If(_skipTests, _skipReason); - // Arrange + // Arrange - Setup test-specific keys + var (keyPrefix, sentinelKey, _) = await SetupTestKeys("SentinelUnchanged"); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { options.Connect(GetEndpoint(), GetCredential()); - options.Select($"{TestKeyPrefix}:*"); + options.Select($"{keyPrefix}:*"); options.ConfigureRefresh(refresh => { - refresh.Register(SentinelKey) + refresh.Register(sentinelKey) .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); @@ -485,11 +597,11 @@ public async Task TryRefreshAsync_ReturnsFalse_WhenSentinelKeyUnchanged() .Build(); // Verify initial values - Assert.Equal("InitialValue1", config[$"{TestKeyPrefix}:Setting1"]); + Assert.Equal("InitialValue1", config[$"{keyPrefix}:Setting1"]); - // Update data but not sentinel - await _configClient.SetConfigurationSettingAsync( - new ConfigurationSetting($"{TestKeyPrefix}:Setting1", "UpdatedValue1")); + // Update data but not sentinel with retry logic + await SetConfigurationSettingWithRetryAsync( + new ConfigurationSetting($"{keyPrefix}:Setting1", "UpdatedValue1")); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); @@ -499,7 +611,7 @@ await _configClient.SetConfigurationSettingAsync( // Assert Assert.False(result); // Should return false as sentinel hasn't changed - Assert.Equal("InitialValue1", config[$"{TestKeyPrefix}:Setting1"]); // Should not update + Assert.Equal("InitialValue1", config[$"{keyPrefix}:Setting1"]); // Should not update } } } diff --git a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj index ec29d616..f2573685 100644 --- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj +++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj @@ -7,7 +7,7 @@ true ..\..\build\AzureAppConfiguration.snk false - false + true @@ -27,6 +27,12 @@ + + + Always + + + From 6b24ac942df3822f29998e2fda5ca797feb4c6db Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 6 Mar 2025 11:22:41 -0800 Subject: [PATCH 09/11] copilot updates --- .../Integration/IntegrationTests.cs | 76 +++++++++++++++---- 1 file changed, 63 insertions(+), 13 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 788ce5a6..ef3e12f3 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -41,9 +41,10 @@ public class IntegrationTests : IAsyncLifetime private const string LocalSettingsFile = "local.settings.json"; // Retry configuration for RBAC permission propagation - private const int MaxRetryAttempts = 5; - private static readonly TimeSpan InitialBackoff = TimeSpan.FromSeconds(1); - private static readonly TimeSpan MaxBackoff = TimeSpan.FromSeconds(15); + private const int MaxRetryAttempts = 30; // Significantly increased from 12 to 30 + private static readonly TimeSpan InitialBackoff = TimeSpan.FromSeconds(5); // Increased from 2s to 5s + private static readonly TimeSpan MaxBackoff = TimeSpan.FromSeconds(180); // Increased from 60s to 180s (3 minutes) + private static readonly TimeSpan InitialRbacWaitTime = TimeSpan.FromSeconds(30); // 30-second initial wait after store creation // Keys to create for testing private readonly List _testSettings = new List @@ -141,6 +142,8 @@ private async Task SetConfigurationSettingWithRetryAsync(ConfigurationSetting se { int attempt = 0; TimeSpan backoff = InitialBackoff; + bool hasPermissionErrors = false; + DateTime startTime = DateTime.UtcNow; while (true) { @@ -148,15 +151,28 @@ private async Task SetConfigurationSettingWithRetryAsync(ConfigurationSetting se { attempt++; + Console.WriteLine($"Attempt {attempt}/{MaxRetryAttempts} to set configuration setting '{setting.Key}'..."); await _configClient.SetConfigurationSettingAsync(setting); // Successfully set the setting, exit the loop + TimeSpan elapsed = DateTime.UtcNow - startTime; + if (hasPermissionErrors) + { + Console.WriteLine($"RBAC permissions propagated successfully after {attempt} attempts and {elapsed.TotalSeconds:F1} seconds"); + } + else + { + Console.WriteLine($"Configuration setting '{setting.Key}' was successfully set on attempt {attempt}"); + } return; } catch (RequestFailedException ex) when ( (ex.Status == 403 || ex.Status == 401) && // Permission/authorization issues attempt < MaxRetryAttempts) { + hasPermissionErrors = true; + TimeSpan elapsed = DateTime.UtcNow - startTime; + // Calculate exponential backoff with jitter Random jitter = new Random(); double jitterFactor = 0.8 + (jitter.NextDouble() * 0.4); // 0.8-1.2 jitter factor @@ -165,30 +181,46 @@ private async Task SetConfigurationSettingWithRetryAsync(ConfigurationSetting se // Don't exceed max backoff if (delay > MaxBackoff) delay = MaxBackoff; - Console.WriteLine($"RBAC permissions not propagated yet (attempt {attempt}/{MaxRetryAttempts}). Retrying in {delay.TotalSeconds:0.##}s..."); + Console.WriteLine($"RBAC permissions not propagated yet (attempt {attempt}/{MaxRetryAttempts}, error {ex.Status}: {ex.Message}). " + + $"{elapsed.TotalSeconds:F1}s elapsed. Waiting {delay.TotalSeconds:0.##}s before retry..."); await Task.Delay(delay); - // Exponential backoff: double the delay for next attempt - backoff = TimeSpan.FromMilliseconds(backoff.TotalMilliseconds * 2); + // Slower-growing exponential backoff for auth errors + backoff = TimeSpan.FromMilliseconds(backoff.TotalMilliseconds * 1.2); + if (backoff > MaxBackoff) backoff = MaxBackoff; } catch (Exception ex) when (attempt < MaxRetryAttempts) { // Handle other transient errors with retry Console.WriteLine($"Error setting configuration (attempt {attempt}/{MaxRetryAttempts}): {ex.Message}"); - await Task.Delay(backoff); + // Use a shorter delay for non-auth errors + TimeSpan delay = TimeSpan.FromSeconds(Math.Min(15, backoff.TotalSeconds / 2)); + await Task.Delay(delay); - // Exponential backoff: double the delay for next attempt - backoff = TimeSpan.FromMilliseconds(backoff.TotalMilliseconds * 2); + // Less aggressive backoff for other types of errors + backoff = TimeSpan.FromMilliseconds(backoff.TotalMilliseconds * 1.1); if (backoff > MaxBackoff) backoff = MaxBackoff; } - // If we've reached max attempts, let the final exception propagate + // If we've reached max attempts, try one last time and let any exception propagate if (attempt >= MaxRetryAttempts) { - await _configClient.SetConfigurationSettingAsync(setting); // Let any exception propagate - break; + Console.WriteLine($"Maximum retry attempts ({MaxRetryAttempts}) reached. Making final attempt..."); + try + { + await _configClient.SetConfigurationSettingAsync(setting); + Console.WriteLine($"Final attempt succeeded for setting '{setting.Key}'!"); + return; + } + catch (RequestFailedException ex) when (ex.Status == 403 || ex.Status == 401) + { + TimeSpan elapsed = DateTime.UtcNow - startTime; + throw new TimeoutException( + $"RBAC permissions did not propagate after {elapsed.TotalSeconds:F1} seconds and {MaxRetryAttempts} attempts. " + + $"Last error: {ex.Status} {ex.Message}", ex); + } } } } @@ -263,10 +295,28 @@ public async Task InitializeAsync() _appConfigEndpoint = new Uri(_appConfigStore.Data.Endpoint); Console.WriteLine($"Store created: {_appConfigEndpoint}"); - + + // Add significant pause after store creation to allow initial RBAC permissions to propagate + Console.WriteLine($"Waiting {InitialRbacWaitTime.TotalSeconds} seconds for initial RBAC permissions to propagate..."); + await Task.Delay(InitialRbacWaitTime); + // Initialize the configuration client for the store _configClient = new ConfigurationClient(_appConfigEndpoint, credential); + // Create a simple test setting first to verify permissions + var testSetting = new ConfigurationSetting($"{TestKeyPrefix}:PermissionTest", "TestValue"); + Console.WriteLine("Testing RBAC permissions with a preliminary setting..."); + try + { + await SetConfigurationSettingWithRetryAsync(testSetting); + Console.WriteLine("Preliminary permission test successful, RBAC permissions are active."); + } + catch (Exception ex) + { + Console.WriteLine($"Warning: Initial permission test failed: {ex.Message}"); + Console.WriteLine("Will still attempt to continue with the main test data setup..."); + } + // Add test settings to the store with retry logic to handle RBAC permission propagation delays Console.WriteLine("Setting up initial test data..."); foreach (var setting in _testSettings) From 28a14cc2f55c6a29f90d1fba16d852c87be5bcca Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 6 Mar 2025 17:02:45 -0800 Subject: [PATCH 10/11] initial working version of integration tests --- .../Integration/IntegrationTests.cs | 207 ++++-------------- 1 file changed, 44 insertions(+), 163 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index ef3e12f3..83bf6a13 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -40,12 +41,6 @@ public class IntegrationTests : IAsyncLifetime private const string DefaultLocation = "eastus"; private const string LocalSettingsFile = "local.settings.json"; - // Retry configuration for RBAC permission propagation - private const int MaxRetryAttempts = 30; // Significantly increased from 12 to 30 - private static readonly TimeSpan InitialBackoff = TimeSpan.FromSeconds(5); // Increased from 2s to 5s - private static readonly TimeSpan MaxBackoff = TimeSpan.FromSeconds(180); // Increased from 60s to 180s (3 minutes) - private static readonly TimeSpan InitialRbacWaitTime = TimeSpan.FromSeconds(30); // 30-second initial wait after store creation - // Keys to create for testing private readonly List _testSettings = new List { @@ -61,6 +56,9 @@ public class IntegrationTests : IAsyncLifetime // Client for direct manipulation of the store private ConfigurationClient _configClient; + // Connection string for the store + private string _connectionString; + // Store management resources private ArmClient _armClient; private string _testStoreName; @@ -83,7 +81,6 @@ private void LoadEnvironmentVariablesFromFile() if (File.Exists(localSettingsPath)) { - Console.WriteLine($"Loading settings from {localSettingsPath}"); try { var config = new ConfigurationBuilder() @@ -127,102 +124,11 @@ private DefaultAzureCredential GetCredential() } /// - /// Gets the endpoint for the App Configuration store. + /// Returns the connection string for connecting to the app configuration store. /// - private Uri GetEndpoint() + private string GetConnectionString() { - return _appConfigEndpoint; - } - - /// - /// Sets a configuration setting with retries to handle RBAC permission propagation delay - /// - /// The configuration setting to add or update - private async Task SetConfigurationSettingWithRetryAsync(ConfigurationSetting setting) - { - int attempt = 0; - TimeSpan backoff = InitialBackoff; - bool hasPermissionErrors = false; - DateTime startTime = DateTime.UtcNow; - - while (true) - { - try - { - attempt++; - - Console.WriteLine($"Attempt {attempt}/{MaxRetryAttempts} to set configuration setting '{setting.Key}'..."); - await _configClient.SetConfigurationSettingAsync(setting); - - // Successfully set the setting, exit the loop - TimeSpan elapsed = DateTime.UtcNow - startTime; - if (hasPermissionErrors) - { - Console.WriteLine($"RBAC permissions propagated successfully after {attempt} attempts and {elapsed.TotalSeconds:F1} seconds"); - } - else - { - Console.WriteLine($"Configuration setting '{setting.Key}' was successfully set on attempt {attempt}"); - } - return; - } - catch (RequestFailedException ex) when ( - (ex.Status == 403 || ex.Status == 401) && // Permission/authorization issues - attempt < MaxRetryAttempts) - { - hasPermissionErrors = true; - TimeSpan elapsed = DateTime.UtcNow - startTime; - - // Calculate exponential backoff with jitter - Random jitter = new Random(); - double jitterFactor = 0.8 + (jitter.NextDouble() * 0.4); // 0.8-1.2 jitter factor - TimeSpan delay = TimeSpan.FromMilliseconds(backoff.TotalMilliseconds * jitterFactor); - - // Don't exceed max backoff - if (delay > MaxBackoff) delay = MaxBackoff; - - Console.WriteLine($"RBAC permissions not propagated yet (attempt {attempt}/{MaxRetryAttempts}, error {ex.Status}: {ex.Message}). " + - $"{elapsed.TotalSeconds:F1}s elapsed. Waiting {delay.TotalSeconds:0.##}s before retry..."); - - await Task.Delay(delay); - - // Slower-growing exponential backoff for auth errors - backoff = TimeSpan.FromMilliseconds(backoff.TotalMilliseconds * 1.2); - if (backoff > MaxBackoff) backoff = MaxBackoff; - } - catch (Exception ex) when (attempt < MaxRetryAttempts) - { - // Handle other transient errors with retry - Console.WriteLine($"Error setting configuration (attempt {attempt}/{MaxRetryAttempts}): {ex.Message}"); - - // Use a shorter delay for non-auth errors - TimeSpan delay = TimeSpan.FromSeconds(Math.Min(15, backoff.TotalSeconds / 2)); - await Task.Delay(delay); - - // Less aggressive backoff for other types of errors - backoff = TimeSpan.FromMilliseconds(backoff.TotalMilliseconds * 1.1); - if (backoff > MaxBackoff) backoff = MaxBackoff; - } - - // If we've reached max attempts, try one last time and let any exception propagate - if (attempt >= MaxRetryAttempts) - { - Console.WriteLine($"Maximum retry attempts ({MaxRetryAttempts}) reached. Making final attempt..."); - try - { - await _configClient.SetConfigurationSettingAsync(setting); - Console.WriteLine($"Final attempt succeeded for setting '{setting.Key}'!"); - return; - } - catch (RequestFailedException ex) when (ex.Status == 403 || ex.Status == 401) - { - TimeSpan elapsed = DateTime.UtcNow - startTime; - throw new TimeoutException( - $"RBAC permissions did not propagate after {elapsed.TotalSeconds:F1} seconds and {MaxRetryAttempts} attempts. " + - $"Last error: {ex.Status} {ex.Message}", ex); - } - } - } + return _connectionString; } /// @@ -260,7 +166,6 @@ public async Task InitializeAsync() if (createResourceGroup) { _testResourceGroupName = $"appconfig-test-{Guid.NewGuid():N}".Substring(0, 20); - Console.WriteLine($"Creating temporary resource group: {_testResourceGroupName}"); var rgData = new ResourceGroupData(new AzureLocation(location)); var rgLro = await subscription.GetResourceGroups().CreateOrUpdateAsync(WaitUntil.Completed, _testResourceGroupName, rgData); @@ -282,7 +187,6 @@ public async Task InitializeAsync() // Create unique store name for this test run _testStoreName = $"integration-{Guid.NewGuid():N}".Substring(0, 20); - Console.WriteLine($"Creating test App Configuration store: {_testStoreName}"); // Create the App Configuration store var storeData = new AppConfigurationStoreData(new AzureLocation(location), new AppConfigurationSku("free")); @@ -294,41 +198,28 @@ public async Task InitializeAsync() _appConfigStore = createOperation.Value; _appConfigEndpoint = new Uri(_appConfigStore.Data.Endpoint); - Console.WriteLine($"Store created: {_appConfigEndpoint}"); - - // Add significant pause after store creation to allow initial RBAC permissions to propagate - Console.WriteLine($"Waiting {InitialRbacWaitTime.TotalSeconds} seconds for initial RBAC permissions to propagate..."); - await Task.Delay(InitialRbacWaitTime); - - // Initialize the configuration client for the store - _configClient = new ConfigurationClient(_appConfigEndpoint, credential); - - // Create a simple test setting first to verify permissions - var testSetting = new ConfigurationSetting($"{TestKeyPrefix}:PermissionTest", "TestValue"); - Console.WriteLine("Testing RBAC permissions with a preliminary setting..."); - try - { - await SetConfigurationSettingWithRetryAsync(testSetting); - Console.WriteLine("Preliminary permission test successful, RBAC permissions are active."); - } - catch (Exception ex) + // Get the connection string for the store instead of using RBAC + var accessKeys = _appConfigStore.GetKeysAsync(); + var primaryKey = await accessKeys.FirstOrDefaultAsync(); + + if (primaryKey == null) { - Console.WriteLine($"Warning: Initial permission test failed: {ex.Message}"); - Console.WriteLine("Will still attempt to continue with the main test data setup..."); + throw new InvalidOperationException("Failed to retrieve access keys from App Configuration store."); } - // Add test settings to the store with retry logic to handle RBAC permission propagation delays - Console.WriteLine("Setting up initial test data..."); + _connectionString = primaryKey.ConnectionString; + + // Initialize the configuration client with the connection string + _configClient = new ConfigurationClient(_connectionString); + + // Add test settings to the store foreach (var setting in _testSettings) { - await SetConfigurationSettingWithRetryAsync(setting); + await _configClient.SetConfigurationSettingAsync(setting); } - - Console.WriteLine("Test data initialized successfully"); } catch (Exception ex) { - Console.WriteLine($"Test initialization failed: {ex}"); _skipTests = true; _skipReason = $"Failed to initialize integration tests: {ex.Message}"; @@ -400,10 +291,10 @@ private string GetUniqueKeyPrefix(string testName) contentType: FeatureManagementConstants.ContentType) }; - // Add test-specific settings to the store with retry logic + // Add test-specific settings to the store foreach (var setting in testSettings) { - await SetConfigurationSettingWithRetryAsync(setting); + await _configClient.SetConfigurationSettingAsync(setting); } return (keyPrefix, sentinelKey, featureFlagKey); @@ -418,7 +309,7 @@ public void LoadConfiguration_RetrievesValuesFromAppConfiguration() var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetEndpoint(), GetCredential()); + options.Connect(GetConnectionString()); options.Select($"{TestKeyPrefix}:*"); }) .Build(); @@ -440,7 +331,7 @@ public async Task TryRefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetEndpoint(), GetCredential()); + options.Connect(GetConnectionString()); options.Select($"{keyPrefix}:*"); options.ConfigureRefresh(refresh => { @@ -455,11 +346,9 @@ public async Task TryRefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() // Verify initial values Assert.Equal("InitialValue1", config[$"{keyPrefix}:Setting1"]); - // Update values in the store with retry logic - await SetConfigurationSettingWithRetryAsync( - new ConfigurationSetting($"{keyPrefix}:Setting1", "UpdatedValue1")); - await SetConfigurationSettingWithRetryAsync( - new ConfigurationSetting(sentinelKey, "Updated")); + // Update values in the store + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{keyPrefix}:Setting1", "UpdatedValue1")); + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(sentinelKey, "Updated")); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); @@ -484,7 +373,7 @@ public async Task TryRefreshAsync_RefreshesOnlySelectedKeys_WhenUsingKeyFilter() var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetEndpoint(), GetCredential()); + options.Connect(GetConnectionString()); options.Select($"{keyPrefix}:*"); // Only refresh Setting1 when sentinel changes @@ -502,13 +391,10 @@ public async Task TryRefreshAsync_RefreshesOnlySelectedKeys_WhenUsingKeyFilter() Assert.Equal("InitialValue1", config[$"{keyPrefix}:Setting1"]); Assert.Equal("InitialValue2", config[$"{keyPrefix}:Setting2"]); - // Update values in the store with retry logic - await SetConfigurationSettingWithRetryAsync( - new ConfigurationSetting($"{keyPrefix}:Setting1", "UpdatedValue1")); - await SetConfigurationSettingWithRetryAsync( - new ConfigurationSetting($"{keyPrefix}:Setting2", "UpdatedValue2")); - await SetConfigurationSettingWithRetryAsync( - new ConfigurationSetting(sentinelKey, "Updated")); + // Update values in the store + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{keyPrefix}:Setting1", "UpdatedValue1")); + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{keyPrefix}:Setting2", "UpdatedValue2")); + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(sentinelKey, "Updated")); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); @@ -534,7 +420,7 @@ public async Task TryRefreshAsync_RefreshesFeatureFlags_WhenConfigured() var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetEndpoint(), GetCredential()); + options.Connect(GetConnectionString()); options.Select($"{keyPrefix}:*"); options.UseFeatureFlags(); @@ -551,14 +437,13 @@ public async Task TryRefreshAsync_RefreshesFeatureFlags_WhenConfigured() // Verify initial feature flag state Assert.Equal("False", config[$"FeatureManagement:{keyPrefix}Feature:Enabled"]); - // Update feature flag in the store with retry logic - await SetConfigurationSettingWithRetryAsync( + // Update feature flag in the store + await _configClient.SetConfigurationSettingAsync( ConfigurationModelFactory.ConfigurationSetting( featureFlagKey, @"{""id"":""" + keyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":true}", contentType: FeatureManagementConstants.ContentType)); - await SetConfigurationSettingWithRetryAsync( - new ConfigurationSetting(sentinelKey, "Updated")); + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(sentinelKey, "Updated")); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); @@ -583,7 +468,7 @@ public async Task RegisterAll_RefreshesAllKeys_WhenSentinelChanged() var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetEndpoint(), GetCredential()); + options.Connect(GetConnectionString()); options.Select($"{keyPrefix}:*"); // Use RegisterAll to refresh everything when sentinel changes @@ -601,13 +486,10 @@ public async Task RegisterAll_RefreshesAllKeys_WhenSentinelChanged() Assert.Equal("InitialValue1", config[$"{keyPrefix}:Setting1"]); Assert.Equal("InitialValue2", config[$"{keyPrefix}:Setting2"]); - // Update all values in the store with retry logic - await SetConfigurationSettingWithRetryAsync( - new ConfigurationSetting($"{keyPrefix}:Setting1", "UpdatedValue1")); - await SetConfigurationSettingWithRetryAsync( - new ConfigurationSetting($"{keyPrefix}:Setting2", "UpdatedValue2")); - await SetConfigurationSettingWithRetryAsync( - new ConfigurationSetting(sentinelKey, "Updated")); + // Update all values in the store + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{keyPrefix}:Setting1", "UpdatedValue1")); + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{keyPrefix}:Setting2", "UpdatedValue2")); + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(sentinelKey, "Updated")); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); @@ -633,7 +515,7 @@ public async Task TryRefreshAsync_ReturnsFalse_WhenSentinelKeyUnchanged() var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetEndpoint(), GetCredential()); + options.Connect(GetConnectionString()); options.Select($"{keyPrefix}:*"); options.ConfigureRefresh(refresh => @@ -649,9 +531,8 @@ public async Task TryRefreshAsync_ReturnsFalse_WhenSentinelKeyUnchanged() // Verify initial values Assert.Equal("InitialValue1", config[$"{keyPrefix}:Setting1"]); - // Update data but not sentinel with retry logic - await SetConfigurationSettingWithRetryAsync( - new ConfigurationSetting($"{keyPrefix}:Setting1", "UpdatedValue1")); + // Update data but not sentinel + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{keyPrefix}:Setting1", "UpdatedValue1")); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); From 68a005cf6c17371a53bb0d738bbfd7d08df2c463 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 6 Mar 2025 17:10:27 -0800 Subject: [PATCH 11/11] remove obsolete tags and update formatting --- .../Integration/IntegrationTests.cs | 4 ---- .../Unit/FeatureManagementTests.cs | 3 --- 2 files changed, 7 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 83bf6a13..996d7e00 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -243,16 +243,12 @@ public async Task DisposeAsync() { if (_appConfigStore != null) { - Console.WriteLine($"Deleting test App Configuration store: {_testStoreName}"); await _appConfigStore.DeleteAsync(WaitUntil.Completed); - Console.WriteLine("Store deleted successfully"); } if (_shouldDeleteResourceGroup && _resourceGroup != null) { - Console.WriteLine($"Deleting temporary resource group: {_testResourceGroupName}"); await _resourceGroup.DeleteAsync(WaitUntil.Completed); - Console.WriteLine("Resource group deleted successfully"); } } catch (Exception ex) diff --git a/tests/Tests.AzureAppConfiguration/Unit/FeatureManagementTests.cs b/tests/Tests.AzureAppConfiguration/Unit/FeatureManagementTests.cs index 075a8ef4..d96e9e39 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/FeatureManagementTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/FeatureManagementTests.cs @@ -731,7 +731,6 @@ public async Task WatchesFeatureFlags() } [Fact] - [Obsolete] public async Task WatchesFeatureFlagsUsingCacheExpirationInterval() { var featureFlags = new List { _kv }; @@ -875,7 +874,6 @@ public async Task SkipRefreshIfRefreshIntervalHasNotElapsed() } [Fact] - [Obsolete] public async Task SkipRefreshIfCacheNotExpired() { var featureFlags = new List { _kv }; @@ -1201,7 +1199,6 @@ MockAsyncPageable GetTestKeys(SettingSelector selector, CancellationToken ct) } [Fact] - [Obsolete] public void AlternateValidFeatureFlagFormats() { var mockResponse = new Mock();