From df290c22122fbe266f5531144239367cc9684d96 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 3 Mar 2025 12:40:13 -0800 Subject: [PATCH 01/57] 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 000000000..b056cef9a --- /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/57] 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 b056cef9a..5b50f2404 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/57] 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 5b50f2404..e6f838cb5 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/57] 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 e6f838cb5..9b45601b5 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/57] 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 aba7684b3..ad69330d5 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 08c95751e..7bbc3dedd 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 9b45601b5..418bfcc88 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 49c58ec60..eb1d27e69 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 d96e9e39e..075a8ef48 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/57] 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 418bfcc88..70cb26cb5 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 eb1d27e69..ec29d616e 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 000000000..f180a652f --- /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/57] 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 70cb26cb5..a39f18081 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/57] 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 a39f18081..788ce5a69 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 ec29d616e..f2573685f 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/57] 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 788ce5a69..ef3e12f3a 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/57] 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 ef3e12f3a..83bf6a13b 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/57] 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 83bf6a13b..996d7e00d 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 075a8ef48..d96e9e39e 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(); From 1c992a524cd92a2c37147f51e593cc587e7e8dc6 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 12 Mar 2025 11:55:30 -0700 Subject: [PATCH 12/57] updating tests --- .../Integration/GetAzureSubscription.ps1 | 51 +++ .../Integration/IntegrationTests.cs | 404 +++++++++++------- .../Tests.AzureAppConfiguration.csproj | 10 +- .../local.settings.json.template | 6 - 4 files changed, 302 insertions(+), 169 deletions(-) create mode 100644 tests/Tests.AzureAppConfiguration/Integration/GetAzureSubscription.ps1 delete mode 100644 tests/Tests.AzureAppConfiguration/local.settings.json.template diff --git a/tests/Tests.AzureAppConfiguration/Integration/GetAzureSubscription.ps1 b/tests/Tests.AzureAppConfiguration/Integration/GetAzureSubscription.ps1 new file mode 100644 index 000000000..7916f45c0 --- /dev/null +++ b/tests/Tests.AzureAppConfiguration/Integration/GetAzureSubscription.ps1 @@ -0,0 +1,51 @@ +#!/usr/bin/env pwsh + +# GetAzureSubscription.ps1 +# This script gets the current Azure subscription ID and saves it to a JSON file + +$ErrorActionPreference = "Stop" +$outputPath = Join-Path $PSScriptRoot "appsettings.Secrets.json" + +try { + # Get current subscription from az CLI + $subscriptionId = az account show --query id -o tsv 2>&1 + + # Check if the command was successful + if ($LASTEXITCODE -ne 0) { + $errorInfo = @{ + Success = $false + ErrorMessage = "Azure CLI command failed with exit code $LASTEXITCODE. Output: $subscriptionId" + } + $errorInfo | ConvertTo-Json | Out-File $outputPath -Encoding utf8 + exit 1 + } + + # Check if the output is empty + if ([string]::IsNullOrWhiteSpace($subscriptionId)) { + $errorInfo = @{ + Success = $false + ErrorMessage = "No active Azure subscription found. Please run 'az login' first." + } + $errorInfo | ConvertTo-Json | Out-File $outputPath -Encoding utf8 + exit 1 + } + + # If successful, save the subscription ID to a JSON file + $result = @{ + Success = $true + SubscriptionId = $subscriptionId.Trim() + } + + $result | ConvertTo-Json | Out-File $outputPath -Encoding utf8 + Write-Output "Subscription information saved to: $outputPath" + exit 0 +} +catch { + $errorInfo = @{ + Success = $false + ErrorMessage = "Error getting Azure subscription: $_" + } + $errorInfo | ConvertTo-Json | Out-File $outputPath -Encoding utf8 + Write-Error $_.Exception.Message + exit 1 +} \ No newline at end of file diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 996d7e00d..ad2944e1b 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -11,8 +11,11 @@ using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -23,6 +26,7 @@ namespace Tests.AzureAppConfiguration /// Integration tests for Azure App Configuration that connect to a real service. /// Creates a temporary App Configuration store for testing and deletes it after the tests are complete. /// Requires Azure credentials with appropriate permissions. + /// NOTE: Before running these tests, execute the GetAzureSubscription.ps1 script to create appsettings.Secrets.json. /// [Trait("Category", "Integration")] [CollectionDefinition(nameof(IntegrationTests), DisableParallelization = true)] @@ -32,14 +36,18 @@ public class IntegrationTests : IAsyncLifetime private const string TestKeyPrefix = "IntegrationTest"; private const string SentinelKey = TestKeyPrefix + ":Sentinel"; private const string FeatureFlagKey = ".appconfig.featureflag/" + TestKeyPrefix + "Feature"; + private const string DefaultLocation = "swedensouth"; + private const string SubscriptionJsonPath = "appsettings.Secrets.json"; - // 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 = "eastus"; - private const string LocalSettingsFile = "local.settings.json"; + /// + /// Class to hold test-specific key information + /// + private class TestContext + { + public string KeyPrefix { get; set; } + public string SentinelKey { get; set; } + public string FeatureFlagKey { get; set; } + } // Keys to create for testing private readonly List _testSettings = new List @@ -63,62 +71,85 @@ public class IntegrationTests : IAsyncLifetime private ArmClient _armClient; private string _testStoreName; private string _testResourceGroupName; - private bool _shouldDeleteResourceGroup = false; private AppConfigurationStoreResource _appConfigStore; private Uri _appConfigEndpoint; private ResourceGroupResource _resourceGroup; + private string _subscriptionId; // Flag indicating whether tests should run private bool _skipTests = false; private string _skipReason = null; /// - /// Loads environment variables from a local settings file if it exists. + /// Gets a DefaultAzureCredential for authentication. /// - private void LoadEnvironmentVariablesFromFile() + private DefaultAzureCredential GetCredential() { - string localSettingsPath = Path.Combine(AppContext.BaseDirectory, LocalSettingsFile); - - if (File.Exists(localSettingsPath)) + try { - 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) + return new DefaultAzureCredential(new DefaultAzureCredentialOptions { - Console.WriteLine($"Error loading local settings: {ex.Message}"); - } + ExcludeSharedTokenCacheCredential = true + }); + } + catch (CredentialUnavailableException ex) + { + _skipTests = true; + _skipReason = $"Azure credentials unavailable: {ex.Message}"; + return null; } } /// - /// Gets a DefaultAzureCredential for authentication. + /// Gets the current subscription ID by reading from the JSON file created by the PowerShell script. + /// NOTE: The PowerShell script must be run manually before running the tests. /// - private DefaultAzureCredential GetCredential() + private string GetCurrentSubscriptionId() { try { - return new DefaultAzureCredential(new DefaultAzureCredentialOptions + // Read the JSON file created by the script + string jsonPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Integration", SubscriptionJsonPath); + + if (!File.Exists(jsonPath)) { - ExcludeSharedTokenCacheCredential = true - }); + _skipTests = true; + _skipReason = $"Subscription JSON file not found at {jsonPath}. Run the GetAzureSubscription.ps1 script first."; + return null; + } + + string jsonContent = File.ReadAllText(jsonPath); + + using JsonDocument doc = JsonDocument.Parse(jsonContent); + JsonElement root = doc.RootElement; + + bool success = root.GetProperty("Success").GetBoolean(); + + if (!success) + { + _skipTests = true; + _skipReason = root.GetProperty("ErrorMessage").GetString(); + return null; + } + + return root.GetProperty("SubscriptionId").GetString(); } - catch (CredentialUnavailableException ex) + catch (FileNotFoundException ex) { _skipTests = true; - _skipReason = $"Azure credentials unavailable: {ex.Message}"; + _skipReason = $"Subscription JSON file not found: {ex.Message}. Run the GetAzureSubscription.ps1 script first."; + return null; + } + catch (JsonException ex) + { + _skipTests = true; + _skipReason = $"Failed to parse subscription JSON: {ex.Message}"; + return null; + } + catch (IOException ex) + { + _skipTests = true; + _skipReason = $"IO error while reading subscription data: {ex.Message}"; return null; } } @@ -138,93 +169,144 @@ public async Task InitializeAsync() { try { - // Load environment variables from local.settings.json if present - LoadEnvironmentVariablesFromFile(); - var credential = GetCredential(); if (_skipTests) return; - // 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)) - { - _skipTests = true; - _skipReason = $"Missing required environment variable: {SubscriptionIdEnvVar}"; - return; - } + // Get the current subscription ID from the JSON file + _subscriptionId = GetCurrentSubscriptionId(); + if (_skipTests) return; // Initialize Azure Resource Manager client _armClient = new ArmClient(credential); - SubscriptionResource subscription = _armClient.GetSubscriptions().Get(subscriptionIdStr); + SubscriptionResource subscription = _armClient.GetSubscriptions().Get(_subscriptionId); - // Create resource group if requested or use existing one - if (createResourceGroup) - { - _testResourceGroupName = $"appconfig-test-{Guid.NewGuid():N}".Substring(0, 20); + // Create a temporary resource group for this test run + _testResourceGroupName = $"appconfig-test-{Guid.NewGuid():N}".Substring(0, 20); - var rgData = new ResourceGroupData(new AzureLocation(location)); + var rgData = new ResourceGroupData(new AzureLocation(DefaultLocation)); + + try + { var rgLro = await subscription.GetResourceGroups().CreateOrUpdateAsync(WaitUntil.Completed, _testResourceGroupName, rgData); _resourceGroup = rgLro.Value; - _shouldDeleteResourceGroup = true; } - else + catch (RequestFailedException ex) { - if (string.IsNullOrEmpty(resourceGroupName)) - { - _skipTests = true; - _skipReason = $"Missing required environment variable: {ResourceGroupEnvVar}"; - return; - } - - _testResourceGroupName = resourceGroupName; - _resourceGroup = await subscription.GetResourceGroups().GetAsync(resourceGroupName); + _skipTests = true; + _skipReason = $"Failed to create resource group: {ex.Message}"; + return; } // Create unique store name for this test run _testStoreName = $"integration-{Guid.NewGuid():N}".Substring(0, 20); // Create the App Configuration store - var storeData = new AppConfigurationStoreData(new AzureLocation(location), new AppConfigurationSku("free")); - var createOperation = await _resourceGroup.GetAppConfigurationStores().CreateOrUpdateAsync( - WaitUntil.Completed, - _testStoreName, - storeData); + var storeData = new AppConfigurationStoreData(new AzureLocation(DefaultLocation), new AppConfigurationSku("free")); - _appConfigStore = createOperation.Value; - _appConfigEndpoint = new Uri(_appConfigStore.Data.Endpoint); - - // Get the connection string for the store instead of using RBAC - var accessKeys = _appConfigStore.GetKeysAsync(); - var primaryKey = await accessKeys.FirstOrDefaultAsync(); + try + { + var createOperation = await _resourceGroup.GetAppConfigurationStores().CreateOrUpdateAsync( + WaitUntil.Completed, + _testStoreName, + storeData); - if (primaryKey == null) + _appConfigStore = createOperation.Value; + _appConfigEndpoint = new Uri(_appConfigStore.Data.Endpoint); + } + catch (RequestFailedException ex) { - throw new InvalidOperationException("Failed to retrieve access keys from App Configuration store."); + _skipTests = true; + _skipReason = $"Failed to create App Configuration store: {ex.Message}"; + await CleanupResourceGroup(); + return; } - _connectionString = primaryKey.ConnectionString; + // Get the connection string for the store + try + { + var accessKeys = _appConfigStore.GetKeysAsync(); + var primaryKey = await accessKeys.FirstOrDefaultAsync(); + + if (primaryKey == null) + { + throw new InvalidOperationException("Failed to retrieve access keys from App Configuration store."); + } + + _connectionString = primaryKey.ConnectionString; - // Initialize the configuration client with the connection string - _configClient = new ConfigurationClient(_connectionString); + // Initialize the configuration client with the connection string + _configClient = new ConfigurationClient(_connectionString); + } + catch (RequestFailedException ex) + { + _skipTests = true; + _skipReason = $"Failed to get access keys: {ex.Message}"; + await CleanupResourceGroup(); + return; + } // Add test settings to the store - foreach (var setting in _testSettings) + try + { + foreach (var setting in _testSettings) + { + await _configClient.SetConfigurationSettingAsync(setting); + } + } + catch (RequestFailedException ex) { - await _configClient.SetConfigurationSettingAsync(setting); + _skipTests = true; + _skipReason = $"Failed to set configuration settings: {ex.Message}"; + await CleanupResourceGroup(); } } - catch (Exception ex) + catch (CredentialUnavailableException ex) + { + _skipTests = true; + _skipReason = $"Azure credentials unavailable: {ex.Message}"; + await CleanupResourceGroup(); + } + catch (InvalidOperationException ex) { _skipTests = true; _skipReason = $"Failed to initialize integration tests: {ex.Message}"; + await CleanupResourceGroup(); + } + catch (RequestFailedException ex) + { + _skipTests = true; + _skipReason = $"Azure request failed: {ex.Message}"; + await CleanupResourceGroup(); + } + catch (TaskCanceledException ex) + { + _skipTests = true; + _skipReason = $"Operation timed out: {ex.Message}"; + await CleanupResourceGroup(); + } + } - // Clean up any partially created resources - await DisposeAsync(); + /// + /// Helper method to clean up the resource group if initialization fails + /// + private async Task CleanupResourceGroup() + { + if (_resourceGroup != null) + { + try + { + await _resourceGroup.DeleteAsync(WaitUntil.Completed); + _resourceGroup = null; + } + catch (RequestFailedException) + { + // Ignore exceptions during cleanup + } + catch (TaskCanceledException) + { + // Ignore timeout exceptions during cleanup + } } } @@ -233,27 +315,28 @@ public async Task InitializeAsync() /// public async Task DisposeAsync() { - // Don't attempt cleanup if we don't have a store to delete - if (_appConfigStore == null && !_shouldDeleteResourceGroup) + // Don't attempt cleanup if we don't have a resource group to delete + if (_resourceGroup == null) { return; } try { - if (_appConfigStore != null) - { - await _appConfigStore.DeleteAsync(WaitUntil.Completed); - } - - if (_shouldDeleteResourceGroup && _resourceGroup != null) - { - await _resourceGroup.DeleteAsync(WaitUntil.Completed); - } + await _resourceGroup.DeleteAsync(WaitUntil.Completed); + _resourceGroup = null; } - catch (Exception ex) + catch (RequestFailedException ex) { - Console.WriteLine($"Test cleanup failed: {ex}. You may need to manually delete the resources: Store={_testStoreName}, ResourceGroup={(_shouldDeleteResourceGroup ? _testResourceGroupName : "N/A")}"); + Console.WriteLine($"Test cleanup failed: {ex.Message}. You may need to manually delete the resources: Store={_testStoreName}, ResourceGroup={_testResourceGroupName}"); + } + catch (InvalidOperationException ex) + { + Console.WriteLine($"Test cleanup failed: {ex.Message}. You may need to manually delete the resources: Store={_testStoreName}, ResourceGroup={_testResourceGroupName}"); + } + catch (TaskCanceledException ex) + { + Console.WriteLine($"Test cleanup timed out: {ex.Message}. You may need to manually delete the resources: Store={_testStoreName}, ResourceGroup={_testResourceGroupName}"); } } @@ -269,7 +352,7 @@ private string GetUniqueKeyPrefix(string testName) /// /// Setup test-specific keys and settings /// - private async Task<(string keyPrefix, string sentinelKey, string featureFlagKey)> SetupTestKeys(string testName) + private async Task SetupTestKeys(string testName) { string keyPrefix = GetUniqueKeyPrefix(testName); string sentinelKey = $"{keyPrefix}:Sentinel"; @@ -293,7 +376,12 @@ private string GetUniqueKeyPrefix(string testName) await _configClient.SetConfigurationSettingAsync(setting); } - return (keyPrefix, sentinelKey, featureFlagKey); + return new TestContext + { + KeyPrefix = keyPrefix, + SentinelKey = sentinelKey, + FeatureFlagKey = featureFlagKey + }; } [Fact] @@ -316,22 +404,22 @@ public void LoadConfiguration_RetrievesValuesFromAppConfiguration() } [Fact] - public async Task TryRefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() + public async Task RefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() { Skip.If(_skipTests, _skipReason); // Arrange - Setup test-specific keys - var (keyPrefix, sentinelKey, _) = await SetupTestKeys("UpdatesConfig"); + var testContext = await SetupTestKeys("UpdatesConfig"); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { options.Connect(GetConnectionString()); - options.Select($"{keyPrefix}:*"); + options.Select($"{testContext.KeyPrefix}:*"); options.ConfigureRefresh(refresh => { - refresh.Register(sentinelKey, refreshAll: true) + refresh.Register(testContext.SentinelKey, refreshAll: true) .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); @@ -340,42 +428,41 @@ public async Task TryRefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() .Build(); // Verify initial values - Assert.Equal("InitialValue1", config[$"{keyPrefix}:Setting1"]); + Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); // Update values in the store - await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{keyPrefix}:Setting1", "UpdatedValue1")); - await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(sentinelKey, "Updated")); + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting1", "UpdatedValue1")); + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(testContext.SentinelKey, "Updated")); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); // Act - var result = await refresher.TryRefreshAsync(); + await refresher.RefreshAsync(); // Assert - Assert.True(result); - Assert.Equal("UpdatedValue1", config[$"{keyPrefix}:Setting1"]); + Assert.Equal("UpdatedValue1", config[$"{testContext.KeyPrefix}:Setting1"]); } [Fact] - public async Task TryRefreshAsync_RefreshesOnlySelectedKeys_WhenUsingKeyFilter() + public async Task RefreshAsync_RefreshesOnlySelectedKeys_WhenUsingKeyFilter() { Skip.If(_skipTests, _skipReason); // Arrange - Setup test-specific keys - var (keyPrefix, sentinelKey, _) = await SetupTestKeys("RefreshesSelectedKeys"); + var testContext = await SetupTestKeys("RefreshesSelectedKeys"); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { options.Connect(GetConnectionString()); - options.Select($"{keyPrefix}:*"); + options.Select($"{testContext.KeyPrefix}:*"); // Only refresh Setting1 when sentinel changes options.ConfigureRefresh(refresh => { - refresh.Register(sentinelKey, $"{keyPrefix}:Setting1", refreshAll: false) + refresh.Register(testContext.SentinelKey, $"{testContext.KeyPrefix}:Setting1", refreshAll: false) .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); @@ -384,45 +471,45 @@ public async Task TryRefreshAsync_RefreshesOnlySelectedKeys_WhenUsingKeyFilter() .Build(); // Verify initial values - Assert.Equal("InitialValue1", config[$"{keyPrefix}:Setting1"]); - Assert.Equal("InitialValue2", config[$"{keyPrefix}:Setting2"]); + Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); + Assert.Equal("InitialValue2", config[$"{testContext.KeyPrefix}:Setting2"]); + // 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")); + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting1", "UpdatedValue1")); + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting2", "UpdatedValue2")); + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(testContext.SentinelKey, "Updated")); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); // Act - var result = await refresher.TryRefreshAsync(); + await refresher.RefreshAsync(); // Assert - Assert.True(result); - Assert.Equal("UpdatedValue1", config[$"{keyPrefix}:Setting1"]); - Assert.Equal("InitialValue2", config[$"{keyPrefix}:Setting2"]); // This value shouldn't change + Assert.Equal("UpdatedValue1", config[$"{testContext.KeyPrefix}:Setting1"]); + Assert.Equal("InitialValue2", config[$"{testContext.KeyPrefix}:Setting2"]); // This value shouldn't change } [Fact] - public async Task TryRefreshAsync_RefreshesFeatureFlags_WhenConfigured() + public async Task RefreshAsync_RefreshesFeatureFlags_WhenConfigured() { Skip.If(_skipTests, _skipReason); // Arrange - Setup test-specific keys - var (keyPrefix, sentinelKey, featureFlagKey) = await SetupTestKeys("RefreshesFeatureFlags"); + var testContext = await SetupTestKeys("RefreshesFeatureFlags"); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { options.Connect(GetConnectionString()); - options.Select($"{keyPrefix}:*"); + options.Select($"{testContext.KeyPrefix}:*"); options.UseFeatureFlags(); options.ConfigureRefresh(refresh => { - refresh.Register(sentinelKey) + refresh.Register(testContext.SentinelKey) .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); @@ -431,25 +518,24 @@ public async Task TryRefreshAsync_RefreshesFeatureFlags_WhenConfigured() .Build(); // Verify initial feature flag state - Assert.Equal("False", config[$"FeatureManagement:{keyPrefix}Feature:Enabled"]); + Assert.Equal("False", config[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); // Update feature flag in the store await _configClient.SetConfigurationSettingAsync( ConfigurationModelFactory.ConfigurationSetting( - featureFlagKey, - @"{""id"":""" + keyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":true}", + testContext.FeatureFlagKey, + @"{""id"":""" + testContext.KeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":true}", contentType: FeatureManagementConstants.ContentType)); - await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(sentinelKey, "Updated")); + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(testContext.SentinelKey, "Updated")); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); // Act - var result = await refresher.TryRefreshAsync(); + await refresher.RefreshAsync(); // Assert - Assert.True(result); - Assert.Equal("True", config[$"FeatureManagement:{keyPrefix}Feature:Enabled"]); + Assert.Equal("True", config[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); } [Fact] @@ -458,14 +544,14 @@ public async Task RegisterAll_RefreshesAllKeys_WhenSentinelChanged() Skip.If(_skipTests, _skipReason); // Arrange - Setup test-specific keys - var (keyPrefix, sentinelKey, _) = await SetupTestKeys("RefreshesAllKeys"); + var testContext = await SetupTestKeys("RefreshesAllKeys"); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { options.Connect(GetConnectionString()); - options.Select($"{keyPrefix}:*"); + options.Select($"{testContext.KeyPrefix}:*"); // Use RegisterAll to refresh everything when sentinel changes options.ConfigureRefresh(refresh => @@ -479,44 +565,43 @@ public async Task RegisterAll_RefreshesAllKeys_WhenSentinelChanged() .Build(); // Verify initial values - Assert.Equal("InitialValue1", config[$"{keyPrefix}:Setting1"]); - Assert.Equal("InitialValue2", config[$"{keyPrefix}:Setting2"]); + Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); + Assert.Equal("InitialValue2", config[$"{testContext.KeyPrefix}:Setting2"]); // 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")); + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting1", "UpdatedValue1")); + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting2", "UpdatedValue2")); + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(testContext.SentinelKey, "Updated")); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); // Act - var result = await refresher.TryRefreshAsync(); + await refresher.RefreshAsync(); // Assert - Assert.True(result); - Assert.Equal("UpdatedValue1", config[$"{keyPrefix}:Setting1"]); - Assert.Equal("UpdatedValue2", config[$"{keyPrefix}:Setting2"]); + Assert.Equal("UpdatedValue1", config[$"{testContext.KeyPrefix}:Setting1"]); + Assert.Equal("UpdatedValue2", config[$"{testContext.KeyPrefix}:Setting2"]); } [Fact] - public async Task TryRefreshAsync_ReturnsFalse_WhenSentinelKeyUnchanged() + public async Task RefreshAsync_ReturnsFalse_WhenSentinelKeyUnchanged() { Skip.If(_skipTests, _skipReason); // Arrange - Setup test-specific keys - var (keyPrefix, sentinelKey, _) = await SetupTestKeys("SentinelUnchanged"); + var testContext = await SetupTestKeys("SentinelUnchanged"); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { options.Connect(GetConnectionString()); - options.Select($"{keyPrefix}:*"); + options.Select($"{testContext.KeyPrefix}:*"); options.ConfigureRefresh(refresh => { - refresh.Register(sentinelKey) + refresh.Register(testContext.SentinelKey) .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); @@ -525,20 +610,19 @@ public async Task TryRefreshAsync_ReturnsFalse_WhenSentinelKeyUnchanged() .Build(); // Verify initial values - Assert.Equal("InitialValue1", config[$"{keyPrefix}:Setting1"]); + Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); // Update data but not sentinel - await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{keyPrefix}:Setting1", "UpdatedValue1")); + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting1", "UpdatedValue1")); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); // Act - var result = await refresher.TryRefreshAsync(); + await refresher.RefreshAsync(); // Assert - Assert.False(result); // Should return false as sentinel hasn't changed - Assert.Equal("InitialValue1", config[$"{keyPrefix}:Setting1"]); // Should not update + Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); // Should not update } } } diff --git a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj index f2573685f..ddb28177a 100644 --- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj +++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj @@ -26,13 +26,17 @@ - + + - + + Always + + Always - + diff --git a/tests/Tests.AzureAppConfiguration/local.settings.json.template b/tests/Tests.AzureAppConfiguration/local.settings.json.template deleted file mode 100644 index f180a652f..000000000 --- a/tests/Tests.AzureAppConfiguration/local.settings.json.template +++ /dev/null @@ -1,6 +0,0 @@ -{ - "AZURE_SUBSCRIPTION_ID": "", - "AZURE_APPCONFIG_RESOURCE_GROUP": "", - "AZURE_LOCATION": "", - "AZURE_CREATE_RESOURCE_GROUP": "true" -} \ No newline at end of file From 5cb73ad190e2f2252fd7d61a564f21afdd8d0122 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 12 Mar 2025 14:08:40 -0700 Subject: [PATCH 13/57] format --- .../Integration/IntegrationTests.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index ad2944e1b..4e91983ec 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -11,12 +11,9 @@ using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement; using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; -using System.Text; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; using Xunit; @@ -474,7 +471,6 @@ public async Task RefreshAsync_RefreshesOnlySelectedKeys_WhenUsingKeyFilter() Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); Assert.Equal("InitialValue2", config[$"{testContext.KeyPrefix}:Setting2"]); - // Update values in the store await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting1", "UpdatedValue1")); await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting2", "UpdatedValue2")); From 5f114da56b0e106eac8c617c3a86f7d8a96a4428 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 14 Mar 2025 13:47:37 -0700 Subject: [PATCH 14/57] remove skipping, add tests --- .gitignore | 4 +- .../Integration/IntegrationTests.cs | 488 ++++++++++-------- 2 files changed, 274 insertions(+), 218 deletions(-) diff --git a/.gitignore b/.gitignore index 530af0645..ee2269f7f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,8 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore -# Azure Functions localsettings file -local.settings.json +# Integration test secrets +appsettings.Secrets.json # User-specific files *.suo diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 4e91983ec..b370cb3c6 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -2,6 +2,7 @@ using Azure.Core; using Azure.Data.AppConfiguration; using Azure.Identity; +using Azure.Messaging.EventGrid.SystemEvents; using Azure.ResourceManager; using Azure.ResourceManager.AppConfiguration; using Azure.ResourceManager.AppConfiguration.Models; @@ -31,8 +32,6 @@ 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"; private const string DefaultLocation = "swedensouth"; private const string SubscriptionJsonPath = "appsettings.Secrets.json"; @@ -46,18 +45,6 @@ private class TestContext public string FeatureFlagKey { get; set; } } - // 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"), - ConfigurationModelFactory.ConfigurationSetting( - FeatureFlagKey, - @"{""id"":""" + TestKeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":false}", - contentType: FeatureManagementConstants.ContentType) - }; - // Client for direct manipulation of the store private ConfigurationClient _configClient; @@ -73,28 +60,15 @@ private class TestContext private ResourceGroupResource _resourceGroup; private string _subscriptionId; - // Flag indicating whether tests should run - private bool _skipTests = false; - private string _skipReason = null; - /// /// Gets a DefaultAzureCredential for authentication. /// private DefaultAzureCredential GetCredential() { - try - { - return new DefaultAzureCredential(new DefaultAzureCredentialOptions - { - ExcludeSharedTokenCacheCredential = true - }); - } - catch (CredentialUnavailableException ex) + return new DefaultAzureCredential(new DefaultAzureCredentialOptions { - _skipTests = true; - _skipReason = $"Azure credentials unavailable: {ex.Message}"; - return null; - } + ExcludeSharedTokenCacheCredential = true + }); } /// @@ -103,52 +77,27 @@ private DefaultAzureCredential GetCredential() /// private string GetCurrentSubscriptionId() { - try - { - // Read the JSON file created by the script - string jsonPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Integration", SubscriptionJsonPath); - - if (!File.Exists(jsonPath)) - { - _skipTests = true; - _skipReason = $"Subscription JSON file not found at {jsonPath}. Run the GetAzureSubscription.ps1 script first."; - return null; - } + // Read the JSON file created by the script + string jsonPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Integration", SubscriptionJsonPath); - string jsonContent = File.ReadAllText(jsonPath); + if (!File.Exists(jsonPath)) + { + throw new InvalidOperationException($"Subscription JSON file not found at {jsonPath}. Run the GetAzureSubscription.ps1 script first."); + } - using JsonDocument doc = JsonDocument.Parse(jsonContent); - JsonElement root = doc.RootElement; + string jsonContent = File.ReadAllText(jsonPath); - bool success = root.GetProperty("Success").GetBoolean(); + using JsonDocument doc = JsonDocument.Parse(jsonContent); + JsonElement root = doc.RootElement; - if (!success) - { - _skipTests = true; - _skipReason = root.GetProperty("ErrorMessage").GetString(); - return null; - } + bool success = root.GetProperty("Success").GetBoolean(); - return root.GetProperty("SubscriptionId").GetString(); - } - catch (FileNotFoundException ex) + if (!success) { - _skipTests = true; - _skipReason = $"Subscription JSON file not found: {ex.Message}. Run the GetAzureSubscription.ps1 script first."; - return null; - } - catch (JsonException ex) - { - _skipTests = true; - _skipReason = $"Failed to parse subscription JSON: {ex.Message}"; - return null; - } - catch (IOException ex) - { - _skipTests = true; - _skipReason = $"IO error while reading subscription data: {ex.Message}"; - return null; + throw new InvalidOperationException(root.GetProperty("ErrorMessage").GetString()); } + + return root.GetProperty("SubscriptionId").GetString(); } /// @@ -164,14 +113,14 @@ private string GetConnectionString() /// public async Task InitializeAsync() { + bool success = false; + try { var credential = GetCredential(); - if (_skipTests) return; // Get the current subscription ID from the JSON file _subscriptionId = GetCurrentSubscriptionId(); - if (_skipTests) return; // Initialize Azure Resource Manager client _armClient = new ArmClient(credential); @@ -183,17 +132,8 @@ public async Task InitializeAsync() var rgData = new ResourceGroupData(new AzureLocation(DefaultLocation)); - try - { - var rgLro = await subscription.GetResourceGroups().CreateOrUpdateAsync(WaitUntil.Completed, _testResourceGroupName, rgData); - _resourceGroup = rgLro.Value; - } - catch (RequestFailedException ex) - { - _skipTests = true; - _skipReason = $"Failed to create resource group: {ex.Message}"; - return; - } + var rgLro = await subscription.GetResourceGroups().CreateOrUpdateAsync(WaitUntil.Completed, _testResourceGroupName, rgData); + _resourceGroup = rgLro.Value; // Create unique store name for this test run _testStoreName = $"integration-{Guid.NewGuid():N}".Substring(0, 20); @@ -201,87 +141,37 @@ public async Task InitializeAsync() // Create the App Configuration store var storeData = new AppConfigurationStoreData(new AzureLocation(DefaultLocation), new AppConfigurationSku("free")); - try - { - var createOperation = await _resourceGroup.GetAppConfigurationStores().CreateOrUpdateAsync( - WaitUntil.Completed, - _testStoreName, - storeData); + var createOperation = await _resourceGroup.GetAppConfigurationStores().CreateOrUpdateAsync( + WaitUntil.Completed, + _testStoreName, + storeData); - _appConfigStore = createOperation.Value; - _appConfigEndpoint = new Uri(_appConfigStore.Data.Endpoint); - } - catch (RequestFailedException ex) - { - _skipTests = true; - _skipReason = $"Failed to create App Configuration store: {ex.Message}"; - await CleanupResourceGroup(); - return; - } + _appConfigStore = createOperation.Value; + _appConfigEndpoint = new Uri(_appConfigStore.Data.Endpoint); // Get the connection string for the store - try - { - var accessKeys = _appConfigStore.GetKeysAsync(); - var primaryKey = await accessKeys.FirstOrDefaultAsync(); - - if (primaryKey == null) - { - throw new InvalidOperationException("Failed to retrieve access keys from App Configuration store."); - } + var accessKeys = _appConfigStore.GetKeysAsync(); + var primaryKey = await accessKeys.FirstOrDefaultAsync(); - _connectionString = primaryKey.ConnectionString; - - // Initialize the configuration client with the connection string - _configClient = new ConfigurationClient(_connectionString); - } - catch (RequestFailedException ex) + if (primaryKey == null) { - _skipTests = true; - _skipReason = $"Failed to get access keys: {ex.Message}"; - await CleanupResourceGroup(); - return; + throw new InvalidOperationException("Failed to retrieve access keys from App Configuration store."); } - // Add test settings to the store - try - { - foreach (var setting in _testSettings) - { - await _configClient.SetConfigurationSettingAsync(setting); - } - } - catch (RequestFailedException ex) + _connectionString = primaryKey.ConnectionString; + + // Initialize the configuration client with the connection string + _configClient = new ConfigurationClient(_connectionString); + + success = true; + } + finally + { + if (!success) { - _skipTests = true; - _skipReason = $"Failed to set configuration settings: {ex.Message}"; await CleanupResourceGroup(); } } - catch (CredentialUnavailableException ex) - { - _skipTests = true; - _skipReason = $"Azure credentials unavailable: {ex.Message}"; - await CleanupResourceGroup(); - } - catch (InvalidOperationException ex) - { - _skipTests = true; - _skipReason = $"Failed to initialize integration tests: {ex.Message}"; - await CleanupResourceGroup(); - } - catch (RequestFailedException ex) - { - _skipTests = true; - _skipReason = $"Azure request failed: {ex.Message}"; - await CleanupResourceGroup(); - } - catch (TaskCanceledException ex) - { - _skipTests = true; - _skipReason = $"Operation timed out: {ex.Message}"; - await CleanupResourceGroup(); - } } /// @@ -364,7 +254,7 @@ private async Task SetupTestKeys(string testName) ConfigurationModelFactory.ConfigurationSetting( featureFlagKey, @"{""id"":""" + keyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":false}", - contentType: FeatureManagementConstants.ContentType) + contentType: FeatureManagementConstants.ContentType + ";charset=utf-8") }; // Add test-specific settings to the store @@ -382,29 +272,28 @@ private async Task SetupTestKeys(string testName) } [Fact] - public void LoadConfiguration_RetrievesValuesFromAppConfiguration() + public async Task LoadConfiguration_RetrievesValuesFromAppConfiguration() { - Skip.If(_skipTests, _skipReason); + // Arrange - Setup test-specific keys + var testContext = await SetupTestKeys("BasicConfig"); - // Arrange & Act + // Act var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { options.Connect(GetConnectionString()); - options.Select($"{TestKeyPrefix}:*"); + options.Select($"{testContext.KeyPrefix}:*"); }) .Build(); // Assert - Assert.Equal("InitialValue1", config[$"{TestKeyPrefix}:Setting1"]); - Assert.Equal("InitialValue2", config[$"{TestKeyPrefix}:Setting2"]); + Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); + Assert.Equal("InitialValue2", config[$"{testContext.KeyPrefix}:Setting2"]); } [Fact] public async Task RefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() { - Skip.If(_skipTests, _skipReason); - // Arrange - Setup test-specific keys var testContext = await SetupTestKeys("UpdatesConfig"); IConfigurationRefresher refresher = null; @@ -442,12 +331,10 @@ public async Task RefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() } [Fact] - public async Task RefreshAsync_RefreshesOnlySelectedKeys_WhenUsingKeyFilter() + public async Task RegisterAll_RefreshesAllKeys() { - Skip.If(_skipTests, _skipReason); - // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("RefreshesSelectedKeys"); + var testContext = await SetupTestKeys("RefreshesAllKeys"); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() @@ -456,10 +343,10 @@ public async Task RefreshAsync_RefreshesOnlySelectedKeys_WhenUsingKeyFilter() options.Connect(GetConnectionString()); options.Select($"{testContext.KeyPrefix}:*"); - // Only refresh Setting1 when sentinel changes + // Use RegisterAll to refresh everything when sentinel changes options.ConfigureRefresh(refresh => { - refresh.Register(testContext.SentinelKey, $"{testContext.KeyPrefix}:Setting1", refreshAll: false) + refresh.RegisterAll() .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); @@ -471,10 +358,9 @@ public async Task RefreshAsync_RefreshesOnlySelectedKeys_WhenUsingKeyFilter() Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); Assert.Equal("InitialValue2", config[$"{testContext.KeyPrefix}:Setting2"]); - // Update values in the store + // Update all values in the store await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting1", "UpdatedValue1")); await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting2", "UpdatedValue2")); - await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(testContext.SentinelKey, "Updated")); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); @@ -484,16 +370,14 @@ public async Task RefreshAsync_RefreshesOnlySelectedKeys_WhenUsingKeyFilter() // Assert Assert.Equal("UpdatedValue1", config[$"{testContext.KeyPrefix}:Setting1"]); - Assert.Equal("InitialValue2", config[$"{testContext.KeyPrefix}:Setting2"]); // This value shouldn't change + Assert.Equal("UpdatedValue2", config[$"{testContext.KeyPrefix}:Setting2"]); } [Fact] - public async Task RefreshAsync_RefreshesFeatureFlags_WhenConfigured() + public async Task RefreshAsync_SentinelKeyUnchanged() { - Skip.If(_skipTests, _skipReason); - // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("RefreshesFeatureFlags"); + var testContext = await SetupTestKeys("SentinelUnchanged"); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() @@ -501,7 +385,6 @@ public async Task RefreshAsync_RefreshesFeatureFlags_WhenConfigured() { options.Connect(GetConnectionString()); options.Select($"{testContext.KeyPrefix}:*"); - options.UseFeatureFlags(); options.ConfigureRefresh(refresh => { @@ -513,16 +396,11 @@ public async Task RefreshAsync_RefreshesFeatureFlags_WhenConfigured() }) .Build(); - // Verify initial feature flag state - Assert.Equal("False", config[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); + // Verify initial values + Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); - // Update feature flag in the store - await _configClient.SetConfigurationSettingAsync( - ConfigurationModelFactory.ConfigurationSetting( - testContext.FeatureFlagKey, - @"{""id"":""" + testContext.KeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":true}", - contentType: FeatureManagementConstants.ContentType)); - await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(testContext.SentinelKey, "Updated")); + // Update data but not sentinel + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting1", "UpdatedValue1")); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); @@ -531,42 +409,50 @@ await _configClient.SetConfigurationSettingAsync( await refresher.RefreshAsync(); // Assert - Assert.Equal("True", config[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); + Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); // Should not update } [Fact] - public async Task RegisterAll_RefreshesAllKeys_WhenSentinelChanged() + public async Task RefreshAsync_RefreshesFeatureFlags_WhenConfigured() { - Skip.If(_skipTests, _skipReason); - - // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("RefreshesAllKeys"); + var testContext = await SetupTestKeys("FeatureFlagRefresh"); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { options.Connect(GetConnectionString()); + // Select the key prefix to include all test keys options.Select($"{testContext.KeyPrefix}:*"); - // Use RegisterAll to refresh everything when sentinel changes + // Configure feature flags with the correct ID pattern + options.UseFeatureFlags(featureFlagOptions => + { + featureFlagOptions.Select(testContext.KeyPrefix + "*"); + featureFlagOptions.SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + options.ConfigureRefresh(refresh => { - refresh.RegisterAll() - .SetRefreshInterval(TimeSpan.FromSeconds(1)); + refresh.Register(testContext.SentinelKey) + .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); refresher = options.GetRefresher(); }) .Build(); - // Verify initial values - Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); - Assert.Equal("InitialValue2", config[$"{testContext.KeyPrefix}:Setting2"]); + // Verify the feature flag is disabled initially + Assert.Equal("False", config[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); - // Update all values in the store - await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting1", "UpdatedValue1")); - await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting2", "UpdatedValue2")); + // Update the feature flag to enabled=true + await _configClient.SetConfigurationSettingAsync( + ConfigurationModelFactory.ConfigurationSetting( + testContext.FeatureFlagKey, + @"{""id"":""" + testContext.KeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":true}", + contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + + // Update the sentinel key to trigger refresh await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(testContext.SentinelKey, "Updated")); // Wait for cache to expire @@ -576,49 +462,219 @@ public async Task RegisterAll_RefreshesAllKeys_WhenSentinelChanged() await refresher.RefreshAsync(); // Assert - Assert.Equal("UpdatedValue1", config[$"{testContext.KeyPrefix}:Setting1"]); - Assert.Equal("UpdatedValue2", config[$"{testContext.KeyPrefix}:Setting2"]); + Assert.Equal("True", config[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); } [Fact] - public async Task RefreshAsync_ReturnsFalse_WhenSentinelKeyUnchanged() + public async Task UseFeatureFlags_WithClientFiltersAndConditions() { - Skip.If(_skipTests, _skipReason); + var testContext = await SetupTestKeys("FeatureFlagFilters"); - // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("SentinelUnchanged"); - IConfigurationRefresher refresher = null; + // Create a feature flag with complex conditions + await _configClient.SetConfigurationSettingAsync( + ConfigurationModelFactory.ConfigurationSetting( + testContext.FeatureFlagKey, + @"{ + ""id"": """ + testContext.KeyPrefix + @"Feature"", + ""description"": ""Test feature with filters"", + ""enabled"": true, + ""conditions"": { + ""client_filters"": [ + { + ""name"": ""Browser"", + ""parameters"": { + ""AllowedBrowsers"": [""Chrome"", ""Edge""] + } + }, + { + ""name"": ""TimeWindow"", + ""parameters"": { + ""Start"": ""\/Date(" + DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeMilliseconds() + @")\/"", + ""End"": ""\/Date(" + DateTimeOffset.UtcNow.AddDays(1).ToUnixTimeMilliseconds() + @")\/"" + } + } + ] + } + }", + contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { options.Connect(GetConnectionString()); - options.Select($"{testContext.KeyPrefix}:*"); + options.UseFeatureFlags(featureFlagOptions => + { + featureFlagOptions.Select(testContext.KeyPrefix + "*"); + }); + }) + .Build(); + // Verify feature flag structure is loaded correctly + Assert.Equal("Browser", config[$"FeatureManagement:{testContext.KeyPrefix}Feature:EnabledFor:0:Name"]); + Assert.Equal("Chrome", config[$"FeatureManagement:{testContext.KeyPrefix}Feature:EnabledFor:0:Parameters:AllowedBrowsers:0"]); + Assert.Equal("Edge", config[$"FeatureManagement:{testContext.KeyPrefix}Feature:EnabledFor:0:Parameters:AllowedBrowsers:1"]); + Assert.Equal("TimeWindow", config[$"FeatureManagement:{testContext.KeyPrefix}Feature:EnabledFor:1:Name"]); + } + + [Fact] + public async Task MultipleProviders_LoadAndRefresh() + { + var testContext1 = await SetupTestKeys("MultiProviderTest1"); + var testContext2 = await SetupTestKeys("MultiProviderTest2"); + IConfigurationRefresher refresher1 = null; + IConfigurationRefresher refresher2 = null; + + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{testContext1.KeyPrefix}:*"); options.ConfigureRefresh(refresh => { - refresh.Register(testContext.SentinelKey) + refresh.Register(testContext1.SentinelKey, true) .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); - refresher = options.GetRefresher(); + refresher1 = options.GetRefresher(); + }) + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{testContext2.KeyPrefix}:*"); + options.ConfigureRefresh(refresh => + { + refresh.Register(testContext2.SentinelKey) + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + + refresher2 = options.GetRefresher(); }) .Build(); // Verify initial values - Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); + Assert.Equal("InitialValue1", config[$"{testContext1.KeyPrefix}:Setting1"]); + Assert.Equal("InitialValue1", config[$"{testContext2.KeyPrefix}:Setting1"]); - // Update data but not sentinel - await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting1", "UpdatedValue1")); + // Update values and sentinel keys + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext1.KeyPrefix}:Setting1", "UpdatedValue1")); + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(testContext1.SentinelKey, "Updated")); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); - // Act - await refresher.RefreshAsync(); + // Refresh only the first provider + await refresher1.RefreshAsync(); - // Assert - Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); // Should not update + // Assert: Only the first provider's values should be updated + Assert.Equal("UpdatedValue1", config[$"{testContext1.KeyPrefix}:Setting1"]); + Assert.Equal("InitialValue1", config[$"{testContext2.KeyPrefix}:Setting1"]); + } + + [Fact] + public async Task FeatureFlag_WithVariants() + { + var testContext = await SetupTestKeys("FeatureFlagVariants"); + + await _configClient.SetConfigurationSettingAsync( + ConfigurationModelFactory.ConfigurationSetting( + testContext.FeatureFlagKey, + @"{""id"":""" + testContext.KeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":true}", + contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + + // Create a feature flag with variants + await _configClient.SetConfigurationSettingAsync( + ConfigurationModelFactory.ConfigurationSetting( + testContext.FeatureFlagKey + "WithVariants", + @"{ + ""id"": """ + testContext.KeyPrefix + @"FeatureWithVariants"", + ""description"": ""Feature flag with variants"", + ""enabled"": true, + ""conditions"": { ""client_filters"": [] }, + ""variants"": [ + { + ""name"": ""LargeSize"", + ""configuration_value"": ""800px"" + }, + { + ""name"": ""MediumSize"", + ""configuration_value"": ""600px"" + }, + { + ""name"": ""SmallSize"", + ""configuration_value"": ""400px"" + } + ], + ""allocation"": { + ""default_when_enabled"": ""MediumSize"" + } + }", + contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.UseFeatureFlags(featureFlagOptions => + { + featureFlagOptions.Select(testContext.KeyPrefix + "*"); + }); + }) + .Build(); + + // Verify variants are loaded correctly + Assert.Equal("True", config[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); + Assert.Equal("LargeSize", config[$"feature_management:feature_flags:0:variants:0:name"]); + Assert.Equal("800px", config[$"feature_management:feature_flags:0:variants:0:configuration_value"]); + Assert.Equal("MediumSize", config[$"feature_management:feature_flags:0:variants:1:name"]); + Assert.Equal("600px", config[$"feature_management:feature_flags:0:variants:1:configuration_value"]); + Assert.Equal("SmallSize", config[$"feature_management:feature_flags:0:variants:2:name"]); + Assert.Equal("400px", config[$"feature_management:feature_flags:0:variants:2:configuration_value"]); + Assert.Equal("MediumSize", config[$"feature_management:feature_flags:0:allocation:default_when_enabled"]); + } + + [Fact] + public async Task JsonContentType_LoadsAndFlattensHierarchicalData() + { + var testContext = await SetupTestKeys("JsonContent"); + + // Create a complex JSON structure + string jsonKey = $"{testContext.KeyPrefix}:JsonConfig"; + await _configClient.SetConfigurationSettingAsync( + ConfigurationModelFactory.ConfigurationSetting( + jsonKey, + @"{ + ""database"": { + ""connection"": { + ""string"": ""Server=myserver;Database=mydb;User Id=sa;Password=mypassword;"", + ""timeout"": 30 + }, + ""retries"": 3, + ""enabled"": true + }, + ""logging"": { + ""level"": ""Information"", + ""providers"": [""Console"", ""Debug"", ""EventLog""] + } + }", + contentType: "application/json")); + + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{testContext.KeyPrefix}:*"); + }) + .Build(); + + // Verify JSON was flattened properly + Assert.Equal("Server=myserver;Database=mydb;User Id=sa;Password=mypassword;", config[$"{jsonKey}:database:connection:string"]); + Assert.Equal("30", config[$"{jsonKey}:database:connection:timeout"]); + Assert.Equal("3", config[$"{jsonKey}:database:retries"]); + Assert.Equal("True", config[$"{jsonKey}:database:enabled"]); + Assert.Equal("Information", config[$"{jsonKey}:logging:level"]); + Assert.Equal("Console", config[$"{jsonKey}:logging:providers:0"]); + Assert.Equal("Debug", config[$"{jsonKey}:logging:providers:1"]); + Assert.Equal("EventLog", config[$"{jsonKey}:logging:providers:2"]); } } } From c495fc47b2bb3db6005a64a6367a5c1841792e60 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 14 Mar 2025 16:39:20 -0700 Subject: [PATCH 15/57] add cleanup stale resource call on startup and dispose --- .../AzureResourceCleanupUtility.cs | 131 ++++++++++++++++++ .../Integration/IntegrationTests.cs | 122 +++++++++++++--- .../Tests.AzureAppConfiguration.csproj | 2 +- 3 files changed, 231 insertions(+), 24 deletions(-) create mode 100644 tests/Tests.AzureAppConfiguration/Integration/AzureResourceCleanupUtility.cs diff --git a/tests/Tests.AzureAppConfiguration/Integration/AzureResourceCleanupUtility.cs b/tests/Tests.AzureAppConfiguration/Integration/AzureResourceCleanupUtility.cs new file mode 100644 index 000000000..3b88e167f --- /dev/null +++ b/tests/Tests.AzureAppConfiguration/Integration/AzureResourceCleanupUtility.cs @@ -0,0 +1,131 @@ +using Azure; +using Azure.Core; +using Azure.Identity; +using Azure.ResourceManager; +using Azure.ResourceManager.Resources; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Tests.AzureAppConfiguration +{ + /// + /// Utility for cleaning up Azure resources created during integration tests. + /// Can be used both programmatically during tests or as a standalone CLI tool. + /// + public class AzureResourceCleanupUtility + { + // Resource tagging and cleanup constants + private readonly string _resourceGroupNamePrefix; + private readonly string _testResourceTag; + + // ARM client for Azure operations + private readonly ArmClient _armClient; + private readonly string _subscriptionId; + + /// + /// Creates a new instance of the Azure resource cleanup utility + /// + /// Azure ARM client to use + /// Subscription ID to target + /// Prefix for identifying test resource groups + /// Tag key used to identify test resources + public AzureResourceCleanupUtility( + ArmClient armClient, + string subscriptionId, + string resourceGroupNamePrefix = "appconfig-test-", + string testResourceTag = "TestResource") + { + _armClient = armClient ?? throw new ArgumentNullException(nameof(armClient)); + _subscriptionId = !string.IsNullOrEmpty(subscriptionId) ? subscriptionId : throw new ArgumentNullException(nameof(subscriptionId)); + _resourceGroupNamePrefix = resourceGroupNamePrefix; + _testResourceTag = testResourceTag; + } + + /// + /// Creates an instance of the utility with default authentication + /// + /// Subscription ID to target + /// A configured cleanup utility + public static AzureResourceCleanupUtility CreateWithDefaultCredentials(string subscriptionId) + { + // Use DefaultAzureCredential for authentication + var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions + { + ExcludeSharedTokenCacheCredential = true + }); + + return new AzureResourceCleanupUtility( + new ArmClient(credential), + subscriptionId); + } + + /// + /// Cleans up test resources left over from previous test runs + /// + /// If true, only reports what would be deleted without actually deleting + /// Optional callback for reporting progress + /// Number of resource groups scheduled for deletion + public async Task CleanupStaleResources(bool dryRun = false, Action progressCallback = null) + { + void ReportProgress(string message) + { + progressCallback?.Invoke(message); + } + + ReportProgress($"Checking for leftover test resources..."); + SubscriptionResource subscription = _armClient.GetSubscriptions().Get(_subscriptionId); + + // Find all resource groups that match our naming pattern and have our test tag + var resourceGroups = subscription.GetResourceGroups(); + + int cleanedCount = 0; + + await foreach (var rgResource in resourceGroups) + { + // Check if this is our test resource group + if (rgResource.Data.Name.StartsWith(_resourceGroupNamePrefix) && + rgResource.Data.Tags.TryGetValue(_testResourceTag, out string isTestResource) && + isTestResource == "true") + { + if (dryRun) + { + ReportProgress($"[DRY RUN] Would delete test resource group: {rgResource.Data.Name}"); + cleanedCount++; + } + else + { + ReportProgress($"Cleaning up test resource group: {rgResource.Data.Name}"); + + try + { + await rgResource.DeleteAsync(WaitUntil.Started); + cleanedCount++; + } + catch (RequestFailedException ex) + { + ReportProgress($"Error deleting resource group {rgResource.Data.Name}: {ex.Message}"); + } + catch (InvalidOperationException ex) + { + ReportProgress($"Invalid operation deleting resource group {rgResource.Data.Name}: {ex.Message}"); + } + catch (TaskCanceledException ex) + { + ReportProgress($"Timeout while deleting resource group {rgResource.Data.Name}: {ex.Message}"); + } + catch (UnauthorizedAccessException ex) + { + ReportProgress($"Unauthorized access when deleting resource group {rgResource.Data.Name}: {ex.Message}"); + } + } + } + } + + var operationType = dryRun ? "identified" : "scheduled for deletion"; + ReportProgress($"Cleanup scan complete. {cleanedCount} test resource groups {operationType}."); + + return cleanedCount; + } + } +} diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index b370cb3c6..d69ad1aa2 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -35,6 +35,13 @@ public class IntegrationTests : IAsyncLifetime private const string DefaultLocation = "swedensouth"; private const string SubscriptionJsonPath = "appsettings.Secrets.json"; + // Resource tagging and cleanup constants + private const string ResourceGroupNamePrefix = "appconfig-test-"; + private const string StoreNamePrefix = "integration-"; + private const string TestResourceTag = "TestResource"; + private const string CreatedByTag = "CreatedBy"; + private const int StaleResourceThresholdHours = 24; // Resources older than this are considered stale + /// /// Class to hold test-specific key information /// @@ -108,6 +115,61 @@ private string GetConnectionString() return _connectionString; } + /// + /// Generate a timestamped, unique resource name with the given prefix + /// + private string GenerateTimestampedResourceName(string prefix) + { + // Format: prefix-yyyyMMddHHmm-randomGuid (trimmed to 20 chars) + string timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmm"); + string randomPart = Guid.NewGuid().ToString("N").Substring(0, 8); + return $"{prefix}{timestamp}-{randomPart}"; + } + + /// + /// Creates a dictionary of resource tags for identifying test resources + /// + private IDictionary CreateResourceTags() + { + return new Dictionary + { + { TestResourceTag, "true" }, + { CreatedByTag, "IntegrationTests" } + }; + } + + /// + /// Cleans up stale test resources from previous test runs + /// + private async Task CleanupStaleResources() + { + try + { + Console.WriteLine("Checking for leftover test resources..."); + + // Use the updated cleanup utility that removes all test resources regardless of age + var cleanupUtility = new AzureResourceCleanupUtility( + _armClient, + _subscriptionId, + ResourceGroupNamePrefix, + TestResourceTag); + + // Run the cleanup with console logging + await cleanupUtility.CleanupStaleResources( + dryRun: false, + progressCallback: message => Console.WriteLine(message)); + } + catch (Exception ex) when ( + ex is RequestFailedException || + ex is InvalidOperationException || + ex is TaskCanceledException || + ex is UnauthorizedAccessException) + { + // Log but don't fail the test run + Console.WriteLine($"Error during test resource cleanup: {ex.Message}"); + } + } + /// /// Creates a temporary Azure App Configuration store and adds test data. /// @@ -125,22 +187,36 @@ public async Task InitializeAsync() // Initialize Azure Resource Manager client _armClient = new ArmClient(credential); + // Clean up any stale resources before creating new ones + await CleanupStaleResources(); + SubscriptionResource subscription = _armClient.GetSubscriptions().Get(_subscriptionId); - // Create a temporary resource group for this test run - _testResourceGroupName = $"appconfig-test-{Guid.NewGuid():N}".Substring(0, 20); + // Create a temporary resource group for this test run with timestamp and tags + _testResourceGroupName = GenerateTimestampedResourceName(ResourceGroupNamePrefix); var rgData = new ResourceGroupData(new AzureLocation(DefaultLocation)); + // Add tags for resource identification + foreach (var tag in CreateResourceTags()) + { + rgData.Tags.Add(tag.Key, tag.Value); + } var rgLro = await subscription.GetResourceGroups().CreateOrUpdateAsync(WaitUntil.Completed, _testResourceGroupName, rgData); _resourceGroup = rgLro.Value; - // Create unique store name for this test run - _testStoreName = $"integration-{Guid.NewGuid():N}".Substring(0, 20); + // Create unique store name for this test run with timestamp + _testStoreName = GenerateTimestampedResourceName(StoreNamePrefix); // Create the App Configuration store var storeData = new AppConfigurationStoreData(new AzureLocation(DefaultLocation), new AppConfigurationSku("free")); + // Add the same tags to the store + foreach (var tag in CreateResourceTags()) + { + storeData.Tags.Add(tag.Key, tag.Value); + } + var createOperation = await _resourceGroup.GetAppConfigurationStores().CreateOrUpdateAsync( WaitUntil.Completed, _testStoreName, @@ -181,19 +257,8 @@ private async Task CleanupResourceGroup() { if (_resourceGroup != null) { - try - { - await _resourceGroup.DeleteAsync(WaitUntil.Completed); - _resourceGroup = null; - } - catch (RequestFailedException) - { - // Ignore exceptions during cleanup - } - catch (TaskCanceledException) - { - // Ignore timeout exceptions during cleanup - } + await _resourceGroup.DeleteAsync(WaitUntil.Completed); + _resourceGroup = null; } } @@ -210,20 +275,31 @@ public async Task DisposeAsync() try { + Console.WriteLine($"Cleaning up test resources: Store={_testStoreName}, ResourceGroup={_testResourceGroupName}"); await _resourceGroup.DeleteAsync(WaitUntil.Completed); _resourceGroup = null; + Console.WriteLine("Resource cleanup completed successfully"); } - catch (RequestFailedException ex) + catch (Exception ex) when ( + ex is RequestFailedException || + ex is InvalidOperationException || + ex is TaskCanceledException) { - Console.WriteLine($"Test cleanup failed: {ex.Message}. You may need to manually delete the resources: Store={_testStoreName}, ResourceGroup={_testResourceGroupName}"); + Console.WriteLine($"Test cleanup failed: {ex.Message}."); } - catch (InvalidOperationException ex) + + // Try to clean up any other stale resources that might exist from failed previous runs + try { - Console.WriteLine($"Test cleanup failed: {ex.Message}. You may need to manually delete the resources: Store={_testStoreName}, ResourceGroup={_testResourceGroupName}"); + await CleanupStaleResources(); } - catch (TaskCanceledException ex) + catch (Exception ex) when ( + ex is RequestFailedException || + ex is InvalidOperationException || + ex is TaskCanceledException || + ex is UnauthorizedAccessException) { - Console.WriteLine($"Test cleanup timed out: {ex.Message}. You may need to manually delete the resources: Store={_testStoreName}, ResourceGroup={_testResourceGroupName}"); + Console.WriteLine($"Error during final stale resource cleanup: {ex.Message}"); } } diff --git a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj index ddb28177a..5dc6bfc07 100644 --- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj +++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj @@ -1,4 +1,4 @@ - + net48;net6.0;net8.0 From 5dbb85647ce2707ceeaee96e9b2a50fe3584de61 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 17 Mar 2025 12:09:45 -0700 Subject: [PATCH 16/57] add back removed line --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index ee2269f7f..39f97d76a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +# Azure Functions localsettings file +local.settings.json + # Integration test secrets appsettings.Secrets.json From b564f09c50ea88e8241b6f020c515f525b6e1c88 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 17 Mar 2025 15:32:59 -0700 Subject: [PATCH 17/57] use persistent resource group --- .../AzureResourceCleanupUtility.cs | 131 ------------ .../Integration/IntegrationTests.cs | 195 +++++++++++------- 2 files changed, 117 insertions(+), 209 deletions(-) delete mode 100644 tests/Tests.AzureAppConfiguration/Integration/AzureResourceCleanupUtility.cs diff --git a/tests/Tests.AzureAppConfiguration/Integration/AzureResourceCleanupUtility.cs b/tests/Tests.AzureAppConfiguration/Integration/AzureResourceCleanupUtility.cs deleted file mode 100644 index 3b88e167f..000000000 --- a/tests/Tests.AzureAppConfiguration/Integration/AzureResourceCleanupUtility.cs +++ /dev/null @@ -1,131 +0,0 @@ -using Azure; -using Azure.Core; -using Azure.Identity; -using Azure.ResourceManager; -using Azure.ResourceManager.Resources; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Tests.AzureAppConfiguration -{ - /// - /// Utility for cleaning up Azure resources created during integration tests. - /// Can be used both programmatically during tests or as a standalone CLI tool. - /// - public class AzureResourceCleanupUtility - { - // Resource tagging and cleanup constants - private readonly string _resourceGroupNamePrefix; - private readonly string _testResourceTag; - - // ARM client for Azure operations - private readonly ArmClient _armClient; - private readonly string _subscriptionId; - - /// - /// Creates a new instance of the Azure resource cleanup utility - /// - /// Azure ARM client to use - /// Subscription ID to target - /// Prefix for identifying test resource groups - /// Tag key used to identify test resources - public AzureResourceCleanupUtility( - ArmClient armClient, - string subscriptionId, - string resourceGroupNamePrefix = "appconfig-test-", - string testResourceTag = "TestResource") - { - _armClient = armClient ?? throw new ArgumentNullException(nameof(armClient)); - _subscriptionId = !string.IsNullOrEmpty(subscriptionId) ? subscriptionId : throw new ArgumentNullException(nameof(subscriptionId)); - _resourceGroupNamePrefix = resourceGroupNamePrefix; - _testResourceTag = testResourceTag; - } - - /// - /// Creates an instance of the utility with default authentication - /// - /// Subscription ID to target - /// A configured cleanup utility - public static AzureResourceCleanupUtility CreateWithDefaultCredentials(string subscriptionId) - { - // Use DefaultAzureCredential for authentication - var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions - { - ExcludeSharedTokenCacheCredential = true - }); - - return new AzureResourceCleanupUtility( - new ArmClient(credential), - subscriptionId); - } - - /// - /// Cleans up test resources left over from previous test runs - /// - /// If true, only reports what would be deleted without actually deleting - /// Optional callback for reporting progress - /// Number of resource groups scheduled for deletion - public async Task CleanupStaleResources(bool dryRun = false, Action progressCallback = null) - { - void ReportProgress(string message) - { - progressCallback?.Invoke(message); - } - - ReportProgress($"Checking for leftover test resources..."); - SubscriptionResource subscription = _armClient.GetSubscriptions().Get(_subscriptionId); - - // Find all resource groups that match our naming pattern and have our test tag - var resourceGroups = subscription.GetResourceGroups(); - - int cleanedCount = 0; - - await foreach (var rgResource in resourceGroups) - { - // Check if this is our test resource group - if (rgResource.Data.Name.StartsWith(_resourceGroupNamePrefix) && - rgResource.Data.Tags.TryGetValue(_testResourceTag, out string isTestResource) && - isTestResource == "true") - { - if (dryRun) - { - ReportProgress($"[DRY RUN] Would delete test resource group: {rgResource.Data.Name}"); - cleanedCount++; - } - else - { - ReportProgress($"Cleaning up test resource group: {rgResource.Data.Name}"); - - try - { - await rgResource.DeleteAsync(WaitUntil.Started); - cleanedCount++; - } - catch (RequestFailedException ex) - { - ReportProgress($"Error deleting resource group {rgResource.Data.Name}: {ex.Message}"); - } - catch (InvalidOperationException ex) - { - ReportProgress($"Invalid operation deleting resource group {rgResource.Data.Name}: {ex.Message}"); - } - catch (TaskCanceledException ex) - { - ReportProgress($"Timeout while deleting resource group {rgResource.Data.Name}: {ex.Message}"); - } - catch (UnauthorizedAccessException ex) - { - ReportProgress($"Unauthorized access when deleting resource group {rgResource.Data.Name}: {ex.Message}"); - } - } - } - } - - var operationType = dryRun ? "identified" : "scheduled for deletion"; - ReportProgress($"Cleanup scan complete. {cleanedCount} test resource groups {operationType}."); - - return cleanedCount; - } - } -} diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index d69ad1aa2..de143b9eb 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -2,7 +2,6 @@ using Azure.Core; using Azure.Data.AppConfiguration; using Azure.Identity; -using Azure.Messaging.EventGrid.SystemEvents; using Azure.ResourceManager; using Azure.ResourceManager.AppConfiguration; using Azure.ResourceManager.AppConfiguration.Models; @@ -36,7 +35,7 @@ public class IntegrationTests : IAsyncLifetime private const string SubscriptionJsonPath = "appsettings.Secrets.json"; // Resource tagging and cleanup constants - private const string ResourceGroupNamePrefix = "appconfig-test-"; + private const string ResourceGroupNamePrefix = "appconfig-dotnetprovider-test-"; private const string StoreNamePrefix = "integration-"; private const string TestResourceTag = "TestResource"; private const string CreatedByTag = "CreatedBy"; @@ -127,46 +126,81 @@ private string GenerateTimestampedResourceName(string prefix) } /// - /// Creates a dictionary of resource tags for identifying test resources + /// Generates a deterministic resource group name based on machine information /// - private IDictionary CreateResourceTags() + private string GetDeterministicResourceGroupName() { - return new Dictionary - { - { TestResourceTag, "true" }, - { CreatedByTag, "IntegrationTests" } - }; + string machineName = Environment.MachineName.ToLowerInvariant(); + string machineHash = Convert.ToBase64String( + System.Security.Cryptography.SHA256.Create() + .ComputeHash(System.Text.Encoding.UTF8.GetBytes(machineName))) + .Replace("/", "-").Replace("+", "-").Replace("=", "").Substring(0, 8); + + return $"{ResourceGroupNamePrefix}{machineHash}"; } /// - /// Cleans up stale test resources from previous test runs + /// Cleans up only stale App Configuration stores, not resource groups /// - private async Task CleanupStaleResources() + private async Task CleanupStaleStores() { + if (_resourceGroup == null) return; + try { - Console.WriteLine("Checking for leftover test resources..."); - - // Use the updated cleanup utility that removes all test resources regardless of age - var cleanupUtility = new AzureResourceCleanupUtility( - _armClient, - _subscriptionId, - ResourceGroupNamePrefix, - TestResourceTag); - - // Run the cleanup with console logging - await cleanupUtility.CleanupStaleResources( - dryRun: false, - progressCallback: message => Console.WriteLine(message)); + var stores = _resourceGroup.GetAppConfigurationStores(); + var staleTime = DateTime.UtcNow.AddHours(-StaleResourceThresholdHours); + + await foreach (var store in stores.GetAllAsync()) + { + // Only delete stores that: + // 1. Start with our test prefix + // 2. Have the TestResourceTag + // 3. Are older than the threshold + if (!store.Data.Name.StartsWith(StoreNamePrefix) || + !store.Data.Tags.ContainsKey(TestResourceTag)) + { + continue; + } + + // Check if the store is a temporary test store + if (store.Data.Tags.ContainsKey("TemporaryStore") && + store.Data.Tags["TemporaryStore"] == "true") + { + // If it has a creation time tag, check if it's stale + if (store.Data.Tags.TryGetValue("CreatedOn", out string createdOnStr) && + DateTime.TryParse(createdOnStr, out DateTime createdOn)) + { + if (createdOn < staleTime) + { + await store.DeleteAsync(WaitUntil.Started); + } + } + else + { + // If no creation time or it can't be parsed, use a heuristic + // based on the timestamp in the name + string name = store.Data.Name; + if (name.Length > StoreNamePrefix.Length + 12) // yyyyMMddHHmm format is 12 chars + { + string timeStampPart = name.Substring(StoreNamePrefix.Length, 12); + if (DateTime.TryParseExact(timeStampPart, "yyyyMMddHHmm", + System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.None, out DateTime timestamp)) + { + if (timestamp < staleTime) + { + await store.DeleteAsync(WaitUntil.Started); + } + } + } + } + } + } } - catch (Exception ex) when ( - ex is RequestFailedException || - ex is InvalidOperationException || - ex is TaskCanceledException || - ex is UnauthorizedAccessException) + catch (Exception ex) { - // Log but don't fail the test run - Console.WriteLine($"Error during test resource cleanup: {ex.Message}"); + Console.WriteLine($"Error during stale store cleanup: {ex.Message}"); } } @@ -187,23 +221,41 @@ public async Task InitializeAsync() // Initialize Azure Resource Manager client _armClient = new ArmClient(credential); - // Clean up any stale resources before creating new ones - await CleanupStaleResources(); + _testResourceGroupName = GetDeterministicResourceGroupName(); SubscriptionResource subscription = _armClient.GetSubscriptions().Get(_subscriptionId); - // Create a temporary resource group for this test run with timestamp and tags - _testResourceGroupName = GenerateTimestampedResourceName(ResourceGroupNamePrefix); + // Check if the resource group already exists + bool resourceGroupExists = false; + try + { + _resourceGroup = subscription.GetResourceGroup(_testResourceGroupName); + // If we get here, the resource group exists + resourceGroupExists = true; + } + catch (RequestFailedException ex) when (ex.Status == 404) + { + // Resource group doesn't exist, we'll create it + resourceGroupExists = false; + } - var rgData = new ResourceGroupData(new AzureLocation(DefaultLocation)); - // Add tags for resource identification - foreach (var tag in CreateResourceTags()) + // Create the resource group if it doesn't exist + if (!resourceGroupExists) { - rgData.Tags.Add(tag.Key, tag.Value); + var rgData = new ResourceGroupData(new AzureLocation(DefaultLocation)); + + // Add tags to identify this as a persistent test resource group + rgData.Tags.Add("PersistentTestResource", "true"); + rgData.Tags.Add(CreatedByTag, "IntegrationTests"); + rgData.Tags.Add("CreatedOn", DateTime.UtcNow.ToString("o")); + + var rgLro = await subscription.GetResourceGroups().CreateOrUpdateAsync( + WaitUntil.Completed, _testResourceGroupName, rgData); + _resourceGroup = rgLro.Value; } - var rgLro = await subscription.GetResourceGroups().CreateOrUpdateAsync(WaitUntil.Completed, _testResourceGroupName, rgData); - _resourceGroup = rgLro.Value; + // Clean up any stale resources before creating new ones + await CleanupStaleStores(); // Create unique store name for this test run with timestamp _testStoreName = GenerateTimestampedResourceName(StoreNamePrefix); @@ -211,11 +263,9 @@ public async Task InitializeAsync() // Create the App Configuration store var storeData = new AppConfigurationStoreData(new AzureLocation(DefaultLocation), new AppConfigurationSku("free")); - // Add the same tags to the store - foreach (var tag in CreateResourceTags()) - { - storeData.Tags.Add(tag.Key, tag.Value); - } + storeData.Tags.Add(TestResourceTag, "true"); + storeData.Tags.Add(CreatedByTag, "IntegrationTests"); + storeData.Tags.Add("TemporaryStore", "true"); var createOperation = await _resourceGroup.GetAppConfigurationStores().CreateOrUpdateAsync( WaitUntil.Completed, @@ -245,53 +295,42 @@ public async Task InitializeAsync() { if (!success) { - await CleanupResourceGroup(); + await CleanupAppConfigurationStore(); } } } /// - /// Helper method to clean up the resource group if initialization fails + /// Deletes only the App Configuration store, not the resource group /// - private async Task CleanupResourceGroup() + private async Task CleanupAppConfigurationStore() { - if (_resourceGroup != null) + if (_appConfigStore != null) { - await _resourceGroup.DeleteAsync(WaitUntil.Completed); - _resourceGroup = null; + try + { + Console.WriteLine($"Cleaning up test store: {_testStoreName}"); + await _appConfigStore.DeleteAsync(WaitUntil.Completed); + _appConfigStore = null; + Console.WriteLine("App Configuration store cleanup completed successfully"); + } + catch (Exception ex) when ( + ex is RequestFailedException || + ex is InvalidOperationException || + ex is TaskCanceledException) + { + Console.WriteLine($"Store cleanup failed: {ex.Message}."); + } } } - /// - /// Cleans up the temporary App Configuration store after tests are complete. - /// public async Task DisposeAsync() { - // Don't attempt cleanup if we don't have a resource group to delete - if (_resourceGroup == null) - { - return; - } - - try - { - Console.WriteLine($"Cleaning up test resources: Store={_testStoreName}, ResourceGroup={_testResourceGroupName}"); - await _resourceGroup.DeleteAsync(WaitUntil.Completed); - _resourceGroup = null; - Console.WriteLine("Resource cleanup completed successfully"); - } - catch (Exception ex) when ( - ex is RequestFailedException || - ex is InvalidOperationException || - ex is TaskCanceledException) - { - Console.WriteLine($"Test cleanup failed: {ex.Message}."); - } + await CleanupAppConfigurationStore(); - // Try to clean up any other stale resources that might exist from failed previous runs try { - await CleanupStaleResources(); + await CleanupStaleStores(); } catch (Exception ex) when ( ex is RequestFailedException || @@ -299,7 +338,7 @@ ex is InvalidOperationException || ex is TaskCanceledException || ex is UnauthorizedAccessException) { - Console.WriteLine($"Error during final stale resource cleanup: {ex.Message}"); + Console.WriteLine($"Error during stale store cleanup: {ex.Message}"); } } From b30523971523af527caed9ca9d7fd7ad5b8d20e4 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 18 Mar 2025 14:54:10 -0700 Subject: [PATCH 18/57] add ordering of apis test --- .../Integration/IntegrationTests.cs | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index de143b9eb..8b4d9fe5a 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -791,5 +791,180 @@ await _configClient.SetConfigurationSettingAsync( Assert.Equal("Debug", config[$"{jsonKey}:logging:providers:1"]); Assert.Equal("EventLog", config[$"{jsonKey}:logging:providers:2"]); } + + [Fact] + public async Task MethodOrderingDoesNotAffectConfiguration() + { + // Arrange - Setup test-specific keys + var testContext = await SetupTestKeys("MethodOrdering"); + + // Add an additional feature flag for testing + await _configClient.SetConfigurationSettingAsync( + ConfigurationModelFactory.ConfigurationSetting( + testContext.FeatureFlagKey + "_Ordering", + @"{ + ""id"": """ + testContext.KeyPrefix + @"FeatureOrdering"", + ""description"": ""Test feature for ordering"", + ""enabled"": true, + ""conditions"": { + ""client_filters"": [] + } + }", + contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + + // Add a section-based setting + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting($"{testContext.KeyPrefix}:Section1:Setting1", "SectionValue1")); + + // Create four different configurations with different method orderings + var configurations = new List(); + IConfigurationRefresher refresher1 = null; + IConfigurationRefresher refresher2 = null; + IConfigurationRefresher refresher3 = null; + IConfigurationRefresher refresher4 = null; + + // Configuration 1: Select -> ConfigureRefresh -> UseFeatureFlags + var config1 = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{testContext.KeyPrefix}:*"); + options.ConfigureRefresh(refresh => + { + refresh.Register(testContext.SentinelKey) + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + options.UseFeatureFlags(featureFlagOptions => + { + featureFlagOptions.Select(testContext.KeyPrefix + "*"); + }); + + refresher1 = options.GetRefresher(); + }) + .Build(); + configurations.Add(config1); + + // Configuration 2: ConfigureRefresh -> Select -> UseFeatureFlags + var config2 = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.ConfigureRefresh(refresh => + { + refresh.Register(testContext.SentinelKey) + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + options.Select($"{testContext.KeyPrefix}:*"); + options.UseFeatureFlags(featureFlagOptions => + { + featureFlagOptions.Select(testContext.KeyPrefix + "*"); + }); + + refresher2 = options.GetRefresher(); + }) + .Build(); + configurations.Add(config2); + + // Configuration 3: UseFeatureFlags -> Select -> ConfigureRefresh + var config3 = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.UseFeatureFlags(featureFlagOptions => + { + featureFlagOptions.Select(testContext.KeyPrefix + "*"); + }); + options.Select($"{testContext.KeyPrefix}:*"); + options.ConfigureRefresh(refresh => + { + refresh.Register(testContext.SentinelKey) + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + + refresher3 = options.GetRefresher(); + }) + .Build(); + configurations.Add(config3); + + // Configuration 4: UseFeatureFlags (with Select inside) -> ConfigureRefresh -> Select + var config4 = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.UseFeatureFlags(featureFlagOptions => + { + featureFlagOptions.Select(testContext.KeyPrefix + "*"); + }); + options.ConfigureRefresh(refresh => + { + refresh.Register(testContext.SentinelKey) + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + options.Select($"{testContext.KeyPrefix}:*"); + + refresher4 = options.GetRefresher(); + }) + .Build(); + configurations.Add(config4); + + // Assert - Initial values should be the same across all configurations + foreach (var config in configurations) + { + // Regular settings + Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); + Assert.Equal("InitialValue2", config[$"{testContext.KeyPrefix}:Setting2"]); + Assert.Equal("SectionValue1", config[$"{testContext.KeyPrefix}:Section1:Setting1"]); + + // Feature flags - check both flags are enabled + Assert.Equal("True", config[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); + Assert.Equal("True", config[$"FeatureManagement:{testContext.KeyPrefix}FeatureOrdering"]); + } + + // Update values in the store + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting($"{testContext.KeyPrefix}:Setting1", "UpdatedValue1")); + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting($"{testContext.KeyPrefix}:Section1:Setting1", "UpdatedSectionValue1")); + + // Update a feature flag + await _configClient.SetConfigurationSettingAsync( + ConfigurationModelFactory.ConfigurationSetting( + testContext.FeatureFlagKey, + @"{ + ""id"": """ + testContext.KeyPrefix + @"Feature"", + ""description"": ""Updated test feature"", + ""enabled"": false, + ""conditions"": { + ""client_filters"": [] + } + }", + contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + + // Update the sentinel key to trigger refresh + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting(testContext.SentinelKey, "Updated")); + + // Wait for cache to expire + await Task.Delay(TimeSpan.FromSeconds(2)); + + // Refresh all configurations + await refresher1.RefreshAsync(); + await refresher2.RefreshAsync(); + await refresher3.RefreshAsync(); + await refresher4.RefreshAsync(); + + // Assert - Updated values should be the same across all configurations + foreach (var config in configurations) + { + // Regular settings + Assert.Equal("UpdatedValue1", config[$"{testContext.KeyPrefix}:Setting1"]); + Assert.Equal("InitialValue2", config[$"{testContext.KeyPrefix}:Setting2"]); + Assert.Equal("UpdatedSectionValue1", config[$"{testContext.KeyPrefix}:Section1:Setting1"]); + + // Feature flags - first one should be updated to false + Assert.Equal("False", config[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); + Assert.Equal("True", config[$"FeatureManagement:{testContext.KeyPrefix}FeatureOrdering"]); + } + } } } From 25502c4dd7f4a33add62f90d2c9d2f1341281c11 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 18 Mar 2025 15:56:06 -0700 Subject: [PATCH 19/57] add refresh test --- .../Integration/IntegrationTests.cs | 144 +++++++++++++++++- 1 file changed, 139 insertions(+), 5 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 8b4d9fe5a..47a1c2825 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -915,8 +915,8 @@ await _configClient.SetConfigurationSettingAsync( Assert.Equal("InitialValue2", config[$"{testContext.KeyPrefix}:Setting2"]); Assert.Equal("SectionValue1", config[$"{testContext.KeyPrefix}:Section1:Setting1"]); - // Feature flags - check both flags are enabled - Assert.Equal("True", config[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); + // Feature flags + Assert.Equal("False", config[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); Assert.Equal("True", config[$"FeatureManagement:{testContext.KeyPrefix}FeatureOrdering"]); } @@ -933,7 +933,7 @@ await _configClient.SetConfigurationSettingAsync( @"{ ""id"": """ + testContext.KeyPrefix + @"Feature"", ""description"": ""Updated test feature"", - ""enabled"": false, + ""enabled"": true, ""conditions"": { ""client_filters"": [] } @@ -961,10 +961,144 @@ await _configClient.SetConfigurationSettingAsync( Assert.Equal("InitialValue2", config[$"{testContext.KeyPrefix}:Setting2"]); Assert.Equal("UpdatedSectionValue1", config[$"{testContext.KeyPrefix}:Section1:Setting1"]); - // Feature flags - first one should be updated to false - Assert.Equal("False", config[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); + // Feature flags - first one should be updated to true + Assert.Equal("True", config[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); Assert.Equal("True", config[$"FeatureManagement:{testContext.KeyPrefix}FeatureOrdering"]); } } + + [Fact] + public async Task RegisterWithRefreshAllAndRegisterAll_BehaveIdentically() + { + // Arrange - Setup test-specific keys + var testContext = await SetupTestKeys("RefreshEquivalency"); + + // Add another feature flag for testing + string secondFeatureFlagKey = $".appconfig.featureflag/{testContext.KeyPrefix}Feature2"; + await _configClient.SetConfigurationSettingAsync( + ConfigurationModelFactory.ConfigurationSetting( + secondFeatureFlagKey, + @"{""id"":""" + testContext.KeyPrefix + @"Feature2"",""description"":""Second test feature"",""enabled"":false}", + contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + + // Create two separate configuration builders with different refresh methods + // First configuration uses Register with refreshAll: true + IConfigurationRefresher refresher1 = null; + var config1 = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{testContext.KeyPrefix}:*"); + options.UseFeatureFlags(featureFlagOptions => + { + featureFlagOptions.Select(testContext.KeyPrefix + "*"); + }); + options.ConfigureRefresh(refresh => + { + refresh.Register(testContext.SentinelKey, refreshAll: true) + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + + refresher1 = options.GetRefresher(); + }) + .Build(); + + // Second configuration uses RegisterAll() + IConfigurationRefresher refresher2 = null; + var config2 = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{testContext.KeyPrefix}:*"); + options.UseFeatureFlags(featureFlagOptions => + { + featureFlagOptions.Select(testContext.KeyPrefix + "*"); + }); + options.ConfigureRefresh(refresh => + { + refresh.RegisterAll() + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + + refresher2 = options.GetRefresher(); + }) + .Build(); + + // Verify initial values for both configurations + Assert.Equal("InitialValue1", config1[$"{testContext.KeyPrefix}:Setting1"]); + Assert.Equal("InitialValue2", config1[$"{testContext.KeyPrefix}:Setting2"]); + Assert.Equal("False", config1[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); + Assert.Equal("False", config1[$"FeatureManagement:{testContext.KeyPrefix}Feature2"]); + + Assert.Equal("InitialValue1", config2[$"{testContext.KeyPrefix}:Setting1"]); + Assert.Equal("InitialValue2", config2[$"{testContext.KeyPrefix}:Setting2"]); + Assert.Equal("False", config2[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); + Assert.Equal("False", config2[$"FeatureManagement:{testContext.KeyPrefix}Feature2"]); + + // Update all values in the store + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting1", "UpdatedValue1")); + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting2", "UpdatedValue2")); + + // Update the feature flags + await _configClient.SetConfigurationSettingAsync( + ConfigurationModelFactory.ConfigurationSetting( + testContext.FeatureFlagKey, + @"{""id"":""" + testContext.KeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":true}", + contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + + await _configClient.SetConfigurationSettingAsync( + ConfigurationModelFactory.ConfigurationSetting( + secondFeatureFlagKey, + @"{""id"":""" + testContext.KeyPrefix + @"Feature2"",""description"":""Second test feature"",""enabled"":true}", + contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + + // Update the sentinel key to trigger refresh + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(testContext.SentinelKey, "Updated")); + + // Wait for cache to expire + await Task.Delay(TimeSpan.FromSeconds(2)); + + // Act - Refresh both configurations + await refresher1.RefreshAsync(); + await refresher2.RefreshAsync(); + + // Assert - Both configurations should be updated the same way + // For config1 (Register with refreshAll: true) + Assert.Equal("UpdatedValue1", config1[$"{testContext.KeyPrefix}:Setting1"]); + Assert.Equal("UpdatedValue2", config1[$"{testContext.KeyPrefix}:Setting2"]); + Assert.Equal("True", config1[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); + Assert.Equal("True", config1[$"FeatureManagement:{testContext.KeyPrefix}Feature2"]); + + // For config2 (RegisterAll) + Assert.Equal("UpdatedValue1", config2[$"{testContext.KeyPrefix}:Setting1"]); + Assert.Equal("UpdatedValue2", config2[$"{testContext.KeyPrefix}:Setting2"]); + Assert.Equal("True", config2[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); + Assert.Equal("True", config2[$"FeatureManagement:{testContext.KeyPrefix}Feature2"]); + + // Test deleting a key and a feature flag + await _configClient.DeleteConfigurationSettingAsync($"{testContext.KeyPrefix}:Setting2"); + await _configClient.DeleteConfigurationSettingAsync(secondFeatureFlagKey); + + // Update the sentinel key again to trigger refresh + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(testContext.SentinelKey, "UpdatedAgain")); + + // Wait for cache to expire + await Task.Delay(TimeSpan.FromSeconds(2)); + + // Refresh both configurations again + await refresher1.RefreshAsync(); + await refresher2.RefreshAsync(); + + // Both configurations should have removed the deleted key-value and feature flag + Assert.Equal("UpdatedValue1", config1[$"{testContext.KeyPrefix}:Setting1"]); + Assert.Null(config1[$"{testContext.KeyPrefix}:Setting2"]); + Assert.Equal("True", config1[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); + Assert.Null(config1[$"FeatureManagement:{testContext.KeyPrefix}Feature2"]); + + Assert.Equal("UpdatedValue1", config2[$"{testContext.KeyPrefix}:Setting1"]); + Assert.Null(config2[$"{testContext.KeyPrefix}:Setting2"]); + Assert.Equal("True", config2[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); + Assert.Null(config2[$"FeatureManagement:{testContext.KeyPrefix}Feature2"]); + } } } From e85e4b65a452639127379bb4c7cbf2cc3180db2c Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 18 Mar 2025 16:34:01 -0700 Subject: [PATCH 20/57] add tests --- .../Integration/IntegrationTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 47a1c2825..044b46b13 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -39,7 +39,7 @@ public class IntegrationTests : IAsyncLifetime private const string StoreNamePrefix = "integration-"; private const string TestResourceTag = "TestResource"; private const string CreatedByTag = "CreatedBy"; - private const int StaleResourceThresholdHours = 24; // Resources older than this are considered stale + private const int StaleResourceThresholdHours = 3; // Resources older than this are considered stale /// /// Class to hold test-specific key information @@ -831,7 +831,7 @@ await _configClient.SetConfigurationSettingAsync( options.Select($"{testContext.KeyPrefix}:*"); options.ConfigureRefresh(refresh => { - refresh.Register(testContext.SentinelKey) + refresh.Register(testContext.SentinelKey, true) .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); options.UseFeatureFlags(featureFlagOptions => @@ -851,7 +851,7 @@ await _configClient.SetConfigurationSettingAsync( options.Connect(GetConnectionString()); options.ConfigureRefresh(refresh => { - refresh.Register(testContext.SentinelKey) + refresh.Register(testContext.SentinelKey, true) .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); options.Select($"{testContext.KeyPrefix}:*"); @@ -877,7 +877,7 @@ await _configClient.SetConfigurationSettingAsync( options.Select($"{testContext.KeyPrefix}:*"); options.ConfigureRefresh(refresh => { - refresh.Register(testContext.SentinelKey) + refresh.Register(testContext.SentinelKey, true) .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); @@ -897,7 +897,7 @@ await _configClient.SetConfigurationSettingAsync( }); options.ConfigureRefresh(refresh => { - refresh.Register(testContext.SentinelKey) + refresh.Register(testContext.SentinelKey, true) .SetRefreshInterval(TimeSpan.FromSeconds(1)); }); options.Select($"{testContext.KeyPrefix}:*"); From 2453aa64a30cfafbecf5a550478d6f4d40258cdc Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 19 Mar 2025 15:21:13 -0700 Subject: [PATCH 21/57] add failover test --- .../Integration/IntegrationTests.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 044b46b13..1f444fd8a 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -1,5 +1,6 @@ using Azure; using Azure.Core; +using Azure.Core.Diagnostics; using Azure.Data.AppConfiguration; using Azure.Identity; using Azure.ResourceManager; @@ -9,10 +10,13 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.AzureAppConfiguration; using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement; +using Microsoft.Extensions.Options; using System; using System.Collections.Generic; +using System.Diagnostics.Tracing; using System.IO; using System.Linq; +using System.Text; using System.Text.Json; using System.Threading.Tasks; using Xunit; @@ -1100,5 +1104,42 @@ await _configClient.SetConfigurationSettingAsync( Assert.Equal("True", config2[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); Assert.Null(config2[$"FeatureManagement:{testContext.KeyPrefix}Feature2"]); } + + [Fact] + public async Task HandlesFailoverOnStartup() + { + // Arrange - Setup test-specific keys + var testContext = await SetupTestKeys("FailoverStartup"); + IConfigurationRefresher refresher = null; + + string connectionString = GetConnectionString(); + + // Create a connection string that will fail + string primaryConnectionString = ConnectionStringUtils.Build( + TestHelpers.PrimaryConfigStoreEndpoint, + ConnectionStringUtils.Parse(connectionString, ConnectionStringUtils.IdSection), + ConnectionStringUtils.Parse(connectionString, ConnectionStringUtils.SecretSection)); + string secondaryConnectionString = connectionString; + + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(new List { primaryConnectionString, secondaryConnectionString }); + options.Select($"{testContext.KeyPrefix}:*"); + + // Configure refresh + options.ConfigureRefresh(refresh => + { + refresh.Register(testContext.SentinelKey, refreshAll: true) + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + + refresher = options.GetRefresher(); + }) + .Build(); + + // Verify initial values + Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); + } } } From f4ee6758689d9349272ba74dd1d486650dda0cad Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 27 Mar 2025 13:51:55 -0700 Subject: [PATCH 22/57] adding snapshot tests first draft --- .../Integration/IntegrationTests.cs | 473 +++++++++++++++++- 1 file changed, 468 insertions(+), 5 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 1f444fd8a..6c72b435f 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -1,6 +1,5 @@ using Azure; using Azure.Core; -using Azure.Core.Diagnostics; using Azure.Data.AppConfiguration; using Azure.Identity; using Azure.ResourceManager; @@ -10,13 +9,10 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.AzureAppConfiguration; using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement; -using Microsoft.Extensions.Options; using System; using System.Collections.Generic; -using System.Diagnostics.Tracing; using System.IO; using System.Linq; -using System.Text; using System.Text.Json; using System.Threading.Tasks; using Xunit; @@ -48,7 +44,7 @@ public class IntegrationTests : IAsyncLifetime /// /// Class to hold test-specific key information /// - private class TestContext + public class TestContext { public string KeyPrefix { get; set; } public string SentinelKey { get; set; } @@ -143,6 +139,35 @@ private string GetDeterministicResourceGroupName() return $"{ResourceGroupNamePrefix}{machineHash}"; } + /// + /// Creates a snapshot with the given name containing the test context's settings + /// + private async Task CreateSnapshot(string snapshotName, TestContext testContext) + { + // Create a snapshot with the test keys + var settingsToInclude = new List + { + new ConfigurationSettingsFilter($"{testContext.KeyPrefix}:*") + }; + + ConfigurationSnapshot snapshot = new ConfigurationSnapshot(settingsToInclude); + + snapshot.SnapshotComposition = SnapshotComposition.Key; + + try + { + // Create the snapshot + CreateSnapshotOperation operation = await _configClient.CreateSnapshotAsync(WaitUntil.Completed, snapshotName, snapshot); + + return operation.Value.Name; + } + catch (Exception ex) + { + Console.WriteLine($"Error creating snapshot: {ex.Message}"); + throw; + } + } + /// /// Cleans up only stale App Configuration stores, not resource groups /// @@ -1141,5 +1166,443 @@ public async Task HandlesFailoverOnStartup() // Verify initial values Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); } + + /// + /// Test verifies that a snapshot can be created and loaded correctly + /// + [Fact] + public async Task LoadSnapshot_RetrievesValuesFromSnapshot() + { + // Arrange - Setup test-specific keys + var testContext = await SetupTestKeys("SnapshotTest"); + string snapshotName = $"snapshot-{testContext.KeyPrefix}"; + + // Create a snapshot with the test keys + await CreateSnapshot(snapshotName, testContext); + + // Update values after snapshot is taken to verify snapshot has original values + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting1", "UpdatedAfterSnapshot")); + + // Act - Load configuration from snapshot + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.SelectSnapshot(snapshotName); + }) + .Build(); + + // Assert - Should have original values from snapshot, not updated values + Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); + Assert.Equal("InitialValue2", config[$"{testContext.KeyPrefix}:Setting2"]); + + // Cleanup - Delete the snapshot + await _configClient.ArchiveSnapshotAsync(snapshotName); + } + + /// + /// Test verifies error handling when a snapshot doesn't exist + /// + [Fact] + public async Task LoadSnapshot_ThrowsException_WhenSnapshotDoesNotExist() + { + // Arrange - Setup test-specific keys + var testContext = await SetupTestKeys("NonExistentSnapshotTest"); + string nonExistentSnapshotName = $"snapshot-does-not-exist-{Guid.NewGuid()}"; + + // Act & Assert - Loading a non-existent snapshot should throw + var exception = await Assert.ThrowsAsync(() => + { + return Task.FromResult(new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.SelectSnapshot(nonExistentSnapshotName); + }) + .Build()); + }); + + // Verify the exception message contains snapshot name + Assert.Contains(nonExistentSnapshotName, exception.Message); + } + + /// + /// Test verifies that multiple snapshots can be loaded in the same configuration + /// + [Fact] + public async Task LoadMultipleSnapshots_MergesConfigurationCorrectly() + { + // Arrange - Setup test-specific keys for two separate snapshots + var testContext1 = await SetupTestKeys("SnapshotMergeTest1"); + var testContext2 = await SetupTestKeys("SnapshotMergeTest2"); + + // Create specific values for second snapshot + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting($"{testContext2.KeyPrefix}:UniqueKey", "UniqueValue")); + + string snapshotName1 = $"snapshot-{testContext1.KeyPrefix}"; + string snapshotName2 = $"snapshot-{testContext2.KeyPrefix}"; + + // Create snapshots + await CreateSnapshot(snapshotName1, testContext1); + await CreateSnapshot(snapshotName2, testContext2); + + try + { + // Act - Load configuration from both snapshots + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.SelectSnapshot(snapshotName1); + options.SelectSnapshot(snapshotName2); + }) + .Build(); + + // Assert - Should have values from both snapshots + Assert.Equal("InitialValue1", config[$"{testContext1.KeyPrefix}:Setting1"]); + Assert.Equal("InitialValue1", config[$"{testContext2.KeyPrefix}:Setting1"]); + Assert.Equal("UniqueValue", config[$"{testContext2.KeyPrefix}:UniqueKey"]); + } + finally + { + // Cleanup - Delete the snapshots + await _configClient.ArchiveSnapshotAsync(snapshotName1); + await _configClient.ArchiveSnapshotAsync(snapshotName2); + } + } + + /// + /// Test verifies that a snapshot can be refreshed and updates are detected + /// + [Fact] + public async Task RefreshSnapshot_UpdatesConfigurationWhenSnapshotChanges() + { + // Arrange - Setup test-specific keys + var testContext = await SetupTestKeys("SnapshotRefreshTest"); + string snapshotName = $"snapshot-{testContext.KeyPrefix}"; + + // Create a snapshot with the test keys + await CreateSnapshot(snapshotName, testContext); + IConfigurationRefresher refresher = null; + + try + { + // Act - Load configuration from snapshot with refresher + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.SelectSnapshot(snapshotName); + options.ConfigureRefresh(refresh => + { + refresh.RegisterAll() + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + + refresher = options.GetRefresher(); + }) + .Build(); + + // Verify initial values + Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); + + // Update a key in the config store + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting($"{testContext.KeyPrefix}:Setting1", "UpdatedValue1")); + + // Create a new snapshot with the updated value + await _configClient.ArchiveSnapshotAsync(snapshotName); + await CreateSnapshot(snapshotName, testContext); + + // Wait for cache to expire + await Task.Delay(TimeSpan.FromSeconds(2)); + + // Refresh the configuration + await refresher.RefreshAsync(); + + // Assert - Should have updated values from refreshed snapshot + Assert.Equal("UpdatedValue1", config[$"{testContext.KeyPrefix}:Setting1"]); + } + finally + { + // Cleanup - Delete the snapshot + await _configClient.ArchiveSnapshotAsync(snapshotName); + } + } + + /// + /// Test verifies that different snapshot composition types are handled correctly + /// + [Fact] + public async Task SnapshotCompositionTypes_AreHandledCorrectly() + { + // Arrange - Setup test-specific keys + var testContext = await SetupTestKeys("SnapshotCompositionTest"); + string keyOnlySnapshotName = $"snapshot-key-{testContext.KeyPrefix}"; + string invalidCompositionSnapshotName = $"snapshot-invalid-{testContext.KeyPrefix}"; + + // Create a snapshot with the test keys + var settingsToInclude = new List + { + new ConfigurationSettingsFilter($"{testContext.KeyPrefix}:*") + }; + + ConfigurationSnapshot keyOnlySnapshot = new ConfigurationSnapshot(settingsToInclude); + + keyOnlySnapshot.SnapshotComposition = SnapshotComposition.Key; + + // Create the snapshot + await _configClient.CreateSnapshotAsync(WaitUntil.Completed, keyOnlySnapshotName, keyOnlySnapshot); + + ConfigurationSnapshot invalidSnapshot = new ConfigurationSnapshot(settingsToInclude); + + invalidSnapshot.SnapshotComposition = SnapshotComposition.KeyLabel; + + // Create the snapshot + await _configClient.CreateSnapshotAsync(WaitUntil.Completed, invalidCompositionSnapshotName, invalidSnapshot); + + try + { + // Act & Assert - Loading a key-only snapshot should work + var config1 = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.SelectSnapshot(keyOnlySnapshotName); + }) + .Build(); + + Assert.Equal("InitialValue1", config1[$"{testContext.KeyPrefix}:Setting1"]); + + // Act & Assert - Loading a snapshot with invalid composition should throw + var exception = await Assert.ThrowsAsync(() => + { + return Task.FromResult(new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.SelectSnapshot(invalidCompositionSnapshotName); + }) + .Build()); + }); + + // Verify the exception message mentions composition type + Assert.Contains("SnapshotComposition", exception.Message); + Assert.Contains("key", exception.Message); + Assert.Contains("KeyAndLabel", exception.Message); + } + finally + { + // Cleanup - Delete the snapshots + await _configClient.ArchiveSnapshotAsync(keyOnlySnapshotName); + await _configClient.ArchiveSnapshotAsync(invalidCompositionSnapshotName); + } + } + + /// + /// Test verifies that snapshots work with feature flags + /// + [Fact] + public async Task SnapshotWithFeatureFlags_LoadsConfigurationCorrectly() + { + // Arrange - Setup test-specific keys + var testContext = await SetupTestKeys("SnapshotFeatureFlagTest"); + string snapshotName = $"snapshot-ff-{testContext.KeyPrefix}"; + + // Update the feature flag to be enabled before creating the snapshot + await _configClient.SetConfigurationSettingAsync( + ConfigurationModelFactory.ConfigurationSetting( + testContext.FeatureFlagKey, + @"{""id"":""" + testContext.KeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":true}", + contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + + // Create a snapshot with the test keys + var settingsToInclude = new List + { + new ConfigurationSettingsFilter($"{testContext.KeyPrefix}:*"), + new ConfigurationSettingsFilter($".appconfig.featureflag/{testContext.KeyPrefix}*") + }; + + ConfigurationSnapshot snapshot = new ConfigurationSnapshot(settingsToInclude); + + snapshot.SnapshotComposition = SnapshotComposition.Key; + + // Create the snapshot + await _configClient.CreateSnapshotAsync(WaitUntil.Completed, snapshotName, snapshot); + + // Update feature flag to disabled after creating snapshot + await _configClient.SetConfigurationSettingAsync( + ConfigurationModelFactory.ConfigurationSetting( + testContext.FeatureFlagKey, + @"{""id"":""" + testContext.KeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":false}", + contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + + try + { + // Act - Load configuration from snapshot with feature flags + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.SelectSnapshot(snapshotName); + options.UseFeatureFlags(); + }) + .Build(); + + // Assert - Should have feature flag enabled state from snapshot + Assert.Equal("True", config[$"FeatureManagement:{testContext.KeyPrefix}Feature"]); + } + finally + { + // Cleanup - Delete the snapshot + await _configClient.ArchiveSnapshotAsync(snapshotName); + } + } + + /// + /// Test verifies call ordering of snapshots, select, and feature flags + /// + [Fact] + public async Task CallOrdering_SnapshotsWithSelectAndFeatureFlags() + { + // Arrange - Setup test-specific keys for multiple snapshots + var mainContext = await SetupTestKeys("SnapshotOrdering"); + var secondContext = await SetupTestKeys("SnapshotOrdering2"); + var thirdContext = await SetupTestKeys("SnapshotOrdering3"); + + // Create specific values for each snapshot + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting($"{mainContext.KeyPrefix}:UniqueMain", "MainValue")); + + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting($"{secondContext.KeyPrefix}:UniqueSecond", "SecondValue")); + + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting($"{thirdContext.KeyPrefix}:UniqueThird", "ThirdValue")); + + // Create additional feature flags + string secondFeatureFlagKey = $".appconfig.featureflag/{mainContext.KeyPrefix}Feature2"; + await _configClient.SetConfigurationSettingAsync( + ConfigurationModelFactory.ConfigurationSetting( + secondFeatureFlagKey, + @"{""id"":""" + mainContext.KeyPrefix + @"Feature2"",""description"":""Second test feature"",""enabled"":true}", + contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + + string thirdFeatureFlagKey = $".appconfig.featureflag/{secondContext.KeyPrefix}Feature"; + await _configClient.SetConfigurationSettingAsync( + ConfigurationModelFactory.ConfigurationSetting( + thirdFeatureFlagKey, + @"{""id"":""" + secondContext.KeyPrefix + @"Feature"",""description"":""Third test feature"",""enabled"":true}", + contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + + // Create snapshots + string snapshot1 = $"snapshot-{mainContext.KeyPrefix}-1"; + string snapshot2 = $"snapshot-{secondContext.KeyPrefix}-2"; + string snapshot3 = $"snapshot-{thirdContext.KeyPrefix}-3"; + + await CreateSnapshot(snapshot1, mainContext); + await CreateSnapshot(snapshot2, secondContext); + await CreateSnapshot(snapshot3, thirdContext); + + try + { + // Test different orderings of SelectSnapshot, Select and UseFeatureFlags + + // Order 1: SelectSnapshot -> Select -> UseFeatureFlags + var config1 = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.SelectSnapshot(snapshot1); + options.Select($"{mainContext.KeyPrefix}:*"); + options.UseFeatureFlags(ff => + { + ff.Select($"{mainContext.KeyPrefix}Feature*"); + }); + }) + .Build(); + + // Order 2: Select -> SelectSnapshot -> UseFeatureFlags + var config2 = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{secondContext.KeyPrefix}:*"); + options.SelectSnapshot(snapshot2); + options.UseFeatureFlags(ff => + { + ff.Select($"{secondContext.KeyPrefix}Feature*"); + }); + }) + .Build(); + + // Order 3: UseFeatureFlags -> SelectSnapshot -> Select + var config3 = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.UseFeatureFlags(); + options.SelectSnapshot(snapshot3); + options.Select($"{thirdContext.KeyPrefix}:*"); + }) + .Build(); + + // Order 4: Multiple snapshots with interleaved operations + var config4 = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.SelectSnapshot(snapshot1); + options.UseFeatureFlags(ff => + { + ff.Select($"{mainContext.KeyPrefix}Feature*"); + }); + options.SelectSnapshot(snapshot2); + options.Select($"{secondContext.KeyPrefix}:*"); + options.UseFeatureFlags(ff => + { + ff.Select($"{secondContext.KeyPrefix}Feature*"); + }); + options.SelectSnapshot(snapshot3); + }) + .Build(); + + // Verify config1: Should have values from snapshot1 and feature flags from mainContext + Assert.Equal("InitialValue1", config1[$"{mainContext.KeyPrefix}:Setting1"]); + Assert.Equal("MainValue", config1[$"{mainContext.KeyPrefix}:UniqueMain"]); + Assert.Equal("True", config1[$"FeatureManagement:{mainContext.KeyPrefix}Feature"]); + Assert.Equal("True", config1[$"FeatureManagement:{mainContext.KeyPrefix}Feature2"]); + + // Verify config2: Should have values from snapshot2 and feature flags from secondContext + Assert.Equal("InitialValue1", config2[$"{secondContext.KeyPrefix}:Setting1"]); + Assert.Equal("SecondValue", config2[$"{secondContext.KeyPrefix}:UniqueSecond"]); + Assert.Equal("True", config2[$"FeatureManagement:{secondContext.KeyPrefix}Feature"]); + + // Verify config3: Should have values from snapshot3 and all feature flags + Assert.Equal("InitialValue1", config3[$"{thirdContext.KeyPrefix}:Setting1"]); + Assert.Equal("ThirdValue", config3[$"{thirdContext.KeyPrefix}:UniqueThird"]); + Assert.Equal("True", config3[$"FeatureManagement:{mainContext.KeyPrefix}Feature"]); + Assert.Equal("True", config3[$"FeatureManagement:{secondContext.KeyPrefix}Feature"]); + + // Verify config4: Should have values from all three snapshots + Assert.Equal("InitialValue1", config4[$"{mainContext.KeyPrefix}:Setting1"]); + Assert.Equal("MainValue", config4[$"{mainContext.KeyPrefix}:UniqueMain"]); + Assert.Equal("InitialValue1", config4[$"{secondContext.KeyPrefix}:Setting1"]); + Assert.Equal("SecondValue", config4[$"{secondContext.KeyPrefix}:UniqueSecond"]); + Assert.Equal("InitialValue1", config4[$"{thirdContext.KeyPrefix}:Setting1"]); + Assert.Equal("ThirdValue", config4[$"{thirdContext.KeyPrefix}:UniqueThird"]); + Assert.Equal("True", config4[$"FeatureManagement:{mainContext.KeyPrefix}Feature"]); + Assert.Equal("True", config4[$"FeatureManagement:{mainContext.KeyPrefix}Feature2"]); + Assert.Equal("True", config4[$"FeatureManagement:{secondContext.KeyPrefix}Feature"]); + } + finally + { + // Cleanup - Delete the snapshots + await _configClient.ArchiveSnapshotAsync(snapshot1); + await _configClient.ArchiveSnapshotAsync(snapshot2); + await _configClient.ArchiveSnapshotAsync(snapshot3); + } + } } } From d9f18ef10d8d1b27fad45faeedaa03ff5cfca7b4 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 27 Mar 2025 15:17:57 -0700 Subject: [PATCH 23/57] fix snapshot tests --- .../Integration/IntegrationTests.cs | 75 +++---------------- .../Tests.AzureAppConfiguration.csproj | 4 +- 2 files changed, 13 insertions(+), 66 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 6c72b435f..d08261431 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -44,11 +44,13 @@ public class IntegrationTests : IAsyncLifetime /// /// Class to hold test-specific key information /// - public class TestContext + private class TestContext { public string KeyPrefix { get; set; } public string SentinelKey { get; set; } public string FeatureFlagKey { get; set; } + public string KeyVaultReferenceKey { get; set; } + public string SecretName { get; set; } } // Client for direct manipulation of the store @@ -318,6 +320,8 @@ public async Task InitializeAsync() // Initialize the configuration client with the connection string _configClient = new ConfigurationClient(_connectionString); + // Create a Key Vault for testing (if needed) + success = true; } finally @@ -1272,65 +1276,6 @@ await _configClient.SetConfigurationSettingAsync( } } - /// - /// Test verifies that a snapshot can be refreshed and updates are detected - /// - [Fact] - public async Task RefreshSnapshot_UpdatesConfigurationWhenSnapshotChanges() - { - // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("SnapshotRefreshTest"); - string snapshotName = $"snapshot-{testContext.KeyPrefix}"; - - // Create a snapshot with the test keys - await CreateSnapshot(snapshotName, testContext); - IConfigurationRefresher refresher = null; - - try - { - // Act - Load configuration from snapshot with refresher - var config = new ConfigurationBuilder() - .AddAzureAppConfiguration(options => - { - options.Connect(GetConnectionString()); - options.SelectSnapshot(snapshotName); - options.ConfigureRefresh(refresh => - { - refresh.RegisterAll() - .SetRefreshInterval(TimeSpan.FromSeconds(1)); - }); - - refresher = options.GetRefresher(); - }) - .Build(); - - // Verify initial values - Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); - - // Update a key in the config store - await _configClient.SetConfigurationSettingAsync( - new ConfigurationSetting($"{testContext.KeyPrefix}:Setting1", "UpdatedValue1")); - - // Create a new snapshot with the updated value - await _configClient.ArchiveSnapshotAsync(snapshotName); - await CreateSnapshot(snapshotName, testContext); - - // Wait for cache to expire - await Task.Delay(TimeSpan.FromSeconds(2)); - - // Refresh the configuration - await refresher.RefreshAsync(); - - // Assert - Should have updated values from refreshed snapshot - Assert.Equal("UpdatedValue1", config[$"{testContext.KeyPrefix}:Setting1"]); - } - finally - { - // Cleanup - Delete the snapshot - await _configClient.ArchiveSnapshotAsync(snapshotName); - } - } - /// /// Test verifies that different snapshot composition types are handled correctly /// @@ -1390,7 +1335,7 @@ public async Task SnapshotCompositionTypes_AreHandledCorrectly() // Verify the exception message mentions composition type Assert.Contains("SnapshotComposition", exception.Message); Assert.Contains("key", exception.Message); - Assert.Contains("KeyAndLabel", exception.Message); + Assert.Contains("label", exception.Message); } finally { @@ -1445,8 +1390,8 @@ await _configClient.SetConfigurationSettingAsync( .AddAzureAppConfiguration(options => { options.Connect(GetConnectionString()); - options.SelectSnapshot(snapshotName); options.UseFeatureFlags(); + options.SelectSnapshot(snapshotName); }) .Build(); @@ -1571,7 +1516,7 @@ await _configClient.SetConfigurationSettingAsync( // Verify config1: Should have values from snapshot1 and feature flags from mainContext Assert.Equal("InitialValue1", config1[$"{mainContext.KeyPrefix}:Setting1"]); Assert.Equal("MainValue", config1[$"{mainContext.KeyPrefix}:UniqueMain"]); - Assert.Equal("True", config1[$"FeatureManagement:{mainContext.KeyPrefix}Feature"]); + Assert.Equal("False", config1[$"FeatureManagement:{mainContext.KeyPrefix}Feature"]); Assert.Equal("True", config1[$"FeatureManagement:{mainContext.KeyPrefix}Feature2"]); // Verify config2: Should have values from snapshot2 and feature flags from secondContext @@ -1582,7 +1527,7 @@ await _configClient.SetConfigurationSettingAsync( // Verify config3: Should have values from snapshot3 and all feature flags Assert.Equal("InitialValue1", config3[$"{thirdContext.KeyPrefix}:Setting1"]); Assert.Equal("ThirdValue", config3[$"{thirdContext.KeyPrefix}:UniqueThird"]); - Assert.Equal("True", config3[$"FeatureManagement:{mainContext.KeyPrefix}Feature"]); + Assert.Equal("False", config3[$"FeatureManagement:{mainContext.KeyPrefix}Feature"]); Assert.Equal("True", config3[$"FeatureManagement:{secondContext.KeyPrefix}Feature"]); // Verify config4: Should have values from all three snapshots @@ -1592,7 +1537,7 @@ await _configClient.SetConfigurationSettingAsync( Assert.Equal("SecondValue", config4[$"{secondContext.KeyPrefix}:UniqueSecond"]); Assert.Equal("InitialValue1", config4[$"{thirdContext.KeyPrefix}:Setting1"]); Assert.Equal("ThirdValue", config4[$"{thirdContext.KeyPrefix}:UniqueThird"]); - Assert.Equal("True", config4[$"FeatureManagement:{mainContext.KeyPrefix}Feature"]); + Assert.Equal("False", config4[$"FeatureManagement:{mainContext.KeyPrefix}Feature"]); Assert.Equal("True", config4[$"FeatureManagement:{mainContext.KeyPrefix}Feature2"]); Assert.Equal("True", config4[$"FeatureManagement:{secondContext.KeyPrefix}Feature"]); } diff --git a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj index 5dc6bfc07..f0db1817a 100644 --- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj +++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj @@ -1,4 +1,4 @@ - + net48;net6.0;net8.0 @@ -14,6 +14,7 @@ + @@ -25,6 +26,7 @@ + From fb8aa368d1f488f93ef5df32516a0eab176bc9a7 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 28 Mar 2025 13:56:03 -0700 Subject: [PATCH 24/57] add key vault tests --- .../Integration/IntegrationTests.cs | 326 +++++++++++++++++- .../Tests.AzureAppConfiguration.csproj | 3 +- 2 files changed, 315 insertions(+), 14 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index d08261431..bc1202721 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -5,9 +5,13 @@ using Azure.ResourceManager; using Azure.ResourceManager.AppConfiguration; using Azure.ResourceManager.AppConfiguration.Models; +using Azure.ResourceManager.KeyVault; +using Azure.ResourceManager.KeyVault.Models; using Azure.ResourceManager.Resources; +using Azure.Security.KeyVault.Secrets; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.AzureAppConfiguration; +using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault; using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement; using System; using System.Collections.Generic; @@ -37,6 +41,7 @@ public class IntegrationTests : IAsyncLifetime // Resource tagging and cleanup constants private const string ResourceGroupNamePrefix = "appconfig-dotnetprovider-test-"; private const string StoreNamePrefix = "integration-"; + private const string KeyVaultNamePrefix = "kv-int-"; private const string TestResourceTag = "TestResource"; private const string CreatedByTag = "CreatedBy"; private const int StaleResourceThresholdHours = 3; // Resources older than this are considered stale @@ -56,15 +61,21 @@ private class TestContext // Client for direct manipulation of the store private ConfigurationClient _configClient; + // Client for Key Vault operations + private SecretClient _secretClient; + // Connection string for the store private string _connectionString; // Store management resources private ArmClient _armClient; private string _testStoreName; + private string _testKeyVaultName; private string _testResourceGroupName; private AppConfigurationStoreResource _appConfigStore; + private KeyVaultResource _keyVault; private Uri _appConfigEndpoint; + private Uri _keyVaultEndpoint; private ResourceGroupResource _resourceGroup; private string _subscriptionId; @@ -171,14 +182,15 @@ private async Task CreateSnapshot(string snapshotName, TestContext testC } /// - /// Cleans up only stale App Configuration stores, not resource groups + /// Cleans up only stale App Configuration stores and Key Vaults, not resource groups /// - private async Task CleanupStaleStores() + private async Task CleanupStaleResources() { if (_resourceGroup == null) return; try { + // Clean up stale App Configuration stores var stores = _resourceGroup.GetAppConfigurationStores(); var staleTime = DateTime.UtcNow.AddHours(-StaleResourceThresholdHours); @@ -228,15 +240,65 @@ private async Task CleanupStaleStores() } } } + + // Clean up stale Key Vaults + var keyVaults = _resourceGroup.GetKeyVaults(); + + await foreach (var vault in keyVaults.GetAllAsync()) + { + // Only delete key vaults that: + // 1. Start with our test prefix + // 2. Have the TestResourceTag + // 3. Are older than the threshold + if (!vault.Data.Name.StartsWith(KeyVaultNamePrefix) || + !vault.Data.Tags.ContainsKey(TestResourceTag)) + { + continue; + } + + // Check if the key vault is a temporary test resource + if (vault.Data.Tags.ContainsKey("TemporaryStore") && + vault.Data.Tags["TemporaryStore"] == "true") + { + // If it has a creation time tag, check if it's stale + if (vault.Data.Tags.TryGetValue("CreatedOn", out string createdOnStr) && + DateTime.TryParse(createdOnStr, out DateTime createdOn)) + { + if (createdOn < staleTime) + { + await vault.DeleteAsync(WaitUntil.Started); + } + } + else + { + // If no creation time or it can't be parsed, use a heuristic + // based on the timestamp in the name + string name = vault.Data.Name; + if (name.Length > KeyVaultNamePrefix.Length + 12) // yyyyMMddHHmm format is 12 chars + { + string timeStampPart = name.Substring(KeyVaultNamePrefix.Length, 12); + if (DateTime.TryParseExact(timeStampPart, "yyyyMMddHHmm", + System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.None, out DateTime timestamp)) + { + if (timestamp < staleTime) + { + await vault.DeleteAsync(WaitUntil.Started); + } + } + } + } + } + } } catch (Exception ex) { - Console.WriteLine($"Error during stale store cleanup: {ex.Message}"); + Console.WriteLine($"Error during stale resource cleanup: {ex.Message}"); } } /// - /// Creates a temporary Azure App Configuration store and adds test data. + /// Creates a temporary Azure App Configuration store and Key Vault, then adds test data. /// public async Task InitializeAsync() { @@ -286,10 +348,11 @@ public async Task InitializeAsync() } // Clean up any stale resources before creating new ones - await CleanupStaleStores(); + await CleanupStaleResources(); // Create unique store name for this test run with timestamp _testStoreName = GenerateTimestampedResourceName(StoreNamePrefix); + _testKeyVaultName = GenerateTimestampedResourceName(KeyVaultNamePrefix); // Create the App Configuration store var storeData = new AppConfigurationStoreData(new AzureLocation(DefaultLocation), new AppConfigurationSku("free")); @@ -297,6 +360,7 @@ public async Task InitializeAsync() storeData.Tags.Add(TestResourceTag, "true"); storeData.Tags.Add(CreatedByTag, "IntegrationTests"); storeData.Tags.Add("TemporaryStore", "true"); + storeData.Tags.Add("CreatedOn", DateTime.UtcNow.ToString("o")); var createOperation = await _resourceGroup.GetAppConfigurationStores().CreateOrUpdateAsync( WaitUntil.Completed, @@ -320,7 +384,60 @@ public async Task InitializeAsync() // Initialize the configuration client with the connection string _configClient = new ConfigurationClient(_connectionString); - // Create a Key Vault for testing (if needed) + // Create the Key Vault + try + { + // Create an access policy for the current user + var userObjectId = await GetCurrentUserObjectId(credential); + + // Define vault properties + var vaultProperties = new KeyVaultProperties( + new Guid(await GetTenantId(credential)), + new KeyVaultSku(KeyVaultSkuFamily.A, KeyVaultSkuName.Standard)); + + //vaultProperties.AccessPolicies = { + // new KeyVaultAccessPolicy + // { + // ObjectId = userObjectId, + // TenantId = new Guid(await GetTenantId(credential)), + // Permissions = new KeyVaultPermissions + // { + // Secrets = { + // KeyVaultSecretPermission.Get, + // KeyVaultSecretPermission.List, + // KeyVaultSecretPermission.Set, + // KeyVaultSecretPermission.Delete + // } + // } + // } + //}, + + // Create Key Vault resource data + var vaultData = new KeyVaultCreateOrUpdateContent(new AzureLocation(DefaultLocation), vaultProperties); + + // Add tags + vaultData.Tags.Add(TestResourceTag, "true"); + vaultData.Tags.Add(CreatedByTag, "IntegrationTests"); + vaultData.Tags.Add("TemporaryStore", "true"); + vaultData.Tags.Add("CreatedOn", DateTime.UtcNow.ToString("o")); + + // Create the vault + var vaultCreateOperation = await _resourceGroup.GetKeyVaults().CreateOrUpdateAsync( + WaitUntil.Completed, + _testKeyVaultName, + vaultData); + + _keyVault = vaultCreateOperation.Value; + _keyVaultEndpoint = _keyVault.Data.Properties.VaultUri; + + // Create a Secret Client for the vault + _secretClient = new SecretClient(_keyVaultEndpoint, credential); + } + catch (Exception ex) + { + Console.WriteLine($"Error creating Key Vault: {ex.Message}"); + // We'll continue without Key Vault if it fails + } success = true; } @@ -328,16 +445,83 @@ public async Task InitializeAsync() { if (!success) { - await CleanupAppConfigurationStore(); + await CleanupResources(); } } } /// - /// Deletes only the App Configuration store, not the resource group + /// Get the current user's Object ID + /// + private async Task GetCurrentUserObjectId(TokenCredential credential) + { + // Use the Microsoft Graph API to get the current user's object ID + var token = await credential.GetTokenAsync( + new TokenRequestContext(new[] { "https://graph.microsoft.com/.default" }), + default); + + // Parse the token to get user information + var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler(); + var jsonToken = handler.ReadToken(token.Token) as System.IdentityModel.Tokens.Jwt.JwtSecurityToken; + + // Read the object id (oid) claim + string oid = jsonToken?.Claims.FirstOrDefault(c => c.Type == "oid")?.Value; + + if (string.IsNullOrEmpty(oid)) + { + throw new InvalidOperationException("Could not determine the current user's object ID."); + } + + return new Guid(oid); + } + + /// + /// Get the tenant ID /// - private async Task CleanupAppConfigurationStore() + private async Task GetTenantId(TokenCredential credential) { + var token = await credential.GetTokenAsync( + new TokenRequestContext(new[] { "https://management.azure.com/.default" }), + default); + + var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler(); + var jsonToken = handler.ReadToken(token.Token) as System.IdentityModel.Tokens.Jwt.JwtSecurityToken; + + string tid = jsonToken?.Claims.FirstOrDefault(c => c.Type == "tid")?.Value; + + if (string.IsNullOrEmpty(tid)) + { + throw new InvalidOperationException("Could not determine the tenant ID."); + } + + return tid; + } + + /// + /// Deletes all created resources + /// + private async Task CleanupResources() + { + // First delete Key Vault + if (_keyVault != null) + { + try + { + Console.WriteLine($"Cleaning up test Key Vault: {_testKeyVaultName}"); + await _keyVault.DeleteAsync(WaitUntil.Completed); + _keyVault = null; + Console.WriteLine("Key Vault cleanup completed successfully"); + } + catch (Exception ex) when ( + ex is RequestFailedException || + ex is InvalidOperationException || + ex is TaskCanceledException) + { + Console.WriteLine($"Key Vault cleanup failed: {ex.Message}."); + } + } + + // Then delete App Configuration store if (_appConfigStore != null) { try @@ -359,11 +543,11 @@ ex is InvalidOperationException || public async Task DisposeAsync() { - await CleanupAppConfigurationStore(); + await CleanupResources(); try { - await CleanupStaleStores(); + await CleanupStaleResources(); } catch (Exception ex) when ( ex is RequestFailedException || @@ -371,7 +555,7 @@ ex is InvalidOperationException || ex is TaskCanceledException || ex is UnauthorizedAccessException) { - Console.WriteLine($"Error during stale store cleanup: {ex.Message}"); + Console.WriteLine($"Error during stale resource cleanup: {ex.Message}"); } } @@ -392,6 +576,8 @@ private async Task SetupTestKeys(string testName) string keyPrefix = GetUniqueKeyPrefix(testName); string sentinelKey = $"{keyPrefix}:Sentinel"; string featureFlagKey = $".appconfig.featureflag/{keyPrefix}Feature"; + string secretName = $"{keyPrefix}-secret"; + string keyVaultReferenceKey = $"{keyPrefix}:KeyVaultRef"; // Create test-specific settings var testSettings = new List @@ -411,11 +597,38 @@ private async Task SetupTestKeys(string testName) await _configClient.SetConfigurationSettingAsync(setting); } + // If Key Vault is available, add a test secret and reference + if (_secretClient != null) + { + try + { + // Create a secret in Key Vault + await _secretClient.SetSecretAsync(secretName, "SecretValue"); + + // Create a Key Vault reference in App Configuration + string keyVaultUri = $"{_keyVaultEndpoint}secrets/{secretName}"; + string keyVaultRefValue = @$"{{""uri"":""{keyVaultUri}""}}"; + + await _configClient.SetConfigurationSettingAsync( + ConfigurationModelFactory.ConfigurationSetting( + keyVaultReferenceKey, + keyVaultRefValue, + contentType: KeyVaultConstants.ContentType)); + } + catch (Exception ex) + { + Console.WriteLine($"Error setting up Key Vault reference: {ex.Message}"); + // Continue without Key Vault reference if it fails + } + } + return new TestContext { KeyPrefix = keyPrefix, SentinelKey = sentinelKey, - FeatureFlagKey = featureFlagKey + FeatureFlagKey = featureFlagKey, + KeyVaultReferenceKey = keyVaultReferenceKey, + SecretName = secretName }; } @@ -1549,5 +1762,92 @@ await _configClient.SetConfigurationSettingAsync( await _configClient.ArchiveSnapshotAsync(snapshot3); } } + + /// + /// Test verifies Key Vault references can be resolved + /// + [Fact] + public async Task KeyVaultReferences_ResolveCorrectly() + { + // Skip if Key Vault is not available + if (_keyVault == null || _secretClient == null) + { + Console.WriteLine("Skipping Key Vault test - Key Vault is not available"); + return; + } + + // Arrange - Setup test-specific keys + var testContext = await SetupTestKeys("KeyVaultReference"); + + // Act - Create configuration with Key Vault support + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{testContext.KeyPrefix}:*"); + options.ConfigureKeyVault(kv => kv.SetCredential(GetCredential())); + }) + .Build(); + + // Assert - Key Vault reference should be resolved to the secret value + Assert.Equal("SecretValue", config[testContext.KeyVaultReferenceKey]); + } + + /// + /// Test verifies Key Vault references refresh correctly + /// + [Fact] + public async Task KeyVaultReferences_RefreshCorrectly() + { + // Skip if Key Vault is not available + if (_keyVault == null || _secretClient == null) + { + Console.WriteLine("Skipping Key Vault test - Key Vault is not available"); + return; + } + + // Arrange - Setup test-specific keys + var testContext = await SetupTestKeys("KeyVaultRefresh"); + IConfigurationRefresher refresher = null; + + // Create configuration with Key Vault support and refresh + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{testContext.KeyPrefix}:*"); + options.ConfigureKeyVault(kv => + { + kv.SetCredential(GetCredential()); + kv.SetSecretRefreshInterval(TimeSpan.FromSeconds(1)); + }); + options.ConfigureRefresh(refresh => + { + refresh.Register(testContext.SentinelKey, refreshAll: true) + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + + refresher = options.GetRefresher(); + }) + .Build(); + + // Verify initial value + Assert.Equal("SecretValue", config[testContext.KeyVaultReferenceKey]); + + // Act - Update the secret in Key Vault + await _secretClient.SetSecretAsync(testContext.SecretName, "UpdatedSecretValue"); + + // Update the sentinel key to trigger refresh + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(testContext.SentinelKey, "Updated")); + + // Wait for cache to expire + await Task.Delay(TimeSpan.FromSeconds(2)); + + // Refresh + await refresher.RefreshAsync(); + + // Assert - Key Vault reference should be updated with the new secret value + Assert.Equal("UpdatedSecretValue", config[testContext.KeyVaultReferenceKey]); + } } } diff --git a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj index f0db1817a..5d6113405 100644 --- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj +++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj @@ -12,8 +12,9 @@ - + + From db881d5d3620b166929c7cc52d66baba1df3c737 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 31 Mar 2025 13:28:07 -0700 Subject: [PATCH 25/57] add more keyvault tests --- .../Integration/IntegrationTests.cs | 247 +++++++++++++++++- 1 file changed, 245 insertions(+), 2 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index bc1202721..3c088a337 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -1,5 +1,6 @@ using Azure; using Azure.Core; +using Azure.Core.Pipeline; using Azure.Data.AppConfiguration; using Azure.Identity; using Azure.ResourceManager; @@ -18,6 +19,7 @@ using System.IO; using System.Linq; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -56,6 +58,7 @@ private class TestContext public string FeatureFlagKey { get; set; } public string KeyVaultReferenceKey { get; set; } public string SecretName { get; set; } + public string SecretValue { get; set; } } // Client for direct manipulation of the store @@ -577,6 +580,7 @@ private async Task SetupTestKeys(string testName) string sentinelKey = $"{keyPrefix}:Sentinel"; string featureFlagKey = $".appconfig.featureflag/{keyPrefix}Feature"; string secretName = $"{keyPrefix}-secret"; + string secretValue = "SecretValue"; string keyVaultReferenceKey = $"{keyPrefix}:KeyVaultRef"; // Create test-specific settings @@ -603,7 +607,7 @@ private async Task SetupTestKeys(string testName) try { // Create a secret in Key Vault - await _secretClient.SetSecretAsync(secretName, "SecretValue"); + await _secretClient.SetSecretAsync(secretName, secretValue); // Create a Key Vault reference in App Configuration string keyVaultUri = $"{_keyVaultEndpoint}secrets/{secretName}"; @@ -628,7 +632,8 @@ await _configClient.SetConfigurationSettingAsync( SentinelKey = sentinelKey, FeatureFlagKey = featureFlagKey, KeyVaultReferenceKey = keyVaultReferenceKey, - SecretName = secretName + SecretName = secretName, + SecretValue = secretValue }; } @@ -1849,5 +1854,243 @@ public async Task KeyVaultReferences_RefreshCorrectly() // Assert - Key Vault reference should be updated with the new secret value Assert.Equal("UpdatedSecretValue", config[testContext.KeyVaultReferenceKey]); } + + /// + /// Helper class to monitor Key Vault requests + /// + private class HttpPipelineTransportWithRequestCount : HttpPipelineTransport + { + private readonly HttpClientTransport _innerTransport = new HttpClientTransport(); + private readonly Action _onRequest; + + public HttpPipelineTransportWithRequestCount(Action onRequest) + { + _onRequest = onRequest; + } + + public override Request CreateRequest() + { + return _innerTransport.CreateRequest(); + } + + public override void Process(HttpMessage message) + { + _onRequest(); + _innerTransport.Process(message); + } + + public override ValueTask ProcessAsync(HttpMessage message) + { + _onRequest(); + return _innerTransport.ProcessAsync(message); + } + } + + /// + /// Tests that Key Vault secrets are properly cached to avoid unnecessary requests. + /// + [Fact] + public async Task KeyVaultReference_UsesCache_DoesNotCallKeyVaultAgain() + { + // Skip if Key Vault is not available + if (_keyVault == null || _secretClient == null) + { + Console.WriteLine("Skipping Key Vault test - Key Vault is not available"); + return; + } + + // Arrange - Setup test-specific keys + var testContext = await SetupTestKeys("KeyVaultCacheTest"); + + // Create a monitoring client to track calls to Key Vault + int requestCount = 0; + var testSecretClient = new SecretClient( + _keyVaultEndpoint, + GetCredential(), + new SecretClientOptions + { + Transport = new HttpPipelineTransportWithRequestCount(() => requestCount++) + }); + + // Act + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{testContext.KeyPrefix}:*"); + options.ConfigureKeyVault(kv => + { + kv.Register(testSecretClient); + }); + }) + .Build(); + + // First access should resolve from Key Vault + var firstValue = config[testContext.KeyVaultReferenceKey]; + int firstRequestCount = requestCount; + + // Second access should use the cache + var secondValue = config[testContext.KeyVaultReferenceKey]; + int secondRequestCount = requestCount; + + // Assert + Assert.Equal(testContext.SecretValue, firstValue); + Assert.Equal(testContext.SecretValue, secondValue); + Assert.Equal(1, firstRequestCount); // Should make exactly one request + Assert.Equal(firstRequestCount, secondRequestCount); // No additional requests for the second access + } + + /// + /// Tests that Key Vault secrets are properly refreshed when the refresh feature is enabled. + /// + [Fact] + public async Task KeyVaultReference_Refresh_WhenSecretChanges() + { + // Skip if Key Vault is not available + if (_keyVault == null || _secretClient == null) + { + Console.WriteLine("Skipping Key Vault test - Key Vault is not available"); + return; + } + + // Arrange - Setup test-specific keys + var testContext = await SetupTestKeys("KeyVaultRefreshTest"); + IConfigurationRefresher refresher = null; + + // Act - Create configuration with refresh capability + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{testContext.KeyPrefix}:*"); + options.ConfigureKeyVault(kv => + { + kv.SetCredential(GetCredential()); + // Set a short refresh interval to make testing faster + kv.SetSecretRefreshInterval(TimeSpan.FromSeconds(1)); + }); + options.ConfigureRefresh(refresh => + { + refresh.Register(testContext.SentinelKey, refreshAll: true) + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + + refresher = options.GetRefresher(); + }) + .Build(); + + // Verify initial value + Assert.Equal(testContext.SecretValue, config[testContext.KeyVaultReferenceKey]); + + // Change the secret in Key Vault + string updatedSecretValue = $"UpdatedSecretValue-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; + await _secretClient.SetSecretAsync(testContext.SecretName, updatedSecretValue); + + // Update the sentinel key to trigger a refresh + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting(testContext.SentinelKey, "Updated")); + + // Wait for caches to expire + await Task.Delay(TimeSpan.FromSeconds(2)); + + // Refresh the configuration + await refresher.RefreshAsync(); + + // Assert - Should have the updated secret value + Assert.Equal(updatedSecretValue, config[testContext.KeyVaultReferenceKey]); + } + + /// + /// Tests that different Key Vault references can have different refresh intervals. + /// + [Fact] + public async Task KeyVaultReference_DifferentRefreshIntervals() + { + // Skip if Key Vault is not available + if (_keyVault == null || _secretClient == null) + { + Console.WriteLine("Skipping Key Vault test - Key Vault is not available"); + return; + } + + // Arrange - Setup test-specific keys + var testContext = await SetupTestKeys("KeyVaultDifferentIntervals"); + IConfigurationRefresher refresher = null; + + // Create a secret in Key Vault with short refresh interval + string secretName1 = $"test-secret1-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; + string secretValue1 = $"SecretValue1-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; + await _secretClient.SetSecretAsync(secretName1, secretValue1); + + // Create another secret in Key Vault with long refresh interval + string secretName2 = $"test-secret2-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; + string secretValue2 = $"SecretValue2-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; + await _secretClient.SetSecretAsync(secretName2, secretValue2); + + // Create Key Vault references in App Configuration + string keyVaultUri = _keyVaultEndpoint.ToString().TrimEnd('/'); + string kvRefKey1 = $"{testContext.KeyPrefix}:KeyVaultRef1"; + string kvRefKey2 = $"{testContext.KeyPrefix}:KeyVaultRef2"; + + await _configClient.SetConfigurationSettingAsync( + ConfigurationModelFactory.ConfigurationSetting( + kvRefKey1, + $@"{{""uri"":""{keyVaultUri}/secrets/{secretName1}""}}", + contentType: KeyVaultConstants.ContentType + "; charset=utf-8")); + + await _configClient.SetConfigurationSettingAsync( + ConfigurationModelFactory.ConfigurationSetting( + kvRefKey2, + $@"{{""uri"":""{keyVaultUri}/secrets/{secretName2}""}}", + contentType: KeyVaultConstants.ContentType + "; charset=utf-8")); + + // Act - Create configuration with different refresh intervals + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{testContext.KeyPrefix}:*"); + options.ConfigureKeyVault(kv => + { + kv.SetCredential(GetCredential()); + // Set different refresh intervals for each secret + kv.SetSecretRefreshInterval(kvRefKey1, TimeSpan.FromSeconds(1)); // Short interval + kv.SetSecretRefreshInterval(kvRefKey2, TimeSpan.FromDays(1)); // Long interval + }); + options.ConfigureRefresh(refresh => + { + refresh.Register(testContext.SentinelKey, refreshAll: true) + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + + refresher = options.GetRefresher(); + }) + .Build(); + + // Verify initial values + Assert.Equal(secretValue1, config[kvRefKey1]); + Assert.Equal(secretValue2, config[kvRefKey2]); + + // Update both secrets in Key Vault + string updatedValue1 = $"UpdatedValue1-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; + string updatedValue2 = $"UpdatedValue2-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; + + await _secretClient.SetSecretAsync(secretName1, updatedValue1); + await _secretClient.SetSecretAsync(secretName2, updatedValue2); + + // Update the sentinel key to trigger refresh + await _configClient.SetConfigurationSettingAsync( + new ConfigurationSetting(testContext.SentinelKey, "Updated")); + + // Wait for the short interval cache to expire + await Task.Delay(TimeSpan.FromSeconds(2)); + + // Refresh the configuration + await refresher.RefreshAsync(); + + // Assert - Only the first secret should be refreshed due to having a short interval + Assert.Equal(updatedValue1, config[kvRefKey1]); // Updated - short refresh interval + Assert.Equal(secretValue2, config[kvRefKey2]); // Not updated - long refresh interval + } } } From ff5596a95f93eb7d80d74a9f46aaf903c82d25dc Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 2 Apr 2025 15:00:09 -0700 Subject: [PATCH 26/57] fix keyvault tests --- .../Integration/IntegrationTests.cs | 597 +++--------------- 1 file changed, 80 insertions(+), 517 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 3c088a337..64feaed3e 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -5,9 +5,7 @@ using Azure.Identity; using Azure.ResourceManager; using Azure.ResourceManager.AppConfiguration; -using Azure.ResourceManager.AppConfiguration.Models; using Azure.ResourceManager.KeyVault; -using Azure.ResourceManager.KeyVault.Models; using Azure.ResourceManager.Resources; using Azure.Security.KeyVault.Secrets; using Microsoft.Extensions.Configuration; @@ -27,7 +25,7 @@ namespace Tests.AzureAppConfiguration { /// /// Integration tests for Azure App Configuration that connect to a real service. - /// Creates a temporary App Configuration store for testing and deletes it after the tests are complete. + /// Uses an existing App Configuration store and Key Vault for testing. /// Requires Azure credentials with appropriate permissions. /// NOTE: Before running these tests, execute the GetAzureSubscription.ps1 script to create appsettings.Secrets.json. /// @@ -37,16 +35,12 @@ public class IntegrationTests : IAsyncLifetime { // Test constants private const string TestKeyPrefix = "IntegrationTest"; - private const string DefaultLocation = "swedensouth"; private const string SubscriptionJsonPath = "appsettings.Secrets.json"; - // Resource tagging and cleanup constants - private const string ResourceGroupNamePrefix = "appconfig-dotnetprovider-test-"; - private const string StoreNamePrefix = "integration-"; - private const string KeyVaultNamePrefix = "kv-int-"; - private const string TestResourceTag = "TestResource"; - private const string CreatedByTag = "CreatedBy"; - private const int StaleResourceThresholdHours = 3; // Resources older than this are considered stale + // Fixed resource names - already existing + private const string AppConfigStoreName = "appconfig-dotnetprovider-integrationtest"; + private const string KeyVaultName = "keyvault-dotnetprovider"; + private const string ResourceGroupName = "dotnetprovider-integrationtest"; /// /// Class to hold test-specific key information @@ -70,17 +64,9 @@ private class TestContext // Connection string for the store private string _connectionString; - // Store management resources - private ArmClient _armClient; - private string _testStoreName; - private string _testKeyVaultName; - private string _testResourceGroupName; - private AppConfigurationStoreResource _appConfigStore; - private KeyVaultResource _keyVault; + // Endpoints for the resources private Uri _appConfigEndpoint; private Uri _keyVaultEndpoint; - private ResourceGroupResource _resourceGroup; - private string _subscriptionId; /// /// Gets a DefaultAzureCredential for authentication. @@ -131,28 +117,12 @@ private string GetConnectionString() } /// - /// Generate a timestamped, unique resource name with the given prefix - /// - private string GenerateTimestampedResourceName(string prefix) - { - // Format: prefix-yyyyMMddHHmm-randomGuid (trimmed to 20 chars) - string timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmm"); - string randomPart = Guid.NewGuid().ToString("N").Substring(0, 8); - return $"{prefix}{timestamp}-{randomPart}"; - } - - /// - /// Generates a deterministic resource group name based on machine information + /// Creates a unique prefix for test keys to ensure test isolation /// - private string GetDeterministicResourceGroupName() + private string GetUniqueKeyPrefix(string testName) { - string machineName = Environment.MachineName.ToLowerInvariant(); - string machineHash = Convert.ToBase64String( - System.Security.Cryptography.SHA256.Create() - .ComputeHash(System.Text.Encoding.UTF8.GetBytes(machineName))) - .Replace("/", "-").Replace("+", "-").Replace("=", "").Substring(0, 8); - - return $"{ResourceGroupNamePrefix}{machineHash}"; + // Use a combination of the test prefix and test method name to ensure uniqueness + return $"{TestKeyPrefix}-{testName}-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; } /// @@ -177,7 +147,7 @@ private async Task CreateSnapshot(string snapshotName, TestContext testC return operation.Value.Name; } - catch (Exception ex) + catch (RequestFailedException ex) { Console.WriteLine($"Error creating snapshot: {ex.Message}"); throw; @@ -185,196 +155,38 @@ private async Task CreateSnapshot(string snapshotName, TestContext testC } /// - /// Cleans up only stale App Configuration stores and Key Vaults, not resource groups - /// - private async Task CleanupStaleResources() - { - if (_resourceGroup == null) return; - - try - { - // Clean up stale App Configuration stores - var stores = _resourceGroup.GetAppConfigurationStores(); - var staleTime = DateTime.UtcNow.AddHours(-StaleResourceThresholdHours); - - await foreach (var store in stores.GetAllAsync()) - { - // Only delete stores that: - // 1. Start with our test prefix - // 2. Have the TestResourceTag - // 3. Are older than the threshold - if (!store.Data.Name.StartsWith(StoreNamePrefix) || - !store.Data.Tags.ContainsKey(TestResourceTag)) - { - continue; - } - - // Check if the store is a temporary test store - if (store.Data.Tags.ContainsKey("TemporaryStore") && - store.Data.Tags["TemporaryStore"] == "true") - { - // If it has a creation time tag, check if it's stale - if (store.Data.Tags.TryGetValue("CreatedOn", out string createdOnStr) && - DateTime.TryParse(createdOnStr, out DateTime createdOn)) - { - if (createdOn < staleTime) - { - await store.DeleteAsync(WaitUntil.Started); - } - } - else - { - // If no creation time or it can't be parsed, use a heuristic - // based on the timestamp in the name - string name = store.Data.Name; - if (name.Length > StoreNamePrefix.Length + 12) // yyyyMMddHHmm format is 12 chars - { - string timeStampPart = name.Substring(StoreNamePrefix.Length, 12); - if (DateTime.TryParseExact(timeStampPart, "yyyyMMddHHmm", - System.Globalization.CultureInfo.InvariantCulture, - System.Globalization.DateTimeStyles.None, out DateTime timestamp)) - { - if (timestamp < staleTime) - { - await store.DeleteAsync(WaitUntil.Started); - } - } - } - } - } - } - - // Clean up stale Key Vaults - var keyVaults = _resourceGroup.GetKeyVaults(); - - await foreach (var vault in keyVaults.GetAllAsync()) - { - // Only delete key vaults that: - // 1. Start with our test prefix - // 2. Have the TestResourceTag - // 3. Are older than the threshold - if (!vault.Data.Name.StartsWith(KeyVaultNamePrefix) || - !vault.Data.Tags.ContainsKey(TestResourceTag)) - { - continue; - } - - // Check if the key vault is a temporary test resource - if (vault.Data.Tags.ContainsKey("TemporaryStore") && - vault.Data.Tags["TemporaryStore"] == "true") - { - // If it has a creation time tag, check if it's stale - if (vault.Data.Tags.TryGetValue("CreatedOn", out string createdOnStr) && - DateTime.TryParse(createdOnStr, out DateTime createdOn)) - { - if (createdOn < staleTime) - { - await vault.DeleteAsync(WaitUntil.Started); - } - } - else - { - // If no creation time or it can't be parsed, use a heuristic - // based on the timestamp in the name - string name = vault.Data.Name; - if (name.Length > KeyVaultNamePrefix.Length + 12) // yyyyMMddHHmm format is 12 chars - { - string timeStampPart = name.Substring(KeyVaultNamePrefix.Length, 12); - if (DateTime.TryParseExact(timeStampPart, "yyyyMMddHHmm", - System.Globalization.CultureInfo.InvariantCulture, - System.Globalization.DateTimeStyles.None, out DateTime timestamp)) - { - if (timestamp < staleTime) - { - await vault.DeleteAsync(WaitUntil.Started); - } - } - } - } - } - } - } - catch (Exception ex) - { - Console.WriteLine($"Error during stale resource cleanup: {ex.Message}"); - } - } - - /// - /// Creates a temporary Azure App Configuration store and Key Vault, then adds test data. + /// Initialize clients to connect to the existing App Configuration store and Key Vault. /// public async Task InitializeAsync() { - bool success = false; - try { var credential = GetCredential(); - - // Get the current subscription ID from the JSON file - _subscriptionId = GetCurrentSubscriptionId(); + string subscriptionId = GetCurrentSubscriptionId(); // Initialize Azure Resource Manager client - _armClient = new ArmClient(credential); + var armClient = new ArmClient(credential); + SubscriptionResource subscription = armClient.GetSubscriptionResource(new ResourceIdentifier($"/subscriptions/{subscriptionId}")); - _testResourceGroupName = GetDeterministicResourceGroupName(); + ResourceGroupResource resourceGroup = await subscription.GetResourceGroups().GetAsync(ResourceGroupName); - SubscriptionResource subscription = _armClient.GetSubscriptions().Get(_subscriptionId); + AppConfigurationStoreResource appConfigStore = null; + KeyVaultResource keyVault = null; - // Check if the resource group already exists - bool resourceGroupExists = false; try { - _resourceGroup = subscription.GetResourceGroup(_testResourceGroupName); - // If we get here, the resource group exists - resourceGroupExists = true; - } - catch (RequestFailedException ex) when (ex.Status == 404) - { - // Resource group doesn't exist, we'll create it - resourceGroupExists = false; + // Get App Configuration store directly using the resource group and store name + appConfigStore = await resourceGroup.GetAppConfigurationStores().GetAsync(AppConfigStoreName); } - - // Create the resource group if it doesn't exist - if (!resourceGroupExists) + catch (RequestFailedException ex) { - var rgData = new ResourceGroupData(new AzureLocation(DefaultLocation)); - - // Add tags to identify this as a persistent test resource group - rgData.Tags.Add("PersistentTestResource", "true"); - rgData.Tags.Add(CreatedByTag, "IntegrationTests"); - rgData.Tags.Add("CreatedOn", DateTime.UtcNow.ToString("o")); - - var rgLro = await subscription.GetResourceGroups().CreateOrUpdateAsync( - WaitUntil.Completed, _testResourceGroupName, rgData); - _resourceGroup = rgLro.Value; + throw new InvalidOperationException($"App Configuration store '{AppConfigStoreName}' not found in resource group '{ResourceGroupName}'. Please create it before running tests.", ex); } - // Clean up any stale resources before creating new ones - await CleanupStaleResources(); - - // Create unique store name for this test run with timestamp - _testStoreName = GenerateTimestampedResourceName(StoreNamePrefix); - _testKeyVaultName = GenerateTimestampedResourceName(KeyVaultNamePrefix); + _appConfigEndpoint = new Uri(appConfigStore.Data.Endpoint); - // Create the App Configuration store - var storeData = new AppConfigurationStoreData(new AzureLocation(DefaultLocation), new AppConfigurationSku("free")); - - storeData.Tags.Add(TestResourceTag, "true"); - storeData.Tags.Add(CreatedByTag, "IntegrationTests"); - storeData.Tags.Add("TemporaryStore", "true"); - storeData.Tags.Add("CreatedOn", DateTime.UtcNow.ToString("o")); - - var createOperation = await _resourceGroup.GetAppConfigurationStores().CreateOrUpdateAsync( - WaitUntil.Completed, - _testStoreName, - storeData); - - _appConfigStore = createOperation.Value; - _appConfigEndpoint = new Uri(_appConfigStore.Data.Endpoint); - - // Get the connection string for the store - var accessKeys = _appConfigStore.GetKeysAsync(); + // Get connection string from the store + var accessKeys = appConfigStore.GetKeysAsync(); var primaryKey = await accessKeys.FirstOrDefaultAsync(); if (primaryKey == null) @@ -387,190 +199,78 @@ public async Task InitializeAsync() // Initialize the configuration client with the connection string _configClient = new ConfigurationClient(_connectionString); - // Create the Key Vault - try - { - // Create an access policy for the current user - var userObjectId = await GetCurrentUserObjectId(credential); - - // Define vault properties - var vaultProperties = new KeyVaultProperties( - new Guid(await GetTenantId(credential)), - new KeyVaultSku(KeyVaultSkuFamily.A, KeyVaultSkuName.Standard)); - - //vaultProperties.AccessPolicies = { - // new KeyVaultAccessPolicy - // { - // ObjectId = userObjectId, - // TenantId = new Guid(await GetTenantId(credential)), - // Permissions = new KeyVaultPermissions - // { - // Secrets = { - // KeyVaultSecretPermission.Get, - // KeyVaultSecretPermission.List, - // KeyVaultSecretPermission.Set, - // KeyVaultSecretPermission.Delete - // } - // } - // } - //}, - - // Create Key Vault resource data - var vaultData = new KeyVaultCreateOrUpdateContent(new AzureLocation(DefaultLocation), vaultProperties); - - // Add tags - vaultData.Tags.Add(TestResourceTag, "true"); - vaultData.Tags.Add(CreatedByTag, "IntegrationTests"); - vaultData.Tags.Add("TemporaryStore", "true"); - vaultData.Tags.Add("CreatedOn", DateTime.UtcNow.ToString("o")); - - // Create the vault - var vaultCreateOperation = await _resourceGroup.GetKeyVaults().CreateOrUpdateAsync( - WaitUntil.Completed, - _testKeyVaultName, - vaultData); - - _keyVault = vaultCreateOperation.Value; - _keyVaultEndpoint = _keyVault.Data.Properties.VaultUri; - - // Create a Secret Client for the vault - _secretClient = new SecretClient(_keyVaultEndpoint, credential); - } - catch (Exception ex) - { - Console.WriteLine($"Error creating Key Vault: {ex.Message}"); - // We'll continue without Key Vault if it fails - } + // Find and initialize Key Vault - look in the same resource group + keyVault = await resourceGroup.GetKeyVaults().GetAsync(KeyVaultName); - success = true; - } - finally - { - if (!success) + if (keyVault == null) { - await CleanupResources(); + throw new InvalidOperationException($"Key Vault '{KeyVaultName}' not found in subscription {subscriptionId}. Please create it before running tests."); } - } - } - /// - /// Get the current user's Object ID - /// - private async Task GetCurrentUserObjectId(TokenCredential credential) - { - // Use the Microsoft Graph API to get the current user's object ID - var token = await credential.GetTokenAsync( - new TokenRequestContext(new[] { "https://graph.microsoft.com/.default" }), - default); - - // Parse the token to get user information - var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler(); - var jsonToken = handler.ReadToken(token.Token) as System.IdentityModel.Tokens.Jwt.JwtSecurityToken; + _keyVaultEndpoint = keyVault.Data.Properties.VaultUri; - // Read the object id (oid) claim - string oid = jsonToken?.Claims.FirstOrDefault(c => c.Type == "oid")?.Value; + // Create a Secret Client for the vault + _secretClient = new SecretClient(_keyVaultEndpoint, credential); - if (string.IsNullOrEmpty(oid)) + Console.WriteLine($"Successfully connected to App Configuration store '{AppConfigStoreName}' and Key Vault '{KeyVaultName}'"); + } + catch (RequestFailedException ex) { - throw new InvalidOperationException("Could not determine the current user's object ID."); + Console.WriteLine($"Azure request failed: {ex.Message}. Status code: {ex.Status}"); + throw; } - - return new Guid(oid); - } - - /// - /// Get the tenant ID - /// - private async Task GetTenantId(TokenCredential credential) - { - var token = await credential.GetTokenAsync( - new TokenRequestContext(new[] { "https://management.azure.com/.default" }), - default); - - var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler(); - var jsonToken = handler.ReadToken(token.Token) as System.IdentityModel.Tokens.Jwt.JwtSecurityToken; - - string tid = jsonToken?.Claims.FirstOrDefault(c => c.Type == "tid")?.Value; - - if (string.IsNullOrEmpty(tid)) + catch (ArgumentException ex) { - throw new InvalidOperationException("Could not determine the tenant ID."); + Console.WriteLine($"Invalid argument: {ex.Message}"); + throw; + } + catch (InvalidOperationException ex) + { + // This is already a specific exception, so just rethrow + Console.WriteLine($"Invalid operation: {ex.Message}"); + throw; } - - return tid; } /// - /// Deletes all created resources + /// Clean up test artifacts but don't delete the actual resources /// - private async Task CleanupResources() + public async Task DisposeAsync() { - // First delete Key Vault - if (_keyVault != null) + try { - try - { - Console.WriteLine($"Cleaning up test Key Vault: {_testKeyVaultName}"); - await _keyVault.DeleteAsync(WaitUntil.Completed); - _keyVault = null; - Console.WriteLine("Key Vault cleanup completed successfully"); - } - catch (Exception ex) when ( - ex is RequestFailedException || - ex is InvalidOperationException || - ex is TaskCanceledException) - { - Console.WriteLine($"Key Vault cleanup failed: {ex.Message}."); - } - } + // Clean up any test-specific snapshots + var snapshots = _configClient.GetSnapshotsAsync(new SnapshotSelector()); - // Then delete App Configuration store - if (_appConfigStore != null) - { - try - { - Console.WriteLine($"Cleaning up test store: {_testStoreName}"); - await _appConfigStore.DeleteAsync(WaitUntil.Completed); - _appConfigStore = null; - Console.WriteLine("App Configuration store cleanup completed successfully"); - } - catch (Exception ex) when ( - ex is RequestFailedException || - ex is InvalidOperationException || - ex is TaskCanceledException) + await foreach (var snapshot in snapshots) { - Console.WriteLine($"Store cleanup failed: {ex.Message}."); + // Only delete snapshots that start with our test prefix + if (snapshot.Name.StartsWith("snapshot-")) + { + try + { + await _configClient.ArchiveSnapshotAsync(snapshot.Name); + } + catch (RequestFailedException ex) + { + Console.WriteLine($"Failed to delete snapshot {snapshot.Name}: {ex.Message}"); + } + } } - } - } - public async Task DisposeAsync() - { - await CleanupResources(); - - try + // Clean up test-specific secrets in Key Vault + // We could implement this if needed, but be careful not to delete non-test secrets + } + catch (RequestFailedException ex) { - await CleanupStaleResources(); + Console.WriteLine($"Error during snapshot enumeration: {ex.Message}"); } - catch (Exception ex) when ( - ex is RequestFailedException || - ex is InvalidOperationException || - ex is TaskCanceledException || - ex is UnauthorizedAccessException) + catch (InvalidOperationException ex) { - Console.WriteLine($"Error during stale resource cleanup: {ex.Message}"); + Console.WriteLine($"Operation error during test cleanup: {ex.Message}"); } } - /// - /// 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 /// @@ -619,9 +319,14 @@ await _configClient.SetConfigurationSettingAsync( keyVaultRefValue, contentType: KeyVaultConstants.ContentType)); } - catch (Exception ex) + catch (RequestFailedException ex) + { + Console.WriteLine($"Error setting up Key Vault secret: {ex.Message}"); + // Continue without Key Vault reference if it fails + } + catch (InvalidOperationException ex) { - Console.WriteLine($"Error setting up Key Vault reference: {ex.Message}"); + Console.WriteLine($"Invalid Key Vault operation: {ex.Message}"); // Continue without Key Vault reference if it fails } } @@ -1774,13 +1479,6 @@ await _configClient.SetConfigurationSettingAsync( [Fact] public async Task KeyVaultReferences_ResolveCorrectly() { - // Skip if Key Vault is not available - if (_keyVault == null || _secretClient == null) - { - Console.WriteLine("Skipping Key Vault test - Key Vault is not available"); - return; - } - // Arrange - Setup test-specific keys var testContext = await SetupTestKeys("KeyVaultReference"); @@ -1798,63 +1496,6 @@ public async Task KeyVaultReferences_ResolveCorrectly() Assert.Equal("SecretValue", config[testContext.KeyVaultReferenceKey]); } - /// - /// Test verifies Key Vault references refresh correctly - /// - [Fact] - public async Task KeyVaultReferences_RefreshCorrectly() - { - // Skip if Key Vault is not available - if (_keyVault == null || _secretClient == null) - { - Console.WriteLine("Skipping Key Vault test - Key Vault is not available"); - return; - } - - // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("KeyVaultRefresh"); - IConfigurationRefresher refresher = null; - - // Create configuration with Key Vault support and refresh - var config = new ConfigurationBuilder() - .AddAzureAppConfiguration(options => - { - options.Connect(GetConnectionString()); - options.Select($"{testContext.KeyPrefix}:*"); - options.ConfigureKeyVault(kv => - { - kv.SetCredential(GetCredential()); - kv.SetSecretRefreshInterval(TimeSpan.FromSeconds(1)); - }); - options.ConfigureRefresh(refresh => - { - refresh.Register(testContext.SentinelKey, refreshAll: true) - .SetRefreshInterval(TimeSpan.FromSeconds(1)); - }); - - refresher = options.GetRefresher(); - }) - .Build(); - - // Verify initial value - Assert.Equal("SecretValue", config[testContext.KeyVaultReferenceKey]); - - // Act - Update the secret in Key Vault - await _secretClient.SetSecretAsync(testContext.SecretName, "UpdatedSecretValue"); - - // Update the sentinel key to trigger refresh - await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(testContext.SentinelKey, "Updated")); - - // Wait for cache to expire - await Task.Delay(TimeSpan.FromSeconds(2)); - - // Refresh - await refresher.RefreshAsync(); - - // Assert - Key Vault reference should be updated with the new secret value - Assert.Equal("UpdatedSecretValue", config[testContext.KeyVaultReferenceKey]); - } - /// /// Helper class to monitor Key Vault requests /// @@ -1892,13 +1533,6 @@ public override ValueTask ProcessAsync(HttpMessage message) [Fact] public async Task KeyVaultReference_UsesCache_DoesNotCallKeyVaultAgain() { - // Skip if Key Vault is not available - if (_keyVault == null || _secretClient == null) - { - Console.WriteLine("Skipping Key Vault test - Key Vault is not available"); - return; - } - // Arrange - Setup test-specific keys var testContext = await SetupTestKeys("KeyVaultCacheTest"); @@ -1940,79 +1574,12 @@ public async Task KeyVaultReference_UsesCache_DoesNotCallKeyVaultAgain() Assert.Equal(firstRequestCount, secondRequestCount); // No additional requests for the second access } - /// - /// Tests that Key Vault secrets are properly refreshed when the refresh feature is enabled. - /// - [Fact] - public async Task KeyVaultReference_Refresh_WhenSecretChanges() - { - // Skip if Key Vault is not available - if (_keyVault == null || _secretClient == null) - { - Console.WriteLine("Skipping Key Vault test - Key Vault is not available"); - return; - } - - // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("KeyVaultRefreshTest"); - IConfigurationRefresher refresher = null; - - // Act - Create configuration with refresh capability - var config = new ConfigurationBuilder() - .AddAzureAppConfiguration(options => - { - options.Connect(GetConnectionString()); - options.Select($"{testContext.KeyPrefix}:*"); - options.ConfigureKeyVault(kv => - { - kv.SetCredential(GetCredential()); - // Set a short refresh interval to make testing faster - kv.SetSecretRefreshInterval(TimeSpan.FromSeconds(1)); - }); - options.ConfigureRefresh(refresh => - { - refresh.Register(testContext.SentinelKey, refreshAll: true) - .SetRefreshInterval(TimeSpan.FromSeconds(1)); - }); - - refresher = options.GetRefresher(); - }) - .Build(); - - // Verify initial value - Assert.Equal(testContext.SecretValue, config[testContext.KeyVaultReferenceKey]); - - // Change the secret in Key Vault - string updatedSecretValue = $"UpdatedSecretValue-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; - await _secretClient.SetSecretAsync(testContext.SecretName, updatedSecretValue); - - // Update the sentinel key to trigger a refresh - await _configClient.SetConfigurationSettingAsync( - new ConfigurationSetting(testContext.SentinelKey, "Updated")); - - // Wait for caches to expire - await Task.Delay(TimeSpan.FromSeconds(2)); - - // Refresh the configuration - await refresher.RefreshAsync(); - - // Assert - Should have the updated secret value - Assert.Equal(updatedSecretValue, config[testContext.KeyVaultReferenceKey]); - } - /// /// Tests that different Key Vault references can have different refresh intervals. /// [Fact] public async Task KeyVaultReference_DifferentRefreshIntervals() { - // Skip if Key Vault is not available - if (_keyVault == null || _secretClient == null) - { - Console.WriteLine("Skipping Key Vault test - Key Vault is not available"); - return; - } - // Arrange - Setup test-specific keys var testContext = await SetupTestKeys("KeyVaultDifferentIntervals"); IConfigurationRefresher refresher = null; @@ -2054,7 +1621,7 @@ await _configClient.SetConfigurationSettingAsync( { kv.SetCredential(GetCredential()); // Set different refresh intervals for each secret - kv.SetSecretRefreshInterval(kvRefKey1, TimeSpan.FromSeconds(1)); // Short interval + kv.SetSecretRefreshInterval(kvRefKey1, TimeSpan.FromSeconds(60)); // Short interval kv.SetSecretRefreshInterval(kvRefKey2, TimeSpan.FromDays(1)); // Long interval }); options.ConfigureRefresh(refresh => @@ -2078,12 +1645,8 @@ await _configClient.SetConfigurationSettingAsync( await _secretClient.SetSecretAsync(secretName1, updatedValue1); await _secretClient.SetSecretAsync(secretName2, updatedValue2); - // Update the sentinel key to trigger refresh - await _configClient.SetConfigurationSettingAsync( - new ConfigurationSetting(testContext.SentinelKey, "Updated")); - // Wait for the short interval cache to expire - await Task.Delay(TimeSpan.FromSeconds(2)); + await Task.Delay(TimeSpan.FromSeconds(61)); // Refresh the configuration await refresher.RefreshAsync(); From 706ed81f0b30fa471a8d68c379030e9edf94bfb4 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 2 Apr 2025 15:13:52 -0700 Subject: [PATCH 27/57] add cleanup of key values/secrets/snapshot --- .../Integration/IntegrationTests.cs | 203 ++++++++++++++++-- 1 file changed, 187 insertions(+), 16 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 64feaed3e..5d2c195e6 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -17,7 +17,6 @@ using System.IO; using System.Linq; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; using Xunit; @@ -36,6 +35,7 @@ public class IntegrationTests : IAsyncLifetime // Test constants private const string TestKeyPrefix = "IntegrationTest"; private const string SubscriptionJsonPath = "appsettings.Secrets.json"; + private static readonly TimeSpan StaleResourceThreshold = TimeSpan.FromHours(3); // Fixed resource names - already existing private const string AppConfigStoreName = "appconfig-dotnetprovider-integrationtest"; @@ -68,6 +68,11 @@ private class TestContext private Uri _appConfigEndpoint; private Uri _keyVaultEndpoint; + // Track resources created by tests for cleanup + private readonly HashSet _createdConfigKeys = new HashSet(); + private readonly HashSet _createdSecretNames = new HashSet(); + private readonly HashSet _createdSnapshotNames = new HashSet(); + /// /// Gets a DefaultAzureCredential for authentication. /// @@ -145,6 +150,9 @@ private async Task CreateSnapshot(string snapshotName, TestContext testC // Create the snapshot CreateSnapshotOperation operation = await _configClient.CreateSnapshotAsync(WaitUntil.Completed, snapshotName, snapshot); + // Track created snapshot for cleanup + _createdSnapshotNames.Add(snapshotName); + return operation.Value.Name; } catch (RequestFailedException ex) @@ -213,6 +221,9 @@ public async Task InitializeAsync() _secretClient = new SecretClient(_keyVaultEndpoint, credential); Console.WriteLine($"Successfully connected to App Configuration store '{AppConfigStoreName}' and Key Vault '{KeyVaultName}'"); + + // Clean up stale resources on startup + await CleanupStaleResources(); } catch (RequestFailedException ex) { @@ -232,43 +243,169 @@ public async Task InitializeAsync() } } + /// + /// Cleans up stale resources that are older than the threshold + /// + private async Task CleanupStaleResources() + { + Console.WriteLine($"Checking for stale resources older than {StaleResourceThreshold}..."); + var cutoffTime = DateTimeOffset.UtcNow.Subtract(StaleResourceThreshold); + var cleanupTasks = new List(); + + try + { + // Clean up stale configuration settings + int staleConfigCount = 0; + var configSettingsToCleanup = new List(); + + // Get all test key-values + var configSettings = _configClient.GetConfigurationSettingsAsync(new SettingSelector + { + KeyFilter = TestKeyPrefix + "*" + }); + + await foreach (var setting in configSettings) + { + // Check if the setting is older than the threshold + if (setting.LastModified < cutoffTime) + { + configSettingsToCleanup.Add(setting); + staleConfigCount++; + } + } + + // Clean up stale feature flags + var featureFlagSettings = _configClient.GetConfigurationSettingsAsync(new SettingSelector + { + KeyFilter = ".appconfig.featureflag/" + TestKeyPrefix + "*" + }); + + await foreach (var setting in featureFlagSettings) + { + if (setting.LastModified < cutoffTime) + { + configSettingsToCleanup.Add(setting); + staleConfigCount++; + } + } + + // Delete stale configuration settings + foreach (var setting in configSettingsToCleanup) + { + cleanupTasks.Add(_configClient.DeleteConfigurationSettingAsync(setting.Key, setting.Label)); + } + + // Clean up stale snapshots + int staleSnapshotCount = 0; + var snapshots = _configClient.GetSnapshotsAsync(new SnapshotSelector()); + await foreach (var snapshot in snapshots) + { + if (snapshot.Name.StartsWith("snapshot-" + TestKeyPrefix) && snapshot.CreatedOn < cutoffTime) + { + cleanupTasks.Add(_configClient.ArchiveSnapshotAsync(snapshot.Name)); + staleSnapshotCount++; + } + } + + // Clean up stale Key Vault secrets + int staleSecretCount = 0; + if (_secretClient != null) + { + var secrets = _secretClient.GetPropertiesOfSecretsAsync(); + await foreach (var secretProperties in secrets) + { + if (secretProperties.Name.StartsWith(TestKeyPrefix) && secretProperties.CreatedOn.HasValue && secretProperties.CreatedOn.Value < cutoffTime) + { + cleanupTasks.Add(_secretClient.StartDeleteSecretAsync(secretProperties.Name)); + staleSecretCount++; + } + } + } + + // Wait for all cleanup tasks to complete + await Task.WhenAll(cleanupTasks); + Console.WriteLine($"Cleaned up {staleConfigCount} stale configuration settings, {staleSnapshotCount} snapshots, and {staleSecretCount} secrets"); + } + catch (RequestFailedException ex) + { + Console.WriteLine($"Error during stale resource cleanup: {ex.Message}"); + // Continue execution even if cleanup fails + } + catch (Exception ex) + { + Console.WriteLine($"Unexpected error during stale resource cleanup: {ex.Message}"); + // Continue execution even if cleanup fails + } + } + /// /// Clean up test artifacts but don't delete the actual resources /// public async Task DisposeAsync() { + var cleanupTasks = new List(); + try { - // Clean up any test-specific snapshots - var snapshots = _configClient.GetSnapshotsAsync(new SnapshotSelector()); + // Clean up all configuration settings created by tests + foreach (var key in _createdConfigKeys) + { + try + { + cleanupTasks.Add(_configClient.DeleteConfigurationSettingAsync(key)); + } + catch (RequestFailedException ex) + { + Console.WriteLine($"Failed to delete configuration setting {key}: {ex.Message}"); + } + } - await foreach (var snapshot in snapshots) + // Clean up all snapshots created by tests + foreach (var snapshotName in _createdSnapshotNames) + { + try + { + cleanupTasks.Add(_configClient.ArchiveSnapshotAsync(snapshotName)); + } + catch (RequestFailedException ex) + { + Console.WriteLine($"Failed to delete snapshot {snapshotName}: {ex.Message}"); + } + } + + // Clean up test-specific secrets in Key Vault + if (_secretClient != null) { - // Only delete snapshots that start with our test prefix - if (snapshot.Name.StartsWith("snapshot-")) + foreach (var secretName in _createdSecretNames) { try { - await _configClient.ArchiveSnapshotAsync(snapshot.Name); + cleanupTasks.Add(_secretClient.StartDeleteSecretAsync(secretName)); } catch (RequestFailedException ex) { - Console.WriteLine($"Failed to delete snapshot {snapshot.Name}: {ex.Message}"); + Console.WriteLine($"Failed to delete secret {secretName}: {ex.Message}"); } } } - // Clean up test-specific secrets in Key Vault - // We could implement this if needed, but be careful not to delete non-test secrets + // Wait for all cleanup tasks to complete + await Task.WhenAll(cleanupTasks); + + Console.WriteLine($"Cleaned up {_createdConfigKeys.Count} configuration settings, {_createdSnapshotNames.Count} snapshots, and {_createdSecretNames.Count} secrets"); } catch (RequestFailedException ex) { - Console.WriteLine($"Error during snapshot enumeration: {ex.Message}"); + Console.WriteLine($"Error during resource cleanup: {ex.Message}"); } catch (InvalidOperationException ex) { Console.WriteLine($"Operation error during test cleanup: {ex.Message}"); } + catch (Exception ex) + { + Console.WriteLine($"Unexpected error during test cleanup: {ex.ToString()}"); + } } /// @@ -299,6 +436,8 @@ private async Task SetupTestKeys(string testName) foreach (var setting in testSettings) { await _configClient.SetConfigurationSettingAsync(setting); + // Track the created key for cleanup + _createdConfigKeys.Add(setting.Key); } // If Key Vault is available, add a test secret and reference @@ -308,16 +447,21 @@ private async Task SetupTestKeys(string testName) { // Create a secret in Key Vault await _secretClient.SetSecretAsync(secretName, secretValue); + // Track the created secret for cleanup + _createdSecretNames.Add(secretName); // Create a Key Vault reference in App Configuration string keyVaultUri = $"{_keyVaultEndpoint}secrets/{secretName}"; string keyVaultRefValue = @$"{{""uri"":""{keyVaultUri}""}}"; - await _configClient.SetConfigurationSettingAsync( - ConfigurationModelFactory.ConfigurationSetting( - keyVaultReferenceKey, - keyVaultRefValue, - contentType: KeyVaultConstants.ContentType)); + var keyVaultRefSetting = ConfigurationModelFactory.ConfigurationSetting( + keyVaultReferenceKey, + keyVaultRefValue, + contentType: KeyVaultConstants.ContentType); + + await _configClient.SetConfigurationSettingAsync(keyVaultRefSetting); + // Track the created key reference for cleanup + _createdConfigKeys.Add(keyVaultReferenceKey); } catch (RequestFailedException ex) { @@ -342,6 +486,33 @@ await _configClient.SetConfigurationSettingAsync( }; } + // Helper method to track additional configuration keys created during tests + private void TrackConfigurationKey(string key) + { + if (!string.IsNullOrEmpty(key)) + { + _createdConfigKeys.Add(key); + } + } + + // Helper method to track additional secrets created during tests + private void TrackKeyVaultSecret(string secretName) + { + if (!string.IsNullOrEmpty(secretName)) + { + _createdSecretNames.Add(secretName); + } + } + + // Helper method to track additional snapshots created during tests + private void TrackSnapshot(string snapshotName) + { + if (!string.IsNullOrEmpty(snapshotName)) + { + _createdSnapshotNames.Add(snapshotName); + } + } + [Fact] public async Task LoadConfiguration_RetrievesValuesFromAppConfiguration() { From c4ce61498892b0e27211efc2f276cff209919e44 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 2 Apr 2025 15:48:37 -0700 Subject: [PATCH 28/57] fix key vault isolation --- .../Integration/IntegrationTests.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 5d2c195e6..0067d7aa6 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -36,6 +36,7 @@ public class IntegrationTests : IAsyncLifetime private const string TestKeyPrefix = "IntegrationTest"; private const string SubscriptionJsonPath = "appsettings.Secrets.json"; private static readonly TimeSpan StaleResourceThreshold = TimeSpan.FromHours(3); + private const string KeyVaultReferenceLabel = "KeyVaultRef"; // Fixed resource names - already existing private const string AppConfigStoreName = "appconfig-dotnetprovider-integrationtest"; @@ -457,6 +458,7 @@ private async Task SetupTestKeys(string testName) var keyVaultRefSetting = ConfigurationModelFactory.ConfigurationSetting( keyVaultReferenceKey, keyVaultRefValue, + label: KeyVaultReferenceLabel, contentType: KeyVaultConstants.ContentType); await _configClient.SetConfigurationSettingAsync(keyVaultRefSetting); @@ -1658,7 +1660,7 @@ public async Task KeyVaultReferences_ResolveCorrectly() .AddAzureAppConfiguration(options => { options.Connect(GetConnectionString()); - options.Select($"{testContext.KeyPrefix}:*"); + options.Select($"{testContext.KeyPrefix}:*", KeyVaultReferenceLabel); options.ConfigureKeyVault(kv => kv.SetCredential(GetCredential())); }) .Build(); @@ -1722,7 +1724,7 @@ public async Task KeyVaultReference_UsesCache_DoesNotCallKeyVaultAgain() .AddAzureAppConfiguration(options => { options.Connect(GetConnectionString()); - options.Select($"{testContext.KeyPrefix}:*"); + options.Select($"{testContext.KeyPrefix}:*", KeyVaultReferenceLabel); options.ConfigureKeyVault(kv => { kv.Register(testSecretClient); @@ -1774,12 +1776,14 @@ await _configClient.SetConfigurationSettingAsync( ConfigurationModelFactory.ConfigurationSetting( kvRefKey1, $@"{{""uri"":""{keyVaultUri}/secrets/{secretName1}""}}", + label: KeyVaultReferenceLabel, contentType: KeyVaultConstants.ContentType + "; charset=utf-8")); await _configClient.SetConfigurationSettingAsync( ConfigurationModelFactory.ConfigurationSetting( kvRefKey2, $@"{{""uri"":""{keyVaultUri}/secrets/{secretName2}""}}", + label: KeyVaultReferenceLabel, contentType: KeyVaultConstants.ContentType + "; charset=utf-8")); // Act - Create configuration with different refresh intervals @@ -1787,7 +1791,7 @@ await _configClient.SetConfigurationSettingAsync( .AddAzureAppConfiguration(options => { options.Connect(GetConnectionString()); - options.Select($"{testContext.KeyPrefix}:*"); + options.Select($"{testContext.KeyPrefix}:*", KeyVaultReferenceLabel); options.ConfigureKeyVault(kv => { kv.SetCredential(GetCredential()); From 41438c1b1df5307047b1f011af057e1614294274 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 4 Apr 2025 12:28:43 -0700 Subject: [PATCH 29/57] remove base exception catches --- .github/copilot-instructions.md | 1 + .../Integration/IntegrationTests.cs | 9 --------- 2 files changed, 1 insertion(+), 9 deletions(-) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..2bc55737e --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1 @@ +We always catch specific exceptions and avoid catching the base Exception class in catch blocks. \ No newline at end of file diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 0067d7aa6..892359abb 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -332,11 +332,6 @@ private async Task CleanupStaleResources() Console.WriteLine($"Error during stale resource cleanup: {ex.Message}"); // Continue execution even if cleanup fails } - catch (Exception ex) - { - Console.WriteLine($"Unexpected error during stale resource cleanup: {ex.Message}"); - // Continue execution even if cleanup fails - } } /// @@ -403,10 +398,6 @@ public async Task DisposeAsync() { Console.WriteLine($"Operation error during test cleanup: {ex.Message}"); } - catch (Exception ex) - { - Console.WriteLine($"Unexpected error during test cleanup: {ex.ToString()}"); - } } /// From a634dd51733f67f137e84bd57a3adb8e93462814 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 7 Apr 2025 10:20:59 -0700 Subject: [PATCH 30/57] update ci.yml to work with integration tests and use github repo secrets --- .github/workflows/ci.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af9b1d227..f57046d06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,22 @@ jobs: - name: Dotnet Pack run: pwsh pack.ps1 + - name: Azure Login with OIDC + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Create Subscription Settings File + run: | + cat > ${{ github.workspace }}/tests/Integration/appsettings.Secrets.json < Date: Tue, 8 Apr 2025 18:18:12 -0700 Subject: [PATCH 31/57] add id token write permission --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f57046d06..8a164a67c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ on: permissions: security-events: write + id-token: write jobs: build: From 0394f7b0d6826c11992a5fd27064f98190509c73 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 9 Apr 2025 10:46:48 -0700 Subject: [PATCH 32/57] update ci.yml step to create subscription file --- .github/workflows/ci.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a164a67c..b21ed09b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,13 +49,16 @@ jobs: subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Create Subscription Settings File + shell: pwsh run: | - cat > ${{ github.workspace }}/tests/Integration/appsettings.Secrets.json < Date: Wed, 9 Apr 2025 11:23:39 -0700 Subject: [PATCH 33/57] edit ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b21ed09b4..85f8827e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: "SubscriptionId": "$subscriptionId" } "@ - Set-Content -Path "${{ github.workspace }}/tests/Integration/appsettings.Secrets.json" -Value $jsonContent + Set-Content -Path "./tests/Integration/appsettings.Secrets.json" -Value $jsonContent - name: Dotnet Test run: pwsh test.ps1 From 658642377e568ba1d88fdaca3614f120d28ae00d Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 9 Apr 2025 13:03:43 -0700 Subject: [PATCH 34/57] fix github action --- .github/workflows/ci.yml | 14 ++------------ .../Integration/IntegrationTests.cs | 7 +++++++ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85f8827e6..2928be53e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,21 +47,11 @@ jobs: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Create Subscription Settings File - shell: pwsh - run: | - $subscriptionId = "${{ secrets.AZURE_SUBSCRIPTION_ID }}" - $jsonContent = @" - { - "Success": true, - "SubscriptionId": "$subscriptionId" - } - "@ - Set-Content -Path "./tests/Integration/appsettings.Secrets.json" -Value $jsonContent - name: Dotnet Test run: pwsh test.ps1 + env: + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Publish Test Results uses: actions/upload-artifact@v4 diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 892359abb..118e83b24 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -91,6 +91,13 @@ private DefaultAzureCredential GetCredential() /// private string GetCurrentSubscriptionId() { + string subscriptionIdFromEnv = Environment.GetEnvironmentVariable("AZURE_SUBSCRIPTION_ID"); + + if (!string.IsNullOrEmpty(subscriptionIdFromEnv)) + { + return subscriptionIdFromEnv; + } + // Read the JSON file created by the script string jsonPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Integration", SubscriptionJsonPath); From 36a045fbbf35ff60b7288e8fd147920ebeaa5629 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 10 Apr 2025 11:01:26 -0700 Subject: [PATCH 35/57] update comment in powershell script --- .../Integration/GetAzureSubscription.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/GetAzureSubscription.ps1 b/tests/Tests.AzureAppConfiguration/Integration/GetAzureSubscription.ps1 index 7916f45c0..66916a7e9 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/GetAzureSubscription.ps1 +++ b/tests/Tests.AzureAppConfiguration/Integration/GetAzureSubscription.ps1 @@ -2,6 +2,7 @@ # GetAzureSubscription.ps1 # This script gets the current Azure subscription ID and saves it to a JSON file +# Must be logged into AppConfig - Dev subscription for the integration tests to work. $ErrorActionPreference = "Stop" $outputPath = Join-Path $PSScriptRoot "appsettings.Secrets.json" @@ -48,4 +49,4 @@ catch { $errorInfo | ConvertTo-Json | Out-File $outputPath -Encoding utf8 Write-Error $_.Exception.Message exit 1 -} \ No newline at end of file +} From 2b2f1f503fdc802400f61e13bea5b7df4f0e199f Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 11 Apr 2025 09:28:27 -0700 Subject: [PATCH 36/57] remove unused packages --- .../Tests.AzureAppConfiguration.csproj | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj index 5d6113405..a213c8fb0 100644 --- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj +++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj @@ -25,9 +25,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - @@ -35,9 +33,6 @@ Always - - Always - From 965f348de288487f28e5f3ea06ffb2bf479871a8 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 11 Apr 2025 15:23:30 -0700 Subject: [PATCH 37/57] update comment, remove unused methods --- .../Integration/IntegrationTests.cs | 29 +------------------ .../Tests.AzureAppConfiguration.csproj | 1 - 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 118e83b24..a50c243e5 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -26,7 +26,7 @@ namespace Tests.AzureAppConfiguration /// Integration tests for Azure App Configuration that connect to a real service. /// Uses an existing App Configuration store and Key Vault for testing. /// Requires Azure credentials with appropriate permissions. - /// NOTE: Before running these tests, execute the GetAzureSubscription.ps1 script to create appsettings.Secrets.json. + /// NOTE: Before running these tests locally, execute the GetAzureSubscription.ps1 script to create appsettings.Secrets.json. /// [Trait("Category", "Integration")] [CollectionDefinition(nameof(IntegrationTests), DisableParallelization = true)] @@ -486,33 +486,6 @@ private async Task SetupTestKeys(string testName) }; } - // Helper method to track additional configuration keys created during tests - private void TrackConfigurationKey(string key) - { - if (!string.IsNullOrEmpty(key)) - { - _createdConfigKeys.Add(key); - } - } - - // Helper method to track additional secrets created during tests - private void TrackKeyVaultSecret(string secretName) - { - if (!string.IsNullOrEmpty(secretName)) - { - _createdSecretNames.Add(secretName); - } - } - - // Helper method to track additional snapshots created during tests - private void TrackSnapshot(string snapshotName) - { - if (!string.IsNullOrEmpty(snapshotName)) - { - _createdSnapshotNames.Add(snapshotName); - } - } - [Fact] public async Task LoadConfiguration_RetrievesValuesFromAppConfiguration() { diff --git a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj index a213c8fb0..eefaff944 100644 --- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj +++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj @@ -28,7 +28,6 @@ - Always From 14445c0dbae753dfb327298695000d3b4b0b11d6 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 14 Apr 2025 12:01:09 -0700 Subject: [PATCH 38/57] add request tracing test --- .../Integration/IntegrationTests.cs | 118 ++++++++++++++++++ .../Tests.AzureAppConfiguration.csproj | 1 + 2 files changed, 119 insertions(+) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index a50c243e5..e66bc7581 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Configuration.AzureAppConfiguration; using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault; using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement; +using Microsoft.FeatureManagement; using System; using System.Collections.Generic; using System.IO; @@ -1801,5 +1802,122 @@ await _configClient.SetConfigurationSettingAsync( Assert.Equal(updatedValue1, config[kvRefKey1]); // Updated - short refresh interval Assert.Equal(secretValue2, config[kvRefKey2]); // Not updated - long refresh interval } + + [Fact] + public async Task RequestTracing_SetsCorrectCorrelationContextHeader() + { + // Arrange - Setup test-specific keys + var testContext = await SetupTestKeys("RequestTracing"); + + // Used to trigger FMVer tag in request tracing + IFeatureManager featureManager; + + // Create a custom HttpPipeline that can inspect outgoing requests + var requestInspector = new RequestInspectionHandler(); + + await _configClient.SetConfigurationSettingAsync( + ConfigurationModelFactory.ConfigurationSetting( + testContext.FeatureFlagKey, + @"{ + ""id"": """ + testContext.KeyPrefix + @"Feature"", + ""description"": ""Test feature with filters"", + ""enabled"": true, + ""conditions"": { + ""client_filters"": [ + { + ""name"": ""Browser"", + ""parameters"": { + ""AllowedBrowsers"": [""Chrome"", ""Edge""] + } + }, + { + ""name"": ""TimeWindow"", + ""parameters"": { + ""Start"": ""\/Date(" + DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeMilliseconds() + @")\/"", + ""End"": ""\/Date(" + DateTimeOffset.UtcNow.AddDays(1).ToUnixTimeMilliseconds() + @")\/"" + } + } + ] + } + }", + contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + + IConfigurationRefresher refresher = null; + + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(GetConnectionString()); + options.Select($"{testContext.KeyPrefix}:*"); + options.ConfigureClientOptions(clientOptions => + { + clientOptions.Transport = new HttpClientTransportWithRequestInspection(requestInspector); + }); + options.ConfigureKeyVault(kv => kv.SetCredential(GetCredential())); + options.UseFeatureFlags(); + options.LoadBalancingEnabled = true; + refresher = options.GetRefresher(); + options.ConfigureRefresh(refresh => + { + refresh.RegisterAll(); + refresh.SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + }) + .Build(); + + // Assert - Verify correlation context headers + + // Basic request should have at least the request type + Assert.Contains(RequestTracingConstants.RequestTypeKey, requestInspector.CorrelationContextHeaders.Last()); + Assert.Contains("Startup", requestInspector.CorrelationContextHeaders.Last()); + Assert.Contains(RequestTracingConstants.KeyVaultConfiguredTag, requestInspector.CorrelationContextHeaders.Last()); + Assert.Contains(RequestTracingConstants.LoadBalancingEnabledTag, requestInspector.CorrelationContextHeaders.Last()); + + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting1", "UpdatedValue1")); + await Task.Delay(1500); + await refresher.RefreshAsync(); + + Assert.Contains("Watch", requestInspector.CorrelationContextHeaders.Last()); + Assert.Contains(RequestTracingConstants.FeatureFlagFilterTypeKey, requestInspector.CorrelationContextHeaders.Last()); + Assert.Contains(RequestTracingConstants.TimeWindowFilter, requestInspector.CorrelationContextHeaders.Last()); + Assert.Contains(RequestTracingConstants.CustomFilter, requestInspector.CorrelationContextHeaders.Last()); + Assert.Contains(RequestTracingConstants.FeatureFlagMaxVariantsKey, requestInspector.CorrelationContextHeaders.Last()); + Assert.Contains($"{RequestTracingConstants.FeatureManagementVersionKey}=4.0.0", requestInspector.CorrelationContextHeaders.Last()); + } + + /// + /// Helper class to track HTTP requests and extract correlation context headers + /// + private class RequestInspectionHandler + { + public List CorrelationContextHeaders { get; } = new List(); + + public void InspectRequest(HttpMessage message) + { + if (message.Request.Headers.TryGetValue(RequestTracingConstants.CorrelationContextHeader, out string header)) + { + CorrelationContextHeaders.Add(header); + } + } + } + + /// + /// Custom HttpPipelineTransport that inspects requests before sending + /// + private class HttpClientTransportWithRequestInspection : HttpClientTransport + { + private readonly RequestInspectionHandler _inspector; + + public HttpClientTransportWithRequestInspection(RequestInspectionHandler inspector) + { + _inspector = inspector; + } + + public override async ValueTask ProcessAsync(HttpMessage message) + { + _inspector.InspectRequest(message); + await base.ProcessAsync(message); + } + } } } diff --git a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj index eefaff944..79f47316e 100644 --- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj +++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj @@ -17,6 +17,7 @@ + From 70540df4ce15b0f8fe52a2b16767e9cf3a9178be Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 14 Apr 2025 15:36:32 -0700 Subject: [PATCH 39/57] check for status code in catch, move cleanupstaleresources call to dispose --- .../Integration/IntegrationTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index e66bc7581..eb29c048f 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -195,7 +195,7 @@ public async Task InitializeAsync() // Get App Configuration store directly using the resource group and store name appConfigStore = await resourceGroup.GetAppConfigurationStores().GetAsync(AppConfigStoreName); } - catch (RequestFailedException ex) + catch (RequestFailedException ex) when (ex.Status == 404) { throw new InvalidOperationException($"App Configuration store '{AppConfigStoreName}' not found in resource group '{ResourceGroupName}'. Please create it before running tests.", ex); } @@ -230,9 +230,6 @@ public async Task InitializeAsync() _secretClient = new SecretClient(_keyVaultEndpoint, credential); Console.WriteLine($"Successfully connected to App Configuration store '{AppConfigStoreName}' and Key Vault '{KeyVaultName}'"); - - // Clean up stale resources on startup - await CleanupStaleResources(); } catch (RequestFailedException ex) { @@ -406,6 +403,9 @@ public async Task DisposeAsync() { Console.WriteLine($"Operation error during test cleanup: {ex.Message}"); } + + // Clean up stale resources on dispose + await CleanupStaleResources(); } /// From 683a07983965f57d8845cb3952bb69896d7af25b Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 14 Apr 2025 17:18:11 -0700 Subject: [PATCH 40/57] PR comments --- .github/copilot-instructions.md | 4 +- .../Integration/GetAzureSubscription.ps1 | 71 ++--- .../Integration/IntegrationTests.cs | 300 ++++++++---------- 3 files changed, 163 insertions(+), 212 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2bc55737e..05034c448 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1 +1,3 @@ -We always catch specific exceptions and avoid catching the base Exception class in catch blocks. \ No newline at end of file +We always catch specific exceptions and avoid catching the base Exception class in catch blocks. + +We never use `var` to declare a variable if the assignment does not include the type of the variable, or the type is not immediately obvious. \ No newline at end of file diff --git a/tests/Tests.AzureAppConfiguration/Integration/GetAzureSubscription.ps1 b/tests/Tests.AzureAppConfiguration/Integration/GetAzureSubscription.ps1 index 66916a7e9..c08e434db 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/GetAzureSubscription.ps1 +++ b/tests/Tests.AzureAppConfiguration/Integration/GetAzureSubscription.ps1 @@ -1,52 +1,47 @@ #!/usr/bin/env pwsh # GetAzureSubscription.ps1 -# This script gets the current Azure subscription ID and saves it to a JSON file -# Must be logged into AppConfig - Dev subscription for the integration tests to work. +# This script gets the AppConfig - Dev subscription ID and saves it to a JSON file -$ErrorActionPreference = "Stop" $outputPath = Join-Path $PSScriptRoot "appsettings.Secrets.json" -try { - # Get current subscription from az CLI - $subscriptionId = az account show --query id -o tsv 2>&1 +Write-Host "Checking for active Azure CLI login" + +az account show | Out-Null + +if ($LASTEXITCODE -ne 0) { + Write-Host "Must be logged in with the Azure CLI to proceed" + + az login - # Check if the command was successful if ($LASTEXITCODE -ne 0) { - $errorInfo = @{ - Success = $false - ErrorMessage = "Azure CLI command failed with exit code $LASTEXITCODE. Output: $subscriptionId" - } - $errorInfo | ConvertTo-Json | Out-File $outputPath -Encoding utf8 - exit 1 + Write-Host "Azure login failed" + return } +} - # Check if the output is empty - if ([string]::IsNullOrWhiteSpace($subscriptionId)) { - $errorInfo = @{ - Success = $false - ErrorMessage = "No active Azure subscription found. Please run 'az login' first." - } - $errorInfo | ConvertTo-Json | Out-File $outputPath -Encoding utf8 - exit 1 - } +az account set --name "AppConfig - Dev" - # If successful, save the subscription ID to a JSON file - $result = @{ - Success = $true - SubscriptionId = $subscriptionId.Trim() - } - - $result | ConvertTo-Json | Out-File $outputPath -Encoding utf8 - Write-Output "Subscription information saved to: $outputPath" - exit 0 +# Get current subscription from az CLI +$subscriptionId = az account show --query id -o tsv 2>&1 + +if ($LASTEXITCODE -ne 0) { + Write-Host "Azure CLI command failed with exit code $LASTEXITCODE. Output: $subscriptionId" + return } -catch { - $errorInfo = @{ - Success = $false - ErrorMessage = "Error getting Azure subscription: $_" - } - $errorInfo | ConvertTo-Json | Out-File $outputPath -Encoding utf8 - Write-Error $_.Exception.Message + +# Check if the output is empty +if ([string]::IsNullOrWhiteSpace($subscriptionId)) { + Write-Host "No active Azure subscription found. Please run 'az login' first." + exit 1 } + +# If successful, save the subscription ID to a JSON file +$result = @{ + SubscriptionId = $subscriptionId.Trim() +} + +$result | ConvertTo-Json | Out-File $outputPath -Encoding utf8 +Write-Host "Subscription information saved to: $outputPath" +exit 0 diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index eb29c048f..8bb736dd1 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -5,6 +5,7 @@ using Azure.Identity; using Azure.ResourceManager; using Azure.ResourceManager.AppConfiguration; +using Azure.ResourceManager.AppConfiguration.Models; using Azure.ResourceManager.KeyVault; using Azure.ResourceManager.Resources; using Azure.Security.KeyVault.Secrets; @@ -44,6 +45,12 @@ public class IntegrationTests : IAsyncLifetime private const string KeyVaultName = "keyvault-dotnetprovider"; private const string ResourceGroupName = "dotnetprovider-integrationtest"; + private readonly DefaultAzureCredential _defaultAzureCredential = new DefaultAzureCredential( + new DefaultAzureCredentialOptions + { + ExcludeSharedTokenCacheCredential = true + }); + /// /// Class to hold test-specific key information /// @@ -75,17 +82,6 @@ private class TestContext private readonly HashSet _createdSecretNames = new HashSet(); private readonly HashSet _createdSnapshotNames = new HashSet(); - /// - /// Gets a DefaultAzureCredential for authentication. - /// - private DefaultAzureCredential GetCredential() - { - return new DefaultAzureCredential(new DefaultAzureCredentialOptions - { - ExcludeSharedTokenCacheCredential = true - }); - } - /// /// Gets the current subscription ID by reading from the JSON file created by the PowerShell script. /// NOTE: The PowerShell script must be run manually before running the tests. @@ -112,24 +108,9 @@ private string GetCurrentSubscriptionId() using JsonDocument doc = JsonDocument.Parse(jsonContent); JsonElement root = doc.RootElement; - bool success = root.GetProperty("Success").GetBoolean(); - - if (!success) - { - throw new InvalidOperationException(root.GetProperty("ErrorMessage").GetString()); - } - return root.GetProperty("SubscriptionId").GetString(); } - /// - /// Returns the connection string for connecting to the app configuration store. - /// - private string GetConnectionString() - { - return _connectionString; - } - /// /// Creates a unique prefix for test keys to ensure test isolation /// @@ -154,21 +135,13 @@ private async Task CreateSnapshot(string snapshotName, TestContext testC snapshot.SnapshotComposition = SnapshotComposition.Key; - try - { - // Create the snapshot - CreateSnapshotOperation operation = await _configClient.CreateSnapshotAsync(WaitUntil.Completed, snapshotName, snapshot); + // Create the snapshot + CreateSnapshotOperation operation = await _configClient.CreateSnapshotAsync(WaitUntil.Completed, snapshotName, snapshot); - // Track created snapshot for cleanup - _createdSnapshotNames.Add(snapshotName); + // Track created snapshot for cleanup + _createdSnapshotNames.Add(snapshotName); - return operation.Value.Name; - } - catch (RequestFailedException ex) - { - Console.WriteLine($"Error creating snapshot: {ex.Message}"); - throw; - } + return operation.Value.Name; } /// @@ -176,77 +149,58 @@ private async Task CreateSnapshot(string snapshotName, TestContext testC /// public async Task InitializeAsync() { - try - { - var credential = GetCredential(); - string subscriptionId = GetCurrentSubscriptionId(); + DefaultAzureCredential credential = _defaultAzureCredential; + string subscriptionId = GetCurrentSubscriptionId(); - // Initialize Azure Resource Manager client - var armClient = new ArmClient(credential); - SubscriptionResource subscription = armClient.GetSubscriptionResource(new ResourceIdentifier($"/subscriptions/{subscriptionId}")); + // Initialize Azure Resource Manager client + var armClient = new ArmClient(credential); + SubscriptionResource subscription = armClient.GetSubscriptionResource(new ResourceIdentifier($"/subscriptions/{subscriptionId}")); - ResourceGroupResource resourceGroup = await subscription.GetResourceGroups().GetAsync(ResourceGroupName); + ResourceGroupResource resourceGroup = await subscription.GetResourceGroups().GetAsync(ResourceGroupName); - AppConfigurationStoreResource appConfigStore = null; - KeyVaultResource keyVault = null; + AppConfigurationStoreResource appConfigStore = null; + KeyVaultResource keyVault = null; - try - { - // Get App Configuration store directly using the resource group and store name - appConfigStore = await resourceGroup.GetAppConfigurationStores().GetAsync(AppConfigStoreName); - } - catch (RequestFailedException ex) when (ex.Status == 404) - { - throw new InvalidOperationException($"App Configuration store '{AppConfigStoreName}' not found in resource group '{ResourceGroupName}'. Please create it before running tests.", ex); - } + try + { + // Get App Configuration store directly using the resource group and store name + appConfigStore = await resourceGroup.GetAppConfigurationStores().GetAsync(AppConfigStoreName); + } + catch (RequestFailedException ex) when (ex.Status == 404) + { + throw new InvalidOperationException($"App Configuration store '{AppConfigStoreName}' not found in resource group '{ResourceGroupName}'. Please create it before running tests.", ex); + } - _appConfigEndpoint = new Uri(appConfigStore.Data.Endpoint); + _appConfigEndpoint = new Uri(appConfigStore.Data.Endpoint); - // Get connection string from the store - var accessKeys = appConfigStore.GetKeysAsync(); - var primaryKey = await accessKeys.FirstOrDefaultAsync(); + // Get connection string from the store + AsyncPageable accessKeys = appConfigStore.GetKeysAsync(); + AppConfigurationStoreApiKey primaryKey = await accessKeys.FirstOrDefaultAsync(); - if (primaryKey == null) - { - throw new InvalidOperationException("Failed to retrieve access keys from App Configuration store."); - } + if (primaryKey == null) + { + throw new InvalidOperationException("Failed to retrieve access keys from App Configuration store."); + } - _connectionString = primaryKey.ConnectionString; + _connectionString = primaryKey.ConnectionString; - // Initialize the configuration client with the connection string - _configClient = new ConfigurationClient(_connectionString); + // Initialize the configuration client with the connection string + _configClient = new ConfigurationClient(_connectionString); - // Find and initialize Key Vault - look in the same resource group - keyVault = await resourceGroup.GetKeyVaults().GetAsync(KeyVaultName); + // Find and initialize Key Vault - look in the same resource group + keyVault = await resourceGroup.GetKeyVaults().GetAsync(KeyVaultName); - if (keyVault == null) - { - throw new InvalidOperationException($"Key Vault '{KeyVaultName}' not found in subscription {subscriptionId}. Please create it before running tests."); - } + if (keyVault == null) + { + throw new InvalidOperationException($"Key Vault '{KeyVaultName}' not found in subscription {subscriptionId}. Please create it before running tests."); + } - _keyVaultEndpoint = keyVault.Data.Properties.VaultUri; + _keyVaultEndpoint = keyVault.Data.Properties.VaultUri; - // Create a Secret Client for the vault - _secretClient = new SecretClient(_keyVaultEndpoint, credential); + // Create a Secret Client for the vault + _secretClient = new SecretClient(_keyVaultEndpoint, credential); - Console.WriteLine($"Successfully connected to App Configuration store '{AppConfigStoreName}' and Key Vault '{KeyVaultName}'"); - } - catch (RequestFailedException ex) - { - Console.WriteLine($"Azure request failed: {ex.Message}. Status code: {ex.Status}"); - throw; - } - catch (ArgumentException ex) - { - Console.WriteLine($"Invalid argument: {ex.Message}"); - throw; - } - catch (InvalidOperationException ex) - { - // This is already a specific exception, so just rethrow - Console.WriteLine($"Invalid operation: {ex.Message}"); - throw; - } + Console.WriteLine($"Successfully connected to App Configuration store '{AppConfigStoreName}' and Key Vault '{KeyVaultName}'"); } /// @@ -265,12 +219,12 @@ private async Task CleanupStaleResources() var configSettingsToCleanup = new List(); // Get all test key-values - var configSettings = _configClient.GetConfigurationSettingsAsync(new SettingSelector + AsyncPageable configSettings = _configClient.GetConfigurationSettingsAsync(new SettingSelector { KeyFilter = TestKeyPrefix + "*" }); - await foreach (var setting in configSettings) + await foreach (ConfigurationSetting setting in configSettings) { // Check if the setting is older than the threshold if (setting.LastModified < cutoffTime) @@ -281,12 +235,12 @@ private async Task CleanupStaleResources() } // Clean up stale feature flags - var featureFlagSettings = _configClient.GetConfigurationSettingsAsync(new SettingSelector + AsyncPageable featureFlagSettings = _configClient.GetConfigurationSettingsAsync(new SettingSelector { KeyFilter = ".appconfig.featureflag/" + TestKeyPrefix + "*" }); - await foreach (var setting in featureFlagSettings) + await foreach (ConfigurationSetting setting in featureFlagSettings) { if (setting.LastModified < cutoffTime) { @@ -296,15 +250,15 @@ private async Task CleanupStaleResources() } // Delete stale configuration settings - foreach (var setting in configSettingsToCleanup) + foreach (ConfigurationSetting setting in configSettingsToCleanup) { cleanupTasks.Add(_configClient.DeleteConfigurationSettingAsync(setting.Key, setting.Label)); } // Clean up stale snapshots int staleSnapshotCount = 0; - var snapshots = _configClient.GetSnapshotsAsync(new SnapshotSelector()); - await foreach (var snapshot in snapshots) + AsyncPageable snapshots = _configClient.GetSnapshotsAsync(new SnapshotSelector()); + await foreach (ConfigurationSnapshot snapshot in snapshots) { if (snapshot.Name.StartsWith("snapshot-" + TestKeyPrefix) && snapshot.CreatedOn < cutoffTime) { @@ -317,8 +271,8 @@ private async Task CleanupStaleResources() int staleSecretCount = 0; if (_secretClient != null) { - var secrets = _secretClient.GetPropertiesOfSecretsAsync(); - await foreach (var secretProperties in secrets) + AsyncPageable secrets = _secretClient.GetPropertiesOfSecretsAsync(); + await foreach (SecretProperties secretProperties in secrets) { if (secretProperties.Name.StartsWith(TestKeyPrefix) && secretProperties.CreatedOn.HasValue && secretProperties.CreatedOn.Value < cutoffTime) { @@ -349,7 +303,7 @@ public async Task DisposeAsync() try { // Clean up all configuration settings created by tests - foreach (var key in _createdConfigKeys) + foreach (string key in _createdConfigKeys) { try { @@ -362,7 +316,7 @@ public async Task DisposeAsync() } // Clean up all snapshots created by tests - foreach (var snapshotName in _createdSnapshotNames) + foreach (string snapshotName in _createdSnapshotNames) { try { @@ -377,7 +331,7 @@ public async Task DisposeAsync() // Clean up test-specific secrets in Key Vault if (_secretClient != null) { - foreach (var secretName in _createdSecretNames) + foreach (string secretName in _createdSecretNames) { try { @@ -433,7 +387,7 @@ private async Task SetupTestKeys(string testName) }; // Add test-specific settings to the store - foreach (var setting in testSettings) + foreach (ConfigurationSetting setting in testSettings) { await _configClient.SetConfigurationSettingAsync(setting); // Track the created key for cleanup @@ -454,7 +408,7 @@ private async Task SetupTestKeys(string testName) string keyVaultUri = $"{_keyVaultEndpoint}secrets/{secretName}"; string keyVaultRefValue = @$"{{""uri"":""{keyVaultUri}""}}"; - var keyVaultRefSetting = ConfigurationModelFactory.ConfigurationSetting( + ConfigurationSetting keyVaultRefSetting = ConfigurationModelFactory.ConfigurationSetting( keyVaultReferenceKey, keyVaultRefValue, label: KeyVaultReferenceLabel, @@ -491,13 +445,13 @@ private async Task SetupTestKeys(string testName) public async Task LoadConfiguration_RetrievesValuesFromAppConfiguration() { // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("BasicConfig"); + TestContext testContext = await SetupTestKeys("BasicConfig"); // Act var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.Select($"{testContext.KeyPrefix}:*"); }) .Build(); @@ -511,13 +465,13 @@ public async Task LoadConfiguration_RetrievesValuesFromAppConfiguration() public async Task RefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() { // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("UpdatesConfig"); + TestContext testContext = await SetupTestKeys("UpdatesConfig"); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.Select($"{testContext.KeyPrefix}:*"); options.ConfigureRefresh(refresh => { @@ -550,13 +504,13 @@ public async Task RefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() public async Task RegisterAll_RefreshesAllKeys() { // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("RefreshesAllKeys"); + TestContext testContext = await SetupTestKeys("RefreshesAllKeys"); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.Select($"{testContext.KeyPrefix}:*"); // Use RegisterAll to refresh everything when sentinel changes @@ -593,13 +547,13 @@ public async Task RegisterAll_RefreshesAllKeys() public async Task RefreshAsync_SentinelKeyUnchanged() { // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("SentinelUnchanged"); + TestContext testContext = await SetupTestKeys("SentinelUnchanged"); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.Select($"{testContext.KeyPrefix}:*"); options.ConfigureRefresh(refresh => @@ -631,13 +585,13 @@ public async Task RefreshAsync_SentinelKeyUnchanged() [Fact] public async Task RefreshAsync_RefreshesFeatureFlags_WhenConfigured() { - var testContext = await SetupTestKeys("FeatureFlagRefresh"); + TestContext testContext = await SetupTestKeys("FeatureFlagRefresh"); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); // Select the key prefix to include all test keys options.Select($"{testContext.KeyPrefix}:*"); @@ -684,7 +638,7 @@ await _configClient.SetConfigurationSettingAsync( [Fact] public async Task UseFeatureFlags_WithClientFiltersAndConditions() { - var testContext = await SetupTestKeys("FeatureFlagFilters"); + TestContext testContext = await SetupTestKeys("FeatureFlagFilters"); // Create a feature flag with complex conditions await _configClient.SetConfigurationSettingAsync( @@ -717,7 +671,7 @@ await _configClient.SetConfigurationSettingAsync( var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.UseFeatureFlags(featureFlagOptions => { featureFlagOptions.Select(testContext.KeyPrefix + "*"); @@ -735,15 +689,15 @@ await _configClient.SetConfigurationSettingAsync( [Fact] public async Task MultipleProviders_LoadAndRefresh() { - var testContext1 = await SetupTestKeys("MultiProviderTest1"); - var testContext2 = await SetupTestKeys("MultiProviderTest2"); + TestContext testContext1 = await SetupTestKeys("MultiProviderTest1"); + TestContext testContext2 = await SetupTestKeys("MultiProviderTest2"); IConfigurationRefresher refresher1 = null; IConfigurationRefresher refresher2 = null; var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.Select($"{testContext1.KeyPrefix}:*"); options.ConfigureRefresh(refresh => { @@ -755,7 +709,7 @@ public async Task MultipleProviders_LoadAndRefresh() }) .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.Select($"{testContext2.KeyPrefix}:*"); options.ConfigureRefresh(refresh => { @@ -789,7 +743,7 @@ public async Task MultipleProviders_LoadAndRefresh() [Fact] public async Task FeatureFlag_WithVariants() { - var testContext = await SetupTestKeys("FeatureFlagVariants"); + TestContext testContext = await SetupTestKeys("FeatureFlagVariants"); await _configClient.SetConfigurationSettingAsync( ConfigurationModelFactory.ConfigurationSetting( @@ -829,7 +783,7 @@ await _configClient.SetConfigurationSettingAsync( var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.UseFeatureFlags(featureFlagOptions => { featureFlagOptions.Select(testContext.KeyPrefix + "*"); @@ -851,7 +805,7 @@ await _configClient.SetConfigurationSettingAsync( [Fact] public async Task JsonContentType_LoadsAndFlattensHierarchicalData() { - var testContext = await SetupTestKeys("JsonContent"); + TestContext testContext = await SetupTestKeys("JsonContent"); // Create a complex JSON structure string jsonKey = $"{testContext.KeyPrefix}:JsonConfig"; @@ -877,7 +831,7 @@ await _configClient.SetConfigurationSettingAsync( var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.Select($"{testContext.KeyPrefix}:*"); }) .Build(); @@ -897,7 +851,7 @@ await _configClient.SetConfigurationSettingAsync( public async Task MethodOrderingDoesNotAffectConfiguration() { // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("MethodOrdering"); + TestContext testContext = await SetupTestKeys("MethodOrdering"); // Add an additional feature flag for testing await _configClient.SetConfigurationSettingAsync( @@ -928,7 +882,7 @@ await _configClient.SetConfigurationSettingAsync( var config1 = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.Select($"{testContext.KeyPrefix}:*"); options.ConfigureRefresh(refresh => { @@ -949,7 +903,7 @@ await _configClient.SetConfigurationSettingAsync( var config2 = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.ConfigureRefresh(refresh => { refresh.Register(testContext.SentinelKey, true) @@ -970,7 +924,7 @@ await _configClient.SetConfigurationSettingAsync( var config3 = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.UseFeatureFlags(featureFlagOptions => { featureFlagOptions.Select(testContext.KeyPrefix + "*"); @@ -991,7 +945,7 @@ await _configClient.SetConfigurationSettingAsync( var config4 = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.UseFeatureFlags(featureFlagOptions => { featureFlagOptions.Select(testContext.KeyPrefix + "*"); @@ -1072,7 +1026,7 @@ await _configClient.SetConfigurationSettingAsync( public async Task RegisterWithRefreshAllAndRegisterAll_BehaveIdentically() { // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("RefreshEquivalency"); + TestContext testContext = await SetupTestKeys("RefreshEquivalency"); // Add another feature flag for testing string secondFeatureFlagKey = $".appconfig.featureflag/{testContext.KeyPrefix}Feature2"; @@ -1088,7 +1042,7 @@ await _configClient.SetConfigurationSettingAsync( var config1 = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.Select($"{testContext.KeyPrefix}:*"); options.UseFeatureFlags(featureFlagOptions => { @@ -1109,7 +1063,7 @@ await _configClient.SetConfigurationSettingAsync( var config2 = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.Select($"{testContext.KeyPrefix}:*"); options.UseFeatureFlags(featureFlagOptions => { @@ -1206,10 +1160,10 @@ await _configClient.SetConfigurationSettingAsync( public async Task HandlesFailoverOnStartup() { // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("FailoverStartup"); + TestContext testContext = await SetupTestKeys("FailoverStartup"); IConfigurationRefresher refresher = null; - string connectionString = GetConnectionString(); + string connectionString = _connectionString; // Create a connection string that will fail string primaryConnectionString = ConnectionStringUtils.Build( @@ -1246,7 +1200,7 @@ public async Task HandlesFailoverOnStartup() public async Task LoadSnapshot_RetrievesValuesFromSnapshot() { // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("SnapshotTest"); + TestContext testContext = await SetupTestKeys("SnapshotTest"); string snapshotName = $"snapshot-{testContext.KeyPrefix}"; // Create a snapshot with the test keys @@ -1259,7 +1213,7 @@ public async Task LoadSnapshot_RetrievesValuesFromSnapshot() var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.SelectSnapshot(snapshotName); }) .Build(); @@ -1279,7 +1233,7 @@ public async Task LoadSnapshot_RetrievesValuesFromSnapshot() public async Task LoadSnapshot_ThrowsException_WhenSnapshotDoesNotExist() { // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("NonExistentSnapshotTest"); + TestContext testContext = await SetupTestKeys("NonExistentSnapshotTest"); string nonExistentSnapshotName = $"snapshot-does-not-exist-{Guid.NewGuid()}"; // Act & Assert - Loading a non-existent snapshot should throw @@ -1288,7 +1242,7 @@ public async Task LoadSnapshot_ThrowsException_WhenSnapshotDoesNotExist() return Task.FromResult(new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.SelectSnapshot(nonExistentSnapshotName); }) .Build()); @@ -1305,8 +1259,8 @@ public async Task LoadSnapshot_ThrowsException_WhenSnapshotDoesNotExist() public async Task LoadMultipleSnapshots_MergesConfigurationCorrectly() { // Arrange - Setup test-specific keys for two separate snapshots - var testContext1 = await SetupTestKeys("SnapshotMergeTest1"); - var testContext2 = await SetupTestKeys("SnapshotMergeTest2"); + TestContext testContext1 = await SetupTestKeys("SnapshotMergeTest1"); + TestContext testContext2 = await SetupTestKeys("SnapshotMergeTest2"); // Create specific values for second snapshot await _configClient.SetConfigurationSettingAsync( @@ -1325,7 +1279,7 @@ await _configClient.SetConfigurationSettingAsync( var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.SelectSnapshot(snapshotName1); options.SelectSnapshot(snapshotName2); }) @@ -1351,7 +1305,7 @@ await _configClient.SetConfigurationSettingAsync( public async Task SnapshotCompositionTypes_AreHandledCorrectly() { // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("SnapshotCompositionTest"); + TestContext testContext = await SetupTestKeys("SnapshotCompositionTest"); string keyOnlySnapshotName = $"snapshot-key-{testContext.KeyPrefix}"; string invalidCompositionSnapshotName = $"snapshot-invalid-{testContext.KeyPrefix}"; @@ -1381,7 +1335,7 @@ public async Task SnapshotCompositionTypes_AreHandledCorrectly() var config1 = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.SelectSnapshot(keyOnlySnapshotName); }) .Build(); @@ -1394,7 +1348,7 @@ public async Task SnapshotCompositionTypes_AreHandledCorrectly() return Task.FromResult(new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.SelectSnapshot(invalidCompositionSnapshotName); }) .Build()); @@ -1420,7 +1374,7 @@ public async Task SnapshotCompositionTypes_AreHandledCorrectly() public async Task SnapshotWithFeatureFlags_LoadsConfigurationCorrectly() { // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("SnapshotFeatureFlagTest"); + TestContext testContext = await SetupTestKeys("SnapshotFeatureFlagTest"); string snapshotName = $"snapshot-ff-{testContext.KeyPrefix}"; // Update the feature flag to be enabled before creating the snapshot @@ -1457,7 +1411,7 @@ await _configClient.SetConfigurationSettingAsync( var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.UseFeatureFlags(); options.SelectSnapshot(snapshotName); }) @@ -1480,9 +1434,9 @@ await _configClient.SetConfigurationSettingAsync( public async Task CallOrdering_SnapshotsWithSelectAndFeatureFlags() { // Arrange - Setup test-specific keys for multiple snapshots - var mainContext = await SetupTestKeys("SnapshotOrdering"); - var secondContext = await SetupTestKeys("SnapshotOrdering2"); - var thirdContext = await SetupTestKeys("SnapshotOrdering3"); + TestContext mainContext = await SetupTestKeys("SnapshotOrdering"); + TestContext secondContext = await SetupTestKeys("SnapshotOrdering2"); + TestContext thirdContext = await SetupTestKeys("SnapshotOrdering3"); // Create specific values for each snapshot await _configClient.SetConfigurationSettingAsync( @@ -1526,7 +1480,7 @@ await _configClient.SetConfigurationSettingAsync( var config1 = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.SelectSnapshot(snapshot1); options.Select($"{mainContext.KeyPrefix}:*"); options.UseFeatureFlags(ff => @@ -1540,7 +1494,7 @@ await _configClient.SetConfigurationSettingAsync( var config2 = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.Select($"{secondContext.KeyPrefix}:*"); options.SelectSnapshot(snapshot2); options.UseFeatureFlags(ff => @@ -1554,7 +1508,7 @@ await _configClient.SetConfigurationSettingAsync( var config3 = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.UseFeatureFlags(); options.SelectSnapshot(snapshot3); options.Select($"{thirdContext.KeyPrefix}:*"); @@ -1565,7 +1519,7 @@ await _configClient.SetConfigurationSettingAsync( var config4 = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.SelectSnapshot(snapshot1); options.UseFeatureFlags(ff => { @@ -1625,15 +1579,15 @@ await _configClient.SetConfigurationSettingAsync( public async Task KeyVaultReferences_ResolveCorrectly() { // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("KeyVaultReference"); + TestContext testContext = await SetupTestKeys("KeyVaultReference"); // Act - Create configuration with Key Vault support var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.Select($"{testContext.KeyPrefix}:*", KeyVaultReferenceLabel); - options.ConfigureKeyVault(kv => kv.SetCredential(GetCredential())); + options.ConfigureKeyVault(kv => kv.SetCredential(_defaultAzureCredential)); }) .Build(); @@ -1679,13 +1633,13 @@ public override ValueTask ProcessAsync(HttpMessage message) public async Task KeyVaultReference_UsesCache_DoesNotCallKeyVaultAgain() { // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("KeyVaultCacheTest"); + TestContext testContext = await SetupTestKeys("KeyVaultCacheTest"); // Create a monitoring client to track calls to Key Vault int requestCount = 0; var testSecretClient = new SecretClient( _keyVaultEndpoint, - GetCredential(), + _defaultAzureCredential, new SecretClientOptions { Transport = new HttpPipelineTransportWithRequestCount(() => requestCount++) @@ -1695,7 +1649,7 @@ public async Task KeyVaultReference_UsesCache_DoesNotCallKeyVaultAgain() var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.Select($"{testContext.KeyPrefix}:*", KeyVaultReferenceLabel); options.ConfigureKeyVault(kv => { @@ -1705,11 +1659,11 @@ public async Task KeyVaultReference_UsesCache_DoesNotCallKeyVaultAgain() .Build(); // First access should resolve from Key Vault - var firstValue = config[testContext.KeyVaultReferenceKey]; + string firstValue = config[testContext.KeyVaultReferenceKey]; int firstRequestCount = requestCount; // Second access should use the cache - var secondValue = config[testContext.KeyVaultReferenceKey]; + string secondValue = config[testContext.KeyVaultReferenceKey]; int secondRequestCount = requestCount; // Assert @@ -1726,7 +1680,7 @@ public async Task KeyVaultReference_UsesCache_DoesNotCallKeyVaultAgain() public async Task KeyVaultReference_DifferentRefreshIntervals() { // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("KeyVaultDifferentIntervals"); + TestContext testContext = await SetupTestKeys("KeyVaultDifferentIntervals"); IConfigurationRefresher refresher = null; // Create a secret in Key Vault with short refresh interval @@ -1762,11 +1716,11 @@ await _configClient.SetConfigurationSettingAsync( var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.Select($"{testContext.KeyPrefix}:*", KeyVaultReferenceLabel); options.ConfigureKeyVault(kv => { - kv.SetCredential(GetCredential()); + kv.SetCredential(_defaultAzureCredential); // Set different refresh intervals for each secret kv.SetSecretRefreshInterval(kvRefKey1, TimeSpan.FromSeconds(60)); // Short interval kv.SetSecretRefreshInterval(kvRefKey2, TimeSpan.FromDays(1)); // Long interval @@ -1807,7 +1761,7 @@ await _configClient.SetConfigurationSettingAsync( public async Task RequestTracing_SetsCorrectCorrelationContextHeader() { // Arrange - Setup test-specific keys - var testContext = await SetupTestKeys("RequestTracing"); + TestContext testContext = await SetupTestKeys("RequestTracing"); // Used to trigger FMVer tag in request tracing IFeatureManager featureManager; @@ -1847,13 +1801,13 @@ await _configClient.SetConfigurationSettingAsync( var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.Connect(GetConnectionString()); + options.Connect(_connectionString); options.Select($"{testContext.KeyPrefix}:*"); options.ConfigureClientOptions(clientOptions => { clientOptions.Transport = new HttpClientTransportWithRequestInspection(requestInspector); }); - options.ConfigureKeyVault(kv => kv.SetCredential(GetCredential())); + options.ConfigureKeyVault(kv => kv.SetCredential(_defaultAzureCredential)); options.UseFeatureFlags(); options.LoadBalancingEnabled = true; refresher = options.GetRefresher(); From b3b42df24e53830920087ea2ba91748bbc15d3ef Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 14 Apr 2025 17:33:00 -0700 Subject: [PATCH 41/57] comments --- .../Integration/IntegrationTests.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 8bb736dd1..b9fcc6598 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -175,14 +175,8 @@ public async Task InitializeAsync() // Get connection string from the store AsyncPageable accessKeys = appConfigStore.GetKeysAsync(); - AppConfigurationStoreApiKey primaryKey = await accessKeys.FirstOrDefaultAsync(); - if (primaryKey == null) - { - throw new InvalidOperationException("Failed to retrieve access keys from App Configuration store."); - } - - _connectionString = primaryKey.ConnectionString; + _connectionString = (await accessKeys.FirstAsync()).ConnectionString; // Initialize the configuration client with the connection string _configClient = new ConfigurationClient(_connectionString); From 811aa7d026832544a204403cb10eecdabe5a296e Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 15 Apr 2025 10:43:36 -0700 Subject: [PATCH 42/57] comments --- .../Integration/IntegrationTests.cs | 78 ++----------------- 1 file changed, 7 insertions(+), 71 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index b9fcc6598..2290c322b 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -51,9 +51,6 @@ public class IntegrationTests : IAsyncLifetime ExcludeSharedTokenCacheCredential = true }); - /// - /// Class to hold test-specific key information - /// private class TestContext { public string KeyPrefix { get; set; } @@ -82,10 +79,6 @@ private class TestContext private readonly HashSet _createdSecretNames = new HashSet(); private readonly HashSet _createdSnapshotNames = new HashSet(); - /// - /// Gets the current subscription ID by reading from the JSON file created by the PowerShell script. - /// NOTE: The PowerShell script must be run manually before running the tests. - /// private string GetCurrentSubscriptionId() { string subscriptionIdFromEnv = Environment.GetEnvironmentVariable("AZURE_SUBSCRIPTION_ID"); @@ -111,26 +104,14 @@ private string GetCurrentSubscriptionId() return root.GetProperty("SubscriptionId").GetString(); } - /// - /// 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)}"; } - /// - /// Creates a snapshot with the given name containing the test context's settings - /// - private async Task CreateSnapshot(string snapshotName, TestContext testContext) + private async Task CreateSnapshot(string snapshotName, IEnumerable settingsToInclude) { - // Create a snapshot with the test keys - var settingsToInclude = new List - { - new ConfigurationSettingsFilter($"{testContext.KeyPrefix}:*") - }; - ConfigurationSnapshot snapshot = new ConfigurationSnapshot(settingsToInclude); snapshot.SnapshotComposition = SnapshotComposition.Key; @@ -144,9 +125,6 @@ private async Task CreateSnapshot(string snapshotName, TestContext testC return operation.Value.Name; } - /// - /// Initialize clients to connect to the existing App Configuration store and Key Vault. - /// public async Task InitializeAsync() { DefaultAzureCredential credential = _defaultAzureCredential; @@ -197,9 +175,6 @@ public async Task InitializeAsync() Console.WriteLine($"Successfully connected to App Configuration store '{AppConfigStoreName}' and Key Vault '{KeyVaultName}'"); } - /// - /// Cleans up stale resources that are older than the threshold - /// private async Task CleanupStaleResources() { Console.WriteLine($"Checking for stale resources older than {StaleResourceThreshold}..."); @@ -287,9 +262,6 @@ private async Task CleanupStaleResources() } } - /// - /// Clean up test artifacts but don't delete the actual resources - /// public async Task DisposeAsync() { var cleanupTasks = new List(); @@ -356,9 +328,6 @@ public async Task DisposeAsync() await CleanupStaleResources(); } - /// - /// Setup test-specific keys and settings - /// private async Task SetupTestKeys(string testName) { string keyPrefix = GetUniqueKeyPrefix(testName); @@ -1187,9 +1156,6 @@ public async Task HandlesFailoverOnStartup() Assert.Equal("InitialValue1", config[$"{testContext.KeyPrefix}:Setting1"]); } - /// - /// Test verifies that a snapshot can be created and loaded correctly - /// [Fact] public async Task LoadSnapshot_RetrievesValuesFromSnapshot() { @@ -1198,7 +1164,7 @@ public async Task LoadSnapshot_RetrievesValuesFromSnapshot() string snapshotName = $"snapshot-{testContext.KeyPrefix}"; // Create a snapshot with the test keys - await CreateSnapshot(snapshotName, testContext); + await CreateSnapshot(snapshotName, new List { new ConfigurationSettingsFilter(testContext.KeyPrefix + "*") }); // Update values after snapshot is taken to verify snapshot has original values await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting($"{testContext.KeyPrefix}:Setting1", "UpdatedAfterSnapshot")); @@ -1220,9 +1186,6 @@ public async Task LoadSnapshot_RetrievesValuesFromSnapshot() await _configClient.ArchiveSnapshotAsync(snapshotName); } - /// - /// Test verifies error handling when a snapshot doesn't exist - /// [Fact] public async Task LoadSnapshot_ThrowsException_WhenSnapshotDoesNotExist() { @@ -1246,9 +1209,6 @@ public async Task LoadSnapshot_ThrowsException_WhenSnapshotDoesNotExist() Assert.Contains(nonExistentSnapshotName, exception.Message); } - /// - /// Test verifies that multiple snapshots can be loaded in the same configuration - /// [Fact] public async Task LoadMultipleSnapshots_MergesConfigurationCorrectly() { @@ -1264,8 +1224,8 @@ await _configClient.SetConfigurationSettingAsync( string snapshotName2 = $"snapshot-{testContext2.KeyPrefix}"; // Create snapshots - await CreateSnapshot(snapshotName1, testContext1); - await CreateSnapshot(snapshotName2, testContext2); + await CreateSnapshot(snapshotName1, new List { new ConfigurationSettingsFilter(testContext1.KeyPrefix + "*") }); + await CreateSnapshot(snapshotName2, new List { new ConfigurationSettingsFilter(testContext2.KeyPrefix + "*") }); try { @@ -1292,9 +1252,6 @@ await _configClient.SetConfigurationSettingAsync( } } - /// - /// Test verifies that different snapshot composition types are handled correctly - /// [Fact] public async Task SnapshotCompositionTypes_AreHandledCorrectly() { @@ -1361,9 +1318,6 @@ public async Task SnapshotCompositionTypes_AreHandledCorrectly() } } - /// - /// Test verifies that snapshots work with feature flags - /// [Fact] public async Task SnapshotWithFeatureFlags_LoadsConfigurationCorrectly() { @@ -1421,9 +1375,6 @@ await _configClient.SetConfigurationSettingAsync( } } - /// - /// Test verifies call ordering of snapshots, select, and feature flags - /// [Fact] public async Task CallOrdering_SnapshotsWithSelectAndFeatureFlags() { @@ -1462,9 +1413,9 @@ await _configClient.SetConfigurationSettingAsync( string snapshot2 = $"snapshot-{secondContext.KeyPrefix}-2"; string snapshot3 = $"snapshot-{thirdContext.KeyPrefix}-3"; - await CreateSnapshot(snapshot1, mainContext); - await CreateSnapshot(snapshot2, secondContext); - await CreateSnapshot(snapshot3, thirdContext); + await CreateSnapshot(snapshot1, new List { new ConfigurationSettingsFilter(mainContext.KeyPrefix + "*") }); + await CreateSnapshot(snapshot2, new List { new ConfigurationSettingsFilter(secondContext.KeyPrefix + "*") }); + await CreateSnapshot(snapshot3, new List { new ConfigurationSettingsFilter(thirdContext.KeyPrefix + "*") }); try { @@ -1566,9 +1517,6 @@ await _configClient.SetConfigurationSettingAsync( } } - /// - /// Test verifies Key Vault references can be resolved - /// [Fact] public async Task KeyVaultReferences_ResolveCorrectly() { @@ -1589,9 +1537,6 @@ public async Task KeyVaultReferences_ResolveCorrectly() Assert.Equal("SecretValue", config[testContext.KeyVaultReferenceKey]); } - /// - /// Helper class to monitor Key Vault requests - /// private class HttpPipelineTransportWithRequestCount : HttpPipelineTransport { private readonly HttpClientTransport _innerTransport = new HttpClientTransport(); @@ -1667,9 +1612,6 @@ public async Task KeyVaultReference_UsesCache_DoesNotCallKeyVaultAgain() Assert.Equal(firstRequestCount, secondRequestCount); // No additional requests for the second access } - /// - /// Tests that different Key Vault references can have different refresh intervals. - /// [Fact] public async Task KeyVaultReference_DifferentRefreshIntervals() { @@ -1833,9 +1775,6 @@ await _configClient.SetConfigurationSettingAsync( Assert.Contains($"{RequestTracingConstants.FeatureManagementVersionKey}=4.0.0", requestInspector.CorrelationContextHeaders.Last()); } - /// - /// Helper class to track HTTP requests and extract correlation context headers - /// private class RequestInspectionHandler { public List CorrelationContextHeaders { get; } = new List(); @@ -1849,9 +1788,6 @@ public void InspectRequest(HttpMessage message) } } - /// - /// Custom HttpPipelineTransport that inspects requests before sending - /// private class HttpClientTransportWithRequestInspection : HttpClientTransport { private readonly RequestInspectionHandler _inspector; From 502ae9f2da0c697ea0906fcd4f8c9dceb6cfc9dc Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 25 Apr 2025 13:18:27 -0700 Subject: [PATCH 43/57] update copilot instructions --- .github/code-gen-instructions.md | 89 ++++++++++++++++++++++++++++++++ .github/copilot-instructions.md | 4 +- 2 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 .github/code-gen-instructions.md diff --git a/.github/code-gen-instructions.md b/.github/code-gen-instructions.md new file mode 100644 index 000000000..a542d2daa --- /dev/null +++ b/.github/code-gen-instructions.md @@ -0,0 +1,89 @@ +# AppConfiguration-DotnetProvider Coding Guidelines + +This document outlines coding guidelines for the Azure App Configuration .NET Provider repository. Follow these guidelines when generating or modifying code. + +## General Guidelines + +1. **Exception Handling**: + * Always catch specific exceptions and avoid catching the base `Exception` class in catch blocks. + * Throw specific exception types (e.g., `ArgumentNullException`, `FormatException`, custom exceptions) rather than generic `System.Exception`. + * Include the parameter name when throwing `ArgumentNullException` using `nameof()`. + +2. **Variable Declaration**: + * Never use `var` to declare a variable if the assignment doesn't include the type or the type isn't immediately obvious. + * Use explicit type names for fields, properties, method parameters, and return types. + * Use `var` only when the type is obvious from the right-hand side (e.g., `var user = new User();`). + +3. **Null Handling**: + * Validate arguments in public methods and constructors with explicit null checks. + * Use explicit `if (argument == null) throw new ArgumentNullException(nameof(argument));` checks at the beginning of methods/constructors. + * Avoid using the null-forgiving operator (`!`) unless absolutely necessary. + +4. **Asynchronous Programming**: + * All async methods should accept a `CancellationToken` as the last parameter. + * Pass the `cancellationToken` down the call stack to all subsequent asynchronous operations. + * Use `Task` or `Task` for asynchronous methods. + +5. **LINQ and Collections**: + * Prefer simple, readable LINQ queries. + * Break down complex LINQ queries into separate statements with intermediate variables. + * Use collection interfaces (e.g., `IList`, `IReadOnlyList`) in parameter and return types. + +6. **Resource Management**: + * Wrap `IDisposable` instances in `using` statements to ensure proper disposal. + * Implement `IDisposable` correctly if your class manages disposable objects. + +7. **Dependency Injection**: + * Use constructor injection for dependencies. + * Store injected dependencies in `private readonly` fields. + * Validate injected dependencies for null in the constructor. + +8. **Naming Conventions**: + * Use `PascalCase` for classes, interfaces, enums, methods, properties, and constants. + * Use `camelCase` for local variables and method parameters. + * Prefix private fields with an underscore (`_`). + * Define constants for error messages and other string literals. + +## AppConfiguration-Specific Guidelines + +1. **Feature Flag Handling**: + * Validate feature flag data structure before processing. + * Handle different feature flag schemas (Microsoft vs .NET) appropriately. + * Use proper error handling when parsing feature flags with clear error messages. + +2. **Configuration Key-Value Processing**: + * Follow adapter pattern for processing different configuration types. + * Properly handle key-value pairs with appropriate content type detection. + * Use `KeyValuePair` for configuration values. + +3. **Content Type Handling**: + * Validate content types before processing. + * Use appropriate content type constants. + * Check content type using extension methods like `IsFeatureFlag()`. + +4. **JSON Parsing**: + * Use `Utf8JsonReader` for performance-critical JSON parsing. + * Validate JSON structure and provide clear error messages for malformed input. + * Handle JSON token types appropriately with proper error handling. + +5. **Refresh Mechanisms**: + * Implement proper configuration refresh patterns. + * Use sentinel-based refresh mechanisms when appropriate. + * Handle refresh failures gracefully. + +## Performance Considerations + +1. **String Handling**: + * Use `StringBuilder` for concatenating multiple strings. + * Define string constants for recurring strings. + * Use string interpolation instead of string concatenation when appropriate. + +2. **Collections**: + * Initialize collections with estimated capacity when possible. + * Use appropriate collection types for the use case (e.g., `List`, `Dictionary`). + * Avoid unnecessary collection allocations. + +3. **Memory Management**: + * Use `Span` and `ReadOnlySpan` for high-performance scenarios. + * Minimize allocations in performance-critical paths. + * Be mindful of closure allocations in LINQ and lambdas. \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 05034c448..ea3523ffa 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,3 +1,3 @@ -We always catch specific exceptions and avoid catching the base Exception class in catch blocks. +This is the Azure App Configuration .NET Provider codebase. The service abides by coding guidelines specified in `github\code-gen-instructions.md`file. -We never use `var` to declare a variable if the assignment does not include the type of the variable, or the type is not immediately obvious. \ No newline at end of file +When suggesting code changes, do not modify the files directly. Instead, provide a detailed explanation of the changes you would make and ask for confirmation before editing the files. You may create markdown files to demonstrate the changes you would like to make. From 2c8a63007d5deeab36f98010de947cfa7f9bd854 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 25 Apr 2025 15:40:57 -0700 Subject: [PATCH 44/57] update copilot instructions --- .github/code-gen-instructions.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/code-gen-instructions.md b/.github/code-gen-instructions.md index a542d2daa..6f000773e 100644 --- a/.github/code-gen-instructions.md +++ b/.github/code-gen-instructions.md @@ -44,6 +44,10 @@ This document outlines coding guidelines for the Azure App Configuration .NET Pr * Prefix private fields with an underscore (`_`). * Define constants for error messages and other string literals. +9. **Comments**: + * Only add comments when it's not obvious what the code is doing. For example, if a variable name is already fairly descriptive, a comment isn't needed explaining its name. + * Add summary comments to public classes and members of those classes. + ## AppConfiguration-Specific Guidelines 1. **Feature Flag Handling**: From 78e73f79d893aa4c68763a5d4ec6ac0267fb656a Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 29 Apr 2025 19:02:23 -0700 Subject: [PATCH 45/57] fix try blocks to only surround throwing code --- .../Integration/IntegrationTests.cs | 116 +++++++++--------- 1 file changed, 57 insertions(+), 59 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 2290c322b..18fdd17f1 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -266,63 +266,52 @@ public async Task DisposeAsync() { var cleanupTasks = new List(); - try + // Clean up all configuration settings created by tests + foreach (string key in _createdConfigKeys) { - // Clean up all configuration settings created by tests - foreach (string key in _createdConfigKeys) + try { - try - { - cleanupTasks.Add(_configClient.DeleteConfigurationSettingAsync(key)); - } - catch (RequestFailedException ex) - { - Console.WriteLine($"Failed to delete configuration setting {key}: {ex.Message}"); - } + cleanupTasks.Add(_configClient.DeleteConfigurationSettingAsync(key)); + } + catch (RequestFailedException ex) + { + Console.WriteLine($"Failed to delete configuration setting {key}: {ex.Message}"); + } + } + + // Clean up all snapshots created by tests + foreach (string snapshotName in _createdSnapshotNames) + { + try + { + cleanupTasks.Add(_configClient.ArchiveSnapshotAsync(snapshotName)); + } + catch (RequestFailedException ex) + { + Console.WriteLine($"Failed to delete snapshot {snapshotName}: {ex.Message}"); } + } - // Clean up all snapshots created by tests - foreach (string snapshotName in _createdSnapshotNames) + // Clean up test-specific secrets in Key Vault + if (_secretClient != null) + { + foreach (string secretName in _createdSecretNames) { try { - cleanupTasks.Add(_configClient.ArchiveSnapshotAsync(snapshotName)); + cleanupTasks.Add(_secretClient.StartDeleteSecretAsync(secretName)); } catch (RequestFailedException ex) { - Console.WriteLine($"Failed to delete snapshot {snapshotName}: {ex.Message}"); - } - } - - // Clean up test-specific secrets in Key Vault - if (_secretClient != null) - { - foreach (string secretName in _createdSecretNames) - { - try - { - cleanupTasks.Add(_secretClient.StartDeleteSecretAsync(secretName)); - } - catch (RequestFailedException ex) - { - Console.WriteLine($"Failed to delete secret {secretName}: {ex.Message}"); - } + Console.WriteLine($"Failed to delete secret {secretName}: {ex.Message}"); } } + } - // Wait for all cleanup tasks to complete - await Task.WhenAll(cleanupTasks); + // Wait for all cleanup tasks to complete + await Task.WhenAll(cleanupTasks); - Console.WriteLine($"Cleaned up {_createdConfigKeys.Count} configuration settings, {_createdSnapshotNames.Count} snapshots, and {_createdSecretNames.Count} secrets"); - } - catch (RequestFailedException ex) - { - Console.WriteLine($"Error during resource cleanup: {ex.Message}"); - } - catch (InvalidOperationException ex) - { - Console.WriteLine($"Operation error during test cleanup: {ex.Message}"); - } + Console.WriteLine($"Cleaned up {_createdConfigKeys.Count} configuration settings, {_createdSnapshotNames.Count} snapshots, and {_createdSecretNames.Count} secrets"); // Clean up stale resources on dispose await CleanupStaleResources(); @@ -364,22 +353,6 @@ private async Task SetupTestKeys(string testName) { // Create a secret in Key Vault await _secretClient.SetSecretAsync(secretName, secretValue); - // Track the created secret for cleanup - _createdSecretNames.Add(secretName); - - // Create a Key Vault reference in App Configuration - string keyVaultUri = $"{_keyVaultEndpoint}secrets/{secretName}"; - string keyVaultRefValue = @$"{{""uri"":""{keyVaultUri}""}}"; - - ConfigurationSetting keyVaultRefSetting = ConfigurationModelFactory.ConfigurationSetting( - keyVaultReferenceKey, - keyVaultRefValue, - label: KeyVaultReferenceLabel, - contentType: KeyVaultConstants.ContentType); - - await _configClient.SetConfigurationSettingAsync(keyVaultRefSetting); - // Track the created key reference for cleanup - _createdConfigKeys.Add(keyVaultReferenceKey); } catch (RequestFailedException ex) { @@ -391,6 +364,31 @@ private async Task SetupTestKeys(string testName) Console.WriteLine($"Invalid Key Vault operation: {ex.Message}"); // Continue without Key Vault reference if it fails } + + // Track the created secret for cleanup + _createdSecretNames.Add(secretName); + + // Create a Key Vault reference in App Configuration + string keyVaultUri = $"{_keyVaultEndpoint}secrets/{secretName}"; + string keyVaultRefValue = @$"{{""uri"":""{keyVaultUri}""}}"; + + ConfigurationSetting keyVaultRefSetting = ConfigurationModelFactory.ConfigurationSetting( + keyVaultReferenceKey, + keyVaultRefValue, + label: KeyVaultReferenceLabel, + contentType: KeyVaultConstants.ContentType); + + try + { + await _configClient.SetConfigurationSettingAsync(keyVaultRefSetting); + } + catch (RequestFailedException ex) + { + Console.WriteLine($"Error setting up Key Vault reference: {ex.Message}"); + } + + // Track the created key reference for cleanup + _createdConfigKeys.Add(keyVaultReferenceKey); } return new TestContext From dc4db90db4a5d2e2e8ac9b09c7a59195be0eb863 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 30 Apr 2025 12:57:29 -0700 Subject: [PATCH 46/57] fix setuptestkeys, simplify comments and setup code --- .../Integration/IntegrationTests.cs | 224 ++++++++---------- 1 file changed, 102 insertions(+), 122 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 18fdd17f1..1bf38cda5 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -19,6 +19,7 @@ using System.IO; using System.Linq; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -40,6 +41,10 @@ public class IntegrationTests : IAsyncLifetime private static readonly TimeSpan StaleResourceThreshold = TimeSpan.FromHours(3); private const string KeyVaultReferenceLabel = "KeyVaultRef"; + // Content type constants + private const string FeatureFlagContentType = FeatureManagementConstants.ContentType + ";charset=utf-8"; + private const string JsonContentType = "application/json"; + // Fixed resource names - already existing private const string AppConfigStoreName = "appconfig-dotnetprovider-integrationtest"; private const string KeyVaultName = "keyvault-dotnetprovider"; @@ -57,21 +62,15 @@ private class TestContext public string SentinelKey { get; set; } public string FeatureFlagKey { get; set; } public string KeyVaultReferenceKey { get; set; } - public string SecretName { get; set; } public string SecretValue { get; set; } } - // Client for direct manipulation of the store private ConfigurationClient _configClient; - // Client for Key Vault operations private SecretClient _secretClient; - // Connection string for the store private string _connectionString; - // Endpoints for the resources - private Uri _appConfigEndpoint; private Uri _keyVaultEndpoint; // Track resources created by tests for cleanup @@ -110,14 +109,17 @@ private string GetUniqueKeyPrefix(string testName) return $"{TestKeyPrefix}-{testName}-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; } - private async Task CreateSnapshot(string snapshotName, IEnumerable settingsToInclude) + private async Task CreateSnapshot(string snapshotName, IEnumerable settingsToInclude, CancellationToken cancellationToken = default) { ConfigurationSnapshot snapshot = new ConfigurationSnapshot(settingsToInclude); snapshot.SnapshotComposition = SnapshotComposition.Key; - // Create the snapshot - CreateSnapshotOperation operation = await _configClient.CreateSnapshotAsync(WaitUntil.Completed, snapshotName, snapshot); + CreateSnapshotOperation operation = await _configClient.CreateSnapshotAsync( + WaitUntil.Completed, + snapshotName, + snapshot, + cancellationToken); // Track created snapshot for cleanup _createdSnapshotNames.Add(snapshotName); @@ -130,18 +132,15 @@ public async Task InitializeAsync() DefaultAzureCredential credential = _defaultAzureCredential; string subscriptionId = GetCurrentSubscriptionId(); - // Initialize Azure Resource Manager client var armClient = new ArmClient(credential); SubscriptionResource subscription = armClient.GetSubscriptionResource(new ResourceIdentifier($"/subscriptions/{subscriptionId}")); ResourceGroupResource resourceGroup = await subscription.GetResourceGroups().GetAsync(ResourceGroupName); AppConfigurationStoreResource appConfigStore = null; - KeyVaultResource keyVault = null; try { - // Get App Configuration store directly using the resource group and store name appConfigStore = await resourceGroup.GetAppConfigurationStores().GetAsync(AppConfigStoreName); } catch (RequestFailedException ex) when (ex.Status == 404) @@ -149,27 +148,27 @@ public async Task InitializeAsync() throw new InvalidOperationException($"App Configuration store '{AppConfigStoreName}' not found in resource group '{ResourceGroupName}'. Please create it before running tests.", ex); } - _appConfigEndpoint = new Uri(appConfigStore.Data.Endpoint); - - // Get connection string from the store AsyncPageable accessKeys = appConfigStore.GetKeysAsync(); _connectionString = (await accessKeys.FirstAsync()).ConnectionString; - // Initialize the configuration client with the connection string _configClient = new ConfigurationClient(_connectionString); + KeyVaultResource keyVault = null; + // Find and initialize Key Vault - look in the same resource group keyVault = await resourceGroup.GetKeyVaults().GetAsync(KeyVaultName); if (keyVault == null) { - throw new InvalidOperationException($"Key Vault '{KeyVaultName}' not found in subscription {subscriptionId}. Please create it before running tests."); + throw new InvalidOperationException( + $"Key Vault '{KeyVaultName}' not found in subscription {subscriptionId}. " + + "This resource is required for integration tests. " + + "Please create the Key Vault with the appropriate permissions before running tests."); } _keyVaultEndpoint = keyVault.Data.Properties.VaultUri; - // Create a Secret Client for the vault _secretClient = new SecretClient(_keyVaultEndpoint, credential); Console.WriteLine($"Successfully connected to App Configuration store '{AppConfigStoreName}' and Key Vault '{KeyVaultName}'"); @@ -181,19 +180,18 @@ private async Task CleanupStaleResources() var cutoffTime = DateTimeOffset.UtcNow.Subtract(StaleResourceThreshold); var cleanupTasks = new List(); + // Clean up stale configuration settings, snapshots, and Key Vault secrets try { - // Clean up stale configuration settings int staleConfigCount = 0; var configSettingsToCleanup = new List(); - // Get all test key-values - AsyncPageable configSettings = _configClient.GetConfigurationSettingsAsync(new SettingSelector + AsyncPageable allSettings = _configClient.GetConfigurationSettingsAsync(new SettingSelector { - KeyFilter = TestKeyPrefix + "*" + KeyFilter = "*" + TestKeyPrefix + "*" // This will match both regular keys and feature flags }); - await foreach (ConfigurationSetting setting in configSettings) + await foreach (ConfigurationSetting setting in allSettings) { // Check if the setting is older than the threshold if (setting.LastModified < cutoffTime) @@ -203,28 +201,11 @@ private async Task CleanupStaleResources() } } - // Clean up stale feature flags - AsyncPageable featureFlagSettings = _configClient.GetConfigurationSettingsAsync(new SettingSelector - { - KeyFilter = ".appconfig.featureflag/" + TestKeyPrefix + "*" - }); - - await foreach (ConfigurationSetting setting in featureFlagSettings) - { - if (setting.LastModified < cutoffTime) - { - configSettingsToCleanup.Add(setting); - staleConfigCount++; - } - } - - // Delete stale configuration settings foreach (ConfigurationSetting setting in configSettingsToCleanup) { cleanupTasks.Add(_configClient.DeleteConfigurationSettingAsync(setting.Key, setting.Label)); } - // Clean up stale snapshots int staleSnapshotCount = 0; AsyncPageable snapshots = _configClient.GetSnapshotsAsync(new SnapshotSelector()); await foreach (ConfigurationSnapshot snapshot in snapshots) @@ -236,7 +217,6 @@ private async Task CleanupStaleResources() } } - // Clean up stale Key Vault secrets int staleSecretCount = 0; if (_secretClient != null) { @@ -266,7 +246,7 @@ public async Task DisposeAsync() { var cleanupTasks = new List(); - // Clean up all configuration settings created by tests + // Clean up stale configuration settings, snapshots, and Key Vault secrets foreach (string key in _createdConfigKeys) { try @@ -279,7 +259,6 @@ public async Task DisposeAsync() } } - // Clean up all snapshots created by tests foreach (string snapshotName in _createdSnapshotNames) { try @@ -292,7 +271,6 @@ public async Task DisposeAsync() } } - // Clean up test-specific secrets in Key Vault if (_secretClient != null) { foreach (string secretName in _createdSecretNames) @@ -313,11 +291,20 @@ public async Task DisposeAsync() Console.WriteLine($"Cleaned up {_createdConfigKeys.Count} configuration settings, {_createdSnapshotNames.Count} snapshots, and {_createdSecretNames.Count} secrets"); - // Clean up stale resources on dispose await CleanupStaleResources(); } - private async Task SetupTestKeys(string testName) + [Flags] + private enum TestDataTypes + { + None = 0, + KeyValues = 1, + FeatureFlags = 2, + KeyVaultReferences = 4, + All = KeyValues | FeatureFlags | KeyVaultReferences + } + + private async Task SetupTestKeys(string testName, TestDataTypes dataTypesToCreate = TestDataTypes.All) { string keyPrefix = GetUniqueKeyPrefix(testName); string sentinelKey = $"{keyPrefix}:Sentinel"; @@ -326,32 +313,42 @@ private async Task SetupTestKeys(string testName) string secretValue = "SecretValue"; string keyVaultReferenceKey = $"{keyPrefix}:KeyVaultRef"; - // Create test-specific settings - var testSettings = new List + // Create test-specific settings if requested + if (dataTypesToCreate.HasFlag(TestDataTypes.KeyValues)) { - new ConfigurationSetting($"{keyPrefix}:Setting1", "InitialValue1"), - new ConfigurationSetting($"{keyPrefix}:Setting2", "InitialValue2"), - new ConfigurationSetting(sentinelKey, "Initial"), - ConfigurationModelFactory.ConfigurationSetting( + var testSettings = new List + { + new ConfigurationSetting($"{keyPrefix}:Setting1", "InitialValue1"), + new ConfigurationSetting($"{keyPrefix}:Setting2", "InitialValue2"), + new ConfigurationSetting(sentinelKey, "Initial") + }; + + foreach (ConfigurationSetting setting in testSettings) + { + await _configClient.SetConfigurationSettingAsync(setting); + // Track the created key for cleanup + _createdConfigKeys.Add(setting.Key); + } + } + + // Create feature flag if requested + if (dataTypesToCreate.HasFlag(TestDataTypes.FeatureFlags)) + { + var featureFlagSetting = ConfigurationModelFactory.ConfigurationSetting( featureFlagKey, @"{""id"":""" + keyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":false}", - contentType: FeatureManagementConstants.ContentType + ";charset=utf-8") - }; + contentType: FeatureFlagContentType); - // Add test-specific settings to the store - foreach (ConfigurationSetting setting in testSettings) - { - await _configClient.SetConfigurationSettingAsync(setting); + await _configClient.SetConfigurationSettingAsync(featureFlagSetting); // Track the created key for cleanup - _createdConfigKeys.Add(setting.Key); + _createdConfigKeys.Add(featureFlagSetting.Key); } - // If Key Vault is available, add a test secret and reference - if (_secretClient != null) + // Create KeyVault reference if requested + if (dataTypesToCreate.HasFlag(TestDataTypes.KeyVaultReferences) && _secretClient != null) { try { - // Create a secret in Key Vault await _secretClient.SetSecretAsync(secretName, secretValue); } catch (RequestFailedException ex) @@ -368,7 +365,6 @@ private async Task SetupTestKeys(string testName) // Track the created secret for cleanup _createdSecretNames.Add(secretName); - // Create a Key Vault reference in App Configuration string keyVaultUri = $"{_keyVaultEndpoint}secrets/{secretName}"; string keyVaultRefValue = @$"{{""uri"":""{keyVaultUri}""}}"; @@ -397,7 +393,6 @@ private async Task SetupTestKeys(string testName) SentinelKey = sentinelKey, FeatureFlagKey = featureFlagKey, KeyVaultReferenceKey = keyVaultReferenceKey, - SecretName = secretName, SecretValue = secretValue }; } @@ -406,7 +401,7 @@ private async Task SetupTestKeys(string testName) public async Task LoadConfiguration_RetrievesValuesFromAppConfiguration() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("BasicConfig"); + TestContext testContext = await SetupTestKeys("BasicConfig", TestDataTypes.KeyValues); // Act var config = new ConfigurationBuilder() @@ -426,7 +421,7 @@ public async Task LoadConfiguration_RetrievesValuesFromAppConfiguration() public async Task RefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("UpdatesConfig"); + TestContext testContext = await SetupTestKeys("UpdatesConfig", TestDataTypes.KeyValues); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() @@ -465,7 +460,7 @@ public async Task RefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() public async Task RegisterAll_RefreshesAllKeys() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("RefreshesAllKeys"); + TestContext testContext = await SetupTestKeys("RefreshesAllKeys", TestDataTypes.KeyValues); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() @@ -508,7 +503,7 @@ public async Task RegisterAll_RefreshesAllKeys() public async Task RefreshAsync_SentinelKeyUnchanged() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("SentinelUnchanged"); + TestContext testContext = await SetupTestKeys("SentinelUnchanged", TestDataTypes.KeyValues); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() @@ -546,15 +541,13 @@ public async Task RefreshAsync_SentinelKeyUnchanged() [Fact] public async Task RefreshAsync_RefreshesFeatureFlags_WhenConfigured() { - TestContext testContext = await SetupTestKeys("FeatureFlagRefresh"); + TestContext testContext = await SetupTestKeys("FeatureFlagRefresh", TestDataTypes.FeatureFlags); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { options.Connect(_connectionString); - // Select the key prefix to include all test keys - options.Select($"{testContext.KeyPrefix}:*"); // Configure feature flags with the correct ID pattern options.UseFeatureFlags(featureFlagOptions => @@ -563,12 +556,6 @@ public async Task RefreshAsync_RefreshesFeatureFlags_WhenConfigured() featureFlagOptions.SetRefreshInterval(TimeSpan.FromSeconds(1)); }); - options.ConfigureRefresh(refresh => - { - refresh.Register(testContext.SentinelKey) - .SetRefreshInterval(TimeSpan.FromSeconds(1)); - }); - refresher = options.GetRefresher(); }) .Build(); @@ -581,10 +568,7 @@ await _configClient.SetConfigurationSettingAsync( ConfigurationModelFactory.ConfigurationSetting( testContext.FeatureFlagKey, @"{""id"":""" + testContext.KeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":true}", - contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); - - // Update the sentinel key to trigger refresh - await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(testContext.SentinelKey, "Updated")); + contentType: FeatureFlagContentType)); // Wait for cache to expire await Task.Delay(TimeSpan.FromSeconds(2)); @@ -599,7 +583,7 @@ await _configClient.SetConfigurationSettingAsync( [Fact] public async Task UseFeatureFlags_WithClientFiltersAndConditions() { - TestContext testContext = await SetupTestKeys("FeatureFlagFilters"); + TestContext testContext = await SetupTestKeys("FeatureFlagFilters", TestDataTypes.FeatureFlags); // Create a feature flag with complex conditions await _configClient.SetConfigurationSettingAsync( @@ -627,7 +611,7 @@ await _configClient.SetConfigurationSettingAsync( ] } }", - contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + contentType: FeatureFlagContentType)); var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => @@ -650,8 +634,8 @@ await _configClient.SetConfigurationSettingAsync( [Fact] public async Task MultipleProviders_LoadAndRefresh() { - TestContext testContext1 = await SetupTestKeys("MultiProviderTest1"); - TestContext testContext2 = await SetupTestKeys("MultiProviderTest2"); + TestContext testContext1 = await SetupTestKeys("MultiProviderTest1", TestDataTypes.KeyValues); + TestContext testContext2 = await SetupTestKeys("MultiProviderTest2", TestDataTypes.KeyValues); IConfigurationRefresher refresher1 = null; IConfigurationRefresher refresher2 = null; @@ -704,13 +688,13 @@ public async Task MultipleProviders_LoadAndRefresh() [Fact] public async Task FeatureFlag_WithVariants() { - TestContext testContext = await SetupTestKeys("FeatureFlagVariants"); + TestContext testContext = await SetupTestKeys("FeatureFlagVariants", TestDataTypes.FeatureFlags); await _configClient.SetConfigurationSettingAsync( ConfigurationModelFactory.ConfigurationSetting( testContext.FeatureFlagKey, @"{""id"":""" + testContext.KeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":true}", - contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + contentType: FeatureFlagContentType)); // Create a feature flag with variants await _configClient.SetConfigurationSettingAsync( @@ -739,7 +723,7 @@ await _configClient.SetConfigurationSettingAsync( ""default_when_enabled"": ""MediumSize"" } }", - contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + contentType: FeatureFlagContentType)); var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => @@ -766,7 +750,7 @@ await _configClient.SetConfigurationSettingAsync( [Fact] public async Task JsonContentType_LoadsAndFlattensHierarchicalData() { - TestContext testContext = await SetupTestKeys("JsonContent"); + TestContext testContext = await SetupTestKeys("JsonContent", TestDataTypes.KeyValues); // Create a complex JSON structure string jsonKey = $"{testContext.KeyPrefix}:JsonConfig"; @@ -787,7 +771,7 @@ await _configClient.SetConfigurationSettingAsync( ""providers"": [""Console"", ""Debug"", ""EventLog""] } }", - contentType: "application/json")); + contentType: JsonContentType)); var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => @@ -812,7 +796,7 @@ await _configClient.SetConfigurationSettingAsync( public async Task MethodOrderingDoesNotAffectConfiguration() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("MethodOrdering"); + TestContext testContext = await SetupTestKeys("MethodOrdering", TestDataTypes.KeyValues | TestDataTypes.FeatureFlags); // Add an additional feature flag for testing await _configClient.SetConfigurationSettingAsync( @@ -826,7 +810,7 @@ await _configClient.SetConfigurationSettingAsync( ""client_filters"": [] } }", - contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + contentType: FeatureFlagContentType)); // Add a section-based setting await _configClient.SetConfigurationSettingAsync( @@ -954,7 +938,7 @@ await _configClient.SetConfigurationSettingAsync( ""client_filters"": [] } }", - contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + contentType: FeatureFlagContentType)); // Update the sentinel key to trigger refresh await _configClient.SetConfigurationSettingAsync( @@ -987,7 +971,7 @@ await _configClient.SetConfigurationSettingAsync( public async Task RegisterWithRefreshAllAndRegisterAll_BehaveIdentically() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("RefreshEquivalency"); + TestContext testContext = await SetupTestKeys("RefreshEquivalency", TestDataTypes.KeyValues | TestDataTypes.FeatureFlags); // Add another feature flag for testing string secondFeatureFlagKey = $".appconfig.featureflag/{testContext.KeyPrefix}Feature2"; @@ -995,7 +979,7 @@ await _configClient.SetConfigurationSettingAsync( ConfigurationModelFactory.ConfigurationSetting( secondFeatureFlagKey, @"{""id"":""" + testContext.KeyPrefix + @"Feature2"",""description"":""Second test feature"",""enabled"":false}", - contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + contentType: FeatureFlagContentType)); // Create two separate configuration builders with different refresh methods // First configuration uses Register with refreshAll: true @@ -1060,13 +1044,13 @@ await _configClient.SetConfigurationSettingAsync( ConfigurationModelFactory.ConfigurationSetting( testContext.FeatureFlagKey, @"{""id"":""" + testContext.KeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":true}", - contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + contentType: FeatureFlagContentType)); await _configClient.SetConfigurationSettingAsync( ConfigurationModelFactory.ConfigurationSetting( secondFeatureFlagKey, @"{""id"":""" + testContext.KeyPrefix + @"Feature2"",""description"":""Second test feature"",""enabled"":true}", - contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + contentType: FeatureFlagContentType)); // Update the sentinel key to trigger refresh await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(testContext.SentinelKey, "Updated")); @@ -1121,7 +1105,7 @@ await _configClient.SetConfigurationSettingAsync( public async Task HandlesFailoverOnStartup() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("FailoverStartup"); + TestContext testContext = await SetupTestKeys("FailoverStartup", TestDataTypes.KeyValues); IConfigurationRefresher refresher = null; string connectionString = _connectionString; @@ -1158,7 +1142,7 @@ public async Task HandlesFailoverOnStartup() public async Task LoadSnapshot_RetrievesValuesFromSnapshot() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("SnapshotTest"); + TestContext testContext = await SetupTestKeys("SnapshotTest", TestDataTypes.KeyValues); string snapshotName = $"snapshot-{testContext.KeyPrefix}"; // Create a snapshot with the test keys @@ -1188,7 +1172,7 @@ public async Task LoadSnapshot_RetrievesValuesFromSnapshot() public async Task LoadSnapshot_ThrowsException_WhenSnapshotDoesNotExist() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("NonExistentSnapshotTest"); + TestContext testContext = await SetupTestKeys("NonExistentSnapshotTest", TestDataTypes.KeyValues); string nonExistentSnapshotName = $"snapshot-does-not-exist-{Guid.NewGuid()}"; // Act & Assert - Loading a non-existent snapshot should throw @@ -1211,8 +1195,8 @@ public async Task LoadSnapshot_ThrowsException_WhenSnapshotDoesNotExist() public async Task LoadMultipleSnapshots_MergesConfigurationCorrectly() { // Arrange - Setup test-specific keys for two separate snapshots - TestContext testContext1 = await SetupTestKeys("SnapshotMergeTest1"); - TestContext testContext2 = await SetupTestKeys("SnapshotMergeTest2"); + TestContext testContext1 = await SetupTestKeys("SnapshotMergeTest1", TestDataTypes.KeyValues); + TestContext testContext2 = await SetupTestKeys("SnapshotMergeTest2", TestDataTypes.KeyValues); // Create specific values for second snapshot await _configClient.SetConfigurationSettingAsync( @@ -1254,7 +1238,7 @@ await _configClient.SetConfigurationSettingAsync( public async Task SnapshotCompositionTypes_AreHandledCorrectly() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("SnapshotCompositionTest"); + TestContext testContext = await SetupTestKeys("SnapshotCompositionTest", TestDataTypes.KeyValues); string keyOnlySnapshotName = $"snapshot-key-{testContext.KeyPrefix}"; string invalidCompositionSnapshotName = $"snapshot-invalid-{testContext.KeyPrefix}"; @@ -1320,7 +1304,7 @@ public async Task SnapshotCompositionTypes_AreHandledCorrectly() public async Task SnapshotWithFeatureFlags_LoadsConfigurationCorrectly() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("SnapshotFeatureFlagTest"); + TestContext testContext = await SetupTestKeys("SnapshotFeatureFlagTest", TestDataTypes.FeatureFlags); string snapshotName = $"snapshot-ff-{testContext.KeyPrefix}"; // Update the feature flag to be enabled before creating the snapshot @@ -1328,12 +1312,11 @@ await _configClient.SetConfigurationSettingAsync( ConfigurationModelFactory.ConfigurationSetting( testContext.FeatureFlagKey, @"{""id"":""" + testContext.KeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":true}", - contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + contentType: FeatureFlagContentType)); // Create a snapshot with the test keys var settingsToInclude = new List { - new ConfigurationSettingsFilter($"{testContext.KeyPrefix}:*"), new ConfigurationSettingsFilter($".appconfig.featureflag/{testContext.KeyPrefix}*") }; @@ -1349,7 +1332,7 @@ await _configClient.SetConfigurationSettingAsync( ConfigurationModelFactory.ConfigurationSetting( testContext.FeatureFlagKey, @"{""id"":""" + testContext.KeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":false}", - contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + contentType: FeatureFlagContentType)); try { @@ -1377,9 +1360,9 @@ await _configClient.SetConfigurationSettingAsync( public async Task CallOrdering_SnapshotsWithSelectAndFeatureFlags() { // Arrange - Setup test-specific keys for multiple snapshots - TestContext mainContext = await SetupTestKeys("SnapshotOrdering"); - TestContext secondContext = await SetupTestKeys("SnapshotOrdering2"); - TestContext thirdContext = await SetupTestKeys("SnapshotOrdering3"); + TestContext mainContext = await SetupTestKeys("SnapshotOrdering", TestDataTypes.KeyValues | TestDataTypes.FeatureFlags); + TestContext secondContext = await SetupTestKeys("SnapshotOrdering2", TestDataTypes.KeyValues | TestDataTypes.FeatureFlags); + TestContext thirdContext = await SetupTestKeys("SnapshotOrdering3", TestDataTypes.KeyValues | TestDataTypes.FeatureFlags); // Create specific values for each snapshot await _configClient.SetConfigurationSettingAsync( @@ -1397,14 +1380,14 @@ await _configClient.SetConfigurationSettingAsync( ConfigurationModelFactory.ConfigurationSetting( secondFeatureFlagKey, @"{""id"":""" + mainContext.KeyPrefix + @"Feature2"",""description"":""Second test feature"",""enabled"":true}", - contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + contentType: FeatureFlagContentType)); string thirdFeatureFlagKey = $".appconfig.featureflag/{secondContext.KeyPrefix}Feature"; await _configClient.SetConfigurationSettingAsync( ConfigurationModelFactory.ConfigurationSetting( thirdFeatureFlagKey, @"{""id"":""" + secondContext.KeyPrefix + @"Feature"",""description"":""Third test feature"",""enabled"":true}", - contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + contentType: FeatureFlagContentType)); // Create snapshots string snapshot1 = $"snapshot-{mainContext.KeyPrefix}-1"; @@ -1519,7 +1502,7 @@ await _configClient.SetConfigurationSettingAsync( public async Task KeyVaultReferences_ResolveCorrectly() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("KeyVaultReference"); + TestContext testContext = await SetupTestKeys("KeyVaultReference", TestDataTypes.KeyVaultReferences); // Act - Create configuration with Key Vault support var config = new ConfigurationBuilder() @@ -1570,7 +1553,7 @@ public override ValueTask ProcessAsync(HttpMessage message) public async Task KeyVaultReference_UsesCache_DoesNotCallKeyVaultAgain() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("KeyVaultCacheTest"); + TestContext testContext = await SetupTestKeys("KeyVaultCacheTest", TestDataTypes.KeyVaultReferences); // Create a monitoring client to track calls to Key Vault int requestCount = 0; @@ -1614,7 +1597,7 @@ public async Task KeyVaultReference_UsesCache_DoesNotCallKeyVaultAgain() public async Task KeyVaultReference_DifferentRefreshIntervals() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("KeyVaultDifferentIntervals"); + TestContext testContext = await SetupTestKeys("KeyVaultDifferentIntervals", TestDataTypes.KeyVaultReferences); IConfigurationRefresher refresher = null; // Create a secret in Key Vault with short refresh interval @@ -1659,11 +1642,6 @@ await _configClient.SetConfigurationSettingAsync( kv.SetSecretRefreshInterval(kvRefKey1, TimeSpan.FromSeconds(60)); // Short interval kv.SetSecretRefreshInterval(kvRefKey2, TimeSpan.FromDays(1)); // Long interval }); - options.ConfigureRefresh(refresh => - { - refresh.Register(testContext.SentinelKey, refreshAll: true) - .SetRefreshInterval(TimeSpan.FromSeconds(1)); - }); refresher = options.GetRefresher(); }) @@ -1728,10 +1706,12 @@ await _configClient.SetConfigurationSettingAsync( ] } }", - contentType: FeatureManagementConstants.ContentType + ";charset=utf-8")); + contentType: FeatureFlagContentType)); IConfigurationRefresher refresher = null; + using HttpClientTransportWithRequestInspection transportWithRequestInspection = new HttpClientTransportWithRequestInspection(requestInspector); + var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { @@ -1739,7 +1719,7 @@ await _configClient.SetConfigurationSettingAsync( options.Select($"{testContext.KeyPrefix}:*"); options.ConfigureClientOptions(clientOptions => { - clientOptions.Transport = new HttpClientTransportWithRequestInspection(requestInspector); + clientOptions.Transport = transportWithRequestInspection; }); options.ConfigureKeyVault(kv => kv.SetCredential(_defaultAzureCredential)); options.UseFeatureFlags(); From eb081bad74ced040c984fd3c1544c6736152a847 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 13 May 2025 13:56:27 -0700 Subject: [PATCH 47/57] tagfilters integration test in progress --- .../AzureAppConfigurationOptions.cs | 2 +- tests/Tests.AzureAppConfiguration/{ => Unit}/TagFiltersTests.cs | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/Tests.AzureAppConfiguration/{ => Unit}/TagFiltersTests.cs (100%) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 1e79daba2..bb48372ab 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -546,7 +546,7 @@ public AzureAppConfigurationOptions ConfigureStartupOptions(Action Date: Tue, 13 May 2025 14:11:24 -0700 Subject: [PATCH 48/57] tagfilters integration test in progress --- .../AzureAppConfigurationOptions.cs | 2 +- .../Integration/IntegrationTests.cs | 506 ++++++++++++++---- 2 files changed, 388 insertions(+), 120 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index bb48372ab..1e79daba2 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -546,7 +546,7 @@ public AzureAppConfigurationOptions ConfigureStartupOptions(Action _createdConfigKeys = new HashSet(); - private readonly HashSet _createdSecretNames = new HashSet(); - private readonly HashSet _createdSnapshotNames = new HashSet(); - private string GetCurrentSubscriptionId() { string subscriptionIdFromEnv = Environment.GetEnvironmentVariable("AZURE_SUBSCRIPTION_ID"); @@ -121,9 +116,6 @@ private async Task CreateSnapshot(string snapshotName, IEnumerable(); @@ -233,6 +226,7 @@ private async Task CleanupStaleResources() // Wait for all cleanup tasks to complete await Task.WhenAll(cleanupTasks); + Console.WriteLine($"Cleaned up {staleConfigCount} stale configuration settings, {staleSnapshotCount} snapshots, and {staleSecretCount} secrets"); } catch (RequestFailedException ex) @@ -244,53 +238,6 @@ private async Task CleanupStaleResources() public async Task DisposeAsync() { - var cleanupTasks = new List(); - - // Clean up stale configuration settings, snapshots, and Key Vault secrets - foreach (string key in _createdConfigKeys) - { - try - { - cleanupTasks.Add(_configClient.DeleteConfigurationSettingAsync(key)); - } - catch (RequestFailedException ex) - { - Console.WriteLine($"Failed to delete configuration setting {key}: {ex.Message}"); - } - } - - foreach (string snapshotName in _createdSnapshotNames) - { - try - { - cleanupTasks.Add(_configClient.ArchiveSnapshotAsync(snapshotName)); - } - catch (RequestFailedException ex) - { - Console.WriteLine($"Failed to delete snapshot {snapshotName}: {ex.Message}"); - } - } - - if (_secretClient != null) - { - foreach (string secretName in _createdSecretNames) - { - try - { - cleanupTasks.Add(_secretClient.StartDeleteSecretAsync(secretName)); - } - catch (RequestFailedException ex) - { - Console.WriteLine($"Failed to delete secret {secretName}: {ex.Message}"); - } - } - } - - // Wait for all cleanup tasks to complete - await Task.WhenAll(cleanupTasks); - - Console.WriteLine($"Cleaned up {_createdConfigKeys.Count} configuration settings, {_createdSnapshotNames.Count} snapshots, and {_createdSecretNames.Count} secrets"); - await CleanupStaleResources(); } @@ -301,6 +248,7 @@ private enum TestDataTypes KeyValues = 1, FeatureFlags = 2, KeyVaultReferences = 4, + TaggedSettings = 8, All = KeyValues | FeatureFlags | KeyVaultReferences } @@ -326,8 +274,6 @@ private async Task SetupTestKeys(string testName, TestDataTypes dat foreach (ConfigurationSetting setting in testSettings) { await _configClient.SetConfigurationSettingAsync(setting); - // Track the created key for cleanup - _createdConfigKeys.Add(setting.Key); } } @@ -340,30 +286,12 @@ private async Task SetupTestKeys(string testName, TestDataTypes dat contentType: FeatureFlagContentType); await _configClient.SetConfigurationSettingAsync(featureFlagSetting); - // Track the created key for cleanup - _createdConfigKeys.Add(featureFlagSetting.Key); } // Create KeyVault reference if requested if (dataTypesToCreate.HasFlag(TestDataTypes.KeyVaultReferences) && _secretClient != null) { - try - { - await _secretClient.SetSecretAsync(secretName, secretValue); - } - catch (RequestFailedException ex) - { - Console.WriteLine($"Error setting up Key Vault secret: {ex.Message}"); - // Continue without Key Vault reference if it fails - } - catch (InvalidOperationException ex) - { - Console.WriteLine($"Invalid Key Vault operation: {ex.Message}"); - // Continue without Key Vault reference if it fails - } - - // Track the created secret for cleanup - _createdSecretNames.Add(secretName); + await _secretClient.SetSecretAsync(secretName, secretValue); string keyVaultUri = $"{_keyVaultEndpoint}secrets/{secretName}"; string keyVaultRefValue = @$"{{""uri"":""{keyVaultUri}""}}"; @@ -374,17 +302,114 @@ private async Task SetupTestKeys(string testName, TestDataTypes dat label: KeyVaultReferenceLabel, contentType: KeyVaultConstants.ContentType); - try + await _configClient.SetConfigurationSettingAsync(keyVaultRefSetting); + } + + // Create tagged settings if requested + if (dataTypesToCreate.HasFlag(TestDataTypes.TaggedSettings)) + { + // Create configuration settings with various tags + var taggedSettings = new List { - await _configClient.SetConfigurationSettingAsync(keyVaultRefSetting); - } - catch (RequestFailedException ex) + // Basic environment tags + CreateSettingWithTags( + $"{keyPrefix}:TaggedSetting1", + "Value1", + new Dictionary { + { "Environment", "Development" }, + { "App", "TestApp" } + }), + + CreateSettingWithTags( + $"{keyPrefix}:TaggedSetting2", + "Value2", + new Dictionary { + { "Environment", "Production" }, + { "App", "TestApp" } + }), + + CreateSettingWithTags( + $"{keyPrefix}:TaggedSetting3", + "Value3", + new Dictionary { + { "Environment", "Development" }, + { "Component", "API" } + }), + + // Special characters in tags + CreateSettingWithTags( + $"{keyPrefix}:TaggedSetting4", + "Value4", + new Dictionary { + { "Special:Tag", "Value:With:Colons" }, + { "Tag@With@At", "Value@With@At" } + }), + + // Empty and null tag values + CreateSettingWithTags( + $"{keyPrefix}:TaggedSetting5", + "Value5", + new Dictionary { + { "EmptyTag", "" }, + { "NullTag", null } + }), + + // Comma in tag name/value + CreateSettingWithTags( + $"{keyPrefix}:TaggedSetting6", + "Value6", + new Dictionary { + { "Tag,With,Commas", "Value,With,Commas" } + }) + }; + + foreach (ConfigurationSetting setting in taggedSettings) { - Console.WriteLine($"Error setting up Key Vault reference: {ex.Message}"); + await _configClient.SetConfigurationSettingAsync(setting); } - // Track the created key reference for cleanup - _createdConfigKeys.Add(keyVaultReferenceKey); + // Create feature flags with tags + var taggedFeatureFlags = new List + { + // Basic environment tags on feature flags + CreateFeatureFlagWithTags( + $"{keyPrefix}FeatureDev", + true, + new Dictionary { + { "Environment", "Development" }, + { "App", "TestApp" } + }), + + CreateFeatureFlagWithTags( + $"{keyPrefix}FeatureProd", + false, + new Dictionary { + { "Environment", "Production" }, + { "App", "TestApp" } + }), + + // Feature flags with special character tags + CreateFeatureFlagWithTags( + $"{keyPrefix}FeatureSpecial", + true, + new Dictionary { + { "Special:Tag", "Value:With:Colons" } + }), + + // Feature flags with empty/null tags + CreateFeatureFlagWithTags( + $"{keyPrefix}FeatureEmpty", + false, + new Dictionary { + { "EmptyTag", "" }, + { "NullTag", null } + }) + }; + + foreach (ConfigurationSetting setting in taggedFeatureFlags) + { + await _configClient.SetConfigurationSettingAsync(setting); + } } return new TestContext @@ -1518,34 +1543,6 @@ public async Task KeyVaultReferences_ResolveCorrectly() Assert.Equal("SecretValue", config[testContext.KeyVaultReferenceKey]); } - private class HttpPipelineTransportWithRequestCount : HttpPipelineTransport - { - private readonly HttpClientTransport _innerTransport = new HttpClientTransport(); - private readonly Action _onRequest; - - public HttpPipelineTransportWithRequestCount(Action onRequest) - { - _onRequest = onRequest; - } - - public override Request CreateRequest() - { - return _innerTransport.CreateRequest(); - } - - public override void Process(HttpMessage message) - { - _onRequest(); - _innerTransport.Process(message); - } - - public override ValueTask ProcessAsync(HttpMessage message) - { - _onRequest(); - return _innerTransport.ProcessAsync(message); - } - } - /// /// Tests that Key Vault secrets are properly cached to avoid unnecessary requests. /// @@ -1753,16 +1750,230 @@ await _configClient.SetConfigurationSettingAsync( Assert.Contains($"{RequestTracingConstants.FeatureManagementVersionKey}=4.0.0", requestInspector.CorrelationContextHeaders.Last()); } - private class RequestInspectionHandler + [Fact] + public async Task TagFilters() { - public List CorrelationContextHeaders { get; } = new List(); + TestContext testContext = await SetupTestKeys("TagFilters", TestDataTypes.TaggedSettings); + string keyPrefix = testContext.KeyPrefix; - public void InspectRequest(HttpMessage message) - { - if (message.Request.Headers.TryGetValue(RequestTracingConstants.CorrelationContextHeader, out string header)) + // Test case 1: Basic tag filtering with single tag + var config1 = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => { - CorrelationContextHeaders.Add(header); - } + options.Connect(_connectionString); + options.Select($"{keyPrefix}:TaggedSetting*", tagFilters: new[] { "Environment=Development" }); + options.UseFeatureFlags(ff => + { + ff.Select($"{keyPrefix}:*", tagFilters: new[] { "Environment=Development" }); + }); + }) + .Build(); + + // Assert - Should only get settings with Environment=Development tag + Assert.Equal("Value1", config1[$"{keyPrefix}:TaggedSetting1"]); + Assert.Equal("Value3", config1[$"{keyPrefix}:TaggedSetting3"]); + Assert.Null(config1[$"{keyPrefix}:TaggedSetting2"]); + Assert.Null(config1[$"{keyPrefix}:TaggedSetting4"]); + Assert.Null(config1[$"{keyPrefix}:TaggedSetting5"]); + Assert.Null(config1[$"{keyPrefix}:TaggedSetting6"]); + + // Feature flags should be filtered as well + Assert.Equal("True", config1[$"FeatureManagement:{keyPrefix}FeatureDev"]); + Assert.Null(config1[$"FeatureManagement:{keyPrefix}FeatureProd"]); + Assert.Null(config1[$"FeatureManagement:{keyPrefix}FeatureSpecial"]); + Assert.Null(config1[$"FeatureManagement:{keyPrefix}FeatureEmpty"]); + + // Test case 2: Multiple tag filters (AND condition) + var config2 = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(_connectionString); + options.Select($"{keyPrefix}:*", tagFilters: new[] { "Environment=Development", "App=TestApp" }); + options.Select($".appconfig.featureflag/{keyPrefix}*", tagFilters: new[] { "Environment=Development", "App=TestApp" }); + options.UseFeatureFlags(); + }) + .Build(); + + // Assert - Should only get settings with both Environment=Development AND App=TestApp tags + Assert.Equal("Value1", config2[$"{keyPrefix}:TaggedSetting1"]); + Assert.Null(config2[$"{keyPrefix}:TaggedSetting2"]); + Assert.Null(config2[$"{keyPrefix}:TaggedSetting3"]); + Assert.Null(config2[$"{keyPrefix}:TaggedSetting4"]); + Assert.Null(config2[$"{keyPrefix}:TaggedSetting5"]); + Assert.Null(config2[$"{keyPrefix}:TaggedSetting6"]); + + // Feature flags + Assert.Equal("True", config2[$"FeatureManagement:{keyPrefix}FeatureDev"]); + Assert.Null(config2[$"FeatureManagement:{keyPrefix}FeatureProd"]); + Assert.Null(config2[$"FeatureManagement:{keyPrefix}FeatureSpecial"]); + Assert.Null(config2[$"FeatureManagement:{keyPrefix}FeatureEmpty"]); + + // Test case 3: Special characters in tags + var config3 = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(_connectionString); + options.Select($"{keyPrefix}:*", tagFilters: new[] { "Special:Tag=Value:With:Colons" }); + options.Select($".appconfig.featureflag/{keyPrefix}*", tagFilters: new[] { "Special:Tag=Value:With:Colons" }); + options.UseFeatureFlags(); + }) + .Build(); + + // Assert - Should only get settings with the special character tag + Assert.Equal("Value4", config3[$"{keyPrefix}:TaggedSetting4"]); + Assert.Null(config3[$"{keyPrefix}:TaggedSetting1"]); + + Assert.Null(config3[$"{keyPrefix}:TaggedSetting2"]); + Assert.Null(config3[$"{keyPrefix}:TaggedSetting3"]); + Assert.Null(config3[$"{keyPrefix}:TaggedSetting5"]); + Assert.Null(config3[$"{keyPrefix}:TaggedSetting6"]); + + // Feature flags + Assert.Equal("True", config3[$"FeatureManagement:{keyPrefix}FeatureSpecial"]); + Assert.Null(config3[$"FeatureManagement:{keyPrefix}FeatureDev"]); + Assert.Null(config3[$"FeatureManagement:{keyPrefix}FeatureProd"]); + Assert.Null(config3[$"FeatureManagement:{keyPrefix}FeatureEmpty"]); + + // Test case 4: Tag with @ symbol + var config4 = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(_connectionString); + options.Select($"{keyPrefix}:*", tagFilters: new[] { "Tag@With@At=Value@With@At" }); + options.UseFeatureFlags(); + }) + .Build(); + + // Assert - Should only get settings with the @ symbol tag + Assert.Equal("Value4", config4[$"{keyPrefix}:TaggedSetting4"]); + Assert.Null(config4[$"{keyPrefix}:TaggedSetting1"]); + Assert.Null(config4[$"{keyPrefix}:TaggedSetting2"]); + Assert.Null(config4[$"{keyPrefix}:TaggedSetting3"]); + Assert.Null(config4[$"{keyPrefix}:TaggedSetting5"]); + Assert.Null(config4[$"{keyPrefix}:TaggedSetting6"]); + + // Test case 5: Empty and null tag values + var config5 = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(_connectionString); + options.Select($"{keyPrefix}:*", tagFilters: new[] { "EmptyTag=", $"NullTag={TagValue.Null}" }); + options.Select($".appconfig.featureflag/{keyPrefix}*", tagFilters: new[] { "EmptyTag=", $"NullTag={TagValue.Null}" }); + options.UseFeatureFlags(); + }) + .Build(); + + // Assert - Should only get settings with both empty and null tag values + Assert.Equal("Value5", config5[$"{keyPrefix}:TaggedSetting5"]); + Assert.Null(config5[$"{keyPrefix}:TaggedSetting1"]); + Assert.Null(config5[$"{keyPrefix}:TaggedSetting2"]); + Assert.Null(config5[$"{keyPrefix}:TaggedSetting3"]); + Assert.Null(config5[$"{keyPrefix}:TaggedSetting4"]); + Assert.Null(config5[$"{keyPrefix}:TaggedSetting6"]); + + // Feature flags + Assert.Equal("False", config5[$"FeatureManagement:{keyPrefix}FeatureEmpty"]); + Assert.Null(config5[$"FeatureManagement:{keyPrefix}FeatureDev"]); + Assert.Null(config5[$"FeatureManagement:{keyPrefix}FeatureProd"]); + Assert.Null(config5[$"FeatureManagement:{keyPrefix}FeatureSpecial"]); + + // Test case 6: Commas in tag name/value + var config6 = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(_connectionString); + options.Select($"{keyPrefix}:*", tagFilters: new[] { "Tag,With,Commas=Value,With,Commas" }); + options.UseFeatureFlags(); + }) + .Build(); + + // Assert - Should only get settings with the comma-containing tag + Assert.Equal("Value6", config6[$"{keyPrefix}:TaggedSetting6"]); + Assert.Null(config6[$"{keyPrefix}:TaggedSetting1"]); + Assert.Null(config6[$"{keyPrefix}:TaggedSetting2"]); + Assert.Null(config6[$"{keyPrefix}:TaggedSetting3"]); + Assert.Null(config6[$"{keyPrefix}:TaggedSetting4"]); + Assert.Null(config6[$"{keyPrefix}:TaggedSetting5"]); + + // Test case 7: Interaction with refresh functionality + IConfigurationRefresher refresher = null; + var config9 = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(_connectionString); + options.Select($"{keyPrefix}:*", tagFilters: new[] { "Environment=Development" }); + options.ConfigureRefresh(refresh => + { + refresh.Register(testContext.SentinelKey, refreshAll: true) + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + refresher = options.GetRefresher(); + }) + .Build(); + + // Assert initial state + Assert.Equal("Value1", config9[$"{keyPrefix}:TaggedSetting1"]); + Assert.Equal("Value3", config9[$"{keyPrefix}:TaggedSetting3"]); + + // Update a tagged setting's value + await _configClient.SetConfigurationSettingAsync( + CreateSettingWithTags( + $"{keyPrefix}:TaggedSetting1", + "UpdatedValue1", + new Dictionary { + { "Environment", "Development" }, + { "App", "TestApp" } + })); + + // Add a new setting with Development tag + await _configClient.SetConfigurationSettingAsync( + CreateSettingWithTags( + $"{keyPrefix}:TaggedSetting7", + "Value7", + new Dictionary { + { "Environment", "Development" } + })); + + // Update the sentinel key to trigger refresh + await _configClient.SetConfigurationSettingAsync(new ConfigurationSetting(testContext.SentinelKey, "Updated")); + + // Wait for cache to expire + await Task.Delay(TimeSpan.FromSeconds(2)); + + // Refresh the configuration + await refresher.RefreshAsync(); + + Assert.Equal("UpdatedValue1", config9[$"{keyPrefix}:TaggedSetting1"]); + Assert.Equal("Value3", config9[$"{keyPrefix}:TaggedSetting3"]); + Assert.Equal("Value7", config9[$"{keyPrefix}:TaggedSetting7"]); + Assert.Null(config9[$"{keyPrefix}:TaggedSetting2"]); + } + + private class HttpPipelineTransportWithRequestCount : HttpPipelineTransport + { + private readonly HttpClientTransport _innerTransport = new HttpClientTransport(); + private readonly Action _onRequest; + + public HttpPipelineTransportWithRequestCount(Action onRequest) + { + _onRequest = onRequest; + } + + public override Request CreateRequest() + { + return _innerTransport.CreateRequest(); + } + + public override void Process(HttpMessage message) + { + _onRequest(); + _innerTransport.Process(message); + } + + public override ValueTask ProcessAsync(HttpMessage message) + { + _onRequest(); + return _innerTransport.ProcessAsync(message); } } @@ -1781,5 +1992,62 @@ public override async ValueTask ProcessAsync(HttpMessage message) await base.ProcessAsync(message); } } + + private class RequestInspectionHandler + { + public List CorrelationContextHeaders { get; } = new List(); + + public void InspectRequest(HttpMessage message) + { + if (message.Request.Headers.TryGetValue(RequestTracingConstants.CorrelationContextHeader, out string header)) + { + CorrelationContextHeaders.Add(header); + } + } + } + + private ConfigurationSetting CreateSettingWithTags(string key, string value, IDictionary tags) + { + var setting = new ConfigurationSetting(key, value); + + if (tags != null) + { + foreach (var tag in tags) + { + setting.Tags.Add(tag.Key, tag.Value); + } + } + + return setting; + } + + private ConfigurationSetting CreateFeatureFlagWithTags(string featureId, bool enabled, IDictionary tags) + { + string jsonValue = $@"{{ + ""id"": ""{featureId}"", + ""description"": ""Test feature flag with tags"", + ""enabled"": {enabled.ToString().ToLowerInvariant()}, + ""conditions"": {{ + ""client_filters"": [] + }} + }}"; + + var setting = new ConfigurationSetting( + key: FeatureManagementConstants.FeatureFlagMarker + featureId, + value: jsonValue) + { + ContentType = FeatureFlagContentType + }; + + if (tags != null) + { + foreach (var tag in tags) + { + setting.Tags.Add(tag.Key, tag.Value); + } + } + + return setting; + } } } From 8af23a3a7f2070442575ea154a0a429703288d71 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 13 May 2025 14:12:51 -0700 Subject: [PATCH 49/57] correct api version --- .../AzureAppConfigurationOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 1e79daba2..bb48372ab 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -546,7 +546,7 @@ public AzureAppConfigurationOptions ConfigureStartupOptions(Action Date: Tue, 13 May 2025 15:12:03 -0700 Subject: [PATCH 50/57] fixed tagfilter test --- .../Integration/IntegrationTests.cs | 94 +++++++------------ 1 file changed, 34 insertions(+), 60 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 1a4c480f8..3b4610413 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -352,14 +352,6 @@ private async Task SetupTestKeys(string testName, TestDataTypes dat new Dictionary { { "EmptyTag", "" }, { "NullTag", null } - }), - - // Comma in tag name/value - CreateSettingWithTags( - $"{keyPrefix}:TaggedSetting6", - "Value6", - new Dictionary { - { "Tag,With,Commas", "Value,With,Commas" } }) }; @@ -373,7 +365,7 @@ private async Task SetupTestKeys(string testName, TestDataTypes dat { // Basic environment tags on feature flags CreateFeatureFlagWithTags( - $"{keyPrefix}FeatureDev", + $"{keyPrefix}:FeatureDev", true, new Dictionary { { "Environment", "Development" }, @@ -381,7 +373,7 @@ private async Task SetupTestKeys(string testName, TestDataTypes dat }), CreateFeatureFlagWithTags( - $"{keyPrefix}FeatureProd", + $"{keyPrefix}:FeatureProd", false, new Dictionary { { "Environment", "Production" }, @@ -390,7 +382,7 @@ private async Task SetupTestKeys(string testName, TestDataTypes dat // Feature flags with special character tags CreateFeatureFlagWithTags( - $"{keyPrefix}FeatureSpecial", + $"{keyPrefix}:FeatureSpecial", true, new Dictionary { { "Special:Tag", "Value:With:Colons" } @@ -398,7 +390,7 @@ private async Task SetupTestKeys(string testName, TestDataTypes dat // Feature flags with empty/null tags CreateFeatureFlagWithTags( - $"{keyPrefix}FeatureEmpty", + $"{keyPrefix}:FeatureEmpty", false, new Dictionary { { "EmptyTag", "" }, @@ -1761,7 +1753,7 @@ public async Task TagFilters() .AddAzureAppConfiguration(options => { options.Connect(_connectionString); - options.Select($"{keyPrefix}:TaggedSetting*", tagFilters: new[] { "Environment=Development" }); + options.Select($"{keyPrefix}:*", tagFilters: new[] { "Environment=Development" }); options.UseFeatureFlags(ff => { ff.Select($"{keyPrefix}:*", tagFilters: new[] { "Environment=Development" }); @@ -1775,13 +1767,12 @@ public async Task TagFilters() Assert.Null(config1[$"{keyPrefix}:TaggedSetting2"]); Assert.Null(config1[$"{keyPrefix}:TaggedSetting4"]); Assert.Null(config1[$"{keyPrefix}:TaggedSetting5"]); - Assert.Null(config1[$"{keyPrefix}:TaggedSetting6"]); // Feature flags should be filtered as well - Assert.Equal("True", config1[$"FeatureManagement:{keyPrefix}FeatureDev"]); - Assert.Null(config1[$"FeatureManagement:{keyPrefix}FeatureProd"]); - Assert.Null(config1[$"FeatureManagement:{keyPrefix}FeatureSpecial"]); - Assert.Null(config1[$"FeatureManagement:{keyPrefix}FeatureEmpty"]); + Assert.Equal("True", config1[$"FeatureManagement:{keyPrefix}:FeatureDev"]); + Assert.Null(config1[$"FeatureManagement:{keyPrefix}:FeatureProd"]); + Assert.Null(config1[$"FeatureManagement:{keyPrefix}:FeatureSpecial"]); + Assert.Null(config1[$"FeatureManagement:{keyPrefix}:FeatureEmpty"]); // Test case 2: Multiple tag filters (AND condition) var config2 = new ConfigurationBuilder() @@ -1789,8 +1780,10 @@ public async Task TagFilters() { options.Connect(_connectionString); options.Select($"{keyPrefix}:*", tagFilters: new[] { "Environment=Development", "App=TestApp" }); - options.Select($".appconfig.featureflag/{keyPrefix}*", tagFilters: new[] { "Environment=Development", "App=TestApp" }); - options.UseFeatureFlags(); + options.UseFeatureFlags(ff => + { + ff.Select($"{keyPrefix}:*", tagFilters: new[] { "Environment=Development", "App=TestApp" }); + }); }) .Build(); @@ -1800,13 +1793,12 @@ public async Task TagFilters() Assert.Null(config2[$"{keyPrefix}:TaggedSetting3"]); Assert.Null(config2[$"{keyPrefix}:TaggedSetting4"]); Assert.Null(config2[$"{keyPrefix}:TaggedSetting5"]); - Assert.Null(config2[$"{keyPrefix}:TaggedSetting6"]); // Feature flags - Assert.Equal("True", config2[$"FeatureManagement:{keyPrefix}FeatureDev"]); - Assert.Null(config2[$"FeatureManagement:{keyPrefix}FeatureProd"]); - Assert.Null(config2[$"FeatureManagement:{keyPrefix}FeatureSpecial"]); - Assert.Null(config2[$"FeatureManagement:{keyPrefix}FeatureEmpty"]); + Assert.Equal("True", config2[$"FeatureManagement:{keyPrefix}:FeatureDev"]); + Assert.Null(config2[$"FeatureManagement:{keyPrefix}:FeatureProd"]); + Assert.Null(config2[$"FeatureManagement:{keyPrefix}:FeatureSpecial"]); + Assert.Null(config2[$"FeatureManagement:{keyPrefix}:FeatureEmpty"]); // Test case 3: Special characters in tags var config3 = new ConfigurationBuilder() @@ -1814,8 +1806,10 @@ public async Task TagFilters() { options.Connect(_connectionString); options.Select($"{keyPrefix}:*", tagFilters: new[] { "Special:Tag=Value:With:Colons" }); - options.Select($".appconfig.featureflag/{keyPrefix}*", tagFilters: new[] { "Special:Tag=Value:With:Colons" }); - options.UseFeatureFlags(); + options.UseFeatureFlags(ff => + { + ff.Select($"{keyPrefix}:*", tagFilters: new[] { "Special:Tag=Value:With:Colons" }); + }); }) .Build(); @@ -1826,13 +1820,12 @@ public async Task TagFilters() Assert.Null(config3[$"{keyPrefix}:TaggedSetting2"]); Assert.Null(config3[$"{keyPrefix}:TaggedSetting3"]); Assert.Null(config3[$"{keyPrefix}:TaggedSetting5"]); - Assert.Null(config3[$"{keyPrefix}:TaggedSetting6"]); // Feature flags - Assert.Equal("True", config3[$"FeatureManagement:{keyPrefix}FeatureSpecial"]); - Assert.Null(config3[$"FeatureManagement:{keyPrefix}FeatureDev"]); - Assert.Null(config3[$"FeatureManagement:{keyPrefix}FeatureProd"]); - Assert.Null(config3[$"FeatureManagement:{keyPrefix}FeatureEmpty"]); + Assert.Equal("True", config3[$"FeatureManagement:{keyPrefix}:FeatureSpecial"]); + Assert.Null(config3[$"FeatureManagement:{keyPrefix}:FeatureDev"]); + Assert.Null(config3[$"FeatureManagement:{keyPrefix}:FeatureProd"]); + Assert.Null(config3[$"FeatureManagement:{keyPrefix}:FeatureEmpty"]); // Test case 4: Tag with @ symbol var config4 = new ConfigurationBuilder() @@ -1840,7 +1833,6 @@ public async Task TagFilters() { options.Connect(_connectionString); options.Select($"{keyPrefix}:*", tagFilters: new[] { "Tag@With@At=Value@With@At" }); - options.UseFeatureFlags(); }) .Build(); @@ -1850,7 +1842,6 @@ public async Task TagFilters() Assert.Null(config4[$"{keyPrefix}:TaggedSetting2"]); Assert.Null(config4[$"{keyPrefix}:TaggedSetting3"]); Assert.Null(config4[$"{keyPrefix}:TaggedSetting5"]); - Assert.Null(config4[$"{keyPrefix}:TaggedSetting6"]); // Test case 5: Empty and null tag values var config5 = new ConfigurationBuilder() @@ -1858,8 +1849,10 @@ public async Task TagFilters() { options.Connect(_connectionString); options.Select($"{keyPrefix}:*", tagFilters: new[] { "EmptyTag=", $"NullTag={TagValue.Null}" }); - options.Select($".appconfig.featureflag/{keyPrefix}*", tagFilters: new[] { "EmptyTag=", $"NullTag={TagValue.Null}" }); - options.UseFeatureFlags(); + options.UseFeatureFlags(ff => + { + ff.Select($"{keyPrefix}:*", tagFilters: new[] { "EmptyTag=", $"NullTag={TagValue.Null}" }); + }); }) .Build(); @@ -1869,33 +1862,14 @@ public async Task TagFilters() Assert.Null(config5[$"{keyPrefix}:TaggedSetting2"]); Assert.Null(config5[$"{keyPrefix}:TaggedSetting3"]); Assert.Null(config5[$"{keyPrefix}:TaggedSetting4"]); - Assert.Null(config5[$"{keyPrefix}:TaggedSetting6"]); // Feature flags - Assert.Equal("False", config5[$"FeatureManagement:{keyPrefix}FeatureEmpty"]); - Assert.Null(config5[$"FeatureManagement:{keyPrefix}FeatureDev"]); - Assert.Null(config5[$"FeatureManagement:{keyPrefix}FeatureProd"]); - Assert.Null(config5[$"FeatureManagement:{keyPrefix}FeatureSpecial"]); - - // Test case 6: Commas in tag name/value - var config6 = new ConfigurationBuilder() - .AddAzureAppConfiguration(options => - { - options.Connect(_connectionString); - options.Select($"{keyPrefix}:*", tagFilters: new[] { "Tag,With,Commas=Value,With,Commas" }); - options.UseFeatureFlags(); - }) - .Build(); - - // Assert - Should only get settings with the comma-containing tag - Assert.Equal("Value6", config6[$"{keyPrefix}:TaggedSetting6"]); - Assert.Null(config6[$"{keyPrefix}:TaggedSetting1"]); - Assert.Null(config6[$"{keyPrefix}:TaggedSetting2"]); - Assert.Null(config6[$"{keyPrefix}:TaggedSetting3"]); - Assert.Null(config6[$"{keyPrefix}:TaggedSetting4"]); - Assert.Null(config6[$"{keyPrefix}:TaggedSetting5"]); + Assert.Equal("False", config5[$"FeatureManagement:{keyPrefix}:FeatureEmpty"]); + Assert.Null(config5[$"FeatureManagement:{keyPrefix}:FeatureDev"]); + Assert.Null(config5[$"FeatureManagement:{keyPrefix}:FeatureProd"]); + Assert.Null(config5[$"FeatureManagement:{keyPrefix}:FeatureSpecial"]); - // Test case 7: Interaction with refresh functionality + // Test case 6: Interaction with refresh functionality IConfigurationRefresher refresher = null; var config9 = new ConfigurationBuilder() .AddAzureAppConfiguration(options => From 7d11cd8f1f96e8efe819e9a39b17716c5e22b6fe Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 14 May 2025 12:54:00 -0700 Subject: [PATCH 51/57] fix requesttracing test --- .../Integration/IntegrationTests.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 3b4610413..1661d9a01 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -1677,6 +1677,20 @@ await _configClient.SetConfigurationSettingAsync( ""id"": """ + testContext.KeyPrefix + @"Feature"", ""description"": ""Test feature with filters"", ""enabled"": true, + ""variants"": [ + { + ""name"": ""LargeSize"", + ""configuration_value"": ""800px"" + }, + { + ""name"": ""MediumSize"", + ""configuration_value"": ""600px"" + }, + { + ""name"": ""SmallSize"", + ""configuration_value"": ""400px"" + } + ], ""conditions"": { ""client_filters"": [ { From 52fbf7453cfc177f00df791eb61553532cf84abb Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 14 May 2025 13:18:34 -0700 Subject: [PATCH 52/57] allow commas in json reader --- .../FeatureManagement/FeatureManagementKeyValueAdapter.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs index cbfa411aa..13408385c 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs @@ -426,7 +426,12 @@ private FeatureFlag ParseFeatureFlag(string settingKey, string settingValue) { var featureFlag = new FeatureFlag(); - var reader = new Utf8JsonReader(System.Text.Encoding.UTF8.GetBytes(settingValue)); + var reader = new Utf8JsonReader( + System.Text.Encoding.UTF8.GetBytes(settingValue), + new JsonReaderOptions + { + AllowTrailingCommas = true + }); try { From 671433d67a1eb99d824eee257d3392e711be5d25 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 16 May 2025 10:10:33 -0700 Subject: [PATCH 53/57] stale threshold --- .../Tests.AzureAppConfiguration/Integration/IntegrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 1661d9a01..511152684 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -38,7 +38,7 @@ public class IntegrationTests : IAsyncLifetime // Test constants private const string TestKeyPrefix = "IntegrationTest"; private const string SubscriptionJsonPath = "appsettings.Secrets.json"; - private static readonly TimeSpan StaleResourceThreshold = TimeSpan.FromHours(2); + private static readonly TimeSpan StaleResourceThreshold = TimeSpan.FromHours(3); private const string KeyVaultReferenceLabel = "KeyVaultRef"; // Content type constants From 6e2a2d1838c2f605a8da877c57aff33c43b0761c Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 16 May 2025 10:16:40 -0700 Subject: [PATCH 54/57] remove copilot instructions --- .github/code-gen-instructions.md | 93 -------------------------------- .github/copilot-instructions.md | 3 -- 2 files changed, 96 deletions(-) delete mode 100644 .github/code-gen-instructions.md delete mode 100644 .github/copilot-instructions.md diff --git a/.github/code-gen-instructions.md b/.github/code-gen-instructions.md deleted file mode 100644 index 6f000773e..000000000 --- a/.github/code-gen-instructions.md +++ /dev/null @@ -1,93 +0,0 @@ -# AppConfiguration-DotnetProvider Coding Guidelines - -This document outlines coding guidelines for the Azure App Configuration .NET Provider repository. Follow these guidelines when generating or modifying code. - -## General Guidelines - -1. **Exception Handling**: - * Always catch specific exceptions and avoid catching the base `Exception` class in catch blocks. - * Throw specific exception types (e.g., `ArgumentNullException`, `FormatException`, custom exceptions) rather than generic `System.Exception`. - * Include the parameter name when throwing `ArgumentNullException` using `nameof()`. - -2. **Variable Declaration**: - * Never use `var` to declare a variable if the assignment doesn't include the type or the type isn't immediately obvious. - * Use explicit type names for fields, properties, method parameters, and return types. - * Use `var` only when the type is obvious from the right-hand side (e.g., `var user = new User();`). - -3. **Null Handling**: - * Validate arguments in public methods and constructors with explicit null checks. - * Use explicit `if (argument == null) throw new ArgumentNullException(nameof(argument));` checks at the beginning of methods/constructors. - * Avoid using the null-forgiving operator (`!`) unless absolutely necessary. - -4. **Asynchronous Programming**: - * All async methods should accept a `CancellationToken` as the last parameter. - * Pass the `cancellationToken` down the call stack to all subsequent asynchronous operations. - * Use `Task` or `Task` for asynchronous methods. - -5. **LINQ and Collections**: - * Prefer simple, readable LINQ queries. - * Break down complex LINQ queries into separate statements with intermediate variables. - * Use collection interfaces (e.g., `IList`, `IReadOnlyList`) in parameter and return types. - -6. **Resource Management**: - * Wrap `IDisposable` instances in `using` statements to ensure proper disposal. - * Implement `IDisposable` correctly if your class manages disposable objects. - -7. **Dependency Injection**: - * Use constructor injection for dependencies. - * Store injected dependencies in `private readonly` fields. - * Validate injected dependencies for null in the constructor. - -8. **Naming Conventions**: - * Use `PascalCase` for classes, interfaces, enums, methods, properties, and constants. - * Use `camelCase` for local variables and method parameters. - * Prefix private fields with an underscore (`_`). - * Define constants for error messages and other string literals. - -9. **Comments**: - * Only add comments when it's not obvious what the code is doing. For example, if a variable name is already fairly descriptive, a comment isn't needed explaining its name. - * Add summary comments to public classes and members of those classes. - -## AppConfiguration-Specific Guidelines - -1. **Feature Flag Handling**: - * Validate feature flag data structure before processing. - * Handle different feature flag schemas (Microsoft vs .NET) appropriately. - * Use proper error handling when parsing feature flags with clear error messages. - -2. **Configuration Key-Value Processing**: - * Follow adapter pattern for processing different configuration types. - * Properly handle key-value pairs with appropriate content type detection. - * Use `KeyValuePair` for configuration values. - -3. **Content Type Handling**: - * Validate content types before processing. - * Use appropriate content type constants. - * Check content type using extension methods like `IsFeatureFlag()`. - -4. **JSON Parsing**: - * Use `Utf8JsonReader` for performance-critical JSON parsing. - * Validate JSON structure and provide clear error messages for malformed input. - * Handle JSON token types appropriately with proper error handling. - -5. **Refresh Mechanisms**: - * Implement proper configuration refresh patterns. - * Use sentinel-based refresh mechanisms when appropriate. - * Handle refresh failures gracefully. - -## Performance Considerations - -1. **String Handling**: - * Use `StringBuilder` for concatenating multiple strings. - * Define string constants for recurring strings. - * Use string interpolation instead of string concatenation when appropriate. - -2. **Collections**: - * Initialize collections with estimated capacity when possible. - * Use appropriate collection types for the use case (e.g., `List`, `Dictionary`). - * Avoid unnecessary collection allocations. - -3. **Memory Management**: - * Use `Span` and `ReadOnlySpan` for high-performance scenarios. - * Minimize allocations in performance-critical paths. - * Be mindful of closure allocations in LINQ and lambdas. \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index ea3523ffa..000000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,3 +0,0 @@ -This is the Azure App Configuration .NET Provider codebase. The service abides by coding guidelines specified in `github\code-gen-instructions.md`file. - -When suggesting code changes, do not modify the files directly. Instead, provide a detailed explanation of the changes you would make and ask for confirmation before editing the files. You may create markdown files to demonstrate the changes you would like to make. From 2a0a4e56102543a90629fd8eefefec16ec08667f Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 16 May 2025 14:59:21 -0700 Subject: [PATCH 55/57] use outputhelper --- .../Integration/IntegrationTests.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 511152684..996f56fd3 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -25,6 +25,8 @@ namespace Tests.AzureAppConfiguration { + using Xunit.Abstractions; + /// /// Integration tests for Azure App Configuration that connect to a real service. /// Uses an existing App Configuration store and Key Vault for testing. @@ -73,6 +75,13 @@ private class TestContext private Uri _keyVaultEndpoint; + private readonly ITestOutputHelper _output; + + public IntegrationTests(ITestOutputHelper output) + { + _output = output; + } + private string GetCurrentSubscriptionId() { string subscriptionIdFromEnv = Environment.GetEnvironmentVariable("AZURE_SUBSCRIPTION_ID"); @@ -163,12 +172,12 @@ public async Task InitializeAsync() _secretClient = new SecretClient(_keyVaultEndpoint, credential); - Console.WriteLine($"Successfully connected to App Configuration store '{AppConfigStoreName}' and Key Vault '{KeyVaultName}'"); + _output.WriteLine($"Successfully connected to App Configuration store '{AppConfigStoreName}' and Key Vault '{KeyVaultName}'"); } private async Task CleanupStaleResources() { - Console.WriteLine($"Checking for stale resources older than {StaleResourceThreshold}..."); + _output.WriteLine($"Checking for stale resources older than {StaleResourceThreshold}..."); var cutoffTime = DateTimeOffset.UtcNow.Subtract(StaleResourceThreshold); var cleanupTasks = new List(); @@ -227,11 +236,11 @@ private async Task CleanupStaleResources() // Wait for all cleanup tasks to complete await Task.WhenAll(cleanupTasks); - Console.WriteLine($"Cleaned up {staleConfigCount} stale configuration settings, {staleSnapshotCount} snapshots, and {staleSecretCount} secrets"); + _output.WriteLine($"Cleaned up {staleConfigCount} stale configuration settings, {staleSnapshotCount} snapshots, and {staleSecretCount} secrets"); } catch (RequestFailedException ex) { - Console.WriteLine($"Error during stale resource cleanup: {ex.Message}"); + _output.WriteLine($"Error during stale resource cleanup: {ex.Message}"); // Continue execution even if cleanup fails } } From 6de8e8b0930f6c2c7b7e5cf531c01b6edd52df7d Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 19 May 2025 10:39:51 -0700 Subject: [PATCH 56/57] fix cleanupstale --- .../Integration/IntegrationTests.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index 996f56fd3..accbc639c 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -188,12 +188,17 @@ private async Task CleanupStaleResources() int staleConfigCount = 0; var configSettingsToCleanup = new List(); - AsyncPageable allSettings = _configClient.GetConfigurationSettingsAsync(new SettingSelector + AsyncPageable kvSettings = _configClient.GetConfigurationSettingsAsync(new SettingSelector { - KeyFilter = "*" + TestKeyPrefix + "*" // This will match both regular keys and feature flags + KeyFilter = TestKeyPrefix + "*" }); - await foreach (ConfigurationSetting setting in allSettings) + AsyncPageable flagSettings = _configClient.GetConfigurationSettingsAsync(new SettingSelector + { + KeyFilter = FeatureManagementConstants.FeatureFlagMarker + TestKeyPrefix + "*" + }); + + await foreach (ConfigurationSetting setting in kvSettings.Concat(flagSettings)) { // Check if the setting is older than the threshold if (setting.LastModified < cutoffTime) From b9157e36339f13a1958170b692d6e02c6d6f5117 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 20 May 2025 15:48:25 -0700 Subject: [PATCH 57/57] update setup method, remove flag --- .../Integration/IntegrationTests.cs | 359 ++++++++++-------- 1 file changed, 195 insertions(+), 164 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index accbc639c..b932b3278 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -255,18 +255,7 @@ public async Task DisposeAsync() await CleanupStaleResources(); } - [Flags] - private enum TestDataTypes - { - None = 0, - KeyValues = 1, - FeatureFlags = 2, - KeyVaultReferences = 4, - TaggedSettings = 8, - All = KeyValues | FeatureFlags | KeyVaultReferences - } - - private async Task SetupTestKeys(string testName, TestDataTypes dataTypesToCreate = TestDataTypes.All) + private TestContext CreateTestContext(string testName) { string keyPrefix = GetUniqueKeyPrefix(testName); string sentinelKey = $"{keyPrefix}:Sentinel"; @@ -275,164 +264,175 @@ private async Task SetupTestKeys(string testName, TestDataTypes dat string secretValue = "SecretValue"; string keyVaultReferenceKey = $"{keyPrefix}:KeyVaultRef"; - // Create test-specific settings if requested - if (dataTypesToCreate.HasFlag(TestDataTypes.KeyValues)) + return new TestContext { - var testSettings = new List - { - new ConfigurationSetting($"{keyPrefix}:Setting1", "InitialValue1"), - new ConfigurationSetting($"{keyPrefix}:Setting2", "InitialValue2"), - new ConfigurationSetting(sentinelKey, "Initial") - }; - - foreach (ConfigurationSetting setting in testSettings) - { - await _configClient.SetConfigurationSettingAsync(setting); - } - } + KeyPrefix = keyPrefix, + SentinelKey = sentinelKey, + FeatureFlagKey = featureFlagKey, + KeyVaultReferenceKey = keyVaultReferenceKey, + SecretValue = secretValue + }; + } - // Create feature flag if requested - if (dataTypesToCreate.HasFlag(TestDataTypes.FeatureFlags)) + private async Task SetupKeyValues(TestContext context) + { + var testSettings = new List { - var featureFlagSetting = ConfigurationModelFactory.ConfigurationSetting( - featureFlagKey, - @"{""id"":""" + keyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":false}", - contentType: FeatureFlagContentType); + new ConfigurationSetting($"{context.KeyPrefix}:Setting1", "InitialValue1"), + new ConfigurationSetting($"{context.KeyPrefix}:Setting2", "InitialValue2"), + new ConfigurationSetting(context.SentinelKey, "Initial") + }; - await _configClient.SetConfigurationSettingAsync(featureFlagSetting); + foreach (ConfigurationSetting setting in testSettings) + { + await _configClient.SetConfigurationSettingAsync(setting); } + } - // Create KeyVault reference if requested - if (dataTypesToCreate.HasFlag(TestDataTypes.KeyVaultReferences) && _secretClient != null) + private async Task SetupFeatureFlags(TestContext context) + { + var featureFlagSetting = ConfigurationModelFactory.ConfigurationSetting( + context.FeatureFlagKey, + @"{""id"":""" + context.KeyPrefix + @"Feature"",""description"":""Test feature"",""enabled"":false}", + contentType: FeatureFlagContentType); + + await _configClient.SetConfigurationSettingAsync(featureFlagSetting); + } + + private async Task SetupKeyVaultReferences(TestContext context) + { + if (_secretClient != null) { - await _secretClient.SetSecretAsync(secretName, secretValue); + await _secretClient.SetSecretAsync(context.KeyPrefix + "-secret", context.SecretValue); - string keyVaultUri = $"{_keyVaultEndpoint}secrets/{secretName}"; + string keyVaultUri = $"{_keyVaultEndpoint}secrets/{context.KeyPrefix}-secret"; string keyVaultRefValue = @$"{{""uri"":""{keyVaultUri}""}}"; ConfigurationSetting keyVaultRefSetting = ConfigurationModelFactory.ConfigurationSetting( - keyVaultReferenceKey, + context.KeyVaultReferenceKey, keyVaultRefValue, label: KeyVaultReferenceLabel, contentType: KeyVaultConstants.ContentType); await _configClient.SetConfigurationSettingAsync(keyVaultRefSetting); } + } - // Create tagged settings if requested - if (dataTypesToCreate.HasFlag(TestDataTypes.TaggedSettings)) + private async Task SetupTaggedSettings(TestContext context) + { + // Create configuration settings with various tags + var taggedSettings = new List { - // Create configuration settings with various tags - var taggedSettings = new List - { - // Basic environment tags - CreateSettingWithTags( - $"{keyPrefix}:TaggedSetting1", - "Value1", - new Dictionary { - { "Environment", "Development" }, - { "App", "TestApp" } - }), - - CreateSettingWithTags( - $"{keyPrefix}:TaggedSetting2", - "Value2", - new Dictionary { - { "Environment", "Production" }, - { "App", "TestApp" } - }), - - CreateSettingWithTags( - $"{keyPrefix}:TaggedSetting3", - "Value3", - new Dictionary { - { "Environment", "Development" }, - { "Component", "API" } - }), - - // Special characters in tags - CreateSettingWithTags( - $"{keyPrefix}:TaggedSetting4", - "Value4", - new Dictionary { - { "Special:Tag", "Value:With:Colons" }, - { "Tag@With@At", "Value@With@At" } - }), - - // Empty and null tag values - CreateSettingWithTags( - $"{keyPrefix}:TaggedSetting5", - "Value5", - new Dictionary { - { "EmptyTag", "" }, - { "NullTag", null } - }) - }; + // Basic environment tags + CreateSettingWithTags( + $"{context.KeyPrefix}:TaggedSetting1", + "Value1", + new Dictionary { + { "Environment", "Development" }, + { "App", "TestApp" } + }), - foreach (ConfigurationSetting setting in taggedSettings) - { - await _configClient.SetConfigurationSettingAsync(setting); - } + CreateSettingWithTags( + $"{context.KeyPrefix}:TaggedSetting2", + "Value2", + new Dictionary { + { "Environment", "Production" }, + { "App", "TestApp" } + }), - // Create feature flags with tags - var taggedFeatureFlags = new List - { - // Basic environment tags on feature flags - CreateFeatureFlagWithTags( - $"{keyPrefix}:FeatureDev", - true, - new Dictionary { - { "Environment", "Development" }, - { "App", "TestApp" } - }), - - CreateFeatureFlagWithTags( - $"{keyPrefix}:FeatureProd", - false, - new Dictionary { - { "Environment", "Production" }, - { "App", "TestApp" } - }), - - // Feature flags with special character tags - CreateFeatureFlagWithTags( - $"{keyPrefix}:FeatureSpecial", - true, - new Dictionary { - { "Special:Tag", "Value:With:Colons" } - }), - - // Feature flags with empty/null tags - CreateFeatureFlagWithTags( - $"{keyPrefix}:FeatureEmpty", - false, - new Dictionary { - { "EmptyTag", "" }, - { "NullTag", null } - }) - }; + CreateSettingWithTags( + $"{context.KeyPrefix}:TaggedSetting3", + "Value3", + new Dictionary { + { "Environment", "Development" }, + { "Component", "API" } + }), + + // Special characters in tags + CreateSettingWithTags( + $"{context.KeyPrefix}:TaggedSetting4", + "Value4", + new Dictionary { + { "Special:Tag", "Value:With:Colons" }, + { "Tag@With@At", "Value@With@At" } + }), + + // Empty and null tag values + CreateSettingWithTags( + $"{context.KeyPrefix}:TaggedSetting5", + "Value5", + new Dictionary { + { "EmptyTag", "" }, + { "NullTag", null } + }) + }; - foreach (ConfigurationSetting setting in taggedFeatureFlags) - { - await _configClient.SetConfigurationSettingAsync(setting); - } + foreach (ConfigurationSetting setting in taggedSettings) + { + await _configClient.SetConfigurationSettingAsync(setting); } - return new TestContext + // Create feature flags with tags + var taggedFeatureFlags = new List { - KeyPrefix = keyPrefix, - SentinelKey = sentinelKey, - FeatureFlagKey = featureFlagKey, - KeyVaultReferenceKey = keyVaultReferenceKey, - SecretValue = secretValue + // Basic environment tags on feature flags + CreateFeatureFlagWithTags( + $"{context.KeyPrefix}:FeatureDev", + true, + new Dictionary { + { "Environment", "Development" }, + { "App", "TestApp" } + }), + + CreateFeatureFlagWithTags( + $"{context.KeyPrefix}:FeatureProd", + false, + new Dictionary { + { "Environment", "Production" }, + { "App", "TestApp" } + }), + + // Feature flags with special character tags + CreateFeatureFlagWithTags( + $"{context.KeyPrefix}:FeatureSpecial", + true, + new Dictionary { + { "Special:Tag", "Value:With:Colons" } + }), + + // Feature flags with empty/null tags + CreateFeatureFlagWithTags( + $"{context.KeyPrefix}:FeatureEmpty", + false, + new Dictionary { + { "EmptyTag", "" }, + { "NullTag", null } + }) }; + + foreach (ConfigurationSetting setting in taggedFeatureFlags) + { + await _configClient.SetConfigurationSettingAsync(setting); + } + } + + private async Task SetupAllTestData(string testName) + { + TestContext context = CreateTestContext(testName); + + await SetupKeyValues(context); + await SetupFeatureFlags(context); + await SetupKeyVaultReferences(context); + + return context; } [Fact] public async Task LoadConfiguration_RetrievesValuesFromAppConfiguration() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("BasicConfig", TestDataTypes.KeyValues); + TestContext testContext = CreateTestContext("BasicConfig"); + await SetupKeyValues(testContext); // Act var config = new ConfigurationBuilder() @@ -452,7 +452,8 @@ public async Task LoadConfiguration_RetrievesValuesFromAppConfiguration() public async Task RefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("UpdatesConfig", TestDataTypes.KeyValues); + TestContext testContext = CreateTestContext("UpdatesConfig"); + await SetupKeyValues(testContext); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() @@ -491,7 +492,8 @@ public async Task RefreshAsync_UpdatesConfiguration_WhenSentinelKeyChanged() public async Task RegisterAll_RefreshesAllKeys() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("RefreshesAllKeys", TestDataTypes.KeyValues); + TestContext testContext = CreateTestContext("RefreshesAllKeys"); + await SetupKeyValues(testContext); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() @@ -534,7 +536,8 @@ public async Task RegisterAll_RefreshesAllKeys() public async Task RefreshAsync_SentinelKeyUnchanged() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("SentinelUnchanged", TestDataTypes.KeyValues); + TestContext testContext = CreateTestContext("SentinelUnchanged"); + await SetupKeyValues(testContext); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() @@ -572,7 +575,8 @@ public async Task RefreshAsync_SentinelKeyUnchanged() [Fact] public async Task RefreshAsync_RefreshesFeatureFlags_WhenConfigured() { - TestContext testContext = await SetupTestKeys("FeatureFlagRefresh", TestDataTypes.FeatureFlags); + TestContext testContext = CreateTestContext("FeatureFlagRefresh"); + await SetupFeatureFlags(testContext); IConfigurationRefresher refresher = null; var config = new ConfigurationBuilder() @@ -614,7 +618,8 @@ await _configClient.SetConfigurationSettingAsync( [Fact] public async Task UseFeatureFlags_WithClientFiltersAndConditions() { - TestContext testContext = await SetupTestKeys("FeatureFlagFilters", TestDataTypes.FeatureFlags); + TestContext testContext = CreateTestContext("FeatureFlagFilters"); + await SetupFeatureFlags(testContext); // Create a feature flag with complex conditions await _configClient.SetConfigurationSettingAsync( @@ -665,8 +670,10 @@ await _configClient.SetConfigurationSettingAsync( [Fact] public async Task MultipleProviders_LoadAndRefresh() { - TestContext testContext1 = await SetupTestKeys("MultiProviderTest1", TestDataTypes.KeyValues); - TestContext testContext2 = await SetupTestKeys("MultiProviderTest2", TestDataTypes.KeyValues); + TestContext testContext1 = CreateTestContext("MultiProviderTest1"); + await SetupKeyValues(testContext1); + TestContext testContext2 = CreateTestContext("MultiProviderTest2"); + await SetupKeyValues(testContext2); IConfigurationRefresher refresher1 = null; IConfigurationRefresher refresher2 = null; @@ -719,7 +726,8 @@ public async Task MultipleProviders_LoadAndRefresh() [Fact] public async Task FeatureFlag_WithVariants() { - TestContext testContext = await SetupTestKeys("FeatureFlagVariants", TestDataTypes.FeatureFlags); + TestContext testContext = CreateTestContext("FeatureFlagVariants"); + await SetupFeatureFlags(testContext); await _configClient.SetConfigurationSettingAsync( ConfigurationModelFactory.ConfigurationSetting( @@ -781,7 +789,8 @@ await _configClient.SetConfigurationSettingAsync( [Fact] public async Task JsonContentType_LoadsAndFlattensHierarchicalData() { - TestContext testContext = await SetupTestKeys("JsonContent", TestDataTypes.KeyValues); + TestContext testContext = CreateTestContext("JsonContent"); + await SetupKeyValues(testContext); // Create a complex JSON structure string jsonKey = $"{testContext.KeyPrefix}:JsonConfig"; @@ -827,7 +836,9 @@ await _configClient.SetConfigurationSettingAsync( public async Task MethodOrderingDoesNotAffectConfiguration() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("MethodOrdering", TestDataTypes.KeyValues | TestDataTypes.FeatureFlags); + TestContext testContext = CreateTestContext("MethodOrdering"); + await SetupKeyValues(testContext); + await SetupFeatureFlags(testContext); // Add an additional feature flag for testing await _configClient.SetConfigurationSettingAsync( @@ -1002,7 +1013,9 @@ await _configClient.SetConfigurationSettingAsync( public async Task RegisterWithRefreshAllAndRegisterAll_BehaveIdentically() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("RefreshEquivalency", TestDataTypes.KeyValues | TestDataTypes.FeatureFlags); + TestContext testContext = CreateTestContext("RefreshEquivalency"); + await SetupKeyValues(testContext); + await SetupFeatureFlags(testContext); // Add another feature flag for testing string secondFeatureFlagKey = $".appconfig.featureflag/{testContext.KeyPrefix}Feature2"; @@ -1136,7 +1149,8 @@ await _configClient.SetConfigurationSettingAsync( public async Task HandlesFailoverOnStartup() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("FailoverStartup", TestDataTypes.KeyValues); + TestContext testContext = CreateTestContext("FailoverStartup"); + await SetupKeyValues(testContext); IConfigurationRefresher refresher = null; string connectionString = _connectionString; @@ -1173,7 +1187,8 @@ public async Task HandlesFailoverOnStartup() public async Task LoadSnapshot_RetrievesValuesFromSnapshot() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("SnapshotTest", TestDataTypes.KeyValues); + TestContext testContext = CreateTestContext("SnapshotTest"); + await SetupKeyValues(testContext); string snapshotName = $"snapshot-{testContext.KeyPrefix}"; // Create a snapshot with the test keys @@ -1203,7 +1218,7 @@ public async Task LoadSnapshot_RetrievesValuesFromSnapshot() public async Task LoadSnapshot_ThrowsException_WhenSnapshotDoesNotExist() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("NonExistentSnapshotTest", TestDataTypes.KeyValues); + TestContext testContext = CreateTestContext("NonExistentSnapshotTest"); string nonExistentSnapshotName = $"snapshot-does-not-exist-{Guid.NewGuid()}"; // Act & Assert - Loading a non-existent snapshot should throw @@ -1226,8 +1241,10 @@ public async Task LoadSnapshot_ThrowsException_WhenSnapshotDoesNotExist() public async Task LoadMultipleSnapshots_MergesConfigurationCorrectly() { // Arrange - Setup test-specific keys for two separate snapshots - TestContext testContext1 = await SetupTestKeys("SnapshotMergeTest1", TestDataTypes.KeyValues); - TestContext testContext2 = await SetupTestKeys("SnapshotMergeTest2", TestDataTypes.KeyValues); + TestContext testContext1 = CreateTestContext("SnapshotMergeTest1"); + await SetupKeyValues(testContext1); + TestContext testContext2 = CreateTestContext("SnapshotMergeTest2"); + await SetupKeyValues(testContext2); // Create specific values for second snapshot await _configClient.SetConfigurationSettingAsync( @@ -1269,7 +1286,8 @@ await _configClient.SetConfigurationSettingAsync( public async Task SnapshotCompositionTypes_AreHandledCorrectly() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("SnapshotCompositionTest", TestDataTypes.KeyValues); + TestContext testContext = CreateTestContext("SnapshotCompositionTest"); + await SetupKeyValues(testContext); string keyOnlySnapshotName = $"snapshot-key-{testContext.KeyPrefix}"; string invalidCompositionSnapshotName = $"snapshot-invalid-{testContext.KeyPrefix}"; @@ -1335,7 +1353,8 @@ public async Task SnapshotCompositionTypes_AreHandledCorrectly() public async Task SnapshotWithFeatureFlags_LoadsConfigurationCorrectly() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("SnapshotFeatureFlagTest", TestDataTypes.FeatureFlags); + TestContext testContext = CreateTestContext("SnapshotFeatureFlagTest"); + await SetupFeatureFlags(testContext); string snapshotName = $"snapshot-ff-{testContext.KeyPrefix}"; // Update the feature flag to be enabled before creating the snapshot @@ -1391,9 +1410,16 @@ await _configClient.SetConfigurationSettingAsync( public async Task CallOrdering_SnapshotsWithSelectAndFeatureFlags() { // Arrange - Setup test-specific keys for multiple snapshots - TestContext mainContext = await SetupTestKeys("SnapshotOrdering", TestDataTypes.KeyValues | TestDataTypes.FeatureFlags); - TestContext secondContext = await SetupTestKeys("SnapshotOrdering2", TestDataTypes.KeyValues | TestDataTypes.FeatureFlags); - TestContext thirdContext = await SetupTestKeys("SnapshotOrdering3", TestDataTypes.KeyValues | TestDataTypes.FeatureFlags); + TestContext mainContext = CreateTestContext("SnapshotOrdering"); + await SetupKeyValues(mainContext); + await SetupFeatureFlags(mainContext); + + TestContext secondContext = CreateTestContext("SnapshotOrdering2"); + await SetupKeyValues(secondContext); + await SetupFeatureFlags(secondContext); + + TestContext thirdContext = CreateTestContext("SnapshotOrdering3"); + await SetupKeyValues(thirdContext); // Create specific values for each snapshot await _configClient.SetConfigurationSettingAsync( @@ -1533,7 +1559,8 @@ await _configClient.SetConfigurationSettingAsync( public async Task KeyVaultReferences_ResolveCorrectly() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("KeyVaultReference", TestDataTypes.KeyVaultReferences); + TestContext testContext = CreateTestContext("KeyVaultReference"); + await SetupKeyVaultReferences(testContext); // Act - Create configuration with Key Vault support var config = new ConfigurationBuilder() @@ -1556,7 +1583,8 @@ public async Task KeyVaultReferences_ResolveCorrectly() public async Task KeyVaultReference_UsesCache_DoesNotCallKeyVaultAgain() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("KeyVaultCacheTest", TestDataTypes.KeyVaultReferences); + TestContext testContext = CreateTestContext("KeyVaultCacheTest"); + await SetupKeyVaultReferences(testContext); // Create a monitoring client to track calls to Key Vault int requestCount = 0; @@ -1600,7 +1628,7 @@ public async Task KeyVaultReference_UsesCache_DoesNotCallKeyVaultAgain() public async Task KeyVaultReference_DifferentRefreshIntervals() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("KeyVaultDifferentIntervals", TestDataTypes.KeyVaultReferences); + TestContext testContext = CreateTestContext("KeyVaultDifferentIntervals"); IConfigurationRefresher refresher = null; // Create a secret in Key Vault with short refresh interval @@ -1676,7 +1704,9 @@ await _configClient.SetConfigurationSettingAsync( public async Task RequestTracing_SetsCorrectCorrelationContextHeader() { // Arrange - Setup test-specific keys - TestContext testContext = await SetupTestKeys("RequestTracing"); + TestContext testContext = CreateTestContext("RequestTracing"); + await SetupFeatureFlags(testContext); + await SetupKeyVaultReferences(testContext); // Used to trigger FMVer tag in request tracing IFeatureManager featureManager; @@ -1773,7 +1803,8 @@ await _configClient.SetConfigurationSettingAsync( [Fact] public async Task TagFilters() { - TestContext testContext = await SetupTestKeys("TagFilters", TestDataTypes.TaggedSettings); + TestContext testContext = CreateTestContext("TagFilters"); + await SetupTaggedSettings(testContext); string keyPrefix = testContext.KeyPrefix; // Test case 1: Basic tag filtering with single tag