diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs index b1ae63c39b..a59e2d8dd6 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs @@ -171,10 +171,14 @@ public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApp var abpPolicyNames = new List(); var otherPolicyNames = new List(); + var permissionNameSet = new HashSet( + (await _permissionDefinitionManager.GetPermissionsAsync()).Select(p => p.Name), + StringComparer.Ordinal); + foreach (var policyName in policyNames) { if (await _defaultAuthorizationPolicyProvider.GetPolicyAsync(policyName) == null && - await _permissionDefinitionManager.GetOrNullAsync(policyName) != null) + permissionNameSet.Contains(policyName)) { abpPolicyNames.Add(policyName); } diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionChecker.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionChecker.cs index 4a5e48c220..7f851f308b 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionChecker.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionChecker.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Security.Principal; @@ -110,11 +111,15 @@ public class PermissionChecker : IPermissionChecker, ITransientDependency var multiTenancySide = claimsPrincipal?.GetMultiTenancySide() ?? CurrentTenant.GetMultiTenancySide(); + var allPermissions = (await PermissionDefinitionManager.GetPermissionsAsync()) + .ToDictionary(p => p.Name, StringComparer.Ordinal); + + var pendingStateCheck = new List(); var permissionDefinitions = new List(); + foreach (var name in names) { - var permission = await PermissionDefinitionManager.GetOrNullAsync(name); - if (permission == null) + if (!allPermissions.TryGetValue(name, out var permission)) { result.Result.Add(name, PermissionGrantResult.Prohibited); continue; @@ -122,11 +127,23 @@ public class PermissionChecker : IPermissionChecker, ITransientDependency result.Result.Add(name, PermissionGrantResult.Undefined); - if (permission.IsEnabled && - await StateCheckerManager.IsEnabledAsync(permission) && - permission.MultiTenancySide.HasFlag(multiTenancySide)) + if (!permission.IsEnabled || !permission.MultiTenancySide.HasFlag(multiTenancySide)) { - permissionDefinitions.Add(permission); + continue; + } + + pendingStateCheck.Add(permission); + } + + if (pendingStateCheck.Count > 0) + { + var stateCheckResult = await StateCheckerManager.IsEnabledAsync(pendingStateCheck.ToArray()); + foreach (var (perm, enabled) in stateCheckResult) + { + if (enabled) + { + permissionDefinitions.Add(perm); + } } } diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionManager.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionManager.cs index 3dbc22fb47..7db0531c78 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionManager.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionManager.cs @@ -75,15 +75,18 @@ public class PermissionDefinitionManager : IPermissionDefinitionManager, ITransi public virtual async Task> GetResourcePermissionsAsync() { var staticResourcePermissions = await _staticStore.GetResourcePermissionsAsync(); - var staticResourcePermissionNames = staticResourcePermissions - .Select(p => p.Name) + var staticResourcePermissionKeys = staticResourcePermissions + .Select(p => (p.ResourceName, p.Name)) .ToImmutableHashSet(); var dynamicResourcePermissions = await _dynamicStore.GetResourcePermissionsAsync(); - /* We prefer static permissions over dynamics */ + /* We prefer static permissions over dynamics. + * Resource permissions are unique by (ResourceName, Name), so we must deduplicate + * using both fields to avoid incorrectly excluding dynamic permissions whose name + * matches a static permission from a different resource. */ return staticResourcePermissions.Concat( - dynamicResourcePermissions.Where(d => !staticResourcePermissionNames.Contains(d.Name)) + dynamicResourcePermissions.Where(d => !staticResourcePermissionKeys.Contains((d.ResourceName, d.Name))) ).ToImmutableList(); } diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionChecker.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionChecker.cs index a920c2e464..73c16c62cc 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionChecker.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionChecker.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Security.Principal; @@ -115,11 +116,16 @@ public class ResourcePermissionChecker : IResourcePermissionChecker, ITransientD var multiTenancySide = claimsPrincipal?.GetMultiTenancySide() ?? CurrentTenant.GetMultiTenancySide(); + var allResourcePermissions = (await PermissionDefinitionManager.GetResourcePermissionsAsync()) + .Where(p => p.ResourceName == resourceName) + .ToDictionary(p => p.Name, StringComparer.Ordinal); + + var pendingStateCheck = new List(); var permissionDefinitions = new List(); + foreach (var name in names) { - var permission = await PermissionDefinitionManager.GetResourcePermissionOrNullAsync(resourceName, name); - if (permission == null) + if (!allResourcePermissions.TryGetValue(name, out var permission)) { result.Result.Add(name, PermissionGrantResult.Prohibited); continue; @@ -127,11 +133,23 @@ public class ResourcePermissionChecker : IResourcePermissionChecker, ITransientD result.Result.Add(name, PermissionGrantResult.Undefined); - if (permission.IsEnabled && - await StateCheckerManager.IsEnabledAsync(permission) && - permission.MultiTenancySide.HasFlag(multiTenancySide)) + if (!permission.IsEnabled || !permission.MultiTenancySide.HasFlag(multiTenancySide)) + { + continue; + } + + pendingStateCheck.Add(permission); + } + + if (pendingStateCheck.Count > 0) + { + var stateCheckResult = await StateCheckerManager.IsEnabledAsync(pendingStateCheck.ToArray()); + foreach (var (perm, enabled) in stateCheckResult) { - permissionDefinitions.Add(permission); + if (enabled) + { + permissionDefinitions.Add(perm); + } } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerManager.cs b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerManager.cs index 542171e979..1726a89f84 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerManager.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerManager.cs @@ -83,6 +83,17 @@ public class SimpleStateCheckerManager : ISimpleStateCheckerManager InternalIsEnabledAsync(TState state, bool useBatchChecker) { + var hasStateCheckers = state.StateCheckers + .WhereIf(!useBatchChecker, x => x is not ISimpleBatchStateChecker) + .Any(); + var hasGlobalCheckers = Options.GlobalStateCheckers + .WhereIf(!useBatchChecker, x => !typeof(ISimpleBatchStateChecker).IsAssignableFrom(x)) + .Any(); + if (!hasStateCheckers && !hasGlobalCheckers) + { + return true; + } + using (var scope = ServiceProvider.CreateScope()) { var context = new SimpleStateCheckerContext(scope.ServiceProvider.GetRequiredService(), state); diff --git a/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/PermissionChecker_BulkWithStateChecker_Tests.cs b/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/PermissionChecker_BulkWithStateChecker_Tests.cs new file mode 100644 index 0000000000..577f209bd3 --- /dev/null +++ b/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/PermissionChecker_BulkWithStateChecker_Tests.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Authorization.Permissions; +using Xunit; + +namespace Volo.Abp.Authorization; + +public class PermissionChecker_BulkWithStateChecker_Tests : AuthorizationTestBase +{ + private readonly IPermissionChecker _permissionChecker; + + public PermissionChecker_BulkWithStateChecker_Tests() + { + _permissionChecker = GetRequiredService(); + } + + [Fact] + public async Task IsGrantedAsync_With_Empty_Names_Should_Return_Empty_Result() + { + var result = await _permissionChecker.IsGrantedAsync(Array.Empty()); + result.Result.ShouldBeEmpty(); + } + + [Fact] + public async Task IsGrantedAsync_StateChecker_Permission_Is_Undefined_When_StateChecker_Fails() + { + // MyPermission1 has TestRequireEditionPermissionSimpleStateChecker. + // Current user (Douglas) has no EditionId claim → StateChecker returns false. + // The permission must stay Undefined (never reaches the value-provider pipeline). + var result = await _permissionChecker.IsGrantedAsync(new[] { "MyPermission1", "MyPermission3" }); + + result.Result["MyPermission1"].ShouldBe(PermissionGrantResult.Undefined); + result.Result["MyPermission3"].ShouldBe(PermissionGrantResult.Granted); + } + + [Fact] + public async Task IsGrantedAsync_Mix_Of_Defined_And_Undefined_Permissions() + { + var result = await _permissionChecker.IsGrantedAsync(new[] + { + "MyPermission3", + "NonExistentPermission", + "MyPermission5" + }); + + result.Result["MyPermission3"].ShouldBe(PermissionGrantResult.Granted); + result.Result["NonExistentPermission"].ShouldBe(PermissionGrantResult.Prohibited); + result.Result["MyPermission5"].ShouldBe(PermissionGrantResult.Granted); + } +} diff --git a/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/ResourcePermissionChecker_BulkWithStateChecker_Tests.cs b/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/ResourcePermissionChecker_BulkWithStateChecker_Tests.cs new file mode 100644 index 0000000000..80533e0890 --- /dev/null +++ b/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/ResourcePermissionChecker_BulkWithStateChecker_Tests.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Authorization.Permissions.Resources; +using Volo.Abp.Authorization.TestServices.Resources; +using Xunit; + +namespace Volo.Abp.Authorization; + +public class ResourcePermissionChecker_BulkWithStateChecker_Tests : AuthorizationTestBase +{ + private readonly IResourcePermissionChecker _resourcePermissionChecker; + + public ResourcePermissionChecker_BulkWithStateChecker_Tests() + { + _resourcePermissionChecker = GetRequiredService(); + } + + [Fact] + public async Task IsGrantedAsync_With_Empty_Names_Should_Return_Empty_Result() + { + var result = await _resourcePermissionChecker.IsGrantedAsync( + Array.Empty(), TestEntityResource.ResourceName, TestEntityResource.ResourceKey5); + + result.Result.ShouldBeEmpty(); + } + + [Fact] + public async Task IsGrantedAsync_StateChecker_Permission_Is_Undefined_When_StateChecker_Fails() + { + // MyResourcePermission1 has TestRequireEditionPermissionSimpleStateChecker. + // Current user (Douglas) has no EditionId claim → StateChecker returns false. + // The permission must stay Undefined (never reaches the value-provider pipeline). + var result = await _resourcePermissionChecker.IsGrantedAsync( + new[] { "MyResourcePermission1", "MyResourcePermission3" }, + TestEntityResource.ResourceName, + TestEntityResource.ResourceKey3); + + result.Result["MyResourcePermission1"].ShouldBe(PermissionGrantResult.Undefined); + result.Result["MyResourcePermission3"].ShouldBe(PermissionGrantResult.Granted); + } + + [Fact] + public async Task IsGrantedAsync_Mix_Of_Defined_And_Undefined_Permissions() + { + var result = await _resourcePermissionChecker.IsGrantedAsync( + new[] { "MyResourcePermission3", "NonExistentPermission", "MyResourcePermission5" }, + TestEntityResource.ResourceName, + TestEntityResource.ResourceKey5); + + result.Result["MyResourcePermission3"].ShouldBe(PermissionGrantResult.Granted); + result.Result["NonExistentPermission"].ShouldBe(PermissionGrantResult.Prohibited); + result.Result["MyResourcePermission5"].ShouldBe(PermissionGrantResult.Granted); + } + +} diff --git a/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/StaticPermissionDefinitionStore_Tests.cs b/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/StaticPermissionDefinitionStore_Tests.cs index 0afaa34f10..19ab829ba1 100644 --- a/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/StaticPermissionDefinitionStore_Tests.cs +++ b/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/StaticPermissionDefinitionStore_Tests.cs @@ -70,4 +70,18 @@ public class StaticPermissionDefinitionStore_Tests : AuthorizationTestBase permissions.ShouldContain(x => x.Name == "MyResourcePermission6"); permissions.ShouldContain(x => x.Name == "MyResourcePermission7"); } + + [Fact] + public async Task GetResourcePermissionsAsync_Same_Name_In_Different_Resources_Should_Both_Be_Returned() + { + // MyResourcePermission7 is defined for both TestEntityResource and TestEntityResource2. + // GetResourcePermissionsAsync must return both entries because resource permissions are + // unique by (ResourceName, Name), not by Name alone. + var permissions = await _store.GetResourcePermissionsAsync(); + + permissions.ShouldContain(x => + x.Name == "MyResourcePermission7" && x.ResourceName == TestEntityResource.ResourceName); + permissions.ShouldContain(x => + x.Name == "MyResourcePermission7" && x.ResourceName == TestEntityResource2.ResourceName); + } } diff --git a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/SimpleStateChecking/SimpleStateChecker_NoCheckers_Test.cs b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/SimpleStateChecking/SimpleStateChecker_NoCheckers_Test.cs new file mode 100644 index 0000000000..95fdadd4d7 --- /dev/null +++ b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/SimpleStateChecking/SimpleStateChecker_NoCheckers_Test.cs @@ -0,0 +1,66 @@ +using System.Globalization; +using System; +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace Volo.Abp.SimpleStateChecking; + +public class SimpleStateChecker_NoCheckers_Test : SimpleStateCheckerTestBase +{ + // No GlobalStateCheckers registered — verifies the fast path when both + // state.StateCheckers and Options.GlobalStateCheckers are empty. + + [Fact] + public async Task Entity_With_No_State_Checkers_Should_Be_Enabled() + { + var entity = new MyStateEntity(); + + (await SimpleStateCheckerManager.IsEnabledAsync(entity)).ShouldBeTrue(); + } + + [Fact] + public async Task Entity_With_No_State_Checkers_Should_Not_Increment_Check_Counts() + { + var entity = new MyStateEntity(); + + await SimpleStateCheckerManager.IsEnabledAsync(entity); + + entity.CheckCount.ShouldBe(0); + entity.GlobalCheckCount.ShouldBe(0); + entity.MultipleCheckCount.ShouldBe(0); + entity.MultipleGlobalCheckCount.ShouldBe(0); + } + + [Fact] + public async Task Multiple_Entities_With_No_State_Checkers_Should_All_Be_Enabled() + { + var entities = new[] + { + new MyStateEntity { CreationTime = DateTime.Parse("2022-01-01", CultureInfo.InvariantCulture) }, + new MyStateEntity { CreationTime = DateTime.Parse("2019-01-01", CultureInfo.InvariantCulture) } + }; + + var result = await SimpleStateCheckerManager.IsEnabledAsync(entities); + + result.Values.ShouldAllBe(v => v); + } + + [Fact] + public async Task Entity_With_Individual_Checker_Should_Still_Be_Checked() + { + var entity = new MyStateEntity + { + CreationTime = DateTime.Parse("2021-01-01", CultureInfo.InvariantCulture) + }; + entity.AddSimpleStateChecker(new MySimpleStateChecker()); + + (await SimpleStateCheckerManager.IsEnabledAsync(entity)).ShouldBeTrue(); + entity.CheckCount.ShouldBe(1); + + entity.CreationTime = DateTime.Parse("2001-01-01", CultureInfo.InvariantCulture); + + (await SimpleStateCheckerManager.IsEnabledAsync(entity)).ShouldBeFalse(); + entity.CheckCount.ShouldBe(2); + } +} diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionGrantCacheItem.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionGrantCacheItem.cs index a89a586cbd..597fe1c6dd 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionGrantCacheItem.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionGrantCacheItem.cs @@ -1,6 +1,4 @@ using System; -using System.Linq; -using Volo.Abp.Text.Formatting; namespace Volo.Abp.PermissionManagement; @@ -28,7 +26,8 @@ public class PermissionGrantCacheItem public static string GetPermissionNameFormCacheKeyOrNull(string cacheKey) { - var result = FormattedStringValueExtracter.Extract(cacheKey, CacheKeyFormat, true); - return result.IsMatch ? result.Matches.Last().Value : null; + const string separator = ",n:"; + var index = cacheKey.LastIndexOf(separator, StringComparison.Ordinal); + return index >= 0 ? cacheKey.Substring(index + separator.Length) : null; } } diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionGrantCacheItem.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionGrantCacheItem.cs index 59362f6f5f..f03a997b92 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionGrantCacheItem.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionGrantCacheItem.cs @@ -1,6 +1,4 @@ using System; -using System.Linq; -using Volo.Abp.Text.Formatting; namespace Volo.Abp.PermissionManagement; @@ -28,7 +26,8 @@ public class ResourcePermissionGrantCacheItem public static string GetPermissionNameFormCacheKeyOrNull(string cacheKey) { - var result = FormattedStringValueExtracter.Extract(cacheKey, CacheKeyFormat, true); - return result.IsMatch ? result.Matches.Last().Value : null; + const string separator = ",n:"; + var index = cacheKey.LastIndexOf(separator, StringComparison.Ordinal); + return index >= 0 ? cacheKey.Substring(index + separator.Length) : null; } } diff --git a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionGrantCacheItem_Tests.cs b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionGrantCacheItem_Tests.cs index e971e5ecfe..0d03918f2b 100644 --- a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionGrantCacheItem_Tests.cs +++ b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionGrantCacheItem_Tests.cs @@ -12,4 +12,25 @@ public class PermissionGrantCacheItem_Tests PermissionGrantCacheItem.GetPermissionNameFormCacheKeyOrNull(key).ShouldBe("aaa"); PermissionGrantCacheItem.GetPermissionNameFormCacheKeyOrNull("aaabbbccc").ShouldBeNull(); } + + [Theory] + [InlineData("MyModule.Users.Create", "R", "admin")] + [InlineData("AbpIdentity.Users", "U", "550e8400-e29b-41d4-a716-446655440000")] + [InlineData("Permission.With.Many.Dots", "T", "tenant-key-123")] + [InlineData("Simple", "R", "")] + public void GetPermissionNameFormCacheKeyOrNull_Should_Extract_PermissionName( + string permissionName, string providerName, string providerKey) + { + var key = PermissionGrantCacheItem.CalculateCacheKey(permissionName, providerName, providerKey); + PermissionGrantCacheItem.GetPermissionNameFormCacheKeyOrNull(key).ShouldBe(permissionName); + } + + [Theory] + [InlineData("")] + [InlineData("no-separator-here")] + [InlineData("pn:R,pk:admin")] + public void GetPermissionNameFormCacheKeyOrNull_Should_Return_Null_For_Invalid_Keys(string invalidKey) + { + PermissionGrantCacheItem.GetPermissionNameFormCacheKeyOrNull(invalidKey).ShouldBeNull(); + } } diff --git a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/ResourcePermissionGrantCacheItem_Tests.cs b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/ResourcePermissionGrantCacheItem_Tests.cs index 1d7b4ea12d..f4c65f0d7a 100644 --- a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/ResourcePermissionGrantCacheItem_Tests.cs +++ b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/ResourcePermissionGrantCacheItem_Tests.cs @@ -12,4 +12,26 @@ public class ResourcePermissionGrantCacheItem_Tests ResourcePermissionGrantCacheItem.GetPermissionNameFormCacheKeyOrNull(key).ShouldBe("aaa"); ResourcePermissionGrantCacheItem.GetPermissionNameFormCacheKeyOrNull("aaabbbccc").ShouldBeNull(); } + + [Theory] + [InlineData("MyModule.Users.Create", "R", "admin")] + [InlineData("AbpIdentity.Users", "U", "550e8400-e29b-41d4-a716-446655440000")] + [InlineData("Permission.With.Many.Dots", "T", "tenant-key-123")] + [InlineData("Simple", "R", "")] + public void GetPermissionNameFormCacheKeyOrNull_Should_Extract_PermissionName( + string permissionName, string providerName, string providerKey) + { + var key = ResourcePermissionGrantCacheItem.CalculateCacheKey( + permissionName, TestEntityResource.ResourceName, TestEntityResource.ResourceKey1, providerName, providerKey); + ResourcePermissionGrantCacheItem.GetPermissionNameFormCacheKeyOrNull(key).ShouldBe(permissionName); + } + + [Theory] + [InlineData("")] + [InlineData("no-separator-here")] + [InlineData("rn:res,rk:key,pn:R,pk:admin")] + public void GetPermissionNameFormCacheKeyOrNull_Should_Return_Null_For_Invalid_Keys(string invalidKey) + { + ResourcePermissionGrantCacheItem.GetPermissionNameFormCacheKeyOrNull(invalidKey).ShouldBeNull(); + } }