diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure/Configuration/ISecureConfigBuilder.cs b/src/StevanFreeborn.Extensions.Configuration.Secure/Configuration/ISecureConfigBuilder.cs
index f940423..d8ff72d 100644
--- a/src/StevanFreeborn.Extensions.Configuration.Secure/Configuration/ISecureConfigBuilder.cs
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure/Configuration/ISecureConfigBuilder.cs
@@ -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;
///
-/// Provides a builder interface for configuring secure configuration services.
+/// Provides a fluent builder interface for configuring secure configuration storage and encryption.
///
public interface ISecureConfigBuilder
{
///
- /// Gets the used to register secure configuration services.
+ /// Configures JSON file-based storage using the provided options instance.
///
- IServiceCollection Services { get; }
+ /// The JSON storage configuration options.
+ /// The current instance for method chaining.
+ ISecureConfigBuilder UseJsonFileStorage(JsonStorageOptions options);
+
+ ///
+ /// Configures JSON file-based storage using an action to configure the options.
+ ///
+ /// An action to configure the .
+ /// The current instance for method chaining.
+ ISecureConfigBuilder UseJsonFileStorage(Action configure);
+
+ ///
+ /// Registers a JSON AOT source-generated context for serializing complex types.
+ ///
+ /// A context that implements that will be used for JSON serialization.
+ /// The current instance for method chaining.
+ ISecureConfigBuilder AddJsonAotContext(IJsonTypeInfoResolver context);
+
+ ///
+ /// Configures a custom storage provider for secure configuration data.
+ ///
+ /// The custom storage provider implementation.
+ /// The current instance for method chaining.
+ ISecureConfigBuilder UseCustomStorage(ISecureStorageProvider provider);
+
+ ///
+ /// Configures encryption using a Base64-encoded encryption key.
+ ///
+ /// The Base64-encoded encryption key string.
+ /// The current instance for method chaining.
+ ISecureConfigBuilder WithBase64EncryptionKey(string key);
+
+ ///
+ /// Configures encryption using a key derived from the machine id.
+ ///
+ /// The current instance for method chaining.
+ ISecureConfigBuilder WithMachineIdKey();
+
+
+ ///
+ /// Configures a custom encryption key provider.
+ ///
+ /// The custom encryption key provider implementation.
+ /// The current instance for method chaining.
+ ISecureConfigBuilder WithCustomKeyProvider(IEncryptionKeyProvider keyProvider);
+
+ ///
+ /// Configures logging using the provided logger factory.
+ ///
+ /// The logger factory to use for logging operations.
+ /// The current instance for method chaining.
+ ISecureConfigBuilder WithLoggerFactory(ILoggerFactory loggerFactory);
+
+ ///
+ /// Configures AES crypto provider for encryption and decryption
+ ///
+ /// The current instance for method chaining.
+ ISecureConfigBuilder WithAesCryptoProvider();
+
+ ///
+ /// Configures the factory function that will be used to create the crypto provider for encryption and decryption
+ ///
+ /// The crypto provider factor to use for encryption and decryption operations.
+ /// The current instance for method chaining.
+ ISecureConfigBuilder WithCustomCryptoProvider(Func cryptoProviderFactory);
}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure/Configuration/SecureConfigBuilder.cs b/src/StevanFreeborn.Extensions.Configuration.Secure/Configuration/SecureConfigBuilder.cs
new file mode 100644
index 0000000..cba3650
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure/Configuration/SecureConfigBuilder.cs
@@ -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? 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 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();
+ 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 cryptoProviderFactory)
+ {
+ CryptoProviderFactory = cryptoProviderFactory;
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure/Configuration/SecureConfigSource.cs b/src/StevanFreeborn.Extensions.Configuration.Secure/Configuration/SecureConfigSource.cs
new file mode 100644
index 0000000..b5b1088
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure/Configuration/SecureConfigSource.cs
@@ -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();
+ return new SecureConfigProvider(_storageProvider, _cryptoProvider, logger);
+ }
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure/SecureConfigExtensions.cs b/src/StevanFreeborn.Extensions.Configuration.Secure/SecureConfigExtensions.cs
new file mode 100644
index 0000000..3221b9a
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure/SecureConfigExtensions.cs
@@ -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;
+
+///
+/// Provides extension methods for configuring secure configuration in .NET applications.
+///
+public static class SecureConfigExtensions
+{
+ private const string JsonSerializerOptionsKey = "SecureConfigJsonSerializerOptions";
+
+ ///
+ /// Adds secure configuration to the configuration builder.
+ ///
+ /// The configuration builder to add secure configuration to.
+ /// An action to configure the secure configuration builder.
+ /// The configuration builder with secure configuration added.
+ /// Thrown when or is null.
+ /// Thrown when a required provider is not configured.
+ public static IConfigurationBuilder AddSecureConfig(
+ this IConfigurationBuilder builder,
+ Action 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);
+ }
+
+ ///
+ /// Adds secure configuration services to the service collection.
+ ///
+ /// The service collection to add secure configuration services to.
+ /// An action to configure the secure configuration builder.
+ /// The service collection with secure configuration services added.
+ /// Thrown when or is null.
+ /// Thrown when a required provider is not configured.
+ public static IServiceCollection AddSecureConfig(
+ this IServiceCollection services,
+ Action 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(sp =>
+ {
+ var storageProvider = sp.GetRequiredService();
+ var cryptoProvider = sp.GetRequiredService();
+ var serializerOptions = sp.GetRequiredKeyedService(JsonSerializerOptionsKey);
+ return new SecureConfig(storageProvider, cryptoProvider, serializerOptions);
+ });
+
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/StevanFreeborn.Extensions.Configuration.Secure.Tests.csproj b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/StevanFreeborn.Extensions.Configuration.Secure.Tests.csproj
index 7f9cc0f..9474eab 100644
--- a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/StevanFreeborn.Extensions.Configuration.Secure.Tests.csproj
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/StevanFreeborn.Extensions.Configuration.Secure.Tests.csproj
@@ -1,7 +1,7 @@
- net11.0
+ net8.0;net10.0;
enable
enable
false
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigBuilderTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigBuilderTests.cs
new file mode 100644
index 0000000..8a05a77
--- /dev/null
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigBuilderTests.cs
@@ -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();
+ }
+
+ [Fact]
+ public void UseJsonFileStorage_WithOptions_ItShouldUseProvidedOptions()
+ {
+ var options = new JsonStorageOptions
+ {
+ FileName = "custom.json",
+ DirectoryPath = "/custom/path"
+ };
+
+ _sut.UseJsonFileStorage(options);
+
+ _sut.StorageProvider.Should().BeOfType();
+ }
+
+ [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();
+ }
+
+ [Fact]
+ public void UseJsonFileStorage_WithConfigureAction_ItShouldApplyConfiguration()
+ {
+ _sut.UseJsonFileStorage(opt =>
+ {
+ opt.FileName = "configured.json";
+ opt.DirectoryPath = "/configured/path";
+ });
+
+ _sut.StorageProvider.Should().BeOfType();
+ }
+
+ [Fact]
+ public void UseJsonFileStorage_WithNullConfigureAction_ItShouldThrowArgumentNullException()
+ {
+ var act = () => _sut.UseJsonFileStorage((Action)null!);
+
+ act.Should().Throw()
+ .WithParameterName("configure");
+ }
+
+ [Fact]
+ public void AddJsonAotContext_WithContext_ItShouldAddToResolverChain()
+ {
+ var mockContext = new Mock();
+
+ 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()
+ .WithParameterName("context");
+ }
+
+ [Fact]
+ public void AddJsonAotContext_WhenCalledMultipleTimes_ItShouldAddAllToChain()
+ {
+ var mockContext1 = new Mock();
+ var mockContext2 = new Mock();
+
+ _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();
+
+ 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()
+ .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();
+ }
+
+ [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();
+ }
+
+ [Fact]
+ public void WithMachineIdKey_WhenCalled_ItShouldUseLoggerFactory()
+ {
+ var mockLoggerFactory = new Mock();
+ var mockLogger = new Mock();
+
+ 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();
+
+ 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()
+ .WithParameterName("provider");
+ }
+
+ [Fact]
+ public void WithLoggerFactory_WithFactory_ItShouldSetLoggerFactory()
+ {
+ var mockFactory = new Mock();
+
+ 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();
+ _sut.WithLoggerFactory(mockFactory.Object);
+
+ _sut.WithLoggerFactory(null!);
+
+ _sut.LoggerFactory.Should().Be(NullLoggerFactory.Instance);
+ }
+
+ [Fact]
+ public void WithAesCryptoProvider_WhenCalled_ItShouldSetCryptoProviderFactory()
+ {
+ var mockKeyProvider = new Mock();
+
+ var result = _sut.WithAesCryptoProvider();
+
+ result.Should().BeSameAs(_sut);
+ _sut.CryptoProviderFactory.Should().NotBeNull();
+
+ var cryptoProvider = _sut.CryptoProviderFactory!(mockKeyProvider.Object);
+ cryptoProvider.Should().BeOfType();
+ }
+
+ [Fact]
+ public void WithCustomCryptoProvider_WithFactory_ItShouldSetCryptoProviderFactory()
+ {
+ var mockCryptoProvider = new Mock();
+ var mockKeyProvider = new Mock();
+ Func 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();
+ var mockKeyProvider = new Mock();
+ var mockLoggerFactory = new Mock();
+ var mockCryptoProvider = new Mock();
+
+ _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();
+ var mockProvider2 = new Mock();
+
+ _sut.UseCustomStorage(mockProvider1.Object)
+ .UseCustomStorage(mockProvider2.Object);
+
+ _sut.StorageProvider.Should().BeSameAs(mockProvider2.Object);
+ }
+
+ [Fact]
+ public void MethodChaining_WhenOverridingKeyProvider_ItShouldUseLastSetProvider()
+ {
+ var mockProvider1 = new Mock();
+ var mockProvider2 = new Mock();
+
+ _sut.WithCustomKeyProvider(mockProvider1.Object)
+ .WithCustomKeyProvider(mockProvider2.Object);
+
+ _sut.KeyProvider.Should().BeSameAs(mockProvider2.Object);
+ }
+
+ [Fact]
+ public void MethodChaining_WhenOverridingLoggerFactory_ItShouldUseLastSetFactory()
+ {
+ var mockFactory1 = new Mock();
+ var mockFactory2 = new Mock();
+
+ _sut.WithLoggerFactory(mockFactory1.Object)
+ .WithLoggerFactory(mockFactory2.Object);
+
+ _sut.LoggerFactory.Should().BeSameAs(mockFactory2.Object);
+ }
+}
\ No newline at end of file
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigProviderTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigProviderTests.cs
index cb3c152..d943da9 100644
--- a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigProviderTests.cs
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigProviderTests.cs
@@ -65,7 +65,7 @@ public class SecureConfigProviderTests
{ "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())).ReturnsAsync(storedData);
_mockCrypto.Setup(m => m.Decrypt("encrypted_valid")).Returns(@"{ ""Name"": ""test"" }");
_mockCrypto.Setup(m => m.Decrypt("encrypted_bad")).Throws(new InvalidOperationException("Decryption failed"));
@@ -76,7 +76,7 @@ public class SecureConfigProviderTests
val.Should().Be("test");
_mockLogger.Verify(logger => logger.Log(
- LogLevel.Error,
+ LogLevel.Warning,
It.Is(id => id.Id == 3),
It.Is((state, type) => state.ToString()!.Contains("Failed to decrypt value for key")),
It.IsAny(),
@@ -495,14 +495,14 @@ public class SecureConfigProviderTests
[Fact]
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())).ReturnsAsync(new Dictionary { { "Bad", "enc" } });
_mockCrypto.Setup(m => m.Decrypt("enc")).Returns("not valid json{{{");
_sut.Load();
_mockLogger.Verify(x => x.Log(
- LogLevel.Error,
+ LogLevel.Warning,
It.IsAny(),
It.Is((v, _) => v.ToString()!.Contains("Bad")),
It.IsAny(),
@@ -511,4 +511,4 @@ public class SecureConfigProviderTests
Times.Once()
);
}
-}
+}
\ No newline at end of file
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigSourceTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigSourceTests.cs
new file mode 100644
index 0000000..7a29345
--- /dev/null
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigSourceTests.cs
@@ -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 _mockStorage = new();
+ private readonly Mock _mockCrypto = new();
+ private readonly Mock _mockLoggerFactory = new();
+
+ [Fact]
+ public void Constructor_WhenStorageProviderIsNull_ItShouldThrowArgumentNullException()
+ {
+ var act = () => new SecureConfigSource(
+ storageProvider: null!,
+ cryptoProvider: _mockCrypto.Object,
+ loggerFactory: _mockLoggerFactory.Object
+ );
+
+ act.Should().Throw()
+ .WithParameterName("storageProvider");
+ }
+
+ [Fact]
+ public void Constructor_WhenCryptoProviderIsNull_ItShouldThrowArgumentNullException()
+ {
+ var act = () => new SecureConfigSource(
+ storageProvider: _mockStorage.Object,
+ cryptoProvider: null!,
+ loggerFactory: _mockLoggerFactory.Object
+ );
+
+ act.Should().Throw()
+ .WithParameterName("cryptoProvider");
+ }
+
+ [Fact]
+ public void Constructor_WhenLoggerFactoryIsNull_ItShouldThrowArgumentNullException()
+ {
+ var act = () => new SecureConfigSource(
+ storageProvider: _mockStorage.Object,
+ cryptoProvider: _mockCrypto.Object,
+ loggerFactory: null!
+ );
+
+ act.Should().Throw()
+ .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();
+
+ var result = sut.Build(mockBuilder.Object);
+
+ result.Should().NotBeNull();
+ result.Should().BeOfType();
+ }
+
+ [Fact]
+ public void Build_WhenCalled_ItShouldCreateLoggerFromFactory()
+ {
+ var mockLogger = new Mock>();
+ _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();
+ 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();
+
+ 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();
+ }
+}
\ No newline at end of file
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigTests.cs
index 029d9ca..b684c6a 100644
--- a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigTests.cs
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigTests.cs
@@ -1,28 +1,31 @@
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
using Moq;
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
using StevanFreeborn.Extensions.Configuration.Secure.Cryptography;
using StevanFreeborn.Extensions.Configuration.Secure.Storage;
-using System.Text.Json;
-
namespace StevanFreeborn.Extensions.Configuration.Secure.Tests.Unit.Configuration;
public class SecureConfigTests
{
private readonly Mock _mockCryptoProvider = new();
private readonly Mock _mockStorageProvider = new();
+ private readonly JsonSerializerOptions _jsonSerializerOptions = new();
private readonly SecureConfig _sut;
public SecureConfigTests()
{
- _sut = new(_mockStorageProvider.Object, _mockCryptoProvider.Object);
+ _sut = new(_mockStorageProvider.Object, _mockCryptoProvider.Object, _jsonSerializerOptions);
}
[Fact]
public void Constructor_WhenCalledWithNullStorageProvider_ItShouldThrowArgumentNullException()
{
- var act = () => new SecureConfig(null!, _mockCryptoProvider.Object);
+ var act = () => new SecureConfig(null!, _mockCryptoProvider.Object, _jsonSerializerOptions);
act.Should().Throw();
}
@@ -30,7 +33,15 @@ public class SecureConfigTests
[Fact]
public void Constructor_WhenCalledWithNullCryptoProvider_ItShouldThrowArgumentNullException()
{
- var act = () => new SecureConfig(_mockStorageProvider.Object, null!);
+ var act = () => new SecureConfig(_mockStorageProvider.Object, null!, _jsonSerializerOptions);
+
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void Constructor_WhenCalledWithNullJsonOptions_ItShouldThrowArgumentNullException()
+ {
+ var act = () => new SecureConfig(_mockStorageProvider.Object, _mockCryptoProvider.Object, null!);
act.Should().Throw();
}
@@ -38,22 +49,22 @@ public class SecureConfigTests
[Fact]
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();
+ await act.Should().ThrowAsync();
}
[Fact]
public async Task SetAsync_WhenCalledWithNullValue_ItShouldThrowArgumentNullException()
{
- var act = async () => await _sut.SetAsync("Key", null!);
+ var act = async () => await _sut.SetAsync("Key", null!, SecureConfigTestsJsonContext.Default.String);
await act.Should().ThrowAsync();
}
[Fact]
- public async Task SetAsync_WhenCalled_ItShouldSerializeGivenValueAndEncryptIt()
+ public async Task SetAsync_WhenCalledWithoutJsonContextSet_ItShouldSerializeGivenValueAndEncryptIt()
{
var key = "Database";
var config = new DummyConfig("localhost", 9999);
@@ -62,17 +73,81 @@ public class SecureConfigTests
_mockCryptoProvider.Setup(m => m.Encrypt(json)).Returns(encryptedString);
+ var act = async () => await _sut.SetAsync(key, config);
+
+ await act.Should().ThrowAsync();
+ }
+
+ [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);
- _mockStorageProvider.Verify(m => m.WriteAsync(key, encryptedString), Times.Once());
+ _mockStorageProvider.Verify(m => m.WriteAsync(key, encryptedString, It.IsAny()), 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()), Times.Once());
}
[Fact]
public async Task GetAsync_WhenCalledWithNullKey_ItShouldThrowArgumentNullException()
{
- var act = async () => await _sut.GetAsync(null!);
+ var act = async () => await _sut.GetAsync(null!, SecureConfigTestsJsonContext.Default.DummyConfig);
- await act.Should().ThrowAsync();
+ await act.Should().ThrowAsync();
+ }
+
+ [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())).ReturnsAsync(encryptedString);
+ _mockCryptoProvider.Setup(m => m.Decrypt(encryptedString)).Returns(json);
+
+ var act = async () => await _sut.GetAsync(key);
+
+ await act.Should().ThrowAsync();
+ }
+
+ [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())).ReturnsAsync(encryptedString);
+ _mockCryptoProvider.Setup(m => m.Decrypt(encryptedString)).Returns(json);
+
+ var result = await _sut.GetAsync(key, SecureConfigTestsJsonContext.Default.DummyConfig);
+
+ result.Should().BeEquivalentTo(expectedConfig);
}
[Fact]
@@ -83,10 +158,10 @@ public class SecureConfigTests
var encryptedString = "encryptedString";
var json = JsonSerializer.Serialize(expectedConfig);
- _mockStorageProvider.Setup(m => m.ReadAsync(key)).ReturnsAsync(encryptedString);
+ _mockStorageProvider.Setup(m => m.ReadAsync(key, It.IsAny())).ReturnsAsync(encryptedString);
_mockCryptoProvider.Setup(m => m.Decrypt(encryptedString)).Returns(json);
- var result = await _sut.GetAsync(key);
+ var result = await _sut.GetAsync(key, SecureConfigTestsJsonContext.Default.DummyConfig);
result.Should().BeEquivalentTo(expectedConfig);
}
@@ -98,9 +173,9 @@ public class SecureConfigTests
var expectedConfig = new DummyConfig("localhost", 9999);
var json = JsonSerializer.Serialize(expectedConfig);
- _mockStorageProvider.Setup(m => m.ReadAsync(key)).ReturnsAsync(string.Empty);
+ _mockStorageProvider.Setup(m => m.ReadAsync(key, It.IsAny())).ReturnsAsync(string.Empty);
- var result = await _sut.GetAsync(key);
+ var result = await _sut.GetAsync(key, SecureConfigTestsJsonContext.Default.DummyConfig);
result.Should().BeNull();
}
@@ -110,13 +185,19 @@ public class SecureConfigTests
{
var key = "Database";
- _mockStorageProvider.Setup(m => m.DeleteAsync(key)).ReturnsAsync(true);
+ _mockStorageProvider.Setup(m => m.DeleteAsync(key, It.IsAny())).ReturnsAsync(true);
var result = await _sut.DeleteAsync(key);
result.Should().BeTrue();
- _mockStorageProvider.Verify(m => m.DeleteAsync(key), Times.Once());
+ _mockStorageProvider.Verify(m => m.DeleteAsync(key, It.IsAny()), 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
+{
}
\ No newline at end of file
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/SecureConfigExtensionsTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/SecureConfigExtensionsTests.cs
new file mode 100644
index 0000000..592af45
--- /dev/null
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/SecureConfigExtensionsTests.cs
@@ -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 _mockStorageProvider = new();
+ private readonly Mock _mockKeyProvider = new();
+ private readonly Mock _mockCryptoProvider = new();
+ private readonly Mock _mockLoggerFactory = new();
+
+ [Fact]
+ public void AddSecureConfig_WithNullBuilder_ItShouldThrowArgumentNullException()
+ {
+ IConfigurationBuilder builder = null!;
+
+ var act = () => builder.AddSecureConfig(config => { });
+
+ act.Should().Throw()
+ .WithParameterName("builder");
+ }
+
+ [Fact]
+ public void AddSecureConfig_WithNullConfigure_ItShouldThrowArgumentNullException()
+ {
+ var builder = new ConfigurationBuilder();
+
+ var act = () => builder.AddSecureConfig(null!);
+
+ act.Should().Throw()
+ .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()
+ .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()
+ .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()
+ .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()))
+ .ReturnsAsync(new Dictionary { { "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()))
+ .ReturnsAsync(new Dictionary());
+
+ var mockLogger = new Mock>();
+
+ _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();
+ }
+
+ [Fact]
+ public void AddSecureConfig_ServiceCollection_WithNullServices_ItShouldThrowArgumentNullException()
+ {
+ IServiceCollection services = null!;
+
+ var act = () => services.AddSecureConfig(config => { });
+
+ act.Should().Throw()
+ .WithParameterName("services");
+ }
+
+ [Fact]
+ public void AddSecureConfig_ServiceCollection_WithNullConfigure_ItShouldThrowArgumentNullException()
+ {
+ var services = new ServiceCollection();
+
+ var act = () => services.AddSecureConfig(null!);
+
+ act.Should().Throw()
+ .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()
+ .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()
+ .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()
+ .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();
+
+ 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();
+ var instance2 = serviceProvider.GetRequiredService();
+
+ 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();
+ var instance2 = serviceProvider.GetRequiredService();
+
+ 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();
+ var instance2 = serviceProvider.GetRequiredService();
+
+ 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();
+ var instance2 = serviceProvider.GetRequiredService();
+
+ 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();
+ var storageProvider = serviceProvider.GetRequiredService();
+
+ var testObject = new TestConfig { Name = "TestName", Value = 123 };
+ await secureConfig.SetAsync("TestKey", testObject);
+
+ var retrievedObject = await secureConfig.GetAsync("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();
+
+ var testObject = new TestConfig { Name = "MachineIdTest", Value = 456 };
+ await secureConfig.SetAsync("MachineTest", testObject);
+
+ var retrievedObject = await secureConfig.GetAsync("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("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()))
+ .ReturnsAsync(new Dictionary { { "SecureSection", encryptedValue } });
+
+ _mockCryptoProvider
+ .Setup(c => c.Decrypt(encryptedValue))
+ .Returns(decryptedJson);
+
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["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()))
+ .ReturnsAsync(new Dictionary { { "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()))
+ .ReturnsAsync(new Dictionary { { "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()))
+ .ReturnsAsync(new Dictionary
+ {
+ { "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
+{
+}
\ No newline at end of file