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(); 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(); 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 TryVerifyAsync(ApiTestFactory factory, Guid tenantId, string code) { using var scope = factory.CreateSystemScope("Concurrent SMS verification"); var service = scope.ServiceProvider.GetRequiredService(); 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>().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); } }