using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Shouldly; using Volo.Abp.MultiTenancy; using Volo.Abp.SimpleStateChecking; using Xunit; namespace Volo.Abp.Features; public class RequireFeaturesSimpleBatchStateChecker_Tests : FeatureTestBase { private readonly ISimpleStateCheckerManager _simpleStateCheckerManager; private readonly ICurrentTenant _currentTenant; public RequireFeaturesSimpleBatchStateChecker_Tests() { _simpleStateCheckerManager = GetRequiredService>(); _currentTenant = GetRequiredService(); } [Fact] public void Switch_Current_Checker_Test() { var checker = RequireFeaturesSimpleBatchStateChecker.Current; checker.ShouldNotBeNull(); RequireFeaturesSimpleBatchStateChecker checker2 = null; using (RequireFeaturesSimpleBatchStateChecker.Use(new RequireFeaturesSimpleBatchStateChecker())) { checker2 = RequireFeaturesSimpleBatchStateChecker.Current; checker2.ShouldNotBeNull(); checker2.ShouldNotBe(checker); } checker2.ShouldNotBeNull(); checker2.ShouldNotBe(checker); } [Fact] public async Task RequireFeaturesSimpleBatchStateChecker_Test() { // Tenant1: BooleanTestFeature1=true, BooleanTestFeature2=true // Tenant2: no boolean features set → false using (_currentTenant.Change(TestFeatureStore.Tenant1Id)) { var myStateEntities = new MyStateEntity[] { new MyStateEntity().RequireFeatures(requiresAll: true, batchCheck: true, "BooleanTestFeature1"), new MyStateEntity().RequireFeatures(requiresAll: true, batchCheck: true, "BooleanTestFeature2"), new MyStateEntity().RequireFeatures(requiresAll: true, batchCheck: true, "BooleanTestFeature1", "BooleanTestFeature2"), new MyStateEntity().RequireFeatures(requiresAll: true, batchCheck: true, "BooleanTestFeature1", "BooleanTestFeature2"), }; var result = await _simpleStateCheckerManager.IsEnabledAsync(myStateEntities); result.Count.ShouldBe(myStateEntities.Length); result[myStateEntities[0]].ShouldBeTrue(); result[myStateEntities[1]].ShouldBeTrue(); result[myStateEntities[2]].ShouldBeTrue(); result[myStateEntities[3]].ShouldBeTrue(); } using (_currentTenant.Change(TestFeatureStore.Tenant2Id)) { var myStateEntities = new MyStateEntity[] { new MyStateEntity().RequireFeatures(requiresAll: true, batchCheck: true, "BooleanTestFeature1"), new MyStateEntity().RequireFeatures(requiresAll: true, batchCheck: true, "BooleanTestFeature2"), new MyStateEntity().RequireFeatures(requiresAll: true, batchCheck: true, "BooleanTestFeature1", "BooleanTestFeature2"), new MyStateEntity().RequireFeatures(requiresAll: false, batchCheck: true, "BooleanTestFeature1", "BooleanTestFeature2"), }; var result = await _simpleStateCheckerManager.IsEnabledAsync(myStateEntities); result.Count.ShouldBe(myStateEntities.Length); result[myStateEntities[0]].ShouldBeFalse(); result[myStateEntities[1]].ShouldBeFalse(); result[myStateEntities[2]].ShouldBeFalse(); result[myStateEntities[3]].ShouldBeFalse(); } } [Fact] public void GetModelOrNull_Returns_First_Win_When_Same_State_Registered_Twice() { // Mirrors IsEnabledAsync's modelLookup behaviour: when the same state is registered // multiple times, the first registration wins. Backed by the dictionary index. var checker = new RequireFeaturesSimpleBatchStateChecker(); var state = new NamedState("A"); checker.AddCheckModels( new RequireFeaturesSimpleBatchStateCheckerModel(state, new[] { "First" }, true)); checker.AddCheckModels( new RequireFeaturesSimpleBatchStateCheckerModel(state, new[] { "Second" }, true)); checker.GetModelOrNull(state)!.FeatureNames.ShouldBe(new[] { "First" }); } [Fact] public void GetModelOrNull_Uses_Same_Equality_As_Runtime() { // The runtime path (IsEnabledAsync) looks up models via HashSet(context.States), // i.e. EqualityComparer.Default. GetModelOrNull must use the same semantics or // a custom TState.Equals would make the runtime gate and the serializer disagree. var checker = new RequireFeaturesSimpleBatchStateChecker(); var stateA1 = new NamedState("A"); var stateA2 = new NamedState("A"); // distinct instance, equal by Name var stateB = new NamedState("B"); checker.AddCheckModels( new RequireFeaturesSimpleBatchStateCheckerModel(stateA1, new[] { "F1" }, true), new RequireFeaturesSimpleBatchStateCheckerModel(stateB, new[] { "F2" }, true)); // Same equality semantics as the runtime: A2 hits A1's model. checker.GetModelOrNull(stateA1).ShouldNotBeNull(); checker.GetModelOrNull(stateA2).ShouldNotBeNull(); checker.GetModelOrNull(stateA2)!.FeatureNames.ShouldBe(new[] { "F1" }); checker.GetModelOrNull(new NamedState("missing")).ShouldBeNull(); } private sealed class NamedState : IHasSimpleStateCheckers, IEquatable { public string Name { get; } public List> StateCheckers { get; } = new(); public NamedState(string name) => Name = name; public bool Equals(NamedState? other) => other is not null && other.Name == Name; public override bool Equals(object? obj) => obj is NamedState other && Equals(other); public override int GetHashCode() => Name.GetHashCode(); } [Fact] public async Task Current_Should_Not_Be_Null_In_Fresh_ExecutionContext() { _ = RequireFeaturesSimpleBatchStateChecker.Current; Task> task; using (ExecutionContext.SuppressFlow()) { task = Task.Run(() => RequireFeaturesSimpleBatchStateChecker.Current); } var current = await task; current.ShouldNotBeNull(); } class MyStateEntity : IHasSimpleStateCheckers { public List> StateCheckers { get; } public MyStateEntity() { StateCheckers = new List>(); } } class MyStateEntity2 : IHasSimpleStateCheckers { public List> StateCheckers { get; } public MyStateEntity2() { StateCheckers = new List>(); } } class MyStateEntity3 : IHasSimpleStateCheckers { public List> StateCheckers { get; } public MyStateEntity3() { StateCheckers = new List>(); } } }