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