Browse Source

Add unit tests for shared user covering shared and separate database

pull/25319/head
maliming 2 weeks ago
parent
commit
ecb23b626d
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  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. 120
      modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/AbpIdentitySharedUserSeparateDbEntityFrameworkCoreTestModule.cs
  5. 54
      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. 160
      modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityUserManager_SharedUser_SeparateDatabase_Tests.cs
  9. 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;
}
}

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

@ -0,0 +1,120 @@
using System;
using System.Collections.Concurrent;
using Microsoft.Data.Sqlite;
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 =
IdentityUserManager_SharedUser_SeparateDatabase_Tests<AbpIdentitySharedUserSeparateDbEntityFrameworkCoreTestModule>.TenantAId;
public static readonly Guid TenantBId =
IdentityUserManager_SharedUser_SeparateDatabase_Tests<AbpIdentitySharedUserSeparateDbEntityFrameworkCoreTestModule>.TenantBId;
// Static cache so the in-memory SQLite databases survive for the full process lifetime
// (without an open connection, in-memory shared-cache databases are discarded). One entry
// per unique connection string.
private static readonly ConcurrentDictionary<string, SqliteConnection> _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();
}
private static void EnsureDatabase(string connectionString)
{
if (_keepAlive.ContainsKey(connectionString))
{
return;
}
var keepAlive = new SqliteConnection(connectionString);
keepAlive.Open();
_keepAlive[connectionString] = keepAlive;
new IdentityDbContext(
new DbContextOptionsBuilder<IdentityDbContext>().UseSqlite(connectionString).Options)
.GetService<IRelationalDatabaseCreator>().CreateTables();
new PermissionManagementDbContext(
new DbContextOptionsBuilder<PermissionManagementDbContext>().UseSqlite(connectionString).Options)
.GetService<IRelationalDatabaseCreator>().CreateTables();
}
}

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

@ -0,0 +1,54 @@
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 = Guid.Parse("11111111-1111-1111-1111-111111111111");
public static readonly Guid TenantBId = Guid.Parse("22222222-2222-2222-2222-222222222222");
public override void ConfigureServices(ServiceConfigurationContext context)
{
// Fixed db names so MongoSandbox's embedded mongod does not accumulate one db per test
// method. Each test method generates unique email / userName so cross-test data does not
// collide on those fields.
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>
{
}

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

@ -0,0 +1,160 @@
using System;
using System.Threading.Tasks;
using Shouldly;
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 = Guid.Parse("11111111-1111-1111-1111-111111111111");
public static readonly Guid TenantBId = Guid.Parse("22222222-2222-2222-2222-222222222222");
protected IdentityUserManager IdentityUserManager { get; }
protected IIdentityUserRepository IdentityUserRepository { get; }
protected ICurrentTenant CurrentTenant { get; }
protected IUnitOfWorkManager UnitOfWorkManager { get; }
protected IdentityUserManager_SharedUser_SeparateDatabase_Tests()
{
IdentityUserManager = GetRequiredService<IdentityUserManager>();
IdentityUserRepository = GetRequiredService<IIdentityUserRepository>();
CurrentTenant = GetRequiredService<ICurrentTenant>();
UnitOfWorkManager = GetRequiredService<IUnitOfWorkManager>();
}
[Fact]
public virtual async Task Tenant_Connection_Should_Not_See_Host_Rows()
{
var probeEmail = $"infra-host-{Guid.NewGuid():N}@abp.io";
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);
await uow.CompleteAsync();
}
using (CurrentTenant.Change(TenantAId))
{
var foundInTenantA = await IdentityUserManager.FindByEmailAsync(probeEmail);
foundInTenantA.ShouldBeNull();
}
}
[Fact]
public virtual async Task Host_Connection_Should_Not_See_Tenant_Rows()
{
var probeEmail = $"infra-tenant-{Guid.NewGuid():N}@abp.io";
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);
await uow.CompleteAsync();
}
using (CurrentTenant.Change(null))
{
var foundInHost = await IdentityUserManager.FindByEmailAsync(probeEmail);
foundInHost.ShouldBeNull();
}
}
[Fact]
public virtual async Task TenantA_Connection_Should_Not_See_TenantB_Rows()
{
var probeEmail = $"infra-cross-{Guid.NewGuid():N}@abp.io";
using (CurrentTenant.Change(TenantAId))
using (var uow = UnitOfWorkManager.Begin(requiresNew: true))
{
var u = new IdentityUser(Guid.NewGuid(), $"a-{Guid.NewGuid():N}", probeEmail, TenantAId);
await IdentityUserRepository.InsertAsync(u);
await uow.CompleteAsync();
}
using (CurrentTenant.Change(TenantBId))
{
var foundInB = await IdentityUserManager.FindByEmailAsync(probeEmail);
foundInB.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))
{
(await IdentityUserManager.FindByEmailAsync(email)).UserName.ShouldBe(nameA);
}
using (CurrentTenant.Change(TenantBId))
{
(await IdentityUserManager.FindByEmailAsync(email)).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))
{
(await IdentityUserManager.FindByNameAsync(userName)).Email.ShouldBe(emailA);
}
using (CurrentTenant.Change(TenantBId))
{
(await IdentityUserManager.FindByNameAsync(userName)).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