forked from gongxuegit/tiku-backend.net
feat: harden SaaS authentication and authorization
This commit is contained in:
94
Tiku.IntegrationTests/Api/SmsVerificationConcurrencyTests.cs
Normal file
94
Tiku.IntegrationTests/Api/SmsVerificationConcurrencyTests.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Auth;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.IntegrationTests.Api;
|
||||
|
||||
public sealed class SmsVerificationConcurrencyTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Concurrent_verification_consumes_a_code_exactly_once()
|
||||
{
|
||||
await using var factory = new ApiTestFactory();
|
||||
var seed = await SeedCodeAsync(factory, "123456");
|
||||
|
||||
var results = await Task.WhenAll(
|
||||
TryVerifyAsync(factory, seed.TenantId, "123456"),
|
||||
TryVerifyAsync(factory, seed.TenantId, "123456"));
|
||||
|
||||
Assert.Single(results, succeeded => succeeded);
|
||||
using var scope = factory.CreateSystemScope("Verify concurrent SMS consumption");
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
var verification = await dbContext.SmsVerificationCodes.SingleAsync(item => item.Id == seed.CodeId);
|
||||
Assert.Equal(SmsVerificationStatus.Verified, verification.Status);
|
||||
Assert.NotNull(verification.ConsumedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Concurrent_invalid_attempts_atomically_block_on_the_fifth_failure()
|
||||
{
|
||||
await using var factory = new ApiTestFactory();
|
||||
var seed = await SeedCodeAsync(factory, "123456");
|
||||
|
||||
var results = await Task.WhenAll(Enumerable.Range(0, 5)
|
||||
.Select(_ => TryVerifyAsync(factory, seed.TenantId, "999999")));
|
||||
|
||||
Assert.DoesNotContain(true, results);
|
||||
using var scope = factory.CreateSystemScope("Verify concurrent SMS blocking");
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
var verification = await dbContext.SmsVerificationCodes.SingleAsync(item => item.Id == seed.CodeId);
|
||||
Assert.Equal(5, verification.Attempts);
|
||||
Assert.Equal(SmsVerificationStatus.Blocked, verification.Status);
|
||||
}
|
||||
|
||||
private static async Task<bool> TryVerifyAsync(ApiTestFactory factory, Guid tenantId, string code)
|
||||
{
|
||||
using var scope = factory.CreateSystemScope("Concurrent SMS verification");
|
||||
var service = scope.ServiceProvider.GetRequiredService<ISmsVerificationService>();
|
||||
try
|
||||
{
|
||||
await service.VerifyCodeAsync(tenantId, "13800000000", SmsPurpose.Login, code);
|
||||
return true;
|
||||
}
|
||||
catch (InvalidCredentialsException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<(Guid TenantId, Guid CodeId)> SeedCodeAsync(
|
||||
ApiTestFactory factory,
|
||||
string code)
|
||||
{
|
||||
using var scope = factory.CreateSystemScope("Read SMS test options");
|
||||
var options = scope.ServiceProvider.GetRequiredService<IOptions<SmsSecurityOptions>>().Value;
|
||||
var tenantId = Guid.NewGuid();
|
||||
var verification = new SmsVerificationCode
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Phone = "13800000000",
|
||||
Purpose = SmsPurpose.Login,
|
||||
CodeHash = SmsCodeHashing.Hash(
|
||||
tenantId,
|
||||
"13800000000",
|
||||
SmsPurpose.Login,
|
||||
code,
|
||||
options.CodePepper),
|
||||
Status = SmsVerificationStatus.Sent,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(5)
|
||||
};
|
||||
await factory.SeedAsync(
|
||||
new Tenant
|
||||
{
|
||||
Id = tenantId,
|
||||
Slug = tenantId.ToString("N"),
|
||||
Name = "Concurrent SMS Tenant"
|
||||
},
|
||||
verification);
|
||||
return (tenantId, verification.Id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user