krona-staff/apps/KronaStaffTerminal.Tests/AdminPinServiceTests.cs

124 lines
4.1 KiB
C#

using System.Security.Cryptography;
using System.Text;
using KronaStaffTerminal.Models;
using KronaStaffTerminal.Services;
using Xunit;
namespace KronaStaffTerminal.Tests;
public class AdminPinServiceTests
{
[Fact]
public void HashAndVerify_ValidPin_ReturnsTrue()
{
var pin = "1234";
var (salt, hash, iterations) = AdminPinService.Hash(pin);
Assert.NotNull(salt);
Assert.StartsWith(AdminPinService.Argon2Prefix, hash);
Assert.True(iterations >= 1);
var verified = AdminPinService.Verify(pin, salt, hash, iterations);
Assert.True(verified);
}
[Fact]
public void Verify_InvalidPin_ReturnsFalse()
{
var (salt, hash, iterations) = AdminPinService.Hash("5678");
var verified = AdminPinService.Verify("0000", salt, hash, iterations);
Assert.False(verified);
}
[Fact]
public void Verify_LegacyPbkdf2Hash_VerifiesSuccessfully()
{
var pin = "9876";
var salt = RandomNumberGenerator.GetBytes(16);
var saltBase64 = Convert.ToBase64String(salt);
var legacyIterations = 120_000;
var hashBytes = Rfc2898DeriveBytes.Pbkdf2(
Encoding.UTF8.GetBytes(pin),
salt,
legacyIterations,
HashAlgorithmName.SHA256,
32);
var hashBase64 = Convert.ToBase64String(hashBytes);
var verified = AdminPinService.Verify(pin, saltBase64, hashBase64, legacyIterations);
Assert.True(verified);
}
[Fact]
public void VerifyAndRecord_LegacyHashUpgradesToArgon2id()
{
var pin = "4321";
var salt = RandomNumberGenerator.GetBytes(16);
var legacyIterations = 120_000;
var hashBytes = Rfc2898DeriveBytes.Pbkdf2(
Encoding.UTF8.GetBytes(pin),
salt,
legacyIterations,
HashAlgorithmName.SHA256,
32);
var settings = new TerminalSettings
{
AdminPinSalt = Convert.ToBase64String(salt),
AdminPinHash = Convert.ToBase64String(hashBytes),
AdminPinIterations = legacyIterations,
FailedAdminPinAttempts = 0
};
var now = DateTimeOffset.UtcNow;
var success = AdminPinService.VerifyAndRecord(pin, settings, now, out var error, out var rehashNeeded);
Assert.True(success);
Assert.Null(error);
Assert.True(rehashNeeded);
Assert.StartsWith(AdminPinService.Argon2Prefix, settings.AdminPinHash);
}
[Fact]
public void VerifyAndRecord_ThreeFailedAttempts_TriggersLockout()
{
var (salt, hash, iterations) = AdminPinService.Hash("1111");
var settings = new TerminalSettings
{
AdminPinSalt = salt,
AdminPinHash = hash,
AdminPinIterations = iterations,
FailedAdminPinAttempts = 0
};
var now = DateTimeOffset.UtcNow;
// Attempt 1: Fail
var r1 = AdminPinService.VerifyAndRecord("0000", settings, now, out var err1, out _);
Assert.False(r1);
Assert.Contains("до блокировки: 2", err1);
// Attempt 2: Fail
var r2 = AdminPinService.VerifyAndRecord("0000", settings, now, out var err2, out _);
Assert.False(r2);
Assert.Contains("до блокировки: 1", err2);
// Attempt 3: Lockout triggered (30s)
var r3 = AdminPinService.VerifyAndRecord("0000", settings, now, out var err3, out _);
Assert.False(r3);
Assert.Contains("заблокирован на 30 сек", err3);
Assert.True(settings.AdminPinBlockedUntilUtc > now);
// Attempt 4: Even with correct PIN, blocked during lockout
var r4 = AdminPinService.VerifyAndRecord("1111", settings, now.AddSeconds(5), out var err4, out _);
Assert.False(r4);
Assert.Contains("временно заблокирован", err4);
// Attempt 5: After lockout expires, correct PIN succeeds
var r5 = AdminPinService.VerifyAndRecord("1111", settings, now.AddSeconds(35), out var err5, out _);
Assert.True(r5);
Assert.Null(err5);
Assert.Equal(0, settings.FailedAdminPinAttempts);
}
}