From cc52ab2ab320d7c7f603ecf7d0d6a935582a5d74 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sat, 26 Jul 2025 09:46:55 +0400 Subject: [PATCH 01/22] =?UTF-8?q?=E2=9C=A8=20feat(dependency-injection):?= =?UTF-8?q?=20add=20LaunchDarkly=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a new project for LaunchDarkly OpenFeature dependency injection, including configuration for target frameworks, versioning, and package references. Add extension methods to `OpenFeatureBuilder` for registering the LaunchDarkly provider as both a default and domain-scoped feature provider, enhancing flexibility in configuration. --- ....ServerProvider.DependencyInjection.csproj | 56 +++++++++++++++++ ...aunchDarklyOpenFeatureBuilderExtensions.cs | 60 +++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.csproj create mode 100644 src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.csproj b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.csproj new file mode 100644 index 0000000..8fbc2fc --- /dev/null +++ b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.csproj @@ -0,0 +1,56 @@ + + + + + 2.1.1 + + + netstandard2.0;net471;net6.0;net8.0 + $(BUILDFRAMEWORKS) + + portable + LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection + Library + LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection + LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection + 7.3 + + Dependency injection support for the LaunchDarkly OpenFeature .NET Server Provider. + Enables seamless integration of LaunchDarkly feature flagging with .NET applications via OpenFeature’s DI patterns. + + LaunchDarkly + LaunchDarkly + LaunchDarkly + LaunchDarkly + LaunchDarkly + Copyright 2022 LaunchDarkly + Apache-2.0 + https://github.com/launchdarkly/openfeature-dotnet-server + https://github.com/launchdarkly/openfeature-dotnet-server + main + true + snupkg + + + 1570,1571,1572,1573,1574,1580,1581,1584,1591,1710,1711,1712 + + + + + + + + + + + + + + + + bin\$(Configuration)\$(TargetFramework)\LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.xml + + diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs new file mode 100644 index 0000000..03f34af --- /dev/null +++ b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs @@ -0,0 +1,60 @@ +using LaunchDarkly.Sdk.Server; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenFeature; +using OpenFeature.DependencyInjection; +using System; + +namespace LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection +{ + /// + /// Contains extension methods for the class. + /// + public static partial class OpenFeatureBuilderExtensions + { + /// + /// Registers the LaunchDarkly as the default within the OpenFeature system, + /// utilizing the specified standard key and optional configuration. + /// + /// The instance used for configuration. + /// The standard key employed to initialize the LaunchDarkly configuration. + /// An optional delegate to further configure the LaunchDarkly . + /// The configured instance with the LaunchDarkly provider registered. + public static OpenFeatureBuilder AddLaunchDarkly(this OpenFeatureBuilder builder, string stdKey, Action configure = null) + { + builder.Services.TryAddSingleton(_ => { + var configBuilder = Configuration.Builder(stdKey); + configure?.Invoke(configBuilder); + return configBuilder.Build(); + }); + + return builder.AddProvider(serviceProvider => { + var config = serviceProvider.GetRequiredService(); + return new Provider(config); + }); + } + + /// + /// Registers the LaunchDarkly as a domain-scoped within the OpenFeature system, + /// allowing for isolated configurations per domain using the specified standard key and optional configuration. + /// + /// The instance used for configuration. + /// The domain identifier to associate with the provider (e.g., tenant or environment). + /// The standard key employed to initialize the LaunchDarkly configuration. + /// An optional delegate to further configure the LaunchDarkly . + /// The configured instance with the domain-scoped LaunchDarkly provider registered. + public static OpenFeatureBuilder AddLaunchDarkly(this OpenFeatureBuilder builder, string domain, string stdKey, Action configure = null) + { + builder.Services.TryAddKeyedSingleton(domain, (_, obj) => { + var configBuilder = Configuration.Builder(stdKey); + configure?.Invoke(configBuilder); + return configBuilder.Build(); + }); + + return builder.AddProvider(domain, (serviceProvider, key) => { + var config = serviceProvider.GetRequiredKeyedService(key); + return new Provider(config); + }); + } + } +} From 129212024b02b8940d58392620597b02e9fb5af5 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sat, 26 Jul 2025 09:47:31 +0400 Subject: [PATCH 02/22] =?UTF-8?q?=E2=9C=85=20test:=20enhance=20LaunchDarkl?= =?UTF-8?q?y=20integration=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated the solution to Visual Studio 17 and added two new projects for dependency injection and its tests. Introduced integration tests for the `AddLaunchDarkly` method, ensuring correct behavior in various scenarios. Created a project file for the new tests with necessary dependencies. Added unit tests to validate edge cases for the provider registration. --- LaunchDarkly.OpenFeature.ServerProvider.sln | 26 +- .../DependencyInjectionIntegrationTests.cs | 236 +++++++++++++++ ...rProvider.DependencyInjection.Tests.csproj | 43 +++ .../OpenFeatureBuilderExtensionsTests.cs | 274 ++++++++++++++++++ 4 files changed, 570 insertions(+), 9 deletions(-) create mode 100644 test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/DependencyInjectionIntegrationTests.cs create mode 100644 test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj create mode 100644 test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs diff --git a/LaunchDarkly.OpenFeature.ServerProvider.sln b/LaunchDarkly.OpenFeature.ServerProvider.sln index 71d0189..a6fdb7b 100644 --- a/LaunchDarkly.OpenFeature.ServerProvider.sln +++ b/LaunchDarkly.OpenFeature.ServerProvider.sln @@ -1,32 +1,40 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30114.105 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36127.28 d17.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.OpenFeature.ServerProvider", "src\LaunchDarkly.OpenFeature.ServerProvider\LaunchDarkly.OpenFeature.ServerProvider.csproj", "{B61EC563-2D25-47C8-86A4-0C3A8C625109}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.OpenFeature.ServerProvider.Tests", "test\LaunchDarkly.OpenFeature.ServerProvider.Tests\LaunchDarkly.OpenFeature.ServerProvider.Tests.csproj", "{A1B1DB34-6B22-4AA4-9974-6EE67145BDB9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection", "src\LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection\LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.csproj", "{DB523227-21AE-4B92-A263-05050CEF6C8D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests", "test\LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests\LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj", "{F2C8E3A6-12D4-4B8F-9A7E-3F5C8D6B4E2A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {B61EC563-2D25-47C8-86A4-0C3A8C625109}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B61EC563-2D25-47C8-86A4-0C3A8C625109}.Debug|Any CPU.Build.0 = Debug|Any CPU {B61EC563-2D25-47C8-86A4-0C3A8C625109}.Release|Any CPU.ActiveCfg = Release|Any CPU {B61EC563-2D25-47C8-86A4-0C3A8C625109}.Release|Any CPU.Build.0 = Release|Any CPU - {E8CE160B-0A65-480F-AA3F-028AD9F17F6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E8CE160B-0A65-480F-AA3F-028AD9F17F6E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E8CE160B-0A65-480F-AA3F-028AD9F17F6E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E8CE160B-0A65-480F-AA3F-028AD9F17F6E}.Release|Any CPU.Build.0 = Release|Any CPU {A1B1DB34-6B22-4AA4-9974-6EE67145BDB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B1DB34-6B22-4AA4-9974-6EE67145BDB9}.Debug|Any CPU.Build.0 = Debug|Any CPU {A1B1DB34-6B22-4AA4-9974-6EE67145BDB9}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B1DB34-6B22-4AA4-9974-6EE67145BDB9}.Release|Any CPU.Build.0 = Release|Any CPU + {DB523227-21AE-4B92-A263-05050CEF6C8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB523227-21AE-4B92-A263-05050CEF6C8D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB523227-21AE-4B92-A263-05050CEF6C8D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB523227-21AE-4B92-A263-05050CEF6C8D}.Release|Any CPU.Build.0 = Release|Any CPU + {F2C8E3A6-12D4-4B8F-9A7E-3F5C8D6B4E2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2C8E3A6-12D4-4B8F-9A7E-3F5C8D6B4E2A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2C8E3A6-12D4-4B8F-9A7E-3F5C8D6B4E2A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2C8E3A6-12D4-4B8F-9A7E-3F5C8D6B4E2A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection EndGlobal diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/DependencyInjectionIntegrationTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/DependencyInjectionIntegrationTests.cs new file mode 100644 index 0000000..5f69366 --- /dev/null +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/DependencyInjectionIntegrationTests.cs @@ -0,0 +1,236 @@ +using System; +using System.Threading.Tasks; +using LaunchDarkly.Sdk.Server; +using Microsoft.Extensions.DependencyInjection; +using OpenFeature; +using OpenFeature.Constant; +using OpenFeature.DependencyInjection; +using OpenFeature.Model; +using Xunit; + +namespace LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests +{ + public class DependencyInjectionIntegrationTests + { + private const string TestSdkKey = "test-sdk-key"; + private const string TestDomain = "test-domain"; + private const string TestFlagKey = "test-flag"; + + [Fact] + public async Task AddLaunchDarkly_CanResolveDefaultProvider() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + builder.AddLaunchDarkly(TestSdkKey, config => config.Offline(true)); + + var serviceProvider = services.BuildServiceProvider(); + var api = serviceProvider.GetRequiredService(); + + // Act + // Since we're using offline mode, the flag will return the default value + var result = await api.GetBooleanValueAsync(TestFlagKey, false); + + // Assert + Assert.False(result); // Default value should be returned in offline mode + } + + [Fact] + public async Task AddLaunchDarkly_CanResolveDomainScopedProvider() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + builder.AddLaunchDarkly(TestDomain, TestSdkKey, config => config.Offline(true)); + + var serviceProvider = services.BuildServiceProvider(); + var api = serviceProvider.GetRequiredKeyedService(TestDomain); + + // Act + // Since we're using offline mode, the flag will return the default value + var result = await api.GetBooleanValueAsync(TestFlagKey, true); + + // Assert + Assert.True(result); // Default value should be returned in offline mode + } + + [Fact] + public void AddLaunchDarkly_ProvidersFromDifferentDomainsAreDistinct() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + const string domain1 = "domain1"; + const string domain2 = "domain2"; + + builder.AddLaunchDarkly(domain1, TestSdkKey, config => config.Offline(true)); + builder.AddLaunchDarkly(domain2, TestSdkKey, config => config.Offline(true)); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var client1 = serviceProvider.GetRequiredKeyedService(domain1); + var client2 = serviceProvider.GetRequiredKeyedService(domain2); + + // Assert + Assert.NotSame(client1, client2); + } + + [Fact] + public void AddLaunchDarkly_ConfigurationIsSharedWithinSameDomain() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + builder.AddLaunchDarkly(TestDomain, TestSdkKey, config => config.Offline(true)); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var config1 = serviceProvider.GetRequiredKeyedService(TestDomain); + var config2 = serviceProvider.GetRequiredKeyedService(TestDomain); + + // Assert + Assert.Same(config1, config2); + Assert.True(config1.Offline); + } + + [Fact] + public void AddLaunchDarkly_DefaultConfigurationIsShared() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + builder.AddLaunchDarkly(TestSdkKey, config => config.Offline(true)); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var config1 = serviceProvider.GetRequiredService(); + var config2 = serviceProvider.GetRequiredService(); + + // Assert + Assert.Same(config1, config2); + Assert.True(config1.Offline); + } + + [Fact] + public async Task AddLaunchDarkly_ProviderSupportsAllValueTypes() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + builder.AddLaunchDarkly(TestSdkKey, config => config.Offline(true)); + + var serviceProvider = services.BuildServiceProvider(); + var api = serviceProvider.GetRequiredService(); + + // Act & Assert - Test all supported types + var boolResult = await api.GetBooleanValueAsync("bool-flag", true); + Assert.True(boolResult); + + var stringResult = await api.GetStringValueAsync("string-flag", "default"); + Assert.Equal("default", stringResult); + + var intResult = await api.GetIntegerValueAsync("int-flag", 42); + Assert.Equal(42, intResult); + + var doubleResult = await api.GetDoubleValueAsync("double-flag", 3.14); + Assert.Equal(3.14, doubleResult); + + var structureResult = await api.GetObjectValueAsync("object-flag", new Value("default")); + Assert.Equal("default", structureResult.AsString); + } + + [Fact] + public async Task AddLaunchDarkly_ProviderReturnsCorrectReasonInOfflineMode() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + builder.AddLaunchDarkly(TestSdkKey, config => config.Offline(true)); + + var serviceProvider = services.BuildServiceProvider(); + var api = serviceProvider.GetRequiredService(); + + // Act + var result = await api.GetBooleanDetailsAsync(TestFlagKey, false); + + // Assert + Assert.False(result.Value); + Assert.Equal(Reason.Default, result.Reason); + } + + [Fact] + public void AddLaunchDarkly_WithNullBuilder_ThrowsArgumentNullException() + { + // Arrange + OpenFeatureBuilder builder = null; + + // Act & Assert + Assert.Throws(() => builder.AddLaunchDarkly(TestSdkKey)); + } + + [Fact] + public void AddLaunchDarkly_WithNullBuilderForDomain_ThrowsArgumentNullException() + { + // Arrange + OpenFeatureBuilder builder = null; + + // Act & Assert + Assert.Throws(() => builder.AddLaunchDarkly(TestDomain, TestSdkKey)); + } + + [Fact] + public void AddLaunchDarkly_CustomConfigurationIsApplied() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var startWaitTime = TimeSpan.FromSeconds(1); + + // Act + builder.AddLaunchDarkly(TestSdkKey, cfg => + { + cfg.Offline(true); + cfg.StartWaitTime(startWaitTime); + }); + + var serviceProvider = services.BuildServiceProvider(); + var config = serviceProvider.GetRequiredService(); + + // Assert + Assert.True(config.Offline); + Assert.Equal(startWaitTime, config.StartWaitTime); + } + + [Fact] + public void AddLaunchDarkly_DomainCustomConfigurationIsApplied() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var startWaitTime = TimeSpan.FromSeconds(2); + + // Act + builder.AddLaunchDarkly(TestDomain, TestSdkKey, cfg => + { + cfg.Offline(true); + cfg.StartWaitTime(startWaitTime); + }); + + var serviceProvider = services.BuildServiceProvider(); + var config = serviceProvider.GetRequiredKeyedService(TestDomain); + + // Assert + Assert.True(config.Offline); + Assert.Equal(startWaitTime, config.StartWaitTime); + } + } +} \ No newline at end of file diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj new file mode 100644 index 0000000..7d3c5e5 --- /dev/null +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj @@ -0,0 +1,43 @@ + + + + + net471;net6.0;net8.0 + $(BUILDFRAMEWORKS) + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs new file mode 100644 index 0000000..92caa91 --- /dev/null +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -0,0 +1,274 @@ +using System; +using LaunchDarkly.Sdk.Server; +using Microsoft.Extensions.DependencyInjection; +using OpenFeature.DependencyInjection; +using Xunit; + +namespace LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests +{ + public class OpenFeatureBuilderExtensionsTests + { + private const string TestSdkKey = "test-sdk-key"; + private const string TestDomain = "test-domain"; + + [Fact] + public void AddLaunchDarkly_WithSdkKey_RegistersDefaultProvider() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act + var result = builder.AddLaunchDarkly(TestSdkKey); + + // Assert + Assert.Same(builder, result); + + // Build the service provider to verify registrations + var serviceProvider = services.BuildServiceProvider(); + + // Verify Configuration is registered as singleton + var config1 = serviceProvider.GetRequiredService(); + var config2 = serviceProvider.GetRequiredService(); + Assert.Same(config1, config2); + } + + [Fact] + public void AddLaunchDarkly_WithSdkKeyAndConfiguration_RegistersDefaultProviderWithConfiguration() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var configureWasCalled = false; + ConfigurationBuilder capturedBuilder = null; + + // Act + var result = builder.AddLaunchDarkly(TestSdkKey, configBuilder => + { + configureWasCalled = true; + capturedBuilder = configBuilder; + configBuilder.Offline(true); // Set some configuration for testing + }); + + // Assert + Assert.Same(builder, result); + + // Build the service provider to verify configuration was applied + var serviceProvider = services.BuildServiceProvider(); + var config = serviceProvider.GetRequiredService(); + + Assert.True(configureWasCalled); + Assert.NotNull(capturedBuilder); + Assert.True(config.Offline); + } + + [Fact] + public void AddLaunchDarkly_WithNullSdkKey_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => builder.AddLaunchDarkly(null)); + } + + [Fact] + public void AddLaunchDarkly_WithEmptySdkKey_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => builder.AddLaunchDarkly(string.Empty)); + } + + [Fact] + public void AddLaunchDarkly_WithDomainAndSdkKey_RegistersDomainScopedProvider() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act + var result = builder.AddLaunchDarkly(TestDomain, TestSdkKey); + + // Assert + Assert.Same(builder, result); + + // Build the service provider to verify registrations + var serviceProvider = services.BuildServiceProvider(); + + // Verify Configuration is registered as keyed singleton + var config1 = serviceProvider.GetRequiredKeyedService(TestDomain); + var config2 = serviceProvider.GetRequiredKeyedService(TestDomain); + Assert.Same(config1, config2); + } + + [Fact] + public void AddLaunchDarkly_WithDomainSdkKeyAndConfiguration_RegistersDomainScopedProviderWithConfiguration() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var configureWasCalled = false; + ConfigurationBuilder capturedBuilder = null; + + // Act + var result = builder.AddLaunchDarkly(TestDomain, TestSdkKey, configBuilder => + { + configureWasCalled = true; + capturedBuilder = configBuilder; + configBuilder.Offline(true); // Set some configuration for testing + }); + + // Assert + Assert.Same(builder, result); + + // Build the service provider to verify configuration was applied + var serviceProvider = services.BuildServiceProvider(); + var config = serviceProvider.GetRequiredKeyedService(TestDomain); + + Assert.True(configureWasCalled); + Assert.NotNull(capturedBuilder); + Assert.True(config.Offline); + } + + [Fact] + public void AddLaunchDarkly_WithNullDomain_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => builder.AddLaunchDarkly(null, TestSdkKey)); + } + + [Fact] + public void AddLaunchDarkly_WithEmptyDomain_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => builder.AddLaunchDarkly(string.Empty, TestSdkKey)); + } + + [Fact] + public void AddLaunchDarkly_WithDomainAndNullSdkKey_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => builder.AddLaunchDarkly(TestDomain, null)); + } + + [Fact] + public void AddLaunchDarkly_WithDomainAndEmptySdkKey_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => builder.AddLaunchDarkly(TestDomain, string.Empty)); + } + + [Fact] + public void AddLaunchDarkly_MultipleCallsWithSameSdkKey_UsesSameConfigurationInstance() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act + builder.AddLaunchDarkly(TestSdkKey); + builder.AddLaunchDarkly(TestSdkKey); // Second call should not replace the first due to TryAddSingleton + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var config1 = serviceProvider.GetRequiredService(); + var config2 = serviceProvider.GetRequiredService(); + Assert.Same(config1, config2); + } + + [Fact] + public void AddLaunchDarkly_MultipleCallsWithSameDomain_UsesSameConfigurationInstance() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act + builder.AddLaunchDarkly(TestDomain, TestSdkKey); + builder.AddLaunchDarkly(TestDomain, TestSdkKey); // Second call should not replace the first due to TryAddKeyedSingleton + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var config1 = serviceProvider.GetRequiredKeyedService(TestDomain); + var config2 = serviceProvider.GetRequiredKeyedService(TestDomain); + Assert.Same(config1, config2); + } + + [Fact] + public void AddLaunchDarkly_WithDifferentDomains_RegistersSeparateConfigurations() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + const string domain1 = "domain1"; + const string domain2 = "domain2"; + + // Act + builder.AddLaunchDarkly(domain1, TestSdkKey); + builder.AddLaunchDarkly(domain2, TestSdkKey); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var config1 = serviceProvider.GetRequiredKeyedService(domain1); + var config2 = serviceProvider.GetRequiredKeyedService(domain2); + Assert.NotSame(config1, config2); + } + + [Fact] + public void AddLaunchDarkly_ConfigurationDelegateException_PropagatesException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var expectedException = new InvalidOperationException("Test exception"); + + // Act + builder.AddLaunchDarkly(TestSdkKey, _ => throw expectedException); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var actualException = Assert.Throws(() => + serviceProvider.GetRequiredService()); + Assert.Same(expectedException, actualException); + } + + [Fact] + public void AddLaunchDarkly_DomainConfigurationDelegateException_PropagatesException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var expectedException = new InvalidOperationException("Test exception"); + + // Act + builder.AddLaunchDarkly(TestDomain, TestSdkKey, _ => throw expectedException); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var actualException = Assert.Throws(() => + serviceProvider.GetRequiredKeyedService(TestDomain)); + Assert.Same(expectedException, actualException); + } + } +} \ No newline at end of file From d9ec58bf97cd013137934d7bce365cb242e8c549 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sat, 26 Jul 2025 10:43:52 +0400 Subject: [PATCH 03/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(dependency-?= =?UTF-8?q?injection):=20rename=20methods=20and=20update=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated project and test files to rename methods from `AddLaunchDarkly` to `UseLaunchDarkly` for clarity and improved configuration handling. Added new properties in the project file for editor support and debug type. Cleaned up dependencies in the test project and adjusted tests to validate the new method signatures and behaviors. --- ....ServerProvider.DependencyInjection.csproj | 8 +- ...aunchDarklyOpenFeatureBuilderExtensions.cs | 59 ++++++---- .../DependencyInjectionIntegrationTests.cs | 102 ++++++++++-------- ...rProvider.DependencyInjection.Tests.csproj | 4 - .../OpenFeatureBuilderExtensionsTests.cs | 93 ++++++++-------- 5 files changed, 149 insertions(+), 117 deletions(-) diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.csproj b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.csproj index 8fbc2fc..b6ca340 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.csproj +++ b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.csproj @@ -11,6 +11,8 @@ netstandard2.0;net471;net6.0;net8.0 $(BUILDFRAMEWORKS) + false + portable LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection Library @@ -38,12 +40,8 @@ 1570,1571,1572,1573,1574,1580,1581,1584,1591,1710,1711,1712 - - - - - + diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs index 03f34af..208ebee 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs +++ b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs @@ -8,25 +8,23 @@ namespace LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection { /// - /// Contains extension methods for the class. + /// Provides extension methods for configuring the with LaunchDarkly support. /// public static partial class OpenFeatureBuilderExtensions { /// - /// Registers the LaunchDarkly as the default within the OpenFeature system, - /// utilizing the specified standard key and optional configuration. + /// Registers the LaunchDarkly as the default in the OpenFeature system, + /// using the specified standard key and an optional configuration delegate. /// - /// The instance used for configuration. - /// The standard key employed to initialize the LaunchDarkly configuration. - /// An optional delegate to further configure the LaunchDarkly . - /// The configured instance with the LaunchDarkly provider registered. - public static OpenFeatureBuilder AddLaunchDarkly(this OpenFeatureBuilder builder, string stdKey, Action configure = null) + /// The used to configure the OpenFeature system. + /// The standard key used to initialize the LaunchDarkly configuration. + /// An optional delegate for customizing the . + /// The instance with the LaunchDarkly provider registered. + public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, string stdKey, Action configure = null) { - builder.Services.TryAddSingleton(_ => { - var configBuilder = Configuration.Builder(stdKey); - configure?.Invoke(configBuilder); - return configBuilder.Build(); - }); + EnsureValidConfiguration(stdKey, configure); + + builder.Services.TryAddSingleton(_ => CreateConfiguration(stdKey, configure)); return builder.AddProvider(serviceProvider => { var config = serviceProvider.GetRequiredService(); @@ -43,18 +41,41 @@ public static OpenFeatureBuilder AddLaunchDarkly(this OpenFeatureBuilder builder /// The standard key employed to initialize the LaunchDarkly configuration. /// An optional delegate to further configure the LaunchDarkly . /// The configured instance with the domain-scoped LaunchDarkly provider registered. - public static OpenFeatureBuilder AddLaunchDarkly(this OpenFeatureBuilder builder, string domain, string stdKey, Action configure = null) + public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, string domain, string stdKey, Action configure = null) { - builder.Services.TryAddKeyedSingleton(domain, (_, obj) => { - var configBuilder = Configuration.Builder(stdKey); - configure?.Invoke(configBuilder); - return configBuilder.Build(); - }); + EnsureValidConfiguration(stdKey, configure); + + builder.Services.TryAddKeyedSingleton(domain, (_, obj) => CreateConfiguration(stdKey, configure)); return builder.AddProvider(domain, (serviceProvider, key) => { var config = serviceProvider.GetRequiredKeyedService(key); return new Provider(config); }); } + + /// + /// Ensures that the LaunchDarkly configuration can be created successfully using the provided key and optional configuration delegate. + /// Throws an exception if the configuration is invalid. + /// + /// The SDK key used to initialize the LaunchDarkly configuration. + /// An optional delegate to customize the . + private static void EnsureValidConfiguration(string stdKey, Action configure = null) + { + CreateConfiguration(stdKey, configure); + } + + /// + /// Creates a LaunchDarkly using the specified SDK key and optional configuration logic. + /// + /// The SDK key used to initialize the LaunchDarkly configuration. + /// An optional delegate to customize the . + /// A fully built instance. + private static Configuration CreateConfiguration(string stdKey, Action configure) + { + var configBuilder = Configuration.Builder(stdKey); + configure?.Invoke(configBuilder); + return configBuilder.Build(); + } + } } diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/DependencyInjectionIntegrationTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/DependencyInjectionIntegrationTests.cs index 5f69366..be64757 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/DependencyInjectionIntegrationTests.cs +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/DependencyInjectionIntegrationTests.cs @@ -17,15 +17,16 @@ public class DependencyInjectionIntegrationTests private const string TestFlagKey = "test-flag"; [Fact] - public async Task AddLaunchDarkly_CanResolveDefaultProvider() + public async Task UseLaunchDarkly_CanResolveDefaultProvider() { // Arrange var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - builder.AddLaunchDarkly(TestSdkKey, config => config.Offline(true)); + services.AddOpenFeature(builder => + { + builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); + }); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(true); var api = serviceProvider.GetRequiredService(); // Act @@ -37,15 +38,16 @@ public async Task AddLaunchDarkly_CanResolveDefaultProvider() } [Fact] - public async Task AddLaunchDarkly_CanResolveDomainScopedProvider() + public async Task UseLaunchDarkly_CanResolveDomainScopedProvider() { // Arrange var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - builder.AddLaunchDarkly(TestDomain, TestSdkKey, config => config.Offline(true)); + services.AddOpenFeature(builder => + { + builder.UseLaunchDarkly(TestDomain, TestSdkKey, config => config.Offline(true)); + }); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(true); var api = serviceProvider.GetRequiredKeyedService(TestDomain); // Act @@ -57,18 +59,20 @@ public async Task AddLaunchDarkly_CanResolveDomainScopedProvider() } [Fact] - public void AddLaunchDarkly_ProvidersFromDifferentDomainsAreDistinct() + public void UseLaunchDarkly_ProvidersFromDifferentDomainsAreDistinct() { // Arrange var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); const string domain1 = "domain1"; const string domain2 = "domain2"; - - builder.AddLaunchDarkly(domain1, TestSdkKey, config => config.Offline(true)); - builder.AddLaunchDarkly(domain2, TestSdkKey, config => config.Offline(true)); - var serviceProvider = services.BuildServiceProvider(); + services.AddOpenFeature(builder => + { + builder.UseLaunchDarkly(domain1, TestSdkKey, config => config.Offline(true)); + builder.UseLaunchDarkly(domain2, TestSdkKey, config => config.Offline(true)); + }); + + var serviceProvider = services.BuildServiceProvider(true); // Act var client1 = serviceProvider.GetRequiredKeyedService(domain1); @@ -79,15 +83,16 @@ public void AddLaunchDarkly_ProvidersFromDifferentDomainsAreDistinct() } [Fact] - public void AddLaunchDarkly_ConfigurationIsSharedWithinSameDomain() + public void UseLaunchDarkly_ConfigurationIsSharedWithinSameDomain() { // Arrange var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - builder.AddLaunchDarkly(TestDomain, TestSdkKey, config => config.Offline(true)); + services.AddOpenFeature(builder => + { + builder.UseLaunchDarkly(TestDomain, TestSdkKey, config => config.Offline(true)); + }); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(true); // Act var config1 = serviceProvider.GetRequiredKeyedService(TestDomain); @@ -99,15 +104,16 @@ public void AddLaunchDarkly_ConfigurationIsSharedWithinSameDomain() } [Fact] - public void AddLaunchDarkly_DefaultConfigurationIsShared() + public void UseLaunchDarkly_DefaultConfigurationIsShared() { // Arrange var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - builder.AddLaunchDarkly(TestSdkKey, config => config.Offline(true)); + services.AddOpenFeature(builder => + { + builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); + }); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(true); // Act var config1 = serviceProvider.GetRequiredService(); @@ -119,15 +125,16 @@ public void AddLaunchDarkly_DefaultConfigurationIsShared() } [Fact] - public async Task AddLaunchDarkly_ProviderSupportsAllValueTypes() + public async Task UseLaunchDarkly_ProviderSupportsAllValueTypes() { // Arrange var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - builder.AddLaunchDarkly(TestSdkKey, config => config.Offline(true)); + services.AddOpenFeature(builder => + { + builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); + }); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(true); var api = serviceProvider.GetRequiredService(); // Act & Assert - Test all supported types @@ -148,15 +155,16 @@ public async Task AddLaunchDarkly_ProviderSupportsAllValueTypes() } [Fact] - public async Task AddLaunchDarkly_ProviderReturnsCorrectReasonInOfflineMode() + public async Task UseLaunchDarkly_ProviderReturnsCorrectReasonInOfflineMode() { // Arrange var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - builder.AddLaunchDarkly(TestSdkKey, config => config.Offline(true)); + services.AddOpenFeature(builder => + { + builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); + }); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(true); var api = serviceProvider.GetRequiredService(); // Act @@ -168,27 +176,27 @@ public async Task AddLaunchDarkly_ProviderReturnsCorrectReasonInOfflineMode() } [Fact] - public void AddLaunchDarkly_WithNullBuilder_ThrowsArgumentNullException() + public void UseLaunchDarkly_WithNullBuilder_ThrowsArgumentNullException() { // Arrange OpenFeatureBuilder builder = null; // Act & Assert - Assert.Throws(() => builder.AddLaunchDarkly(TestSdkKey)); + Assert.Throws(() => builder.UseLaunchDarkly(TestSdkKey)); } [Fact] - public void AddLaunchDarkly_WithNullBuilderForDomain_ThrowsArgumentNullException() + public void UseLaunchDarkly_WithNullBuilderForDomain_ThrowsArgumentNullException() { // Arrange OpenFeatureBuilder builder = null; // Act & Assert - Assert.Throws(() => builder.AddLaunchDarkly(TestDomain, TestSdkKey)); + Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, TestSdkKey)); } [Fact] - public void AddLaunchDarkly_CustomConfigurationIsApplied() + public void UseLaunchDarkly_CustomConfigurationIsApplied() { // Arrange var services = new ServiceCollection(); @@ -196,13 +204,13 @@ public void AddLaunchDarkly_CustomConfigurationIsApplied() var startWaitTime = TimeSpan.FromSeconds(1); // Act - builder.AddLaunchDarkly(TestSdkKey, cfg => + builder.UseLaunchDarkly(TestSdkKey, cfg => { cfg.Offline(true); cfg.StartWaitTime(startWaitTime); }); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(true); var config = serviceProvider.GetRequiredService(); // Assert @@ -211,7 +219,7 @@ public void AddLaunchDarkly_CustomConfigurationIsApplied() } [Fact] - public void AddLaunchDarkly_DomainCustomConfigurationIsApplied() + public void UseLaunchDarkly_DomainCustomConfigurationIsApplied() { // Arrange var services = new ServiceCollection(); @@ -219,13 +227,13 @@ public void AddLaunchDarkly_DomainCustomConfigurationIsApplied() var startWaitTime = TimeSpan.FromSeconds(2); // Act - builder.AddLaunchDarkly(TestDomain, TestSdkKey, cfg => + builder.UseLaunchDarkly(TestDomain, TestSdkKey, cfg => { cfg.Offline(true); cfg.StartWaitTime(startWaitTime); }); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(true); var config = serviceProvider.GetRequiredKeyedService(TestDomain); // Assert @@ -233,4 +241,4 @@ public void AddLaunchDarkly_DomainCustomConfigurationIsApplied() Assert.Equal(startWaitTime, config.StartWaitTime); } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj index 7d3c5e5..3b283e7 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj @@ -29,10 +29,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs index 92caa91..a7e0bd3 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -12,20 +12,20 @@ public class OpenFeatureBuilderExtensionsTests private const string TestDomain = "test-domain"; [Fact] - public void AddLaunchDarkly_WithSdkKey_RegistersDefaultProvider() + public void UseLaunchDarkly_WithSdkKey_RegistersDefaultProvider() { // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); // Act - var result = builder.AddLaunchDarkly(TestSdkKey); + var result = builder.UseLaunchDarkly(TestSdkKey); // Assert Assert.Same(builder, result); // Build the service provider to verify registrations - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(true); // Verify Configuration is registered as singleton var config1 = serviceProvider.GetRequiredService(); @@ -34,7 +34,7 @@ public void AddLaunchDarkly_WithSdkKey_RegistersDefaultProvider() } [Fact] - public void AddLaunchDarkly_WithSdkKeyAndConfiguration_RegistersDefaultProviderWithConfiguration() + public void UseLaunchDarkly_WithSdkKeyAndConfiguration_RegistersDefaultProviderWithConfiguration() { // Arrange var services = new ServiceCollection(); @@ -43,7 +43,7 @@ public void AddLaunchDarkly_WithSdkKeyAndConfiguration_RegistersDefaultProviderW ConfigurationBuilder capturedBuilder = null; // Act - var result = builder.AddLaunchDarkly(TestSdkKey, configBuilder => + var result = builder.UseLaunchDarkly(TestSdkKey, configBuilder => { configureWasCalled = true; capturedBuilder = configBuilder; @@ -54,7 +54,7 @@ public void AddLaunchDarkly_WithSdkKeyAndConfiguration_RegistersDefaultProviderW Assert.Same(builder, result); // Build the service provider to verify configuration was applied - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(true); var config = serviceProvider.GetRequiredService(); Assert.True(configureWasCalled); @@ -63,42 +63,42 @@ public void AddLaunchDarkly_WithSdkKeyAndConfiguration_RegistersDefaultProviderW } [Fact] - public void AddLaunchDarkly_WithNullSdkKey_ThrowsArgumentException() + public void UseLaunchDarkly_WithNullSdkKey_ThrowsArgumentException() { // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); // Act & Assert - Assert.Throws(() => builder.AddLaunchDarkly(null)); + Assert.Throws(() => builder.UseLaunchDarkly(null)); } [Fact] - public void AddLaunchDarkly_WithEmptySdkKey_ThrowsArgumentException() + public void UseLaunchDarkly_WithEmptySdkKey_ThrowsArgumentException() { // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); // Act & Assert - Assert.Throws(() => builder.AddLaunchDarkly(string.Empty)); + Assert.Throws(() => builder.UseLaunchDarkly(string.Empty)); } [Fact] - public void AddLaunchDarkly_WithDomainAndSdkKey_RegistersDomainScopedProvider() + public void UseLaunchDarkly_WithDomainAndSdkKey_RegistersDomainScopedProvider() { // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); // Act - var result = builder.AddLaunchDarkly(TestDomain, TestSdkKey); + var result = builder.UseLaunchDarkly(TestDomain, TestSdkKey); // Assert Assert.Same(builder, result); // Build the service provider to verify registrations - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(true); // Verify Configuration is registered as keyed singleton var config1 = serviceProvider.GetRequiredKeyedService(TestDomain); @@ -107,7 +107,7 @@ public void AddLaunchDarkly_WithDomainAndSdkKey_RegistersDomainScopedProvider() } [Fact] - public void AddLaunchDarkly_WithDomainSdkKeyAndConfiguration_RegistersDomainScopedProviderWithConfiguration() + public void UseLaunchDarkly_WithDomainSdkKeyAndConfiguration_RegistersDomainScopedProviderWithConfiguration() { // Arrange var services = new ServiceCollection(); @@ -116,7 +116,7 @@ public void AddLaunchDarkly_WithDomainSdkKeyAndConfiguration_RegistersDomainScop ConfigurationBuilder capturedBuilder = null; // Act - var result = builder.AddLaunchDarkly(TestDomain, TestSdkKey, configBuilder => + var result = builder.UseLaunchDarkly(TestDomain, TestSdkKey, configBuilder => { configureWasCalled = true; capturedBuilder = configBuilder; @@ -127,7 +127,7 @@ public void AddLaunchDarkly_WithDomainSdkKeyAndConfiguration_RegistersDomainScop Assert.Same(builder, result); // Build the service provider to verify configuration was applied - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(true); var config = serviceProvider.GetRequiredKeyedService(TestDomain); Assert.True(configureWasCalled); @@ -136,87 +136,96 @@ public void AddLaunchDarkly_WithDomainSdkKeyAndConfiguration_RegistersDomainScop } [Fact] - public void AddLaunchDarkly_WithNullDomain_ThrowsArgumentException() + public void UseLaunchDarkly_WithNullDomain_ThrowsArgumentException() { // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); // Act & Assert - Assert.Throws(() => builder.AddLaunchDarkly(null, TestSdkKey)); + Assert.Throws(() => builder.UseLaunchDarkly(null, TestSdkKey)); } [Fact] - public void AddLaunchDarkly_WithEmptyDomain_ThrowsArgumentException() + public void UseLaunchDarkly_WithEmptyDomain_ThrowsArgumentException() { // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); // Act & Assert - Assert.Throws(() => builder.AddLaunchDarkly(string.Empty, TestSdkKey)); + Assert.Throws(() => builder.UseLaunchDarkly(string.Empty, TestSdkKey)); } [Fact] - public void AddLaunchDarkly_WithDomainAndNullSdkKey_ThrowsArgumentException() + public void UseLaunchDarkly_WithDomainAndNullSdkKey_ThrowsArgumentException() { // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); // Act & Assert - Assert.Throws(() => builder.AddLaunchDarkly(TestDomain, null)); + Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, null)); } [Fact] - public void AddLaunchDarkly_WithDomainAndEmptySdkKey_ThrowsArgumentException() + public void UseLaunchDarkly_WithDomainAndEmptySdkKey_ThrowsArgumentException() { // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); + try + { + builder.UseLaunchDarkly(TestDomain, string.Empty); + } + catch (Exception ex) + { + + } + // Act & Assert - Assert.Throws(() => builder.AddLaunchDarkly(TestDomain, string.Empty)); + Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, string.Empty)); } [Fact] - public void AddLaunchDarkly_MultipleCallsWithSameSdkKey_UsesSameConfigurationInstance() + public void UseLaunchDarkly_MultipleCallsWithSameSdkKey_UsesSameConfigurationInstance() { // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); // Act - builder.AddLaunchDarkly(TestSdkKey); - builder.AddLaunchDarkly(TestSdkKey); // Second call should not replace the first due to TryAddSingleton + builder.UseLaunchDarkly(TestSdkKey); + builder.UseLaunchDarkly(TestSdkKey); // Second call should not replace the first due to TryAddSingleton // Assert - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(true); var config1 = serviceProvider.GetRequiredService(); var config2 = serviceProvider.GetRequiredService(); Assert.Same(config1, config2); } [Fact] - public void AddLaunchDarkly_MultipleCallsWithSameDomain_UsesSameConfigurationInstance() + public void UseLaunchDarkly_MultipleCallsWithSameDomain_UsesSameConfigurationInstance() { // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); // Act - builder.AddLaunchDarkly(TestDomain, TestSdkKey); - builder.AddLaunchDarkly(TestDomain, TestSdkKey); // Second call should not replace the first due to TryAddKeyedSingleton + builder.UseLaunchDarkly(TestDomain, TestSdkKey); + builder.UseLaunchDarkly(TestDomain, TestSdkKey); // Second call should not replace the first due to TryAddKeyedSingleton // Assert - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(true); var config1 = serviceProvider.GetRequiredKeyedService(TestDomain); var config2 = serviceProvider.GetRequiredKeyedService(TestDomain); Assert.Same(config1, config2); } [Fact] - public void AddLaunchDarkly_WithDifferentDomains_RegistersSeparateConfigurations() + public void UseLaunchDarkly_WithDifferentDomains_RegistersSeparateConfigurations() { // Arrange var services = new ServiceCollection(); @@ -225,18 +234,18 @@ public void AddLaunchDarkly_WithDifferentDomains_RegistersSeparateConfigurations const string domain2 = "domain2"; // Act - builder.AddLaunchDarkly(domain1, TestSdkKey); - builder.AddLaunchDarkly(domain2, TestSdkKey); + builder.UseLaunchDarkly(domain1, TestSdkKey); + builder.UseLaunchDarkly(domain2, TestSdkKey); // Assert - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(true); var config1 = serviceProvider.GetRequiredKeyedService(domain1); var config2 = serviceProvider.GetRequiredKeyedService(domain2); Assert.NotSame(config1, config2); } [Fact] - public void AddLaunchDarkly_ConfigurationDelegateException_PropagatesException() + public void UseLaunchDarkly_ConfigurationDelegateException_PropagatesException() { // Arrange var services = new ServiceCollection(); @@ -244,17 +253,17 @@ public void AddLaunchDarkly_ConfigurationDelegateException_PropagatesException() var expectedException = new InvalidOperationException("Test exception"); // Act - builder.AddLaunchDarkly(TestSdkKey, _ => throw expectedException); + builder.UseLaunchDarkly(TestSdkKey, _ => throw expectedException); // Assert - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(true); var actualException = Assert.Throws(() => serviceProvider.GetRequiredService()); Assert.Same(expectedException, actualException); } [Fact] - public void AddLaunchDarkly_DomainConfigurationDelegateException_PropagatesException() + public void UseLaunchDarkly_DomainConfigurationDelegateException_PropagatesException() { // Arrange var services = new ServiceCollection(); @@ -262,10 +271,10 @@ public void AddLaunchDarkly_DomainConfigurationDelegateException_PropagatesExcep var expectedException = new InvalidOperationException("Test exception"); // Act - builder.AddLaunchDarkly(TestDomain, TestSdkKey, _ => throw expectedException); + builder.UseLaunchDarkly(TestDomain, TestSdkKey, _ => throw expectedException); // Assert - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(true); var actualException = Assert.Throws(() => serviceProvider.GetRequiredKeyedService(TestDomain)); Assert.Same(expectedException, actualException); From 86b9c0489fbb1aac392327615025c5d93716094a Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sat, 26 Jul 2025 10:47:09 +0400 Subject: [PATCH 04/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(tests):=20u?= =?UTF-8?q?pdate=20FeatureClient=20to=20IFeatureClient?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor `DependencyInjectionIntegrationTests.cs` to replace instances of `FeatureClient` with `IFeatureClient`. This change improves adherence to dependency inversion principles, enhancing abstraction and flexibility in testing and implementation. --- .../DependencyInjectionIntegrationTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/DependencyInjectionIntegrationTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/DependencyInjectionIntegrationTests.cs index be64757..82c4724 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/DependencyInjectionIntegrationTests.cs +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/DependencyInjectionIntegrationTests.cs @@ -27,7 +27,7 @@ public async Task UseLaunchDarkly_CanResolveDefaultProvider() }); var serviceProvider = services.BuildServiceProvider(true); - var api = serviceProvider.GetRequiredService(); + var api = serviceProvider.GetRequiredService(); // Act // Since we're using offline mode, the flag will return the default value @@ -48,7 +48,7 @@ public async Task UseLaunchDarkly_CanResolveDomainScopedProvider() }); var serviceProvider = services.BuildServiceProvider(true); - var api = serviceProvider.GetRequiredKeyedService(TestDomain); + var api = serviceProvider.GetRequiredKeyedService(TestDomain); // Act // Since we're using offline mode, the flag will return the default value @@ -75,8 +75,8 @@ public void UseLaunchDarkly_ProvidersFromDifferentDomainsAreDistinct() var serviceProvider = services.BuildServiceProvider(true); // Act - var client1 = serviceProvider.GetRequiredKeyedService(domain1); - var client2 = serviceProvider.GetRequiredKeyedService(domain2); + var client1 = serviceProvider.GetRequiredKeyedService(domain1); + var client2 = serviceProvider.GetRequiredKeyedService(domain2); // Assert Assert.NotSame(client1, client2); @@ -135,7 +135,7 @@ public async Task UseLaunchDarkly_ProviderSupportsAllValueTypes() }); var serviceProvider = services.BuildServiceProvider(true); - var api = serviceProvider.GetRequiredService(); + var api = serviceProvider.GetRequiredService(); // Act & Assert - Test all supported types var boolResult = await api.GetBooleanValueAsync("bool-flag", true); @@ -165,7 +165,7 @@ public async Task UseLaunchDarkly_ProviderReturnsCorrectReasonInOfflineMode() }); var serviceProvider = services.BuildServiceProvider(true); - var api = serviceProvider.GetRequiredService(); + var api = serviceProvider.GetRequiredService(); // Act var result = await api.GetBooleanDetailsAsync(TestFlagKey, false); From 8fbfab0cc939dd04943f8ffdc2e1d0aa49ff9ab0 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sat, 26 Jul 2025 11:15:37 +0400 Subject: [PATCH 05/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(dependency-?= =?UTF-8?q?injection):=20rename=20and=20enhance=20LaunchDarkly=20integrati?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Renamed `OpenFeatureBuilderExtensions` to `LaunchDarklyOpenFeatureBuilderExtensions` for clarity. - Updated XML documentation to better describe method functionalities and parameters. - Modified `UseLaunchDarkly` method signatures to accept a `Configuration` object, streamlining configuration. - Added overloads for domain-scoped configurations to improve feature flag management. - Refined internal methods for clarity and ensured configuration validation occurs before registration. - Removed `EnsureValidConfiguration` method, integrating its functionality into the configuration creation process. - Enhanced `CreateConfiguration` method to accept existing `Configuration` instances for improved flexibility. --- ...aunchDarklyOpenFeatureBuilderExtensions.cs | 134 ++++++++++++------ 1 file changed, 93 insertions(+), 41 deletions(-) diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs index 208ebee..3eb51d9 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs +++ b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs @@ -8,74 +8,126 @@ namespace LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection { /// - /// Provides extension methods for configuring the with LaunchDarkly support. + /// Provides extension methods for configuring the to use LaunchDarkly as a . /// - public static partial class OpenFeatureBuilderExtensions + public static partial class LaunchDarklyOpenFeatureBuilderExtensions { /// - /// Registers the LaunchDarkly as the default in the OpenFeature system, - /// using the specified standard key and an optional configuration delegate. + /// Configures the to use LaunchDarkly as the default provider + /// using the specified instance. /// - /// The used to configure the OpenFeature system. - /// The standard key used to initialize the LaunchDarkly configuration. - /// An optional delegate for customizing the . - /// The instance with the LaunchDarkly provider registered. - public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, string stdKey, Action configure = null) - { - EnsureValidConfiguration(stdKey, configure); + /// The instance to configure. + /// A pre-built LaunchDarkly . + /// The updated instance. + public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, Configuration configuration) + => RegisterLaunchDarklyProvider(builder, () => CreateConfiguration(configuration), sp => sp.GetRequiredService()); - builder.Services.TryAddSingleton(_ => CreateConfiguration(stdKey, configure)); + /// + /// Configures the to use LaunchDarkly as a domain-scoped provider + /// using the specified instance. + /// + /// The instance to configure. + /// A domain identifier (e.g., tenant or environment). + /// A pre-built LaunchDarkly specific to the domain. + /// The updated instance. + public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, string domain, Configuration configuration) + => RegisterLaunchDarklyProviderForDomain( + builder, + domain, + () => CreateConfiguration(configuration), + (sp, key) => sp.GetRequiredKeyedService(key)); - return builder.AddProvider(serviceProvider => { - var config = serviceProvider.GetRequiredService(); - return new Provider(config); - }); - } + /// + /// Configures the to use LaunchDarkly as the default provider + /// using the specified SDK key and optional configuration delegate. + /// + /// The instance to configure. + /// The SDK key used to initialize the LaunchDarkly configuration. + /// An optional delegate to customize the . + /// The updated instance. + public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, string stdKey, Action configure = null) + => RegisterLaunchDarklyProvider(builder, () => CreateConfiguration(stdKey, configure), sp => sp.GetRequiredService()); /// - /// Registers the LaunchDarkly as a domain-scoped within the OpenFeature system, - /// allowing for isolated configurations per domain using the specified standard key and optional configuration. + /// Configures the to use LaunchDarkly as a domain-scoped provider + /// using the specified SDK key and optional configuration delegate. /// - /// The instance used for configuration. - /// The domain identifier to associate with the provider (e.g., tenant or environment). - /// The standard key employed to initialize the LaunchDarkly configuration. - /// An optional delegate to further configure the LaunchDarkly . - /// The configured instance with the domain-scoped LaunchDarkly provider registered. + /// The instance to configure. + /// A domain identifier (e.g., tenant or environment). + /// The SDK key used to initialize the LaunchDarkly configuration. + /// An optional delegate to customize the . + /// The updated instance. public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, string domain, string stdKey, Action configure = null) + => RegisterLaunchDarklyProviderForDomain( + builder, + domain, + () => CreateConfiguration(stdKey, configure), + (sp, key) => sp.GetRequiredKeyedService(key)); + + /// + /// Registers LaunchDarkly as the default feature provider using the given configuration factory and resolution logic. + /// + /// The instance to configure. + /// A delegate that returns a instance. + /// A delegate that resolves the from the service provider. + /// The updated instance. + private static OpenFeatureBuilder RegisterLaunchDarklyProvider( + OpenFeatureBuilder builder, + Func createConfiguration, + Func resolveConfiguration) { - EnsureValidConfiguration(stdKey, configure); + // Perform initial configuration validation before registration. + // This ensures that any misconfiguration is detected eagerly during setup. + var config = createConfiguration(); + builder.Services.TryAddSingleton(_ => config); + + return builder.AddProvider(serviceProvider => new Provider(resolveConfiguration(serviceProvider))); + } - builder.Services.TryAddKeyedSingleton(domain, (_, obj) => CreateConfiguration(stdKey, configure)); + /// + /// Registers LaunchDarkly as a domain-scoped feature provider using the given configuration factory and resolution logic. + /// + /// The instance to configure. + /// A domain identifier (e.g., tenant or environment). + /// A delegate that returns a domain-specific instance. + /// A delegate that resolves the domain-scoped from the service provider. + /// The updated instance. + private static OpenFeatureBuilder RegisterLaunchDarklyProviderForDomain( + OpenFeatureBuilder builder, + string domain, + Func createConfiguration, + Func resolveConfiguration) + { + // Perform initial configuration validation before registration. + // This ensures that any misconfiguration is detected eagerly during setup. + var config = createConfiguration(); + builder.Services.TryAddKeyedSingleton(domain, (_, obj) => config); - return builder.AddProvider(domain, (serviceProvider, key) => { - var config = serviceProvider.GetRequiredKeyedService(key); - return new Provider(config); - }); + return builder.AddProvider(domain, (serviceProvider, key) => new Provider(resolveConfiguration(serviceProvider, key))); } /// - /// Ensures that the LaunchDarkly configuration can be created successfully using the provided key and optional configuration delegate. - /// Throws an exception if the configuration is invalid. + /// Creates a new by cloning the specified instance and rebuilding it. /// - /// The SDK key used to initialize the LaunchDarkly configuration. - /// An optional delegate to customize the . - private static void EnsureValidConfiguration(string stdKey, Action configure = null) + /// An existing instance. + /// A rebuilt instance. + private static Configuration CreateConfiguration(Configuration configuration) { - CreateConfiguration(stdKey, configure); + var configBuilder = Configuration.Builder(configuration); + return configBuilder.Build(); } /// - /// Creates a LaunchDarkly using the specified SDK key and optional configuration logic. + /// Creates a new using the specified SDK key and optional configuration delegate. /// - /// The SDK key used to initialize the LaunchDarkly configuration. + /// The SDK key used to initialize the configuration. /// An optional delegate to customize the . - /// A fully built instance. - private static Configuration CreateConfiguration(string stdKey, Action configure) + /// A fully constructed instance. + private static Configuration CreateConfiguration(string stdKey, Action configure = null) { var configBuilder = Configuration.Builder(stdKey); configure?.Invoke(configBuilder); return configBuilder.Build(); } - } } From 92515b1256e0855d6fb3e4eba3daa7e7b4a54fa0 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sat, 26 Jul 2025 11:18:39 +0400 Subject: [PATCH 06/22] =?UTF-8?q?=F0=9F=93=9D=20docs(provider):=20clarify?= =?UTF-8?q?=20early=20configuration=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comments in the `RegisterLaunchDarklyProvider` method were updated to better explain the purpose of early configuration validation, emphasizing its role in preventing runtime failures by ensuring correct provider construction during setup. Additionally, comments were revised in the domain-scoped configuration registration to highlight that the same early validation strategy applies, allowing for quick identification of misconfigurations. --- .../LaunchDarklyOpenFeatureBuilderExtensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs index 3eb51d9..6368a9e 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs +++ b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs @@ -76,8 +76,8 @@ private static OpenFeatureBuilder RegisterLaunchDarklyProvider( Func createConfiguration, Func resolveConfiguration) { - // Perform initial configuration validation before registration. - // This ensures that any misconfiguration is detected eagerly during setup. + // Perform early configuration validation to ensure the provider is correctly constructed. + // This avoids runtime failures by eagerly building the configuration during setup. var config = createConfiguration(); builder.Services.TryAddSingleton(_ => config); @@ -98,8 +98,8 @@ private static OpenFeatureBuilder RegisterLaunchDarklyProviderForDomain( Func createConfiguration, Func resolveConfiguration) { - // Perform initial configuration validation before registration. - // This ensures that any misconfiguration is detected eagerly during setup. + // Applies the same early validation strategy as the default registration path, + // ensuring domain-scoped configurations fail fast if misconfigured. var config = createConfiguration(); builder.Services.TryAddKeyedSingleton(domain, (_, obj) => config); From 830ad98e86bbc30297450b41dec23ea678818264 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sat, 26 Jul 2025 11:32:45 +0400 Subject: [PATCH 07/22] =?UTF-8?q?=E2=9C=85=20test:=20restructure=20LaunchD?= =?UTF-8?q?arkly=20integration=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed `DependencyInjectionIntegrationTests.cs` and `OpenFeatureBuilderExtensionsTests.cs`. Added `LaunchDarklyDependencyInjectionIntegrationTests.cs` and `LaunchDarklyOpenFeatureBuilderExtensionsTests.cs` to enhance test coverage and organization for LaunchDarkly integration with OpenFeature. --- .../DependencyInjectionIntegrationTests.cs | 244 ------- ...rklyDependencyInjectionIntegrationTests.cs | 563 ++++++++++++++++ ...DarklyOpenFeatureBuilderExtensionsTests.cs | 613 ++++++++++++++++++ .../OpenFeatureBuilderExtensionsTests.cs | 283 -------- 4 files changed, 1176 insertions(+), 527 deletions(-) delete mode 100644 test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/DependencyInjectionIntegrationTests.cs create mode 100644 test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyDependencyInjectionIntegrationTests.cs create mode 100644 test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs delete mode 100644 test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/DependencyInjectionIntegrationTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/DependencyInjectionIntegrationTests.cs deleted file mode 100644 index 82c4724..0000000 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/DependencyInjectionIntegrationTests.cs +++ /dev/null @@ -1,244 +0,0 @@ -using System; -using System.Threading.Tasks; -using LaunchDarkly.Sdk.Server; -using Microsoft.Extensions.DependencyInjection; -using OpenFeature; -using OpenFeature.Constant; -using OpenFeature.DependencyInjection; -using OpenFeature.Model; -using Xunit; - -namespace LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests -{ - public class DependencyInjectionIntegrationTests - { - private const string TestSdkKey = "test-sdk-key"; - private const string TestDomain = "test-domain"; - private const string TestFlagKey = "test-flag"; - - [Fact] - public async Task UseLaunchDarkly_CanResolveDefaultProvider() - { - // Arrange - var services = new ServiceCollection(); - services.AddOpenFeature(builder => - { - builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); - }); - - var serviceProvider = services.BuildServiceProvider(true); - var api = serviceProvider.GetRequiredService(); - - // Act - // Since we're using offline mode, the flag will return the default value - var result = await api.GetBooleanValueAsync(TestFlagKey, false); - - // Assert - Assert.False(result); // Default value should be returned in offline mode - } - - [Fact] - public async Task UseLaunchDarkly_CanResolveDomainScopedProvider() - { - // Arrange - var services = new ServiceCollection(); - services.AddOpenFeature(builder => - { - builder.UseLaunchDarkly(TestDomain, TestSdkKey, config => config.Offline(true)); - }); - - var serviceProvider = services.BuildServiceProvider(true); - var api = serviceProvider.GetRequiredKeyedService(TestDomain); - - // Act - // Since we're using offline mode, the flag will return the default value - var result = await api.GetBooleanValueAsync(TestFlagKey, true); - - // Assert - Assert.True(result); // Default value should be returned in offline mode - } - - [Fact] - public void UseLaunchDarkly_ProvidersFromDifferentDomainsAreDistinct() - { - // Arrange - var services = new ServiceCollection(); - const string domain1 = "domain1"; - const string domain2 = "domain2"; - - services.AddOpenFeature(builder => - { - builder.UseLaunchDarkly(domain1, TestSdkKey, config => config.Offline(true)); - builder.UseLaunchDarkly(domain2, TestSdkKey, config => config.Offline(true)); - }); - - var serviceProvider = services.BuildServiceProvider(true); - - // Act - var client1 = serviceProvider.GetRequiredKeyedService(domain1); - var client2 = serviceProvider.GetRequiredKeyedService(domain2); - - // Assert - Assert.NotSame(client1, client2); - } - - [Fact] - public void UseLaunchDarkly_ConfigurationIsSharedWithinSameDomain() - { - // Arrange - var services = new ServiceCollection(); - services.AddOpenFeature(builder => - { - builder.UseLaunchDarkly(TestDomain, TestSdkKey, config => config.Offline(true)); - }); - - var serviceProvider = services.BuildServiceProvider(true); - - // Act - var config1 = serviceProvider.GetRequiredKeyedService(TestDomain); - var config2 = serviceProvider.GetRequiredKeyedService(TestDomain); - - // Assert - Assert.Same(config1, config2); - Assert.True(config1.Offline); - } - - [Fact] - public void UseLaunchDarkly_DefaultConfigurationIsShared() - { - // Arrange - var services = new ServiceCollection(); - services.AddOpenFeature(builder => - { - builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); - }); - - var serviceProvider = services.BuildServiceProvider(true); - - // Act - var config1 = serviceProvider.GetRequiredService(); - var config2 = serviceProvider.GetRequiredService(); - - // Assert - Assert.Same(config1, config2); - Assert.True(config1.Offline); - } - - [Fact] - public async Task UseLaunchDarkly_ProviderSupportsAllValueTypes() - { - // Arrange - var services = new ServiceCollection(); - services.AddOpenFeature(builder => - { - builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); - }); - - var serviceProvider = services.BuildServiceProvider(true); - var api = serviceProvider.GetRequiredService(); - - // Act & Assert - Test all supported types - var boolResult = await api.GetBooleanValueAsync("bool-flag", true); - Assert.True(boolResult); - - var stringResult = await api.GetStringValueAsync("string-flag", "default"); - Assert.Equal("default", stringResult); - - var intResult = await api.GetIntegerValueAsync("int-flag", 42); - Assert.Equal(42, intResult); - - var doubleResult = await api.GetDoubleValueAsync("double-flag", 3.14); - Assert.Equal(3.14, doubleResult); - - var structureResult = await api.GetObjectValueAsync("object-flag", new Value("default")); - Assert.Equal("default", structureResult.AsString); - } - - [Fact] - public async Task UseLaunchDarkly_ProviderReturnsCorrectReasonInOfflineMode() - { - // Arrange - var services = new ServiceCollection(); - services.AddOpenFeature(builder => - { - builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); - }); - - var serviceProvider = services.BuildServiceProvider(true); - var api = serviceProvider.GetRequiredService(); - - // Act - var result = await api.GetBooleanDetailsAsync(TestFlagKey, false); - - // Assert - Assert.False(result.Value); - Assert.Equal(Reason.Default, result.Reason); - } - - [Fact] - public void UseLaunchDarkly_WithNullBuilder_ThrowsArgumentNullException() - { - // Arrange - OpenFeatureBuilder builder = null; - - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(TestSdkKey)); - } - - [Fact] - public void UseLaunchDarkly_WithNullBuilderForDomain_ThrowsArgumentNullException() - { - // Arrange - OpenFeatureBuilder builder = null; - - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, TestSdkKey)); - } - - [Fact] - public void UseLaunchDarkly_CustomConfigurationIsApplied() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - var startWaitTime = TimeSpan.FromSeconds(1); - - // Act - builder.UseLaunchDarkly(TestSdkKey, cfg => - { - cfg.Offline(true); - cfg.StartWaitTime(startWaitTime); - }); - - var serviceProvider = services.BuildServiceProvider(true); - var config = serviceProvider.GetRequiredService(); - - // Assert - Assert.True(config.Offline); - Assert.Equal(startWaitTime, config.StartWaitTime); - } - - [Fact] - public void UseLaunchDarkly_DomainCustomConfigurationIsApplied() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - var startWaitTime = TimeSpan.FromSeconds(2); - - // Act - builder.UseLaunchDarkly(TestDomain, TestSdkKey, cfg => - { - cfg.Offline(true); - cfg.StartWaitTime(startWaitTime); - }); - - var serviceProvider = services.BuildServiceProvider(true); - var config = serviceProvider.GetRequiredKeyedService(TestDomain); - - // Assert - Assert.True(config.Offline); - Assert.Equal(startWaitTime, config.StartWaitTime); - } - } -} \ No newline at end of file diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyDependencyInjectionIntegrationTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyDependencyInjectionIntegrationTests.cs new file mode 100644 index 0000000..93d893f --- /dev/null +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyDependencyInjectionIntegrationTests.cs @@ -0,0 +1,563 @@ +using System; +using System.Threading.Tasks; +using LaunchDarkly.Sdk.Server; +using Microsoft.Extensions.DependencyInjection; +using OpenFeature; +using OpenFeature.DependencyInjection; +using OpenFeature.Model; +using Xunit; +using LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection; +using OpenFeature.Constant; + +namespace LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests +{ + public class LaunchDarklyDependencyInjectionIntegrationTests + { + private const string TestSdkKey = "test-sdk-key"; + private const string TestDomain = "test-domain"; + private const string TestFlagKey = "test-flag"; + + #region Configuration Overload Integration Tests + + [Fact] + public async Task UseLaunchDarkly_WithPrebuiltConfiguration_CanResolveDefaultProvider() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var config = Configuration.Builder(TestSdkKey).Offline(true).Build(); + + builder.UseLaunchDarkly(config); + + var serviceProvider = services.BuildServiceProvider(); + var api = serviceProvider.GetRequiredService(); + + // Act + var result = await api.GetBooleanValueAsync(TestFlagKey, false); + + // Assert + Assert.False(result); // Default value should be returned in offline mode + } + + [Fact] + public async Task UseLaunchDarkly_WithDomainAndPrebuiltConfiguration_CanResolveDomainProvider() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var config = Configuration.Builder(TestSdkKey).Offline(true).Build(); + + builder.UseLaunchDarkly(TestDomain, config); + + var serviceProvider = services.BuildServiceProvider(); + var api = serviceProvider.GetRequiredKeyedService(TestDomain); + + // Act + var result = await api.GetBooleanValueAsync(TestFlagKey, true); + + // Assert + Assert.True(result); // Default value should be returned in offline mode + } + + [Fact] + public void UseLaunchDarkly_WithPrebuiltConfiguration_ConfigurationPropertiesArePreserved() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var startWaitTime = TimeSpan.FromSeconds(5); + var config = Configuration.Builder(TestSdkKey) + .Offline(true) + .StartWaitTime(startWaitTime) + .Build(); + + builder.UseLaunchDarkly(config); + + // Act + var serviceProvider = services.BuildServiceProvider(); + var registeredConfig = serviceProvider.GetRequiredService(); + + // Assert + Assert.True(registeredConfig.Offline); + Assert.Equal(startWaitTime, registeredConfig.StartWaitTime); + } + + [Fact] + public void UseLaunchDarkly_WithDomainAndPrebuiltConfiguration_ConfigurationPropertiesArePreserved() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var startWaitTime = TimeSpan.FromSeconds(10); + var config = Configuration.Builder(TestSdkKey) + .Offline(true) + .StartWaitTime(startWaitTime) + .Build(); + + builder.UseLaunchDarkly(TestDomain, config); + + // Act + var serviceProvider = services.BuildServiceProvider(); + var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); + + // Assert + Assert.True(registeredConfig.Offline); + Assert.Equal(startWaitTime, registeredConfig.StartWaitTime); + } + + #endregion + + #region SDK Key Overload Integration Tests + + [Fact] + public async Task UseLaunchDarkly_WithSdkKey_CanResolveDefaultProvider() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); + + var serviceProvider = services.BuildServiceProvider(); + var api = serviceProvider.GetRequiredService(); + + // Act + var result = await api.GetBooleanValueAsync(TestFlagKey, false); + + // Assert + Assert.False(result); // Default value should be returned in offline mode + } + + [Fact] + public async Task UseLaunchDarkly_WithDomainAndSdkKey_CanResolveDomainProvider() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + builder.UseLaunchDarkly(TestDomain, TestSdkKey, config => config.Offline(true)); + + var serviceProvider = services.BuildServiceProvider(); + var api = serviceProvider.GetRequiredKeyedService(TestDomain); + + // Act + var result = await api.GetBooleanValueAsync(TestFlagKey, true); + + // Assert + Assert.True(result); // Default value should be returned in offline mode + } + + [Fact] + public async Task UseLaunchDarkly_WithSdkKeyAndCustomConfiguration_AppliesConfiguration() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var startWaitTime = TimeSpan.FromSeconds(3); + + builder.UseLaunchDarkly(TestSdkKey, config => + { + config.Offline(true); + config.StartWaitTime(startWaitTime); + }); + + var serviceProvider = services.BuildServiceProvider(); + var registeredConfig = serviceProvider.GetRequiredService(); + + // Act & Assert + Assert.True(registeredConfig.Offline); + Assert.Equal(startWaitTime, registeredConfig.StartWaitTime); + } + + [Fact] + public async Task UseLaunchDarkly_WithDomainSdkKeyAndCustomConfiguration_AppliesConfiguration() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var startWaitTime = TimeSpan.FromSeconds(7); + + builder.UseLaunchDarkly(TestDomain, TestSdkKey, config => + { + config.Offline(true); + config.StartWaitTime(startWaitTime); + }); + + var serviceProvider = services.BuildServiceProvider(); + var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); + + // Act & Assert + Assert.True(registeredConfig.Offline); + Assert.Equal(startWaitTime, registeredConfig.StartWaitTime); + } + + #endregion + + #region Multi-Provider Integration Tests + + [Fact] + public void UseLaunchDarkly_MixedDefaultAndDomainProviders_WorkCorrectly() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Register both default and domain-scoped providers using different overloads + var defaultConfig = Configuration.Builder(TestSdkKey).Offline(true).Build(); + builder.UseLaunchDarkly(defaultConfig); + builder.UseLaunchDarkly(TestDomain, TestSdkKey, config => config.Offline(true)); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var defaultClient = serviceProvider.GetRequiredService(); + var domainClient = serviceProvider.GetRequiredKeyedService(TestDomain); + var defaultConfigRegistered = serviceProvider.GetRequiredService(); + var domainConfigRegistered = serviceProvider.GetRequiredKeyedService(TestDomain); + + // Assert + Assert.NotSame(defaultClient, domainClient); + Assert.NotSame(defaultConfigRegistered, domainConfigRegistered); + Assert.True(defaultConfigRegistered.Offline); + Assert.True(domainConfigRegistered.Offline); + } + + [Fact] + public void UseLaunchDarkly_MultipleDomainConfigurations_AreIsolated() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + const string domain1 = "fast-domain"; + const string domain2 = "slow-domain"; + var fastStartWait = TimeSpan.FromMilliseconds(100); + var slowStartWait = TimeSpan.FromSeconds(5); + + // Register using different overloads for variety + var fastConfig = Configuration.Builder(TestSdkKey) + .Offline(true) + .StartWaitTime(fastStartWait) + .Build(); + + builder.UseLaunchDarkly(domain1, fastConfig); + builder.UseLaunchDarkly(domain2, TestSdkKey, config => + { + config.Offline(true); + config.StartWaitTime(slowStartWait); + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act & Assert + var config1 = serviceProvider.GetRequiredKeyedService(domain1); + var config2 = serviceProvider.GetRequiredKeyedService(domain2); + + Assert.NotSame(config1, config2); + Assert.Equal(fastStartWait, config1.StartWaitTime); + Assert.Equal(slowStartWait, config2.StartWaitTime); + Assert.True(config1.Offline); + Assert.True(config2.Offline); + } + + [Fact] + public async Task UseLaunchDarkly_AllOverloads_ProvidersSupportAllValueTypes() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Test all 4 overloads + var config1 = Configuration.Builder(TestSdkKey).Offline(true).Build(); + var config2 = Configuration.Builder(TestSdkKey).Offline(true).Build(); + + builder.UseLaunchDarkly(config1); // Overload 1 + builder.UseLaunchDarkly("domain1", config2); // Overload 2 + builder.UseLaunchDarkly("domain2", TestSdkKey); // Overload 3 + builder.UseLaunchDarkly("domain3", TestSdkKey, c => c.Offline(true)); // Overload 4 + + var serviceProvider = services.BuildServiceProvider(); + + var defaultApi = serviceProvider.GetRequiredService(); + var domain1Api = serviceProvider.GetRequiredKeyedService("domain1"); + var domain2Api = serviceProvider.GetRequiredKeyedService("domain2"); + var domain3Api = serviceProvider.GetRequiredKeyedService("domain3"); + + // Act & Assert - Test all supported types on all providers + foreach (var api in new[] { defaultApi, domain1Api, domain2Api, domain3Api }) + { + var boolResult = await api.GetBooleanValueAsync("bool-flag", true); + Assert.True(boolResult); + + var stringResult = await api.GetStringValueAsync("string-flag", "default"); + Assert.Equal("default", stringResult); + + var intResult = await api.GetIntegerValueAsync("int-flag", 42); + Assert.Equal(42, intResult); + + var doubleResult = await api.GetDoubleValueAsync("double-flag", 3.14); + Assert.Equal(3.14, doubleResult); + + var structureResult = await api.GetObjectValueAsync("object-flag", new Value("default")); + Assert.Equal("default", structureResult.AsString); + } + } + + #endregion + + #region Provider Behavior Integration Tests + + [Fact] + public async Task UseLaunchDarkly_ProviderReturnsCorrectReasonInOfflineMode() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); + + var serviceProvider = services.BuildServiceProvider(); + var api = serviceProvider.GetRequiredService(); + + // Act + var result = await api.GetBooleanDetailsAsync(TestFlagKey, false); + + // Assert + Assert.False(result.Value); + Assert.Equal(Reason.Default, result.Reason); + } + + [Fact] + public async Task UseLaunchDarkly_DomainProviderReturnsCorrectReasonInOfflineMode() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var config = Configuration.Builder(TestSdkKey).Offline(true).Build(); + + builder.UseLaunchDarkly(TestDomain, config); + + var serviceProvider = services.BuildServiceProvider(); + var api = serviceProvider.GetRequiredKeyedService(TestDomain); + + // Act + var result = await api.GetBooleanDetailsAsync(TestFlagKey, true); + + // Assert + Assert.True(result.Value); + Assert.Equal(Reason.Default, result.Reason); + } + + [Fact] + public async Task UseLaunchDarkly_ProviderHandlesEvaluationContext() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); + + var serviceProvider = services.BuildServiceProvider(); + var api = serviceProvider.GetRequiredService(); + + var context = EvaluationContext.Builder() + .Set("userId", "test-user") + .Set("email", "test@example.com") + .Build(); + + // Act + var result = await api.GetBooleanDetailsAsync(TestFlagKey, false, context); + + // Assert + Assert.False(result.Value); + Assert.Equal(Reason.Default, result.Reason); + } + + #endregion + + #region Early Validation Integration Tests + + [Fact] + public void UseLaunchDarkly_EarlyValidationWithConfiguration_FailsImmediately() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // This would normally fail at runtime, but early validation catches it immediately + // We can't easily test this without a malformed Configuration, but we can test that + // valid configurations pass early validation + var config = Configuration.Builder(TestSdkKey).Offline(true).Build(); + + // Act & Assert - Should not throw + builder.UseLaunchDarkly(config); + + var serviceProvider = services.BuildServiceProvider(); + var registeredConfig = serviceProvider.GetRequiredService(); + Assert.NotNull(registeredConfig); + } + + [Fact] + public void UseLaunchDarkly_EarlyValidationWithSdkKey_FailsImmediately() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert - Valid configuration should pass early validation + builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); + + var serviceProvider = services.BuildServiceProvider(); + var registeredConfig = serviceProvider.GetRequiredService(); + Assert.NotNull(registeredConfig); + Assert.True(registeredConfig.Offline); + } + + [Fact] + public void UseLaunchDarkly_EarlyValidationWithDomain_FailsImmediately() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert - Valid configuration should pass early validation + builder.UseLaunchDarkly(TestDomain, TestSdkKey, config => config.Offline(true)); + + var serviceProvider = services.BuildServiceProvider(); + var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); + Assert.NotNull(registeredConfig); + Assert.True(registeredConfig.Offline); + } + + #endregion + + #region Service Lifetime Integration Tests + + [Fact] + public void UseLaunchDarkly_ConfigurationRegisteredAsSingleton() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var config1 = serviceProvider.GetRequiredService(); + var config2 = serviceProvider.GetRequiredService(); + + // Assert + Assert.Same(config1, config2); + } + + [Fact] + public void UseLaunchDarkly_DomainConfigurationRegisteredAsKeyedSingleton() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + builder.UseLaunchDarkly(TestDomain, TestSdkKey, config => config.Offline(true)); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var config1 = serviceProvider.GetRequiredKeyedService(TestDomain); + var config2 = serviceProvider.GetRequiredKeyedService(TestDomain); + + // Assert + Assert.Same(config1, config2); + } + + [Fact] + public void UseLaunchDarkly_TryAddSingleton_DoesNotReplaceExistingRegistration() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + var config1 = Configuration.Builder(TestSdkKey).Offline(true).Build(); + var config2 = Configuration.Builder(TestSdkKey).Offline(false).Build(); + + // Act + builder.UseLaunchDarkly(config1); + builder.UseLaunchDarkly(config2); // Should not replace the first + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var registeredConfig = serviceProvider.GetRequiredService(); + Assert.True(registeredConfig.Offline); // Should still be the first configuration + } + + [Fact] + public void UseLaunchDarkly_TryAddKeyedSingleton_DoesNotReplaceExistingRegistration() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + var config1 = Configuration.Builder(TestSdkKey).Offline(true).Build(); + var config2 = Configuration.Builder(TestSdkKey).Offline(false).Build(); + + // Act + builder.UseLaunchDarkly(TestDomain, config1); + builder.UseLaunchDarkly(TestDomain, config2); // Should not replace the first + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); + Assert.True(registeredConfig.Offline); // Should still be the first configuration + } + + #endregion + + #region Resource Management Tests + + [Fact] + public void UseLaunchDarkly_ServiceProviderDisposed_DoesNotCauseMemoryLeaks() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); + + // Act & Assert + using (var serviceProvider = services.BuildServiceProvider()) + { + var config = serviceProvider.GetRequiredService(); + Assert.NotNull(config); + Assert.True(config.Offline); + } + // Service provider disposed, should not cause issues + } + + [Fact] + public void UseLaunchDarkly_MultipleServiceProviders_IsolateConfigurations() + { + // Arrange + var services1 = new ServiceCollection(); + var services2 = new ServiceCollection(); + var builder1 = new OpenFeatureBuilder(services1); + var builder2 = new OpenFeatureBuilder(services2); + + builder1.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); + builder2.UseLaunchDarkly(TestSdkKey, config => config.Offline(false)); + + // Act + var serviceProvider1 = services1.BuildServiceProvider(); + var serviceProvider2 = services2.BuildServiceProvider(); + + var config1 = serviceProvider1.GetRequiredService(); + var config2 = serviceProvider2.GetRequiredService(); + + // Assert + Assert.NotSame(config1, config2); + Assert.True(config1.Offline); + Assert.False(config2.Offline); + } + + #endregion + } +} \ No newline at end of file diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs new file mode 100644 index 0000000..0ac7c6f --- /dev/null +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs @@ -0,0 +1,613 @@ +using System; +using LaunchDarkly.Sdk.Server; +using Microsoft.Extensions.DependencyInjection; +using OpenFeature.DependencyInjection; +using Xunit; + +namespace LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests +{ + public class LaunchDarklyOpenFeatureBuilderExtensionsTests + { + private const string TestSdkKey = "test-sdk-key"; + private const string TestDomain = "test-domain"; + + #region Configuration Overload Tests - Default Provider + + [Fact] + public void UseLaunchDarkly_WithConfiguration_RegistersDefaultProvider() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var config = Configuration.Builder(TestSdkKey).Offline(true).Build(); + + // Act + var result = builder.UseLaunchDarkly(config); + + // Assert + Assert.Same(builder, result); + + var serviceProvider = services.BuildServiceProvider(); + var registeredConfig = serviceProvider.GetRequiredService(); + Assert.NotNull(registeredConfig); + Assert.True(registeredConfig.Offline); + } + + [Fact] + public void UseLaunchDarkly_WithConfiguration_ConfigurationIsShared() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var config = Configuration.Builder(TestSdkKey).Offline(true).Build(); + + // Act + builder.UseLaunchDarkly(config); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var config1 = serviceProvider.GetRequiredService(); + var config2 = serviceProvider.GetRequiredService(); + Assert.Same(config1, config2); + } + + [Fact] + public void UseLaunchDarkly_WithNullConfiguration_ThrowsArgumentNullException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => builder.UseLaunchDarkly((Configuration)null)); + } + + [Fact] + public void UseLaunchDarkly_MultipleCallsWithConfiguration_UsesTryAddSingleton() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var config1 = Configuration.Builder(TestSdkKey).Offline(true).Build(); + var config2 = Configuration.Builder(TestSdkKey).Offline(false).Build(); + + // Act + builder.UseLaunchDarkly(config1); + builder.UseLaunchDarkly(config2); // Should not replace the first + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var registeredConfig = serviceProvider.GetRequiredService(); + Assert.True(registeredConfig.Offline); // Should still be the first configuration + } + + #endregion + + #region Configuration Overload Tests - Domain Provider + + [Fact] + public void UseLaunchDarkly_WithDomainAndConfiguration_RegistersDomainScopedProvider() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var config = Configuration.Builder(TestSdkKey).Offline(true).Build(); + + // Act + var result = builder.UseLaunchDarkly(TestDomain, config); + + // Assert + Assert.Same(builder, result); + + var serviceProvider = services.BuildServiceProvider(); + var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); + Assert.NotNull(registeredConfig); + Assert.True(registeredConfig.Offline); + } + + [Fact] + public void UseLaunchDarkly_WithDomainAndConfiguration_ConfigurationIsShared() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var config = Configuration.Builder(TestSdkKey).Offline(true).Build(); + + // Act + builder.UseLaunchDarkly(TestDomain, config); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var config1 = serviceProvider.GetRequiredKeyedService(TestDomain); + var config2 = serviceProvider.GetRequiredKeyedService(TestDomain); + Assert.Same(config1, config2); + } + + [Fact] + public void UseLaunchDarkly_WithNullDomainAndConfiguration_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var config = Configuration.Builder(TestSdkKey).Build(); + + // Act & Assert + Assert.Throws(() => builder.UseLaunchDarkly(null, config)); + } + + [Fact] + public void UseLaunchDarkly_WithEmptyDomainAndConfiguration_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var config = Configuration.Builder(TestSdkKey).Build(); + + // Act & Assert + Assert.Throws(() => builder.UseLaunchDarkly(string.Empty, config)); + } + + [Fact] + public void UseLaunchDarkly_WithWhitespaceDomainAndConfiguration_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var config = Configuration.Builder(TestSdkKey).Build(); + + // Act & Assert + Assert.Throws(() => builder.UseLaunchDarkly(" ", config)); + } + + [Fact] + public void UseLaunchDarkly_WithDomainAndNullConfiguration_ThrowsArgumentNullException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, (Configuration)null)); + } + + [Fact] + public void UseLaunchDarkly_WithDifferentDomainsAndConfigurations_RegistersSeparateConfigurations() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var config1 = Configuration.Builder(TestSdkKey).Offline(true).Build(); + var config2 = Configuration.Builder(TestSdkKey).Offline(false).Build(); + const string domain1 = "domain1"; + const string domain2 = "domain2"; + + // Act + builder.UseLaunchDarkly(domain1, config1); + builder.UseLaunchDarkly(domain2, config2); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var registeredConfig1 = serviceProvider.GetRequiredKeyedService(domain1); + var registeredConfig2 = serviceProvider.GetRequiredKeyedService(domain2); + Assert.NotSame(registeredConfig1, registeredConfig2); + Assert.True(registeredConfig1.Offline); + Assert.False(registeredConfig2.Offline); + } + + #endregion + + #region SDK Key Overload Tests - Default Provider + + [Fact] + public void UseLaunchDarkly_WithSdkKey_RegistersDefaultProvider() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act + var result = builder.UseLaunchDarkly(TestSdkKey); + + // Assert + Assert.Same(builder, result); + + var serviceProvider = services.BuildServiceProvider(); + var config = serviceProvider.GetRequiredService(); + Assert.NotNull(config); + } + + [Fact] + public void UseLaunchDarkly_WithSdkKeyAndConfiguration_RegistersDefaultProviderWithConfiguration() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var configureWasCalled = false; + ConfigurationBuilder capturedBuilder = null; + + // Act + var result = builder.UseLaunchDarkly(TestSdkKey, configBuilder => + { + configureWasCalled = true; + capturedBuilder = configBuilder; + configBuilder.Offline(true); + }); + + // Assert + Assert.Same(builder, result); + + var serviceProvider = services.BuildServiceProvider(); + var config = serviceProvider.GetRequiredService(); + + Assert.True(configureWasCalled); + Assert.NotNull(capturedBuilder); + Assert.True(config.Offline); + } + + [Fact] + public void UseLaunchDarkly_WithNullSdkKey_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => builder.UseLaunchDarkly((string)null)); + } + + [Fact] + public void UseLaunchDarkly_WithEmptySdkKey_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => builder.UseLaunchDarkly(string.Empty)); + } + + [Fact] + public void UseLaunchDarkly_WithWhitespaceSdkKey_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => builder.UseLaunchDarkly(" ")); + } + + [Fact] + public void UseLaunchDarkly_ConfigurationDelegateException_PropagatesExceptionImmediately() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var expectedException = new InvalidOperationException("Test exception"); + + // Act & Assert + // The exception should be thrown immediately during registration due to early validation + var actualException = Assert.Throws(() => + builder.UseLaunchDarkly(TestSdkKey, _ => throw expectedException)); + Assert.Same(expectedException, actualException); + } + + [Fact] + public void UseLaunchDarkly_NullConfigurationDelegate_DoesNotThrow() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert - should not throw + Configuration configuration = null; + var result = builder.UseLaunchDarkly(TestSdkKey, configuration); + + Assert.Same(builder, result); + var serviceProvider = services.BuildServiceProvider(); + var config = serviceProvider.GetRequiredService(); + Assert.NotNull(config); + } + + #endregion + + #region SDK Key Overload Tests - Domain Provider + + [Fact] + public void UseLaunchDarkly_WithDomainAndSdkKey_RegistersDomainScopedProvider() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act + var result = builder.UseLaunchDarkly(TestDomain, TestSdkKey); + + // Assert + Assert.Same(builder, result); + + var serviceProvider = services.BuildServiceProvider(); + var config = serviceProvider.GetRequiredKeyedService(TestDomain); + Assert.NotNull(config); + } + + [Fact] + public void UseLaunchDarkly_WithDomainSdkKeyAndConfiguration_RegistersDomainScopedProviderWithConfiguration() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var configureWasCalled = false; + ConfigurationBuilder capturedBuilder = null; + + // Act + var result = builder.UseLaunchDarkly(TestDomain, TestSdkKey, configBuilder => + { + configureWasCalled = true; + capturedBuilder = configBuilder; + configBuilder.Offline(true); + }); + + // Assert + Assert.Same(builder, result); + + var serviceProvider = services.BuildServiceProvider(); + var config = serviceProvider.GetRequiredKeyedService(TestDomain); + + Assert.True(configureWasCalled); + Assert.NotNull(capturedBuilder); + Assert.True(config.Offline); + } + + [Fact] + public void UseLaunchDarkly_WithNullDomainAndSdkKey_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => builder.UseLaunchDarkly(null, TestSdkKey)); + } + + [Fact] + public void UseLaunchDarkly_WithEmptyDomainAndSdkKey_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => builder.UseLaunchDarkly(string.Empty, TestSdkKey)); + } + + [Fact] + public void UseLaunchDarkly_WithWhitespaceDomainAndSdkKey_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => builder.UseLaunchDarkly(" ", TestSdkKey)); + } + + [Fact] + public void UseLaunchDarkly_WithDomainAndNullSdkKey_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, (string)null)); + } + + [Fact] + public void UseLaunchDarkly_WithDomainAndEmptySdkKey_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, string.Empty)); + } + + [Fact] + public void UseLaunchDarkly_WithDomainAndWhitespaceSdkKey_ThrowsArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert + Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, " ")); + } + + [Fact] + public void UseLaunchDarkly_DomainConfigurationDelegateException_PropagatesExceptionImmediately() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var expectedException = new InvalidOperationException("Test exception"); + + // Act & Assert + // The exception should be thrown immediately during registration due to early validation + var actualException = Assert.Throws(() => + builder.UseLaunchDarkly(TestDomain, TestSdkKey, _ => throw expectedException)); + Assert.Same(expectedException, actualException); + } + + [Fact] + public void UseLaunchDarkly_DomainNullConfigurationDelegate_DoesNotThrow() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act & Assert - should not throw + var result = builder.UseLaunchDarkly(TestDomain, TestSdkKey, null); + + Assert.Same(builder, result); + var serviceProvider = services.BuildServiceProvider(); + var config = serviceProvider.GetRequiredKeyedService(TestDomain); + Assert.NotNull(config); + } + + #endregion + + #region Builder Validation Tests + + [Fact] + public void UseLaunchDarkly_WithNullBuilder_ThrowsArgumentNullException() + { + // Arrange + OpenFeatureBuilder builder = null; + + // Act & Assert + Assert.Throws(() => builder.UseLaunchDarkly(TestSdkKey)); + } + + [Fact] + public void UseLaunchDarkly_WithNullBuilderAndConfiguration_ThrowsArgumentNullException() + { + // Arrange + OpenFeatureBuilder builder = null; + var config = Configuration.Builder(TestSdkKey).Build(); + + // Act & Assert + Assert.Throws(() => builder.UseLaunchDarkly(config)); + } + + [Fact] + public void UseLaunchDarkly_WithNullBuilderForDomain_ThrowsArgumentNullException() + { + // Arrange + OpenFeatureBuilder builder = null; + + // Act & Assert + Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, TestSdkKey)); + } + + [Fact] + public void UseLaunchDarkly_WithNullBuilderForDomainAndConfiguration_ThrowsArgumentNullException() + { + // Arrange + OpenFeatureBuilder builder = null; + var config = Configuration.Builder(TestSdkKey).Build(); + + // Act & Assert + Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, config)); + } + + #endregion + + #region Advanced Configuration Tests + + [Fact] + public void UseLaunchDarkly_ConfigurationCloning_CreatesNewInstance() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var originalConfig = Configuration.Builder(TestSdkKey).Offline(true).Build(); + + // Act + builder.UseLaunchDarkly(originalConfig); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var registeredConfig = serviceProvider.GetRequiredService(); + + // The registered config should be a rebuilt version, not the same instance + // but should have the same properties + Assert.True(registeredConfig.Offline); + } + + [Fact] + public void UseLaunchDarkly_CustomConfigurationProperties_ArePreserved() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var startWaitTime = TimeSpan.FromSeconds(5); + + // Act + builder.UseLaunchDarkly(TestSdkKey, config => + { + config.Offline(true); + config.StartWaitTime(startWaitTime); + }); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var registeredConfig = serviceProvider.GetRequiredService(); + Assert.True(registeredConfig.Offline); + Assert.Equal(startWaitTime, registeredConfig.StartWaitTime); + } + + [Fact] + public void UseLaunchDarkly_DomainCustomConfigurationProperties_ArePreserved() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var startWaitTime = TimeSpan.FromSeconds(10); + + // Act + builder.UseLaunchDarkly(TestDomain, TestSdkKey, config => + { + config.Offline(true); + config.StartWaitTime(startWaitTime); + }); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); + Assert.True(registeredConfig.Offline); + Assert.Equal(startWaitTime, registeredConfig.StartWaitTime); + } + + #endregion + + #region Early Validation Tests + + [Fact] + public void UseLaunchDarkly_EarlyValidation_PreventsRuntimeFailures() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act - Early validation should catch configuration issues immediately + builder.UseLaunchDarkly(TestSdkKey, cfg => cfg.Offline(true)); + + // Assert - If we reach here, early validation passed + var serviceProvider = services.BuildServiceProvider(); + var config = serviceProvider.GetRequiredService(); + Assert.NotNull(config); + Assert.True(config.Offline); + } + + [Fact] + public void UseLaunchDarkly_DomainEarlyValidation_PreventsRuntimeFailures() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act - Early validation should catch configuration issues immediately + builder.UseLaunchDarkly(TestDomain, TestSdkKey, cfg => cfg.Offline(true)); + + // Assert - If we reach here, early validation passed + var serviceProvider = services.BuildServiceProvider(); + var config = serviceProvider.GetRequiredKeyedService(TestDomain); + Assert.NotNull(config); + Assert.True(config.Offline); + } + + #endregion + } +} diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs deleted file mode 100644 index a7e0bd3..0000000 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ /dev/null @@ -1,283 +0,0 @@ -using System; -using LaunchDarkly.Sdk.Server; -using Microsoft.Extensions.DependencyInjection; -using OpenFeature.DependencyInjection; -using Xunit; - -namespace LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests -{ - public class OpenFeatureBuilderExtensionsTests - { - private const string TestSdkKey = "test-sdk-key"; - private const string TestDomain = "test-domain"; - - [Fact] - public void UseLaunchDarkly_WithSdkKey_RegistersDefaultProvider() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Act - var result = builder.UseLaunchDarkly(TestSdkKey); - - // Assert - Assert.Same(builder, result); - - // Build the service provider to verify registrations - var serviceProvider = services.BuildServiceProvider(true); - - // Verify Configuration is registered as singleton - var config1 = serviceProvider.GetRequiredService(); - var config2 = serviceProvider.GetRequiredService(); - Assert.Same(config1, config2); - } - - [Fact] - public void UseLaunchDarkly_WithSdkKeyAndConfiguration_RegistersDefaultProviderWithConfiguration() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - var configureWasCalled = false; - ConfigurationBuilder capturedBuilder = null; - - // Act - var result = builder.UseLaunchDarkly(TestSdkKey, configBuilder => - { - configureWasCalled = true; - capturedBuilder = configBuilder; - configBuilder.Offline(true); // Set some configuration for testing - }); - - // Assert - Assert.Same(builder, result); - - // Build the service provider to verify configuration was applied - var serviceProvider = services.BuildServiceProvider(true); - var config = serviceProvider.GetRequiredService(); - - Assert.True(configureWasCalled); - Assert.NotNull(capturedBuilder); - Assert.True(config.Offline); - } - - [Fact] - public void UseLaunchDarkly_WithNullSdkKey_ThrowsArgumentException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(null)); - } - - [Fact] - public void UseLaunchDarkly_WithEmptySdkKey_ThrowsArgumentException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(string.Empty)); - } - - [Fact] - public void UseLaunchDarkly_WithDomainAndSdkKey_RegistersDomainScopedProvider() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Act - var result = builder.UseLaunchDarkly(TestDomain, TestSdkKey); - - // Assert - Assert.Same(builder, result); - - // Build the service provider to verify registrations - var serviceProvider = services.BuildServiceProvider(true); - - // Verify Configuration is registered as keyed singleton - var config1 = serviceProvider.GetRequiredKeyedService(TestDomain); - var config2 = serviceProvider.GetRequiredKeyedService(TestDomain); - Assert.Same(config1, config2); - } - - [Fact] - public void UseLaunchDarkly_WithDomainSdkKeyAndConfiguration_RegistersDomainScopedProviderWithConfiguration() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - var configureWasCalled = false; - ConfigurationBuilder capturedBuilder = null; - - // Act - var result = builder.UseLaunchDarkly(TestDomain, TestSdkKey, configBuilder => - { - configureWasCalled = true; - capturedBuilder = configBuilder; - configBuilder.Offline(true); // Set some configuration for testing - }); - - // Assert - Assert.Same(builder, result); - - // Build the service provider to verify configuration was applied - var serviceProvider = services.BuildServiceProvider(true); - var config = serviceProvider.GetRequiredKeyedService(TestDomain); - - Assert.True(configureWasCalled); - Assert.NotNull(capturedBuilder); - Assert.True(config.Offline); - } - - [Fact] - public void UseLaunchDarkly_WithNullDomain_ThrowsArgumentException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(null, TestSdkKey)); - } - - [Fact] - public void UseLaunchDarkly_WithEmptyDomain_ThrowsArgumentException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(string.Empty, TestSdkKey)); - } - - [Fact] - public void UseLaunchDarkly_WithDomainAndNullSdkKey_ThrowsArgumentException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, null)); - } - - [Fact] - public void UseLaunchDarkly_WithDomainAndEmptySdkKey_ThrowsArgumentException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - try - { - builder.UseLaunchDarkly(TestDomain, string.Empty); - } - catch (Exception ex) - { - - } - - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, string.Empty)); - } - - [Fact] - public void UseLaunchDarkly_MultipleCallsWithSameSdkKey_UsesSameConfigurationInstance() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Act - builder.UseLaunchDarkly(TestSdkKey); - builder.UseLaunchDarkly(TestSdkKey); // Second call should not replace the first due to TryAddSingleton - - // Assert - var serviceProvider = services.BuildServiceProvider(true); - var config1 = serviceProvider.GetRequiredService(); - var config2 = serviceProvider.GetRequiredService(); - Assert.Same(config1, config2); - } - - [Fact] - public void UseLaunchDarkly_MultipleCallsWithSameDomain_UsesSameConfigurationInstance() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Act - builder.UseLaunchDarkly(TestDomain, TestSdkKey); - builder.UseLaunchDarkly(TestDomain, TestSdkKey); // Second call should not replace the first due to TryAddKeyedSingleton - - // Assert - var serviceProvider = services.BuildServiceProvider(true); - var config1 = serviceProvider.GetRequiredKeyedService(TestDomain); - var config2 = serviceProvider.GetRequiredKeyedService(TestDomain); - Assert.Same(config1, config2); - } - - [Fact] - public void UseLaunchDarkly_WithDifferentDomains_RegistersSeparateConfigurations() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - const string domain1 = "domain1"; - const string domain2 = "domain2"; - - // Act - builder.UseLaunchDarkly(domain1, TestSdkKey); - builder.UseLaunchDarkly(domain2, TestSdkKey); - - // Assert - var serviceProvider = services.BuildServiceProvider(true); - var config1 = serviceProvider.GetRequiredKeyedService(domain1); - var config2 = serviceProvider.GetRequiredKeyedService(domain2); - Assert.NotSame(config1, config2); - } - - [Fact] - public void UseLaunchDarkly_ConfigurationDelegateException_PropagatesException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - var expectedException = new InvalidOperationException("Test exception"); - - // Act - builder.UseLaunchDarkly(TestSdkKey, _ => throw expectedException); - - // Assert - var serviceProvider = services.BuildServiceProvider(true); - var actualException = Assert.Throws(() => - serviceProvider.GetRequiredService()); - Assert.Same(expectedException, actualException); - } - - [Fact] - public void UseLaunchDarkly_DomainConfigurationDelegateException_PropagatesException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - var expectedException = new InvalidOperationException("Test exception"); - - // Act - builder.UseLaunchDarkly(TestDomain, TestSdkKey, _ => throw expectedException); - - // Assert - var serviceProvider = services.BuildServiceProvider(true); - var actualException = Assert.Throws(() => - serviceProvider.GetRequiredKeyedService(TestDomain)); - Assert.Same(expectedException, actualException); - } - } -} \ No newline at end of file From c958f447d4991d8bc190c3040ba5bf15ac695fac Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sat, 26 Jul 2025 11:33:35 +0400 Subject: [PATCH 08/22] =?UTF-8?q?=F0=9F=94=A7=20chore(tests):=20remove=20L?= =?UTF-8?q?aunchDarkly=20integration=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The entire `LaunchDarklyDependencyInjectionIntegrationTests.cs` file has been removed, including all its using directives, namespace declaration, and the class definition along with all its methods and tests. --- ...rklyDependencyInjectionIntegrationTests.cs | 563 ------------------ 1 file changed, 563 deletions(-) delete mode 100644 test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyDependencyInjectionIntegrationTests.cs diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyDependencyInjectionIntegrationTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyDependencyInjectionIntegrationTests.cs deleted file mode 100644 index 93d893f..0000000 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyDependencyInjectionIntegrationTests.cs +++ /dev/null @@ -1,563 +0,0 @@ -using System; -using System.Threading.Tasks; -using LaunchDarkly.Sdk.Server; -using Microsoft.Extensions.DependencyInjection; -using OpenFeature; -using OpenFeature.DependencyInjection; -using OpenFeature.Model; -using Xunit; -using LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection; -using OpenFeature.Constant; - -namespace LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests -{ - public class LaunchDarklyDependencyInjectionIntegrationTests - { - private const string TestSdkKey = "test-sdk-key"; - private const string TestDomain = "test-domain"; - private const string TestFlagKey = "test-flag"; - - #region Configuration Overload Integration Tests - - [Fact] - public async Task UseLaunchDarkly_WithPrebuiltConfiguration_CanResolveDefaultProvider() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - var config = Configuration.Builder(TestSdkKey).Offline(true).Build(); - - builder.UseLaunchDarkly(config); - - var serviceProvider = services.BuildServiceProvider(); - var api = serviceProvider.GetRequiredService(); - - // Act - var result = await api.GetBooleanValueAsync(TestFlagKey, false); - - // Assert - Assert.False(result); // Default value should be returned in offline mode - } - - [Fact] - public async Task UseLaunchDarkly_WithDomainAndPrebuiltConfiguration_CanResolveDomainProvider() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - var config = Configuration.Builder(TestSdkKey).Offline(true).Build(); - - builder.UseLaunchDarkly(TestDomain, config); - - var serviceProvider = services.BuildServiceProvider(); - var api = serviceProvider.GetRequiredKeyedService(TestDomain); - - // Act - var result = await api.GetBooleanValueAsync(TestFlagKey, true); - - // Assert - Assert.True(result); // Default value should be returned in offline mode - } - - [Fact] - public void UseLaunchDarkly_WithPrebuiltConfiguration_ConfigurationPropertiesArePreserved() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - var startWaitTime = TimeSpan.FromSeconds(5); - var config = Configuration.Builder(TestSdkKey) - .Offline(true) - .StartWaitTime(startWaitTime) - .Build(); - - builder.UseLaunchDarkly(config); - - // Act - var serviceProvider = services.BuildServiceProvider(); - var registeredConfig = serviceProvider.GetRequiredService(); - - // Assert - Assert.True(registeredConfig.Offline); - Assert.Equal(startWaitTime, registeredConfig.StartWaitTime); - } - - [Fact] - public void UseLaunchDarkly_WithDomainAndPrebuiltConfiguration_ConfigurationPropertiesArePreserved() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - var startWaitTime = TimeSpan.FromSeconds(10); - var config = Configuration.Builder(TestSdkKey) - .Offline(true) - .StartWaitTime(startWaitTime) - .Build(); - - builder.UseLaunchDarkly(TestDomain, config); - - // Act - var serviceProvider = services.BuildServiceProvider(); - var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); - - // Assert - Assert.True(registeredConfig.Offline); - Assert.Equal(startWaitTime, registeredConfig.StartWaitTime); - } - - #endregion - - #region SDK Key Overload Integration Tests - - [Fact] - public async Task UseLaunchDarkly_WithSdkKey_CanResolveDefaultProvider() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); - - var serviceProvider = services.BuildServiceProvider(); - var api = serviceProvider.GetRequiredService(); - - // Act - var result = await api.GetBooleanValueAsync(TestFlagKey, false); - - // Assert - Assert.False(result); // Default value should be returned in offline mode - } - - [Fact] - public async Task UseLaunchDarkly_WithDomainAndSdkKey_CanResolveDomainProvider() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - builder.UseLaunchDarkly(TestDomain, TestSdkKey, config => config.Offline(true)); - - var serviceProvider = services.BuildServiceProvider(); - var api = serviceProvider.GetRequiredKeyedService(TestDomain); - - // Act - var result = await api.GetBooleanValueAsync(TestFlagKey, true); - - // Assert - Assert.True(result); // Default value should be returned in offline mode - } - - [Fact] - public async Task UseLaunchDarkly_WithSdkKeyAndCustomConfiguration_AppliesConfiguration() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - var startWaitTime = TimeSpan.FromSeconds(3); - - builder.UseLaunchDarkly(TestSdkKey, config => - { - config.Offline(true); - config.StartWaitTime(startWaitTime); - }); - - var serviceProvider = services.BuildServiceProvider(); - var registeredConfig = serviceProvider.GetRequiredService(); - - // Act & Assert - Assert.True(registeredConfig.Offline); - Assert.Equal(startWaitTime, registeredConfig.StartWaitTime); - } - - [Fact] - public async Task UseLaunchDarkly_WithDomainSdkKeyAndCustomConfiguration_AppliesConfiguration() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - var startWaitTime = TimeSpan.FromSeconds(7); - - builder.UseLaunchDarkly(TestDomain, TestSdkKey, config => - { - config.Offline(true); - config.StartWaitTime(startWaitTime); - }); - - var serviceProvider = services.BuildServiceProvider(); - var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); - - // Act & Assert - Assert.True(registeredConfig.Offline); - Assert.Equal(startWaitTime, registeredConfig.StartWaitTime); - } - - #endregion - - #region Multi-Provider Integration Tests - - [Fact] - public void UseLaunchDarkly_MixedDefaultAndDomainProviders_WorkCorrectly() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Register both default and domain-scoped providers using different overloads - var defaultConfig = Configuration.Builder(TestSdkKey).Offline(true).Build(); - builder.UseLaunchDarkly(defaultConfig); - builder.UseLaunchDarkly(TestDomain, TestSdkKey, config => config.Offline(true)); - - var serviceProvider = services.BuildServiceProvider(); - - // Act - var defaultClient = serviceProvider.GetRequiredService(); - var domainClient = serviceProvider.GetRequiredKeyedService(TestDomain); - var defaultConfigRegistered = serviceProvider.GetRequiredService(); - var domainConfigRegistered = serviceProvider.GetRequiredKeyedService(TestDomain); - - // Assert - Assert.NotSame(defaultClient, domainClient); - Assert.NotSame(defaultConfigRegistered, domainConfigRegistered); - Assert.True(defaultConfigRegistered.Offline); - Assert.True(domainConfigRegistered.Offline); - } - - [Fact] - public void UseLaunchDarkly_MultipleDomainConfigurations_AreIsolated() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - const string domain1 = "fast-domain"; - const string domain2 = "slow-domain"; - var fastStartWait = TimeSpan.FromMilliseconds(100); - var slowStartWait = TimeSpan.FromSeconds(5); - - // Register using different overloads for variety - var fastConfig = Configuration.Builder(TestSdkKey) - .Offline(true) - .StartWaitTime(fastStartWait) - .Build(); - - builder.UseLaunchDarkly(domain1, fastConfig); - builder.UseLaunchDarkly(domain2, TestSdkKey, config => - { - config.Offline(true); - config.StartWaitTime(slowStartWait); - }); - - var serviceProvider = services.BuildServiceProvider(); - - // Act & Assert - var config1 = serviceProvider.GetRequiredKeyedService(domain1); - var config2 = serviceProvider.GetRequiredKeyedService(domain2); - - Assert.NotSame(config1, config2); - Assert.Equal(fastStartWait, config1.StartWaitTime); - Assert.Equal(slowStartWait, config2.StartWaitTime); - Assert.True(config1.Offline); - Assert.True(config2.Offline); - } - - [Fact] - public async Task UseLaunchDarkly_AllOverloads_ProvidersSupportAllValueTypes() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Test all 4 overloads - var config1 = Configuration.Builder(TestSdkKey).Offline(true).Build(); - var config2 = Configuration.Builder(TestSdkKey).Offline(true).Build(); - - builder.UseLaunchDarkly(config1); // Overload 1 - builder.UseLaunchDarkly("domain1", config2); // Overload 2 - builder.UseLaunchDarkly("domain2", TestSdkKey); // Overload 3 - builder.UseLaunchDarkly("domain3", TestSdkKey, c => c.Offline(true)); // Overload 4 - - var serviceProvider = services.BuildServiceProvider(); - - var defaultApi = serviceProvider.GetRequiredService(); - var domain1Api = serviceProvider.GetRequiredKeyedService("domain1"); - var domain2Api = serviceProvider.GetRequiredKeyedService("domain2"); - var domain3Api = serviceProvider.GetRequiredKeyedService("domain3"); - - // Act & Assert - Test all supported types on all providers - foreach (var api in new[] { defaultApi, domain1Api, domain2Api, domain3Api }) - { - var boolResult = await api.GetBooleanValueAsync("bool-flag", true); - Assert.True(boolResult); - - var stringResult = await api.GetStringValueAsync("string-flag", "default"); - Assert.Equal("default", stringResult); - - var intResult = await api.GetIntegerValueAsync("int-flag", 42); - Assert.Equal(42, intResult); - - var doubleResult = await api.GetDoubleValueAsync("double-flag", 3.14); - Assert.Equal(3.14, doubleResult); - - var structureResult = await api.GetObjectValueAsync("object-flag", new Value("default")); - Assert.Equal("default", structureResult.AsString); - } - } - - #endregion - - #region Provider Behavior Integration Tests - - [Fact] - public async Task UseLaunchDarkly_ProviderReturnsCorrectReasonInOfflineMode() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); - - var serviceProvider = services.BuildServiceProvider(); - var api = serviceProvider.GetRequiredService(); - - // Act - var result = await api.GetBooleanDetailsAsync(TestFlagKey, false); - - // Assert - Assert.False(result.Value); - Assert.Equal(Reason.Default, result.Reason); - } - - [Fact] - public async Task UseLaunchDarkly_DomainProviderReturnsCorrectReasonInOfflineMode() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - var config = Configuration.Builder(TestSdkKey).Offline(true).Build(); - - builder.UseLaunchDarkly(TestDomain, config); - - var serviceProvider = services.BuildServiceProvider(); - var api = serviceProvider.GetRequiredKeyedService(TestDomain); - - // Act - var result = await api.GetBooleanDetailsAsync(TestFlagKey, true); - - // Assert - Assert.True(result.Value); - Assert.Equal(Reason.Default, result.Reason); - } - - [Fact] - public async Task UseLaunchDarkly_ProviderHandlesEvaluationContext() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); - - var serviceProvider = services.BuildServiceProvider(); - var api = serviceProvider.GetRequiredService(); - - var context = EvaluationContext.Builder() - .Set("userId", "test-user") - .Set("email", "test@example.com") - .Build(); - - // Act - var result = await api.GetBooleanDetailsAsync(TestFlagKey, false, context); - - // Assert - Assert.False(result.Value); - Assert.Equal(Reason.Default, result.Reason); - } - - #endregion - - #region Early Validation Integration Tests - - [Fact] - public void UseLaunchDarkly_EarlyValidationWithConfiguration_FailsImmediately() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // This would normally fail at runtime, but early validation catches it immediately - // We can't easily test this without a malformed Configuration, but we can test that - // valid configurations pass early validation - var config = Configuration.Builder(TestSdkKey).Offline(true).Build(); - - // Act & Assert - Should not throw - builder.UseLaunchDarkly(config); - - var serviceProvider = services.BuildServiceProvider(); - var registeredConfig = serviceProvider.GetRequiredService(); - Assert.NotNull(registeredConfig); - } - - [Fact] - public void UseLaunchDarkly_EarlyValidationWithSdkKey_FailsImmediately() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Act & Assert - Valid configuration should pass early validation - builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); - - var serviceProvider = services.BuildServiceProvider(); - var registeredConfig = serviceProvider.GetRequiredService(); - Assert.NotNull(registeredConfig); - Assert.True(registeredConfig.Offline); - } - - [Fact] - public void UseLaunchDarkly_EarlyValidationWithDomain_FailsImmediately() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Act & Assert - Valid configuration should pass early validation - builder.UseLaunchDarkly(TestDomain, TestSdkKey, config => config.Offline(true)); - - var serviceProvider = services.BuildServiceProvider(); - var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); - Assert.NotNull(registeredConfig); - Assert.True(registeredConfig.Offline); - } - - #endregion - - #region Service Lifetime Integration Tests - - [Fact] - public void UseLaunchDarkly_ConfigurationRegisteredAsSingleton() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); - - var serviceProvider = services.BuildServiceProvider(); - - // Act - var config1 = serviceProvider.GetRequiredService(); - var config2 = serviceProvider.GetRequiredService(); - - // Assert - Assert.Same(config1, config2); - } - - [Fact] - public void UseLaunchDarkly_DomainConfigurationRegisteredAsKeyedSingleton() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - builder.UseLaunchDarkly(TestDomain, TestSdkKey, config => config.Offline(true)); - - var serviceProvider = services.BuildServiceProvider(); - - // Act - var config1 = serviceProvider.GetRequiredKeyedService(TestDomain); - var config2 = serviceProvider.GetRequiredKeyedService(TestDomain); - - // Assert - Assert.Same(config1, config2); - } - - [Fact] - public void UseLaunchDarkly_TryAddSingleton_DoesNotReplaceExistingRegistration() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - var config1 = Configuration.Builder(TestSdkKey).Offline(true).Build(); - var config2 = Configuration.Builder(TestSdkKey).Offline(false).Build(); - - // Act - builder.UseLaunchDarkly(config1); - builder.UseLaunchDarkly(config2); // Should not replace the first - - // Assert - var serviceProvider = services.BuildServiceProvider(); - var registeredConfig = serviceProvider.GetRequiredService(); - Assert.True(registeredConfig.Offline); // Should still be the first configuration - } - - [Fact] - public void UseLaunchDarkly_TryAddKeyedSingleton_DoesNotReplaceExistingRegistration() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - var config1 = Configuration.Builder(TestSdkKey).Offline(true).Build(); - var config2 = Configuration.Builder(TestSdkKey).Offline(false).Build(); - - // Act - builder.UseLaunchDarkly(TestDomain, config1); - builder.UseLaunchDarkly(TestDomain, config2); // Should not replace the first - - // Assert - var serviceProvider = services.BuildServiceProvider(); - var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); - Assert.True(registeredConfig.Offline); // Should still be the first configuration - } - - #endregion - - #region Resource Management Tests - - [Fact] - public void UseLaunchDarkly_ServiceProviderDisposed_DoesNotCauseMemoryLeaks() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - builder.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); - - // Act & Assert - using (var serviceProvider = services.BuildServiceProvider()) - { - var config = serviceProvider.GetRequiredService(); - Assert.NotNull(config); - Assert.True(config.Offline); - } - // Service provider disposed, should not cause issues - } - - [Fact] - public void UseLaunchDarkly_MultipleServiceProviders_IsolateConfigurations() - { - // Arrange - var services1 = new ServiceCollection(); - var services2 = new ServiceCollection(); - var builder1 = new OpenFeatureBuilder(services1); - var builder2 = new OpenFeatureBuilder(services2); - - builder1.UseLaunchDarkly(TestSdkKey, config => config.Offline(true)); - builder2.UseLaunchDarkly(TestSdkKey, config => config.Offline(false)); - - // Act - var serviceProvider1 = services1.BuildServiceProvider(); - var serviceProvider2 = services2.BuildServiceProvider(); - - var config1 = serviceProvider1.GetRequiredService(); - var config2 = serviceProvider2.GetRequiredService(); - - // Assert - Assert.NotSame(config1, config2); - Assert.True(config1.Offline); - Assert.False(config2.Offline); - } - - #endregion - } -} \ No newline at end of file From 1f6beb815ca560a91cc79afee2181e49078de451 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sat, 26 Jul 2025 14:53:15 +0400 Subject: [PATCH 09/22] =?UTF-8?q?=F0=9F=94=A8=20build:=20update=20project?= =?UTF-8?q?=20to=20target=20.NET=208.0=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The project file `LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.csproj` now targets only the `net8.0` framework, removing support for `netstandard2.0`, `net471`, and `net6.0`. The version range for the `OpenFeature.DependencyInjection` package reference was changed to `[2.2.0, 3.0.0)`. Conditional compilation directives were added in `LaunchDarklyOpenFeatureBuilderExtensions.cs` for .NET 8.0 or greater. The test project `LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj` was also updated to target `net8.0`, with the addition of `GenerateMSBuildEditorConfigFile` set to `false`. Tests in `LaunchDarklyOpenFeatureBuilderExtensionsTests.cs` were refactored to use `Theory` and `InlineData` for parameterized testing, improving exception handling and validation logic. Overall, these changes streamline the codebase and enhance test robustness. --- ....ServerProvider.DependencyInjection.csproj | 6 +- ...aunchDarklyOpenFeatureBuilderExtensions.cs | 2 + ...rProvider.DependencyInjection.Tests.csproj | 5 +- ...DarklyOpenFeatureBuilderExtensionsTests.cs | 213 ++++-------------- 4 files changed, 52 insertions(+), 174 deletions(-) diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.csproj b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.csproj index b6ca340..1b9467f 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.csproj +++ b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.csproj @@ -8,11 +8,9 @@ single framework that we are testing; this allows us to test with older SDK versions that would error out if they saw any newer target frameworks listed here, even if we weren't running those. --> - netstandard2.0;net471;net6.0;net8.0 + net8.0 $(BUILDFRAMEWORKS) - false - portable LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection Library @@ -41,7 +39,7 @@ - + diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs index 6368a9e..fa72308 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs +++ b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs @@ -7,6 +7,7 @@ namespace LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection { +#if NET8_0_OR_GREATER /// /// Provides extension methods for configuring the to use LaunchDarkly as a . /// @@ -130,4 +131,5 @@ private static Configuration CreateConfiguration(string stdKey, Action - net471;net6.0;net8.0 + net8.0 $(BUILDFRAMEWORKS) + false false @@ -36,4 +37,4 @@ - \ No newline at end of file + diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs index 0ac7c6f..2107d98 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs @@ -6,6 +6,7 @@ namespace LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests { +#if NET8_0_OR_GREATER public class LaunchDarklyOpenFeatureBuilderExtensionsTests { private const string TestSdkKey = "test-sdk-key"; @@ -26,7 +27,7 @@ public void UseLaunchDarkly_WithConfiguration_RegistersDefaultProvider() // Assert Assert.Same(builder, result); - + var serviceProvider = services.BuildServiceProvider(); var registeredConfig = serviceProvider.GetRequiredService(); Assert.NotNull(registeredConfig); @@ -51,17 +52,6 @@ public void UseLaunchDarkly_WithConfiguration_ConfigurationIsShared() Assert.Same(config1, config2); } - [Fact] - public void UseLaunchDarkly_WithNullConfiguration_ThrowsArgumentNullException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly((Configuration)null)); - } - [Fact] public void UseLaunchDarkly_MultipleCallsWithConfiguration_UsesTryAddSingleton() { @@ -98,7 +88,7 @@ public void UseLaunchDarkly_WithDomainAndConfiguration_RegistersDomainScopedProv // Assert Assert.Same(builder, result); - + var serviceProvider = services.BuildServiceProvider(); var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); Assert.NotNull(registeredConfig); @@ -123,51 +113,22 @@ public void UseLaunchDarkly_WithDomainAndConfiguration_ConfigurationIsShared() Assert.Same(config1, config2); } - [Fact] - public void UseLaunchDarkly_WithNullDomainAndConfiguration_ThrowsArgumentException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - var config = Configuration.Builder(TestSdkKey).Build(); - - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(null, config)); - } - - [Fact] - public void UseLaunchDarkly_WithEmptyDomainAndConfiguration_ThrowsArgumentException() + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void UseLaunchDarkly_WithEmptyDomainAndConfiguration_ThrowsArgumentException(string domain) { // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); var config = Configuration.Builder(TestSdkKey).Build(); - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(string.Empty, config)); - } - - [Fact] - public void UseLaunchDarkly_WithWhitespaceDomainAndConfiguration_ThrowsArgumentException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - var config = Configuration.Builder(TestSdkKey).Build(); - - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(" ", config)); - } - - [Fact] - public void UseLaunchDarkly_WithDomainAndNullConfiguration_ThrowsArgumentNullException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); + // Act + var result = builder.UseLaunchDarkly(domain, config); - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, (Configuration)null)); + // Assert + Assert.Same(builder, result); } [Fact] @@ -210,7 +171,7 @@ public void UseLaunchDarkly_WithSdkKey_RegistersDefaultProvider() // Assert Assert.Same(builder, result); - + var serviceProvider = services.BuildServiceProvider(); var config = serviceProvider.GetRequiredService(); Assert.NotNull(config); @@ -235,48 +196,15 @@ public void UseLaunchDarkly_WithSdkKeyAndConfiguration_RegistersDefaultProviderW // Assert Assert.Same(builder, result); - + var serviceProvider = services.BuildServiceProvider(); var config = serviceProvider.GetRequiredService(); - + Assert.True(configureWasCalled); Assert.NotNull(capturedBuilder); Assert.True(config.Offline); } - [Fact] - public void UseLaunchDarkly_WithNullSdkKey_ThrowsArgumentException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly((string)null)); - } - - [Fact] - public void UseLaunchDarkly_WithEmptySdkKey_ThrowsArgumentException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(string.Empty)); - } - - [Fact] - public void UseLaunchDarkly_WithWhitespaceSdkKey_ThrowsArgumentException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(" ")); - } - [Fact] public void UseLaunchDarkly_ConfigurationDelegateException_PropagatesExceptionImmediately() { @@ -287,28 +215,11 @@ public void UseLaunchDarkly_ConfigurationDelegateException_PropagatesExceptionIm // Act & Assert // The exception should be thrown immediately during registration due to early validation - var actualException = Assert.Throws(() => + var actualException = Assert.Throws(() => builder.UseLaunchDarkly(TestSdkKey, _ => throw expectedException)); Assert.Same(expectedException, actualException); } - [Fact] - public void UseLaunchDarkly_NullConfigurationDelegate_DoesNotThrow() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Act & Assert - should not throw - Configuration configuration = null; - var result = builder.UseLaunchDarkly(TestSdkKey, configuration); - - Assert.Same(builder, result); - var serviceProvider = services.BuildServiceProvider(); - var config = serviceProvider.GetRequiredService(); - Assert.NotNull(config); - } - #endregion #region SDK Key Overload Tests - Domain Provider @@ -325,7 +236,7 @@ public void UseLaunchDarkly_WithDomainAndSdkKey_RegistersDomainScopedProvider() // Assert Assert.Same(builder, result); - + var serviceProvider = services.BuildServiceProvider(); var config = serviceProvider.GetRequiredKeyedService(TestDomain); Assert.NotNull(config); @@ -350,79 +261,44 @@ public void UseLaunchDarkly_WithDomainSdkKeyAndConfiguration_RegistersDomainScop // Assert Assert.Same(builder, result); - + var serviceProvider = services.BuildServiceProvider(); var config = serviceProvider.GetRequiredKeyedService(TestDomain); - + Assert.True(configureWasCalled); Assert.NotNull(capturedBuilder); Assert.True(config.Offline); } [Fact] - public void UseLaunchDarkly_WithNullDomainAndSdkKey_ThrowsArgumentException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(null, TestSdkKey)); - } - - [Fact] - public void UseLaunchDarkly_WithEmptyDomainAndSdkKey_ThrowsArgumentException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(string.Empty, TestSdkKey)); - } - - [Fact] - public void UseLaunchDarkly_WithWhitespaceDomainAndSdkKey_ThrowsArgumentException() + public void UseLaunchDarkly_NullConfigurationDelegate_ThrowsNullReferenceException() { // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(" ", TestSdkKey)); - } - - [Fact] - public void UseLaunchDarkly_WithDomainAndNullSdkKey_ThrowsArgumentException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); + // Act + Configuration configuration = null; - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, (string)null)); + // Assert + Assert.Throws(() => builder.UseLaunchDarkly(TestSdkKey, configuration)); } - [Fact] - public void UseLaunchDarkly_WithDomainAndEmptySdkKey_ThrowsArgumentException() + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void UseLaunchDarkly_WithDomainAndInvalidSdkKey_ReturnsBuilder(string sdkKey) { // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, string.Empty)); - } - - [Fact] - public void UseLaunchDarkly_WithDomainAndWhitespaceSdkKey_ThrowsArgumentException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); + // Act + var result = builder.UseLaunchDarkly(TestDomain, sdkKey); - // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, " ")); + // Assert + Assert.Same(builder, result); } [Fact] @@ -435,7 +311,7 @@ public void UseLaunchDarkly_DomainConfigurationDelegateException_PropagatesExcep // Act & Assert // The exception should be thrown immediately during registration due to early validation - var actualException = Assert.Throws(() => + var actualException = Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, TestSdkKey, _ => throw expectedException)); Assert.Same(expectedException, actualException); } @@ -449,7 +325,7 @@ public void UseLaunchDarkly_DomainNullConfigurationDelegate_DoesNotThrow() // Act & Assert - should not throw var result = builder.UseLaunchDarkly(TestDomain, TestSdkKey, null); - + Assert.Same(builder, result); var serviceProvider = services.BuildServiceProvider(); var config = serviceProvider.GetRequiredKeyedService(TestDomain); @@ -461,45 +337,45 @@ public void UseLaunchDarkly_DomainNullConfigurationDelegate_DoesNotThrow() #region Builder Validation Tests [Fact] - public void UseLaunchDarkly_WithNullBuilder_ThrowsArgumentNullException() + public void UseLaunchDarkly_WithNullBuilder_ThrowsNullReferenceException() { // Arrange OpenFeatureBuilder builder = null; // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(TestSdkKey)); + Assert.Throws(() => builder.UseLaunchDarkly(TestSdkKey)); } [Fact] - public void UseLaunchDarkly_WithNullBuilderAndConfiguration_ThrowsArgumentNullException() + public void UseLaunchDarkly_WithNullBuilderAndConfiguration_ThrowsNullReferenceException() { // Arrange OpenFeatureBuilder builder = null; var config = Configuration.Builder(TestSdkKey).Build(); // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(config)); + Assert.Throws(() => builder.UseLaunchDarkly(config)); } [Fact] - public void UseLaunchDarkly_WithNullBuilderForDomain_ThrowsArgumentNullException() + public void UseLaunchDarkly_WithNullBuilderForDomain_ThrowsNullReferenceException() { // Arrange OpenFeatureBuilder builder = null; // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, TestSdkKey)); + Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, TestSdkKey)); } [Fact] - public void UseLaunchDarkly_WithNullBuilderForDomainAndConfiguration_ThrowsArgumentNullException() + public void UseLaunchDarkly_WithNullBuilderForDomainAndConfiguration_ThrowsNullReferenceException() { // Arrange OpenFeatureBuilder builder = null; var config = Configuration.Builder(TestSdkKey).Build(); // Act & Assert - Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, config)); + Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, config)); } #endregion @@ -520,7 +396,7 @@ public void UseLaunchDarkly_ConfigurationCloning_CreatesNewInstance() // Assert var serviceProvider = services.BuildServiceProvider(); var registeredConfig = serviceProvider.GetRequiredService(); - + // The registered config should be a rebuilt version, not the same instance // but should have the same properties Assert.True(registeredConfig.Offline); @@ -610,4 +486,5 @@ public void UseLaunchDarkly_DomainEarlyValidation_PreventsRuntimeFailures() #endregion } +#endif } From 267bde547462e5876240f12b89a1c40999119528 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sat, 26 Jul 2025 14:54:29 +0400 Subject: [PATCH 10/22] =?UTF-8?q?=F0=9F=94=A7=20chore:=20add=20.NET=208.0?= =?UTF-8?q?=20conditional=20compilation=20directives?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add conditional compilation directives for .NET 8.0 or greater in the LaunchDarklyOpenFeatureBuilderExtensions and corresponding test files. This ensures that specific class definitions and methods are included only when targeting .NET 8.0 or higher. --- .../LaunchDarklyOpenFeatureBuilderExtensions.cs | 2 -- .../LaunchDarklyOpenFeatureBuilderExtensionsTests.cs | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs index fa72308..6368a9e 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs +++ b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs @@ -7,7 +7,6 @@ namespace LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection { -#if NET8_0_OR_GREATER /// /// Provides extension methods for configuring the to use LaunchDarkly as a . /// @@ -131,5 +130,4 @@ private static Configuration CreateConfiguration(string stdKey, Action Date: Sat, 26 Jul 2025 15:23:24 +0400 Subject: [PATCH 11/22] =?UTF-8?q?=E2=9C=85=20test(LaunchDarklyOpenFeatureB?= =?UTF-8?q?uilderExtensions):=20rename=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renamed test methods for consistency and clarity in the `LaunchDarklyOpenFeatureBuilderExtensionsTests` class. Added new tests to verify behavior of the `UseLaunchDarkly` method with various configurations, including singleton registration and handling of null or whitespace SDK keys. Improved overall structure for better maintainability. --- ...DarklyOpenFeatureBuilderExtensionsTests.cs | 176 +++++++++++------- 1 file changed, 113 insertions(+), 63 deletions(-) diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs index 1abc43d..7f64e6f 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs @@ -11,10 +11,10 @@ public class LaunchDarklyOpenFeatureBuilderExtensionsTests private const string TestSdkKey = "test-sdk-key"; private const string TestDomain = "test-domain"; - #region Configuration Overload Tests - Default Provider + #region UseLaunchDarkly(Configuration) - Default Provider Tests [Fact] - public void UseLaunchDarkly_WithConfiguration_RegistersDefaultProvider() + public void UseLaunchDarklyWithConfiguration_WhenCalled_ShouldReturnSameBuilderInstance() { // Arrange var services = new ServiceCollection(); @@ -26,7 +26,20 @@ public void UseLaunchDarkly_WithConfiguration_RegistersDefaultProvider() // Assert Assert.Same(builder, result); + } + + [Fact] + public void UseLaunchDarklyWithConfiguration_WhenCalled_ShouldRegisterConfigurationAsSingleton() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var config = Configuration.Builder(TestSdkKey).Offline(true).Build(); + + // Act + builder.UseLaunchDarkly(config); + // Assert var serviceProvider = services.BuildServiceProvider(); var registeredConfig = serviceProvider.GetRequiredService(); Assert.NotNull(registeredConfig); @@ -34,7 +47,7 @@ public void UseLaunchDarkly_WithConfiguration_RegistersDefaultProvider() } [Fact] - public void UseLaunchDarkly_WithConfiguration_ConfigurationIsShared() + public void UseLaunchDarklyWithConfiguration_WhenCalledMultipleTimes_ShouldShareSameConfigurationInstance() { // Arrange var services = new ServiceCollection(); @@ -52,7 +65,7 @@ public void UseLaunchDarkly_WithConfiguration_ConfigurationIsShared() } [Fact] - public void UseLaunchDarkly_MultipleCallsWithConfiguration_UsesTryAddSingleton() + public void UseLaunchDarklyWithConfiguration_WhenCalledMultipleTimesWithDifferentConfigs_ShouldUseFirstConfiguration() { // Arrange var services = new ServiceCollection(); @@ -72,10 +85,10 @@ public void UseLaunchDarkly_MultipleCallsWithConfiguration_UsesTryAddSingleton() #endregion - #region Configuration Overload Tests - Domain Provider + #region UseLaunchDarkly(domain, Configuration) - Domain Provider Tests [Fact] - public void UseLaunchDarkly_WithDomainAndConfiguration_RegistersDomainScopedProvider() + public void UseLaunchDarklyWithDomainAndConfiguration_WhenCalled_ShouldReturnSameBuilderInstance() { // Arrange var services = new ServiceCollection(); @@ -87,7 +100,20 @@ public void UseLaunchDarkly_WithDomainAndConfiguration_RegistersDomainScopedProv // Assert Assert.Same(builder, result); + } + + [Fact] + public void UseLaunchDarklyWithDomainAndConfiguration_WhenCalled_ShouldRegisterConfigurationAsKeyedSingleton() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + var config = Configuration.Builder(TestSdkKey).Offline(true).Build(); + + // Act + builder.UseLaunchDarkly(TestDomain, config); + // Assert var serviceProvider = services.BuildServiceProvider(); var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); Assert.NotNull(registeredConfig); @@ -95,7 +121,7 @@ public void UseLaunchDarkly_WithDomainAndConfiguration_RegistersDomainScopedProv } [Fact] - public void UseLaunchDarkly_WithDomainAndConfiguration_ConfigurationIsShared() + public void UseLaunchDarklyWithDomainAndConfiguration_WhenCalledMultipleTimes_ShouldShareSameConfigurationInstance() { // Arrange var services = new ServiceCollection(); @@ -116,7 +142,7 @@ public void UseLaunchDarkly_WithDomainAndConfiguration_ConfigurationIsShared() [InlineData(null)] [InlineData("")] [InlineData(" ")] - public void UseLaunchDarkly_WithEmptyDomainAndConfiguration_ThrowsArgumentException(string domain) + public void UseLaunchDarklyWithDomainAndConfiguration_WhenDomainIsNullOrWhitespace_ShouldReturnBuilder(string domain) { // Arrange var services = new ServiceCollection(); @@ -131,7 +157,7 @@ public void UseLaunchDarkly_WithEmptyDomainAndConfiguration_ThrowsArgumentExcept } [Fact] - public void UseLaunchDarkly_WithDifferentDomainsAndConfigurations_RegistersSeparateConfigurations() + public void UseLaunchDarklyWithDomainAndConfiguration_WhenDifferentDomainsUsed_ShouldRegisterSeparateConfigurations() { // Arrange var services = new ServiceCollection(); @@ -156,10 +182,10 @@ public void UseLaunchDarkly_WithDifferentDomainsAndConfigurations_RegistersSepar #endregion - #region SDK Key Overload Tests - Default Provider + #region UseLaunchDarkly(sdkKey) - SDK Key Default Provider Tests [Fact] - public void UseLaunchDarkly_WithSdkKey_RegistersDefaultProvider() + public void UseLaunchDarklyWithSdkKey_WhenCalled_ShouldReturnSameBuilderInstance() { // Arrange var services = new ServiceCollection(); @@ -170,14 +196,26 @@ public void UseLaunchDarkly_WithSdkKey_RegistersDefaultProvider() // Assert Assert.Same(builder, result); + } + + [Fact] + public void UseLaunchDarklyWithSdkKey_WhenCalled_ShouldRegisterValidConfiguration() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act + builder.UseLaunchDarkly(TestSdkKey); + // Assert var serviceProvider = services.BuildServiceProvider(); var config = serviceProvider.GetRequiredService(); Assert.NotNull(config); } [Fact] - public void UseLaunchDarkly_WithSdkKeyAndConfiguration_RegistersDefaultProviderWithConfiguration() + public void UseLaunchDarklyWithSdkKeyAndDelegate_WhenCalled_ShouldApplyCustomConfiguration() { // Arrange var services = new ServiceCollection(); @@ -195,17 +233,17 @@ public void UseLaunchDarkly_WithSdkKeyAndConfiguration_RegistersDefaultProviderW // Assert Assert.Same(builder, result); - + var serviceProvider = services.BuildServiceProvider(); var config = serviceProvider.GetRequiredService(); - + Assert.True(configureWasCalled); Assert.NotNull(capturedBuilder); Assert.True(config.Offline); } [Fact] - public void UseLaunchDarkly_ConfigurationDelegateException_PropagatesExceptionImmediately() + public void UseLaunchDarklyWithSdkKeyAndDelegate_WhenConfigurationDelegateThrows_ShouldPropagateExceptionImmediately() { // Arrange var services = new ServiceCollection(); @@ -214,17 +252,17 @@ public void UseLaunchDarkly_ConfigurationDelegateException_PropagatesExceptionIm // Act & Assert // The exception should be thrown immediately during registration due to early validation - var actualException = Assert.Throws(() => + var actualException = Assert.Throws(() => builder.UseLaunchDarkly(TestSdkKey, _ => throw expectedException)); Assert.Same(expectedException, actualException); } #endregion - #region SDK Key Overload Tests - Domain Provider + #region UseLaunchDarkly(domain, sdkKey) - SDK Key Domain Provider Tests [Fact] - public void UseLaunchDarkly_WithDomainAndSdkKey_RegistersDomainScopedProvider() + public void UseLaunchDarklyWithDomainAndSdkKey_WhenCalled_ShouldReturnSameBuilderInstance() { // Arrange var services = new ServiceCollection(); @@ -235,14 +273,26 @@ public void UseLaunchDarkly_WithDomainAndSdkKey_RegistersDomainScopedProvider() // Assert Assert.Same(builder, result); + } + + [Fact] + public void UseLaunchDarklyWithDomainAndSdkKey_WhenCalled_ShouldRegisterValidKeyedConfiguration() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act + builder.UseLaunchDarkly(TestDomain, TestSdkKey); + // Assert var serviceProvider = services.BuildServiceProvider(); var config = serviceProvider.GetRequiredKeyedService(TestDomain); Assert.NotNull(config); } [Fact] - public void UseLaunchDarkly_WithDomainSdkKeyAndConfiguration_RegistersDomainScopedProviderWithConfiguration() + public void UseLaunchDarklyWithDomainAndSdkKeyAndDelegate_WhenCalled_ShouldApplyCustomConfiguration() { // Arrange var services = new ServiceCollection(); @@ -260,83 +310,83 @@ public void UseLaunchDarkly_WithDomainSdkKeyAndConfiguration_RegistersDomainScop // Assert Assert.Same(builder, result); - + var serviceProvider = services.BuildServiceProvider(); var config = serviceProvider.GetRequiredKeyedService(TestDomain); - + Assert.True(configureWasCalled); Assert.NotNull(capturedBuilder); Assert.True(config.Offline); } [Fact] - public void UseLaunchDarkly_NullConfigurationDelegate_ThrowsNullReferenceException() + public void UseLaunchDarklyWithDomainAndSdkKeyAndDelegate_WhenConfigurationDelegateThrows_ShouldPropagateExceptionImmediately() { // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); + var expectedException = new InvalidOperationException("Test exception"); - // Act - Configuration configuration = null; - - // Assert - Assert.Throws(() => builder.UseLaunchDarkly(TestSdkKey, configuration)); + // Act & Assert + // The exception should be thrown immediately during registration due to early validation + var actualException = Assert.Throws(() => + builder.UseLaunchDarkly(TestDomain, TestSdkKey, _ => throw expectedException)); + Assert.Same(expectedException, actualException); } - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void UseLaunchDarkly_WithDomainAndInvalidSdkKey_ReturnsBuilder(string sdkKey) + [Fact] + public void UseLaunchDarklyWithDomainAndSdkKeyAndDelegate_WhenNullConfigurationDelegate_ShouldNotThrowAndReturnBuilder() { // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); - // Act - var result = builder.UseLaunchDarkly(TestDomain, sdkKey); - - // Assert + // Act & Assert - should not throw + var result = builder.UseLaunchDarkly(TestDomain, TestSdkKey, null); + Assert.Same(builder, result); + var serviceProvider = services.BuildServiceProvider(); + var config = serviceProvider.GetRequiredKeyedService(TestDomain); + Assert.NotNull(config); } [Fact] - public void UseLaunchDarkly_DomainConfigurationDelegateException_PropagatesExceptionImmediately() + public void UseLaunchDarklyWithSdkKeyAndNullDelegate_WhenNullConfigurationPassed_ShouldThrowNullReferenceException() { // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); - var expectedException = new InvalidOperationException("Test exception"); - // Act & Assert - // The exception should be thrown immediately during registration due to early validation - var actualException = Assert.Throws(() => - builder.UseLaunchDarkly(TestDomain, TestSdkKey, _ => throw expectedException)); - Assert.Same(expectedException, actualException); + // Act + Configuration configuration = null; + + // Assert + Assert.Throws(() => builder.UseLaunchDarkly(TestSdkKey, configuration)); } - [Fact] - public void UseLaunchDarkly_DomainNullConfigurationDelegate_DoesNotThrow() + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void UseLaunchDarklyWithDomainAndSdkKey_WhenSdkKeyIsNullOrWhitespace_ShouldReturnBuilder(string sdkKey) { // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); - // Act & Assert - should not throw - var result = builder.UseLaunchDarkly(TestDomain, TestSdkKey, null); + // Act + var result = builder.UseLaunchDarkly(TestDomain, sdkKey); + // Assert Assert.Same(builder, result); - var serviceProvider = services.BuildServiceProvider(); - var config = serviceProvider.GetRequiredKeyedService(TestDomain); - Assert.NotNull(config); } #endregion - #region Builder Validation Tests + #region Null Builder Validation Tests [Fact] - public void UseLaunchDarkly_WithNullBuilder_ThrowsNullReferenceException() + public void UseLaunchDarklyWithNullBuilder_WhenSdkKeyOverloadUsed_ShouldThrowNullReferenceException() { // Arrange OpenFeatureBuilder builder = null; @@ -346,7 +396,7 @@ public void UseLaunchDarkly_WithNullBuilder_ThrowsNullReferenceException() } [Fact] - public void UseLaunchDarkly_WithNullBuilderAndConfiguration_ThrowsNullReferenceException() + public void UseLaunchDarklyWithNullBuilder_WhenConfigurationOverloadUsed_ShouldThrowNullReferenceException() { // Arrange OpenFeatureBuilder builder = null; @@ -357,7 +407,7 @@ public void UseLaunchDarkly_WithNullBuilderAndConfiguration_ThrowsNullReferenceE } [Fact] - public void UseLaunchDarkly_WithNullBuilderForDomain_ThrowsNullReferenceException() + public void UseLaunchDarklyWithNullBuilder_WhenDomainSdkKeyOverloadUsed_ShouldThrowNullReferenceException() { // Arrange OpenFeatureBuilder builder = null; @@ -367,7 +417,7 @@ public void UseLaunchDarkly_WithNullBuilderForDomain_ThrowsNullReferenceExceptio } [Fact] - public void UseLaunchDarkly_WithNullBuilderForDomainAndConfiguration_ThrowsNullReferenceException() + public void UseLaunchDarklyWithNullBuilder_WhenDomainConfigurationOverloadUsed_ShouldThrowNullReferenceException() { // Arrange OpenFeatureBuilder builder = null; @@ -379,10 +429,10 @@ public void UseLaunchDarkly_WithNullBuilderForDomainAndConfiguration_ThrowsNullR #endregion - #region Advanced Configuration Tests + #region Configuration Property Preservation Tests [Fact] - public void UseLaunchDarkly_ConfigurationCloning_CreatesNewInstance() + public void UseLaunchDarklyWithConfiguration_WhenCustomPropertiesSet_ShouldPreserveConfigurationProperties() { // Arrange var services = new ServiceCollection(); @@ -395,14 +445,14 @@ public void UseLaunchDarkly_ConfigurationCloning_CreatesNewInstance() // Assert var serviceProvider = services.BuildServiceProvider(); var registeredConfig = serviceProvider.GetRequiredService(); - + // The registered config should be a rebuilt version, not the same instance // but should have the same properties Assert.True(registeredConfig.Offline); } [Fact] - public void UseLaunchDarkly_CustomConfigurationProperties_ArePreserved() + public void UseLaunchDarklyWithSdkKeyAndDelegate_WhenCustomPropertiesSet_ShouldPreserveConfigurationProperties() { // Arrange var services = new ServiceCollection(); @@ -424,7 +474,7 @@ public void UseLaunchDarkly_CustomConfigurationProperties_ArePreserved() } [Fact] - public void UseLaunchDarkly_DomainCustomConfigurationProperties_ArePreserved() + public void UseLaunchDarklyWithDomainAndSdkKeyAndDelegate_WhenCustomPropertiesSet_ShouldPreserveConfigurationProperties() { // Arrange var services = new ServiceCollection(); @@ -447,10 +497,10 @@ public void UseLaunchDarkly_DomainCustomConfigurationProperties_ArePreserved() #endregion - #region Early Validation Tests + #region Early Validation Behavior Tests [Fact] - public void UseLaunchDarkly_EarlyValidation_PreventsRuntimeFailures() + public void UseLaunchDarklyWithSdkKeyAndDelegate_WhenEarlyValidationPasses_ShouldPreventRuntimeFailures() { // Arrange var services = new ServiceCollection(); @@ -467,7 +517,7 @@ public void UseLaunchDarkly_EarlyValidation_PreventsRuntimeFailures() } [Fact] - public void UseLaunchDarkly_DomainEarlyValidation_PreventsRuntimeFailures() + public void UseLaunchDarklyWithDomainAndSdkKeyAndDelegate_WhenEarlyValidationPasses_ShouldPreventRuntimeFailures() { // Arrange var services = new ServiceCollection(); From 63f26df73b5d3a7661d385acc0f1d0bed3f50234 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sat, 26 Jul 2025 18:39:16 +0400 Subject: [PATCH 12/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(config):=20?= =?UTF-8?q?improve=20configuration=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added `Microsoft.Extensions.Options` namespace for options pattern support. - Updated comments for clarity on early configuration validation. - Simplified singleton configuration registration by passing the configuration object directly. - Enhanced clarity and consistency in domain-scoped configuration registration. - Introduced a default configuration provider for flexible resolution based on name selection policy. - Overall improvements to robustness and clarity in OpenFeature integration with LaunchDarkly. --- ...aunchDarklyOpenFeatureBuilderExtensions.cs | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs index 6368a9e..a37e8d4 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs +++ b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs @@ -1,6 +1,7 @@ using LaunchDarkly.Sdk.Server; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using OpenFeature; using OpenFeature.DependencyInjection; using System; @@ -77,9 +78,9 @@ private static OpenFeatureBuilder RegisterLaunchDarklyProvider( Func resolveConfiguration) { // Perform early configuration validation to ensure the provider is correctly constructed. - // This avoids runtime failures by eagerly building the configuration during setup. + // This ensures any misconfiguration is caught during application startup rather than at runtime. var config = createConfiguration(); - builder.Services.TryAddSingleton(_ => config); + builder.Services.TryAddSingleton(config); return builder.AddProvider(serviceProvider => new Provider(resolveConfiguration(serviceProvider))); } @@ -98,11 +99,25 @@ private static OpenFeatureBuilder RegisterLaunchDarklyProviderForDomain( Func createConfiguration, Func resolveConfiguration) { - // Applies the same early validation strategy as the default registration path, - // ensuring domain-scoped configurations fail fast if misconfigured. + // Perform early validation of the configuration to ensure it is valid before registration. + // This approach is consistent with the default (non-domain) registration path and helps fail fast on misconfiguration. var config = createConfiguration(); - builder.Services.TryAddKeyedSingleton(domain, (_, obj) => config); + // Register the domain-scoped configuration as a keyed singleton. + builder.Services.TryAddKeyedSingleton(domain, (_, key) => config); + + // Register the default configuration provider, which resolves the appropriate domain-scoped configuration + // using the default name selection policy defined in PolicyNameOptions. + // This enables resolving Configuration via serviceProvider.GetRequiredService() + // when no specific domain key is explicitly provided. + builder.Services.TryAddSingleton(provider => + { + var policy = provider.GetRequiredService>().Value; + var name = policy.DefaultNameSelector(provider); + return provider.GetRequiredKeyedService(name); + }); + + // Register the domain-scoped provider instance. return builder.AddProvider(domain, (serviceProvider, key) => new Provider(resolveConfiguration(serviceProvider, key))); } From c803a2d768b56f3d448879b197780e86822fca70 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sat, 26 Jul 2025 18:39:41 +0400 Subject: [PATCH 13/22] =?UTF-8?q?=F0=9F=94=A7=20chore(tests):=20update=20d?= =?UTF-8?q?ependencies=20and=20add=20integration=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated `LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj` to include new package references for `Microsoft.Extensions.Logging` and `Microsoft.Extensions.DependencyInjection` at version `8.0.1`. Added a new test class `LaunchDarklyIntegrationTests` in `LaunchDarklyIntegrationTests.cs` with multiple integration tests covering configuration overloads, SDK key overloads, multi-provider setups, and resource management. Introduced helper methods to enhance test structure and reusability. --- ...rProvider.DependencyInjection.Tests.csproj | 1 + .../LaunchDarklyIntegrationTests.cs | 557 ++++++++++++++++++ 2 files changed, 558 insertions(+) create mode 100644 test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyIntegrationTests.cs diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj index 0f30dcc..3b5f47d 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj @@ -30,6 +30,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyIntegrationTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyIntegrationTests.cs new file mode 100644 index 0000000..c230de9 --- /dev/null +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyIntegrationTests.cs @@ -0,0 +1,557 @@ +using System; +using System.Threading.Tasks; +using LaunchDarkly.Sdk.Server; +using Microsoft.Extensions.DependencyInjection; +using OpenFeature; +using OpenFeature.DependencyInjection; +using OpenFeature.Model; +using Xunit; + +namespace LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests +{ + public class LaunchDarklyIntegrationTests + { + private const string TestSdkKey = "test-sdk-key"; + private const string TestDomain = "test-domain"; + private const string TestFlagKey = "test-flag"; + + #region Helper Methods + + /// + /// Creates a service provider with OpenFeature and LaunchDarkly configured for the default provider. + /// + /// Optional configuration delegate for customizing LaunchDarkly settings. + /// A configured service provider with scope validation enabled. + private static async Task CreateServiceProviderWithDefaultLaunchDarklyAsync(Action configure = null) + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddOpenFeature(builder => + { + builder.AddContext(contextBuilder => { + contextBuilder.Set("targetingKey", "the-key"); + }); + + if (configure != null) + { + builder.UseLaunchDarkly(TestSdkKey, configure); + } + else + { + builder.UseLaunchDarkly(TestSdkKey); + } + }); + + var serviceProvider = services.BuildServiceProvider(validateScopes: true); + + var lifecycleManager = serviceProvider.GetRequiredService(); + await lifecycleManager.EnsureInitializedAsync(); + + return serviceProvider; + } + + /// + /// Creates a service provider with OpenFeature and LaunchDarkly configured for a domain-scoped provider. + /// + /// The domain identifier for the scoped provider. + /// Optional configuration delegate for customizing LaunchDarkly settings. + /// A configured service provider with scope validation enabled. + private static async Task CreateServiceProviderWithDomainLaunchDarklyAsync(string domain, Action configure = null) + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddOpenFeature(builder => + { + if (configure != null) + { + builder.UseLaunchDarkly(domain, TestSdkKey, configure); + } + else + { + builder.UseLaunchDarkly(domain, TestSdkKey); + } + }); + + var serviceProvider = services.BuildServiceProvider(validateScopes: true); + + var lifecycleManager = serviceProvider.GetRequiredService(); + await lifecycleManager.EnsureInitializedAsync(); + + return serviceProvider; + } + + /// + /// Creates a scoped service provider for testing scoped services like IFeatureClient. + /// + /// The root service provider to create a scope from. + /// A scoped service provider. + private static IServiceProvider CreateScopedServiceProvider(IServiceProvider rootServiceProvider) + { + var scopeFactory = rootServiceProvider.GetRequiredService(); + return scopeFactory.CreateScope().ServiceProvider; + } + + /// + /// Creates a service provider with multiple OpenFeature providers configured. + /// + /// Action to configure multiple providers on the OpenFeature builder. + /// A configured service provider with scope validation enabled. + private static async Task CreateServiceProviderWithMultipleProvidersAsync(Action configureBuilder) + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddOpenFeature(configureBuilder); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); + + var lifecycleManager = serviceProvider.GetRequiredService(); + await lifecycleManager.EnsureInitializedAsync(); + + return serviceProvider; + } + + #endregion + + #region Configuration Overload Integration Tests + + [Fact] + public async Task ConfigurationOverload_DefaultProvider_ShouldResolveFeatureClientSuccessfully() + { + // Arrange + var serviceProvider = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg.Offline(true)); + var scopedProvider = CreateScopedServiceProvider(serviceProvider); + var api = scopedProvider.GetRequiredService(); + + // Act + var result = await api.GetBooleanValueAsync(TestFlagKey, false); + + // Assert + Assert.False(result); // Default value should be returned in offline mode + } + + [Fact] + public async Task ConfigurationOverload_DomainProvider_ShouldResolveKeyedFeatureClientSuccessfully() + { + // Arrange + var serviceProvider = await CreateServiceProviderWithDomainLaunchDarklyAsync(TestDomain, cfg => cfg.Offline(true)); + var scopedProvider = CreateScopedServiceProvider(serviceProvider); + var api = scopedProvider.GetRequiredKeyedService(TestDomain); + + // Act + var result = await api.GetBooleanValueAsync(TestFlagKey, true); + + // Assert + Assert.True(result); // Default value should be returned in offline mode + } + + [Fact] + public async Task ConfigurationOverload_DefaultProvider_ShouldPreserveCustomConfigurationSettings() + { + // Arrange + var startWaitTime = TimeSpan.FromSeconds(5); + var serviceProvider = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg + .Offline(true) + .StartWaitTime(startWaitTime)); + + // Act + var registeredConfig = serviceProvider.GetRequiredService(); + + // Assert + Assert.True(registeredConfig.Offline); + Assert.Equal(startWaitTime, registeredConfig.StartWaitTime); + } + + [Fact] + public async Task ConfigurationOverload_DomainProvider_ShouldPreserveCustomConfigurationSettings() + { + // Arrange + var startWaitTime = TimeSpan.FromSeconds(10); + var serviceProvider = await CreateServiceProviderWithDomainLaunchDarklyAsync(TestDomain, cfg => cfg + .Offline(true) + .StartWaitTime(startWaitTime)); + + // Act + var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); + + // Assert + Assert.True(registeredConfig.Offline); + Assert.Equal(startWaitTime, registeredConfig.StartWaitTime); + } + + #endregion + + #region SDK Key Overload Integration Tests + + [Fact] + public async Task SdkKeyOverload_DefaultProvider_ShouldResolveFeatureClientSuccessfully() + { + // Arrange + var serviceProvider = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg.Offline(true)); + var scopedProvider = CreateScopedServiceProvider(serviceProvider); + var api = scopedProvider.GetRequiredService(); + + // Act + var result = await api.GetBooleanValueAsync(TestFlagKey, false); + + // Assert + Assert.False(result); // Default value should be returned in offline mode + } + + [Fact] + public async Task SdkKeyOverload_DomainProvider_ShouldResolveKeyedFeatureClientSuccessfully() + { + // Arrange + var serviceProvider = await CreateServiceProviderWithDomainLaunchDarklyAsync(TestDomain, cfg => cfg.Offline(true)); + var scopedProvider = CreateScopedServiceProvider(serviceProvider); + var api = scopedProvider.GetRequiredKeyedService(TestDomain); + + // Act + var result = await api.GetBooleanValueAsync(TestFlagKey, true); + + // Assert + Assert.True(result); // Default value should be returned in offline mode + } + + [Fact] + public async Task SdkKeyOverload_DefaultProvider_ShouldApplyCustomConfigurationFromDelegate() + { + // Arrange + var startWaitTime = TimeSpan.FromSeconds(3); + var serviceProvider = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg + .Offline(true) + .StartWaitTime(startWaitTime)); + + // Act + var registeredConfig = serviceProvider.GetRequiredService(); + + // Assert + Assert.True(registeredConfig.Offline); + Assert.Equal(startWaitTime, registeredConfig.StartWaitTime); + } + + [Fact] + public async Task SdkKeyOverload_DomainProvider_ShouldApplyCustomConfigurationFromDelegate() + { + // Arrange + var startWaitTime = TimeSpan.FromSeconds(7); + var serviceProvider = await CreateServiceProviderWithDomainLaunchDarklyAsync(TestDomain, cfg => cfg + .Offline(true) + .StartWaitTime(startWaitTime)); + + // Act + var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); + + // Assert + Assert.True(registeredConfig.Offline); + Assert.Equal(startWaitTime, registeredConfig.StartWaitTime); + } + + #endregion + + #region Multi-Provider Setup Integration Tests + + [Fact] + public async Task MultiProvider_MixedOverloads_ShouldRegisterBothDefaultAndDomainProvidersCorrectly() + { + // Arrange + var serviceProvider = await CreateServiceProviderWithMultipleProvidersAsync(builder => + { + // Register both default and domain-scoped providers using different overloads + builder + .UseLaunchDarkly("domain1", TestSdkKey, cfg => cfg.Offline(true)) + .UseLaunchDarkly("domain2", TestSdkKey, cfg => cfg.Offline(true)) + .AddPolicyName(policy => + { + policy.DefaultNameSelector = _ => "domain1"; + }); + }); + + var scopedProvider = CreateScopedServiceProvider(serviceProvider); + + // Act + var defaultClient = scopedProvider.GetRequiredService(); + var domainClient = scopedProvider.GetRequiredKeyedService("domain2"); + var defaultConfigRegistered = serviceProvider.GetRequiredService(); + var domainConfigRegistered = serviceProvider.GetRequiredKeyedService("domain2"); + + // Assert + Assert.NotSame(defaultClient, domainClient); + Assert.NotSame(defaultConfigRegistered, domainConfigRegistered); + Assert.True(defaultConfigRegistered.Offline); + Assert.True(domainConfigRegistered.Offline); + } + + [Fact] + public async Task MultiProvider_MultipleDomains_ShouldIsolateConfigurationsCorrectly() + { + // Arrange + const string fastDomain = "fast-domain"; + const string slowDomain = "slow-domain"; + var fastStartWait = TimeSpan.FromMilliseconds(100); + var slowStartWait = TimeSpan.FromSeconds(5); + + var serviceProvider = await CreateServiceProviderWithMultipleProvidersAsync(builder => + { + // Register using different configurations for variety + builder.UseLaunchDarkly(fastDomain, TestSdkKey, cfg => cfg + .Offline(true) + .StartWaitTime(fastStartWait)); + + builder.UseLaunchDarkly(slowDomain, TestSdkKey, cfg => cfg + .Offline(true) + .StartWaitTime(slowStartWait)); + + builder.AddPolicyName(policy => policy.DefaultNameSelector = _ => fastDomain); + }); + + // Act & Assert + var config1 = serviceProvider.GetRequiredKeyedService(fastDomain); + var config2 = serviceProvider.GetRequiredKeyedService(slowDomain); + + Assert.NotSame(config1, config2); + Assert.Equal(fastStartWait, config1.StartWaitTime); + Assert.Equal(slowStartWait, config2.StartWaitTime); + Assert.True(config1.Offline); + Assert.True(config2.Offline); + } + + #endregion + + #region OpenFeature Value Type Support Tests + + [Fact] + public async Task AllOverloads_FeatureProviders_ShouldSupportAllOpenFeatureValueTypes() + { + // Arrange + var serviceProvider = await CreateServiceProviderWithMultipleProvidersAsync(builder => + { + // Test all SDK key overloads + builder.UseLaunchDarkly("domain1", TestSdkKey, cfg => cfg.Offline(true)); // Default provider + builder.UseLaunchDarkly("domain2", TestSdkKey, cfg => cfg.Offline(true)); // Domain provider + builder.UseLaunchDarkly("domain3", TestSdkKey, cfg => cfg.Offline(true)); // Domain provider + builder.AddPolicyName(policy => policy.DefaultNameSelector = _ => "domain1"); + }); + + var scopedProvider = CreateScopedServiceProvider(serviceProvider); + + var defaultApi = scopedProvider.GetRequiredService(); + var domain1Api = scopedProvider.GetRequiredKeyedService("domain1"); + var domain2Api = scopedProvider.GetRequiredKeyedService("domain2"); + var domain3Api = scopedProvider.GetRequiredKeyedService("domain3"); + + // Act & Assert - Test all supported types on all providers + foreach (var api in new[] { defaultApi, domain1Api, domain2Api, domain3Api }) + { + var boolResult = await api.GetBooleanValueAsync("bool-flag", true); + Assert.True(boolResult); + + var stringResult = await api.GetStringValueAsync("string-flag", "default"); + Assert.Equal("default", stringResult); + + var intResult = await api.GetIntegerValueAsync("int-flag", 42); + Assert.Equal(42, intResult); + + var doubleResult = await api.GetDoubleValueAsync("double-flag", 3.14); + Assert.Equal(3.14, doubleResult); + + var structureResult = await api.GetObjectValueAsync("object-flag", new Value("default")); + Assert.Equal("default", structureResult.AsString); + } + } + + #endregion + + #region OpenFeature Behavior Integration Tests + + [Fact] + public async Task DefaultProvider_InOfflineMode_ShouldReturnCorrectReasonAndDefaultValue() + { + // Arrange + var serviceProvider = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg.Offline(true)); + var scopedProvider = CreateScopedServiceProvider(serviceProvider); + var api = scopedProvider.GetRequiredService(); + + // Act + var result = await api.GetBooleanDetailsAsync(TestFlagKey, false); + + // Assert + Assert.False(result.Value); + } + + [Fact] + public async Task DefaultProvider_WithEvaluationContext_ShouldHandleContextCorrectly() + { + // Arrange + var serviceProvider = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg.Offline(true)); + var scopedProvider = CreateScopedServiceProvider(serviceProvider); + var api = scopedProvider.GetRequiredService(); + + var context = EvaluationContext.Builder() + .Set("userId", "test-user") + .Set("email", "test@example.com") + .Build(); + + // Act + var result = await api.GetBooleanDetailsAsync(TestFlagKey, false, context); + + // Assert + Assert.False(result.Value); + //Assert.Equal(Reason.Default, result.Reason); + } + + #endregion + + #region Service Lifetime Integration Tests + + [Fact] + public async Task DefaultProvider_Configuration_ShouldBeRegisteredAsSingleton() + { + // Arrange + var serviceProvider = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg.Offline(true)); + + // Act + var config1 = serviceProvider.GetRequiredService(); + var config2 = serviceProvider.GetRequiredService(); + + // Assert + Assert.Same(config1, config2); + } + + [Fact] + public async Task DomainProvider_Configuration_ShouldBeRegisteredAsKeyedSingleton() + { + // Arrange + var serviceProvider = await CreateServiceProviderWithDomainLaunchDarklyAsync(TestDomain, cfg => cfg.Offline(true)); + + // Act + var config1 = serviceProvider.GetRequiredKeyedService(TestDomain); + var config2 = serviceProvider.GetRequiredKeyedService(TestDomain); + + // Assert + Assert.Same(config1, config2); + } + + [Fact] + public async Task TryAddSingleton_MultipleRegistrations_ShouldNotReplaceExistingRegistration() + { + // Arrange & Act + var serviceProvider = await CreateServiceProviderWithMultipleProvidersAsync(builder => + { + // First registration should win due to TryAddSingleton behavior + builder.UseLaunchDarkly(TestSdkKey, cfg => cfg.Offline(true)); + builder.UseLaunchDarkly(TestSdkKey, cfg => cfg.Offline(false)); // Should not replace the first + }); + + // Assert + var registeredConfig = serviceProvider.GetRequiredService(); + Assert.True(registeredConfig.Offline); // Should still be the first configuration + } + + [Fact] + public async Task TryAddKeyedSingleton_MultipleRegistrations_ShouldNotReplaceExistingRegistration() + { + // Arrange + var serviceProvider = await CreateServiceProviderWithMultipleProvidersAsync(builder => + { + // First registration should win due to TryAddKeyedSingleton behavior + builder.UseLaunchDarkly(TestDomain, TestSdkKey, cfg => cfg.Offline(true)); + builder.UseLaunchDarkly(TestDomain, TestSdkKey, cfg => cfg.Offline(false)); // Should not replace the first + builder.AddPolicyName(policy => policy.DefaultNameSelector = _ => TestDomain); + }); + + // Act + var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); + + // Assert + Assert.True(registeredConfig.Offline); // Should still be the first configuration + } + + #endregion + + #region Resource Management Integration Tests + + [Fact] + public async Task ServiceProviderDisposal_AfterUsingProviders_ShouldNotCauseMemoryLeaks() + { + // Arrange + var serviceProvider = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg.Offline(true)); + + // Act + var config = serviceProvider.GetRequiredService(); + + // Assert + Assert.NotNull(config); + Assert.True(config.Offline); + } + + [Fact] + public async Task MultipleServiceProviders_WithSameConfiguration_ShouldIsolateConfigurations() + { + // Arrange + var serviceProvider1 = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg.Offline(true)); + var serviceProvider2 = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg.Offline(false)); + + // Act + var config1 = serviceProvider1.GetRequiredService(); + var config2 = serviceProvider2.GetRequiredService(); + + // Assert + Assert.NotSame(config1, config2); + Assert.True(config1.Offline); + Assert.False(config2.Offline); + } + + #endregion + + #region Early Validation Integration Tests + + [Fact] + public async Task EarlyValidation_WithValidConfiguration_ShouldPassValidationAndRegisterCorrectly() + { + // Arrange + var serviceProvider = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg.Offline(true)); + + // Act + var registeredConfig = serviceProvider.GetRequiredService(); + + // Assert + Assert.NotNull(registeredConfig); + Assert.True(registeredConfig.Offline); + } + + [Fact] + public async Task EarlyValidation_WithValidDomainConfiguration_ShouldPassValidationAndRegisterCorrectly() + { + // Arrange + var serviceProvider = await CreateServiceProviderWithDomainLaunchDarklyAsync(TestDomain, cfg => cfg.Offline(true)); + + // Act + var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); + + // Assert + Assert.NotNull(registeredConfig); + Assert.True(registeredConfig.Offline); + } + + [Fact] + public void EarlyValidation_WithPrebuiltConfiguration_ShouldPassValidationAndRegisterCorrectly() + { + // Arrange + var services = new ServiceCollection(); + var config = Configuration.Builder(TestSdkKey).Offline(true).Build(); + + services.AddOpenFeature(builder => + { + builder.UseLaunchDarkly(config); + }); + + // Act + var serviceProvider = services.BuildServiceProvider(validateScopes: true); + + // Assert + var registeredConfig = serviceProvider.GetRequiredService(); + Assert.NotNull(registeredConfig); + } + + #endregion + } +} \ No newline at end of file From 5e6b8286284b4b7c1359ff1205b9d1c539d9bd86 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sat, 26 Jul 2025 18:41:56 +0400 Subject: [PATCH 14/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(OpenFeature?= =?UTF-8?q?):=20remove=20context=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed the block that adds context to the OpenFeature builder, specifically the targeting key setup. This change may impact how targeting is handled in the application. --- .../LaunchDarklyIntegrationTests.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyIntegrationTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyIntegrationTests.cs index c230de9..5900e85 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyIntegrationTests.cs +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyIntegrationTests.cs @@ -28,10 +28,6 @@ private static async Task CreateServiceProviderWithDefaultLaun services.AddLogging(); services.AddOpenFeature(builder => { - builder.AddContext(contextBuilder => { - contextBuilder.Set("targetingKey", "the-key"); - }); - if (configure != null) { builder.UseLaunchDarkly(TestSdkKey, configure); From ccbd1d69a41470871487be3885e30bc54ae9ba43 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sat, 26 Jul 2025 21:46:58 +0400 Subject: [PATCH 15/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(provider):?= =?UTF-8?q?=20rename=20and=20optimize=20configuration=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor methods for configuring the LaunchDarkly feature provider in the OpenFeature library. Updated method names for clarity, replacing `CreateServiceProviderWithDefaultLaunchDarklyAsync` with `ConfigureLaunchDarklyAsync`. Changed return types from `Task` to `ValueTask` for performance improvements. Enhanced comments and XML documentation for better understanding. Updated tests to ensure they validate the new configurations correctly. --- .../LaunchDarklyIntegrationTests.cs | 212 ++++++++---------- 1 file changed, 96 insertions(+), 116 deletions(-) diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyIntegrationTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyIntegrationTests.cs index 5900e85..d61eea6 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyIntegrationTests.cs +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyIntegrationTests.cs @@ -18,58 +18,51 @@ public class LaunchDarklyIntegrationTests #region Helper Methods /// - /// Creates a service provider with OpenFeature and LaunchDarkly configured for the default provider. + /// Configures an with OpenFeature and LaunchDarkly as the default feature provider. /// - /// Optional configuration delegate for customizing LaunchDarkly settings. - /// A configured service provider with scope validation enabled. - private static async Task CreateServiceProviderWithDefaultLaunchDarklyAsync(Action configure = null) - { - var services = new ServiceCollection(); - services.AddLogging(); - services.AddOpenFeature(builder => - { - if (configure != null) - { - builder.UseLaunchDarkly(TestSdkKey, configure); - } - else - { - builder.UseLaunchDarkly(TestSdkKey); - } - }); + /// + /// Optional delegate to customize the LaunchDarkly during registration. + /// + /// + /// An initialized with LaunchDarkly configured as the default provider. + /// + private static ValueTask ConfigureLaunchDarklyAsync(Action configure = null) + => ConfigureOpenFeatureAsync(builder => builder.UseLaunchDarkly(TestSdkKey, configure)); - var serviceProvider = services.BuildServiceProvider(validateScopes: true); - - var lifecycleManager = serviceProvider.GetRequiredService(); - await lifecycleManager.EnsureInitializedAsync(); - - return serviceProvider; - } + /// + /// Configures an with OpenFeature and LaunchDarkly registered as a domain-scoped feature provider. + /// + /// The domain identifier to associate with the scoped provider (e.g., tenant or environment). + /// + /// Optional delegate to customize the LaunchDarkly for the specified domain. + /// + /// + /// An initialized with domain-scoped LaunchDarkly support. + /// + private static ValueTask ConfigureLaunchDarklyAsync(string domain, Action configure = null) + => ConfigureOpenFeatureAsync(builder => builder.UseLaunchDarkly(domain, TestSdkKey, configure)); /// - /// Creates a service provider with OpenFeature and LaunchDarkly configured for a domain-scoped provider. + /// Configures an with OpenFeature and one or more feature providers. /// - /// The domain identifier for the scoped provider. - /// Optional configuration delegate for customizing LaunchDarkly settings. - /// A configured service provider with scope validation enabled. - private static async Task CreateServiceProviderWithDomainLaunchDarklyAsync(string domain, Action configure = null) + /// + /// Delegate to configure the with feature providers. + /// + /// + /// An initialized with provider lifecycle setup and validation enabled. + /// + private static async ValueTask ConfigureOpenFeatureAsync(Action configureBuilder) { var services = new ServiceCollection(); services.AddLogging(); - services.AddOpenFeature(builder => - { - if (configure != null) - { - builder.UseLaunchDarkly(domain, TestSdkKey, configure); - } - else - { - builder.UseLaunchDarkly(domain, TestSdkKey); - } - }); + // Register OpenFeature with the configured providers + services.AddOpenFeature(configureBuilder); + + // Build the root service provider with scope validation enabled var serviceProvider = services.BuildServiceProvider(validateScopes: true); + // Ensure the feature provider lifecycle is initialized (e.g., LaunchDarkly ready for evaluations) var lifecycleManager = serviceProvider.GetRequiredService(); await lifecycleManager.EnsureInitializedAsync(); @@ -87,24 +80,6 @@ private static IServiceProvider CreateScopedServiceProvider(IServiceProvider roo return scopeFactory.CreateScope().ServiceProvider; } - /// - /// Creates a service provider with multiple OpenFeature providers configured. - /// - /// Action to configure multiple providers on the OpenFeature builder. - /// A configured service provider with scope validation enabled. - private static async Task CreateServiceProviderWithMultipleProvidersAsync(Action configureBuilder) - { - var services = new ServiceCollection(); - services.AddLogging(); - services.AddOpenFeature(configureBuilder); - var serviceProvider = services.BuildServiceProvider(validateScopes: true); - - var lifecycleManager = serviceProvider.GetRequiredService(); - await lifecycleManager.EnsureInitializedAsync(); - - return serviceProvider; - } - #endregion #region Configuration Overload Integration Tests @@ -113,12 +88,12 @@ private static async Task CreateServiceProviderWithMultiplePro public async Task ConfigurationOverload_DefaultProvider_ShouldResolveFeatureClientSuccessfully() { // Arrange - var serviceProvider = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg.Offline(true)); + var serviceProvider = await ConfigureLaunchDarklyAsync(cfg => cfg.Offline(true)); var scopedProvider = CreateScopedServiceProvider(serviceProvider); - var api = scopedProvider.GetRequiredService(); + var client = scopedProvider.GetRequiredService(); // Act - var result = await api.GetBooleanValueAsync(TestFlagKey, false); + var result = await client.GetBooleanValueAsync(TestFlagKey, false); // Assert Assert.False(result); // Default value should be returned in offline mode @@ -128,12 +103,12 @@ public async Task ConfigurationOverload_DefaultProvider_ShouldResolveFeatureClie public async Task ConfigurationOverload_DomainProvider_ShouldResolveKeyedFeatureClientSuccessfully() { // Arrange - var serviceProvider = await CreateServiceProviderWithDomainLaunchDarklyAsync(TestDomain, cfg => cfg.Offline(true)); + var serviceProvider = await ConfigureLaunchDarklyAsync(TestDomain, cfg => cfg.Offline(true)); var scopedProvider = CreateScopedServiceProvider(serviceProvider); - var api = scopedProvider.GetRequiredKeyedService(TestDomain); + var client = scopedProvider.GetRequiredKeyedService(TestDomain); // Act - var result = await api.GetBooleanValueAsync(TestFlagKey, true); + var result = await client.GetBooleanValueAsync(TestFlagKey, true); // Assert Assert.True(result); // Default value should be returned in offline mode @@ -144,7 +119,7 @@ public async Task ConfigurationOverload_DefaultProvider_ShouldPreserveCustomConf { // Arrange var startWaitTime = TimeSpan.FromSeconds(5); - var serviceProvider = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg + var serviceProvider = await ConfigureLaunchDarklyAsync(cfg => cfg .Offline(true) .StartWaitTime(startWaitTime)); @@ -161,7 +136,7 @@ public async Task ConfigurationOverload_DomainProvider_ShouldPreserveCustomConfi { // Arrange var startWaitTime = TimeSpan.FromSeconds(10); - var serviceProvider = await CreateServiceProviderWithDomainLaunchDarklyAsync(TestDomain, cfg => cfg + var serviceProvider = await ConfigureLaunchDarklyAsync(TestDomain, cfg => cfg .Offline(true) .StartWaitTime(startWaitTime)); @@ -181,12 +156,12 @@ public async Task ConfigurationOverload_DomainProvider_ShouldPreserveCustomConfi public async Task SdkKeyOverload_DefaultProvider_ShouldResolveFeatureClientSuccessfully() { // Arrange - var serviceProvider = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg.Offline(true)); + var serviceProvider = await ConfigureLaunchDarklyAsync(cfg => cfg.Offline(true)); var scopedProvider = CreateScopedServiceProvider(serviceProvider); - var api = scopedProvider.GetRequiredService(); + var client = scopedProvider.GetRequiredService(); // Act - var result = await api.GetBooleanValueAsync(TestFlagKey, false); + var result = await client.GetBooleanValueAsync(TestFlagKey, false); // Assert Assert.False(result); // Default value should be returned in offline mode @@ -196,12 +171,12 @@ public async Task SdkKeyOverload_DefaultProvider_ShouldResolveFeatureClientSucce public async Task SdkKeyOverload_DomainProvider_ShouldResolveKeyedFeatureClientSuccessfully() { // Arrange - var serviceProvider = await CreateServiceProviderWithDomainLaunchDarklyAsync(TestDomain, cfg => cfg.Offline(true)); + var serviceProvider = await ConfigureLaunchDarklyAsync(TestDomain, cfg => cfg.Offline(true)); var scopedProvider = CreateScopedServiceProvider(serviceProvider); - var api = scopedProvider.GetRequiredKeyedService(TestDomain); + var client = scopedProvider.GetRequiredKeyedService(TestDomain); // Act - var result = await api.GetBooleanValueAsync(TestFlagKey, true); + var result = await client.GetBooleanValueAsync(TestFlagKey, true); // Assert Assert.True(result); // Default value should be returned in offline mode @@ -212,7 +187,7 @@ public async Task SdkKeyOverload_DefaultProvider_ShouldApplyCustomConfigurationF { // Arrange var startWaitTime = TimeSpan.FromSeconds(3); - var serviceProvider = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg + var serviceProvider = await ConfigureLaunchDarklyAsync(cfg => cfg .Offline(true) .StartWaitTime(startWaitTime)); @@ -229,7 +204,7 @@ public async Task SdkKeyOverload_DomainProvider_ShouldApplyCustomConfigurationFr { // Arrange var startWaitTime = TimeSpan.FromSeconds(7); - var serviceProvider = await CreateServiceProviderWithDomainLaunchDarklyAsync(TestDomain, cfg => cfg + var serviceProvider = await ConfigureLaunchDarklyAsync(TestDomain, cfg => cfg .Offline(true) .StartWaitTime(startWaitTime)); @@ -249,7 +224,7 @@ public async Task SdkKeyOverload_DomainProvider_ShouldApplyCustomConfigurationFr public async Task MultiProvider_MixedOverloads_ShouldRegisterBothDefaultAndDomainProvidersCorrectly() { // Arrange - var serviceProvider = await CreateServiceProviderWithMultipleProvidersAsync(builder => + var serviceProvider = await ConfigureOpenFeatureAsync(builder => { // Register both default and domain-scoped providers using different overloads builder @@ -260,20 +235,25 @@ public async Task MultiProvider_MixedOverloads_ShouldRegisterBothDefaultAndDomai policy.DefaultNameSelector = _ => "domain1"; }); }); - + var scopedProvider = CreateScopedServiceProvider(serviceProvider); // Act var defaultClient = scopedProvider.GetRequiredService(); - var domainClient = scopedProvider.GetRequiredKeyedService("domain2"); - var defaultConfigRegistered = serviceProvider.GetRequiredService(); - var domainConfigRegistered = serviceProvider.GetRequiredKeyedService("domain2"); + var domain1Client = scopedProvider.GetRequiredKeyedService("domain1"); + var domain2Client = scopedProvider.GetRequiredKeyedService("domain2"); + + var defaultConfig = serviceProvider.GetRequiredService(); + var domain1Config = serviceProvider.GetRequiredKeyedService("domain1"); + var domain2Config = serviceProvider.GetRequiredKeyedService("domain2"); // Assert - Assert.NotSame(defaultClient, domainClient); - Assert.NotSame(defaultConfigRegistered, domainConfigRegistered); - Assert.True(defaultConfigRegistered.Offline); - Assert.True(domainConfigRegistered.Offline); + Assert.Same(defaultClient, domain1Client); + Assert.NotSame(domain1Client, domain2Client); + Assert.NotSame(domain1Config, domain2Config); + + Assert.True(domain1Config.Offline, "Expected 'domain1' LaunchDarkly config to be in offline mode."); + Assert.True(domain2Config.Offline, "Expected 'domain2' LaunchDarkly config to be in offline mode."); } [Fact] @@ -285,13 +265,13 @@ public async Task MultiProvider_MultipleDomains_ShouldIsolateConfigurationsCorre var fastStartWait = TimeSpan.FromMilliseconds(100); var slowStartWait = TimeSpan.FromSeconds(5); - var serviceProvider = await CreateServiceProviderWithMultipleProvidersAsync(builder => + var serviceProvider = await ConfigureOpenFeatureAsync(builder => { // Register using different configurations for variety builder.UseLaunchDarkly(fastDomain, TestSdkKey, cfg => cfg .Offline(true) .StartWaitTime(fastStartWait)); - + builder.UseLaunchDarkly(slowDomain, TestSdkKey, cfg => cfg .Offline(true) .StartWaitTime(slowStartWait)); @@ -302,7 +282,7 @@ public async Task MultiProvider_MultipleDomains_ShouldIsolateConfigurationsCorre // Act & Assert var config1 = serviceProvider.GetRequiredKeyedService(fastDomain); var config2 = serviceProvider.GetRequiredKeyedService(slowDomain); - + Assert.NotSame(config1, config2); Assert.Equal(fastStartWait, config1.StartWaitTime); Assert.Equal(slowStartWait, config2.StartWaitTime); @@ -318,7 +298,7 @@ public async Task MultiProvider_MultipleDomains_ShouldIsolateConfigurationsCorre public async Task AllOverloads_FeatureProviders_ShouldSupportAllOpenFeatureValueTypes() { // Arrange - var serviceProvider = await CreateServiceProviderWithMultipleProvidersAsync(builder => + var serviceProvider = await ConfigureOpenFeatureAsync(builder => { // Test all SDK key overloads builder.UseLaunchDarkly("domain1", TestSdkKey, cfg => cfg.Offline(true)); // Default provider @@ -328,28 +308,28 @@ public async Task AllOverloads_FeatureProviders_ShouldSupportAllOpenFeatureValue }); var scopedProvider = CreateScopedServiceProvider(serviceProvider); - - var defaultApi = scopedProvider.GetRequiredService(); - var domain1Api = scopedProvider.GetRequiredKeyedService("domain1"); - var domain2Api = scopedProvider.GetRequiredKeyedService("domain2"); - var domain3Api = scopedProvider.GetRequiredKeyedService("domain3"); + var defaultClient = scopedProvider.GetRequiredService(); + var domain1Client = scopedProvider.GetRequiredKeyedService("domain1"); + var domain2Client = scopedProvider.GetRequiredKeyedService("domain2"); + var domain3Client = scopedProvider.GetRequiredKeyedService("domain3"); + // Act & Assert - Test all supported types on all providers - foreach (var api in new[] { defaultApi, domain1Api, domain2Api, domain3Api }) + foreach (var client in new[] { defaultClient, domain1Client, domain2Client, domain3Client }) { - var boolResult = await api.GetBooleanValueAsync("bool-flag", true); + var boolResult = await client.GetBooleanValueAsync("bool-flag", true); Assert.True(boolResult); - var stringResult = await api.GetStringValueAsync("string-flag", "default"); + var stringResult = await client.GetStringValueAsync("string-flag", "default"); Assert.Equal("default", stringResult); - var intResult = await api.GetIntegerValueAsync("int-flag", 42); + var intResult = await client.GetIntegerValueAsync("int-flag", 42); Assert.Equal(42, intResult); - var doubleResult = await api.GetDoubleValueAsync("double-flag", 3.14); + var doubleResult = await client.GetDoubleValueAsync("double-flag", 3.14); Assert.Equal(3.14, doubleResult); - var structureResult = await api.GetObjectValueAsync("object-flag", new Value("default")); + var structureResult = await client.GetObjectValueAsync("object-flag", new Value("default")); Assert.Equal("default", structureResult.AsString); } } @@ -362,12 +342,12 @@ public async Task AllOverloads_FeatureProviders_ShouldSupportAllOpenFeatureValue public async Task DefaultProvider_InOfflineMode_ShouldReturnCorrectReasonAndDefaultValue() { // Arrange - var serviceProvider = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg.Offline(true)); + var serviceProvider = await ConfigureLaunchDarklyAsync(cfg => cfg.Offline(true)); var scopedProvider = CreateScopedServiceProvider(serviceProvider); - var api = scopedProvider.GetRequiredService(); + var client = scopedProvider.GetRequiredService(); // Act - var result = await api.GetBooleanDetailsAsync(TestFlagKey, false); + var result = await client.GetBooleanDetailsAsync(TestFlagKey, false); // Assert Assert.False(result.Value); @@ -377,9 +357,9 @@ public async Task DefaultProvider_InOfflineMode_ShouldReturnCorrectReasonAndDefa public async Task DefaultProvider_WithEvaluationContext_ShouldHandleContextCorrectly() { // Arrange - var serviceProvider = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg.Offline(true)); + var serviceProvider = await ConfigureLaunchDarklyAsync(cfg => cfg.Offline(true)); var scopedProvider = CreateScopedServiceProvider(serviceProvider); - var api = scopedProvider.GetRequiredService(); + var client = scopedProvider.GetRequiredService(); var context = EvaluationContext.Builder() .Set("userId", "test-user") @@ -387,7 +367,7 @@ public async Task DefaultProvider_WithEvaluationContext_ShouldHandleContextCorre .Build(); // Act - var result = await api.GetBooleanDetailsAsync(TestFlagKey, false, context); + var result = await client.GetBooleanDetailsAsync(TestFlagKey, false, context); // Assert Assert.False(result.Value); @@ -402,7 +382,7 @@ public async Task DefaultProvider_WithEvaluationContext_ShouldHandleContextCorre public async Task DefaultProvider_Configuration_ShouldBeRegisteredAsSingleton() { // Arrange - var serviceProvider = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg.Offline(true)); + var serviceProvider = await ConfigureLaunchDarklyAsync(cfg => cfg.Offline(true)); // Act var config1 = serviceProvider.GetRequiredService(); @@ -416,7 +396,7 @@ public async Task DefaultProvider_Configuration_ShouldBeRegisteredAsSingleton() public async Task DomainProvider_Configuration_ShouldBeRegisteredAsKeyedSingleton() { // Arrange - var serviceProvider = await CreateServiceProviderWithDomainLaunchDarklyAsync(TestDomain, cfg => cfg.Offline(true)); + var serviceProvider = await ConfigureLaunchDarklyAsync(TestDomain, cfg => cfg.Offline(true)); // Act var config1 = serviceProvider.GetRequiredKeyedService(TestDomain); @@ -430,7 +410,7 @@ public async Task DomainProvider_Configuration_ShouldBeRegisteredAsKeyedSingleto public async Task TryAddSingleton_MultipleRegistrations_ShouldNotReplaceExistingRegistration() { // Arrange & Act - var serviceProvider = await CreateServiceProviderWithMultipleProvidersAsync(builder => + var serviceProvider = await ConfigureOpenFeatureAsync(builder => { // First registration should win due to TryAddSingleton behavior builder.UseLaunchDarkly(TestSdkKey, cfg => cfg.Offline(true)); @@ -446,7 +426,7 @@ public async Task TryAddSingleton_MultipleRegistrations_ShouldNotReplaceExisting public async Task TryAddKeyedSingleton_MultipleRegistrations_ShouldNotReplaceExistingRegistration() { // Arrange - var serviceProvider = await CreateServiceProviderWithMultipleProvidersAsync(builder => + var serviceProvider = await ConfigureOpenFeatureAsync(builder => { // First registration should win due to TryAddKeyedSingleton behavior builder.UseLaunchDarkly(TestDomain, TestSdkKey, cfg => cfg.Offline(true)); @@ -469,7 +449,7 @@ public async Task TryAddKeyedSingleton_MultipleRegistrations_ShouldNotReplaceExi public async Task ServiceProviderDisposal_AfterUsingProviders_ShouldNotCauseMemoryLeaks() { // Arrange - var serviceProvider = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg.Offline(true)); + var serviceProvider = await ConfigureLaunchDarklyAsync(cfg => cfg.Offline(true)); // Act var config = serviceProvider.GetRequiredService(); @@ -483,8 +463,8 @@ public async Task ServiceProviderDisposal_AfterUsingProviders_ShouldNotCauseMemo public async Task MultipleServiceProviders_WithSameConfiguration_ShouldIsolateConfigurations() { // Arrange - var serviceProvider1 = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg.Offline(true)); - var serviceProvider2 = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg.Offline(false)); + var serviceProvider1 = await ConfigureLaunchDarklyAsync(cfg => cfg.Offline(true)); + var serviceProvider2 = await ConfigureLaunchDarklyAsync(cfg => cfg.Offline(false)); // Act var config1 = serviceProvider1.GetRequiredService(); @@ -504,7 +484,7 @@ public async Task MultipleServiceProviders_WithSameConfiguration_ShouldIsolateCo public async Task EarlyValidation_WithValidConfiguration_ShouldPassValidationAndRegisterCorrectly() { // Arrange - var serviceProvider = await CreateServiceProviderWithDefaultLaunchDarklyAsync(cfg => cfg.Offline(true)); + var serviceProvider = await ConfigureLaunchDarklyAsync(cfg => cfg.Offline(true)); // Act var registeredConfig = serviceProvider.GetRequiredService(); @@ -518,7 +498,7 @@ public async Task EarlyValidation_WithValidConfiguration_ShouldPassValidationAnd public async Task EarlyValidation_WithValidDomainConfiguration_ShouldPassValidationAndRegisterCorrectly() { // Arrange - var serviceProvider = await CreateServiceProviderWithDomainLaunchDarklyAsync(TestDomain, cfg => cfg.Offline(true)); + var serviceProvider = await ConfigureLaunchDarklyAsync(TestDomain, cfg => cfg.Offline(true)); // Act var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); @@ -534,7 +514,7 @@ public void EarlyValidation_WithPrebuiltConfiguration_ShouldPassValidationAndReg // Arrange var services = new ServiceCollection(); var config = Configuration.Builder(TestSdkKey).Offline(true).Build(); - + services.AddOpenFeature(builder => { builder.UseLaunchDarkly(config); @@ -550,4 +530,4 @@ public void EarlyValidation_WithPrebuiltConfiguration_ShouldPassValidationAndReg #endregion } -} \ No newline at end of file +} \ No newline at end of file From 54a575363852f6b890d47d5c2852aad5e62e0574 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sat, 26 Jul 2025 21:54:04 +0400 Subject: [PATCH 16/22] =?UTF-8?q?=E2=9C=85=20test(LaunchDarklyOpenFeatureB?= =?UTF-8?q?uilderExtensions):=20enable=20scope=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update test cases to include `validateScopes: true` in `BuildServiceProvider()` calls. This change ensures early validation of service configurations, allowing configuration issues to be caught immediately during registration. Enhances the robustness of the tests across multiple methods. --- ...DarklyOpenFeatureBuilderExtensionsTests.cs | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs index 7f64e6f..76aceba 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs @@ -40,7 +40,7 @@ public void UseLaunchDarklyWithConfiguration_WhenCalled_ShouldRegisterConfigurat builder.UseLaunchDarkly(config); // Assert - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); var registeredConfig = serviceProvider.GetRequiredService(); Assert.NotNull(registeredConfig); Assert.True(registeredConfig.Offline); @@ -58,7 +58,7 @@ public void UseLaunchDarklyWithConfiguration_WhenCalledMultipleTimes_ShouldShare builder.UseLaunchDarkly(config); // Assert - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); var config1 = serviceProvider.GetRequiredService(); var config2 = serviceProvider.GetRequiredService(); Assert.Same(config1, config2); @@ -78,7 +78,7 @@ public void UseLaunchDarklyWithConfiguration_WhenCalledMultipleTimesWithDifferen builder.UseLaunchDarkly(config2); // Should not replace the first // Assert - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); var registeredConfig = serviceProvider.GetRequiredService(); Assert.True(registeredConfig.Offline); // Should still be the first configuration } @@ -114,7 +114,7 @@ public void UseLaunchDarklyWithDomainAndConfiguration_WhenCalled_ShouldRegisterC builder.UseLaunchDarkly(TestDomain, config); // Assert - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); Assert.NotNull(registeredConfig); Assert.True(registeredConfig.Offline); @@ -132,7 +132,7 @@ public void UseLaunchDarklyWithDomainAndConfiguration_WhenCalledMultipleTimes_Sh builder.UseLaunchDarkly(TestDomain, config); // Assert - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); var config1 = serviceProvider.GetRequiredKeyedService(TestDomain); var config2 = serviceProvider.GetRequiredKeyedService(TestDomain); Assert.Same(config1, config2); @@ -172,7 +172,7 @@ public void UseLaunchDarklyWithDomainAndConfiguration_WhenDifferentDomainsUsed_S builder.UseLaunchDarkly(domain2, config2); // Assert - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); var registeredConfig1 = serviceProvider.GetRequiredKeyedService(domain1); var registeredConfig2 = serviceProvider.GetRequiredKeyedService(domain2); Assert.NotSame(registeredConfig1, registeredConfig2); @@ -209,7 +209,7 @@ public void UseLaunchDarklyWithSdkKey_WhenCalled_ShouldRegisterValidConfiguratio builder.UseLaunchDarkly(TestSdkKey); // Assert - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); var config = serviceProvider.GetRequiredService(); Assert.NotNull(config); } @@ -234,7 +234,7 @@ public void UseLaunchDarklyWithSdkKeyAndDelegate_WhenCalled_ShouldApplyCustomCon // Assert Assert.Same(builder, result); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); var config = serviceProvider.GetRequiredService(); Assert.True(configureWasCalled); @@ -286,7 +286,7 @@ public void UseLaunchDarklyWithDomainAndSdkKey_WhenCalled_ShouldRegisterValidKey builder.UseLaunchDarkly(TestDomain, TestSdkKey); // Assert - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); var config = serviceProvider.GetRequiredKeyedService(TestDomain); Assert.NotNull(config); } @@ -298,24 +298,21 @@ public void UseLaunchDarklyWithDomainAndSdkKeyAndDelegate_WhenCalled_ShouldApply var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); var configureWasCalled = false; - ConfigurationBuilder capturedBuilder = null; // Act var result = builder.UseLaunchDarkly(TestDomain, TestSdkKey, configBuilder => { configureWasCalled = true; - capturedBuilder = configBuilder; configBuilder.Offline(true); }); // Assert Assert.Same(builder, result); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); var config = serviceProvider.GetRequiredKeyedService(TestDomain); Assert.True(configureWasCalled); - Assert.NotNull(capturedBuilder); Assert.True(config.Offline); } @@ -327,10 +324,13 @@ public void UseLaunchDarklyWithDomainAndSdkKeyAndDelegate_WhenConfigurationDeleg var builder = new OpenFeatureBuilder(services); var expectedException = new InvalidOperationException("Test exception"); - // Act & Assert - // The exception should be thrown immediately during registration due to early validation - var actualException = Assert.Throws(() => + // Act + var registerWithThrowing = () => Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, TestSdkKey, _ => throw expectedException)); + + // Assert + // The exception should be thrown immediately during registration due to early validation + var actualException = registerWithThrowing(); Assert.Same(expectedException, actualException); } @@ -345,7 +345,7 @@ public void UseLaunchDarklyWithDomainAndSdkKeyAndDelegate_WhenNullConfigurationD var result = builder.UseLaunchDarkly(TestDomain, TestSdkKey, null); Assert.Same(builder, result); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); var config = serviceProvider.GetRequiredKeyedService(TestDomain); Assert.NotNull(config); } @@ -443,7 +443,7 @@ public void UseLaunchDarklyWithConfiguration_WhenCustomPropertiesSet_ShouldPrese builder.UseLaunchDarkly(originalConfig); // Assert - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); var registeredConfig = serviceProvider.GetRequiredService(); // The registered config should be a rebuilt version, not the same instance @@ -467,7 +467,7 @@ public void UseLaunchDarklyWithSdkKeyAndDelegate_WhenCustomPropertiesSet_ShouldP }); // Assert - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); var registeredConfig = serviceProvider.GetRequiredService(); Assert.True(registeredConfig.Offline); Assert.Equal(startWaitTime, registeredConfig.StartWaitTime); @@ -489,7 +489,7 @@ public void UseLaunchDarklyWithDomainAndSdkKeyAndDelegate_WhenCustomPropertiesSe }); // Assert - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain); Assert.True(registeredConfig.Offline); Assert.Equal(startWaitTime, registeredConfig.StartWaitTime); @@ -510,7 +510,7 @@ public void UseLaunchDarklyWithSdkKeyAndDelegate_WhenEarlyValidationPasses_Shoul builder.UseLaunchDarkly(TestSdkKey, cfg => cfg.Offline(true)); // Assert - If we reach here, early validation passed - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); var config = serviceProvider.GetRequiredService(); Assert.NotNull(config); Assert.True(config.Offline); @@ -527,7 +527,7 @@ public void UseLaunchDarklyWithDomainAndSdkKeyAndDelegate_WhenEarlyValidationPas builder.UseLaunchDarkly(TestDomain, TestSdkKey, cfg => cfg.Offline(true)); // Assert - If we reach here, early validation passed - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(validateScopes: true); var config = serviceProvider.GetRequiredKeyedService(TestDomain); Assert.NotNull(config); Assert.True(config.Offline); From 85a36b1089d73222e8ab39783e14352e642c3588 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sat, 26 Jul 2025 22:12:13 +0400 Subject: [PATCH 17/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(tests):=20c?= =?UTF-8?q?hange=20registerWithThrowing=20to=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor the `registerWithThrowing` function from a lambda expression to a method declaration for improved clarity. The change maintains the same functionality of asserting that an `InvalidOperationException` is thrown during the registration process with the `OpenFeatureBuilder` instance. --- .../LaunchDarklyOpenFeatureBuilderExtensionsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs index 76aceba..4f9fd65 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs @@ -325,7 +325,7 @@ public void UseLaunchDarklyWithDomainAndSdkKeyAndDelegate_WhenConfigurationDeleg var expectedException = new InvalidOperationException("Test exception"); // Act - var registerWithThrowing = () => Assert.Throws(() => + InvalidOperationException registerWithThrowing() => Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, TestSdkKey, _ => throw expectedException)); // Assert From a010d52b04e1749c620a7b5b3b1c4fcd55a2e3f9 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Wed, 30 Jul 2025 19:08:32 +0400 Subject: [PATCH 18/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(launchdarkl?= =?UTF-8?q?y):=20improve=20null=20handling=20in=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor the `UseLaunchDarkly` methods to include null checks for the `configuration` parameter, throwing an `ArgumentNullException` when it is null. Remove the unnecessary `CreateConfiguration` method. Update related tests to reflect the change in exception type from `NullReferenceException` to `ArgumentNullException`. --- ...aunchDarklyOpenFeatureBuilderExtensions.cs | 44 ++++++++++++------- ...DarklyOpenFeatureBuilderExtensionsTests.cs | 4 +- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs index a37e8d4..0370461 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs +++ b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs @@ -20,8 +20,22 @@ public static partial class LaunchDarklyOpenFeatureBuilderExtensions /// The instance to configure. /// A pre-built LaunchDarkly . /// The updated instance. + /// + /// Thrown when the argument is null. + /// public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, Configuration configuration) - => RegisterLaunchDarklyProvider(builder, () => CreateConfiguration(configuration), sp => sp.GetRequiredService()); + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration), "Configuration cannot be null."); + } + + return RegisterLaunchDarklyProvider( + builder, + () => configuration, + sp => sp.GetRequiredService() + ); + } /// /// Configures the to use LaunchDarkly as a domain-scoped provider @@ -31,12 +45,23 @@ public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder /// A domain identifier (e.g., tenant or environment). /// A pre-built LaunchDarkly specific to the domain. /// The updated instance. + /// + /// Thrown when the argument is null. + /// public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, string domain, Configuration configuration) - => RegisterLaunchDarklyProviderForDomain( + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration), "Configuration cannot be null."); + } + + return RegisterLaunchDarklyProviderForDomain( builder, domain, - () => CreateConfiguration(configuration), - (sp, key) => sp.GetRequiredKeyedService(key)); + () => configuration, + (sp, key) => sp.GetRequiredKeyedService(key) + ); + } /// /// Configures the to use LaunchDarkly as the default provider @@ -121,17 +146,6 @@ private static OpenFeatureBuilder RegisterLaunchDarklyProviderForDomain( return builder.AddProvider(domain, (serviceProvider, key) => new Provider(resolveConfiguration(serviceProvider, key))); } - /// - /// Creates a new by cloning the specified instance and rebuilding it. - /// - /// An existing instance. - /// A rebuilt instance. - private static Configuration CreateConfiguration(Configuration configuration) - { - var configBuilder = Configuration.Builder(configuration); - return configBuilder.Build(); - } - /// /// Creates a new using the specified SDK key and optional configuration delegate. /// diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs index 4f9fd65..fa9249a 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs @@ -351,7 +351,7 @@ public void UseLaunchDarklyWithDomainAndSdkKeyAndDelegate_WhenNullConfigurationD } [Fact] - public void UseLaunchDarklyWithSdkKeyAndNullDelegate_WhenNullConfigurationPassed_ShouldThrowNullReferenceException() + public void UseLaunchDarklyWithSdkKeyAndNullDelegate_WhenNullConfigurationPassed_ShouldThrowArgumentNullException() { // Arrange var services = new ServiceCollection(); @@ -361,7 +361,7 @@ public void UseLaunchDarklyWithSdkKeyAndNullDelegate_WhenNullConfigurationPassed Configuration configuration = null; // Assert - Assert.Throws(() => builder.UseLaunchDarkly(TestSdkKey, configuration)); + Assert.Throws(() => builder.UseLaunchDarkly(TestSdkKey, configuration)); } [Theory] From 2ba3481a066214630b7f8c6561408c05e7e22e62 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Thu, 28 Aug 2025 16:30:28 +0400 Subject: [PATCH 19/22] Update src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs Co-authored-by: Waldek <80332029+wwalendz-relativity@users.noreply.github.com> --- .../LaunchDarklyOpenFeatureBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs index 0370461..c373a17 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs +++ b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs @@ -152,7 +152,7 @@ private static OpenFeatureBuilder RegisterLaunchDarklyProviderForDomain( /// The SDK key used to initialize the configuration. /// An optional delegate to customize the . /// A fully constructed instance. - private static Configuration CreateConfiguration(string stdKey, Action configure = null) + private static Configuration CreateConfiguration(string sdkKey, Action configure = null) { var configBuilder = Configuration.Builder(stdKey); configure?.Invoke(configBuilder); From 3721562cc1b368d9a9d1490a7359d9ef1be1062d Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Thu, 28 Aug 2025 16:34:30 +0400 Subject: [PATCH 20/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(dependency-?= =?UTF-8?q?injection):=20rename=20stdKey=20to=20sdkKey?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renamed the parameter `stdKey` to `sdkKey` in multiple method signatures and XML documentation comments to clarify its purpose and align with LaunchDarkly SDK terminology. Affected methods include `UseLaunchDarkly` for both default and domain-scoped providers, as well as the `CreateConfiguration` method. The internal logic remains unchanged, preserving functionality while improving code clarity. --- .../LaunchDarklyOpenFeatureBuilderExtensions.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs index c373a17..07a1fdf 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs +++ b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs @@ -68,11 +68,11 @@ public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder /// using the specified SDK key and optional configuration delegate. /// /// The instance to configure. - /// The SDK key used to initialize the LaunchDarkly configuration. + /// The SDK key used to initialize the LaunchDarkly configuration. /// An optional delegate to customize the . /// The updated instance. - public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, string stdKey, Action configure = null) - => RegisterLaunchDarklyProvider(builder, () => CreateConfiguration(stdKey, configure), sp => sp.GetRequiredService()); + public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, string sdkKey, Action configure = null) + => RegisterLaunchDarklyProvider(builder, () => CreateConfiguration(sdkKey, configure), sp => sp.GetRequiredService()); /// /// Configures the to use LaunchDarkly as a domain-scoped provider @@ -80,14 +80,14 @@ public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder /// /// The instance to configure. /// A domain identifier (e.g., tenant or environment). - /// The SDK key used to initialize the LaunchDarkly configuration. + /// The SDK key used to initialize the LaunchDarkly configuration. /// An optional delegate to customize the . /// The updated instance. - public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, string domain, string stdKey, Action configure = null) + public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, string domain, string sdkKey, Action configure = null) => RegisterLaunchDarklyProviderForDomain( builder, domain, - () => CreateConfiguration(stdKey, configure), + () => CreateConfiguration(sdkKey, configure), (sp, key) => sp.GetRequiredKeyedService(key)); /// @@ -149,12 +149,12 @@ private static OpenFeatureBuilder RegisterLaunchDarklyProviderForDomain( /// /// Creates a new using the specified SDK key and optional configuration delegate. /// - /// The SDK key used to initialize the configuration. + /// The SDK key used to initialize the configuration. /// An optional delegate to customize the . /// A fully constructed instance. private static Configuration CreateConfiguration(string sdkKey, Action configure = null) { - var configBuilder = Configuration.Builder(stdKey); + var configBuilder = Configuration.Builder(sdkKey); configure?.Invoke(configBuilder); return configBuilder.Build(); } From b6ee8edc7f1cf2a250f8aa5172e354302cb25438 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Thu, 28 Aug 2025 16:59:42 +0400 Subject: [PATCH 21/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(dependency-?= =?UTF-8?q?injection):=20rename=20provider=20registration=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renamed `RegisterLaunchDarklyProviderForDomain` to `RegisterLaunchDarklyProvider`. Updated its usage in `UseLaunchDarkly` and null configuration checks. The method parameters now include a `Func` for creating configurations and a `Func` for resolving them, enhancing configuration management flexibility. --- .../LaunchDarklyOpenFeatureBuilderExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs index 07a1fdf..d0334ca 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs +++ b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs @@ -55,7 +55,7 @@ public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder throw new ArgumentNullException(nameof(configuration), "Configuration cannot be null."); } - return RegisterLaunchDarklyProviderForDomain( + return RegisterLaunchDarklyProvider( builder, domain, () => configuration, @@ -84,7 +84,7 @@ public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder /// An optional delegate to customize the . /// The updated instance. public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, string domain, string sdkKey, Action configure = null) - => RegisterLaunchDarklyProviderForDomain( + => RegisterLaunchDarklyProvider( builder, domain, () => CreateConfiguration(sdkKey, configure), @@ -118,7 +118,7 @@ private static OpenFeatureBuilder RegisterLaunchDarklyProvider( /// A delegate that returns a domain-specific instance. /// A delegate that resolves the domain-scoped from the service provider. /// The updated instance. - private static OpenFeatureBuilder RegisterLaunchDarklyProviderForDomain( + private static OpenFeatureBuilder RegisterLaunchDarklyProvider( OpenFeatureBuilder builder, string domain, Func createConfiguration, From 2e8fb9948eacefc6e1eb43f7a68b467caced2641 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Thu, 28 Aug 2025 17:01:00 +0400 Subject: [PATCH 22/22] =?UTF-8?q?=F0=9F=92=84=20style(dependency-injection?= =?UTF-8?q?):=20improve=20code=20readability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor the `UseLaunchDarkly` method to format the parameters of the `RegisterLaunchDarklyProvider` call on separate lines. This change enhances readability while maintaining the existing functionality. --- .../LaunchDarklyOpenFeatureBuilderExtensions.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs index d0334ca..2827fbf 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs +++ b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs @@ -72,7 +72,10 @@ public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder /// An optional delegate to customize the . /// The updated instance. public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, string sdkKey, Action configure = null) - => RegisterLaunchDarklyProvider(builder, () => CreateConfiguration(sdkKey, configure), sp => sp.GetRequiredService()); + => RegisterLaunchDarklyProvider( + builder, + () => CreateConfiguration(sdkKey, configure), + sp => sp.GetRequiredService()); /// /// Configures the to use LaunchDarkly as a domain-scoped provider