mirror of https://github.com/abpframework/abp.git
Browse Source
# Conflicts: # docs/en/package-version-changes.md # templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyCompanyName.MyProjectName.Blazor.WebAssembly.Client.csproj # templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyCompanyName.MyProjectName.Blazor.Client.csproj # templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj # templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Client.csproj # templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client.csproj # templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj # templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj # templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyCompanyName.MyProjectName.Blazor.Host.Client.csprojpull/25235/head
61 changed files with 1292 additions and 153 deletions
@ -0,0 +1,3 @@ |
|||
{ |
|||
"role": "lib.test" |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\common.test.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net10.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\AbpTestBase\AbpTestBase.csproj" /> |
|||
<ProjectReference Include="..\..\src\Volo.Abp.RabbitMQ\Volo.Abp.RabbitMQ.csproj" /> |
|||
<PackageReference Include="Microsoft.NET.Test.Sdk" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,359 @@ |
|||
#nullable enable |
|||
|
|||
using System; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using NSubstitute; |
|||
using RabbitMQ.Client; |
|||
using Shouldly; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.RabbitMQ; |
|||
|
|||
public class ChannelPool_Tests |
|||
{ |
|||
private static readonly TimeSpan RaceTimeout = TimeSpan.FromSeconds(5); |
|||
|
|||
// ---- Regression tests for issue #25310 / PR #25311 --------------------
|
|||
|
|||
/// <summary>
|
|||
/// After a RabbitMQ restart the cached channel becomes closed. Two callers
|
|||
/// that both observe the stale poolItem must not deadlock while the pool
|
|||
/// replaces it with a fresh channel.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public async Task AcquireAsync_Should_Not_Hang_When_Channel_Closed_With_Concurrent_Callers() |
|||
{ |
|||
var fixture = BuildConnectionPool(); |
|||
var channelPool = new TestChannelPool(fixture.Pool); |
|||
|
|||
using (await channelPool.AcquireAsync("q")) |
|||
{ |
|||
} |
|||
|
|||
fixture.Channel1.IsClosed.Returns(true); |
|||
fixture.Channel1 |
|||
.DisposeAsync() |
|||
.Returns(_ => new ValueTask(Task.Delay(300))); |
|||
|
|||
using var barrier = new Barrier(2); |
|||
|
|||
var firstCaller = Task.Run(async () => |
|||
{ |
|||
barrier.SignalAndWait(RaceTimeout); |
|||
using var accessor = await channelPool.AcquireAsync("q"); |
|||
}); |
|||
|
|||
var secondCaller = Task.Run(async () => |
|||
{ |
|||
barrier.SignalAndWait(RaceTimeout); |
|||
await Task.Delay(50); |
|||
using var accessor = await channelPool.AcquireAsync("q"); |
|||
}); |
|||
|
|||
var completed = Task.WhenAll(firstCaller, secondCaller); |
|||
var winner = await Task.WhenAny(completed, Task.Delay(RaceTimeout)); |
|||
|
|||
winner.ShouldBe( |
|||
completed, |
|||
$"AcquireAsync is hanging on a stale poolItem. firstCaller={firstCaller.Status}, secondCaller={secondCaller.Status}"); |
|||
|
|||
await completed; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// When several callers race through the closed-channel recovery path, only one new
|
|||
/// channel must be created and the dictionary must keep exactly the channel that callers
|
|||
/// receive — otherwise the replacement leaks on the broker side.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public async Task AcquireAsync_Should_Create_Only_One_Replacement_Channel_When_Many_Callers_Race() |
|||
{ |
|||
var fixture = BuildConnectionPool(); |
|||
var channelPool = new TestChannelPool(fixture.Pool); |
|||
|
|||
using (await channelPool.AcquireAsync("q")) |
|||
{ |
|||
} |
|||
|
|||
fixture.Channel1.IsClosed.Returns(true); |
|||
|
|||
const int callerCount = 8; |
|||
using var barrier = new Barrier(callerCount); |
|||
var tasks = new Task<IChannel>[callerCount]; |
|||
for (var i = 0; i < callerCount; i++) |
|||
{ |
|||
tasks[i] = Task.Run(async () => |
|||
{ |
|||
barrier.SignalAndWait(RaceTimeout); |
|||
using var accessor = await channelPool.AcquireAsync("q"); |
|||
return accessor.Channel; |
|||
}); |
|||
} |
|||
|
|||
var channels = await Task.WhenAll(tasks); |
|||
|
|||
foreach (var channel in channels) |
|||
{ |
|||
channel.ShouldBe(fixture.Channel2, "every caller must receive the single replacement channel"); |
|||
} |
|||
|
|||
fixture.CreateChannelCalls.ShouldBe( |
|||
2, |
|||
$"exactly one initial + one replacement channel should be created, but got {fixture.CreateChannelCalls}"); |
|||
} |
|||
|
|||
// ---- Behavior guard-rails: make sure the refactor preserves core semantics ----
|
|||
|
|||
[Fact] |
|||
public async Task AcquireAsync_Should_Return_Channel_From_ConnectionPool() |
|||
{ |
|||
var fixture = BuildConnectionPool(); |
|||
var channelPool = new TestChannelPool(fixture.Pool); |
|||
|
|||
using var accessor = await channelPool.AcquireAsync("q"); |
|||
|
|||
accessor.Channel.ShouldBe(fixture.Channel1); |
|||
accessor.Name.ShouldBe("q"); |
|||
fixture.CreateChannelCalls.ShouldBe(1); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task AcquireAsync_Should_Reuse_Cached_Channel_On_Subsequent_Calls() |
|||
{ |
|||
var fixture = BuildConnectionPool(); |
|||
var channelPool = new TestChannelPool(fixture.Pool); |
|||
|
|||
using (var first = await channelPool.AcquireAsync("q")) |
|||
{ |
|||
first.Channel.ShouldBe(fixture.Channel1); |
|||
} |
|||
using (var second = await channelPool.AcquireAsync("q")) |
|||
{ |
|||
second.Channel.ShouldBe(fixture.Channel1); |
|||
} |
|||
using (var third = await channelPool.AcquireAsync("q")) |
|||
{ |
|||
third.Channel.ShouldBe(fixture.Channel1); |
|||
} |
|||
|
|||
fixture.CreateChannelCalls.ShouldBe(1, "a healthy cached channel must be reused"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task AcquireAsync_Should_Keep_Separate_PoolItems_For_Different_ChannelNames() |
|||
{ |
|||
var fixture = BuildConnectionPool(); |
|||
var channelPool = new TestChannelPool(fixture.Pool); |
|||
|
|||
using var a = await channelPool.AcquireAsync("queue-a"); |
|||
using var b = await channelPool.AcquireAsync("queue-b"); |
|||
|
|||
a.Channel.ShouldBe(fixture.Channel1); |
|||
b.Channel.ShouldBe(fixture.Channel2); |
|||
a.Name.ShouldBe("queue-a"); |
|||
b.Name.ShouldBe("queue-b"); |
|||
fixture.CreateChannelCalls.ShouldBe(2); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task AcquireAsync_Should_Serialize_Concurrent_Callers_On_Same_Channel() |
|||
{ |
|||
var fixture = BuildConnectionPool(); |
|||
var channelPool = new TestChannelPool(fixture.Pool); |
|||
|
|||
var first = await channelPool.AcquireAsync("q"); |
|||
|
|||
var secondTask = Task.Run(() => channelPool.AcquireAsync("q")); |
|||
|
|||
await Task.Delay(100); |
|||
secondTask.IsCompleted.ShouldBeFalse("second caller must block while the channel is held"); |
|||
|
|||
first.Dispose(); |
|||
|
|||
var completed = await Task.WhenAny(secondTask, Task.Delay(RaceTimeout)); |
|||
completed.ShouldBe(secondTask, "second caller must be unblocked after release"); |
|||
|
|||
var secondAccessor = await secondTask; |
|||
secondAccessor.Channel.ShouldBe(fixture.Channel1, "the cached channel is reused"); |
|||
|
|||
secondAccessor.Dispose(); |
|||
fixture.CreateChannelCalls.ShouldBe(1, "channel is reused, never recreated"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task AcquireAsync_Should_Not_Rebuild_When_Cached_Channel_Is_Healthy() |
|||
{ |
|||
var fixture = BuildConnectionPool(); |
|||
var channelPool = new TestChannelPool(fixture.Pool); |
|||
|
|||
using (await channelPool.AcquireAsync("q")) |
|||
{ |
|||
} |
|||
// Channel stays open, so subsequent calls must not go through the rebuild branch.
|
|||
using (await channelPool.AcquireAsync("q")) |
|||
{ |
|||
} |
|||
|
|||
await fixture.Channel1.DidNotReceive().DisposeAsync(); |
|||
fixture.CreateChannelCalls.ShouldBe(1); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task AcquireAsync_Should_Propagate_Exception_When_CreateChannel_Fails() |
|||
{ |
|||
var channel = Substitute.For<IChannel>(); |
|||
channel.IsClosed.Returns(false); |
|||
|
|||
var connection = Substitute.For<IConnection>(); |
|||
var attempts = 0; |
|||
connection |
|||
.CreateChannelAsync(Arg.Any<CreateChannelOptions?>(), Arg.Any<CancellationToken>()) |
|||
.Returns(_ => |
|||
{ |
|||
var n = Interlocked.Increment(ref attempts); |
|||
if (n == 1) |
|||
{ |
|||
throw new InvalidOperationException("broker down"); |
|||
} |
|||
return Task.FromResult(channel); |
|||
}); |
|||
|
|||
var connectionPool = Substitute.For<IConnectionPool>(); |
|||
connectionPool.GetAsync(Arg.Any<string?>()).Returns(Task.FromResult(connection)); |
|||
|
|||
var channelPool = new TestChannelPool(connectionPool); |
|||
|
|||
await Should.ThrowAsync<InvalidOperationException>(() => channelPool.AcquireAsync("q")); |
|||
|
|||
// Subsequent call should be able to succeed (broker came back).
|
|||
using var accessor = await channelPool.AcquireAsync("q"); |
|||
accessor.Channel.ShouldBe(channel); |
|||
attempts.ShouldBe(2); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task AcquireAsync_Should_Dispose_Stale_Channel_Even_When_Recreate_Fails() |
|||
{ |
|||
var staleChannel = Substitute.For<IChannel>(); |
|||
staleChannel.IsClosed.Returns(false); |
|||
|
|||
var connection = Substitute.For<IConnection>(); |
|||
var attempts = 0; |
|||
connection |
|||
.CreateChannelAsync(Arg.Any<CreateChannelOptions?>(), Arg.Any<CancellationToken>()) |
|||
.Returns(_ => |
|||
{ |
|||
var n = Interlocked.Increment(ref attempts); |
|||
if (n == 1) |
|||
{ |
|||
return Task.FromResult(staleChannel); |
|||
} |
|||
throw new InvalidOperationException("broker still down"); |
|||
}); |
|||
|
|||
var connectionPool = Substitute.For<IConnectionPool>(); |
|||
connectionPool.GetAsync(Arg.Any<string?>()).Returns(Task.FromResult(connection)); |
|||
|
|||
var channelPool = new TestChannelPool(connectionPool); |
|||
|
|||
using (await channelPool.AcquireAsync("q")) |
|||
{ |
|||
} |
|||
|
|||
staleChannel.IsClosed.Returns(true); |
|||
|
|||
await Should.ThrowAsync<InvalidOperationException>(() => channelPool.AcquireAsync("q")); |
|||
|
|||
await staleChannel.Received(1).DisposeAsync(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task AcquireAsync_Should_Throw_After_Pool_Disposed() |
|||
{ |
|||
var fixture = BuildConnectionPool(); |
|||
var channelPool = new TestChannelPool(fixture.Pool); |
|||
|
|||
await channelPool.DisposeAsync(); |
|||
|
|||
await Should.ThrowAsync<ObjectDisposedException>(() => channelPool.AcquireAsync("q")); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task AcquireAsync_Should_Only_Create_One_Channel_Even_When_First_Callers_Race() |
|||
{ |
|||
var fixture = BuildConnectionPool(); |
|||
var channelPool = new TestChannelPool(fixture.Pool); |
|||
|
|||
const int callerCount = 8; |
|||
using var barrier = new Barrier(callerCount); |
|||
var tasks = new Task<IChannel>[callerCount]; |
|||
for (var i = 0; i < callerCount; i++) |
|||
{ |
|||
tasks[i] = Task.Run(async () => |
|||
{ |
|||
barrier.SignalAndWait(RaceTimeout); |
|||
using var accessor = await channelPool.AcquireAsync("q"); |
|||
return accessor.Channel; |
|||
}); |
|||
} |
|||
|
|||
var channels = await Task.WhenAll(tasks); |
|||
|
|||
foreach (var channel in channels) |
|||
{ |
|||
channel.ShouldBe(fixture.Channel1, "the first-creation semaphore must serialize initial creation"); |
|||
} |
|||
fixture.CreateChannelCalls.ShouldBe(1); |
|||
} |
|||
|
|||
// ---- Fixture ---------------------------------------------------------
|
|||
|
|||
private static ConnectionPoolFixture BuildConnectionPool() |
|||
{ |
|||
var channel1 = Substitute.For<IChannel>(); |
|||
channel1.IsClosed.Returns(false); |
|||
|
|||
var channel2 = Substitute.For<IChannel>(); |
|||
channel2.IsClosed.Returns(false); |
|||
|
|||
var fixture = new ConnectionPoolFixture(channel1, channel2); |
|||
|
|||
var connection = Substitute.For<IConnection>(); |
|||
connection |
|||
.CreateChannelAsync(Arg.Any<CreateChannelOptions?>(), Arg.Any<CancellationToken>()) |
|||
.Returns(_ => |
|||
{ |
|||
var n = Interlocked.Increment(ref fixture.CreateChannelCallsField); |
|||
return n == 1 ? Task.FromResult(channel1) : Task.FromResult(channel2); |
|||
}); |
|||
|
|||
var connectionPool = Substitute.For<IConnectionPool>(); |
|||
connectionPool.GetAsync(Arg.Any<string?>()).Returns(Task.FromResult(connection)); |
|||
|
|||
fixture.Pool = connectionPool; |
|||
return fixture; |
|||
} |
|||
|
|||
private sealed class ConnectionPoolFixture |
|||
{ |
|||
public IConnectionPool Pool { get; set; } = default!; |
|||
public IChannel Channel1 { get; } |
|||
public IChannel Channel2 { get; } |
|||
public int CreateChannelCallsField; |
|||
public int CreateChannelCalls => CreateChannelCallsField; |
|||
|
|||
public ConnectionPoolFixture(IChannel channel1, IChannel channel2) |
|||
{ |
|||
Channel1 = channel1; |
|||
Channel2 = channel2; |
|||
} |
|||
} |
|||
|
|||
private sealed class TestChannelPool : ChannelPool |
|||
{ |
|||
public TestChannelPool(IConnectionPool connectionPool) : base(connectionPool) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,135 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Uow; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
public class GetTwoFactorAuthenticationUser_Tests : SharedAbpIdentityAspNetCoreTestBase |
|||
{ |
|||
[Fact] |
|||
public async Task Should_Resolve_Tenant_User_By_Id_When_Current_Tenant_Is_Host() |
|||
{ |
|||
var userRepository = GetRequiredService<IIdentityUserRepository>(); |
|||
var currentTenant = GetRequiredService<ICurrentTenant>(); |
|||
var unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>(); |
|||
|
|||
var tenantId = Guid.NewGuid(); |
|||
Guid tenantUserId; |
|||
|
|||
using (var uow = unitOfWorkManager.Begin()) |
|||
{ |
|||
using (currentTenant.Change(tenantId)) |
|||
{ |
|||
var user = new IdentityUser(Guid.NewGuid(), "shared-2fa-tenant-user", "shared-2fa-tenant-user@abp.io", tenantId); |
|||
await userRepository.InsertAsync(user); |
|||
tenantUserId = user.Id; |
|||
} |
|||
await uow.CompleteAsync(); |
|||
} |
|||
|
|||
await WriteTwoFactorCookieAsync(tenantUserId); |
|||
|
|||
var getResponse = await Client.GetAsync("/api/signin-test/get-two-factor-user"); |
|||
getResponse.EnsureSuccessStatusCode(); |
|||
var content = await getResponse.Content.ReadAsStringAsync(); |
|||
|
|||
content.ShouldBe(tenantUserId.ToString()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Return_Null_When_No_Two_Factor_Cookie() |
|||
{ |
|||
var getResponse = await Client.GetAsync("/api/signin-test/get-two-factor-user"); |
|||
getResponse.EnsureSuccessStatusCode(); |
|||
var content = await getResponse.Content.ReadAsStringAsync(); |
|||
|
|||
content.ShouldBe("null"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task TwoFactorSignInAsync_Should_Resolve_Tenant_User_In_Shared_Mode() |
|||
{ |
|||
// If the tenant switch inside AbpSignInManager.TwoFactorSignInAsync regresses,
|
|||
// the base implementation would not find the user and return SignInResult.Failed.
|
|||
// An inactive tenant user lets PreSignInCheck return NotAllowed, proving the user
|
|||
// was actually located and inspected under its own tenant context.
|
|||
var tenantUserId = await CreateInactiveTenantUserAsync("shared-2fa-signin", "shared-2fa-signin@abp.io"); |
|||
|
|||
await WriteTwoFactorCookieAsync(tenantUserId); |
|||
|
|||
var response = await Client.GetAsync("/api/signin-test/two-factor-signin?provider=Email&code=invalid"); |
|||
response.EnsureSuccessStatusCode(); |
|||
var result = await response.Content.ReadAsStringAsync(); |
|||
|
|||
result.ShouldBe("NotAllowed"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task TwoFactorRecoveryCodeSignInAsync_Should_Resolve_Tenant_User_In_Shared_Mode() |
|||
{ |
|||
// AbpSignInManager.TwoFactorRecoveryCodeSignInAsync runs PreSignInCheck after locating
|
|||
// the user under its own tenant context. An inactive tenant user therefore produces
|
|||
// NotAllowed only when the override actually resolves the user; a regression would
|
|||
// return Failed instead.
|
|||
var tenantUserId = await CreateInactiveTenantUserAsync("shared-2fa-recovery", "shared-2fa-recovery@abp.io"); |
|||
|
|||
await WriteTwoFactorCookieAsync(tenantUserId); |
|||
|
|||
var response = await Client.GetAsync("/api/signin-test/two-factor-recovery-signin?recoveryCode=invalid"); |
|||
response.EnsureSuccessStatusCode(); |
|||
var result = await response.Content.ReadAsStringAsync(); |
|||
|
|||
result.ShouldBe("NotAllowed"); |
|||
} |
|||
|
|||
private async Task<Guid> CreateInactiveTenantUserAsync(string userName, string email) |
|||
{ |
|||
return await CreateTenantUserAsync(userName, email, isActive: false); |
|||
} |
|||
|
|||
private async Task<Guid> CreateTenantUserAsync(string userName, string email, bool isActive) |
|||
{ |
|||
var userRepository = GetRequiredService<IIdentityUserRepository>(); |
|||
var currentTenant = GetRequiredService<ICurrentTenant>(); |
|||
var unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>(); |
|||
|
|||
var tenantId = Guid.NewGuid(); |
|||
Guid userId; |
|||
|
|||
using (var uow = unitOfWorkManager.Begin()) |
|||
{ |
|||
using (currentTenant.Change(tenantId)) |
|||
{ |
|||
var user = new IdentityUser(Guid.NewGuid(), userName, email, tenantId); |
|||
if (!isActive) |
|||
{ |
|||
user.SetIsActive(false); |
|||
} |
|||
await userRepository.InsertAsync(user); |
|||
userId = user.Id; |
|||
} |
|||
await uow.CompleteAsync(); |
|||
} |
|||
|
|||
return userId; |
|||
} |
|||
|
|||
private async Task WriteTwoFactorCookieAsync(Guid userId) |
|||
{ |
|||
var writeResponse = await Client.GetAsync($"/api/signin-test/write-two-factor-cookie?userId={userId}"); |
|||
writeResponse.EnsureSuccessStatusCode(); |
|||
|
|||
if (writeResponse.Headers.TryGetValues("Set-Cookie", out var setCookies)) |
|||
{ |
|||
Client.DefaultRequestHeaders.Remove("Cookie"); |
|||
foreach (var cookie in setCookies) |
|||
{ |
|||
Client.DefaultRequestHeaders.Add("Cookie", cookie.Split(';').First()); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Volo.Abp.Uow; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
public class Isolated_TwoFactor_Tests : AbpIdentityAspNetCoreTestBase |
|||
{ |
|||
[Fact] |
|||
public async Task TwoFactorRecoveryCodeSignInAsync_Should_Return_NotAllowed_For_Inactive_User() |
|||
{ |
|||
// The AbpSignInManager override adds PreSignInCheck to the recovery-code path (the base
|
|||
// AspNetCore Identity implementation does not). This test asserts that behavior also works
|
|||
// in the default (isolated) configuration so the new invariant is protected across modes.
|
|||
var userManager = GetRequiredService<IdentityUserManager>(); |
|||
var userRepository = GetRequiredService<IIdentityUserRepository>(); |
|||
var unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>(); |
|||
|
|||
Guid userId; |
|||
using (var uow = unitOfWorkManager.Begin()) |
|||
{ |
|||
var user = new IdentityUser(Guid.NewGuid(), "iso-recovery-inactive", "iso-recovery-inactive@abp.io"); |
|||
(await userManager.CreateAsync(user, "Iso!9Aa")).Succeeded.ShouldBeTrue(); |
|||
user.SetIsActive(false); |
|||
await userRepository.UpdateAsync(user); |
|||
userId = user.Id; |
|||
await uow.CompleteAsync(); |
|||
} |
|||
|
|||
var writeResponse = await Client.GetAsync($"/api/signin-test/write-two-factor-cookie?userId={userId}"); |
|||
writeResponse.EnsureSuccessStatusCode(); |
|||
if (writeResponse.Headers.TryGetValues("Set-Cookie", out var setCookies)) |
|||
{ |
|||
Client.DefaultRequestHeaders.Remove("Cookie"); |
|||
foreach (var cookie in setCookies) |
|||
{ |
|||
Client.DefaultRequestHeaders.Add("Cookie", cookie.Split(';').First()); |
|||
} |
|||
} |
|||
|
|||
var response = await Client.GetAsync("/api/signin-test/two-factor-recovery-signin?recoveryCode=invalid"); |
|||
response.EnsureSuccessStatusCode(); |
|||
var result = await response.Content.ReadAsStringAsync(); |
|||
|
|||
result.ShouldBe("NotAllowed"); |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
using Volo.Abp.AspNetCore.TestBase; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
public abstract class SharedAbpIdentityAspNetCoreTestBase : AbpAspNetCoreIntegratedTestBase<SharedAbpIdentityAspNetCoreTestStartup> |
|||
{ |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
[DependsOn(typeof(AbpIdentityAspNetCoreTestModule))] |
|||
public class SharedAbpIdentityAspNetCoreTestModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpMultiTenancyOptions>(options => |
|||
{ |
|||
options.IsEnabled = true; |
|||
options.UserSharingStrategy = TenantUserSharingStrategy.Shared; |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using Microsoft.AspNetCore.Builder; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Logging; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
public class SharedAbpIdentityAspNetCoreTestStartup |
|||
{ |
|||
public void ConfigureServices(IServiceCollection services) |
|||
{ |
|||
services.AddApplication<SharedAbpIdentityAspNetCoreTestModule>(); |
|||
} |
|||
|
|||
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) |
|||
{ |
|||
app.InitializeApplication(); |
|||
} |
|||
} |
|||
@ -0,0 +1,113 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Shouldly; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Uow; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
public class Shared_SignIn_Tests : SharedAbpIdentityAspNetCoreTestBase |
|||
{ |
|||
[Fact] |
|||
public async Task PasswordSignInAsync_Should_Sign_In_Tenant_User_From_Host_Context() |
|||
{ |
|||
// In shared mode, calling PasswordSignInAsync with a username while CurrentTenant is host
|
|||
// must resolve the tenant user via FindSharedUserByNameAsync, apply the user's tenant
|
|||
// IdentityOptions (the fix added in AbpSignInManager), and complete the sign-in.
|
|||
var userManager = GetRequiredService<IdentityUserManager>(); |
|||
var currentTenant = GetRequiredService<ICurrentTenant>(); |
|||
var unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>(); |
|||
|
|||
var tenantId = Guid.NewGuid(); |
|||
const string userName = "shared-password-signin"; |
|||
const string password = "Shared!9Aa"; |
|||
|
|||
using (var uow = unitOfWorkManager.Begin()) |
|||
{ |
|||
using (currentTenant.Change(tenantId)) |
|||
{ |
|||
var user = new IdentityUser(Guid.NewGuid(), userName, userName + "@abp.io", tenantId); |
|||
(await userManager.CreateAsync(user, password)).Succeeded.ShouldBeTrue(); |
|||
} |
|||
await uow.CompleteAsync(); |
|||
} |
|||
|
|||
var response = await Client.GetAsync($"/api/signin-test/password?userName={userName}&password={password}"); |
|||
response.EnsureSuccessStatusCode(); |
|||
var result = await response.Content.ReadAsStringAsync(); |
|||
|
|||
result.ShouldBe("Succeeded"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task PasswordSignInAsync_Should_Fail_For_Wrong_Password_In_Shared_Mode() |
|||
{ |
|||
var userManager = GetRequiredService<IdentityUserManager>(); |
|||
var currentTenant = GetRequiredService<ICurrentTenant>(); |
|||
var unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>(); |
|||
|
|||
var tenantId = Guid.NewGuid(); |
|||
const string userName = "shared-password-wrong"; |
|||
|
|||
using (var uow = unitOfWorkManager.Begin()) |
|||
{ |
|||
using (currentTenant.Change(tenantId)) |
|||
{ |
|||
var user = new IdentityUser(Guid.NewGuid(), userName, userName + "@abp.io", tenantId); |
|||
(await userManager.CreateAsync(user, "Shared!9Aa")).Succeeded.ShouldBeTrue(); |
|||
} |
|||
await uow.CompleteAsync(); |
|||
} |
|||
|
|||
var response = await Client.GetAsync($"/api/signin-test/password?userName={userName}&password=wrong"); |
|||
response.EnsureSuccessStatusCode(); |
|||
var result = await response.Content.ReadAsStringAsync(); |
|||
|
|||
result.ShouldBe("Failed"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task ExternalLoginSignInAsync_Should_Sign_In_Tenant_User_From_Host_Context() |
|||
{ |
|||
// Covers the AbpSignInManager.ExternalLoginSignInAsync override: finds the user via
|
|||
// FindSharedUserByLoginAsync, switches CurrentTenant to user.TenantId, applies tenant
|
|||
// IdentityOptions, then calls PreSignInCheck + SignInOrTwoFactorAsync.
|
|||
const string loginProvider = "test-provider"; |
|||
var providerKey = "ext-" + Guid.NewGuid().ToString("N").Substring(0, 8); |
|||
|
|||
var userManager = GetRequiredService<IdentityUserManager>(); |
|||
var currentTenant = GetRequiredService<ICurrentTenant>(); |
|||
var unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>(); |
|||
|
|||
var tenantId = Guid.NewGuid(); |
|||
|
|||
using (var uow = unitOfWorkManager.Begin()) |
|||
{ |
|||
using (currentTenant.Change(tenantId)) |
|||
{ |
|||
var user = new IdentityUser(Guid.NewGuid(), "shared-external-signin", "shared-external-signin@abp.io", tenantId); |
|||
(await userManager.CreateAsync(user, "Shared!9Aa")).Succeeded.ShouldBeTrue(); |
|||
(await userManager.AddLoginAsync(user, new UserLoginInfo(loginProvider, providerKey, "Test Provider"))).Succeeded.ShouldBeTrue(); |
|||
} |
|||
await uow.CompleteAsync(); |
|||
} |
|||
|
|||
var response = await Client.GetAsync($"/api/signin-test/external-login-signin?loginProvider={loginProvider}&providerKey={providerKey}"); |
|||
response.EnsureSuccessStatusCode(); |
|||
var result = await response.Content.ReadAsStringAsync(); |
|||
|
|||
result.ShouldBe("Succeeded"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task ExternalLoginSignInAsync_Should_Fail_For_Unknown_Provider_Key_In_Shared_Mode() |
|||
{ |
|||
var response = await Client.GetAsync($"/api/signin-test/external-login-signin?loginProvider=unknown&providerKey=none"); |
|||
response.EnsureSuccessStatusCode(); |
|||
var result = await response.Content.ReadAsStringAsync(); |
|||
|
|||
result.ShouldBe("Failed"); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue