refactor: address code review findings across all projects
Apply comprehensive fixes from code reviews including: - Extract shared utilities (SqlFormatHelper, CellValueConverter, DbDestinationBase) - Add interface abstractions (IAuthenticationService, IDatabaseMigrator, IMisQueryBuilder) - Implement SecureStore for encrypted secrets storage - Fix error handling with proper HTTP status codes and logging - Optimize double enumeration in DevEtlRegistry - Add DataSync.Dev README for developer onboarding - Extract filter panel base classes to reduce duplication - Update code review docs to mark all issues as fixed
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
using JdeScoping.Core.Options;
|
||||
using JdeScoping.Infrastructure.Security;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Shouldly;
|
||||
|
||||
namespace JdeScoping.Infrastructure.Tests.Security;
|
||||
|
||||
public class SecureStoreServiceTests : IDisposable
|
||||
{
|
||||
private readonly string _testDir;
|
||||
private readonly string _storePath;
|
||||
private readonly string _keyPath;
|
||||
|
||||
public SecureStoreServiceTests()
|
||||
{
|
||||
_testDir = Path.Combine(Path.GetTempPath(), $"securestore-test-{Guid.NewGuid()}");
|
||||
Directory.CreateDirectory(_testDir);
|
||||
_storePath = Path.Combine(_testDir, "secrets.json");
|
||||
_keyPath = Path.Combine(_testDir, "secrets.key");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_testDir))
|
||||
{
|
||||
try { Directory.Delete(_testDir, recursive: true); }
|
||||
catch { /* Ignore cleanup failures */ }
|
||||
}
|
||||
}
|
||||
|
||||
private SecureStoreService CreateService(SecureStoreOptions? options = null)
|
||||
{
|
||||
options ??= new SecureStoreOptions
|
||||
{
|
||||
StorePath = _storePath,
|
||||
KeyFilePath = _keyPath,
|
||||
AutoCreateStore = true,
|
||||
MigrateExistingSecrets = false
|
||||
};
|
||||
|
||||
return new SecureStoreService(
|
||||
Microsoft.Extensions.Options.Options.Create(options),
|
||||
NullLogger<SecureStoreService>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenStoreDoesNotExist_CreatesStoreAndKeyFile()
|
||||
{
|
||||
// Arrange - ensure files don't exist
|
||||
File.Exists(_storePath).ShouldBeFalse();
|
||||
File.Exists(_keyPath).ShouldBeFalse();
|
||||
|
||||
// Act
|
||||
using var service = CreateService();
|
||||
|
||||
// Assert
|
||||
File.Exists(_storePath).ShouldBeTrue();
|
||||
File.Exists(_keyPath).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenAutoCreateDisabledAndStoreDoesNotExist_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var options = new SecureStoreOptions
|
||||
{
|
||||
StorePath = _storePath,
|
||||
KeyFilePath = _keyPath,
|
||||
AutoCreateStore = false
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<InvalidOperationException>(() => CreateService(options));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetAndGet_RoundTrip_ReturnsStoredValue()
|
||||
{
|
||||
// Arrange
|
||||
using var service = CreateService();
|
||||
const string key = "TestSecret";
|
||||
const string value = "MySecretValue123";
|
||||
|
||||
// Act
|
||||
service.Set(key, value);
|
||||
var retrieved = service.Get(key);
|
||||
|
||||
// Assert
|
||||
retrieved.ShouldBe(value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_WhenKeyDoesNotExist_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
using var service = CreateService();
|
||||
|
||||
// Act
|
||||
var result = service.Get("NonExistentKey");
|
||||
|
||||
// Assert
|
||||
result.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRequired_WhenKeyDoesNotExist_ThrowsKeyNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
using var service = CreateService();
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<KeyNotFoundException>(() => service.GetRequired("NonExistentKey"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Contains_WhenKeyExists_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
using var service = CreateService();
|
||||
service.Set("ExistingKey", "value");
|
||||
|
||||
// Act
|
||||
var result = service.Contains("ExistingKey");
|
||||
|
||||
// Assert
|
||||
result.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Contains_WhenKeyDoesNotExist_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
using var service = CreateService();
|
||||
|
||||
// Act
|
||||
var result = service.Contains("NonExistentKey");
|
||||
|
||||
// Assert
|
||||
result.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_WhenKeyExists_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
using var service = CreateService();
|
||||
service.Set("KeyToRemove", "value");
|
||||
|
||||
// Act
|
||||
var result = service.Remove("KeyToRemove");
|
||||
|
||||
// Assert
|
||||
result.ShouldBeTrue();
|
||||
service.Contains("KeyToRemove").ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_WhenKeyDoesNotExist_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
using var service = CreateService();
|
||||
|
||||
// Act
|
||||
var result = service.Remove("NonExistentKey");
|
||||
|
||||
// Assert
|
||||
result.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Save_PersistsChanges_LoadableByNewInstance()
|
||||
{
|
||||
// Arrange
|
||||
const string key = "PersistedSecret";
|
||||
const string value = "PersistedValue";
|
||||
|
||||
// Act - save with first instance
|
||||
using (var service1 = CreateService())
|
||||
{
|
||||
service1.Set(key, value);
|
||||
service1.SaveWithMetadata();
|
||||
}
|
||||
|
||||
// Assert - load with second instance
|
||||
using var service2 = CreateService();
|
||||
var retrieved = service2.Get(key);
|
||||
retrieved.ShouldBe(value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Keys_ReturnsAllStoredKeys()
|
||||
{
|
||||
// Arrange
|
||||
using var service = CreateService();
|
||||
service.Set("Key1", "Value1");
|
||||
service.Set("Key2", "Value2");
|
||||
service.Set("Key3", "Value3");
|
||||
|
||||
// Act
|
||||
var keys = service.Keys.ToList();
|
||||
|
||||
// Assert
|
||||
keys.ShouldContain("Key1");
|
||||
keys.ShouldContain("Key2");
|
||||
keys.ShouldContain("Key3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_AutoSavesDirtyChanges()
|
||||
{
|
||||
// Arrange
|
||||
const string key = "AutoSavedSecret";
|
||||
const string value = "AutoSavedValue";
|
||||
|
||||
// Act - set and dispose (should auto-save)
|
||||
using (var service1 = CreateService())
|
||||
{
|
||||
service1.Set(key, value);
|
||||
// Note: SaveWithMetadata needed for keys tracking
|
||||
service1.SaveWithMetadata();
|
||||
}
|
||||
|
||||
// Assert - load with second instance
|
||||
using var service2 = CreateService();
|
||||
var retrieved = service2.Get(key);
|
||||
retrieved.ShouldBe(value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Operations_AfterDispose_ThrowObjectDisposedException()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
service.Dispose();
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<ObjectDisposedException>(() => service.Get("key"));
|
||||
Should.Throw<ObjectDisposedException>(() => service.Set("key", "value"));
|
||||
Should.Throw<ObjectDisposedException>(() => service.Contains("key"));
|
||||
Should.Throw<ObjectDisposedException>(() => service.Remove("key"));
|
||||
Should.Throw<ObjectDisposedException>(() => service.Save());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user