mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 06:03:12 +00:00
366 lines
20 KiB
C#
366 lines
20 KiB
C#
using Data.SeaHavenIndustries;
|
|
using FluentAssertions;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.Extensions.Options;
|
|
using Moq;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
using SeaHaven.Services.Configuration;
|
|
using SeaHaven.Services.DTOs;
|
|
using SeaHaven.Services.Helpers;
|
|
using SeaHaven.Services.Implementation;
|
|
using SeaHaven.Services.Interfaces;
|
|
using System.Security.Claims;
|
|
using Xunit;
|
|
|
|
namespace Api.SeaHavenIndustries.Tests;
|
|
|
|
public class AuthenticationServiceTests
|
|
{
|
|
private static AuthenticationService NewService(
|
|
Mock<IUserDataService> userData,
|
|
Mock<IForgetPasswordDataService> forget,
|
|
Mock<IPasswordResetEmailQueue> email,
|
|
out Mock<IUserRoleStore<ApplicationUser>> store,
|
|
out Mock<IPasswordHasher<ApplicationUser>> hasher)
|
|
{
|
|
var (manager, s, h) = IdentityTestHelpers.CreateUserManager();
|
|
store = s;
|
|
hasher = h;
|
|
return new AuthenticationService(manager, Microsoft.Extensions.Options.Options.Create(JwtOptions), userData.Object, forget.Object, email.Object, new InMemoryPasswordResetThrottle(TimeProvider.System), TimeProvider.System);
|
|
}
|
|
|
|
private static JwtOptions JwtOptions => new()
|
|
{
|
|
Secret = new string('x', 64),
|
|
ValidIssuer = "issuer",
|
|
ValidAudience = "audience"
|
|
};
|
|
|
|
[Fact]
|
|
public async Task Login_UnknownUser_ReturnsNull()
|
|
{
|
|
var service = NewService(new Mock<IUserDataService>(), new Mock<IForgetPasswordDataService>(), new Mock<IPasswordResetEmailQueue>(), out var store, out _);
|
|
store.Setup(s => s.FindByNameAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync((ApplicationUser?)null);
|
|
|
|
var result = await service.LoginAsync("nobody", "pw", CancellationToken.None);
|
|
|
|
result.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Login_DeletedUser_RejectedBeforePasswordCheck()
|
|
{
|
|
var deletedUser = IdentityTestHelpers.User(isDeleted: true);
|
|
var service = NewService(new Mock<IUserDataService>(), new Mock<IForgetPasswordDataService>(), new Mock<IPasswordResetEmailQueue>(), out var store, out var hasher);
|
|
store.Setup(s => s.FindByNameAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(deletedUser);
|
|
store.Setup(s => s.GetRolesAsync(deletedUser, It.IsAny<CancellationToken>())).ReturnsAsync(new List<string>());
|
|
|
|
var result = await service.LoginAsync("alice", "pw", CancellationToken.None);
|
|
|
|
result.Should().BeNull();
|
|
hasher.Verify(h => h.VerifyHashedPassword(It.IsAny<ApplicationUser>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Login_ValidUser_ReturnsTokenFirstRoleAndIdentity()
|
|
{
|
|
var user = IdentityTestHelpers.User();
|
|
var service = NewService(new Mock<IUserDataService>(), new Mock<IForgetPasswordDataService>(), new Mock<IPasswordResetEmailQueue>(), out var store, out var hasher);
|
|
store.Setup(s => s.FindByNameAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(user);
|
|
store.Setup(s => s.GetRolesAsync(user, It.IsAny<CancellationToken>())).ReturnsAsync(new List<string> { "Admin", "Manager" });
|
|
store.As<Microsoft.AspNetCore.Identity.IUserPasswordStore<ApplicationUser>>()
|
|
.Setup(s => s.GetPasswordHashAsync(user, It.IsAny<CancellationToken>())).ReturnsAsync("hash");
|
|
hasher.Setup(h => h.VerifyHashedPassword(user, "hash", "pw")).Returns(Microsoft.AspNetCore.Identity.PasswordVerificationResult.Success);
|
|
|
|
var result = await service.LoginAsync("alice", "pw", CancellationToken.None);
|
|
|
|
result.Should().NotBeNull();
|
|
result!.Id.Should().Be(user.Id);
|
|
result.Email.Should().Be(user.Email);
|
|
result.PhoneNumber.Should().Be(user.PhoneNumber);
|
|
result.Fullname.Should().Be("Alice Q");
|
|
result.UserRole.Should().Be("Admin");
|
|
result.Token.Should().NotBeNullOrEmpty();
|
|
result.Expiration.Should().BeAfter(DateTime.Now.AddDays(9));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Login_BadPassword_ReturnsNull()
|
|
{
|
|
var user = IdentityTestHelpers.User();
|
|
var service = NewService(new Mock<IUserDataService>(), new Mock<IForgetPasswordDataService>(), new Mock<IPasswordResetEmailQueue>(), out var store, out var hasher);
|
|
store.Setup(s => s.FindByNameAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(user);
|
|
store.As<Microsoft.AspNetCore.Identity.IUserPasswordStore<ApplicationUser>>()
|
|
.Setup(s => s.GetPasswordHashAsync(user, It.IsAny<CancellationToken>())).ReturnsAsync("hash");
|
|
hasher.Setup(h => h.VerifyHashedPassword(user, "hash", "wrong")).Returns(Microsoft.AspNetCore.Identity.PasswordVerificationResult.Failed);
|
|
|
|
var result = await service.LoginAsync("alice", "wrong", CancellationToken.None);
|
|
|
|
result.Should().BeNull();
|
|
}
|
|
|
|
private static byte[] ResetKey => PasswordResetCodeSecrets.DeriveKey(JwtOptions.Secret);
|
|
|
|
[Fact]
|
|
public async Task ForgetPassword_RegisteredEmail_StoresOnlyAKeyedHashAndQueuesTheCode()
|
|
{
|
|
var user = IdentityTestHelpers.User();
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetByEmailNormalizedAsync("alice@example.com", It.IsAny<CancellationToken>())).ReturnsAsync(user);
|
|
var forget = new Mock<IForgetPasswordDataService>();
|
|
var email = new Mock<IPasswordResetEmailQueue>();
|
|
string? stored = null, salt = null, body = null;
|
|
DateTime expires = default;
|
|
forget.Setup(f => f.ReplaceCodeAsync(user.Email!, user.Id, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<DateTime>(), It.IsAny<DateTime>(), It.IsAny<CancellationToken>()))
|
|
.Callback<string, string, string, string, DateTime, DateTime, CancellationToken>((_, _, h, s, e, _, _) => { stored = h; salt = s; expires = e; })
|
|
.Returns(Task.CompletedTask);
|
|
email.Setup(e => e.TryEnqueue(user.Email!, "Forget Password Request.", It.IsAny<string>()))
|
|
.Callback<string, string, string>((_, _, b) => body = b)
|
|
.Returns(true);
|
|
|
|
var service = NewService(userData, forget, email, out _, out _);
|
|
var before = DateTime.UtcNow;
|
|
|
|
await service.ForgetPasswordAsync("alice@example.com", CancellationToken.None);
|
|
|
|
var code = System.Text.RegularExpressions.Regex.Match(body!, @"Your Password Reset Code is: (\d{6})").Groups[1].Value;
|
|
code.Should().HaveLength(6);
|
|
stored.Should().NotBe(code).And.MatchRegex("^[0-9a-f]{64}$");
|
|
PasswordResetCodeSecrets.Matches(ResetKey, salt!, code, stored!).Should().BeTrue();
|
|
expires.Should().BeCloseTo(before.AddMinutes(15), TimeSpan.FromSeconds(5));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ForgetPassword_UnknownEmail_MakesTheSameDataCallsAndQueuesNothing()
|
|
{
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetByEmailNormalizedAsync("alice@example.com", It.IsAny<CancellationToken>())).ReturnsAsync(IdentityTestHelpers.User());
|
|
var registered = new Mock<IForgetPasswordDataService>();
|
|
var unregistered = new Mock<IForgetPasswordDataService>();
|
|
var email = new Mock<IPasswordResetEmailQueue>();
|
|
email.Setup(e => e.TryEnqueue(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(true);
|
|
|
|
await NewService(userData, registered, email, out _, out _).ForgetPasswordAsync("alice@example.com", CancellationToken.None);
|
|
await NewService(userData, unregistered, email, out _, out _).ForgetPasswordAsync("nope@example.com", CancellationToken.None);
|
|
|
|
registered.Invocations.Select(call => call.Method.Name)
|
|
.Should().Equal(unregistered.Invocations.Select(call => call.Method.Name))
|
|
.And.Equal(nameof(IForgetPasswordDataService.ReplaceCodeAsync));
|
|
unregistered.Verify(f => f.ReplaceCodeAsync("nope@example.com", string.Empty, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<DateTime>(), It.IsAny<DateTime>(), It.IsAny<CancellationToken>()), Times.Once);
|
|
email.Verify(e => e.TryEnqueue(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ForgetPassword_EmailThatCannotBeQueued_DropsTheCodeAndDoesNotCountTheRequest()
|
|
{
|
|
var user = IdentityTestHelpers.User();
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetByEmailNormalizedAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(user);
|
|
var forget = new Mock<IForgetPasswordDataService>();
|
|
var stored = new List<(string Hash, string Salt)>();
|
|
forget.Setup(f => f.ReplaceCodeAsync(user.Email!, user.Id, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<DateTime>(), It.IsAny<DateTime>(), It.IsAny<CancellationToken>()))
|
|
.Callback<string, string, string, string, DateTime, DateTime, CancellationToken>((_, _, h, s, _, _, _) => stored.Add((h, s)))
|
|
.Returns(Task.CompletedTask);
|
|
var email = new Mock<IPasswordResetEmailQueue>();
|
|
var bodies = new List<string>();
|
|
// The queue is full for the first three requests.
|
|
email.Setup(e => e.TryEnqueue(user.Email!, It.IsAny<string>(), It.IsAny<string>()))
|
|
.Returns<string, string, string>((_, _, b) =>
|
|
{
|
|
bodies.Add(b);
|
|
return bodies.Count > 3;
|
|
});
|
|
|
|
var service = NewService(userData, forget, email, out _, out _);
|
|
for (var request = 0; request < 4; request++)
|
|
await service.ForgetPasswordAsync("alice@example.com", CancellationToken.None);
|
|
|
|
// The three undelivered requests did not use up the hourly limit of three.
|
|
email.Verify(e => e.TryEnqueue(user.Email!, It.IsAny<string>(), It.IsAny<string>()), Times.Exactly(4));
|
|
stored.Should().HaveCount(4);
|
|
for (var request = 0; request < 3; request++)
|
|
{
|
|
var code = System.Text.RegularExpressions.Regex.Match(bodies[request], @"Your Password Reset Code is: (\d{6})").Groups[1].Value;
|
|
PasswordResetCodeSecrets.Matches(ResetKey, stored[request].Salt, code, stored[request].Hash).Should().BeFalse();
|
|
}
|
|
var delivered = System.Text.RegularExpressions.Regex.Match(bodies[3], @"Your Password Reset Code is: (\d{6})").Groups[1].Value;
|
|
PasswordResetCodeSecrets.Matches(ResetKey, stored[3].Salt, delivered, stored[3].Hash).Should().BeTrue();
|
|
forget.Verify(f => f.RemoveByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ForgetPassword_EmailThatCannotBeQueued_MakesTheSameDataCallsAsAnUnknownEmail()
|
|
{
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetByEmailNormalizedAsync("alice@example.com", It.IsAny<CancellationToken>())).ReturnsAsync(IdentityTestHelpers.User());
|
|
var registered = new Mock<IForgetPasswordDataService>();
|
|
var unregistered = new Mock<IForgetPasswordDataService>();
|
|
var email = new Mock<IPasswordResetEmailQueue>();
|
|
email.Setup(e => e.TryEnqueue(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(false);
|
|
|
|
await NewService(userData, registered, email, out _, out _).ForgetPasswordAsync("alice@example.com", CancellationToken.None);
|
|
await NewService(userData, unregistered, email, out _, out _).ForgetPasswordAsync("nope@example.com", CancellationToken.None);
|
|
|
|
registered.Invocations.Select(call => call.Method.Name)
|
|
.Should().Equal(unregistered.Invocations.Select(call => call.Method.Name))
|
|
.And.Equal(nameof(IForgetPasswordDataService.ReplaceCodeAsync));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ForgetPassword_DeletedAccount_IsTreatedAsUnregistered()
|
|
{
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetByEmailNormalizedAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(IdentityTestHelpers.User(isDeleted: true));
|
|
var forget = new Mock<IForgetPasswordDataService>();
|
|
var email = new Mock<IPasswordResetEmailQueue>();
|
|
|
|
var service = NewService(userData, forget, email, out _, out _);
|
|
|
|
await service.ForgetPasswordAsync("alice@example.com", CancellationToken.None);
|
|
|
|
email.Verify(e => e.TryEnqueue(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null, "123456")]
|
|
[InlineData("", "123456")]
|
|
[InlineData(" ", "123456")]
|
|
[InlineData("a@b.com", null)]
|
|
[InlineData("a@b.com", "")]
|
|
public async Task VerifyCode_WithoutEmailOrCode_FailsWithoutTouchingStoredCodes(string? emailAddress, string? code)
|
|
{
|
|
var forget = new Mock<IForgetPasswordDataService>(MockBehavior.Strict);
|
|
|
|
var service = NewService(new Mock<IUserDataService>(), forget, new Mock<IPasswordResetEmailQueue>(), out _, out _);
|
|
|
|
var result = await service.VerifyCodeAsync(emailAddress, code, CancellationToken.None);
|
|
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ResetPassword_NoPendingCodeForEmail_ReturnsFalseWithoutReset()
|
|
{
|
|
var forget = new Mock<IForgetPasswordDataService>();
|
|
forget.Setup(f => f.GetByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync((ForgetPasswordCode?)null);
|
|
|
|
var service = NewService(new Mock<IUserDataService>(), forget, new Mock<IPasswordResetEmailQueue>(), out var store, out _);
|
|
|
|
var result = await service.ResetPasswordAsync("a@b.com", "999999", "new", CancellationToken.None);
|
|
|
|
result.Should().BeFalse();
|
|
store.Verify(s => s.FindByIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
|
forget.Verify(f => f.TryConsumeAttemptAsync(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<DateTime>(), It.IsAny<CancellationToken>()), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ResetPassword_AttemptBudgetSpent_DeletesTheCodeAndFails()
|
|
{
|
|
var pending = new ForgetPasswordCode { Id = 7, Email = "a@b.com", UserId = "u1", CodeSalt = "s", CodeHash = PasswordResetCodeSecrets.Hash(ResetKey, "s", "123456"), FailedAttempts = 5 };
|
|
var forget = new Mock<IForgetPasswordDataService>();
|
|
forget.Setup(f => f.GetByEmailAsync("a@b.com", It.IsAny<CancellationToken>())).ReturnsAsync(pending);
|
|
forget.Setup(f => f.TryConsumeAttemptAsync(7, AuthenticationService.MaxCodeAttempts, It.IsAny<DateTime>(), It.IsAny<CancellationToken>())).ReturnsAsync(false);
|
|
|
|
var service = NewService(new Mock<IUserDataService>(), forget, new Mock<IPasswordResetEmailQueue>(), out var store, out _);
|
|
|
|
var result = await service.ResetPasswordAsync("a@b.com", "123456", "New@67890", CancellationToken.None);
|
|
|
|
result.Should().BeFalse();
|
|
forget.Verify(f => f.RemoveIssuedThroughAsync("a@b.com", 7, It.IsAny<CancellationToken>()), Times.Once);
|
|
store.Verify(s => s.FindByIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public async Task VerifyCode_FailedOrCancelledLookups_LeaveTheAccountCheckBudgetUntouched(bool cancelled)
|
|
{
|
|
var pending = new ForgetPasswordCode { Id = 7, Email = "a@b.com", UserId = "u1", CodeSalt = "s", CodeHash = PasswordResetCodeSecrets.Hash(ResetKey, "s", "123456") };
|
|
var forget = new Mock<IForgetPasswordDataService>();
|
|
var lookups = 0;
|
|
forget.Setup(f => f.GetByEmailAsync("a@b.com", It.IsAny<CancellationToken>()))
|
|
.Returns(() => ++lookups <= InMemoryPasswordResetThrottle.FailedChecksPerDay
|
|
? Task.FromException<ForgetPasswordCode?>(cancelled ? new OperationCanceledException() : new InvalidOperationException("database unavailable"))
|
|
: Task.FromResult<ForgetPasswordCode?>(pending));
|
|
forget.Setup(f => f.TryConsumeAttemptAsync(7, AuthenticationService.MaxCodeAttempts, It.IsAny<DateTime>(), It.IsAny<CancellationToken>())).ReturnsAsync(true);
|
|
var service = NewService(new Mock<IUserDataService>(), forget, new Mock<IPasswordResetEmailQueue>(), out _, out _);
|
|
|
|
for (var check = 0; check < InMemoryPasswordResetThrottle.FailedChecksPerDay; check++)
|
|
{
|
|
var act = () => service.VerifyCodeAsync("a@b.com", "123456", CancellationToken.None);
|
|
await act.Should().ThrowAsync<Exception>();
|
|
}
|
|
|
|
(await service.VerifyCodeAsync("a@b.com", "123456", CancellationToken.None)).Should().BeTrue();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public async Task VerifyCode_FailedOrCancelledAttemptConsumes_LeaveTheAccountCheckBudgetUntouched(bool cancelled)
|
|
{
|
|
var pending = new ForgetPasswordCode { Id = 7, Email = "a@b.com", UserId = "u1", CodeSalt = "s", CodeHash = PasswordResetCodeSecrets.Hash(ResetKey, "s", "123456") };
|
|
var forget = new Mock<IForgetPasswordDataService>();
|
|
forget.Setup(f => f.GetByEmailAsync("a@b.com", It.IsAny<CancellationToken>())).ReturnsAsync(pending);
|
|
var consumes = 0;
|
|
forget.Setup(f => f.TryConsumeAttemptAsync(7, AuthenticationService.MaxCodeAttempts, It.IsAny<DateTime>(), It.IsAny<CancellationToken>()))
|
|
.Returns(() => ++consumes <= InMemoryPasswordResetThrottle.FailedChecksPerDay
|
|
? Task.FromException<bool>(cancelled ? new OperationCanceledException() : new InvalidOperationException("database unavailable"))
|
|
: Task.FromResult(true));
|
|
var service = NewService(new Mock<IUserDataService>(), forget, new Mock<IPasswordResetEmailQueue>(), out _, out _);
|
|
|
|
for (var check = 0; check < InMemoryPasswordResetThrottle.FailedChecksPerDay; check++)
|
|
{
|
|
var act = () => service.VerifyCodeAsync("a@b.com", "123456", CancellationToken.None);
|
|
await act.Should().ThrowAsync<Exception>();
|
|
}
|
|
|
|
(await service.VerifyCodeAsync("a@b.com", "123456", CancellationToken.None)).Should().BeTrue();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public async Task ForgetPassword_FailedOrCancelledWrites_DoNotUseUpTheEmailRequestLimit(bool cancelled)
|
|
{
|
|
var user = IdentityTestHelpers.User();
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetByEmailNormalizedAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(user);
|
|
var forget = new Mock<IForgetPasswordDataService>();
|
|
var writes = 0;
|
|
forget.Setup(f => f.ReplaceCodeAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<DateTime>(), It.IsAny<DateTime>(), It.IsAny<CancellationToken>()))
|
|
.Returns(() => ++writes <= InMemoryPasswordResetThrottle.CodeRequestsPerHour
|
|
? Task.FromException(cancelled ? new OperationCanceledException() : new InvalidOperationException("database unavailable"))
|
|
: Task.CompletedTask);
|
|
var email = new Mock<IPasswordResetEmailQueue>();
|
|
email.Setup(e => e.TryEnqueue(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(true);
|
|
var service = NewService(userData, forget, email, out _, out _);
|
|
|
|
for (var request = 0; request < InMemoryPasswordResetThrottle.CodeRequestsPerHour; request++)
|
|
{
|
|
var act = () => service.ForgetPasswordAsync("alice@example.com", CancellationToken.None);
|
|
await act.Should().ThrowAsync<Exception>();
|
|
}
|
|
|
|
await service.ForgetPasswordAsync("alice@example.com", CancellationToken.None);
|
|
|
|
writes.Should().Be(InMemoryPasswordResetThrottle.CodeRequestsPerHour + 1);
|
|
forget.Verify(f => f.PurgeExpiredAsync(It.IsAny<DateTime>(), It.IsAny<CancellationToken>()), Times.Never);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("123456", "123456", true)]
|
|
[InlineData("123456", " 123456 ", true)]
|
|
[InlineData("123456", "123457", false)]
|
|
public void ResetCodeHash_IsSaltedAndComparedByValue(string issued, string candidate, bool expected)
|
|
{
|
|
var salt = PasswordResetCodeSecrets.NewSalt();
|
|
var hash = PasswordResetCodeSecrets.Hash(ResetKey, salt, issued);
|
|
|
|
PasswordResetCodeSecrets.Matches(ResetKey, salt, candidate, hash).Should().Be(expected);
|
|
PasswordResetCodeSecrets.Hash(ResetKey, PasswordResetCodeSecrets.NewSalt(), issued).Should().NotBe(hash);
|
|
PasswordResetCodeSecrets.Matches(ResetKey, "", issued, hash).Should().BeFalse();
|
|
PasswordResetCodeSecrets.Matches(ResetKey, salt, issued, "").Should().BeFalse();
|
|
}
|
|
}
|