mirror of https://github.com/abpframework/abp.git
committed by
GitHub
10 changed files with 674 additions and 232 deletions
@ -0,0 +1,7 @@ |
|||||
|
using Volo.Abp.Identity.EntityFrameworkCore; |
||||
|
|
||||
|
namespace Volo.Abp.Identity; |
||||
|
|
||||
|
public class IdentityUserManager_SharedUser_SeparateDatabase_Tests : IdentityUserManager_SharedUser_SeparateDatabase_Tests<AbpIdentitySharedUserSeparateDbEntityFrameworkCoreTestModule> |
||||
|
{ |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
namespace Volo.Abp.Identity; |
||||
|
|
||||
|
public class IdentityUserManager_SharedUser_Tests : IdentityUserManager_SharedUser_Tests<AbpIdentityDomainTestModule> |
||||
|
{ |
||||
|
} |
||||
@ -0,0 +1,123 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Microsoft.EntityFrameworkCore; |
||||
|
using Microsoft.EntityFrameworkCore.Infrastructure; |
||||
|
using Microsoft.EntityFrameworkCore.Storage; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.EntityFrameworkCore; |
||||
|
using Volo.Abp.EntityFrameworkCore.Sqlite; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
using Volo.Abp.MultiTenancy.ConfigurationStore; |
||||
|
using Volo.Abp.PermissionManagement.EntityFrameworkCore; |
||||
|
using Volo.Abp.Uow; |
||||
|
|
||||
|
namespace Volo.Abp.Identity.EntityFrameworkCore; |
||||
|
|
||||
|
// EF/SQLite equivalent of the MongoDB separate-database test module: each predefined tenant
|
||||
|
// has its own keep-alive in-memory SQLite connection. Each test method (and therefore each
|
||||
|
// AbpApplication) gets a unique connection-string suffix so the test-data seeder runs into
|
||||
|
// fresh databases instead of duplicating into shared cache.
|
||||
|
[DependsOn( |
||||
|
typeof(AbpIdentityTestBaseModule), |
||||
|
typeof(AbpPermissionManagementEntityFrameworkCoreModule), |
||||
|
typeof(AbpIdentityEntityFrameworkCoreModule), |
||||
|
typeof(AbpEntityFrameworkCoreSqliteModule))] |
||||
|
public class AbpIdentitySharedUserSeparateDbEntityFrameworkCoreTestModule : AbpModule |
||||
|
{ |
||||
|
public static readonly Guid TenantAId = IdentitySharedUserSeparateDbConstants.TenantAId; |
||||
|
public static readonly Guid TenantBId = IdentitySharedUserSeparateDbConstants.TenantBId; |
||||
|
|
||||
|
// Per-app keep-alive connections so the in-memory SQLite databases survive for the test's
|
||||
|
// lifetime (without an open connection, shared-cache in-memory databases are discarded).
|
||||
|
// Uses AbpUnitTestSqliteConnection (SemaphoreSlim around CreateCommand) — SQLite isn't
|
||||
|
// thread-safe and parallel xUnit collections would otherwise race. Disposed in
|
||||
|
// OnApplicationShutdown so connections do not accumulate across tests.
|
||||
|
private readonly List<AbpUnitTestSqliteConnection> _keepAlive = new(); |
||||
|
|
||||
|
public override void PreConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
PreConfigure<AbpSqliteOptions>(x => x.BusyTimeout = null); |
||||
|
} |
||||
|
|
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
// Unique-per-app suffix so each test method gets a fresh trio of databases (the seeder
|
||||
|
// in AbpIdentityTestBaseModule.OnApplicationInitialization expects to write into empty
|
||||
|
// tables, which would fail if test methods reused the same shared-cache database).
|
||||
|
var suffix = Guid.NewGuid().ToString("N"); |
||||
|
var hostConnection = $"Data Source=AbpIdentity_SeparateDb_Host_{suffix};Mode=Memory;Cache=Shared"; |
||||
|
var tenantAConnection = $"Data Source=AbpIdentity_SeparateDb_TenantA_{suffix};Mode=Memory;Cache=Shared"; |
||||
|
var tenantBConnection = $"Data Source=AbpIdentity_SeparateDb_TenantB_{suffix};Mode=Memory;Cache=Shared"; |
||||
|
|
||||
|
EnsureDatabase(hostConnection); |
||||
|
EnsureDatabase(tenantAConnection); |
||||
|
EnsureDatabase(tenantBConnection); |
||||
|
|
||||
|
Configure<AbpDbConnectionOptions>(options => |
||||
|
{ |
||||
|
options.ConnectionStrings.Default = hostConnection; |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpDbContextOptions>(options => |
||||
|
{ |
||||
|
options.Configure(ctx => |
||||
|
{ |
||||
|
ctx.DbContextOptions.UseSqlite(ctx.ConnectionString); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpMultiTenancyOptions>(options => |
||||
|
{ |
||||
|
options.IsEnabled = true; |
||||
|
options.UserSharingStrategy = TenantUserSharingStrategy.Shared; |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpDefaultTenantStoreOptions>(options => |
||||
|
{ |
||||
|
options.Tenants = new[] |
||||
|
{ |
||||
|
new TenantConfiguration(TenantAId, "tenant-a") |
||||
|
{ |
||||
|
ConnectionStrings = new ConnectionStrings |
||||
|
{ |
||||
|
{ ConnectionStrings.DefaultConnectionStringName, tenantAConnection } |
||||
|
} |
||||
|
}, |
||||
|
new TenantConfiguration(TenantBId, "tenant-b") |
||||
|
{ |
||||
|
ConnectionStrings = new ConnectionStrings |
||||
|
{ |
||||
|
{ ConnectionStrings.DefaultConnectionStringName, tenantBConnection } |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
context.Services.AddAlwaysDisableUnitOfWorkTransaction(); |
||||
|
} |
||||
|
|
||||
|
public override void OnApplicationShutdown(ApplicationShutdownContext context) |
||||
|
{ |
||||
|
foreach (var connection in _keepAlive) |
||||
|
{ |
||||
|
connection.Dispose(); |
||||
|
} |
||||
|
_keepAlive.Clear(); |
||||
|
} |
||||
|
|
||||
|
private void EnsureDatabase(string connectionString) |
||||
|
{ |
||||
|
var keepAlive = new AbpUnitTestSqliteConnection(connectionString); |
||||
|
keepAlive.Open(); |
||||
|
_keepAlive.Add(keepAlive); |
||||
|
|
||||
|
new IdentityDbContext( |
||||
|
new DbContextOptionsBuilder<IdentityDbContext>().UseSqlite(connectionString).Options) |
||||
|
.GetService<IRelationalDatabaseCreator>().CreateTables(); |
||||
|
|
||||
|
new PermissionManagementDbContext( |
||||
|
new DbContextOptionsBuilder<PermissionManagementDbContext>().UseSqlite(connectionString).Options) |
||||
|
.GetService<IRelationalDatabaseCreator>().CreateTables(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
using System; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
using Volo.Abp.MultiTenancy.ConfigurationStore; |
||||
|
|
||||
|
namespace Volo.Abp.Identity.MongoDB; |
||||
|
|
||||
|
// Registers two predefined tenants, each pointing at its own physical MongoDB database.
|
||||
|
// Used by SharedUser tests that need true cross-database isolation (the only place where
|
||||
|
// AbpIdentityUserValidator's shared-mode owner lookup and FindSharedUserBy*Async deviate from
|
||||
|
// single-DB behavior).
|
||||
|
[DependsOn(typeof(AbpIdentityMongoDbTestModule))] |
||||
|
public class AbpIdentitySharedUserSeparateDbMongoDbTestModule : AbpModule |
||||
|
{ |
||||
|
public static readonly Guid TenantAId = IdentitySharedUserSeparateDbConstants.TenantAId; |
||||
|
public static readonly Guid TenantBId = IdentitySharedUserSeparateDbConstants.TenantBId; |
||||
|
|
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
// Fixed tenant db names so MongoSandbox's embedded mongod does not accumulate one tenant
|
||||
|
// db per test method. The host/default database is still configured by
|
||||
|
// AbpIdentityMongoDbTestModule (random per test run), since AbpIdentityTestBaseModule
|
||||
|
// re-seeds the host admin/role on every AbpApplication initialization. Each test method
|
||||
|
// generates unique email / userName so cross-test data does not collide.
|
||||
|
var tenantAConnection = MongoDbFixture.GetConnectionString("AbpIdentity_SharedSeparateDb_TenantA"); |
||||
|
var tenantBConnection = MongoDbFixture.GetConnectionString("AbpIdentity_SharedSeparateDb_TenantB"); |
||||
|
|
||||
|
Configure<AbpMultiTenancyOptions>(options => |
||||
|
{ |
||||
|
options.IsEnabled = true; |
||||
|
options.UserSharingStrategy = TenantUserSharingStrategy.Shared; |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpDefaultTenantStoreOptions>(options => |
||||
|
{ |
||||
|
options.Tenants = new[] |
||||
|
{ |
||||
|
new TenantConfiguration(TenantAId, "tenant-a") |
||||
|
{ |
||||
|
ConnectionStrings = new ConnectionStrings |
||||
|
{ |
||||
|
{ ConnectionStrings.DefaultConnectionStringName, tenantAConnection } |
||||
|
} |
||||
|
}, |
||||
|
new TenantConfiguration(TenantBId, "tenant-b") |
||||
|
{ |
||||
|
ConnectionStrings = new ConnectionStrings |
||||
|
{ |
||||
|
{ ConnectionStrings.DefaultConnectionStringName, tenantBConnection } |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.Identity.MongoDB; |
||||
|
|
||||
|
[Collection(MongoTestCollection.Name)] |
||||
|
public class IdentityUserManager_SharedUser_SeparateDatabase_Tests : IdentityUserManager_SharedUser_SeparateDatabase_Tests<AbpIdentitySharedUserSeparateDbMongoDbTestModule> |
||||
|
{ |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.Identity.MongoDB; |
||||
|
|
||||
|
[Collection(MongoTestCollection.Name)] |
||||
|
public class IdentityUserManager_SharedUser_Tests : IdentityUserManager_SharedUser_Tests<AbpIdentityMongoDbTestModule> |
||||
|
{ |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Volo.Abp.Identity; |
||||
|
|
||||
|
// Single source of truth for the predefined tenant ids used by the shared-user separate-database
|
||||
|
// test suite. Concrete EF/Mongo test modules and the abstract test class both reference these
|
||||
|
// constants directly so the modules don't have to type-couple back into the test class.
|
||||
|
public static class IdentitySharedUserSeparateDbConstants |
||||
|
{ |
||||
|
public static readonly Guid TenantAId = Guid.Parse("11111111-1111-1111-1111-111111111111"); |
||||
|
public static readonly Guid TenantBId = Guid.Parse("22222222-2222-2222-2222-222222222222"); |
||||
|
} |
||||
@ -0,0 +1,201 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
using Volo.Abp.Uow; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.Identity; |
||||
|
|
||||
|
// Multi-tenant separate-database isolation tests, runnable on any storage backend that lets
|
||||
|
// each predefined tenant resolve to its own physical connection. EF (SQLite per-tenant
|
||||
|
// keep-alive) and MongoDB (per-tenant database) implementations both work.
|
||||
|
//
|
||||
|
// NOTE on scope: open-source IdentityUserManager.FindSharedUserBy* assumes a single shared
|
||||
|
// database; cross-DB shared-user resolution is the Pro UserSharingManager's responsibility.
|
||||
|
// These tests therefore only assert the layer the open-source framework owns: tenant
|
||||
|
// connection routing and IMultiTenant filter behavior.
|
||||
|
//
|
||||
|
// Concrete subclasses must register two predefined tenants (TenantAId / TenantBId from this
|
||||
|
// class) each with its own connection string, and enable shared-user mode in the test module.
|
||||
|
public abstract class IdentityUserManager_SharedUser_SeparateDatabase_Tests<TStartupModule> : AbpIdentityTestBase<TStartupModule> |
||||
|
where TStartupModule : IAbpModule |
||||
|
{ |
||||
|
public static readonly Guid TenantAId = IdentitySharedUserSeparateDbConstants.TenantAId; |
||||
|
public static readonly Guid TenantBId = IdentitySharedUserSeparateDbConstants.TenantBId; |
||||
|
|
||||
|
protected IdentityUserManager IdentityUserManager { get; } |
||||
|
protected IIdentityUserRepository IdentityUserRepository { get; } |
||||
|
protected ICurrentTenant CurrentTenant { get; } |
||||
|
protected IUnitOfWorkManager UnitOfWorkManager { get; } |
||||
|
protected IDataFilter DataFilter { get; } |
||||
|
|
||||
|
protected IdentityUserManager_SharedUser_SeparateDatabase_Tests() |
||||
|
{ |
||||
|
IdentityUserManager = GetRequiredService<IdentityUserManager>(); |
||||
|
IdentityUserRepository = GetRequiredService<IIdentityUserRepository>(); |
||||
|
CurrentTenant = GetRequiredService<ICurrentTenant>(); |
||||
|
UnitOfWorkManager = GetRequiredService<IUnitOfWorkManager>(); |
||||
|
DataFilter = GetRequiredService<IDataFilter>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public virtual async Task Tenant_Connection_Should_Not_See_Host_Rows() |
||||
|
{ |
||||
|
// Disables IMultiTenant before querying so this test fails if connection routing is
|
||||
|
// broken (a tenant context unexpectedly hitting the host db) rather than being masked
|
||||
|
// by the data filter.
|
||||
|
var probeEmail = $"infra-host-{Guid.NewGuid():N}@abp.io"; |
||||
|
Guid hostUserId; |
||||
|
|
||||
|
using (CurrentTenant.Change(null)) |
||||
|
using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) |
||||
|
{ |
||||
|
var hostUser = new IdentityUser(Guid.NewGuid(), $"infra-host-{Guid.NewGuid():N}", probeEmail, null); |
||||
|
await IdentityUserRepository.InsertAsync(hostUser); |
||||
|
hostUserId = hostUser.Id; |
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
|
||||
|
using (CurrentTenant.Change(TenantAId)) |
||||
|
using (DataFilter.Disable<IMultiTenant>()) |
||||
|
using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) |
||||
|
{ |
||||
|
(await IdentityUserRepository.GetListAsync()).ShouldNotContain(u => u.Id == hostUserId); |
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
|
||||
|
using (CurrentTenant.Change(TenantAId)) |
||||
|
{ |
||||
|
(await IdentityUserManager.FindByEmailAsync(probeEmail)).ShouldBeNull(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public virtual async Task Host_Connection_Should_Not_See_Tenant_Rows() |
||||
|
{ |
||||
|
var probeEmail = $"infra-tenant-{Guid.NewGuid():N}@abp.io"; |
||||
|
Guid tenantUserId; |
||||
|
|
||||
|
using (CurrentTenant.Change(TenantAId)) |
||||
|
using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) |
||||
|
{ |
||||
|
var tenantUser = new IdentityUser(Guid.NewGuid(), $"infra-t-{Guid.NewGuid():N}", probeEmail, TenantAId); |
||||
|
await IdentityUserRepository.InsertAsync(tenantUser); |
||||
|
tenantUserId = tenantUser.Id; |
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
|
||||
|
using (CurrentTenant.Change(null)) |
||||
|
using (DataFilter.Disable<IMultiTenant>()) |
||||
|
using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) |
||||
|
{ |
||||
|
(await IdentityUserRepository.GetListAsync()).ShouldNotContain(u => u.Id == tenantUserId); |
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
|
||||
|
using (CurrentTenant.Change(null)) |
||||
|
{ |
||||
|
(await IdentityUserManager.FindByEmailAsync(probeEmail)).ShouldBeNull(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public virtual async Task TenantA_Connection_Should_Not_See_TenantB_Rows() |
||||
|
{ |
||||
|
var probeEmail = $"infra-cross-{Guid.NewGuid():N}@abp.io"; |
||||
|
Guid tenantBUserId; |
||||
|
|
||||
|
using (CurrentTenant.Change(TenantBId)) |
||||
|
using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) |
||||
|
{ |
||||
|
var u = new IdentityUser(Guid.NewGuid(), $"b-{Guid.NewGuid():N}", probeEmail, TenantBId); |
||||
|
await IdentityUserRepository.InsertAsync(u); |
||||
|
tenantBUserId = u.Id; |
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
|
||||
|
using (CurrentTenant.Change(TenantAId)) |
||||
|
using (DataFilter.Disable<IMultiTenant>()) |
||||
|
using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) |
||||
|
{ |
||||
|
(await IdentityUserRepository.GetListAsync()).ShouldNotContain(u => u.Id == tenantBUserId); |
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
|
||||
|
using (CurrentTenant.Change(TenantAId)) |
||||
|
{ |
||||
|
(await IdentityUserManager.FindByEmailAsync(probeEmail)).ShouldBeNull(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public virtual async Task Different_Tenants_Should_Allow_Same_Email_With_Their_Own_Rows() |
||||
|
{ |
||||
|
var email = $"same-email-{Guid.NewGuid():N}@abp.io"; |
||||
|
var nameA = $"a-{Guid.NewGuid():N}"; |
||||
|
var nameB = $"b-{Guid.NewGuid():N}"; |
||||
|
|
||||
|
using (CurrentTenant.Change(TenantAId)) |
||||
|
using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) |
||||
|
{ |
||||
|
await IdentityUserRepository.InsertAsync(new IdentityUser(Guid.NewGuid(), nameA, email, TenantAId)); |
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
using (CurrentTenant.Change(TenantBId)) |
||||
|
using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) |
||||
|
{ |
||||
|
await IdentityUserRepository.InsertAsync(new IdentityUser(Guid.NewGuid(), nameB, email, TenantBId)); |
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
|
||||
|
using (CurrentTenant.Change(TenantAId)) |
||||
|
{ |
||||
|
var userA = await IdentityUserManager.FindByEmailAsync(email); |
||||
|
userA.ShouldNotBeNull(); |
||||
|
userA.UserName.ShouldBe(nameA); |
||||
|
} |
||||
|
using (CurrentTenant.Change(TenantBId)) |
||||
|
{ |
||||
|
var userB = await IdentityUserManager.FindByEmailAsync(email); |
||||
|
userB.ShouldNotBeNull(); |
||||
|
userB.UserName.ShouldBe(nameB); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public virtual async Task Different_Tenants_Should_Allow_Same_UserName_With_Their_Own_Rows() |
||||
|
{ |
||||
|
var userName = $"same-name-{Guid.NewGuid():N}"; |
||||
|
var emailA = $"a-{Guid.NewGuid():N}@abp.io"; |
||||
|
var emailB = $"b-{Guid.NewGuid():N}@abp.io"; |
||||
|
|
||||
|
using (CurrentTenant.Change(TenantAId)) |
||||
|
using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) |
||||
|
{ |
||||
|
await IdentityUserRepository.InsertAsync(new IdentityUser(Guid.NewGuid(), userName, emailA, TenantAId)); |
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
using (CurrentTenant.Change(TenantBId)) |
||||
|
using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) |
||||
|
{ |
||||
|
await IdentityUserRepository.InsertAsync(new IdentityUser(Guid.NewGuid(), userName, emailB, TenantBId)); |
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
|
||||
|
using (CurrentTenant.Change(TenantAId)) |
||||
|
{ |
||||
|
var userInTenantA = await IdentityUserManager.FindByNameAsync(userName); |
||||
|
userInTenantA.ShouldNotBeNull(); |
||||
|
userInTenantA.Email.ShouldBe(emailA); |
||||
|
} |
||||
|
using (CurrentTenant.Change(TenantBId)) |
||||
|
{ |
||||
|
var userInTenantB = await IdentityUserManager.FindByNameAsync(userName); |
||||
|
userInTenantB.ShouldNotBeNull(); |
||||
|
userInTenantB.Email.ShouldBe(emailB); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,254 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
using Volo.Abp.Uow; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.Identity; |
||||
|
|
||||
|
// Abstract test suite for IdentityUserManager.FindSharedUserBy*Async under
|
||||
|
// TenantUserSharingStrategy.Shared. Concrete subclasses in Domain.Tests (EF) and
|
||||
|
// MongoDB.Tests pick the storage backend by passing the corresponding TStartupModule.
|
||||
|
public abstract class IdentityUserManager_SharedUser_Tests<TStartupModule> : AbpIdentityTestBase<TStartupModule> |
||||
|
where TStartupModule : IAbpModule |
||||
|
{ |
||||
|
protected IdentityUserManager IdentityUserManager { get; } |
||||
|
protected IIdentityUserRepository IdentityUserRepository { get; } |
||||
|
protected ICurrentTenant CurrentTenant { get; } |
||||
|
protected IUnitOfWorkManager UnitOfWorkManager { get; } |
||||
|
|
||||
|
protected IdentityUserManager_SharedUser_Tests() |
||||
|
{ |
||||
|
IdentityUserManager = GetRequiredService<IdentityUserManager>(); |
||||
|
IdentityUserRepository = GetRequiredService<IIdentityUserRepository>(); |
||||
|
CurrentTenant = GetRequiredService<ICurrentTenant>(); |
||||
|
UnitOfWorkManager = GetRequiredService<IUnitOfWorkManager>(); |
||||
|
} |
||||
|
|
||||
|
protected override void AfterAddApplication(IServiceCollection services) |
||||
|
{ |
||||
|
services.Configure<AbpMultiTenancyOptions>(options => |
||||
|
{ |
||||
|
options.IsEnabled = true; |
||||
|
options.UserSharingStrategy = TenantUserSharingStrategy.Shared; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public virtual async Task FindSharedUserByEmailAsync_Should_Return_Host_User() |
||||
|
{ |
||||
|
var tenantId = Guid.NewGuid(); |
||||
|
var email = $"shared-email-{Guid.NewGuid():N}@abp.io"; |
||||
|
|
||||
|
using (var uow = UnitOfWorkManager.Begin()) |
||||
|
{ |
||||
|
await CreateUserAsync(null, $"shared-host-email-{Guid.NewGuid():N}", email); |
||||
|
await CreateUserAsync(tenantId, $"shared-tenant-email-{Guid.NewGuid():N}", email); |
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
|
||||
|
using (CurrentTenant.Change(tenantId)) |
||||
|
{ |
||||
|
var user = await IdentityUserManager.FindSharedUserByEmailAsync(email); |
||||
|
user.ShouldNotBeNull(); |
||||
|
user.TenantId.ShouldBeNull(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public virtual async Task FindSharedUserByEmailAsync_Should_Find_Tenant_User_When_No_Host_User() |
||||
|
{ |
||||
|
var tenantId = Guid.NewGuid(); |
||||
|
var email = $"shared-tenant-only-{Guid.NewGuid():N}@abp.io"; |
||||
|
|
||||
|
using (var uow = UnitOfWorkManager.Begin()) |
||||
|
{ |
||||
|
await CreateUserAsync(tenantId, $"shared-tenant-only-{Guid.NewGuid():N}", email); |
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
|
||||
|
using (CurrentTenant.Change(null)) |
||||
|
{ |
||||
|
var user = await IdentityUserManager.FindSharedUserByEmailAsync(email); |
||||
|
user.ShouldNotBeNull(); |
||||
|
user.TenantId.ShouldBe(tenantId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public virtual async Task FindSharedUserByEmailAsync_Should_Return_Null_For_Unknown_Email() |
||||
|
{ |
||||
|
using (CurrentTenant.Change(null)) |
||||
|
{ |
||||
|
var user = await IdentityUserManager.FindSharedUserByEmailAsync($"missing-{Guid.NewGuid():N}@abp.io"); |
||||
|
user.ShouldBeNull(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public virtual async Task FindSharedUserByNameAsync_Should_Return_Host_User() |
||||
|
{ |
||||
|
var tenantId = Guid.NewGuid(); |
||||
|
var userName = $"shared-name-{Guid.NewGuid():N}"; |
||||
|
|
||||
|
using (var uow = UnitOfWorkManager.Begin()) |
||||
|
{ |
||||
|
await CreateUserAsync(null, userName, $"host-{Guid.NewGuid():N}@abp.io"); |
||||
|
await CreateUserAsync(tenantId, userName, $"tenant-{Guid.NewGuid():N}@abp.io"); |
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
|
||||
|
using (CurrentTenant.Change(tenantId)) |
||||
|
{ |
||||
|
var user = await IdentityUserManager.FindSharedUserByNameAsync(userName); |
||||
|
user.ShouldNotBeNull(); |
||||
|
user.TenantId.ShouldBeNull(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public virtual async Task FindSharedUserByLoginAsync_Should_Return_Host_User() |
||||
|
{ |
||||
|
var tenantId = Guid.NewGuid(); |
||||
|
var loginProvider = "github"; |
||||
|
var providerKey = $"shared-login-{Guid.NewGuid():N}"; |
||||
|
|
||||
|
using (var uow = UnitOfWorkManager.Begin()) |
||||
|
{ |
||||
|
await CreateUserAsync(null, $"host-login-{Guid.NewGuid():N}", $"host-login-{Guid.NewGuid():N}@abp.io", |
||||
|
u => u.AddLogin(new UserLoginInfo(loginProvider, providerKey, "Shared Login"))); |
||||
|
await CreateUserAsync(tenantId, $"tenant-login-{Guid.NewGuid():N}", $"tenant-login-{Guid.NewGuid():N}@abp.io", |
||||
|
u => u.AddLogin(new UserLoginInfo(loginProvider, providerKey, "Shared Login"))); |
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
|
||||
|
using (CurrentTenant.Change(tenantId)) |
||||
|
{ |
||||
|
var user = await IdentityUserManager.FindSharedUserByLoginAsync(loginProvider, providerKey); |
||||
|
user.ShouldNotBeNull(); |
||||
|
user.TenantId.ShouldBeNull(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public virtual async Task FindSharedUserByPasskeyIdAsync_Should_Return_Host_User() |
||||
|
{ |
||||
|
var tenantId = Guid.NewGuid(); |
||||
|
var credentialId = Guid.NewGuid().ToByteArray(); |
||||
|
|
||||
|
using (var uow = UnitOfWorkManager.Begin()) |
||||
|
{ |
||||
|
await CreateUserAsync(null, $"shared-host-passkey-{Guid.NewGuid():N}", $"shared-host-passkey-{Guid.NewGuid():N}@abp.io", |
||||
|
u => u.AddPasskey(credentialId, new IdentityPasskeyData())); |
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
|
||||
|
using (CurrentTenant.Change(tenantId)) |
||||
|
{ |
||||
|
var user = await IdentityUserManager.FindSharedUserByPasskeyIdAsync(credentialId); |
||||
|
user.ShouldNotBeNull(); |
||||
|
user.TenantId.ShouldBeNull(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public virtual async Task FindSharedUserByIdAsync_Should_Find_Tenant_User_From_Host_Context() |
||||
|
{ |
||||
|
// Core 2FA shared-mode bug condition: a tenant-only user must be reachable by id from
|
||||
|
// a host context (CurrentTenant=null). The IMultiTenant filter would otherwise hide it.
|
||||
|
var tenantId = Guid.NewGuid(); |
||||
|
IdentityUser tenantUser; |
||||
|
|
||||
|
using (var uow = UnitOfWorkManager.Begin()) |
||||
|
{ |
||||
|
tenantUser = await CreateUserAsync(tenantId, $"shared-id-tenant-{Guid.NewGuid():N}", $"shared-id-tenant-{Guid.NewGuid():N}@abp.io"); |
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
|
||||
|
using (CurrentTenant.Change(null)) |
||||
|
{ |
||||
|
var user = await IdentityUserManager.FindSharedUserByIdAsync(tenantUser.Id.ToString()); |
||||
|
user.ShouldNotBeNull(); |
||||
|
user.Id.ShouldBe(tenantUser.Id); |
||||
|
user.TenantId.ShouldBe(tenantId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public virtual async Task FindSharedUserByIdAsync_Should_Find_Host_User_From_Tenant_Context() |
||||
|
{ |
||||
|
var tenantId = Guid.NewGuid(); |
||||
|
IdentityUser hostUser; |
||||
|
|
||||
|
using (var uow = UnitOfWorkManager.Begin()) |
||||
|
{ |
||||
|
hostUser = await CreateUserAsync(null, $"shared-id-host-{Guid.NewGuid():N}", $"shared-id-host-{Guid.NewGuid():N}@abp.io"); |
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
|
||||
|
using (CurrentTenant.Change(tenantId)) |
||||
|
{ |
||||
|
var user = await IdentityUserManager.FindSharedUserByIdAsync(hostUser.Id.ToString()); |
||||
|
user.ShouldNotBeNull(); |
||||
|
user.TenantId.ShouldBeNull(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public virtual async Task FindSharedUserByIdAsync_Should_Return_Null_For_Unknown_Id() |
||||
|
{ |
||||
|
using (CurrentTenant.Change(null)) |
||||
|
{ |
||||
|
var user = await IdentityUserManager.FindSharedUserByIdAsync(Guid.NewGuid().ToString()); |
||||
|
user.ShouldBeNull(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public virtual async Task Login_Then_TwoFactor_MidFlow_Should_Resolve_Same_Tenant_User() |
||||
|
{ |
||||
|
// End-to-end shape of the 2FA shared-mode regression: a host-context lookup-by-name
|
||||
|
// followed by lookup-by-id must return the same tenant row both times.
|
||||
|
var tenantId = Guid.NewGuid(); |
||||
|
var userName = $"shared-2fa-{Guid.NewGuid():N}"; |
||||
|
|
||||
|
using (var uow = UnitOfWorkManager.Begin()) |
||||
|
{ |
||||
|
await CreateUserAsync(tenantId, userName, $"{userName}@abp.io"); |
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
|
||||
|
using (CurrentTenant.Change(null)) |
||||
|
{ |
||||
|
var loginUser = await IdentityUserManager.FindSharedUserByNameAsync(userName); |
||||
|
loginUser.ShouldNotBeNull(); |
||||
|
loginUser.TenantId.ShouldBe(tenantId); |
||||
|
|
||||
|
var twoFactorUser = await IdentityUserManager.FindSharedUserByIdAsync(loginUser.Id.ToString()); |
||||
|
twoFactorUser.ShouldNotBeNull(); |
||||
|
twoFactorUser.Id.ShouldBe(loginUser.Id); |
||||
|
twoFactorUser.TenantId.ShouldBe(tenantId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected async Task<IdentityUser> CreateUserAsync( |
||||
|
Guid? tenantId, |
||||
|
string userName, |
||||
|
string email, |
||||
|
Action<IdentityUser> configureUser = null) |
||||
|
{ |
||||
|
var user = new IdentityUser(Guid.NewGuid(), userName, email, tenantId); |
||||
|
configureUser?.Invoke(user); |
||||
|
|
||||
|
using (CurrentTenant.Change(tenantId)) |
||||
|
{ |
||||
|
await IdentityUserRepository.InsertAsync(user); |
||||
|
} |
||||
|
|
||||
|
return user; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue