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); } }