bfc1c8064a
Eliminates placeholder substitution (${KEY}) in favor of storing complete
connection strings as single encrypted values. SecureStore now auto-creates
entries for all connection strings defined in appsettings. ConfigManager
editor reads/writes values directly to SecureStore.
433 lines
13 KiB
C#
433 lines
13 KiB
C#
using System.IO;
|
|
using JdeScoping.ConfigManager.Services.SecureStore;
|
|
|
|
namespace JdeScoping.ConfigManager.Tests.Services.SecureStore;
|
|
|
|
public class SecureStoreManagerTests : IDisposable
|
|
{
|
|
private readonly string _testDirectory;
|
|
private readonly SecureStoreManager _sut;
|
|
|
|
public SecureStoreManagerTests()
|
|
{
|
|
_testDirectory = Path.Combine(Path.GetTempPath(), $"SecureStoreTests_{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(_testDirectory);
|
|
_sut = new SecureStoreManager();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_sut.Dispose();
|
|
if (Directory.Exists(_testDirectory))
|
|
{
|
|
Directory.Delete(_testDirectory, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void IsStoreOpen_WhenNoStoreOpen_ReturnsFalse()
|
|
{
|
|
_sut.IsStoreOpen.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void CurrentStorePath_WhenNoStoreOpen_ReturnsNull()
|
|
{
|
|
_sut.CurrentStorePath.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void HasUnsavedChanges_WhenNoStoreOpen_ReturnsFalse()
|
|
{
|
|
_sut.HasUnsavedChanges.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateStore_WithKeyFile_CreatesStoreAndKeyFile()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "test.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
|
|
// Act
|
|
_sut.CreateStore(storePath, keyPath);
|
|
|
|
// Assert
|
|
_sut.IsStoreOpen.ShouldBeTrue();
|
|
_sut.CurrentStorePath.ShouldBe(storePath);
|
|
File.Exists(storePath).ShouldBeTrue();
|
|
File.Exists(keyPath).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void OpenStore_WithValidKeyFile_OpensStore()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "test.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
_sut.CreateStore(storePath, keyPath);
|
|
_sut.CloseStore();
|
|
|
|
// Act
|
|
_sut.OpenStore(storePath, keyPath);
|
|
|
|
// Assert
|
|
_sut.IsStoreOpen.ShouldBeTrue();
|
|
_sut.CurrentStorePath.ShouldBe(storePath);
|
|
}
|
|
|
|
[Fact]
|
|
public void OpenStore_WithNonExistentStore_ThrowsFileNotFoundException()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "nonexistent.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
|
|
// Act & Assert
|
|
Should.Throw<FileNotFoundException>(() => _sut.OpenStore(storePath, keyPath));
|
|
}
|
|
|
|
[Fact]
|
|
public void CloseStore_ClosesOpenStore()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "test.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
_sut.CreateStore(storePath, keyPath);
|
|
|
|
// Act
|
|
_sut.CloseStore();
|
|
|
|
// Assert
|
|
_sut.IsStoreOpen.ShouldBeFalse();
|
|
_sut.CurrentStorePath.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void SetSecret_AddsSecretAndMarksUnsaved()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "test.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
_sut.CreateStore(storePath, keyPath);
|
|
_sut.Save(); // Save to clear unsaved flag
|
|
|
|
// Act
|
|
_sut.SetSecret("testKey", "testValue");
|
|
|
|
// Assert
|
|
_sut.HasUnsavedChanges.ShouldBeTrue();
|
|
_sut.GetKeys().ShouldContain("testKey");
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSecret_ReturnsCorrectValue()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "test.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
_sut.CreateStore(storePath, keyPath);
|
|
_sut.SetSecret("testKey", "testValue");
|
|
|
|
// Act
|
|
var value = _sut.GetSecret("testKey");
|
|
|
|
// Assert
|
|
value.ShouldBe("testValue");
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSecret_WhenKeyNotFound_ThrowsKeyNotFoundException()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "test.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
_sut.CreateStore(storePath, keyPath);
|
|
|
|
// Act & Assert
|
|
Should.Throw<KeyNotFoundException>(() => _sut.GetSecret("nonexistent"));
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoveSecret_RemovesSecretAndMarksUnsaved()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "test.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
_sut.CreateStore(storePath, keyPath);
|
|
_sut.SetSecret("testKey", "testValue");
|
|
_sut.Save();
|
|
|
|
// Act
|
|
_sut.RemoveSecret("testKey");
|
|
|
|
// Assert
|
|
_sut.HasUnsavedChanges.ShouldBeTrue();
|
|
_sut.GetKeys().ShouldNotContain("testKey");
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoveSecret_WhenKeyNotFound_ThrowsKeyNotFoundException()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "test.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
_sut.CreateStore(storePath, keyPath);
|
|
|
|
// Act & Assert
|
|
Should.Throw<KeyNotFoundException>(() => _sut.RemoveSecret("nonexistent"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Save_PersistsSecretsToStore()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "test.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
_sut.CreateStore(storePath, keyPath);
|
|
_sut.SetSecret("testKey", "testValue");
|
|
|
|
// Act
|
|
_sut.Save();
|
|
_sut.CloseStore();
|
|
_sut.OpenStore(storePath, keyPath);
|
|
|
|
// Assert
|
|
_sut.GetKeys().ShouldContain("testKey");
|
|
_sut.GetSecret("testKey").ShouldBe("testValue");
|
|
}
|
|
|
|
[Fact]
|
|
public void Save_ClearsUnsavedChangesFlag()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "test.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
_sut.CreateStore(storePath, keyPath);
|
|
_sut.SetSecret("testKey", "testValue");
|
|
_sut.HasUnsavedChanges.ShouldBeTrue();
|
|
|
|
// Act
|
|
_sut.Save();
|
|
|
|
// Assert
|
|
_sut.HasUnsavedChanges.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void GetKeys_ReturnsAllSecretKeys()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "test.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
_sut.CreateStore(storePath, keyPath);
|
|
_sut.SetSecret("key1", "value1");
|
|
_sut.SetSecret("key2", "value2");
|
|
_sut.SetSecret("key3", "value3");
|
|
|
|
// Act
|
|
var keys = _sut.GetKeys();
|
|
|
|
// Assert
|
|
keys.Count.ShouldBe(3);
|
|
keys.ShouldContain("key1");
|
|
keys.ShouldContain("key2");
|
|
keys.ShouldContain("key3");
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateKeyFile_CreatesNewKeyFile()
|
|
{
|
|
// Arrange
|
|
var keyPath = Path.Combine(_testDirectory, "generated.key");
|
|
|
|
// Act
|
|
_sut.GenerateKeyFile(keyPath);
|
|
|
|
// Assert
|
|
File.Exists(keyPath).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void ExportKey_WhenNoStoreOpen_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var keyPath = Path.Combine(_testDirectory, "export.key");
|
|
|
|
// Act & Assert
|
|
Should.Throw<InvalidOperationException>(() => _sut.ExportKey(keyPath));
|
|
}
|
|
|
|
[Fact]
|
|
public void ExportKey_WhenStoreOpen_ExportsKey()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "test.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
var exportPath = Path.Combine(_testDirectory, "export.key");
|
|
_sut.CreateStore(storePath, keyPath);
|
|
|
|
// Act
|
|
_sut.ExportKey(exportPath);
|
|
|
|
// Assert
|
|
File.Exists(exportPath).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void GetKeys_WhenNoStoreOpen_ThrowsInvalidOperationException()
|
|
{
|
|
// Act & Assert
|
|
Should.Throw<InvalidOperationException>(() => _sut.GetKeys());
|
|
}
|
|
|
|
[Fact]
|
|
public void SetSecret_WhenNoStoreOpen_ThrowsInvalidOperationException()
|
|
{
|
|
// Act & Assert
|
|
Should.Throw<InvalidOperationException>(() => _sut.SetSecret("key", "value"));
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSecret_WhenNoStoreOpen_ThrowsInvalidOperationException()
|
|
{
|
|
// Act & Assert
|
|
Should.Throw<InvalidOperationException>(() => _sut.GetSecret("key"));
|
|
}
|
|
|
|
[Fact]
|
|
public void EnsureAllRequiredEntries_AddsMissingKeys()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "test.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
_sut.CreateStore(storePath, keyPath);
|
|
_sut.SetSecret("existingKey", "existingValue");
|
|
_sut.Save();
|
|
|
|
// Act
|
|
var addedKeys = _sut.EnsureAllRequiredEntries(
|
|
new[] { "existingKey", "newKey1" },
|
|
new[] { "LotFinder", "JDE" });
|
|
|
|
// Assert
|
|
addedKeys.Count.ShouldBe(3);
|
|
addedKeys.ShouldContain("newKey1");
|
|
addedKeys.ShouldContain("LotFinder");
|
|
addedKeys.ShouldContain("JDE");
|
|
addedKeys.ShouldNotContain("existingKey");
|
|
|
|
// Verify all keys exist
|
|
_sut.GetKeys().ShouldContain("existingKey");
|
|
_sut.GetKeys().ShouldContain("newKey1");
|
|
_sut.GetKeys().ShouldContain("LotFinder");
|
|
_sut.GetKeys().ShouldContain("JDE");
|
|
}
|
|
|
|
[Fact]
|
|
public void EnsureAllRequiredEntries_CreatesEmptyValues()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "test.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
_sut.CreateStore(storePath, keyPath);
|
|
|
|
// Act
|
|
_sut.EnsureAllRequiredEntries(
|
|
new[] { "newKey" },
|
|
Array.Empty<string>());
|
|
|
|
// Assert
|
|
_sut.GetSecret("newKey").ShouldBe(string.Empty);
|
|
}
|
|
|
|
[Fact]
|
|
public void EnsureAllRequiredEntries_DoesNotOverwriteExistingValues()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "test.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
_sut.CreateStore(storePath, keyPath);
|
|
_sut.SetSecret("existingKey", "originalValue");
|
|
_sut.Save();
|
|
|
|
// Act
|
|
var addedKeys = _sut.EnsureAllRequiredEntries(
|
|
new[] { "existingKey" },
|
|
Array.Empty<string>());
|
|
|
|
// Assert
|
|
addedKeys.ShouldBeEmpty(); // Key already existed
|
|
_sut.GetSecret("existingKey").ShouldBe("originalValue"); // Value preserved
|
|
}
|
|
|
|
[Fact]
|
|
public void EnsureAllRequiredEntries_AutoSavesWhenKeysAdded()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "test.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
_sut.CreateStore(storePath, keyPath);
|
|
|
|
// Act
|
|
_sut.EnsureAllRequiredEntries(
|
|
new[] { "newKey" },
|
|
Array.Empty<string>());
|
|
|
|
// Assert - should auto-save (HasUnsavedChanges should be false after calling EnsureAllRequiredEntries)
|
|
_sut.HasUnsavedChanges.ShouldBeFalse();
|
|
|
|
// Verify persistence
|
|
_sut.CloseStore();
|
|
_sut.OpenStore(storePath, keyPath);
|
|
_sut.GetKeys().ShouldContain("newKey");
|
|
}
|
|
|
|
[Fact]
|
|
public void EnsureAllRequiredEntries_ReturnsEmptyList_WhenNoMissingKeys()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "test.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
_sut.CreateStore(storePath, keyPath);
|
|
_sut.SetSecret("key1", "value1");
|
|
_sut.SetSecret("key2", "value2");
|
|
_sut.Save();
|
|
|
|
// Act
|
|
var addedKeys = _sut.EnsureAllRequiredEntries(
|
|
new[] { "key1" },
|
|
new[] { "key2" });
|
|
|
|
// Assert
|
|
addedKeys.ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void EnsureAllRequiredEntries_WhenNoStoreOpen_ThrowsInvalidOperationException()
|
|
{
|
|
// Act & Assert
|
|
Should.Throw<InvalidOperationException>(() => _sut.EnsureAllRequiredEntries(
|
|
new[] { "key" },
|
|
Array.Empty<string>()));
|
|
}
|
|
|
|
[Fact]
|
|
public void EnsureAllRequiredEntries_HandlesDuplicatesAcrossLists()
|
|
{
|
|
// Arrange
|
|
var storePath = Path.Combine(_testDirectory, "test.json");
|
|
var keyPath = Path.Combine(_testDirectory, "test.key");
|
|
_sut.CreateStore(storePath, keyPath);
|
|
|
|
// Act - same key in both lists should not cause issues
|
|
var addedKeys = _sut.EnsureAllRequiredEntries(
|
|
new[] { "sharedKey" },
|
|
new[] { "sharedKey" });
|
|
|
|
// Assert - should only add once
|
|
addedKeys.Count.ShouldBe(1);
|
|
addedKeys.ShouldContain("sharedKey");
|
|
}
|
|
}
|