2026-04-02 13:52:58 -05:00
|
|
|
using System.Text.Json;
|
|
|
|
|
using System.Text.Json.Serialization;
|
|
|
|
|
|
2026-03-30 08:03:27 -05:00
|
|
|
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 SecureConfigTests
|
|
|
|
|
{
|
|
|
|
|
private readonly Mock<ICryptoProvider> _mockCryptoProvider = new();
|
|
|
|
|
private readonly Mock<ISecureStorageProvider> _mockStorageProvider = new();
|
2026-04-02 13:52:58 -05:00
|
|
|
private readonly JsonSerializerOptions _jsonSerializerOptions = new();
|
2026-03-30 08:03:27 -05:00
|
|
|
private readonly SecureConfig _sut;
|
|
|
|
|
|
|
|
|
|
public SecureConfigTests()
|
|
|
|
|
{
|
2026-04-02 13:52:58 -05:00
|
|
|
_sut = new(_mockStorageProvider.Object, _mockCryptoProvider.Object, _jsonSerializerOptions);
|
2026-03-30 08:03:27 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Constructor_WhenCalledWithNullStorageProvider_ItShouldThrowArgumentNullException()
|
|
|
|
|
{
|
2026-04-02 13:52:58 -05:00
|
|
|
var act = () => new SecureConfig(null!, _mockCryptoProvider.Object, _jsonSerializerOptions);
|
2026-03-30 08:03:27 -05:00
|
|
|
|
|
|
|
|
act.Should().Throw<ArgumentNullException>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Constructor_WhenCalledWithNullCryptoProvider_ItShouldThrowArgumentNullException()
|
|
|
|
|
{
|
2026-04-02 13:52:58 -05:00
|
|
|
var act = () => new SecureConfig(_mockStorageProvider.Object, null!, _jsonSerializerOptions);
|
|
|
|
|
|
|
|
|
|
act.Should().Throw<ArgumentNullException>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Constructor_WhenCalledWithNullJsonOptions_ItShouldThrowArgumentNullException()
|
|
|
|
|
{
|
|
|
|
|
var act = () => new SecureConfig(_mockStorageProvider.Object, _mockCryptoProvider.Object, null!);
|
2026-03-30 08:03:27 -05:00
|
|
|
|
|
|
|
|
act.Should().Throw<ArgumentNullException>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task SetAsync_WhenCalledWithNullKey_ItShouldThrowArgumentNullException()
|
|
|
|
|
{
|
2026-04-02 13:52:58 -05:00
|
|
|
var act = async () => await _sut.SetAsync(null!, string.Empty, SecureConfigTestsJsonContext.Default.String);
|
2026-03-30 08:03:27 -05:00
|
|
|
|
2026-04-02 13:52:58 -05:00
|
|
|
await act.Should().ThrowAsync<ArgumentException>();
|
2026-03-30 08:03:27 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task SetAsync_WhenCalledWithNullValue_ItShouldThrowArgumentNullException()
|
|
|
|
|
{
|
2026-04-02 13:52:58 -05:00
|
|
|
var act = async () => await _sut.SetAsync("Key", null!, SecureConfigTestsJsonContext.Default.String);
|
2026-03-30 08:03:27 -05:00
|
|
|
|
|
|
|
|
await act.Should().ThrowAsync<ArgumentNullException>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
2026-04-02 13:52:58 -05:00
|
|
|
public async Task SetAsync_WhenCalledWithoutJsonContextSet_ItShouldSerializeGivenValueAndEncryptIt()
|
2026-03-30 08:03:27 -05:00
|
|
|
{
|
|
|
|
|
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);
|
|
|
|
|
|
2026-04-02 13:52:58 -05:00
|
|
|
var act = async () => await _sut.SetAsync(key, config);
|
|
|
|
|
|
|
|
|
|
await act.Should().ThrowAsync<InvalidOperationException>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task SetAsync_WhenCalledWithJsonContextSet_ItShouldSerializeGivenValueAndEncryptIt()
|
|
|
|
|
{
|
|
|
|
|
var key = "Database";
|
|
|
|
|
var config = new DummyConfig("localhost", 9999);
|
|
|
|
|
var encryptedString = "encryptedString";
|
|
|
|
|
var json = JsonSerializer.Serialize(config);
|
|
|
|
|
|
|
|
|
|
_jsonSerializerOptions.TypeInfoResolverChain.Insert(0, SecureConfigTestsJsonContext.Default);
|
|
|
|
|
_mockCryptoProvider.Setup(m => m.Encrypt(json)).Returns(encryptedString);
|
|
|
|
|
|
2026-03-30 08:03:27 -05:00
|
|
|
await _sut.SetAsync(key, config);
|
|
|
|
|
|
2026-04-02 13:52:58 -05:00
|
|
|
_mockStorageProvider.Verify(m => m.WriteAsync(key, encryptedString, It.IsAny<CancellationToken>()), Times.Once());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task SetAsync_WhenCalledWithTypeInfo_ItShouldSerializeGivenValueAndEncryptIt()
|
|
|
|
|
{
|
|
|
|
|
var key = "Database";
|
|
|
|
|
var config = new DummyConfig("localhost", 9999);
|
|
|
|
|
var encryptedString = "encryptedString";
|
|
|
|
|
var json = JsonSerializer.Serialize(config);
|
|
|
|
|
|
|
|
|
|
_mockCryptoProvider.Setup(m => m.Encrypt(json)).Returns(encryptedString);
|
|
|
|
|
|
|
|
|
|
await _sut.SetAsync(key, config, SecureConfigTestsJsonContext.Default.DummyConfig);
|
|
|
|
|
|
|
|
|
|
_mockStorageProvider.Verify(m => m.WriteAsync(key, encryptedString, It.IsAny<CancellationToken>()), Times.Once());
|
2026-03-30 08:03:27 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetAsync_WhenCalledWithNullKey_ItShouldThrowArgumentNullException()
|
|
|
|
|
{
|
2026-04-02 13:52:58 -05:00
|
|
|
var act = async () => await _sut.GetAsync(null!, SecureConfigTestsJsonContext.Default.DummyConfig);
|
2026-03-30 08:03:27 -05:00
|
|
|
|
2026-04-02 13:52:58 -05:00
|
|
|
await act.Should().ThrowAsync<ArgumentException>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetAsync_WhenKeyExistsAndJsonContextNotSet_ItShouldReadDecryptAndDeserializeTheValue()
|
|
|
|
|
{
|
|
|
|
|
var key = "Database";
|
|
|
|
|
var expectedConfig = new DummyConfig("localhost", 9999);
|
|
|
|
|
var encryptedString = "encryptedString";
|
|
|
|
|
var json = JsonSerializer.Serialize(expectedConfig);
|
|
|
|
|
|
|
|
|
|
_mockStorageProvider.Setup(m => m.ReadAsync(key, It.IsAny<CancellationToken>())).ReturnsAsync(encryptedString);
|
|
|
|
|
_mockCryptoProvider.Setup(m => m.Decrypt(encryptedString)).Returns(json);
|
|
|
|
|
|
|
|
|
|
var act = async () => await _sut.GetAsync<DummyConfig>(key);
|
|
|
|
|
|
|
|
|
|
await act.Should().ThrowAsync<InvalidOperationException>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetAsync_WhenKeyExistsAndJsonContextIsSet_ItShouldReadDecryptAndDeserializeTheValue()
|
|
|
|
|
{
|
|
|
|
|
var key = "Database";
|
|
|
|
|
var expectedConfig = new DummyConfig("localhost", 9999);
|
|
|
|
|
var encryptedString = "encryptedString";
|
|
|
|
|
var json = JsonSerializer.Serialize(expectedConfig);
|
|
|
|
|
|
|
|
|
|
_jsonSerializerOptions.TypeInfoResolverChain.Insert(0, SecureConfigTestsJsonContext.Default);
|
|
|
|
|
_mockStorageProvider.Setup(m => m.ReadAsync(key, It.IsAny<CancellationToken>())).ReturnsAsync(encryptedString);
|
|
|
|
|
_mockCryptoProvider.Setup(m => m.Decrypt(encryptedString)).Returns(json);
|
|
|
|
|
|
|
|
|
|
var result = await _sut.GetAsync(key, SecureConfigTestsJsonContext.Default.DummyConfig);
|
|
|
|
|
|
|
|
|
|
result.Should().BeEquivalentTo(expectedConfig);
|
2026-03-30 08:03:27 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetAsync_WhenKeyExists_ItShouldReadDecryptAndDeserializeTheValue()
|
|
|
|
|
{
|
|
|
|
|
var key = "Database";
|
|
|
|
|
var expectedConfig = new DummyConfig("localhost", 9999);
|
|
|
|
|
var encryptedString = "encryptedString";
|
|
|
|
|
var json = JsonSerializer.Serialize(expectedConfig);
|
|
|
|
|
|
2026-04-02 13:52:58 -05:00
|
|
|
_mockStorageProvider.Setup(m => m.ReadAsync(key, It.IsAny<CancellationToken>())).ReturnsAsync(encryptedString);
|
2026-03-30 08:03:27 -05:00
|
|
|
_mockCryptoProvider.Setup(m => m.Decrypt(encryptedString)).Returns(json);
|
|
|
|
|
|
2026-04-02 13:52:58 -05:00
|
|
|
var result = await _sut.GetAsync(key, SecureConfigTestsJsonContext.Default.DummyConfig);
|
2026-03-30 08:03:27 -05:00
|
|
|
|
|
|
|
|
result.Should().BeEquivalentTo(expectedConfig);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetAsync_WhenKeyDoesNotExist_ItShouldReturnDefaultValue()
|
|
|
|
|
{
|
|
|
|
|
var key = "Database";
|
|
|
|
|
var expectedConfig = new DummyConfig("localhost", 9999);
|
|
|
|
|
var json = JsonSerializer.Serialize(expectedConfig);
|
|
|
|
|
|
2026-04-02 13:52:58 -05:00
|
|
|
_mockStorageProvider.Setup(m => m.ReadAsync(key, It.IsAny<CancellationToken>())).ReturnsAsync(string.Empty);
|
2026-03-30 08:03:27 -05:00
|
|
|
|
2026-04-02 13:52:58 -05:00
|
|
|
var result = await _sut.GetAsync(key, SecureConfigTestsJsonContext.Default.DummyConfig);
|
2026-03-30 08:03:27 -05:00
|
|
|
|
|
|
|
|
result.Should().BeNull();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task DeleteAsync_WhenCalled_ItShouldRemoveValue()
|
|
|
|
|
{
|
|
|
|
|
var key = "Database";
|
|
|
|
|
|
2026-04-02 13:52:58 -05:00
|
|
|
_mockStorageProvider.Setup(m => m.DeleteAsync(key, It.IsAny<CancellationToken>())).ReturnsAsync(true);
|
2026-03-30 08:03:27 -05:00
|
|
|
|
|
|
|
|
var result = await _sut.DeleteAsync(key);
|
|
|
|
|
|
|
|
|
|
result.Should().BeTrue();
|
2026-04-02 13:52:58 -05:00
|
|
|
_mockStorageProvider.Verify(m => m.DeleteAsync(key, It.IsAny<CancellationToken>()), Times.Once());
|
2026-03-30 08:03:27 -05:00
|
|
|
}
|
2026-04-02 13:52:58 -05:00
|
|
|
}
|
2026-03-30 08:03:27 -05:00
|
|
|
|
2026-04-02 13:52:58 -05:00
|
|
|
internal sealed record DummyConfig(string Host, int Port);
|
|
|
|
|
|
|
|
|
|
[JsonSerializable(typeof(string))]
|
|
|
|
|
[JsonSerializable(typeof(DummyConfig))]
|
|
|
|
|
internal partial class SecureConfigTestsJsonContext : JsonSerializerContext
|
|
|
|
|
{
|
2026-03-30 08:03:27 -05:00
|
|
|
}
|