tests: add integration tests
- refactored to use temp directory helper class - consolidated test key generation to helper class
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+252
@@ -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<ISecureConfig>();
|
||||||
|
|
||||||
|
secureConfig.Should().NotBeNull();
|
||||||
|
secureConfig.Should().BeOfType<SecureConfig>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddSecureConfig_WithRealProviders_ItShouldSetAndGetValue()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSecureConfig(CreateDefaultConfig(_tempDir.Path));
|
||||||
|
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
var secureConfig = provider.GetRequiredService<ISecureConfig>();
|
||||||
|
|
||||||
|
var testValue = new IntegrationTestConfig { Name = "TestName", Value = 42 };
|
||||||
|
await secureConfig.SetAsync("test-key", testValue);
|
||||||
|
|
||||||
|
var result = await secureConfig.GetAsync<IntegrationTestConfig>("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<ISecureConfig>();
|
||||||
|
|
||||||
|
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<IntegrationTestConfig>("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<ISecureConfig>();
|
||||||
|
|
||||||
|
var result = await secureConfig.GetAsync<IntegrationTestConfig>("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<ISecureStorageProvider>();
|
||||||
|
var cryptoProvider = provider.GetRequiredService<ICryptoProvider>();
|
||||||
|
var secureConfig = provider.GetRequiredService<ISecureConfig>();
|
||||||
|
|
||||||
|
storageProvider.Should().NotBeNull();
|
||||||
|
cryptoProvider.Should().NotBeNull();
|
||||||
|
secureConfig.Should().NotBeNull();
|
||||||
|
|
||||||
|
storageProvider.Should().BeOfType<JsonFileStorageProvider>();
|
||||||
|
cryptoProvider.Should().BeOfType<AesCryptoProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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<ISecureConfig>();
|
||||||
|
|
||||||
|
var testValue = new IntegrationTestConfig { Name = "MachineIdTest", Value = 99 };
|
||||||
|
await secureConfig.SetAsync("machine-key", testValue);
|
||||||
|
|
||||||
|
var result = await secureConfig.GetAsync<IntegrationTestConfig>("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<ISecureConfig>();
|
||||||
|
|
||||||
|
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<ISecureConfigBuilder> 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
|
||||||
|
{
|
||||||
|
}
|
||||||
+261
@@ -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<ISecureConfig>();
|
||||||
|
|
||||||
|
await secureConfig.SetAsync(nameof(TestApiOptions), new TestApiOptions { ApiKey = "initial-key" });
|
||||||
|
|
||||||
|
var config = (IConfigurationRoot)host.Services.GetRequiredService<IConfiguration>();
|
||||||
|
config.Reload();
|
||||||
|
|
||||||
|
var options = scope.ServiceProvider.GetRequiredService<IOptions<TestApiOptions>>();
|
||||||
|
|
||||||
|
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<IOptionsSnapshot<TestApiOptions>>();
|
||||||
|
|
||||||
|
using var scope2 = host.Services.CreateScope();
|
||||||
|
var snapshot2 = scope2.ServiceProvider.GetRequiredService<IOptionsSnapshot<TestApiOptions>>();
|
||||||
|
|
||||||
|
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<IOptionsMonitor<TestApiOptions>>();
|
||||||
|
|
||||||
|
monitor.Should().NotBeNull();
|
||||||
|
monitor.CurrentValue.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConfigureSecureConfig_WhenValueUpdatedAndReloaded_ItShouldReflectInIOptionsMonitor()
|
||||||
|
{
|
||||||
|
var host = await CreateHostAsync();
|
||||||
|
|
||||||
|
var secureConfig = host.Services.GetRequiredService<ISecureConfig>();
|
||||||
|
var monitor = host.Services.GetRequiredService<IOptionsMonitor<TestApiOptions>>();
|
||||||
|
var config = (IConfigurationRoot)host.Services.GetRequiredService<IConfiguration>();
|
||||||
|
|
||||||
|
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<IOptionsSnapshot<TestApiOptions>>();
|
||||||
|
var originalValue = snapshotBefore.Value.ApiKey;
|
||||||
|
|
||||||
|
var secureConfig = host.Services.GetRequiredService<ISecureConfig>();
|
||||||
|
var config = (IConfigurationRoot)host.Services.GetRequiredService<IConfiguration>();
|
||||||
|
|
||||||
|
await secureConfig.SetAsync(nameof(TestApiOptions), new TestApiOptions { ApiKey = "snapshot-updated-key" });
|
||||||
|
|
||||||
|
config.Reload();
|
||||||
|
|
||||||
|
using var scope2 = host.Services.CreateScope();
|
||||||
|
var snapshotAfter = scope2.ServiceProvider.GetRequiredService<IOptionsSnapshot<TestApiOptions>>();
|
||||||
|
|
||||||
|
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<ISecureConfig>();
|
||||||
|
var options = host.Services.GetRequiredService<IOptions<TestApiOptions>>();
|
||||||
|
var config = (IConfigurationRoot)host.Services.GetRequiredService<IConfiguration>();
|
||||||
|
|
||||||
|
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<ISecureConfig>();
|
||||||
|
var config = (IConfigurationRoot)host.Services.GetRequiredService<IConfiguration>();
|
||||||
|
|
||||||
|
await secureConfig.SetAsync(nameof(TestApiOptions), new TestApiOptions { ApiKey = "nested-key" });
|
||||||
|
|
||||||
|
config.Reload();
|
||||||
|
|
||||||
|
var options = host.Services.GetRequiredService<IOptions<TestApiOptions>>();
|
||||||
|
|
||||||
|
options.Value.ApiKey.Should().Be("nested-key");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConfigureSecureConfig_WithFullHost_ItShouldStartAndResolveAllServices()
|
||||||
|
{
|
||||||
|
var host = await CreateHostAsync();
|
||||||
|
|
||||||
|
var secureConfig = host.Services.GetRequiredService<ISecureConfig>();
|
||||||
|
var options = host.Services.GetRequiredService<IOptions<TestApiOptions>>();
|
||||||
|
var snapshotFactory = host.Services.GetRequiredService<IServiceScopeFactory>();
|
||||||
|
var monitor = host.Services.GetRequiredService<IOptionsMonitor<TestApiOptions>>();
|
||||||
|
var configuration = host.Services.GetRequiredService<IConfiguration>();
|
||||||
|
|
||||||
|
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<IOptionsSnapshot<TestApiOptions>>();
|
||||||
|
snapshot.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConfigureSecureConfig_WhenValueUpdated_ItShouldPropagateThroughEntirePipeline()
|
||||||
|
{
|
||||||
|
var host = await CreateHostAsync();
|
||||||
|
|
||||||
|
var secureConfig = host.Services.GetRequiredService<ISecureConfig>();
|
||||||
|
var options = host.Services.GetRequiredService<IOptions<TestApiOptions>>();
|
||||||
|
var monitor = host.Services.GetRequiredService<IOptionsMonitor<TestApiOptions>>();
|
||||||
|
var config = (IConfigurationRoot)host.Services.GetRequiredService<IConfiguration>();
|
||||||
|
|
||||||
|
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<IOptionsSnapshot<TestApiOptions>>();
|
||||||
|
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<ISecureConfigBuilder> CreateConfigBuilder() => builder =>
|
||||||
|
{
|
||||||
|
builder
|
||||||
|
.WithBase64EncryptionKey(_base64Key)
|
||||||
|
.WithAesCryptoProvider()
|
||||||
|
.UseJsonFileStorage(new JsonStorageOptions
|
||||||
|
{
|
||||||
|
DirectoryPath = _tempDir,
|
||||||
|
FileName = "secure-config.json"
|
||||||
|
})
|
||||||
|
.AddJsonAotContext(OptionsTestJsonContext.Default);
|
||||||
|
};
|
||||||
|
|
||||||
|
private async Task<IHost> CreateHostAsync()
|
||||||
|
{
|
||||||
|
var configure = CreateConfigBuilder();
|
||||||
|
|
||||||
|
var hostBuilder = Host.CreateDefaultBuilder()
|
||||||
|
.ConfigureLogging(c => c.ClearProviders())
|
||||||
|
.ConfigureAppConfiguration((_, builder) =>
|
||||||
|
{
|
||||||
|
builder.AddSecureConfig(configure);
|
||||||
|
})
|
||||||
|
.ConfigureServices((context, services) =>
|
||||||
|
{
|
||||||
|
services.Configure<TestApiOptions>(context.Configuration.GetSection(nameof(TestApiOptions)));
|
||||||
|
services.AddSecureConfig(configure);
|
||||||
|
});
|
||||||
|
|
||||||
|
var host = hostBuilder.Build();
|
||||||
|
await host.StartAsync();
|
||||||
|
|
||||||
|
var secureConfig = host.Services.GetRequiredService<ISecureConfig>();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty((await secureConfig.GetAsync<TestApiOptions>(nameof(TestApiOptions)))?.ApiKey))
|
||||||
|
{
|
||||||
|
await secureConfig.SetAsync(nameof(TestApiOptions), new TestApiOptions { ApiKey = "default-key" });
|
||||||
|
var config = (IConfigurationRoot)host.Services.GetRequiredService<IConfiguration>();
|
||||||
|
config.Reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class TestApiOptions
|
||||||
|
{
|
||||||
|
public string ApiKey { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonSerializable(typeof(TestApiOptions))]
|
||||||
|
internal partial class OptionsTestJsonContext : JsonSerializerContext
|
||||||
|
{
|
||||||
|
}
|
||||||
+1
@@ -13,6 +13,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
|
||||||
<PackageReference Include="Moq" Version="4.20.72" />
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
|||||||
+17
-69
@@ -11,6 +11,7 @@ using Moq;
|
|||||||
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||||
using StevanFreeborn.Extensions.Configuration.Secure.Cryptography;
|
using StevanFreeborn.Extensions.Configuration.Secure.Cryptography;
|
||||||
using StevanFreeborn.Extensions.Configuration.Secure.Storage;
|
using StevanFreeborn.Extensions.Configuration.Secure.Storage;
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Tests.Common;
|
||||||
|
|
||||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Tests.Unit;
|
namespace StevanFreeborn.Extensions.Configuration.Secure.Tests.Unit;
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ public class SecureConfigExtensionsTests
|
|||||||
|
|
||||||
var act = () => builder.AddSecureConfig(config =>
|
var act = () => builder.AddSecureConfig(config =>
|
||||||
{
|
{
|
||||||
config.WithBase64EncryptionKey(GetValidBase64Key());
|
config.WithBase64EncryptionKey(KeyGenerator.GetValidBase64Key());
|
||||||
config.WithAesCryptoProvider();
|
config.WithAesCryptoProvider();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -81,7 +82,7 @@ public class SecureConfigExtensionsTests
|
|||||||
var act = () => builder.AddSecureConfig(config =>
|
var act = () => builder.AddSecureConfig(config =>
|
||||||
{
|
{
|
||||||
config.UseCustomStorage(_mockStorageProvider.Object);
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
config.WithBase64EncryptionKey(GetValidBase64Key());
|
config.WithBase64EncryptionKey(KeyGenerator.GetValidBase64Key());
|
||||||
});
|
});
|
||||||
|
|
||||||
act.Should().Throw<InvalidOperationException>()
|
act.Should().Throw<InvalidOperationException>()
|
||||||
@@ -96,7 +97,7 @@ public class SecureConfigExtensionsTests
|
|||||||
var result = builder.AddSecureConfig(config =>
|
var result = builder.AddSecureConfig(config =>
|
||||||
{
|
{
|
||||||
config.UseCustomStorage(_mockStorageProvider.Object);
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
config.WithBase64EncryptionKey(GetValidBase64Key());
|
config.WithBase64EncryptionKey(KeyGenerator.GetValidBase64Key());
|
||||||
config.WithAesCryptoProvider();
|
config.WithAesCryptoProvider();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -134,12 +135,8 @@ public class SecureConfigExtensionsTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void AddSecureConfig_WithJsonFileStorage_ItShouldWorkEndToEnd()
|
public void AddSecureConfig_WithJsonFileStorage_ItShouldWorkEndToEnd()
|
||||||
{
|
{
|
||||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
using var tempDir = new TempDirectory();
|
||||||
var filePath = Path.Combine(tempDir, "test_config.json");
|
var filePath = Path.Combine(tempDir.Path, "test_config.json");
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(tempDir);
|
|
||||||
|
|
||||||
var keyBytes = new byte[32];
|
var keyBytes = new byte[32];
|
||||||
RandomNumberGenerator.Fill(keyBytes);
|
RandomNumberGenerator.Fill(keyBytes);
|
||||||
@@ -156,7 +153,7 @@ public class SecureConfigExtensionsTests
|
|||||||
{
|
{
|
||||||
config.UseJsonFileStorage(options =>
|
config.UseJsonFileStorage(options =>
|
||||||
{
|
{
|
||||||
options.DirectoryPath = tempDir;
|
options.DirectoryPath = tempDir.Path;
|
||||||
options.FileName = "test_config.json";
|
options.FileName = "test_config.json";
|
||||||
});
|
});
|
||||||
config.WithBase64EncryptionKey(Convert.ToBase64String(keyBytes));
|
config.WithBase64EncryptionKey(Convert.ToBase64String(keyBytes));
|
||||||
@@ -167,19 +164,6 @@ public class SecureConfigExtensionsTests
|
|||||||
configuration["Settings:AppName"].Should().Be("TestApp");
|
configuration["Settings:AppName"].Should().Be("TestApp");
|
||||||
configuration["Settings:Version"].Should().Be("1.0.0");
|
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]
|
[Fact]
|
||||||
public void AddSecureConfig_WithLoggerFactory_ItShouldUseProvidedLoggerFactory()
|
public void AddSecureConfig_WithLoggerFactory_ItShouldUseProvidedLoggerFactory()
|
||||||
@@ -199,7 +183,7 @@ public class SecureConfigExtensionsTests
|
|||||||
builder.AddSecureConfig(config =>
|
builder.AddSecureConfig(config =>
|
||||||
{
|
{
|
||||||
config.UseCustomStorage(_mockStorageProvider.Object);
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
config.WithBase64EncryptionKey(GetValidBase64Key());
|
config.WithBase64EncryptionKey(KeyGenerator.GetValidBase64Key());
|
||||||
config.WithAesCryptoProvider();
|
config.WithAesCryptoProvider();
|
||||||
config.WithLoggerFactory(_mockLoggerFactory.Object);
|
config.WithLoggerFactory(_mockLoggerFactory.Object);
|
||||||
})
|
})
|
||||||
@@ -219,14 +203,14 @@ public class SecureConfigExtensionsTests
|
|||||||
builder.AddSecureConfig(config =>
|
builder.AddSecureConfig(config =>
|
||||||
{
|
{
|
||||||
config.UseCustomStorage(_mockStorageProvider.Object);
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
config.WithBase64EncryptionKey(GetValidBase64Key());
|
config.WithBase64EncryptionKey(KeyGenerator.GetValidBase64Key());
|
||||||
config.WithAesCryptoProvider();
|
config.WithAesCryptoProvider();
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.AddSecureConfig(config =>
|
builder.AddSecureConfig(config =>
|
||||||
{
|
{
|
||||||
config.UseCustomStorage(_mockStorageProvider.Object);
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
config.WithBase64EncryptionKey(GetValidBase64Key());
|
config.WithBase64EncryptionKey(KeyGenerator.GetValidBase64Key());
|
||||||
config.WithAesCryptoProvider();
|
config.WithAesCryptoProvider();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -263,7 +247,7 @@ public class SecureConfigExtensionsTests
|
|||||||
|
|
||||||
var act = () => services.AddSecureConfig(config =>
|
var act = () => services.AddSecureConfig(config =>
|
||||||
{
|
{
|
||||||
config.WithBase64EncryptionKey(GetValidBase64Key());
|
config.WithBase64EncryptionKey(KeyGenerator.GetValidBase64Key());
|
||||||
config.WithAesCryptoProvider();
|
config.WithAesCryptoProvider();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -294,7 +278,7 @@ public class SecureConfigExtensionsTests
|
|||||||
var act = () => services.AddSecureConfig(config =>
|
var act = () => services.AddSecureConfig(config =>
|
||||||
{
|
{
|
||||||
config.UseCustomStorage(_mockStorageProvider.Object);
|
config.UseCustomStorage(_mockStorageProvider.Object);
|
||||||
config.WithBase64EncryptionKey(GetValidBase64Key());
|
config.WithBase64EncryptionKey(KeyGenerator.GetValidBase64Key());
|
||||||
});
|
});
|
||||||
|
|
||||||
act.Should().Throw<InvalidOperationException>()
|
act.Should().Throw<InvalidOperationException>()
|
||||||
@@ -498,12 +482,8 @@ public class SecureConfigExtensionsTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task AddSecureConfig_ServiceCollection_WithRealAesCryptoProvider_ItShouldWorkEndToEnd()
|
public async Task AddSecureConfig_ServiceCollection_WithRealAesCryptoProvider_ItShouldWorkEndToEnd()
|
||||||
{
|
{
|
||||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
using var tempDir = new TempDirectory();
|
||||||
var filePath = Path.Combine(tempDir, "test_config.json");
|
var filePath = Path.Combine(tempDir.Path, "test_config.json");
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(tempDir);
|
|
||||||
|
|
||||||
var keyBytes = new byte[32];
|
var keyBytes = new byte[32];
|
||||||
RandomNumberGenerator.Fill(keyBytes);
|
RandomNumberGenerator.Fill(keyBytes);
|
||||||
@@ -516,7 +496,7 @@ public class SecureConfigExtensionsTests
|
|||||||
config.AddJsonAotContext(SecureConfigExtensionsTestsJsonContext.Default);
|
config.AddJsonAotContext(SecureConfigExtensionsTestsJsonContext.Default);
|
||||||
config.UseJsonFileStorage(options =>
|
config.UseJsonFileStorage(options =>
|
||||||
{
|
{
|
||||||
options.DirectoryPath = tempDir;
|
options.DirectoryPath = tempDir.Path;
|
||||||
options.FileName = "test_config.json";
|
options.FileName = "test_config.json";
|
||||||
});
|
});
|
||||||
config.WithBase64EncryptionKey(base64Key);
|
config.WithBase64EncryptionKey(base64Key);
|
||||||
@@ -538,28 +518,11 @@ public class SecureConfigExtensionsTests
|
|||||||
|
|
||||||
File.Exists(filePath).Should().BeTrue();
|
File.Exists(filePath).Should().BeTrue();
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (File.Exists(filePath))
|
|
||||||
{
|
|
||||||
File.Delete(filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Directory.Exists(tempDir))
|
|
||||||
{
|
|
||||||
Directory.Delete(tempDir, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task AddSecureConfig_ServiceCollection_WithMachineIdKey_ItShouldWorkEndToEnd()
|
public async Task AddSecureConfig_ServiceCollection_WithMachineIdKey_ItShouldWorkEndToEnd()
|
||||||
{
|
{
|
||||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
using var tempDir = new TempDirectory();
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(tempDir);
|
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
@@ -568,7 +531,7 @@ public class SecureConfigExtensionsTests
|
|||||||
config.AddJsonAotContext(SecureConfigExtensionsTestsJsonContext.Default);
|
config.AddJsonAotContext(SecureConfigExtensionsTestsJsonContext.Default);
|
||||||
config.UseJsonFileStorage(options =>
|
config.UseJsonFileStorage(options =>
|
||||||
{
|
{
|
||||||
options.DirectoryPath = tempDir;
|
options.DirectoryPath = tempDir.Path;
|
||||||
options.FileName = "test_config.json";
|
options.FileName = "test_config.json";
|
||||||
});
|
});
|
||||||
config.WithMachineIdKey();
|
config.WithMachineIdKey();
|
||||||
@@ -587,14 +550,6 @@ public class SecureConfigExtensionsTests
|
|||||||
retrievedObject.Name.Should().Be("MachineIdTest");
|
retrievedObject.Name.Should().Be("MachineIdTest");
|
||||||
retrievedObject.Value.Should().Be(456);
|
retrievedObject.Value.Should().Be(456);
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (Directory.Exists(tempDir))
|
|
||||||
{
|
|
||||||
Directory.Delete(tempDir, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AddSecureConfig_ServiceCollection_ItShouldRegisterJsonSerializerOptions()
|
public void AddSecureConfig_ServiceCollection_ItShouldRegisterJsonSerializerOptions()
|
||||||
@@ -744,13 +699,6 @@ public class SecureConfigExtensionsTests
|
|||||||
configuration["Section1:Setting1"].Should().Be("Value1");
|
configuration["Section1:Setting1"].Should().Be("Value1");
|
||||||
configuration["Section2:Setting2"].Should().Be("Value2");
|
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
|
internal sealed class TestConfig
|
||||||
|
|||||||
+4
-10
@@ -1,22 +1,20 @@
|
|||||||
using StevanFreeborn.Extensions.Configuration.Secure.Storage;
|
using StevanFreeborn.Extensions.Configuration.Secure.Storage;
|
||||||
|
using StevanFreeborn.Extensions.Configuration.Secure.Tests.Common;
|
||||||
|
|
||||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Tests.Unit.Storage;
|
namespace StevanFreeborn.Extensions.Configuration.Secure.Tests.Unit.Storage;
|
||||||
|
|
||||||
public class JsonFileStorageProviderTests : IDisposable
|
public class JsonFileStorageProviderTests : IDisposable
|
||||||
{
|
{
|
||||||
private readonly string _tmpDirectory;
|
private readonly TempDirectory _tempDir = new();
|
||||||
private readonly JsonStorageOptions _options;
|
private readonly JsonStorageOptions _options;
|
||||||
private readonly JsonFileStorageProvider _sut;
|
private readonly JsonFileStorageProvider _sut;
|
||||||
|
|
||||||
public JsonFileStorageProviderTests()
|
public JsonFileStorageProviderTests()
|
||||||
{
|
{
|
||||||
_tmpDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
|
||||||
Directory.CreateDirectory(_tmpDirectory);
|
|
||||||
|
|
||||||
_options = new()
|
_options = new()
|
||||||
{
|
{
|
||||||
FileName = "testsettings.json",
|
FileName = "testsettings.json",
|
||||||
DirectoryPath = _tmpDirectory,
|
DirectoryPath = _tempDir.Path,
|
||||||
};
|
};
|
||||||
|
|
||||||
_sut = new(_options);
|
_sut = new(_options);
|
||||||
@@ -72,11 +70,7 @@ public class JsonFileStorageProviderTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (Directory.Exists(_tmpDirectory))
|
_tempDir.Dispose();
|
||||||
{
|
|
||||||
Directory.Delete(_tmpDirectory, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user