Browse Source

Merge pull request #25952 from abpframework/auto-merge/rel-10-7/4759

Merge branch dev with rel-10.7
pull/25979/head
Volosoft Agent 3 days ago
committed by GitHub
parent
commit
9fb43459a6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      framework/src/Volo.Abp.Gdpr.Abstractions/Volo.Abp.Gdpr.Abstractions.csproj
  2. 4
      framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/AbpGdprAbstractionsModule.cs
  3. 13
      framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprUserDataDeletionRequestedEto.cs
  4. 13
      framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprUserDataPreparedEto.cs
  5. 13
      framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprUserDataRequestedEto.cs
  6. 23
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs
  7. 59
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnitManager.cs
  8. 52
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/UserDeletedEventHandler.cs
  9. 5
      modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Delete_Tests.cs
  10. 5
      modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/OrganizationUnitManager_CreateMany_Tests.cs
  11. 139
      modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/OrganizationUnitManager_Tests.cs
  12. 8
      modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/IdentityUserManager_Delete_Tests.cs
  13. 8
      modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/OrganizationUnitManager_CreateMany_Tests.cs
  14. 477
      modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityUserManager_Delete_Tests.cs
  15. 54
      modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitManager_CreateMany_Tests.cs

1
framework/src/Volo.Abp.Gdpr.Abstractions/Volo.Abp.Gdpr.Abstractions.csproj

@ -12,6 +12,7 @@
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Core\Volo.Abp.Core.csproj" />
<ProjectReference Include="..\Volo.Abp.EventBus.Abstractions\Volo.Abp.EventBus.Abstractions.csproj" />
</ItemGroup>
</Project>

4
framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/AbpGdprAbstractionsModule.cs

@ -1,7 +1,11 @@
using Volo.Abp.Modularity;
using Volo.Abp.EventBus.Abstractions;
namespace Volo.Abp.Gdpr;
[DependsOn(
typeof(AbpEventBusAbstractionsModule)
)]
public class AbpGdprAbstractionsModule : AbpModule
{
}

13
framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprUserDataDeletionRequestedEto.cs

@ -1,9 +1,18 @@
using System;
using Volo.Abp.EventBus;
namespace Volo.Abp.Gdpr;
[Serializable]
public class GdprUserDataDeletionRequestedEto
public class GdprUserDataDeletionRequestedEto : IEventDataMayHaveTenantId
{
public Guid? TenantId { get; set; }
public Guid UserId { get; set; }
}
public bool IsMultiTenant(out Guid? tenantId)
{
tenantId = TenantId;
return TenantId.HasValue;
}
}

13
framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprUserDataPreparedEto.cs

@ -1,13 +1,22 @@
using System;
using Volo.Abp.EventBus;
namespace Volo.Abp.Gdpr;
[Serializable]
public class GdprUserDataPreparedEto
public class GdprUserDataPreparedEto : IEventDataMayHaveTenantId
{
public Guid? TenantId { get; set; }
public Guid RequestId { get; set; }
public string Provider { get; set; } = default!;
public GdprDataInfo Data { get; set; } = default!;
}
public bool IsMultiTenant(out Guid? tenantId)
{
tenantId = TenantId;
return TenantId.HasValue;
}
}

13
framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprUserDataRequestedEto.cs

@ -1,11 +1,20 @@
using System;
using Volo.Abp.EventBus;
namespace Volo.Abp.Gdpr;
[Serializable]
public class GdprUserDataRequestedEto
public class GdprUserDataRequestedEto : IEventDataMayHaveTenantId
{
public Guid? TenantId { get; set; }
public Guid UserId { get; set; }
public Guid RequestId { get; set; }
}
public bool IsMultiTenant(out Guid? tenantId)
{
tenantId = TenantId;
return TenantId.HasValue;
}
}

23
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs

@ -98,13 +98,32 @@ public class IdentityUserManager : UserManager<IdentityUser>, IDomainService
public async override Task<IdentityResult> DeleteAsync(IdentityUser user)
{
//The user may have been loaded without details.
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.Claims, CancellationToken);
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.Roles, CancellationToken);
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.Tokens, CancellationToken);
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.Logins, CancellationToken);
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.OrganizationUnits, CancellationToken);
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.PasswordHistories, CancellationToken);
await UserRepository.EnsureCollectionLoadedAsync(user, x => x.Passkeys, CancellationToken);
user.Claims.Clear();
user.Roles.Clear();
user.Tokens.Clear();
user.Logins.Clear();
user.OrganizationUnits.Clear();
await IdentityLinkUserRepository.DeleteAsync(new IdentityLinkUserInfo(user.Id, user.TenantId), CancellationToken);
await UpdateAsync(user);
user.PasswordHistories.Clear();
user.Passkeys.Clear();
//Soft deleting reloads the original values, the store saves the changes without validating the user.
//Nothing else is deleted before this succeeds, it is where the user is checked for concurrency.
(await Store.UpdateAsync(user, CancellationToken)).CheckErrors();
//They are in the host database and deleting them here covers the current unit of work.
using (CurrentTenant.Change(null))
{
await IdentityLinkUserRepository.DeleteAsync(new IdentityLinkUserInfo(user.Id, user.TenantId), CancellationToken);
}
return await base.DeleteAsync(user);
}

59
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnitManager.cs

@ -48,6 +48,51 @@ public class OrganizationUnitManager : DomainService
await OrganizationUnitRepository.InsertAsync(organizationUnit);
}
/// <summary>
/// Creates the given organization units by querying the siblings of a parent once instead of once
/// per organization unit. They all have to belong to the current tenant and their parents must
/// already exist. <see cref="GetNextChildCodeAsync"/> is used for the first organization unit of a
/// parent, the codes of the rest follow it. Custom validation should be added by overriding
/// <see cref="ValidateOrganizationUnitAsync(OrganizationUnit, List{OrganizationUnit})"/>.
/// </summary>
[UnitOfWork]
public virtual async Task CreateManyAsync(List<OrganizationUnit> organizationUnits)
{
Check.NotNull(organizationUnits, nameof(organizationUnits));
if (organizationUnits.Any(x => x.TenantId != CurrentTenant.Id))
{
throw new AbpException("Organization units of another tenant can not be created, change the current tenant instead!");
}
var groups = organizationUnits.GroupBy(x => x.ParentId).ToList();
foreach (var group in groups)
{
await ValidateParentTenantAsync(group.Key, CurrentTenant.Id);
var siblings = await FindChildrenAsync(group.Key);
string lastCode = null;
foreach (var organizationUnit in group)
{
organizationUnit.Code = lastCode = lastCode == null
? await GetNextChildCodeAsync(group.Key)
: OrganizationUnit.CalculateNextCode(lastCode);
await ValidateOrganizationUnitAsync(organizationUnit, siblings);
siblings.Add(organizationUnit);
}
}
//Nothing is inserted before every group is validated.
foreach (var group in groups)
{
await OrganizationUnitRepository.InsertManyAsync(group.ToList());
}
}
public virtual async Task UpdateAsync(OrganizationUnit organizationUnit)
{
await ValidateOrganizationUnitAsync(organizationUnit);
@ -141,15 +186,21 @@ public class OrganizationUnitManager : DomainService
protected virtual async Task ValidateOrganizationUnitAsync(OrganizationUnit organizationUnit)
{
var siblings = (await FindChildrenAsync(organizationUnit.ParentId))
.Where(ou => ou.Id != organizationUnit.Id)
.ToList();
await ValidateOrganizationUnitAsync(organizationUnit, await FindChildrenAsync(organizationUnit.ParentId));
}
if (siblings.Any(ou => ou.DisplayName == organizationUnit.DisplayName))
/// <summary>
/// Validates the organization unit against the given siblings, so they are not queried again.
/// </summary>
protected virtual Task ValidateOrganizationUnitAsync(OrganizationUnit organizationUnit, List<OrganizationUnit> siblings)
{
if (siblings.Any(ou => ou.Id != organizationUnit.Id && ou.DisplayName == organizationUnit.DisplayName))
{
throw new BusinessException(IdentityErrorCodes.DuplicateOrganizationUnitDisplayName)
.WithData("0", organizationUnit.DisplayName);
}
return Task.CompletedTask;
}
protected virtual async Task ValidateParentTenantAsync(Guid? parentId, Guid? tenantId)

52
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/UserDeletedEventHandler.cs

@ -0,0 +1,52 @@
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.EventBus;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;
namespace Volo.Abp.Identity;
//Sessions, user delegations and link users have no navigation from IdentityUser,
//so clearing the user's collections doesn't cover them.
public class UserDeletedEventHandler :
ILocalEventHandler<EntityDeletedEventData<IdentityUser>>,
ITransientDependency
{
protected IIdentitySessionRepository IdentitySessionRepository { get; }
protected IIdentityUserDelegationRepository IdentityUserDelegationRepository { get; }
protected IIdentityLinkUserRepository IdentityLinkUserRepository { get; }
protected ICurrentTenant CurrentTenant { get; }
public UserDeletedEventHandler(
IIdentitySessionRepository identitySessionRepository,
IIdentityUserDelegationRepository identityUserDelegationRepository,
IIdentityLinkUserRepository identityLinkUserRepository,
ICurrentTenant currentTenant)
{
IdentitySessionRepository = identitySessionRepository;
IdentityUserDelegationRepository = identityUserDelegationRepository;
IdentityLinkUserRepository = identityLinkUserRepository;
CurrentTenant = currentTenant;
}
[UnitOfWork]
public virtual async Task HandleEventAsync(EntityDeletedEventData<IdentityUser> eventData)
{
var user = eventData.Entity;
await IdentitySessionRepository.DeleteAllAsync(user.Id);
var delegations = await IdentityUserDelegationRepository.GetListAsync(sourceUserId: user.Id, targetUserId: null);
delegations.AddRange(await IdentityUserDelegationRepository.GetListAsync(sourceUserId: null, targetUserId: user.Id));
//A delegation of the user to itself is returned by both queries.
await IdentityUserDelegationRepository.DeleteManyAsync(delegations.DistinctBy(x => x.Id).ToList());
//Link users are stored in the host database.
using (CurrentTenant.Change(null))
{
await IdentityLinkUserRepository.DeleteAsync(new IdentityLinkUserInfo(user.Id, user.TenantId));
}
}
}

5
modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Delete_Tests.cs

@ -0,0 +1,5 @@
namespace Volo.Abp.Identity;
public class IdentityUserManager_Delete_Tests : IdentityUserManager_Delete_Tests<AbpIdentityDomainTestModule>
{
}

5
modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/OrganizationUnitManager_CreateMany_Tests.cs

@ -0,0 +1,5 @@
namespace Volo.Abp.Identity;
public class OrganizationUnitManager_CreateMany_Tests : OrganizationUnitManager_CreateMany_Tests<AbpIdentityDomainTestModule>
{
}

139
modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/OrganizationUnitManager_Tests.cs

@ -1,9 +1,16 @@
using Microsoft.AspNetCore.Identity;
using Shouldly;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Threading;
using Volo.Abp.Security.Claims;
using Volo.Abp.Identity.Localization;
using Volo.Abp.Caching;
using Microsoft.Extensions.Localization;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;
@ -179,4 +186,136 @@ public class OrganizationUnitManager_Tests : AbpIdentityDomainTestBase
}
}
}
[Fact]
public async Task CreateManyAsync()
{
List<OrganizationUnit> organizationUnits;
using (var uow = _unitOfWorkManager.Begin())
{
var lastRootCode = (await _organizationUnitRepository.GetChildrenAsync(null))
.OrderBy(x => x.Code).Last().Code;
organizationUnits = Enumerable.Range(0, 5)
.Select(_ => new OrganizationUnit(_guidGenerator.Create(), $"batch-{Guid.NewGuid():N}"))
.ToList();
await _organizationUnitManager.CreateManyAsync(organizationUnits);
await uow.CompleteAsync();
foreach (var organizationUnit in organizationUnits)
{
lastRootCode = OrganizationUnit.CalculateNextCode(lastRootCode);
organizationUnit.Code.ShouldBe(lastRootCode);
}
}
using (var uow = _unitOfWorkManager.Begin())
{
foreach (var organizationUnit in organizationUnits)
{
(await _organizationUnitRepository.GetAsync(organizationUnit.Id)).Code.ShouldBe(organizationUnit.Code);
}
await uow.CompleteAsync();
}
}
[Fact]
public async Task CreateManyAsync_Should_Not_Allow_Duplicate_Display_Name_In_The_Batch()
{
using (var uow = _unitOfWorkManager.Begin())
{
var displayName = $"batch-duplicate-{Guid.NewGuid():N}";
await Should.ThrowAsync<BusinessException>(async () =>
await _organizationUnitManager.CreateManyAsync([
new OrganizationUnit(_guidGenerator.Create(), displayName),
new OrganizationUnit(_guidGenerator.Create(), displayName)
]));
await uow.CompleteAsync();
}
}
[Fact]
public async Task CreateManyAsync_Should_Not_Create_Organization_Units_Of_Another_Tenant()
{
using (var uow = _unitOfWorkManager.Begin())
{
await Should.ThrowAsync<AbpException>(async () =>
await _organizationUnitManager.CreateManyAsync([
new OrganizationUnit(_guidGenerator.Create(), "another-tenant", null, Guid.NewGuid())
]));
await uow.CompleteAsync();
}
}
[Fact]
public async Task CreateManyAsync_Should_Use_The_Overridden_Extension_Points()
{
var manager = new TestOrganizationUnitManager(
_organizationUnitRepository,
GetRequiredService<IStringLocalizer<IdentityResource>>(),
_identityRoleRepository,
GetRequiredService<IDistributedCache<AbpDynamicClaimCacheItem>>(),
GetRequiredService<ICancellationTokenProvider>())
{
LazyServiceProvider = GetRequiredService<IAbpLazyServiceProvider>()
};
using (var uow = _unitOfWorkManager.Begin())
{
await Should.ThrowAsync<BusinessException>(async () =>
await manager.CreateManyAsync([new OrganizationUnit(_guidGenerator.Create(), "rejected-by-the-override")]));
await manager.CreateManyAsync([
new OrganizationUnit(_guidGenerator.Create(), $"extension-point-1-{Guid.NewGuid():N}"),
new OrganizationUnit(_guidGenerator.Create(), $"extension-point-2-{Guid.NewGuid():N}")
]);
await uow.CompleteAsync();
}
//Every organization unit is validated, the code generator is only used for the first one of a parent.
//Both calls above created a root organization unit, so the code generator was used twice.
manager.ValidateCallCount.ShouldBe(3);
manager.GetNextChildCodeCallCount.ShouldBe(2);
}
public class TestOrganizationUnitManager : OrganizationUnitManager
{
public int ValidateCallCount { get; private set; }
public int GetNextChildCodeCallCount { get; private set; }
public TestOrganizationUnitManager(
IOrganizationUnitRepository organizationUnitRepository,
IStringLocalizer<IdentityResource> localizer,
IIdentityRoleRepository identityRoleRepository,
IDistributedCache<AbpDynamicClaimCacheItem> dynamicClaimCache,
ICancellationTokenProvider cancellationTokenProvider)
: base(organizationUnitRepository, localizer, identityRoleRepository, dynamicClaimCache, cancellationTokenProvider)
{
}
public override async Task<string> GetNextChildCodeAsync(Guid? parentId)
{
GetNextChildCodeCallCount++;
return await base.GetNextChildCodeAsync(parentId);
}
protected override async Task ValidateOrganizationUnitAsync(OrganizationUnit organizationUnit, List<OrganizationUnit> siblings)
{
ValidateCallCount++;
if (organizationUnit.DisplayName == "rejected-by-the-override")
{
throw new BusinessException(IdentityErrorCodes.DuplicateOrganizationUnitDisplayName);
}
await base.ValidateOrganizationUnitAsync(organizationUnit, siblings);
}
}
}

8
modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/IdentityUserManager_Delete_Tests.cs

@ -0,0 +1,8 @@
using Xunit;
namespace Volo.Abp.Identity.MongoDB;
[Collection(MongoTestCollection.Name)]
public class IdentityUserManager_Delete_Tests : IdentityUserManager_Delete_Tests<AbpIdentityMongoDbTestModule>
{
}

8
modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/OrganizationUnitManager_CreateMany_Tests.cs

@ -0,0 +1,8 @@
using Xunit;
namespace Volo.Abp.Identity.MongoDB;
[Collection(MongoTestCollection.Name)]
public class OrganizationUnitManager_CreateMany_Tests : OrganizationUnitManager_CreateMany_Tests<AbpIdentityMongoDbTestModule>
{
}

477
modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityUserManager_Delete_Tests.cs

@ -0,0 +1,477 @@
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Shouldly;
using Volo.Abp.Data;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Caching;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Security.Claims;
using Volo.Abp.Uow;
using Xunit;
namespace Volo.Abp.Identity;
public abstract class IdentityUserManager_Delete_Tests<TStartupModule> : AbpIdentityTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
protected IdentityUserManager IdentityUserManager { get; }
protected IIdentityUserRepository IdentityUserRepository { get; }
protected IOrganizationUnitRepository OrganizationUnitRepository { get; }
protected IdentityLinkUserManager IdentityLinkUserManager { get; }
protected IIdentitySessionRepository IdentitySessionRepository { get; }
protected IIdentityUserDelegationRepository IdentityUserDelegationRepository { get; }
protected ILookupNormalizer LookupNormalizer { get; }
protected IUnitOfWorkManager UnitOfWorkManager { get; }
protected IDataFilter DataFilter { get; }
protected ICurrentTenant CurrentTenant { get; }
protected IDistributedCache<AbpDynamicClaimCacheItem> DynamicClaimCache { get; }
protected IdentityUserManager_Delete_Tests()
{
CurrentTenant = GetRequiredService<ICurrentTenant>();
DynamicClaimCache = GetRequiredService<IDistributedCache<AbpDynamicClaimCacheItem>>();
IdentityUserManager = GetRequiredService<IdentityUserManager>();
IdentityUserRepository = GetRequiredService<IIdentityUserRepository>();
OrganizationUnitRepository = GetRequiredService<IOrganizationUnitRepository>();
IdentityLinkUserManager = GetRequiredService<IdentityLinkUserManager>();
IdentitySessionRepository = GetRequiredService<IIdentitySessionRepository>();
IdentityUserDelegationRepository = GetRequiredService<IIdentityUserDelegationRepository>();
LookupNormalizer = GetRequiredService<ILookupNormalizer>();
UnitOfWorkManager = GetRequiredService<IUnitOfWorkManager>();
DataFilter = GetRequiredService<IDataFilter>();
}
[Fact]
public virtual async Task DeleteAsync_Should_Remove_All_Related_Data()
{
var userId = Guid.NewGuid();
var linkedUserId = Guid.NewGuid();
using (var uow = UnitOfWorkManager.Begin())
{
(await IdentityUserManager.CreateAsync(
new IdentityUser(userId, $"delete-{userId:N}", $"delete-{userId:N}@abp.io"))).CheckErrors();
(await IdentityUserManager.CreateAsync(
new IdentityUser(linkedUserId, $"linked-{linkedUserId:N}", $"linked-{linkedUserId:N}@abp.io"))).CheckErrors();
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
var user = await IdentityUserManager.GetByIdAsync(userId);
await IdentityUserManager.AddClaimAsync(user, new Claim("test", "test"));
await IdentityUserManager.AddLoginAsync(user, new UserLoginInfo("test", "test", "test"));
await IdentityUserManager.AddToRoleAsync(user, "moderator");
user.SetToken("test", "test", "test");
user.AddPasswordHistory("test");
user.AddPasskey([1, 2, 3], new IdentityPasskeyData());
await IdentityUserManager.AddToOrganizationUnitAsync(
user,
await OrganizationUnitRepository.GetAsync(LookupNormalizer.NormalizeName("OU11")));
await IdentityLinkUserManager.LinkAsync(
new IdentityLinkUserInfo(userId),
new IdentityLinkUserInfo(linkedUserId));
await IdentitySessionRepository.InsertAsync(new IdentitySession(
Guid.NewGuid(), $"session-{userId:N}", "Web", "Chrome", userId, null, "MyApp", "127.0.0.1", DateTime.UtcNow));
await IdentityUserDelegationRepository.InsertAsync(new IdentityUserDelegation(
Guid.NewGuid(), userId, linkedUserId, DateTime.UtcNow, DateTime.UtcNow.AddDays(1)));
await IdentityUserDelegationRepository.InsertAsync(new IdentityUserDelegation(
Guid.NewGuid(), linkedUserId, userId, DateTime.UtcNow, DateTime.UtcNow.AddDays(1)));
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
var user = await IdentityUserManager.GetByIdAsync(userId);
user.Claims.Count.ShouldBeGreaterThan(0);
user.Logins.Count.ShouldBeGreaterThan(0);
user.Roles.Count.ShouldBeGreaterThan(0);
user.Tokens.Count.ShouldBeGreaterThan(0);
user.OrganizationUnits.Count.ShouldBeGreaterThan(0);
user.PasswordHistories.Count.ShouldBeGreaterThan(0);
user.Passkeys.Count.ShouldBeGreaterThan(0);
(await IdentityUserManager.DeleteAsync(user)).CheckErrors();
await uow.CompleteAsync();
}
//The user is soft deleted, disable the filter to see what is left behind.
using (var uow = UnitOfWorkManager.Begin())
using (DataFilter.Disable<ISoftDelete>())
{
var deletedUser = await IdentityUserRepository.FindAsync(userId);
deletedUser.ShouldNotBeNull();
deletedUser.Claims.Count.ShouldBe(0);
deletedUser.Logins.Count.ShouldBe(0);
deletedUser.Roles.Count.ShouldBe(0);
deletedUser.Tokens.Count.ShouldBe(0);
deletedUser.OrganizationUnits.Count.ShouldBe(0);
deletedUser.PasswordHistories.Count.ShouldBe(0);
deletedUser.Passkeys.Count.ShouldBe(0);
(await IdentityLinkUserManager.IsLinkedAsync(
new IdentityLinkUserInfo(userId),
new IdentityLinkUserInfo(linkedUserId))).ShouldBeFalse();
(await IdentitySessionRepository.GetCountAsync(userId: userId)).ShouldBe(0);
(await IdentityUserDelegationRepository.GetListAsync(sourceUserId: userId, targetUserId: null)).ShouldBeEmpty();
(await IdentityUserDelegationRepository.GetListAsync(sourceUserId: null, targetUserId: userId)).ShouldBeEmpty();
await uow.CompleteAsync();
}
}
[Fact]
public virtual async Task DeleteAsync_Should_Remove_Related_Data_Of_A_User_Loaded_Without_Details()
{
var userId = Guid.NewGuid();
var userName = $"no-details-{userId:N}";
using (var uow = UnitOfWorkManager.Begin())
{
(await IdentityUserManager.CreateAsync(
new IdentityUser(userId, userName, $"{userName}@abp.io"))).CheckErrors();
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
var user = await IdentityUserManager.GetByIdAsync(userId);
(await IdentityUserManager.AddClaimAsync(user, new Claim("test", "test"))).CheckErrors();
(await IdentityUserManager.AddLoginAsync(user, new UserLoginInfo("test", "test", "test"))).CheckErrors();
(await IdentityUserManager.AddToRoleAsync(user, "moderator")).CheckErrors();
user.SetToken("test", "test", "test");
user.AddPasswordHistory("test");
user.AddPasskey([1, 2, 3], new IdentityPasskeyData());
await IdentityUserManager.AddToOrganizationUnitAsync(
user,
await OrganizationUnitRepository.GetAsync(LookupNormalizer.NormalizeName("OU11")));
await IdentityUserRepository.UpdateAsync(user);
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
var user = await IdentityUserRepository.FindByNormalizedUserNameAsync(
LookupNormalizer.NormalizeName(userName),
includeDetails: false);
(await IdentityUserManager.DeleteAsync(user)).CheckErrors();
await uow.CompleteAsync();
}
//Every collection of the user has to be loaded before it is cleared.
using (var uow = UnitOfWorkManager.Begin())
using (DataFilter.Disable<ISoftDelete>())
{
var deletedUser = await IdentityUserRepository.FindAsync(userId);
deletedUser.ShouldNotBeNull();
deletedUser.Claims.Count.ShouldBe(0);
deletedUser.Logins.Count.ShouldBe(0);
deletedUser.Roles.Count.ShouldBe(0);
deletedUser.Tokens.Count.ShouldBe(0);
deletedUser.OrganizationUnits.Count.ShouldBe(0);
deletedUser.PasswordHistories.Count.ShouldBe(0);
deletedUser.Passkeys.Count.ShouldBe(0);
await uow.CompleteAsync();
}
}
[Fact]
public virtual async Task Deleting_A_User_Through_The_Repository_Should_Remove_Sessions_Delegations_And_Links()
{
var userId = Guid.NewGuid();
var linkedUserId = Guid.NewGuid();
using (var uow = UnitOfWorkManager.Begin())
{
(await IdentityUserManager.CreateAsync(
new IdentityUser(userId, $"repo-{userId:N}", $"repo-{userId:N}@abp.io"))).CheckErrors();
(await IdentityUserManager.CreateAsync(
new IdentityUser(linkedUserId, $"repo-linked-{linkedUserId:N}", $"repo-linked-{linkedUserId:N}@abp.io"))).CheckErrors();
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
await IdentityLinkUserManager.LinkAsync(
new IdentityLinkUserInfo(userId),
new IdentityLinkUserInfo(linkedUserId));
await IdentitySessionRepository.InsertAsync(new IdentitySession(
Guid.NewGuid(), $"repo-session-{userId:N}", "Web", "Chrome", userId, null, "MyApp", "127.0.0.1", DateTime.UtcNow));
await IdentityUserDelegationRepository.InsertAsync(new IdentityUserDelegation(
Guid.NewGuid(), userId, linkedUserId, DateTime.UtcNow, DateTime.UtcNow.AddDays(1)));
await uow.CompleteAsync();
}
//Custom code may delete the user without using IdentityUserManager.
using (var uow = UnitOfWorkManager.Begin())
{
await IdentityUserRepository.DeleteAsync(await IdentityUserRepository.GetAsync(userId));
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
(await IdentitySessionRepository.GetCountAsync(userId: userId)).ShouldBe(0);
(await IdentityUserDelegationRepository.GetListAsync(sourceUserId: userId, targetUserId: null)).ShouldBeEmpty();
(await IdentityLinkUserManager.IsLinkedAsync(
new IdentityLinkUserInfo(userId),
new IdentityLinkUserInfo(linkedUserId))).ShouldBeFalse();
await uow.CompleteAsync();
}
}
[Fact]
public virtual async Task Should_Delete_A_User_That_Does_Not_Pass_The_User_Validators()
{
var userId = Guid.NewGuid();
//Insert with the repository, so the user name is not validated.
using (var uow = UnitOfWorkManager.Begin())
{
var user = new IdentityUser(userId, $"invalid user name {userId:N}", $"invalid-{userId:N}@abp.io");
user.AddPasswordHistory("test");
await IdentityUserRepository.InsertAsync(user);
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
(await IdentityUserManager.DeleteAsync(await IdentityUserRepository.GetAsync(userId))).CheckErrors();
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
using (DataFilter.Disable<ISoftDelete>())
{
var deletedUser = await IdentityUserRepository.FindAsync(userId);
deletedUser.IsDeleted.ShouldBeTrue();
deletedUser.PasswordHistories.Count.ShouldBe(0);
await uow.CompleteAsync();
}
}
[Fact]
public virtual async Task Should_Remove_The_Related_Data_Of_A_Tenant_User_When_The_Unit_Of_Work_Completes_In_The_Host()
{
var tenantId = Guid.NewGuid();
var userId = Guid.NewGuid();
using (var uow = UnitOfWorkManager.Begin())
using (CurrentTenant.Change(tenantId))
{
await IdentityUserRepository.InsertAsync(
new IdentityUser(userId, $"tenant-{userId:N}", $"tenant-{userId:N}@abp.io", tenantId));
await IdentitySessionRepository.InsertAsync(new IdentitySession(
Guid.NewGuid(), $"tenant-session-{userId:N}", "Web", "Chrome", userId, tenantId, "MyApp", "127.0.0.1", DateTime.UtcNow));
await IdentityUserDelegationRepository.InsertAsync(new IdentityUserDelegation(
Guid.NewGuid(), userId, Guid.NewGuid(), DateTime.UtcNow, DateTime.UtcNow.AddDays(1), tenantId));
await uow.CompleteAsync();
}
//The event is published while the unit of work completes, the current tenant is the host then.
using (var uow = UnitOfWorkManager.Begin())
{
using (CurrentTenant.Change(tenantId))
{
await IdentityUserRepository.DeleteAsync(await IdentityUserRepository.GetAsync(userId));
}
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
using (CurrentTenant.Change(tenantId))
{
(await IdentitySessionRepository.GetCountAsync(userId: userId)).ShouldBe(0);
(await IdentityUserDelegationRepository.GetListAsync(sourceUserId: userId, targetUserId: null)).ShouldBeEmpty();
await uow.CompleteAsync();
}
}
[Fact]
public virtual async Task Should_Remove_The_Dynamic_Claims_Cache_Of_A_Deleted_User()
{
var userId = Guid.NewGuid();
var cacheKey = AbpDynamicClaimCacheItem.CalculateCacheKey(userId, null);
using (var uow = UnitOfWorkManager.Begin())
{
(await IdentityUserManager.CreateAsync(
new IdentityUser(userId, $"claims-cache-{userId:N}", $"claims-cache-{userId:N}@abp.io"))).CheckErrors();
await uow.CompleteAsync();
}
await DynamicClaimCache.SetAsync(cacheKey, new AbpDynamicClaimCacheItem());
(await DynamicClaimCache.GetAsync(cacheKey)).ShouldNotBeNull();
using (var uow = UnitOfWorkManager.Begin())
{
(await IdentityUserManager.DeleteAsync(await IdentityUserRepository.GetAsync(userId))).CheckErrors();
await uow.CompleteAsync();
}
(await DynamicClaimCache.GetAsync(cacheKey)).ShouldBeNull();
}
[Fact]
public virtual async Task Deleting_A_Stale_User_Should_Throw_A_Concurrency_Exception()
{
var userId = Guid.NewGuid();
IdentityUser staleUser;
using (var uow = UnitOfWorkManager.Begin())
{
(await IdentityUserManager.CreateAsync(
new IdentityUser(userId, $"stale-{userId:N}", $"stale-{userId:N}@abp.io"))).CheckErrors();
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
staleUser = await IdentityUserRepository.GetAsync(userId);
await uow.CompleteAsync();
}
//Change the user, so the instance loaded above has an old concurrency stamp.
using (var uow = UnitOfWorkManager.Begin())
{
var user = await IdentityUserRepository.GetAsync(userId);
user.Name = "Changed";
await IdentityUserRepository.UpdateAsync(user);
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
await Should.ThrowAsync<AbpIdentityResultException>(
async () => await IdentityUserManager.DeleteAsync(staleUser));
}
}
[Fact]
public virtual async Task Deleting_A_User_Through_The_Repository_Should_Not_Clear_Its_Own_Collections()
{
var userId = Guid.NewGuid();
using (var uow = UnitOfWorkManager.Begin())
{
(await IdentityUserManager.CreateAsync(
new IdentityUser(userId, $"repo-collections-{userId:N}", $"repo-collections-{userId:N}@abp.io"))).CheckErrors();
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
var user = await IdentityUserManager.GetByIdAsync(userId);
(await IdentityUserManager.AddToRoleAsync(user, "moderator")).CheckErrors();
user.AddPasswordHistory("test");
await IdentityUserRepository.UpdateAsync(user);
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
await IdentityUserRepository.DeleteAsync(await IdentityUserRepository.GetAsync(userId));
await uow.CompleteAsync();
}
//UserDeletedEventHandler only covers the aggregates that have no navigation from the user,
//the collections of the user itself are cleared by IdentityUserManager.DeleteAsync.
using (var uow = UnitOfWorkManager.Begin())
using (DataFilter.Disable<ISoftDelete>())
{
var deletedUser = await IdentityUserRepository.FindAsync(userId);
deletedUser.Roles.Count.ShouldBe(1);
deletedUser.PasswordHistories.Count.ShouldBe(1);
await uow.CompleteAsync();
}
}
[Fact]
public virtual async Task Deleting_A_Stale_User_Should_Not_Delete_Its_Link_Users()
{
var userId = Guid.NewGuid();
var linkedUserId = Guid.NewGuid();
IdentityUser staleUser;
using (var uow = UnitOfWorkManager.Begin())
{
(await IdentityUserManager.CreateAsync(
new IdentityUser(userId, $"stale-link-{userId:N}", $"stale-link-{userId:N}@abp.io"))).CheckErrors();
(await IdentityUserManager.CreateAsync(
new IdentityUser(linkedUserId, $"stale-linked-{linkedUserId:N}", $"stale-linked-{linkedUserId:N}@abp.io"))).CheckErrors();
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
await IdentityLinkUserManager.LinkAsync(
new IdentityLinkUserInfo(userId),
new IdentityLinkUserInfo(linkedUserId));
staleUser = await IdentityUserRepository.GetAsync(userId);
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
var user = await IdentityUserRepository.GetAsync(userId);
user.Name = "Changed";
await IdentityUserRepository.UpdateAsync(user);
await uow.CompleteAsync();
}
using (var uow = UnitOfWorkManager.Begin())
{
await Should.ThrowAsync<AbpIdentityResultException>(
async () => await IdentityUserManager.DeleteAsync(staleUser));
}
//The user is still there, so its link users must be there as well.
using (var uow = UnitOfWorkManager.Begin())
{
(await IdentityLinkUserManager.IsLinkedAsync(
new IdentityLinkUserInfo(userId),
new IdentityLinkUserInfo(linkedUserId))).ShouldBeTrue();
await uow.CompleteAsync();
}
}
}

54
modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitManager_CreateMany_Tests.cs

@ -0,0 +1,54 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Shouldly;
using Volo.Abp.Guids;
using Volo.Abp.Modularity;
using Volo.Abp.Uow;
using Xunit;
namespace Volo.Abp.Identity;
public abstract class OrganizationUnitManager_CreateMany_Tests<TStartupModule> : AbpIdentityTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
protected OrganizationUnitManager OrganizationUnitManager { get; }
protected IOrganizationUnitRepository OrganizationUnitRepository { get; }
protected ILookupNormalizer LookupNormalizer { get; }
protected IUnitOfWorkManager UnitOfWorkManager { get; }
protected IGuidGenerator GuidGenerator { get; }
protected OrganizationUnitManager_CreateMany_Tests()
{
OrganizationUnitManager = GetRequiredService<OrganizationUnitManager>();
OrganizationUnitRepository = GetRequiredService<IOrganizationUnitRepository>();
LookupNormalizer = GetRequiredService<ILookupNormalizer>();
UnitOfWorkManager = GetRequiredService<IUnitOfWorkManager>();
GuidGenerator = GetRequiredService<IGuidGenerator>();
}
[Fact]
public virtual async Task Should_Not_Insert_Anything_When_A_Group_Is_Not_Valid()
{
var validDisplayName = $"valid-{Guid.NewGuid():N}";
using (var uow = UnitOfWorkManager.Begin())
{
var parent = await OrganizationUnitRepository.GetAsync("OU1");
//The second group is not valid, OU11 is already a child of OU1.
await Should.ThrowAsync<BusinessException>(async () =>
await OrganizationUnitManager.CreateManyAsync([
new OrganizationUnit(GuidGenerator.Create(), validDisplayName),
new OrganizationUnit(GuidGenerator.Create(), "OU11", parent.Id)
]));
}
using (var uow = UnitOfWorkManager.Begin())
{
(await OrganizationUnitRepository.GetAsync(validDisplayName)).ShouldBeNull();
await uow.CompleteAsync();
}
}
}
Loading…
Cancel
Save