From a8b2ec07843ddfa67f94b10b3926be43335f2d5c Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Thu, 2 Apr 2026 20:31:06 -0500 Subject: [PATCH] tests: add integration tests - refactored to use temp directory helper class - consolidated test key generation to helper class --- .../Common/KeyGenerator.cs | 13 + .../Common/TempDirectory.cs | 20 ++ .../SecureConfigExtensionsTests.cs | 252 +++++++++++++++++ .../SecureConfigOptionsIntegrationTests.cs | 261 ++++++++++++++++++ ...tensions.Configuration.Secure.Tests.csproj | 1 + .../Unit/SecureConfigExtensionsTests.cs | 204 +++++--------- .../Storage/JsonFileStorageProviderTests.cs | 14 +- 7 files changed, 627 insertions(+), 138 deletions(-) create mode 100644 tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Common/KeyGenerator.cs create mode 100644 tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Common/TempDirectory.cs create mode 100644 tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Integration/SecureConfigExtensionsTests.cs create mode 100644 tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Integration/SecureConfigOptionsIntegrationTests.cs diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Common/KeyGenerator.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Common/KeyGenerator.cs new file mode 100644 index 0000000..f4f2be2 --- /dev/null +++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Common/KeyGenerator.cs @@ -0,0 +1,13 @@ +using System.Security.Cryptography; + +namespace StevanFreeborn.Extensions.Configuration.Secure.Tests.Common; + +internal static class KeyGenerator +{ + public static string GetValidBase64Key() + { + var key = new byte[32]; + RandomNumberGenerator.Fill(key); + return Convert.ToBase64String(key); + } +} \ No newline at end of file diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Common/TempDirectory.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Common/TempDirectory.cs new file mode 100644 index 0000000..0ccb65e --- /dev/null +++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Common/TempDirectory.cs @@ -0,0 +1,20 @@ +namespace StevanFreeborn.Extensions.Configuration.Secure.Tests.Common; + +internal sealed class TempDirectory : IDisposable +{ + public string Path { get; } + + public TempDirectory() + { + Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(Path); + } + + public void Dispose() + { + if (Directory.Exists(Path)) + { + Directory.Delete(Path, true); + } + } +} diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Integration/SecureConfigExtensionsTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Integration/SecureConfigExtensionsTests.cs new file mode 100644 index 0000000..b1e8952 --- /dev/null +++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Integration/SecureConfigExtensionsTests.cs @@ -0,0 +1,252 @@ +using System.Security.Cryptography; +using System.Text.Json.Serialization; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +using StevanFreeborn.Extensions.Configuration.Secure.Configuration; +using StevanFreeborn.Extensions.Configuration.Secure.Cryptography; +using StevanFreeborn.Extensions.Configuration.Secure.Storage; +using StevanFreeborn.Extensions.Configuration.Secure.Tests.Common; + +namespace StevanFreeborn.Extensions.Configuration.Secure.Tests.Integration; + +public class SecureConfigExtensionsTests : IDisposable +{ + private readonly TempDirectory _tempDir = new(); + + public void Dispose() + { + _tempDir.Dispose(); + GC.SuppressFinalize(this); + } + + [Fact] + public void AddSecureConfig_ToConfigurationBuilder_ItShouldReturnConfigurationBuilder() + { + var builder = new ConfigurationBuilder(); + + var result = builder.AddSecureConfig(CreateDefaultConfig(_tempDir.Path)); + + result.Should().BeSameAs(builder); + } + + [Fact] + public void AddSecureConfig_ToConfigurationBuilder_ItShouldAddSecureConfigSource() + { + var builder = new ConfigurationBuilder(); + + builder.AddSecureConfig(CreateDefaultConfig(_tempDir.Path)); + + builder.Sources.Should().ContainSingle(s => s is SecureConfigSource); + } + + [Fact] + public void AddSecureConfig_ToServiceCollection_ItShouldReturnServiceCollection() + { + var services = new ServiceCollection(); + + var result = services.AddSecureConfig(CreateDefaultConfig(_tempDir.Path)); + + result.Should().BeSameAs(services); + } + + [Fact] + public void AddSecureConfig_ToServiceCollection_ItShouldRegisterISecureConfig() + { + var services = new ServiceCollection(); + + services.AddSecureConfig(CreateDefaultConfig(_tempDir.Path)); + + var descriptor = services.Should().ContainSingle(d => d.ServiceType == typeof(ISecureConfig)).Subject; + descriptor.Lifetime.Should().Be(ServiceLifetime.Singleton); + } + + [Fact] + public void AddSecureConfig_ToServiceCollection_ItShouldRegisterAllDependencies() + { + var services = new ServiceCollection(); + + services.AddSecureConfig(CreateDefaultConfig(_tempDir.Path)); + + services.Should().Contain(d => d.ServiceType == typeof(ISecureStorageProvider)); + services.Should().Contain(d => d.ServiceType == typeof(IEncryptionKeyProvider)); + services.Should().Contain(d => d.ServiceType == typeof(ICryptoProvider)); + services.Should().Contain(d => d.ServiceType == typeof(ISecureConfig)); + } + + [Fact] + public void AddSecureConfig_ToServiceCollection_ItShouldNotDuplicateExistingRegistrations() + { + var services = new ServiceCollection(); + + services.AddSecureConfig(CreateDefaultConfig(_tempDir.Path)); + services.AddSecureConfig(CreateDefaultConfig(_tempDir.Path)); + + services.Count(d => d.ServiceType == typeof(ISecureConfig)).Should().Be(1); + services.Count(d => d.ServiceType == typeof(ISecureStorageProvider)).Should().Be(1); + services.Count(d => d.ServiceType == typeof(IEncryptionKeyProvider)).Should().Be(1); + services.Count(d => d.ServiceType == typeof(ICryptoProvider)).Should().Be(1); + } + + [Fact] + public async Task AddSecureConfig_WithRealProviders_ItShouldResolveISecureConfig() + { + var services = new ServiceCollection(); + + services.AddSecureConfig(CreateDefaultConfig(_tempDir.Path)); + + var provider = services.BuildServiceProvider(); + var secureConfig = provider.GetRequiredService(); + + secureConfig.Should().NotBeNull(); + secureConfig.Should().BeOfType(); + } + + [Fact] + public async Task AddSecureConfig_WithRealProviders_ItShouldSetAndGetValue() + { + var services = new ServiceCollection(); + + services.AddSecureConfig(CreateDefaultConfig(_tempDir.Path)); + + var provider = services.BuildServiceProvider(); + var secureConfig = provider.GetRequiredService(); + + var testValue = new IntegrationTestConfig { Name = "TestName", Value = 42 }; + await secureConfig.SetAsync("test-key", testValue); + + var result = await secureConfig.GetAsync("test-key"); + + result.Should().NotBeNull(); + result!.Name.Should().Be("TestName"); + result.Value.Should().Be(42); + } + + [Fact] + public async Task AddSecureConfig_WithRealProviders_ItShouldDeleteValue() + { + var services = new ServiceCollection(); + + services.AddSecureConfig(CreateDefaultConfig(_tempDir.Path)); + + var provider = services.BuildServiceProvider(); + var secureConfig = provider.GetRequiredService(); + + await secureConfig.SetAsync("delete-key", new IntegrationTestConfig { Name = "ToDelete", Value = 1 }); + + var deleted = await secureConfig.DeleteAsync("delete-key"); + + deleted.Should().BeTrue(); + + var result = await secureConfig.GetAsync("delete-key"); + result.Should().BeNull(); + } + + [Fact] + public async Task AddSecureConfig_WithRealProviders_ItShouldReturnDefaultForMissingKey() + { + var services = new ServiceCollection(); + + services.AddSecureConfig(CreateDefaultConfig(_tempDir.Path)); + + var provider = services.BuildServiceProvider(); + var secureConfig = provider.GetRequiredService(); + + var result = await secureConfig.GetAsync("nonexistent-key"); + + result.Should().BeNull(); + } + + [Fact] + public async Task AddSecureConfig_WithRealProviders_ItShouldResolveAllServicesFromContainer() + { + var services = new ServiceCollection(); + + services.AddSecureConfig(CreateDefaultConfig(_tempDir.Path)); + + var provider = services.BuildServiceProvider(); + + var storageProvider = provider.GetRequiredService(); + var cryptoProvider = provider.GetRequiredService(); + var secureConfig = provider.GetRequiredService(); + + storageProvider.Should().NotBeNull(); + cryptoProvider.Should().NotBeNull(); + secureConfig.Should().NotBeNull(); + + storageProvider.Should().BeOfType(); + cryptoProvider.Should().BeOfType(); + } + + [Fact] + public async Task AddSecureConfig_WithMachineIdKey_ItShouldWorkEndToEnd() + { + var services = new ServiceCollection(); + + services.AddSecureConfig(builder => + { + builder + .WithMachineIdKey() + .WithAesCryptoProvider() + .UseJsonFileStorage(new JsonStorageOptions + { + DirectoryPath = _tempDir.Path, + FileName = "secure-config.json", + }) + .AddJsonAotContext(IntegrationTestJsonContext.Default); + }); + + var provider = services.BuildServiceProvider(); + var secureConfig = provider.GetRequiredService(); + + var testValue = new IntegrationTestConfig { Name = "MachineIdTest", Value = 99 }; + await secureConfig.SetAsync("machine-key", testValue); + + var result = await secureConfig.GetAsync("machine-key"); + + result.Should().NotBeNull(); + result!.Name.Should().Be("MachineIdTest"); + result.Value.Should().Be(99); + } + + [Fact] + public async Task AddSecureConfig_WithTypedOverload_ItShouldSerializeAndDeserializeCorrectly() + { + var services = new ServiceCollection(); + + services.AddSecureConfig(CreateDefaultConfig(_tempDir.Path)); + + var provider = services.BuildServiceProvider(); + var secureConfig = provider.GetRequiredService(); + + var testValue = new IntegrationTestConfig { Name = "TypedTest", Value = 777 }; + await secureConfig.SetAsync("typed-key", testValue, IntegrationTestJsonContext.Default.IntegrationTestConfig); + + var result = await secureConfig.GetAsync("typed-key", IntegrationTestJsonContext.Default.IntegrationTestConfig); + + result.Should().NotBeNull(); + result!.Name.Should().Be("TypedTest"); + result.Value.Should().Be(777); + } + + private static Action CreateDefaultConfig(string tempDir) => builder => + { + builder + .WithBase64EncryptionKey(KeyGenerator.GetValidBase64Key()) + .WithAesCryptoProvider() + .UseJsonFileStorage(new JsonStorageOptions { DirectoryPath = tempDir, FileName = "secure-config.json" }) + .AddJsonAotContext(IntegrationTestJsonContext.Default); + }; +} + +internal sealed class IntegrationTestConfig +{ + public string Name { get; init; } = string.Empty; + public int Value { get; init; } +} + +[JsonSerializable(typeof(IntegrationTestConfig))] +internal partial class IntegrationTestJsonContext : JsonSerializerContext +{ +} diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Integration/SecureConfigOptionsIntegrationTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Integration/SecureConfigOptionsIntegrationTests.cs new file mode 100644 index 0000000..a087e5c --- /dev/null +++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Integration/SecureConfigOptionsIntegrationTests.cs @@ -0,0 +1,261 @@ +using System.Security.Cryptography; +using System.Text.Json.Serialization; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using StevanFreeborn.Extensions.Configuration.Secure.Configuration; +using StevanFreeborn.Extensions.Configuration.Secure.Storage; +using StevanFreeborn.Extensions.Configuration.Secure.Tests.Common; + +namespace StevanFreeborn.Extensions.Configuration.Secure.Tests.Integration; + +public class SecureConfigOptionsIntegrationTests : IDisposable +{ + private readonly string _tempDir; + private readonly string _base64Key; + + public SecureConfigOptionsIntegrationTests() + { + _tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(_tempDir); + _base64Key = KeyGenerator.GetValidBase64Key(); + } + + [Fact] + public async Task ConfigureSecureConfig_WithIOptions_ItShouldResolveOptions() + { + var host = await CreateHostAsync(); + + using var scope = host.Services.CreateScope(); + var secureConfig = scope.ServiceProvider.GetRequiredService(); + + await secureConfig.SetAsync(nameof(TestApiOptions), new TestApiOptions { ApiKey = "initial-key" }); + + var config = (IConfigurationRoot)host.Services.GetRequiredService(); + config.Reload(); + + var options = scope.ServiceProvider.GetRequiredService>(); + + options.Value.Should().NotBeNull(); + options.Value.ApiKey.Should().Be("initial-key"); + } + + [Fact] + public async Task ConfigureSecureConfig_WithIOptionsSnapshot_ItShouldResolveNewInstancePerScope() + { + var host = await CreateHostAsync(); + + using var scope1 = host.Services.CreateScope(); + var snapshot1 = scope1.ServiceProvider.GetRequiredService>(); + + using var scope2 = host.Services.CreateScope(); + var snapshot2 = scope2.ServiceProvider.GetRequiredService>(); + + snapshot1.Should().NotBeSameAs(snapshot2); + snapshot1.Value.Should().BeEquivalentTo(snapshot2.Value); + } + + [Fact] + public async Task ConfigureSecureConfig_WithIOptionsMonitor_ItShouldResolveMonitor() + { + var host = await CreateHostAsync(); + + var monitor = host.Services.GetRequiredService>(); + + monitor.Should().NotBeNull(); + monitor.CurrentValue.Should().NotBeNull(); + } + + [Fact] + public async Task ConfigureSecureConfig_WhenValueUpdatedAndReloaded_ItShouldReflectInIOptionsMonitor() + { + var host = await CreateHostAsync(); + + var secureConfig = host.Services.GetRequiredService(); + var monitor = host.Services.GetRequiredService>(); + var config = (IConfigurationRoot)host.Services.GetRequiredService(); + + var originalValue = monitor.CurrentValue.ApiKey; + + await secureConfig.SetAsync(nameof(TestApiOptions), new TestApiOptions { ApiKey = "updated-key" }); + + config.Reload(); + + monitor.CurrentValue.ApiKey.Should().Be("updated-key"); + monitor.CurrentValue.ApiKey.Should().NotBe(originalValue); + } + + [Fact] + public async Task ConfigureSecureConfig_WhenValueUpdatedAndReloaded_ItShouldReflectInNewIOptionsSnapshot() + { + var host = await CreateHostAsync(); + + using var scope1 = host.Services.CreateScope(); + var snapshotBefore = scope1.ServiceProvider.GetRequiredService>(); + var originalValue = snapshotBefore.Value.ApiKey; + + var secureConfig = host.Services.GetRequiredService(); + var config = (IConfigurationRoot)host.Services.GetRequiredService(); + + await secureConfig.SetAsync(nameof(TestApiOptions), new TestApiOptions { ApiKey = "snapshot-updated-key" }); + + config.Reload(); + + using var scope2 = host.Services.CreateScope(); + var snapshotAfter = scope2.ServiceProvider.GetRequiredService>(); + + snapshotAfter.Value.ApiKey.Should().Be("snapshot-updated-key"); + snapshotAfter.Value.ApiKey.Should().NotBe(originalValue); + } + + [Fact] + public async Task ConfigureSecureConfig_WhenValueUpdatedAndReloaded_ItShouldNotUpdateExistingIOptions() + { + var host = await CreateHostAsync(); + + var secureConfig = host.Services.GetRequiredService(); + var options = host.Services.GetRequiredService>(); + var config = (IConfigurationRoot)host.Services.GetRequiredService(); + + var originalValue = options.Value.ApiKey; + + await secureConfig.SetAsync(nameof(TestApiOptions), new TestApiOptions { ApiKey = "should-not-update" }); + + config.Reload(); + + options.Value.ApiKey.Should().Be(originalValue); + } + + [Fact] + public async Task ConfigureSecureConfig_WithNestedOptions_ItShouldBindCorrectly() + { + var host = await CreateHostAsync(); + + var secureConfig = host.Services.GetRequiredService(); + var config = (IConfigurationRoot)host.Services.GetRequiredService(); + + await secureConfig.SetAsync(nameof(TestApiOptions), new TestApiOptions { ApiKey = "nested-key" }); + + config.Reload(); + + var options = host.Services.GetRequiredService>(); + + options.Value.ApiKey.Should().Be("nested-key"); + } + + [Fact] + public async Task ConfigureSecureConfig_WithFullHost_ItShouldStartAndResolveAllServices() + { + var host = await CreateHostAsync(); + + var secureConfig = host.Services.GetRequiredService(); + var options = host.Services.GetRequiredService>(); + var snapshotFactory = host.Services.GetRequiredService(); + var monitor = host.Services.GetRequiredService>(); + var configuration = host.Services.GetRequiredService(); + + secureConfig.Should().NotBeNull(); + options.Should().NotBeNull(); + snapshotFactory.Should().NotBeNull(); + monitor.Should().NotBeNull(); + configuration.Should().NotBeNull(); + + using var scope = snapshotFactory.CreateScope(); + var snapshot = scope.ServiceProvider.GetRequiredService>(); + snapshot.Should().NotBeNull(); + } + + [Fact] + public async Task ConfigureSecureConfig_WhenValueUpdated_ItShouldPropagateThroughEntirePipeline() + { + var host = await CreateHostAsync(); + + var secureConfig = host.Services.GetRequiredService(); + var options = host.Services.GetRequiredService>(); + var monitor = host.Services.GetRequiredService>(); + var config = (IConfigurationRoot)host.Services.GetRequiredService(); + + var originalOptionsValue = options.Value.ApiKey; + var originalMonitorValue = monitor.CurrentValue.ApiKey; + + await secureConfig.SetAsync(nameof(TestApiOptions), new TestApiOptions { ApiKey = "pipeline-updated-key" }); + + config.Reload(); + + monitor.CurrentValue.ApiKey.Should().Be("pipeline-updated-key"); + + using var scope = host.Services.CreateScope(); + var snapshot = scope.ServiceProvider.GetRequiredService>(); + snapshot.Value.ApiKey.Should().Be("pipeline-updated-key"); + + options.Value.ApiKey.Should().Be(originalOptionsValue); + } + + public void Dispose() + { + if (Directory.Exists(_tempDir)) + { + Directory.Delete(_tempDir, true); + } + + GC.SuppressFinalize(this); + } + + private Action CreateConfigBuilder() => builder => + { + builder + .WithBase64EncryptionKey(_base64Key) + .WithAesCryptoProvider() + .UseJsonFileStorage(new JsonStorageOptions + { + DirectoryPath = _tempDir, + FileName = "secure-config.json" + }) + .AddJsonAotContext(OptionsTestJsonContext.Default); + }; + + private async Task CreateHostAsync() + { + var configure = CreateConfigBuilder(); + + var hostBuilder = Host.CreateDefaultBuilder() + .ConfigureLogging(c => c.ClearProviders()) + .ConfigureAppConfiguration((_, builder) => + { + builder.AddSecureConfig(configure); + }) + .ConfigureServices((context, services) => + { + services.Configure(context.Configuration.GetSection(nameof(TestApiOptions))); + services.AddSecureConfig(configure); + }); + + var host = hostBuilder.Build(); + await host.StartAsync(); + + var secureConfig = host.Services.GetRequiredService(); + + if (string.IsNullOrEmpty((await secureConfig.GetAsync(nameof(TestApiOptions)))?.ApiKey)) + { + await secureConfig.SetAsync(nameof(TestApiOptions), new TestApiOptions { ApiKey = "default-key" }); + var config = (IConfigurationRoot)host.Services.GetRequiredService(); + config.Reload(); + } + + return host; + } +} + +internal sealed class TestApiOptions +{ + public string ApiKey { get; init; } = string.Empty; +} + +[JsonSerializable(typeof(TestApiOptions))] +internal partial class OptionsTestJsonContext : JsonSerializerContext +{ +} 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 9474eab..a330012 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 @@ -13,6 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/SecureConfigExtensionsTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/SecureConfigExtensionsTests.cs index 4bdea46..9dbd4cb 100644 --- a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/SecureConfigExtensionsTests.cs +++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/SecureConfigExtensionsTests.cs @@ -11,6 +11,7 @@ using Moq; using StevanFreeborn.Extensions.Configuration.Secure.Configuration; using StevanFreeborn.Extensions.Configuration.Secure.Cryptography; using StevanFreeborn.Extensions.Configuration.Secure.Storage; +using StevanFreeborn.Extensions.Configuration.Secure.Tests.Common; namespace StevanFreeborn.Extensions.Configuration.Secure.Tests.Unit; @@ -50,7 +51,7 @@ public class SecureConfigExtensionsTests var act = () => builder.AddSecureConfig(config => { - config.WithBase64EncryptionKey(GetValidBase64Key()); + config.WithBase64EncryptionKey(KeyGenerator.GetValidBase64Key()); config.WithAesCryptoProvider(); }); @@ -81,7 +82,7 @@ public class SecureConfigExtensionsTests var act = () => builder.AddSecureConfig(config => { config.UseCustomStorage(_mockStorageProvider.Object); - config.WithBase64EncryptionKey(GetValidBase64Key()); + config.WithBase64EncryptionKey(KeyGenerator.GetValidBase64Key()); }); act.Should().Throw() @@ -96,7 +97,7 @@ public class SecureConfigExtensionsTests var result = builder.AddSecureConfig(config => { config.UseCustomStorage(_mockStorageProvider.Object); - config.WithBase64EncryptionKey(GetValidBase64Key()); + config.WithBase64EncryptionKey(KeyGenerator.GetValidBase64Key()); config.WithAesCryptoProvider(); }); @@ -134,51 +135,34 @@ public class SecureConfigExtensionsTests [Fact] public void AddSecureConfig_WithJsonFileStorage_ItShouldWorkEndToEnd() { - var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - var filePath = Path.Combine(tempDir, "test_config.json"); + using var tempDir = new TempDirectory(); + var filePath = Path.Combine(tempDir.Path, "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 keyBytes = new byte[32]; - RandomNumberGenerator.Fill(keyBytes); - var keyProvider = new StaticKeyProvider(Convert.ToBase64String(keyBytes)); - var cryptoProvider = new AesCryptoProvider(keyProvider); + var originalData = /*lang=json,strict*/ @"{ ""AppName"": ""TestApp"", ""Version"": ""1.0.0"" }"; + var encryptedData = cryptoProvider.Encrypt(originalData); - var originalData = /*lang=json,strict*/ @"{ ""AppName"": ""TestApp"", ""Version"": ""1.0.0"" }"; - var encryptedData = cryptoProvider.Encrypt(originalData); + File.WriteAllText(filePath, $"{{\"Settings\":\"{encryptedData}\"}}"); - File.WriteAllText(filePath, $"{{\"Settings\":\"{encryptedData}\"}}"); - - var configuration = new ConfigurationBuilder() - .AddSecureConfig(config => + var configuration = new ConfigurationBuilder() + .AddSecureConfig(config => + { + config.UseJsonFileStorage(options => { - config.UseJsonFileStorage(options => - { - options.DirectoryPath = tempDir; - options.FileName = "test_config.json"; - }); - config.WithBase64EncryptionKey(Convert.ToBase64String(keyBytes)); - config.WithAesCryptoProvider(); - }) - .Build(); + options.DirectoryPath = tempDir.Path; + 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); - } - } + configuration["Settings:AppName"].Should().Be("TestApp"); + configuration["Settings:Version"].Should().Be("1.0.0"); } [Fact] @@ -199,7 +183,7 @@ public class SecureConfigExtensionsTests builder.AddSecureConfig(config => { config.UseCustomStorage(_mockStorageProvider.Object); - config.WithBase64EncryptionKey(GetValidBase64Key()); + config.WithBase64EncryptionKey(KeyGenerator.GetValidBase64Key()); config.WithAesCryptoProvider(); config.WithLoggerFactory(_mockLoggerFactory.Object); }) @@ -219,14 +203,14 @@ public class SecureConfigExtensionsTests builder.AddSecureConfig(config => { config.UseCustomStorage(_mockStorageProvider.Object); - config.WithBase64EncryptionKey(GetValidBase64Key()); + config.WithBase64EncryptionKey(KeyGenerator.GetValidBase64Key()); config.WithAesCryptoProvider(); }); builder.AddSecureConfig(config => { config.UseCustomStorage(_mockStorageProvider.Object); - config.WithBase64EncryptionKey(GetValidBase64Key()); + config.WithBase64EncryptionKey(KeyGenerator.GetValidBase64Key()); config.WithAesCryptoProvider(); }); @@ -263,7 +247,7 @@ public class SecureConfigExtensionsTests var act = () => services.AddSecureConfig(config => { - config.WithBase64EncryptionKey(GetValidBase64Key()); + config.WithBase64EncryptionKey(KeyGenerator.GetValidBase64Key()); config.WithAesCryptoProvider(); }); @@ -294,7 +278,7 @@ public class SecureConfigExtensionsTests var act = () => services.AddSecureConfig(config => { config.UseCustomStorage(_mockStorageProvider.Object); - config.WithBase64EncryptionKey(GetValidBase64Key()); + config.WithBase64EncryptionKey(KeyGenerator.GetValidBase64Key()); }); act.Should().Throw() @@ -498,102 +482,73 @@ public class SecureConfigExtensionsTests [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"); + using var tempDir = new TempDirectory(); + var filePath = Path.Combine(tempDir.Path, "test_config.json"); - try + var keyBytes = new byte[32]; + RandomNumberGenerator.Fill(keyBytes); + var base64Key = Convert.ToBase64String(keyBytes); + + var services = new ServiceCollection(); + + services.AddSecureConfig(config => { - 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 => { - config.AddJsonAotContext(SecureConfigExtensionsTestsJsonContext.Default); - config.UseJsonFileStorage(options => - { - options.DirectoryPath = tempDir; - options.FileName = "test_config.json"; - }); - config.WithBase64EncryptionKey(base64Key); - config.WithAesCryptoProvider(); + options.DirectoryPath = tempDir.Path; + options.FileName = "test_config.json"; }); + config.WithBase64EncryptionKey(base64Key); + config.WithAesCryptoProvider(); + }); - var serviceProvider = services.BuildServiceProvider(); - var secureConfig = serviceProvider.GetRequiredService(); - var storageProvider = serviceProvider.GetRequiredService(); + 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 testObject = new TestConfig { Name = "TestName", Value = 123 }; + await secureConfig.SetAsync("TestKey", testObject); - var retrievedObject = await secureConfig.GetAsync("TestKey"); + var retrievedObject = await secureConfig.GetAsync("TestKey"); - retrievedObject.Should().NotBeNull(); - retrievedObject.Name.Should().Be("TestName"); - retrievedObject.Value.Should().Be(123); + 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); - } - } + File.Exists(filePath).Should().BeTrue(); } [Fact] public async Task AddSecureConfig_ServiceCollection_WithMachineIdKey_ItShouldWorkEndToEnd() { - var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + using var tempDir = new TempDirectory(); - try + var services = new ServiceCollection(); + + services.AddSecureConfig(config => { - Directory.CreateDirectory(tempDir); - - var services = new ServiceCollection(); - - services.AddSecureConfig(config => + config.AddJsonAotContext(SecureConfigExtensionsTestsJsonContext.Default); + config.UseJsonFileStorage(options => { - config.AddJsonAotContext(SecureConfigExtensionsTestsJsonContext.Default); - config.UseJsonFileStorage(options => - { - options.DirectoryPath = tempDir; - options.FileName = "test_config.json"; - }); - config.WithMachineIdKey(); - config.WithAesCryptoProvider(); + options.DirectoryPath = tempDir.Path; + options.FileName = "test_config.json"; }); + config.WithMachineIdKey(); + config.WithAesCryptoProvider(); + }); - var serviceProvider = services.BuildServiceProvider(); - var secureConfig = serviceProvider.GetRequiredService(); + var serviceProvider = services.BuildServiceProvider(); + var secureConfig = serviceProvider.GetRequiredService(); - var testObject = new TestConfig { Name = "MachineIdTest", Value = 456 }; - await secureConfig.SetAsync("MachineTest", testObject); + var testObject = new TestConfig { Name = "MachineIdTest", Value = 456 }; + await secureConfig.SetAsync("MachineTest", testObject); - var retrievedObject = await secureConfig.GetAsync("MachineTest"); + 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); - } - } + retrievedObject.Should().NotBeNull(); + retrievedObject.Name.Should().Be("MachineIdTest"); + retrievedObject.Value.Should().Be(456); } [Fact] @@ -744,13 +699,6 @@ public class SecureConfigExtensionsTests 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 diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Storage/JsonFileStorageProviderTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Storage/JsonFileStorageProviderTests.cs index 922e827..9235e7e 100644 --- a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Storage/JsonFileStorageProviderTests.cs +++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Storage/JsonFileStorageProviderTests.cs @@ -1,22 +1,20 @@ using StevanFreeborn.Extensions.Configuration.Secure.Storage; +using StevanFreeborn.Extensions.Configuration.Secure.Tests.Common; namespace StevanFreeborn.Extensions.Configuration.Secure.Tests.Unit.Storage; public class JsonFileStorageProviderTests : IDisposable { - private readonly string _tmpDirectory; + private readonly TempDirectory _tempDir = new(); private readonly JsonStorageOptions _options; private readonly JsonFileStorageProvider _sut; public JsonFileStorageProviderTests() { - _tmpDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - Directory.CreateDirectory(_tmpDirectory); - _options = new() { FileName = "testsettings.json", - DirectoryPath = _tmpDirectory, + DirectoryPath = _tempDir.Path, }; _sut = new(_options); @@ -72,11 +70,7 @@ public class JsonFileStorageProviderTests : IDisposable public void Dispose() { - if (Directory.Exists(_tmpDirectory)) - { - Directory.Delete(_tmpDirectory, true); - } - + _tempDir.Dispose(); GC.SuppressFinalize(this); }