Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using Volo.Abp.Identity.EntityFrameworkCore;

namespace Volo.Abp.Identity;

public class IdentityUserManager_SharedUser_SeparateDatabase_Tests : IdentityUserManager_SharedUser_SeparateDatabase_Tests<AbpIdentitySharedUserSeparateDbEntityFrameworkCoreTestModule>
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace Volo.Abp.Identity;

public class IdentityUserManager_SharedUser_Tests : IdentityUserManager_SharedUser_Tests<AbpIdentityDomainTestModule>
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -491,235 +491,3 @@ private static void AddPeriodicallyChangePasswordSettings()
}
}

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;
}
}
Original file line number Diff line number Diff line change
@@ -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();

Comment thread
maliming marked this conversation as resolved.
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);
Comment thread
maliming marked this conversation as resolved.

new IdentityDbContext(
new DbContextOptionsBuilder<IdentityDbContext>().UseSqlite(connectionString).Options)
.GetService<IRelationalDatabaseCreator>().CreateTables();

new PermissionManagementDbContext(
new DbContextOptionsBuilder<PermissionManagementDbContext>().UseSqlite(connectionString).Options)
.GetService<IRelationalDatabaseCreator>().CreateTables();
}
}
Loading
Loading