Skip to content

Commit 211e984

Browse files
committed
Migrate Dependency Injection tests over to the Hosting code
* Copy the existing Dependency Injection tests over to a new Hosting.Tests project * Fix issue with the Integration tests Signed-off-by: Kyle Julian <[email protected]>
1 parent 4d5592f commit 211e984

12 files changed

+681
-4
lines changed

OpenFeature.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
<Project Path="test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj" />
6363
<Project Path="test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj" />
6464
<Project Path="test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj" />
65+
<Project Path="test/OpenFeature.Hosting.Tests/OpenFeature.Hosting.Tests.csproj" />
6566
<Project Path="test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj" />
6667
<Project Path="test/OpenFeature.Tests/OpenFeature.Tests.csproj" />
6768
</Folder>

src/OpenFeature.Hosting/DependencyInjection/OpenFeatureServiceCollectionExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public static IServiceCollection AddOpenFeature(this IServiceCollection services
3131
var builder = new OpenFeatureBuilder(services);
3232
configure(builder);
3333

34+
builder.Services.AddHostedService<HostedFeatureLifecycleService>();
35+
3436
// If a default provider is specified without additional providers,
3537
// return early as no extra configuration is needed.
3638
if (builder.HasDefaultProvider && builder.DomainBoundProviderRegistrationCount == 0)
@@ -56,8 +58,6 @@ public static IServiceCollection AddOpenFeature(this IServiceCollection services
5658

5759
builder.AddPolicyBasedClient();
5860

59-
builder.Services.AddHostedService<HostedFeatureLifecycleService>();
60-
6161
return services;
6262
}
6363
}

src/OpenFeature.Hosting/OpenFeature.Hosting.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,9 @@
1313
<ProjectReference Include="..\OpenFeature\OpenFeature.csproj" />
1414
</ItemGroup>
1515

16+
<ItemGroup>
17+
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
18+
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
19+
</ItemGroup>
20+
1621
</Project>
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.DependencyInjection.Extensions;
3+
using Microsoft.Extensions.Logging.Abstractions;
4+
using OpenFeature.Constant;
5+
using OpenFeature.DependencyInjection;
6+
using OpenFeature.DependencyInjection.Internal;
7+
using OpenFeature.Model;
8+
9+
namespace OpenFeature.Hosting.Tests.DependencyInjection;
10+
11+
public class FeatureLifecycleManagerTests
12+
{
13+
private readonly IServiceCollection _serviceCollection;
14+
15+
public FeatureLifecycleManagerTests()
16+
{
17+
Api.Instance.SetContext(null);
18+
Api.Instance.ClearHooks();
19+
20+
_serviceCollection = new ServiceCollection()
21+
.Configure<OpenFeatureOptions>(options =>
22+
{
23+
options.AddDefaultProviderName();
24+
});
25+
}
26+
27+
[Fact]
28+
public async Task EnsureInitializedAsync_ShouldLogAndSetProvider_WhenProviderExists()
29+
{
30+
// Arrange
31+
var featureProvider = new NoOpFeatureProvider();
32+
_serviceCollection.AddSingleton<FeatureProvider>(featureProvider);
33+
34+
var serviceProvider = _serviceCollection.BuildServiceProvider();
35+
var sut = new FeatureLifecycleManager(Api.Instance, serviceProvider, NullLogger<FeatureLifecycleManager>.Instance);
36+
37+
// Act
38+
await sut.EnsureInitializedAsync().ConfigureAwait(true);
39+
40+
// Assert
41+
Assert.Equal(featureProvider, Api.Instance.GetProvider());
42+
}
43+
44+
[Fact]
45+
public async Task EnsureInitializedAsync_ShouldThrowException_WhenProviderDoesNotExist()
46+
{
47+
// Arrange
48+
_serviceCollection.RemoveAll<FeatureProvider>();
49+
50+
var serviceProvider = _serviceCollection.BuildServiceProvider();
51+
var sut = new FeatureLifecycleManager(Api.Instance, serviceProvider, NullLogger<FeatureLifecycleManager>.Instance);
52+
53+
// Act
54+
var act = () => sut.EnsureInitializedAsync().AsTask();
55+
56+
// Assert
57+
var exception = await Assert.ThrowsAsync<InvalidOperationException>(act).ConfigureAwait(true);
58+
Assert.NotNull(exception);
59+
Assert.False(string.IsNullOrWhiteSpace(exception.Message));
60+
}
61+
62+
[Fact]
63+
public async Task EnsureInitializedAsync_ShouldSetHook_WhenHooksAreRegistered()
64+
{
65+
// Arrange
66+
var featureProvider = new NoOpFeatureProvider();
67+
var hook = new NoOpHook();
68+
69+
_serviceCollection.AddSingleton<FeatureProvider>(featureProvider)
70+
.AddKeyedSingleton<Hook>("NoOpHook", (_, key) => hook)
71+
.Configure<OpenFeatureOptions>(options =>
72+
{
73+
options.AddHookName("NoOpHook");
74+
});
75+
76+
var serviceProvider = _serviceCollection.BuildServiceProvider();
77+
var sut = new FeatureLifecycleManager(Api.Instance, serviceProvider, NullLogger<FeatureLifecycleManager>.Instance);
78+
79+
// Act
80+
await sut.EnsureInitializedAsync().ConfigureAwait(true);
81+
82+
// Assert
83+
var actual = Api.Instance.GetHooks().FirstOrDefault();
84+
Assert.Equal(hook, actual);
85+
}
86+
87+
[Fact]
88+
public async Task EnsureInitializedAsync_ShouldSetHandler_WhenHandlersAreRegistered()
89+
{
90+
// Arrange
91+
EventHandlerDelegate eventHandlerDelegate = (_) => { };
92+
var featureProvider = new NoOpFeatureProvider();
93+
var handler = new EventHandlerDelegateWrapper(ProviderEventTypes.ProviderReady, eventHandlerDelegate);
94+
95+
_serviceCollection.AddSingleton<FeatureProvider>(featureProvider)
96+
.AddSingleton(_ => handler);
97+
98+
var serviceProvider = _serviceCollection.BuildServiceProvider();
99+
var sut = new FeatureLifecycleManager(Api.Instance, serviceProvider, NullLogger<FeatureLifecycleManager>.Instance);
100+
101+
// Act
102+
await sut.EnsureInitializedAsync().ConfigureAwait(true);
103+
}
104+
105+
[Fact]
106+
public async Task EnsureInitializedAsync_ShouldSetHandler_WhenMultipleHandlersAreRegistered()
107+
{
108+
// Arrange
109+
EventHandlerDelegate eventHandlerDelegate1 = (_) => { };
110+
EventHandlerDelegate eventHandlerDelegate2 = (_) => { };
111+
var featureProvider = new NoOpFeatureProvider();
112+
var handler1 = new EventHandlerDelegateWrapper(ProviderEventTypes.ProviderReady, eventHandlerDelegate1);
113+
var handler2 = new EventHandlerDelegateWrapper(ProviderEventTypes.ProviderReady, eventHandlerDelegate2);
114+
115+
_serviceCollection.AddSingleton<FeatureProvider>(featureProvider)
116+
.AddSingleton(_ => handler1)
117+
.AddSingleton(_ => handler2);
118+
119+
var serviceProvider = _serviceCollection.BuildServiceProvider();
120+
var sut = new FeatureLifecycleManager(Api.Instance, serviceProvider, NullLogger<FeatureLifecycleManager>.Instance);
121+
122+
// Act
123+
await sut.EnsureInitializedAsync().ConfigureAwait(true);
124+
}
125+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using OpenFeature.Model;
2+
3+
namespace OpenFeature.Hosting.Tests.DependencyInjection;
4+
5+
// This class replicates the NoOpFeatureProvider implementation from src/OpenFeature/NoOpFeatureProvider.cs.
6+
// It is used here to facilitate unit testing without relying on the internal NoOpFeatureProvider class.
7+
// If the InternalsVisibleTo attribute is added to the OpenFeature project,
8+
// this class can be removed and the original NoOpFeatureProvider can be directly accessed for testing.
9+
internal sealed class NoOpFeatureProvider : FeatureProvider
10+
{
11+
private readonly Metadata _metadata = new Metadata(NoOpProvider.NoOpProviderName);
12+
13+
public override Metadata GetMetadata()
14+
{
15+
return this._metadata;
16+
}
17+
18+
public override Task<ResolutionDetails<bool>> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
19+
{
20+
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
21+
}
22+
23+
public override Task<ResolutionDetails<string>> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
24+
{
25+
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
26+
}
27+
28+
public override Task<ResolutionDetails<int>> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
29+
{
30+
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
31+
}
32+
33+
public override Task<ResolutionDetails<double>> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
34+
{
35+
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
36+
}
37+
38+
public override Task<ResolutionDetails<Value>> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
39+
{
40+
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
41+
}
42+
43+
private static ResolutionDetails<T> NoOpResponse<T>(string flagKey, T defaultValue)
44+
{
45+
return new ResolutionDetails<T>(
46+
flagKey,
47+
defaultValue,
48+
reason: NoOpProvider.ReasonNoOp,
49+
variant: NoOpProvider.Variant
50+
);
51+
}
52+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using OpenFeature.Model;
2+
3+
namespace OpenFeature.Hosting.Tests.DependencyInjection;
4+
5+
internal class NoOpHook : Hook
6+
{
7+
public override ValueTask<EvaluationContext> BeforeAsync<T>(HookContext<T> context, IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
8+
{
9+
return base.BeforeAsync(context, hints, cancellationToken);
10+
}
11+
12+
public override ValueTask AfterAsync<T>(HookContext<T> context, FlagEvaluationDetails<T> details, IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
13+
{
14+
return base.AfterAsync(context, details, hints, cancellationToken);
15+
}
16+
17+
public override ValueTask FinallyAsync<T>(HookContext<T> context, FlagEvaluationDetails<T> evaluationDetails, IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
18+
{
19+
return base.FinallyAsync(context, evaluationDetails, hints, cancellationToken);
20+
}
21+
22+
public override ValueTask ErrorAsync<T>(HookContext<T> context, Exception error, IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
23+
{
24+
return base.ErrorAsync(context, error, hints, cancellationToken);
25+
}
26+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace OpenFeature.Hosting.Tests.DependencyInjection;
2+
3+
internal static class NoOpProvider
4+
{
5+
public const string NoOpProviderName = "No-op Provider";
6+
public const string ReasonNoOp = "No-op";
7+
public const string Variant = "No-op";
8+
}

0 commit comments

Comments
 (0)