using System; using System.Collections.Generic; using MediaBrowser.Model.Cryptography; namespace Jellyfin.Plugin.WatchedTogether.Tests; /// /// A crypto provider that accepts exactly the (stored hash, submitted password) pairs it is given. /// /// /// Hand-written rather than mocked: takes a /// ReadOnlySpan<char>, and a ref struct cannot be used as a generic type argument to /// Moq's It.IsAny<T>. /// public sealed class StubCryptoProvider : ICryptoProvider { private readonly IReadOnlyList<(string Hash, string Password)> _validPairs; public StubCryptoProvider(IReadOnlyList<(string Hash, string Password)> validPairs) { _validPairs = validPairs; } /// /// Gets the salt of each credential this provider was asked to verify, in call order. Lets a /// test assert which member's password was checked first. /// public List VerifiedSalts { get; } = new(); public string DefaultHashMethod => "PBKDF2-SHA512"; public bool Verify(PasswordHash hash, ReadOnlySpan password) { var candidate = password.ToString(); // Identify the stored credential by its salt rather than by re-formatting the whole hash, // which need not round-trip through Parse/ToString byte for byte. var salt = Convert.ToHexString(hash.Salt); VerifiedSalts.Add(salt); foreach (var (validHash, validPassword) in _validPairs) { var expectedSalt = validHash.Split('$')[3]; if (string.Equals(salt, expectedSalt, StringComparison.OrdinalIgnoreCase) && candidate == validPassword) { return true; } } return false; } public PasswordHash CreatePasswordHash(ReadOnlySpan password) => throw new NotSupportedException(); public byte[] GenerateSalt() => throw new NotSupportedException(); public byte[] GenerateSalt(int length) => throw new NotSupportedException(); }