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 { }