feat: implement config builder, source, and extensions
This commit is contained in:
+73
-4
@@ -1,14 +1,83 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using System.Text.Json.Serialization.Metadata;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Cryptography;
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Storage;
|
||||||
|
|
||||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
namespace StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides a builder interface for configuring secure configuration services.
|
/// Provides a fluent builder interface for configuring secure configuration storage and encryption.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ISecureConfigBuilder
|
public interface ISecureConfigBuilder
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the <see cref="IServiceCollection"/> used to register secure configuration services.
|
/// Configures JSON file-based storage using the provided options instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IServiceCollection Services { get; }
|
/// <param name="options">The JSON storage configuration options.</param>
|
||||||
|
/// <returns>The current <see cref="ISecureConfigBuilder"/> instance for method chaining.</returns>
|
||||||
|
ISecureConfigBuilder UseJsonFileStorage(JsonStorageOptions options);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures JSON file-based storage using an action to configure the options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configure">An action to configure the <see cref="JsonStorageOptions"/>.</param>
|
||||||
|
/// <returns>The current <see cref="ISecureConfigBuilder"/> instance for method chaining.</returns>
|
||||||
|
ISecureConfigBuilder UseJsonFileStorage(Action<JsonStorageOptions> configure);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a JSON AOT source-generated context for serializing complex types.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">A context that implements <see cref="IJsonTypeInfoResolver"/> that will be used for JSON serialization.</param>
|
||||||
|
/// <returns>The current <see cref="ISecureConfigBuilder"/> instance for method chaining.</returns>
|
||||||
|
ISecureConfigBuilder AddJsonAotContext(IJsonTypeInfoResolver context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures a custom storage provider for secure configuration data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="provider">The custom storage provider implementation.</param>
|
||||||
|
/// <returns>The current <see cref="ISecureConfigBuilder"/> instance for method chaining.</returns>
|
||||||
|
ISecureConfigBuilder UseCustomStorage(ISecureStorageProvider provider);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures encryption using a Base64-encoded encryption key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The Base64-encoded encryption key string.</param>
|
||||||
|
/// <returns>The current <see cref="ISecureConfigBuilder"/> instance for method chaining.</returns>
|
||||||
|
ISecureConfigBuilder WithBase64EncryptionKey(string key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures encryption using a key derived from the machine id.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The current <see cref="ISecureConfigBuilder"/> instance for method chaining.</returns>
|
||||||
|
ISecureConfigBuilder WithMachineIdKey();
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures a custom encryption key provider.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keyProvider">The custom encryption key provider implementation.</param>
|
||||||
|
/// <returns>The current <see cref="ISecureConfigBuilder"/> instance for method chaining.</returns>
|
||||||
|
ISecureConfigBuilder WithCustomKeyProvider(IEncryptionKeyProvider keyProvider);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures logging using the provided logger factory.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loggerFactory">The logger factory to use for logging operations.</param>
|
||||||
|
/// <returns>The current <see cref="ISecureConfigBuilder"/> instance for method chaining.</returns>
|
||||||
|
ISecureConfigBuilder WithLoggerFactory(ILoggerFactory loggerFactory);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures AES crypto provider for encryption and decryption
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The current <see cref="ISecureConfigBuilder"/> instance for method chaining.</returns>
|
||||||
|
ISecureConfigBuilder WithAesCryptoProvider();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the factory function that will be used to create the crypto provider for encryption and decryption
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cryptoProviderFactory">The crypto provider factor to use for encryption and decryption operations.</param>
|
||||||
|
/// <returns>The current <see cref="ISecureConfigBuilder"/> instance for method chaining.</returns>
|
||||||
|
ISecureConfigBuilder WithCustomCryptoProvider(Func<IEncryptionKeyProvider, ICryptoProvider> cryptoProviderFactory);
|
||||||
}
|
}
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization.Metadata;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Cryptography;
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Storage;
|
||||||
|
|
||||||
|
namespace StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||||
|
|
||||||
|
internal sealed class SecureConfigBuilder : ISecureConfigBuilder
|
||||||
|
{
|
||||||
|
internal ISecureStorageProvider? StorageProvider { get; private set; }
|
||||||
|
internal Func<IEncryptionKeyProvider, ICryptoProvider>? CryptoProviderFactory { get; private set; }
|
||||||
|
internal IEncryptionKeyProvider? KeyProvider { get; private set; }
|
||||||
|
internal ILoggerFactory LoggerFactory { get; private set; } = NullLoggerFactory.Instance;
|
||||||
|
internal JsonSerializerOptions SerializerOptions { get; } = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
public ISecureConfigBuilder UseJsonFileStorage(JsonStorageOptions options)
|
||||||
|
{
|
||||||
|
StorageProvider = new JsonFileStorageProvider(options);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ISecureConfigBuilder UseJsonFileStorage(Action<JsonStorageOptions> configure)
|
||||||
|
{
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
ArgumentNullException.ThrowIfNull(configure);
|
||||||
|
#else
|
||||||
|
if (configure is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(configure));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var options = new JsonStorageOptions();
|
||||||
|
configure.Invoke(options);
|
||||||
|
StorageProvider = new JsonFileStorageProvider(options);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ISecureConfigBuilder AddJsonAotContext(IJsonTypeInfoResolver context)
|
||||||
|
{
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
#else
|
||||||
|
if (context is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(context));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SerializerOptions.TypeInfoResolverChain.Insert(0, context);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ISecureConfigBuilder UseCustomStorage(ISecureStorageProvider provider)
|
||||||
|
{
|
||||||
|
StorageProvider = provider ?? throw new ArgumentNullException(nameof(provider));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ISecureConfigBuilder WithBase64EncryptionKey(string key)
|
||||||
|
{
|
||||||
|
KeyProvider = new StaticKeyProvider(key);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ISecureConfigBuilder WithMachineIdKey()
|
||||||
|
{
|
||||||
|
var logger = LoggerFactory.CreateLogger<MachineIdKeyGenerator>();
|
||||||
|
KeyProvider = new MachineIdKeyProvider(new MachineIdKeyGenerator(logger));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ISecureConfigBuilder WithCustomKeyProvider(IEncryptionKeyProvider provider)
|
||||||
|
{
|
||||||
|
KeyProvider = provider ?? throw new ArgumentNullException(nameof(provider));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ISecureConfigBuilder WithLoggerFactory(ILoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
LoggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ISecureConfigBuilder WithAesCryptoProvider()
|
||||||
|
{
|
||||||
|
CryptoProviderFactory = (kp) => new AesCryptoProvider(kp);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ISecureConfigBuilder WithCustomCryptoProvider(Func<IEncryptionKeyProvider, ICryptoProvider> cryptoProviderFactory)
|
||||||
|
{
|
||||||
|
CryptoProviderFactory = cryptoProviderFactory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Cryptography;
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Storage;
|
||||||
|
|
||||||
|
namespace StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||||
|
|
||||||
|
internal sealed class SecureConfigSource(
|
||||||
|
ISecureStorageProvider storageProvider,
|
||||||
|
ICryptoProvider cryptoProvider,
|
||||||
|
ILoggerFactory loggerFactory
|
||||||
|
) : IConfigurationSource
|
||||||
|
{
|
||||||
|
private readonly ISecureStorageProvider _storageProvider = storageProvider
|
||||||
|
?? throw new ArgumentNullException(nameof(storageProvider));
|
||||||
|
|
||||||
|
private readonly ICryptoProvider _cryptoProvider = cryptoProvider
|
||||||
|
?? throw new ArgumentNullException(nameof(cryptoProvider));
|
||||||
|
|
||||||
|
private readonly ILoggerFactory _loggerFactory = loggerFactory
|
||||||
|
?? throw new ArgumentNullException(nameof(loggerFactory));
|
||||||
|
|
||||||
|
public IConfigurationProvider Build(IConfigurationBuilder builder)
|
||||||
|
{
|
||||||
|
var logger = _loggerFactory.CreateLogger<SecureConfigProvider>();
|
||||||
|
return new SecureConfigProvider(_storageProvider, _cryptoProvider, logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Cryptography;
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Storage;
|
||||||
|
|
||||||
|
namespace StevanFreeborn.Extensions.Configuration.Secure;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for configuring secure configuration in .NET applications.
|
||||||
|
/// </summary>
|
||||||
|
public static class SecureConfigExtensions
|
||||||
|
{
|
||||||
|
private const string JsonSerializerOptionsKey = "SecureConfigJsonSerializerOptions";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds secure configuration to the configuration builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The configuration builder to add secure configuration to.</param>
|
||||||
|
/// <param name="configure">An action to configure the secure configuration builder.</param>
|
||||||
|
/// <returns>The configuration builder with secure configuration added.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="configure"/> is null.</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">Thrown when a required provider is not configured.</exception>
|
||||||
|
public static IConfigurationBuilder AddSecureConfig(
|
||||||
|
this IConfigurationBuilder builder,
|
||||||
|
Action<ISecureConfigBuilder> configure
|
||||||
|
)
|
||||||
|
{
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
ArgumentNullException.ThrowIfNull(builder);
|
||||||
|
ArgumentNullException.ThrowIfNull(configure);
|
||||||
|
#else
|
||||||
|
if (builder is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(builder));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configure is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(configure));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var configBuilder = new SecureConfigBuilder();
|
||||||
|
|
||||||
|
configure.Invoke(configBuilder);
|
||||||
|
|
||||||
|
if (configBuilder.StorageProvider is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("A storage provider must be configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configBuilder.KeyProvider is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("A key provider must be configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configBuilder.CryptoProviderFactory is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("A crypto provider must be configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var cryptoProvider = configBuilder.CryptoProviderFactory.Invoke(configBuilder.KeyProvider);
|
||||||
|
|
||||||
|
var source = new SecureConfigSource(configBuilder.StorageProvider, cryptoProvider, configBuilder.LoggerFactory);
|
||||||
|
return builder.Add(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds secure configuration services to the service collection.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The service collection to add secure configuration services to.</param>
|
||||||
|
/// <param name="configure">An action to configure the secure configuration builder.</param>
|
||||||
|
/// <returns>The service collection with secure configuration services added.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="configure"/> is null.</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">Thrown when a required provider is not configured.</exception>
|
||||||
|
public static IServiceCollection AddSecureConfig(
|
||||||
|
this IServiceCollection services,
|
||||||
|
Action<ISecureConfigBuilder> configure
|
||||||
|
)
|
||||||
|
{
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
ArgumentNullException.ThrowIfNull(configure);
|
||||||
|
#else
|
||||||
|
if (services is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(services));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configure is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(configure));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var configBuilder = new SecureConfigBuilder();
|
||||||
|
|
||||||
|
configure(configBuilder);
|
||||||
|
|
||||||
|
if (configBuilder.StorageProvider is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("A storage provider must be configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configBuilder.KeyProvider is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("A key provider must be configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configBuilder.CryptoProviderFactory is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("A crypto provider must be configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
services.TryAddKeyedSingleton(JsonSerializerOptionsKey, configBuilder.SerializerOptions);
|
||||||
|
services.TryAddSingleton(configBuilder.StorageProvider);
|
||||||
|
services.TryAddSingleton(configBuilder.KeyProvider);
|
||||||
|
services.TryAddSingleton(configBuilder.CryptoProviderFactory.Invoke(configBuilder.KeyProvider));
|
||||||
|
services.TryAddSingleton<ISecureConfig>(sp =>
|
||||||
|
{
|
||||||
|
var storageProvider = sp.GetRequiredService<ISecureStorageProvider>();
|
||||||
|
var cryptoProvider = sp.GetRequiredService<ICryptoProvider>();
|
||||||
|
var serializerOptions = sp.GetRequiredKeyedService<JsonSerializerOptions>(JsonSerializerOptionsKey);
|
||||||
|
return new SecureConfig(storageProvider, cryptoProvider, serializerOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net11.0</TargetFramework>
|
<TargetFrameworks>net8.0;net10.0;</TargetFrameworks>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
|
|||||||
+331
@@ -0,0 +1,331 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text.Json.Serialization.Metadata;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
|
using Moq;
|
||||||
|
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Cryptography;
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Storage;
|
||||||
|
|
||||||
|
namespace StevanFreeborn.Extensions.Configuration.Secure.Tests.Unit.Configuration;
|
||||||
|
public class SecureConfigBuilderTests
|
||||||
|
{
|
||||||
|
private readonly SecureConfigBuilder _sut = new();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenCalled_ItShouldInitializeWithDefaultValues()
|
||||||
|
{
|
||||||
|
_sut.StorageProvider.Should().BeNull();
|
||||||
|
_sut.CryptoProviderFactory.Should().BeNull();
|
||||||
|
_sut.KeyProvider.Should().BeNull();
|
||||||
|
_sut.LoggerFactory.Should().Be(NullLoggerFactory.Instance);
|
||||||
|
_sut.SerializerOptions.PropertyNameCaseInsensitive.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UseJsonFileStorage_WithOptions_ItShouldSetStorageProvider()
|
||||||
|
{
|
||||||
|
var options = new JsonStorageOptions
|
||||||
|
{
|
||||||
|
FileName = "test_config.json",
|
||||||
|
DirectoryPath = "/tmp/config"
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _sut.UseJsonFileStorage(options);
|
||||||
|
|
||||||
|
result.Should().BeSameAs(_sut);
|
||||||
|
_sut.StorageProvider.Should().NotBeNull();
|
||||||
|
_sut.StorageProvider.Should().BeOfType<JsonFileStorageProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UseJsonFileStorage_WithOptions_ItShouldUseProvidedOptions()
|
||||||
|
{
|
||||||
|
var options = new JsonStorageOptions
|
||||||
|
{
|
||||||
|
FileName = "custom.json",
|
||||||
|
DirectoryPath = "/custom/path"
|
||||||
|
};
|
||||||
|
|
||||||
|
_sut.UseJsonFileStorage(options);
|
||||||
|
|
||||||
|
_sut.StorageProvider.Should().BeOfType<JsonFileStorageProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UseJsonFileStorage_WithConfigureAction_ItShouldSetStorageProvider()
|
||||||
|
{
|
||||||
|
var result = _sut.UseJsonFileStorage(opt => opt.FileName = "test.json");
|
||||||
|
|
||||||
|
result.Should().BeSameAs(_sut);
|
||||||
|
_sut.StorageProvider.Should().NotBeNull();
|
||||||
|
_sut.StorageProvider.Should().BeOfType<JsonFileStorageProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UseJsonFileStorage_WithConfigureAction_ItShouldApplyConfiguration()
|
||||||
|
{
|
||||||
|
_sut.UseJsonFileStorage(opt =>
|
||||||
|
{
|
||||||
|
opt.FileName = "configured.json";
|
||||||
|
opt.DirectoryPath = "/configured/path";
|
||||||
|
});
|
||||||
|
|
||||||
|
_sut.StorageProvider.Should().BeOfType<JsonFileStorageProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UseJsonFileStorage_WithNullConfigureAction_ItShouldThrowArgumentNullException()
|
||||||
|
{
|
||||||
|
var act = () => _sut.UseJsonFileStorage((Action<JsonStorageOptions>)null!);
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentNullException>()
|
||||||
|
.WithParameterName("configure");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddJsonAotContext_WithContext_ItShouldAddToResolverChain()
|
||||||
|
{
|
||||||
|
var mockContext = new Mock<IJsonTypeInfoResolver>();
|
||||||
|
|
||||||
|
var result = _sut.AddJsonAotContext(mockContext.Object);
|
||||||
|
|
||||||
|
result.Should().BeSameAs(_sut);
|
||||||
|
_sut.SerializerOptions.TypeInfoResolverChain.Should().Contain(mockContext.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddJsonAotContext_WithNullContext_ItShouldThrowArgumentNullException()
|
||||||
|
{
|
||||||
|
var act = () => _sut.AddJsonAotContext(null!);
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentNullException>()
|
||||||
|
.WithParameterName("context");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddJsonAotContext_WhenCalledMultipleTimes_ItShouldAddAllToChain()
|
||||||
|
{
|
||||||
|
var mockContext1 = new Mock<IJsonTypeInfoResolver>();
|
||||||
|
var mockContext2 = new Mock<IJsonTypeInfoResolver>();
|
||||||
|
|
||||||
|
_sut.AddJsonAotContext(mockContext1.Object);
|
||||||
|
_sut.AddJsonAotContext(mockContext2.Object);
|
||||||
|
|
||||||
|
_sut.SerializerOptions.TypeInfoResolverChain.Should().Contain(mockContext1.Object);
|
||||||
|
_sut.SerializerOptions.TypeInfoResolverChain.Should().Contain(mockContext2.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UseCustomStorage_WithProvider_ItShouldSetStorageProvider()
|
||||||
|
{
|
||||||
|
var mockProvider = new Mock<ISecureStorageProvider>();
|
||||||
|
|
||||||
|
var result = _sut.UseCustomStorage(mockProvider.Object);
|
||||||
|
|
||||||
|
result.Should().BeSameAs(_sut);
|
||||||
|
_sut.StorageProvider.Should().BeSameAs(mockProvider.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UseCustomStorage_WithNullProvider_ItShouldThrowArgumentNullException()
|
||||||
|
{
|
||||||
|
var act = () => _sut.UseCustomStorage(null!);
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentNullException>()
|
||||||
|
.WithParameterName("provider");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithBase64EncryptionKey_WithValidKey_ItShouldSetKeyProvider()
|
||||||
|
{
|
||||||
|
var validKey = Convert.ToBase64String(new byte[32]);
|
||||||
|
|
||||||
|
var result = _sut.WithBase64EncryptionKey(validKey);
|
||||||
|
|
||||||
|
result.Should().BeSameAs(_sut);
|
||||||
|
_sut.KeyProvider.Should().NotBeNull();
|
||||||
|
_sut.KeyProvider.Should().BeOfType<StaticKeyProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithBase64EncryptionKey_WithValidKey_ItShouldReturnCorrectKey()
|
||||||
|
{
|
||||||
|
var keyBytes = new byte[32];
|
||||||
|
RandomNumberGenerator.Fill(keyBytes);
|
||||||
|
|
||||||
|
var validKey = Convert.ToBase64String(keyBytes);
|
||||||
|
|
||||||
|
_sut.WithBase64EncryptionKey(validKey);
|
||||||
|
|
||||||
|
_sut.KeyProvider!.GetKey().Should().Equal(keyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithMachineIdKey_WhenCalled_ItShouldSetKeyProvider()
|
||||||
|
{
|
||||||
|
var result = _sut.WithMachineIdKey();
|
||||||
|
|
||||||
|
result.Should().BeSameAs(_sut);
|
||||||
|
_sut.KeyProvider.Should().NotBeNull();
|
||||||
|
_sut.KeyProvider.Should().BeOfType<MachineIdKeyProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithMachineIdKey_WhenCalled_ItShouldUseLoggerFactory()
|
||||||
|
{
|
||||||
|
var mockLoggerFactory = new Mock<ILoggerFactory>();
|
||||||
|
var mockLogger = new Mock<ILogger>();
|
||||||
|
|
||||||
|
mockLoggerFactory.Setup(f => f.CreateLogger(typeof(MachineIdKeyGenerator).FullName!))
|
||||||
|
.Returns(mockLogger.Object);
|
||||||
|
|
||||||
|
_sut.WithLoggerFactory(mockLoggerFactory.Object);
|
||||||
|
|
||||||
|
_sut.WithMachineIdKey();
|
||||||
|
|
||||||
|
mockLoggerFactory.Verify(f => f.CreateLogger(typeof(MachineIdKeyGenerator).FullName!), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithCustomKeyProvider_WithProvider_ItShouldSetKeyProvider()
|
||||||
|
{
|
||||||
|
var mockProvider = new Mock<IEncryptionKeyProvider>();
|
||||||
|
|
||||||
|
var result = _sut.WithCustomKeyProvider(mockProvider.Object);
|
||||||
|
|
||||||
|
result.Should().BeSameAs(_sut);
|
||||||
|
_sut.KeyProvider.Should().BeSameAs(mockProvider.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithCustomKeyProvider_WithNullProvider_ItShouldThrowArgumentNullException()
|
||||||
|
{
|
||||||
|
var act = () => _sut.WithCustomKeyProvider(null!);
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentNullException>()
|
||||||
|
.WithParameterName("provider");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithLoggerFactory_WithFactory_ItShouldSetLoggerFactory()
|
||||||
|
{
|
||||||
|
var mockFactory = new Mock<ILoggerFactory>();
|
||||||
|
|
||||||
|
var result = _sut.WithLoggerFactory(mockFactory.Object);
|
||||||
|
|
||||||
|
result.Should().BeSameAs(_sut);
|
||||||
|
_sut.LoggerFactory.Should().BeSameAs(mockFactory.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithLoggerFactory_WithNullFactory_ItShouldSetNullLoggerFactory()
|
||||||
|
{
|
||||||
|
var mockFactory = new Mock<ILoggerFactory>();
|
||||||
|
_sut.WithLoggerFactory(mockFactory.Object);
|
||||||
|
|
||||||
|
_sut.WithLoggerFactory(null!);
|
||||||
|
|
||||||
|
_sut.LoggerFactory.Should().Be(NullLoggerFactory.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithAesCryptoProvider_WhenCalled_ItShouldSetCryptoProviderFactory()
|
||||||
|
{
|
||||||
|
var mockKeyProvider = new Mock<IEncryptionKeyProvider>();
|
||||||
|
|
||||||
|
var result = _sut.WithAesCryptoProvider();
|
||||||
|
|
||||||
|
result.Should().BeSameAs(_sut);
|
||||||
|
_sut.CryptoProviderFactory.Should().NotBeNull();
|
||||||
|
|
||||||
|
var cryptoProvider = _sut.CryptoProviderFactory!(mockKeyProvider.Object);
|
||||||
|
cryptoProvider.Should().BeOfType<AesCryptoProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithCustomCryptoProvider_WithFactory_ItShouldSetCryptoProviderFactory()
|
||||||
|
{
|
||||||
|
var mockCryptoProvider = new Mock<ICryptoProvider>();
|
||||||
|
var mockKeyProvider = new Mock<IEncryptionKeyProvider>();
|
||||||
|
Func<IEncryptionKeyProvider, ICryptoProvider> factory = (kp) => mockCryptoProvider.Object;
|
||||||
|
|
||||||
|
var result = _sut.WithCustomCryptoProvider(factory);
|
||||||
|
|
||||||
|
result.Should().BeSameAs(_sut);
|
||||||
|
_sut.CryptoProviderFactory.Should().NotBeNull();
|
||||||
|
|
||||||
|
var cryptoProvider = _sut.CryptoProviderFactory!(mockKeyProvider.Object);
|
||||||
|
cryptoProvider.Should().BeSameAs(mockCryptoProvider.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithCustomCryptoProvider_WithNullFactory_ItShouldSetNull()
|
||||||
|
{
|
||||||
|
_sut.WithCustomCryptoProvider(null!);
|
||||||
|
|
||||||
|
_sut.CryptoProviderFactory.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MethodChaining_WhenAllMethodsCalled_ItShouldConfigureAllProperties()
|
||||||
|
{
|
||||||
|
var mockStorageProvider = new Mock<ISecureStorageProvider>();
|
||||||
|
var mockKeyProvider = new Mock<IEncryptionKeyProvider>();
|
||||||
|
var mockLoggerFactory = new Mock<ILoggerFactory>();
|
||||||
|
var mockCryptoProvider = new Mock<ICryptoProvider>();
|
||||||
|
|
||||||
|
_sut.UseCustomStorage(mockStorageProvider.Object)
|
||||||
|
.WithCustomKeyProvider(mockKeyProvider.Object)
|
||||||
|
.WithLoggerFactory(mockLoggerFactory.Object)
|
||||||
|
.WithCustomCryptoProvider((kp) => mockCryptoProvider.Object);
|
||||||
|
|
||||||
|
_sut.StorageProvider.Should().BeSameAs(mockStorageProvider.Object);
|
||||||
|
_sut.KeyProvider.Should().BeSameAs(mockKeyProvider.Object);
|
||||||
|
_sut.LoggerFactory.Should().BeSameAs(mockLoggerFactory.Object);
|
||||||
|
_sut.CryptoProviderFactory.Should().NotBeNull();
|
||||||
|
|
||||||
|
var provider = _sut.CryptoProviderFactory!(mockKeyProvider.Object);
|
||||||
|
provider.Should().BeSameAs(mockCryptoProvider.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MethodChaining_WhenOverridingStorage_ItShouldUseLastSetProvider()
|
||||||
|
{
|
||||||
|
var mockProvider1 = new Mock<ISecureStorageProvider>();
|
||||||
|
var mockProvider2 = new Mock<ISecureStorageProvider>();
|
||||||
|
|
||||||
|
_sut.UseCustomStorage(mockProvider1.Object)
|
||||||
|
.UseCustomStorage(mockProvider2.Object);
|
||||||
|
|
||||||
|
_sut.StorageProvider.Should().BeSameAs(mockProvider2.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MethodChaining_WhenOverridingKeyProvider_ItShouldUseLastSetProvider()
|
||||||
|
{
|
||||||
|
var mockProvider1 = new Mock<IEncryptionKeyProvider>();
|
||||||
|
var mockProvider2 = new Mock<IEncryptionKeyProvider>();
|
||||||
|
|
||||||
|
_sut.WithCustomKeyProvider(mockProvider1.Object)
|
||||||
|
.WithCustomKeyProvider(mockProvider2.Object);
|
||||||
|
|
||||||
|
_sut.KeyProvider.Should().BeSameAs(mockProvider2.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MethodChaining_WhenOverridingLoggerFactory_ItShouldUseLastSetFactory()
|
||||||
|
{
|
||||||
|
var mockFactory1 = new Mock<ILoggerFactory>();
|
||||||
|
var mockFactory2 = new Mock<ILoggerFactory>();
|
||||||
|
|
||||||
|
_sut.WithLoggerFactory(mockFactory1.Object)
|
||||||
|
.WithLoggerFactory(mockFactory2.Object);
|
||||||
|
|
||||||
|
_sut.LoggerFactory.Should().BeSameAs(mockFactory2.Object);
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-4
@@ -65,7 +65,7 @@ public class SecureConfigProviderTests
|
|||||||
{ "BadKey", "encrypted_bad" },
|
{ "BadKey", "encrypted_bad" },
|
||||||
};
|
};
|
||||||
|
|
||||||
_mockLogger.Setup(m => m.IsEnabled(LogLevel.Error)).Returns(true);
|
_mockLogger.Setup(m => m.IsEnabled(LogLevel.Warning)).Returns(true);
|
||||||
_mockStorage.Setup(m => m.ReadAllAsync(It.IsAny<CancellationToken>())).ReturnsAsync(storedData);
|
_mockStorage.Setup(m => m.ReadAllAsync(It.IsAny<CancellationToken>())).ReturnsAsync(storedData);
|
||||||
_mockCrypto.Setup(m => m.Decrypt("encrypted_valid")).Returns(@"{ ""Name"": ""test"" }");
|
_mockCrypto.Setup(m => m.Decrypt("encrypted_valid")).Returns(@"{ ""Name"": ""test"" }");
|
||||||
_mockCrypto.Setup(m => m.Decrypt("encrypted_bad")).Throws(new InvalidOperationException("Decryption failed"));
|
_mockCrypto.Setup(m => m.Decrypt("encrypted_bad")).Throws(new InvalidOperationException("Decryption failed"));
|
||||||
@@ -76,7 +76,7 @@ public class SecureConfigProviderTests
|
|||||||
val.Should().Be("test");
|
val.Should().Be("test");
|
||||||
|
|
||||||
_mockLogger.Verify(logger => logger.Log(
|
_mockLogger.Verify(logger => logger.Log(
|
||||||
LogLevel.Error,
|
LogLevel.Warning,
|
||||||
It.Is<EventId>(id => id.Id == 3),
|
It.Is<EventId>(id => id.Id == 3),
|
||||||
It.Is<It.IsAnyType>((state, type) => state.ToString()!.Contains("Failed to decrypt value for key")),
|
It.Is<It.IsAnyType>((state, type) => state.ToString()!.Contains("Failed to decrypt value for key")),
|
||||||
It.IsAny<Exception>(),
|
It.IsAny<Exception>(),
|
||||||
@@ -495,14 +495,14 @@ public class SecureConfigProviderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Load_WhenCalledWithInvalidJson_ItShouldLogErrorAndContinue()
|
public void Load_WhenCalledWithInvalidJson_ItShouldLogErrorAndContinue()
|
||||||
{
|
{
|
||||||
_mockLogger.Setup(m => m.IsEnabled(LogLevel.Error)).Returns(true);
|
_mockLogger.Setup(m => m.IsEnabled(LogLevel.Warning)).Returns(true);
|
||||||
_mockStorage.Setup(m => m.ReadAllAsync(It.IsAny<CancellationToken>())).ReturnsAsync(new Dictionary<string, string> { { "Bad", "enc" } });
|
_mockStorage.Setup(m => m.ReadAllAsync(It.IsAny<CancellationToken>())).ReturnsAsync(new Dictionary<string, string> { { "Bad", "enc" } });
|
||||||
_mockCrypto.Setup(m => m.Decrypt("enc")).Returns("not valid json{{{");
|
_mockCrypto.Setup(m => m.Decrypt("enc")).Returns("not valid json{{{");
|
||||||
|
|
||||||
_sut.Load();
|
_sut.Load();
|
||||||
|
|
||||||
_mockLogger.Verify(x => x.Log(
|
_mockLogger.Verify(x => x.Log(
|
||||||
LogLevel.Error,
|
LogLevel.Warning,
|
||||||
It.IsAny<EventId>(),
|
It.IsAny<EventId>(),
|
||||||
It.Is<It.IsAnyType>((v, _) => v.ToString()!.Contains("Bad")),
|
It.Is<It.IsAnyType>((v, _) => v.ToString()!.Contains("Bad")),
|
||||||
It.IsAny<Exception>(),
|
It.IsAny<Exception>(),
|
||||||
|
|||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
using Moq;
|
||||||
|
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Cryptography;
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Storage;
|
||||||
|
|
||||||
|
namespace StevanFreeborn.Extensions.Configuration.Secure.Tests.Unit.Configuration;
|
||||||
|
|
||||||
|
public class SecureConfigSourceTests
|
||||||
|
{
|
||||||
|
private readonly Mock<ISecureStorageProvider> _mockStorage = new();
|
||||||
|
private readonly Mock<ICryptoProvider> _mockCrypto = new();
|
||||||
|
private readonly Mock<ILoggerFactory> _mockLoggerFactory = new();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenStorageProviderIsNull_ItShouldThrowArgumentNullException()
|
||||||
|
{
|
||||||
|
var act = () => new SecureConfigSource(
|
||||||
|
storageProvider: null!,
|
||||||
|
cryptoProvider: _mockCrypto.Object,
|
||||||
|
loggerFactory: _mockLoggerFactory.Object
|
||||||
|
);
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentNullException>()
|
||||||
|
.WithParameterName("storageProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenCryptoProviderIsNull_ItShouldThrowArgumentNullException()
|
||||||
|
{
|
||||||
|
var act = () => new SecureConfigSource(
|
||||||
|
storageProvider: _mockStorage.Object,
|
||||||
|
cryptoProvider: null!,
|
||||||
|
loggerFactory: _mockLoggerFactory.Object
|
||||||
|
);
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentNullException>()
|
||||||
|
.WithParameterName("cryptoProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenLoggerFactoryIsNull_ItShouldThrowArgumentNullException()
|
||||||
|
{
|
||||||
|
var act = () => new SecureConfigSource(
|
||||||
|
storageProvider: _mockStorage.Object,
|
||||||
|
cryptoProvider: _mockCrypto.Object,
|
||||||
|
loggerFactory: null!
|
||||||
|
);
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentNullException>()
|
||||||
|
.WithParameterName("loggerFactory");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenAllDependenciesAreProvided_ItShouldNotThrow()
|
||||||
|
{
|
||||||
|
var act = () => new SecureConfigSource(
|
||||||
|
storageProvider: _mockStorage.Object,
|
||||||
|
cryptoProvider: _mockCrypto.Object,
|
||||||
|
loggerFactory: _mockLoggerFactory.Object
|
||||||
|
);
|
||||||
|
|
||||||
|
act.Should().NotThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Build_WhenCalled_ItShouldReturnSecureConfigProvider()
|
||||||
|
{
|
||||||
|
var sut = new SecureConfigSource(
|
||||||
|
storageProvider: _mockStorage.Object,
|
||||||
|
cryptoProvider: _mockCrypto.Object,
|
||||||
|
loggerFactory: _mockLoggerFactory.Object
|
||||||
|
);
|
||||||
|
|
||||||
|
var mockBuilder = new Mock<IConfigurationBuilder>();
|
||||||
|
|
||||||
|
var result = sut.Build(mockBuilder.Object);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Should().BeOfType<SecureConfigProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Build_WhenCalled_ItShouldCreateLoggerFromFactory()
|
||||||
|
{
|
||||||
|
var mockLogger = new Mock<ILogger<SecureConfigProvider>>();
|
||||||
|
_mockLoggerFactory
|
||||||
|
.Setup(f => f.CreateLogger(typeof(SecureConfigProvider).FullName!))
|
||||||
|
.Returns(mockLogger.Object);
|
||||||
|
|
||||||
|
var sut = new SecureConfigSource(
|
||||||
|
storageProvider: _mockStorage.Object,
|
||||||
|
cryptoProvider: _mockCrypto.Object,
|
||||||
|
loggerFactory: _mockLoggerFactory.Object
|
||||||
|
);
|
||||||
|
|
||||||
|
var mockBuilder = new Mock<IConfigurationBuilder>();
|
||||||
|
sut.Build(mockBuilder.Object);
|
||||||
|
|
||||||
|
_mockLoggerFactory.Verify(
|
||||||
|
f => f.CreateLogger(typeof(SecureConfigProvider).FullName!),
|
||||||
|
Times.Once()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Build_WhenCalledMultipleTimes_ItShouldReturnNewProviderInstance()
|
||||||
|
{
|
||||||
|
var sut = new SecureConfigSource(
|
||||||
|
storageProvider: _mockStorage.Object,
|
||||||
|
cryptoProvider: _mockCrypto.Object,
|
||||||
|
loggerFactory: _mockLoggerFactory.Object
|
||||||
|
);
|
||||||
|
|
||||||
|
var mockBuilder = new Mock<IConfigurationBuilder>();
|
||||||
|
|
||||||
|
var result1 = sut.Build(mockBuilder.Object);
|
||||||
|
var result2 = sut.Build(mockBuilder.Object);
|
||||||
|
|
||||||
|
result1.Should().NotBeSameAs(result2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Build_WhenCalledWithNullBuilder_ItShouldStillReturnProvider()
|
||||||
|
{
|
||||||
|
var sut = new SecureConfigSource(
|
||||||
|
storageProvider: _mockStorage.Object,
|
||||||
|
cryptoProvider: _mockCrypto.Object,
|
||||||
|
loggerFactory: _mockLoggerFactory.Object
|
||||||
|
);
|
||||||
|
|
||||||
|
var result = sut.Build(builder: null!);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Should().BeOfType<SecureConfigProvider>();
|
||||||
|
}
|
||||||
|
}
|
||||||
+100
-19
@@ -1,28 +1,31 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
||||||
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||||
using StevanFreeborn.Extensions.Configuration.Secure.Cryptography;
|
using StevanFreeborn.Extensions.Configuration.Secure.Cryptography;
|
||||||
using StevanFreeborn.Extensions.Configuration.Secure.Storage;
|
using StevanFreeborn.Extensions.Configuration.Secure.Storage;
|
||||||
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Tests.Unit.Configuration;
|
namespace StevanFreeborn.Extensions.Configuration.Secure.Tests.Unit.Configuration;
|
||||||
|
|
||||||
public class SecureConfigTests
|
public class SecureConfigTests
|
||||||
{
|
{
|
||||||
private readonly Mock<ICryptoProvider> _mockCryptoProvider = new();
|
private readonly Mock<ICryptoProvider> _mockCryptoProvider = new();
|
||||||
private readonly Mock<ISecureStorageProvider> _mockStorageProvider = new();
|
private readonly Mock<ISecureStorageProvider> _mockStorageProvider = new();
|
||||||
|
private readonly JsonSerializerOptions _jsonSerializerOptions = new();
|
||||||
private readonly SecureConfig _sut;
|
private readonly SecureConfig _sut;
|
||||||
|
|
||||||
public SecureConfigTests()
|
public SecureConfigTests()
|
||||||
{
|
{
|
||||||
_sut = new(_mockStorageProvider.Object, _mockCryptoProvider.Object);
|
_sut = new(_mockStorageProvider.Object, _mockCryptoProvider.Object, _jsonSerializerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Constructor_WhenCalledWithNullStorageProvider_ItShouldThrowArgumentNullException()
|
public void Constructor_WhenCalledWithNullStorageProvider_ItShouldThrowArgumentNullException()
|
||||||
{
|
{
|
||||||
var act = () => new SecureConfig(null!, _mockCryptoProvider.Object);
|
var act = () => new SecureConfig(null!, _mockCryptoProvider.Object, _jsonSerializerOptions);
|
||||||
|
|
||||||
act.Should().Throw<ArgumentNullException>();
|
act.Should().Throw<ArgumentNullException>();
|
||||||
}
|
}
|
||||||
@@ -30,7 +33,15 @@ public class SecureConfigTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Constructor_WhenCalledWithNullCryptoProvider_ItShouldThrowArgumentNullException()
|
public void Constructor_WhenCalledWithNullCryptoProvider_ItShouldThrowArgumentNullException()
|
||||||
{
|
{
|
||||||
var act = () => new SecureConfig(_mockStorageProvider.Object, null!);
|
var act = () => new SecureConfig(_mockStorageProvider.Object, null!, _jsonSerializerOptions);
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentNullException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenCalledWithNullJsonOptions_ItShouldThrowArgumentNullException()
|
||||||
|
{
|
||||||
|
var act = () => new SecureConfig(_mockStorageProvider.Object, _mockCryptoProvider.Object, null!);
|
||||||
|
|
||||||
act.Should().Throw<ArgumentNullException>();
|
act.Should().Throw<ArgumentNullException>();
|
||||||
}
|
}
|
||||||
@@ -38,22 +49,22 @@ public class SecureConfigTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task SetAsync_WhenCalledWithNullKey_ItShouldThrowArgumentNullException()
|
public async Task SetAsync_WhenCalledWithNullKey_ItShouldThrowArgumentNullException()
|
||||||
{
|
{
|
||||||
var act = async () => await _sut.SetAsync(null!, string.Empty);
|
var act = async () => await _sut.SetAsync(null!, string.Empty, SecureConfigTestsJsonContext.Default.String);
|
||||||
|
|
||||||
await act.Should().ThrowAsync<ArgumentNullException>();
|
await act.Should().ThrowAsync<ArgumentException>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SetAsync_WhenCalledWithNullValue_ItShouldThrowArgumentNullException()
|
public async Task SetAsync_WhenCalledWithNullValue_ItShouldThrowArgumentNullException()
|
||||||
{
|
{
|
||||||
var act = async () => await _sut.SetAsync<string>("Key", null!);
|
var act = async () => await _sut.SetAsync("Key", null!, SecureConfigTestsJsonContext.Default.String);
|
||||||
|
|
||||||
await act.Should().ThrowAsync<ArgumentNullException>();
|
await act.Should().ThrowAsync<ArgumentNullException>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SetAsync_WhenCalled_ItShouldSerializeGivenValueAndEncryptIt()
|
public async Task SetAsync_WhenCalledWithoutJsonContextSet_ItShouldSerializeGivenValueAndEncryptIt()
|
||||||
{
|
{
|
||||||
var key = "Database";
|
var key = "Database";
|
||||||
var config = new DummyConfig("localhost", 9999);
|
var config = new DummyConfig("localhost", 9999);
|
||||||
@@ -62,17 +73,81 @@ public class SecureConfigTests
|
|||||||
|
|
||||||
_mockCryptoProvider.Setup(m => m.Encrypt(json)).Returns(encryptedString);
|
_mockCryptoProvider.Setup(m => m.Encrypt(json)).Returns(encryptedString);
|
||||||
|
|
||||||
|
var act = async () => await _sut.SetAsync(key, config);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<InvalidOperationException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetAsync_WhenCalledWithJsonContextSet_ItShouldSerializeGivenValueAndEncryptIt()
|
||||||
|
{
|
||||||
|
var key = "Database";
|
||||||
|
var config = new DummyConfig("localhost", 9999);
|
||||||
|
var encryptedString = "encryptedString";
|
||||||
|
var json = JsonSerializer.Serialize(config);
|
||||||
|
|
||||||
|
_jsonSerializerOptions.TypeInfoResolverChain.Insert(0, SecureConfigTestsJsonContext.Default);
|
||||||
|
_mockCryptoProvider.Setup(m => m.Encrypt(json)).Returns(encryptedString);
|
||||||
|
|
||||||
await _sut.SetAsync(key, config);
|
await _sut.SetAsync(key, config);
|
||||||
|
|
||||||
_mockStorageProvider.Verify(m => m.WriteAsync(key, encryptedString), Times.Once());
|
_mockStorageProvider.Verify(m => m.WriteAsync(key, encryptedString, It.IsAny<CancellationToken>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetAsync_WhenCalledWithTypeInfo_ItShouldSerializeGivenValueAndEncryptIt()
|
||||||
|
{
|
||||||
|
var key = "Database";
|
||||||
|
var config = new DummyConfig("localhost", 9999);
|
||||||
|
var encryptedString = "encryptedString";
|
||||||
|
var json = JsonSerializer.Serialize(config);
|
||||||
|
|
||||||
|
_mockCryptoProvider.Setup(m => m.Encrypt(json)).Returns(encryptedString);
|
||||||
|
|
||||||
|
await _sut.SetAsync(key, config, SecureConfigTestsJsonContext.Default.DummyConfig);
|
||||||
|
|
||||||
|
_mockStorageProvider.Verify(m => m.WriteAsync(key, encryptedString, It.IsAny<CancellationToken>()), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetAsync_WhenCalledWithNullKey_ItShouldThrowArgumentNullException()
|
public async Task GetAsync_WhenCalledWithNullKey_ItShouldThrowArgumentNullException()
|
||||||
{
|
{
|
||||||
var act = async () => await _sut.GetAsync<DummyConfig>(null!);
|
var act = async () => await _sut.GetAsync(null!, SecureConfigTestsJsonContext.Default.DummyConfig);
|
||||||
|
|
||||||
await act.Should().ThrowAsync<ArgumentNullException>();
|
await act.Should().ThrowAsync<ArgumentException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetAsync_WhenKeyExistsAndJsonContextNotSet_ItShouldReadDecryptAndDeserializeTheValue()
|
||||||
|
{
|
||||||
|
var key = "Database";
|
||||||
|
var expectedConfig = new DummyConfig("localhost", 9999);
|
||||||
|
var encryptedString = "encryptedString";
|
||||||
|
var json = JsonSerializer.Serialize(expectedConfig);
|
||||||
|
|
||||||
|
_mockStorageProvider.Setup(m => m.ReadAsync(key, It.IsAny<CancellationToken>())).ReturnsAsync(encryptedString);
|
||||||
|
_mockCryptoProvider.Setup(m => m.Decrypt(encryptedString)).Returns(json);
|
||||||
|
|
||||||
|
var act = async () => await _sut.GetAsync<DummyConfig>(key);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<InvalidOperationException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetAsync_WhenKeyExistsAndJsonContextIsSet_ItShouldReadDecryptAndDeserializeTheValue()
|
||||||
|
{
|
||||||
|
var key = "Database";
|
||||||
|
var expectedConfig = new DummyConfig("localhost", 9999);
|
||||||
|
var encryptedString = "encryptedString";
|
||||||
|
var json = JsonSerializer.Serialize(expectedConfig);
|
||||||
|
|
||||||
|
_jsonSerializerOptions.TypeInfoResolverChain.Insert(0, SecureConfigTestsJsonContext.Default);
|
||||||
|
_mockStorageProvider.Setup(m => m.ReadAsync(key, It.IsAny<CancellationToken>())).ReturnsAsync(encryptedString);
|
||||||
|
_mockCryptoProvider.Setup(m => m.Decrypt(encryptedString)).Returns(json);
|
||||||
|
|
||||||
|
var result = await _sut.GetAsync(key, SecureConfigTestsJsonContext.Default.DummyConfig);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(expectedConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -83,10 +158,10 @@ public class SecureConfigTests
|
|||||||
var encryptedString = "encryptedString";
|
var encryptedString = "encryptedString";
|
||||||
var json = JsonSerializer.Serialize(expectedConfig);
|
var json = JsonSerializer.Serialize(expectedConfig);
|
||||||
|
|
||||||
_mockStorageProvider.Setup(m => m.ReadAsync(key)).ReturnsAsync(encryptedString);
|
_mockStorageProvider.Setup(m => m.ReadAsync(key, It.IsAny<CancellationToken>())).ReturnsAsync(encryptedString);
|
||||||
_mockCryptoProvider.Setup(m => m.Decrypt(encryptedString)).Returns(json);
|
_mockCryptoProvider.Setup(m => m.Decrypt(encryptedString)).Returns(json);
|
||||||
|
|
||||||
var result = await _sut.GetAsync<DummyConfig>(key);
|
var result = await _sut.GetAsync(key, SecureConfigTestsJsonContext.Default.DummyConfig);
|
||||||
|
|
||||||
result.Should().BeEquivalentTo(expectedConfig);
|
result.Should().BeEquivalentTo(expectedConfig);
|
||||||
}
|
}
|
||||||
@@ -98,9 +173,9 @@ public class SecureConfigTests
|
|||||||
var expectedConfig = new DummyConfig("localhost", 9999);
|
var expectedConfig = new DummyConfig("localhost", 9999);
|
||||||
var json = JsonSerializer.Serialize(expectedConfig);
|
var json = JsonSerializer.Serialize(expectedConfig);
|
||||||
|
|
||||||
_mockStorageProvider.Setup(m => m.ReadAsync(key)).ReturnsAsync(string.Empty);
|
_mockStorageProvider.Setup(m => m.ReadAsync(key, It.IsAny<CancellationToken>())).ReturnsAsync(string.Empty);
|
||||||
|
|
||||||
var result = await _sut.GetAsync<DummyConfig>(key);
|
var result = await _sut.GetAsync(key, SecureConfigTestsJsonContext.Default.DummyConfig);
|
||||||
|
|
||||||
result.Should().BeNull();
|
result.Should().BeNull();
|
||||||
}
|
}
|
||||||
@@ -110,13 +185,19 @@ public class SecureConfigTests
|
|||||||
{
|
{
|
||||||
var key = "Database";
|
var key = "Database";
|
||||||
|
|
||||||
_mockStorageProvider.Setup(m => m.DeleteAsync(key)).ReturnsAsync(true);
|
_mockStorageProvider.Setup(m => m.DeleteAsync(key, It.IsAny<CancellationToken>())).ReturnsAsync(true);
|
||||||
|
|
||||||
var result = await _sut.DeleteAsync(key);
|
var result = await _sut.DeleteAsync(key);
|
||||||
|
|
||||||
result.Should().BeTrue();
|
result.Should().BeTrue();
|
||||||
_mockStorageProvider.Verify(m => m.DeleteAsync(key), Times.Once());
|
_mockStorageProvider.Verify(m => m.DeleteAsync(key, It.IsAny<CancellationToken>()), Times.Once());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed record DummyConfig(string Host, int Port);
|
internal sealed record DummyConfig(string Host, int Port);
|
||||||
|
|
||||||
|
[JsonSerializable(typeof(string))]
|
||||||
|
[JsonSerializable(typeof(DummyConfig))]
|
||||||
|
internal partial class SecureConfigTestsJsonContext : JsonSerializerContext
|
||||||
|
{
|
||||||
}
|
}
|
||||||
+748
@@ -0,0 +1,748 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
using Moq;
|
||||||
|
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Cryptography;
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Storage;
|
||||||
|
|
||||||
|
namespace StevanFreeborn.Extensions.Configuration.Secure.Tests.Unit;
|
||||||
|
|
||||||
|
public class SecureConfigExtensionsTests
|
||||||
|
{
|
||||||
|
private readonly Mock<ISecureStorageProvider> _mockStorageProvider = new();
|
||||||
|
private readonly Mock<IEncryptionKeyProvider> _mockKeyProvider = new();
|
||||||
|
private readonly Mock<ICryptoProvider> _mockCryptoProvider = new();
|
||||||
|
private readonly Mock<ILoggerFactory> _mockLoggerFactory = new();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_WithNullBuilder_ItShouldThrowArgumentNullException()
|
||||||
|
{
|
||||||
|
IConfigurationBuilder builder = null!;
|
||||||
|
|
||||||
|
var act = () => builder.AddSecureConfig(config => { });
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentNullException>()
|
||||||
|
.WithParameterName("builder");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_WithNullConfigure_ItShouldThrowArgumentNullException()
|
||||||
|
{
|
||||||
|
var builder = new ConfigurationBuilder();
|
||||||
|
|
||||||
|
var act = () => builder.AddSecureConfig(null!);
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentNullException>()
|
||||||
|
.WithParameterName("configure");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_WhenStorageProviderNotConfigured_ItShouldThrowInvalidOperationException()
|
||||||
|
{
|
||||||
|
var builder = new ConfigurationBuilder();
|
||||||
|
|
||||||
|
var act = () => builder.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.WithBase64EncryptionKey(GetValidBase64Key());
|
||||||
|
config.WithAesCryptoProvider();
|
||||||
|
});
|
||||||
|
|
||||||
|
act.Should().Throw<InvalidOperationException>()
|
||||||
|
.WithMessage("A storage provider must be configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_WhenKeyProviderNotConfigured_ItShouldThrowInvalidOperationException()
|
||||||
|
{
|
||||||
|
var builder = new ConfigurationBuilder();
|
||||||
|
|
||||||
|
var act = () => builder.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithAesCryptoProvider();
|
||||||
|
});
|
||||||
|
|
||||||
|
act.Should().Throw<InvalidOperationException>()
|
||||||
|
.WithMessage("A key provider must be configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_WhenCryptoProviderNotConfigured_ItShouldThrowInvalidOperationException()
|
||||||
|
{
|
||||||
|
var builder = new ConfigurationBuilder();
|
||||||
|
|
||||||
|
var act = () => builder.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithBase64EncryptionKey(GetValidBase64Key());
|
||||||
|
});
|
||||||
|
|
||||||
|
act.Should().Throw<InvalidOperationException>()
|
||||||
|
.WithMessage("A crypto provider must be configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_WhenProperlyConfigured_ItShouldAddSecureConfigSource()
|
||||||
|
{
|
||||||
|
var builder = new ConfigurationBuilder();
|
||||||
|
|
||||||
|
var result = builder.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithBase64EncryptionKey(GetValidBase64Key());
|
||||||
|
config.WithAesCryptoProvider();
|
||||||
|
});
|
||||||
|
|
||||||
|
result.Should().BeSameAs(builder);
|
||||||
|
builder.Sources.Should().ContainSingle(s => s is SecureConfigSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_WhenProperlyConfigured_ItShouldBuildConfigurationProvider()
|
||||||
|
{
|
||||||
|
var encryptedValue = "encrypted_value";
|
||||||
|
var decryptedJson = @"{ ""Setting"": ""Value"", ""Number"": 42 }";
|
||||||
|
|
||||||
|
_mockStorageProvider
|
||||||
|
.Setup(s => s.ReadAllAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new Dictionary<string, string> { { "MySection", encryptedValue } });
|
||||||
|
|
||||||
|
_mockCryptoProvider
|
||||||
|
.Setup(c => c.Decrypt(encryptedValue))
|
||||||
|
.Returns(decryptedJson);
|
||||||
|
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithCustomKeyProvider(_mockKeyProvider.Object);
|
||||||
|
config.WithCustomCryptoProvider((kp) => _mockCryptoProvider.Object);
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
configuration.GetSection("MySection")["Setting"].Should().Be("Value");
|
||||||
|
configuration.GetSection("MySection")["Number"].Should().Be("42");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_WithJsonFileStorage_ItShouldWorkEndToEnd()
|
||||||
|
{
|
||||||
|
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||||
|
var filePath = Path.Combine(tempDir, "test_config.json");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
|
||||||
|
var keyBytes = new byte[32];
|
||||||
|
RandomNumberGenerator.Fill(keyBytes);
|
||||||
|
var keyProvider = new StaticKeyProvider(Convert.ToBase64String(keyBytes));
|
||||||
|
var cryptoProvider = new AesCryptoProvider(keyProvider);
|
||||||
|
|
||||||
|
var originalData = @"{ ""AppName"": ""TestApp"", ""Version"": ""1.0.0"" }";
|
||||||
|
var encryptedData = cryptoProvider.Encrypt(originalData);
|
||||||
|
|
||||||
|
File.WriteAllText(filePath, $"{{\"Settings\":\"{encryptedData}\"}}");
|
||||||
|
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseJsonFileStorage(options =>
|
||||||
|
{
|
||||||
|
options.DirectoryPath = tempDir;
|
||||||
|
options.FileName = "test_config.json";
|
||||||
|
});
|
||||||
|
config.WithBase64EncryptionKey(Convert.ToBase64String(keyBytes));
|
||||||
|
config.WithAesCryptoProvider();
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
configuration["Settings:AppName"].Should().Be("TestApp");
|
||||||
|
configuration["Settings:Version"].Should().Be("1.0.0");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (File.Exists(filePath)) File.Delete(filePath);
|
||||||
|
if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_WithLoggerFactory_ItShouldUseProvidedLoggerFactory()
|
||||||
|
{
|
||||||
|
_mockStorageProvider
|
||||||
|
.Setup(s => s.ReadAllAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new Dictionary<string, string>());
|
||||||
|
|
||||||
|
var mockLogger = new Mock<ILogger<SecureConfigProvider>>();
|
||||||
|
|
||||||
|
_mockLoggerFactory
|
||||||
|
.Setup(f => f.CreateLogger(typeof(SecureConfigProvider).FullName!))
|
||||||
|
.Returns(mockLogger.Object);
|
||||||
|
|
||||||
|
var builder = new ConfigurationBuilder();
|
||||||
|
|
||||||
|
builder.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithBase64EncryptionKey(GetValidBase64Key());
|
||||||
|
config.WithAesCryptoProvider();
|
||||||
|
config.WithLoggerFactory(_mockLoggerFactory.Object);
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_mockLoggerFactory.Verify(
|
||||||
|
f => f.CreateLogger(typeof(SecureConfigProvider).FullName!),
|
||||||
|
Times.Once()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_WithMultipleCalls_ItShouldAddMultipleSources()
|
||||||
|
{
|
||||||
|
var builder = new ConfigurationBuilder();
|
||||||
|
|
||||||
|
builder.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithBase64EncryptionKey(GetValidBase64Key());
|
||||||
|
config.WithAesCryptoProvider();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithBase64EncryptionKey(GetValidBase64Key());
|
||||||
|
config.WithAesCryptoProvider();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Sources.Should().HaveCount(2);
|
||||||
|
builder.Sources.Should().AllBeOfType<SecureConfigSource>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_ServiceCollection_WithNullServices_ItShouldThrowArgumentNullException()
|
||||||
|
{
|
||||||
|
IServiceCollection services = null!;
|
||||||
|
|
||||||
|
var act = () => services.AddSecureConfig(config => { });
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentNullException>()
|
||||||
|
.WithParameterName("services");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_ServiceCollection_WithNullConfigure_ItShouldThrowArgumentNullException()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
var act = () => services.AddSecureConfig(null!);
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentNullException>()
|
||||||
|
.WithParameterName("configure");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_ServiceCollection_WhenStorageProviderNotConfigured_ItShouldThrowInvalidOperationException()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
var act = () => services.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.WithBase64EncryptionKey(GetValidBase64Key());
|
||||||
|
config.WithAesCryptoProvider();
|
||||||
|
});
|
||||||
|
|
||||||
|
act.Should().Throw<InvalidOperationException>()
|
||||||
|
.WithMessage("A storage provider must be configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_ServiceCollection_WhenKeyProviderNotConfigured_ItShouldThrowInvalidOperationException()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
var act = () => services.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithAesCryptoProvider();
|
||||||
|
});
|
||||||
|
|
||||||
|
act.Should().Throw<InvalidOperationException>()
|
||||||
|
.WithMessage("A key provider must be configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_ServiceCollection_WhenCryptoProviderNotConfigured_ItShouldThrowInvalidOperationException()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
var act = () => services.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithBase64EncryptionKey(GetValidBase64Key());
|
||||||
|
});
|
||||||
|
|
||||||
|
act.Should().Throw<InvalidOperationException>()
|
||||||
|
.WithMessage("A crypto provider must be configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_ServiceCollection_WhenProperlyConfigured_ItShouldRegisterServices()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithCustomKeyProvider(_mockKeyProvider.Object);
|
||||||
|
config.WithCustomCryptoProvider((kp) => _mockCryptoProvider.Object);
|
||||||
|
});
|
||||||
|
|
||||||
|
services.Should().Contain(s => s.ServiceType == typeof(ISecureStorageProvider));
|
||||||
|
services.Should().Contain(s => s.ServiceType == typeof(IEncryptionKeyProvider));
|
||||||
|
services.Should().Contain(s => s.ServiceType == typeof(ICryptoProvider));
|
||||||
|
services.Should().Contain(s => s.ServiceType == typeof(ISecureConfig));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_ServiceCollection_ItShouldReturnServiceCollection()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
var result = services.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithCustomKeyProvider(_mockKeyProvider.Object);
|
||||||
|
config.WithCustomCryptoProvider((kp) => _mockCryptoProvider.Object);
|
||||||
|
});
|
||||||
|
|
||||||
|
result.Should().BeSameAs(services);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_ServiceCollection_ItShouldRegisterStorageProviderAsSingleton()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithCustomKeyProvider(_mockKeyProvider.Object);
|
||||||
|
config.WithCustomCryptoProvider((kp) => _mockCryptoProvider.Object);
|
||||||
|
});
|
||||||
|
|
||||||
|
var storageDescriptor = services.First(s => s.ServiceType == typeof(ISecureStorageProvider));
|
||||||
|
storageDescriptor.Lifetime.Should().Be(ServiceLifetime.Singleton);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_ServiceCollection_ItShouldRegisterKeyProviderAsSingleton()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithCustomKeyProvider(_mockKeyProvider.Object);
|
||||||
|
config.WithCustomCryptoProvider((kp) => _mockCryptoProvider.Object);
|
||||||
|
});
|
||||||
|
|
||||||
|
var keyDescriptor = services.First(s => s.ServiceType == typeof(IEncryptionKeyProvider));
|
||||||
|
keyDescriptor.Lifetime.Should().Be(ServiceLifetime.Singleton);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_ServiceCollection_ItShouldRegisterCryptoProviderAsSingleton()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithCustomKeyProvider(_mockKeyProvider.Object);
|
||||||
|
config.WithCustomCryptoProvider((kp) => _mockCryptoProvider.Object);
|
||||||
|
});
|
||||||
|
|
||||||
|
var cryptoDescriptor = services.First(s => s.ServiceType == typeof(ICryptoProvider));
|
||||||
|
cryptoDescriptor.Lifetime.Should().Be(ServiceLifetime.Singleton);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_ServiceCollection_ItShouldRegisterSecureConfigAsSingleton()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithCustomKeyProvider(_mockKeyProvider.Object);
|
||||||
|
config.WithCustomCryptoProvider((kp) => _mockCryptoProvider.Object);
|
||||||
|
});
|
||||||
|
|
||||||
|
var secureConfigDescriptor = services.First(s => s.ServiceType == typeof(ISecureConfig));
|
||||||
|
secureConfigDescriptor.Lifetime.Should().Be(ServiceLifetime.Singleton);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_ServiceCollection_ItShouldResolveSecureConfigFromDI()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithCustomKeyProvider(_mockKeyProvider.Object);
|
||||||
|
config.WithCustomCryptoProvider((kp) => _mockCryptoProvider.Object);
|
||||||
|
});
|
||||||
|
|
||||||
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
var secureConfig = serviceProvider.GetService<ISecureConfig>();
|
||||||
|
|
||||||
|
secureConfig.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_ServiceCollection_ItShouldResolveSameSecureConfigInstance()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithCustomKeyProvider(_mockKeyProvider.Object);
|
||||||
|
config.WithCustomCryptoProvider((kp) => _mockCryptoProvider.Object);
|
||||||
|
});
|
||||||
|
|
||||||
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
var instance1 = serviceProvider.GetRequiredService<ISecureConfig>();
|
||||||
|
var instance2 = serviceProvider.GetRequiredService<ISecureConfig>();
|
||||||
|
|
||||||
|
instance1.Should().BeSameAs(instance2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_ServiceCollection_ItShouldResolveSameStorageProviderInstance()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithCustomKeyProvider(_mockKeyProvider.Object);
|
||||||
|
config.WithCustomCryptoProvider((kp) => _mockCryptoProvider.Object);
|
||||||
|
});
|
||||||
|
|
||||||
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
var instance1 = serviceProvider.GetRequiredService<ISecureStorageProvider>();
|
||||||
|
var instance2 = serviceProvider.GetRequiredService<ISecureStorageProvider>();
|
||||||
|
|
||||||
|
instance1.Should().BeSameAs(_mockStorageProvider.Object);
|
||||||
|
instance2.Should().BeSameAs(_mockStorageProvider.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_ServiceCollection_ItShouldResolveSameKeyProviderInstance()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithCustomKeyProvider(_mockKeyProvider.Object);
|
||||||
|
config.WithCustomCryptoProvider((kp) => _mockCryptoProvider.Object);
|
||||||
|
});
|
||||||
|
|
||||||
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
var instance1 = serviceProvider.GetRequiredService<IEncryptionKeyProvider>();
|
||||||
|
var instance2 = serviceProvider.GetRequiredService<IEncryptionKeyProvider>();
|
||||||
|
|
||||||
|
instance1.Should().BeSameAs(_mockKeyProvider.Object);
|
||||||
|
instance2.Should().BeSameAs(_mockKeyProvider.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_ServiceCollection_ItShouldResolveSameCryptoProviderInstance()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithCustomKeyProvider(_mockKeyProvider.Object);
|
||||||
|
config.WithCustomCryptoProvider((kp) => _mockCryptoProvider.Object);
|
||||||
|
});
|
||||||
|
|
||||||
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
var instance1 = serviceProvider.GetRequiredService<ICryptoProvider>();
|
||||||
|
var instance2 = serviceProvider.GetRequiredService<ICryptoProvider>();
|
||||||
|
|
||||||
|
instance1.Should().BeSameAs(_mockCryptoProvider.Object);
|
||||||
|
instance2.Should().BeSameAs(_mockCryptoProvider.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddSecureConfig_ServiceCollection_WithRealAesCryptoProvider_ItShouldWorkEndToEnd()
|
||||||
|
{
|
||||||
|
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||||
|
var filePath = Path.Combine(tempDir, "test_config.json");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
|
||||||
|
var keyBytes = new byte[32];
|
||||||
|
RandomNumberGenerator.Fill(keyBytes);
|
||||||
|
var base64Key = Convert.ToBase64String(keyBytes);
|
||||||
|
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.AddJsonAotContext(SecureConfigExtensionsTestsJsonContext.Default);
|
||||||
|
config.UseJsonFileStorage(options =>
|
||||||
|
{
|
||||||
|
options.DirectoryPath = tempDir;
|
||||||
|
options.FileName = "test_config.json";
|
||||||
|
});
|
||||||
|
config.WithBase64EncryptionKey(base64Key);
|
||||||
|
config.WithAesCryptoProvider();
|
||||||
|
});
|
||||||
|
|
||||||
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
var secureConfig = serviceProvider.GetRequiredService<ISecureConfig>();
|
||||||
|
var storageProvider = serviceProvider.GetRequiredService<ISecureStorageProvider>();
|
||||||
|
|
||||||
|
var testObject = new TestConfig { Name = "TestName", Value = 123 };
|
||||||
|
await secureConfig.SetAsync("TestKey", testObject);
|
||||||
|
|
||||||
|
var retrievedObject = await secureConfig.GetAsync<TestConfig>("TestKey");
|
||||||
|
|
||||||
|
retrievedObject.Should().NotBeNull();
|
||||||
|
retrievedObject!.Name.Should().Be("TestName");
|
||||||
|
retrievedObject.Value.Should().Be(123);
|
||||||
|
|
||||||
|
File.Exists(filePath).Should().BeTrue();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (File.Exists(filePath)) File.Delete(filePath);
|
||||||
|
if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddSecureConfig_ServiceCollection_WithMachineIdKey_ItShouldWorkEndToEnd()
|
||||||
|
{
|
||||||
|
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.AddJsonAotContext(SecureConfigExtensionsTestsJsonContext.Default);
|
||||||
|
config.UseJsonFileStorage(options =>
|
||||||
|
{
|
||||||
|
options.DirectoryPath = tempDir;
|
||||||
|
options.FileName = "test_config.json";
|
||||||
|
});
|
||||||
|
config.WithMachineIdKey();
|
||||||
|
config.WithAesCryptoProvider();
|
||||||
|
});
|
||||||
|
|
||||||
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
var secureConfig = serviceProvider.GetRequiredService<ISecureConfig>();
|
||||||
|
|
||||||
|
var testObject = new TestConfig { Name = "MachineIdTest", Value = 456 };
|
||||||
|
await secureConfig.SetAsync("MachineTest", testObject);
|
||||||
|
|
||||||
|
var retrievedObject = await secureConfig.GetAsync<TestConfig>("MachineTest");
|
||||||
|
|
||||||
|
retrievedObject.Should().NotBeNull();
|
||||||
|
retrievedObject!.Name.Should().Be("MachineIdTest");
|
||||||
|
retrievedObject.Value.Should().Be(456);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_ServiceCollection_ItShouldRegisterJsonSerializerOptions()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithCustomKeyProvider(_mockKeyProvider.Object);
|
||||||
|
config.WithCustomCryptoProvider((kp) => _mockCryptoProvider.Object);
|
||||||
|
});
|
||||||
|
|
||||||
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
var keyedService = serviceProvider.GetKeyedService<JsonSerializerOptions>("SecureConfigJsonSerializerOptions");
|
||||||
|
|
||||||
|
keyedService.Should().NotBeNull();
|
||||||
|
keyedService!.PropertyNameCaseInsensitive.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_CombinedWithOtherProviders_ItShouldMergeConfiguration()
|
||||||
|
{
|
||||||
|
var encryptedValue = "encrypted_value";
|
||||||
|
var decryptedJson = @"{ ""SecureSetting"": ""SecureValue"" }";
|
||||||
|
|
||||||
|
_mockStorageProvider
|
||||||
|
.Setup(s => s.ReadAllAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new Dictionary<string, string> { { "SecureSection", encryptedValue } });
|
||||||
|
|
||||||
|
_mockCryptoProvider
|
||||||
|
.Setup(c => c.Decrypt(encryptedValue))
|
||||||
|
.Returns(decryptedJson);
|
||||||
|
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
["RegularSetting"] = "RegularValue",
|
||||||
|
["AnotherSetting"] = "AnotherValue"
|
||||||
|
})
|
||||||
|
.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithCustomKeyProvider(_mockKeyProvider.Object);
|
||||||
|
config.WithCustomCryptoProvider((kp) => _mockCryptoProvider.Object);
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
configuration["RegularSetting"].Should().Be("RegularValue");
|
||||||
|
configuration["AnotherSetting"].Should().Be("AnotherValue");
|
||||||
|
configuration["SecureSection:SecureSetting"].Should().Be("SecureValue");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_WithNestedConfiguration_ItShouldFlattenCorrectly()
|
||||||
|
{
|
||||||
|
var encryptedValue = "encrypted_nested";
|
||||||
|
var decryptedJson = @"{
|
||||||
|
""Level1"": {
|
||||||
|
""Level2"": {
|
||||||
|
""Setting"": ""NestedValue""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}";
|
||||||
|
|
||||||
|
_mockStorageProvider
|
||||||
|
.Setup(s => s.ReadAllAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new Dictionary<string, string> { { "Nested", encryptedValue } });
|
||||||
|
|
||||||
|
_mockCryptoProvider
|
||||||
|
.Setup(c => c.Decrypt(encryptedValue))
|
||||||
|
.Returns(decryptedJson);
|
||||||
|
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithCustomKeyProvider(_mockKeyProvider.Object);
|
||||||
|
config.WithCustomCryptoProvider((kp) => _mockCryptoProvider.Object);
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
configuration["Nested:Level1:Level2:Setting"].Should().Be("NestedValue");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_WithArrays_ItShouldIndexCorrectly()
|
||||||
|
{
|
||||||
|
var encryptedValue = "encrypted_array";
|
||||||
|
var decryptedJson = @"{ ""Items"": [""First"", ""Second"", ""Third""] }";
|
||||||
|
|
||||||
|
_mockStorageProvider
|
||||||
|
.Setup(s => s.ReadAllAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new Dictionary<string, string> { { "ArraySection", encryptedValue } });
|
||||||
|
|
||||||
|
_mockCryptoProvider
|
||||||
|
.Setup(c => c.Decrypt(encryptedValue))
|
||||||
|
.Returns(decryptedJson);
|
||||||
|
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithCustomKeyProvider(_mockKeyProvider.Object);
|
||||||
|
config.WithCustomCryptoProvider((kp) => _mockCryptoProvider.Object);
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
configuration["ArraySection:Items:0"].Should().Be("First");
|
||||||
|
configuration["ArraySection:Items:1"].Should().Be("Second");
|
||||||
|
configuration["ArraySection:Items:2"].Should().Be("Third");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSecureConfig_WithMultipleSections_ItShouldLoadAllSections()
|
||||||
|
{
|
||||||
|
var encrypted1 = "encrypted1";
|
||||||
|
var encrypted2 = "encrypted2";
|
||||||
|
var decrypted1 = @"{ ""Setting1"": ""Value1"" }";
|
||||||
|
var decrypted2 = @"{ ""Setting2"": ""Value2"" }";
|
||||||
|
|
||||||
|
_mockStorageProvider
|
||||||
|
.Setup(s => s.ReadAllAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "Section1", encrypted1 },
|
||||||
|
{ "Section2", encrypted2 }
|
||||||
|
});
|
||||||
|
|
||||||
|
_mockCryptoProvider
|
||||||
|
.Setup(c => c.Decrypt("encrypted1"))
|
||||||
|
.Returns(decrypted1);
|
||||||
|
|
||||||
|
_mockCryptoProvider
|
||||||
|
.Setup(c => c.Decrypt("encrypted2"))
|
||||||
|
.Returns(decrypted2);
|
||||||
|
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddSecureConfig(config =>
|
||||||
|
{
|
||||||
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
|
config.WithCustomKeyProvider(_mockKeyProvider.Object);
|
||||||
|
config.WithCustomCryptoProvider((kp) => _mockCryptoProvider.Object);
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
configuration["Section1:Setting1"].Should().Be("Value1");
|
||||||
|
configuration["Section2:Setting2"].Should().Be("Value2");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetValidBase64Key()
|
||||||
|
{
|
||||||
|
var keyBytes = new byte[32];
|
||||||
|
RandomNumberGenerator.Fill(keyBytes);
|
||||||
|
return Convert.ToBase64String(keyBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class TestConfig
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public int Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonSerializable(typeof(TestConfig))]
|
||||||
|
internal partial class SecureConfigExtensionsTestsJsonContext : JsonSerializerContext
|
||||||
|
{
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user