Add unit tests for RabbitMq and Udp transport servers and clients
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implemented comprehensive unit tests for RabbitMqTransportServer, covering constructor, disposal, connection management, event handlers, and exception handling. - Added configuration tests for RabbitMqTransportServer to validate SSL, durable queues, auto-recovery, and custom virtual host options. - Created unit tests for UdpFrameProtocol, including frame parsing and serialization, header size validation, and round-trip data preservation. - Developed tests for UdpTransportClient, focusing on connection handling, event subscriptions, and exception scenarios. - Established tests for UdpTransportServer, ensuring proper start/stop behavior, connection state management, and event handling. - Included tests for UdpTransportOptions to verify default values and modification capabilities. - Enhanced service registration tests for Udp transport services in the dependency injection container.
This commit is contained in:
@@ -4,43 +4,65 @@ using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Blake3;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Digests;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
public sealed class DefaultCryptoHash : ICryptoHash
|
||||
{
|
||||
private readonly IOptionsMonitor<CryptoHashOptions> options;
|
||||
private readonly ILogger<DefaultCryptoHash> logger;
|
||||
private readonly IOptionsMonitor<CryptoHashOptions> _hashOptions;
|
||||
private readonly IOptionsMonitor<CryptoComplianceOptions> _complianceOptions;
|
||||
private readonly ILogger<DefaultCryptoHash> _logger;
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public DefaultCryptoHash(
|
||||
IOptionsMonitor<CryptoHashOptions> options,
|
||||
IOptionsMonitor<CryptoHashOptions> hashOptions,
|
||||
IOptionsMonitor<CryptoComplianceOptions>? complianceOptions = null,
|
||||
ILogger<DefaultCryptoHash>? logger = null)
|
||||
{
|
||||
this.options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
this.logger = logger ?? NullLogger<DefaultCryptoHash>.Instance;
|
||||
_hashOptions = hashOptions ?? throw new ArgumentNullException(nameof(hashOptions));
|
||||
_complianceOptions = complianceOptions ?? new StaticComplianceOptionsMonitor(new CryptoComplianceOptions());
|
||||
_logger = logger ?? NullLogger<DefaultCryptoHash>.Instance;
|
||||
}
|
||||
|
||||
internal DefaultCryptoHash(CryptoHashOptions? options = null)
|
||||
: this(new StaticOptionsMonitor(options ?? new CryptoHashOptions()), NullLogger<DefaultCryptoHash>.Instance)
|
||||
internal DefaultCryptoHash(CryptoHashOptions? hashOptions = null, CryptoComplianceOptions? complianceOptions = null)
|
||||
: this(
|
||||
new StaticOptionsMonitor(hashOptions ?? new CryptoHashOptions()),
|
||||
new StaticComplianceOptionsMonitor(complianceOptions ?? new CryptoComplianceOptions()),
|
||||
NullLogger<DefaultCryptoHash>.Instance)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="DefaultCryptoHash"/> instance for use in tests.
|
||||
/// Uses default options with no compliance profile.
|
||||
/// </summary>
|
||||
public static DefaultCryptoHash CreateForTests()
|
||||
=> new(new CryptoHashOptions(), new CryptoComplianceOptions());
|
||||
|
||||
public byte[] ComputeHash(ReadOnlySpan<byte> data, string? algorithmId = null)
|
||||
{
|
||||
var algorithm = NormalizeAlgorithm(algorithmId);
|
||||
return algorithm switch
|
||||
return ComputeHashWithAlgorithm(data, algorithm);
|
||||
}
|
||||
|
||||
private static byte[] ComputeHashWithAlgorithm(ReadOnlySpan<byte> data, string algorithm)
|
||||
{
|
||||
return algorithm.ToUpperInvariant() switch
|
||||
{
|
||||
HashAlgorithms.Sha256 => ComputeSha256(data),
|
||||
HashAlgorithms.Sha512 => ComputeSha512(data),
|
||||
HashAlgorithms.Gost3411_2012_256 => GostDigestUtilities.ComputeDigest(data, use256: true),
|
||||
HashAlgorithms.Gost3411_2012_512 => GostDigestUtilities.ComputeDigest(data, use256: false),
|
||||
_ => throw new InvalidOperationException($"Unsupported hash algorithm {algorithm}.")
|
||||
"SHA256" => ComputeSha256(data),
|
||||
"SHA384" => ComputeSha384(data),
|
||||
"SHA512" => ComputeSha512(data),
|
||||
"GOST3411-2012-256" => GostDigestUtilities.ComputeDigest(data, use256: true),
|
||||
"GOST3411-2012-512" => GostDigestUtilities.ComputeDigest(data, use256: false),
|
||||
"BLAKE3-256" => ComputeBlake3(data),
|
||||
"SM3" => ComputeSm3(data),
|
||||
_ => throw new InvalidOperationException($"Unsupported hash algorithm '{algorithm}'.")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -56,13 +78,21 @@ public sealed class DefaultCryptoHash : ICryptoHash
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var algorithm = NormalizeAlgorithm(algorithmId);
|
||||
return algorithm switch
|
||||
return await ComputeHashWithAlgorithmAsync(stream, algorithm, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async ValueTask<byte[]> ComputeHashWithAlgorithmAsync(Stream stream, string algorithm, CancellationToken cancellationToken)
|
||||
{
|
||||
return algorithm.ToUpperInvariant() switch
|
||||
{
|
||||
HashAlgorithms.Sha256 => await ComputeShaStreamAsync(HashAlgorithmName.SHA256, stream, cancellationToken).ConfigureAwait(false),
|
||||
HashAlgorithms.Sha512 => await ComputeShaStreamAsync(HashAlgorithmName.SHA512, stream, cancellationToken).ConfigureAwait(false),
|
||||
HashAlgorithms.Gost3411_2012_256 => await ComputeGostStreamAsync(use256: true, stream, cancellationToken).ConfigureAwait(false),
|
||||
HashAlgorithms.Gost3411_2012_512 => await ComputeGostStreamAsync(use256: false, stream, cancellationToken).ConfigureAwait(false),
|
||||
_ => throw new InvalidOperationException($"Unsupported hash algorithm {algorithm}.")
|
||||
"SHA256" => await ComputeShaStreamAsync(HashAlgorithmName.SHA256, stream, cancellationToken).ConfigureAwait(false),
|
||||
"SHA384" => await ComputeShaStreamAsync(HashAlgorithmName.SHA384, stream, cancellationToken).ConfigureAwait(false),
|
||||
"SHA512" => await ComputeShaStreamAsync(HashAlgorithmName.SHA512, stream, cancellationToken).ConfigureAwait(false),
|
||||
"GOST3411-2012-256" => await ComputeGostStreamAsync(use256: true, stream, cancellationToken).ConfigureAwait(false),
|
||||
"GOST3411-2012-512" => await ComputeGostStreamAsync(use256: false, stream, cancellationToken).ConfigureAwait(false),
|
||||
"BLAKE3-256" => await ComputeBlake3StreamAsync(stream, cancellationToken).ConfigureAwait(false),
|
||||
"SM3" => await ComputeSm3StreamAsync(stream, cancellationToken).ConfigureAwait(false),
|
||||
_ => throw new InvalidOperationException($"Unsupported hash algorithm '{algorithm}'.")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -130,7 +160,7 @@ public sealed class DefaultCryptoHash : ICryptoHash
|
||||
|
||||
private string NormalizeAlgorithm(string? algorithmId)
|
||||
{
|
||||
var defaultAlgorithm = options.CurrentValue?.DefaultAlgorithm;
|
||||
var defaultAlgorithm = _hashOptions.CurrentValue?.DefaultAlgorithm;
|
||||
if (!string.IsNullOrWhiteSpace(algorithmId))
|
||||
{
|
||||
return algorithmId.Trim().ToUpperInvariant();
|
||||
@@ -144,26 +174,194 @@ public sealed class DefaultCryptoHash : ICryptoHash
|
||||
return HashAlgorithms.Sha256;
|
||||
}
|
||||
|
||||
#region Purpose-based methods
|
||||
|
||||
private ComplianceProfile GetActiveProfile()
|
||||
{
|
||||
var opts = _complianceOptions.CurrentValue;
|
||||
opts.ApplyEnvironmentOverrides();
|
||||
return ComplianceProfiles.GetProfile(opts.ProfileId);
|
||||
}
|
||||
|
||||
public byte[] ComputeHashForPurpose(ReadOnlySpan<byte> data, string purpose)
|
||||
{
|
||||
var algorithm = GetAlgorithmForPurpose(purpose);
|
||||
return ComputeHashWithAlgorithm(data, algorithm);
|
||||
}
|
||||
|
||||
public string ComputeHashHexForPurpose(ReadOnlySpan<byte> data, string purpose)
|
||||
=> Convert.ToHexString(ComputeHashForPurpose(data, purpose)).ToLowerInvariant();
|
||||
|
||||
public string ComputeHashBase64ForPurpose(ReadOnlySpan<byte> data, string purpose)
|
||||
=> Convert.ToBase64String(ComputeHashForPurpose(data, purpose));
|
||||
|
||||
public async ValueTask<byte[]> ComputeHashForPurposeAsync(Stream stream, string purpose, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var algorithm = GetAlgorithmForPurpose(purpose);
|
||||
return await ComputeHashWithAlgorithmAsync(stream, algorithm, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask<string> ComputeHashHexForPurposeAsync(Stream stream, string purpose, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bytes = await ComputeHashForPurposeAsync(stream, purpose, cancellationToken).ConfigureAwait(false);
|
||||
return Convert.ToHexString(bytes).ToLowerInvariant();
|
||||
}
|
||||
|
||||
public string GetAlgorithmForPurpose(string purpose)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(purpose))
|
||||
{
|
||||
throw new ArgumentException("Purpose cannot be null or empty.", nameof(purpose));
|
||||
}
|
||||
|
||||
var opts = _complianceOptions.CurrentValue;
|
||||
opts.ApplyEnvironmentOverrides();
|
||||
|
||||
// Check for purpose overrides first
|
||||
if (opts.PurposeOverrides?.TryGetValue(purpose, out var overrideAlgorithm) == true &&
|
||||
!string.IsNullOrWhiteSpace(overrideAlgorithm))
|
||||
{
|
||||
return overrideAlgorithm;
|
||||
}
|
||||
|
||||
// Get from active profile
|
||||
var profile = ComplianceProfiles.GetProfile(opts.ProfileId);
|
||||
return profile.GetAlgorithmForPurpose(purpose);
|
||||
}
|
||||
|
||||
public string GetHashPrefix(string purpose)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(purpose))
|
||||
{
|
||||
throw new ArgumentException("Purpose cannot be null or empty.", nameof(purpose));
|
||||
}
|
||||
|
||||
var profile = GetActiveProfile();
|
||||
return profile.GetHashPrefix(purpose);
|
||||
}
|
||||
|
||||
public string ComputePrefixedHashForPurpose(ReadOnlySpan<byte> data, string purpose)
|
||||
{
|
||||
var prefix = GetHashPrefix(purpose);
|
||||
var hash = ComputeHashHexForPurpose(data, purpose);
|
||||
return prefix + hash;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Algorithm implementations
|
||||
|
||||
private static byte[] ComputeSha384(ReadOnlySpan<byte> data)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[48];
|
||||
SHA384.HashData(data, buffer);
|
||||
return buffer.ToArray();
|
||||
}
|
||||
|
||||
private static byte[] ComputeBlake3(ReadOnlySpan<byte> data)
|
||||
{
|
||||
using var hasher = Hasher.New();
|
||||
hasher.Update(data);
|
||||
var hash = hasher.Finalize();
|
||||
return hash.AsSpan().ToArray();
|
||||
}
|
||||
|
||||
private static byte[] ComputeSm3(ReadOnlySpan<byte> data)
|
||||
{
|
||||
var digest = new SM3Digest();
|
||||
digest.BlockUpdate(data);
|
||||
var output = new byte[digest.GetDigestSize()];
|
||||
digest.DoFinal(output, 0);
|
||||
return output;
|
||||
}
|
||||
|
||||
private static async ValueTask<byte[]> ComputeBlake3StreamAsync(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
using var hasher = Hasher.New();
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(128 * 1024);
|
||||
try
|
||||
{
|
||||
int bytesRead;
|
||||
while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0)
|
||||
{
|
||||
hasher.Update(buffer.AsSpan(0, bytesRead));
|
||||
}
|
||||
|
||||
var hash = hasher.Finalize();
|
||||
return hash.AsSpan().ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private static async ValueTask<byte[]> ComputeSm3StreamAsync(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
var digest = new SM3Digest();
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(128 * 1024);
|
||||
try
|
||||
{
|
||||
int bytesRead;
|
||||
while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0)
|
||||
{
|
||||
digest.BlockUpdate(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
var output = new byte[digest.GetDigestSize()];
|
||||
digest.DoFinal(output, 0);
|
||||
return output;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static options monitors
|
||||
|
||||
private sealed class StaticOptionsMonitor : IOptionsMonitor<CryptoHashOptions>
|
||||
{
|
||||
private readonly CryptoHashOptions options;
|
||||
private readonly CryptoHashOptions _options;
|
||||
|
||||
public StaticOptionsMonitor(CryptoHashOptions options)
|
||||
=> this.options = options;
|
||||
=> _options = options;
|
||||
|
||||
public CryptoHashOptions CurrentValue => options;
|
||||
public CryptoHashOptions CurrentValue => _options;
|
||||
|
||||
public CryptoHashOptions Get(string? name) => options;
|
||||
public CryptoHashOptions Get(string? name) => _options;
|
||||
|
||||
public IDisposable OnChange(Action<CryptoHashOptions, string> listener)
|
||||
=> NullDisposable.Instance;
|
||||
}
|
||||
|
||||
private sealed class NullDisposable : IDisposable
|
||||
private sealed class StaticComplianceOptionsMonitor : IOptionsMonitor<CryptoComplianceOptions>
|
||||
{
|
||||
private readonly CryptoComplianceOptions _options;
|
||||
|
||||
public StaticComplianceOptionsMonitor(CryptoComplianceOptions options)
|
||||
=> _options = options;
|
||||
|
||||
public CryptoComplianceOptions CurrentValue => _options;
|
||||
|
||||
public CryptoComplianceOptions Get(string? name) => _options;
|
||||
|
||||
public IDisposable OnChange(Action<CryptoComplianceOptions, string> listener)
|
||||
=> NullDisposable.Instance;
|
||||
}
|
||||
|
||||
private sealed class NullDisposable : IDisposable
|
||||
{
|
||||
public static readonly NullDisposable Instance = new();
|
||||
public void Dispose()
|
||||
{
|
||||
public static readonly NullDisposable Instance = new();
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user