feat: add phase five operations foundation
This commit is contained in:
@@ -135,6 +135,60 @@ public sealed class AuthServiceTests
|
||||
entity.FailureCode == "invalid_sms_code");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Sms_send_failure_records_failed_code_and_does_not_leave_usable_verification()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var seed = await SeedUserAsync(context, new PasswordHasher().Hash("passw0rd!"));
|
||||
var smsService = new SmsVerificationService(context, new FailingSmsProvider());
|
||||
|
||||
var exception = await Assert.ThrowsAsync<SmsProviderException>(() =>
|
||||
smsService.CreateCodeAsync(new SendSmsCodeRequest(
|
||||
seed.TenantId,
|
||||
seed.Phone,
|
||||
SmsPurpose.Login,
|
||||
null,
|
||||
null)));
|
||||
|
||||
Assert.Equal("sms_provider_send_failed", exception.Code);
|
||||
var verification = Assert.Single(context.SmsVerificationCodes);
|
||||
Assert.Equal(SmsVerificationStatus.Failed, verification.Status);
|
||||
Assert.Equal("failed", verification.Provider);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidCredentialsException>(() =>
|
||||
smsService.VerifyCodeAsync(
|
||||
seed.TenantId,
|
||||
seed.Phone,
|
||||
SmsPurpose.Login,
|
||||
"123456"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Failed_sms_code_is_never_accepted_even_when_hash_matches()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var seed = await SeedUserAsync(context, new PasswordHasher().Hash("passw0rd!"));
|
||||
context.SmsVerificationCodes.Add(new SmsVerificationCode
|
||||
{
|
||||
TenantId = seed.TenantId,
|
||||
Phone = seed.Phone,
|
||||
Purpose = SmsPurpose.Login,
|
||||
CodeHash = SmsCodeHashing.Hash(seed.TenantId, seed.Phone, SmsPurpose.Login, "123456"),
|
||||
Status = SmsVerificationStatus.Failed,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(5)
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
var smsService = new SmsVerificationService(context, new FakeSmsProvider());
|
||||
|
||||
await Assert.ThrowsAsync<InvalidCredentialsException>(() =>
|
||||
smsService.VerifyCodeAsync(
|
||||
seed.TenantId,
|
||||
seed.Phone,
|
||||
SmsPurpose.Login,
|
||||
"123456"));
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task Revoked_refresh_token_cannot_be_refreshed()
|
||||
{
|
||||
@@ -410,6 +464,14 @@ public sealed class AuthServiceTests
|
||||
Task.FromResult(new SmsProviderSendResult("fake", "accepted"));
|
||||
}
|
||||
|
||||
private sealed class FailingSmsProvider : ISmsProvider
|
||||
{
|
||||
public Task<SmsProviderSendResult> SendAsync(
|
||||
SmsProviderSendRequest request,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new InvalidOperationException("provider unavailable");
|
||||
}
|
||||
|
||||
private sealed class FakeWechatOAuthClient(
|
||||
WechatIdentity? WebIdentity = null,
|
||||
WechatIdentity? MiniAppIdentity = null) : IWechatOAuthClient
|
||||
|
||||
Reference in New Issue
Block a user