Browse Source

Merge pull request #25320 from abpframework/auto-merge/rel-10-2/4519

Merge branch rel-10.3 with rel-10.2
pull/25321/head
Volosoft Agent 1 week ago
committed by GitHub
parent
commit
d0db5a7269
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 7
      modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_SharedUser_SeparateDatabase_Tests.cs
  2. 5
      modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_SharedUser_Tests.cs
  3. 232
      modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs
  4. 123
      modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/AbpIdentitySharedUserSeparateDbEntityFrameworkCoreTestModule.cs
  5. 56
      modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/AbpIdentitySharedUserSeparateDbMongoDbTestModule.cs
  6. 8
      modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/IdentityUserManager_SharedUser_SeparateDatabase_Tests.cs
  7. 8
      modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/IdentityUserManager_SharedUser_Tests.cs
  8. 12
      modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentitySharedUserSeparateDbConstants.cs
  9. 201
      modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityUserManager_SharedUser_SeparateDatabase_Tests.cs
  10. 254
      modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityUserManager_SharedUser_Tests.cs

7
modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_SharedUser_SeparateDatabase_Tests.cs

@ -0,0 +1,7 @@
using Volo.Abp.Identity.EntityFrameworkCore;
namespace Volo.Abp.Identity;
public class IdentityUserManager_SharedUser_SeparateDatabase_Tests : IdentityUserManager_SharedUser_SeparateDatabase_Tests<AbpIdentitySharedUserSeparateDbEntityFrameworkCoreTestModule>
{
}

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

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

232
modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs

@ -491,235 +491,3 @@ public class IdentityUserManager_Tests : AbpIdentityDomainTestBase
}
}
public class SharedTenantUserSharingStrategy_IdentityUserManager_Tests : AbpIdentityDomainTestBase
{
private readonly IdentityUserManager _identityUserManager;
private readonly IIdentityUserRepository _identityUserRepository;
private readonly ICurrentTenant _currentTenant;
private readonly IUnitOfWorkManager _unitOfWorkManager;
public SharedTenantUserSharingStrategy_IdentityUserManager_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 async Task FindSharedUserByEmailAsync_Should_Return_Host_User()
{
var tenantId = Guid.NewGuid();
var email = "shared-email@abp.io";
using (var uow = _unitOfWorkManager.Begin())
{
await CreateUserAsync(null, "shared-host-email", email);
await CreateUserAsync(tenantId, "shared-tenant-email", email);
await uow.CompleteAsync();
}
using (_currentTenant.Change(tenantId))
{
var user = await _identityUserManager.FindSharedUserByEmailAsync(email);
user.ShouldNotBeNull();
user.TenantId.ShouldBeNull();
user.UserName.ShouldBe("shared-host-email");
}
}
[Fact]
public async Task FindSharedUserByNameAsync_Should_Return_Host_User()
{
var tenantId = Guid.NewGuid();
var userName = "shared-user-name";
using (var uow = _unitOfWorkManager.Begin())
{
await CreateUserAsync(null, userName, "shared-host-name@abp.io");
await CreateUserAsync(tenantId, userName, "shared-tenant-name@abp.io");
await uow.CompleteAsync();
}
using (_currentTenant.Change(tenantId))
{
var user = await _identityUserManager.FindSharedUserByNameAsync(userName);
user.ShouldNotBeNull();
user.TenantId.ShouldBeNull();
user.UserName.ShouldBe(userName);
}
}
[Fact]
public async Task FindSharedUserByLoginAsync_Should_Return_Host_User()
{
var tenantId = Guid.NewGuid();
const string loginProvider = "github";
const string providerKey = "shared-login";
using (var uow = _unitOfWorkManager.Begin())
{
await CreateUserAsync(null, "shared-host-login", "shared-host-login@abp.io", user =>
{
user.AddLogin(new UserLoginInfo(loginProvider, providerKey, "Shared Login"));
});
await CreateUserAsync(tenantId, "shared-tenant-login", "shared-tenant-login@abp.io", user =>
{
user.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();
user.UserName.ShouldBe("shared-host-login");
}
}
[Fact]
public async Task FindSharedUserByPasskeyIdAsync_Should_Return_Host_User()
{
var tenantId = Guid.NewGuid();
var credentialId = new byte[] { 10, 20, 30, 40, 50, 60 };
using (var uow = _unitOfWorkManager.Begin())
{
await CreateUserAsync(null, "shared-host-passkey", "shared-host-passkey@abp.io", user =>
{
user.AddPasskey(credentialId, new IdentityPasskeyData());
});
await uow.CompleteAsync();
}
using (_currentTenant.Change(tenantId))
{
var user = await _identityUserManager.FindSharedUserByPasskeyIdAsync(credentialId);
user.ShouldNotBeNull();
user.TenantId.ShouldBeNull();
user.UserName.ShouldBe("shared-host-passkey");
}
}
[Fact]
public async Task FindSharedUserByIdAsync_Should_Find_Tenant_User_From_Host_Context()
{
var tenantId = Guid.NewGuid();
IdentityUser tenantUser;
using (var uow = _unitOfWorkManager.Begin())
{
tenantUser = await CreateUserAsync(tenantId, "shared-id-tenant-only", "shared-id-tenant-only@abp.io");
await uow.CompleteAsync();
}
// Simulates the 2FA mid-flow on a Shared deployment: CurrentTenant is null
// but the user row only exists under a tenant. FindByIdAsync alone would be
// filtered out by the IMultiTenant filter, so FindSharedUserByIdAsync must
// disable the filter and still return the tenant user.
using (_currentTenant.Change(null))
{
var user = await _identityUserManager.FindSharedUserByIdAsync(tenantUser.Id.ToString());
user.ShouldNotBeNull();
user.Id.ShouldBe(tenantUser.Id);
user.TenantId.ShouldBe(tenantId);
user.UserName.ShouldBe("shared-id-tenant-only");
}
}
[Fact]
public 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-only", "shared-id-host-only@abp.io");
await uow.CompleteAsync();
}
using (_currentTenant.Change(tenantId))
{
var user = await _identityUserManager.FindSharedUserByIdAsync(hostUser.Id.ToString());
user.ShouldNotBeNull();
user.Id.ShouldBe(hostUser.Id);
user.TenantId.ShouldBeNull();
user.UserName.ShouldBe("shared-id-host-only");
}
}
[Fact]
public 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 async Task Login_Then_TwoFactor_MidFlow_Should_Resolve_Same_Tenant_User_In_Shared_Mode()
{
// Covers the data-access contract behind the 2FA redirect bug:
// 1. login lookup (by user name) must find a tenant user from a host context,
// 2. the 2FA mid-flow lookup (by id) must then return the same tenant user
// from the same host context. Regressing either side re-opens the bug.
var tenantId = Guid.NewGuid();
using (var uow = _unitOfWorkManager.Begin())
{
await CreateUserAsync(tenantId, "shared-2fa-linked", "shared-2fa-linked@abp.io");
await uow.CompleteAsync();
}
using (_currentTenant.Change(null))
{
var loginUser = await _identityUserManager.FindSharedUserByNameAsync("shared-2fa-linked");
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);
}
}
private 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;
}
}

123
modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/AbpIdentitySharedUserSeparateDbEntityFrameworkCoreTestModule.cs

@ -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();
}
}

56
modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/AbpIdentitySharedUserSeparateDbMongoDbTestModule.cs

@ -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 }
}
}
};
});
}
}

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

@ -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>
{
}

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

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

12
modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentitySharedUserSeparateDbConstants.cs

@ -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");
}

201
modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityUserManager_SharedUser_SeparateDatabase_Tests.cs

@ -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);
}
}
}

254
modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityUserManager_SharedUser_Tests.cs

@ -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…
Cancel
Save