604bfe919c
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
154 lines
4.8 KiB
C#
154 lines
4.8 KiB
C#
using JdeScoping.Infrastructure.Security;
|
|
using JdeScoping.Infrastructure.Tests.Helpers;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Shouldly;
|
|
using System.Security.Cryptography;
|
|
|
|
namespace JdeScoping.Infrastructure.Tests.Security;
|
|
|
|
public class SecureStoreRsaKeyServiceTests : IDisposable
|
|
{
|
|
private readonly InMemorySecureStore _secureStore;
|
|
|
|
public SecureStoreRsaKeyServiceTests()
|
|
{
|
|
_secureStore = new InMemorySecureStore();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_secureStore.Dispose();
|
|
}
|
|
|
|
private SecureStoreRsaKeyService CreateService()
|
|
{
|
|
return new SecureStoreRsaKeyService(
|
|
_secureStore,
|
|
NullLogger<SecureStoreRsaKeyService>.Instance);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WhenNoKeyInStore_GeneratesNewKeyAndStoresIt()
|
|
{
|
|
// Arrange - ensure no key exists
|
|
_secureStore.Contains(SecureStoreRsaKeyService.RsaPrivateKeyName).ShouldBeFalse();
|
|
|
|
// Act
|
|
using var service = CreateService();
|
|
|
|
// Assert - key should now be stored
|
|
_secureStore.Contains(SecureStoreRsaKeyService.RsaPrivateKeyName).ShouldBeTrue();
|
|
var storedKey = _secureStore.Get(SecureStoreRsaKeyService.RsaPrivateKeyName);
|
|
storedKey.ShouldStartWith("-----BEGIN RSA PRIVATE KEY-----");
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WhenKeyExistsInStore_LoadsExistingKey()
|
|
{
|
|
// Arrange - pre-store a key
|
|
using var originalRsa = RSA.Create(2048);
|
|
var originalPem = originalRsa.ExportRSAPrivateKeyPem();
|
|
var originalPublicKey = originalRsa.ExportSubjectPublicKeyInfoPem();
|
|
_secureStore.Set(SecureStoreRsaKeyService.RsaPrivateKeyName, originalPem);
|
|
|
|
// Act
|
|
using var service = CreateService();
|
|
|
|
// Assert - should load the same key
|
|
var loadedPublicKey = service.GetPublicKeyPem();
|
|
loadedPublicKey.ShouldBe(originalPublicKey);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetPublicKeyPem_ReturnsValidPemFormat()
|
|
{
|
|
// Arrange
|
|
using var service = CreateService();
|
|
|
|
// Act
|
|
var pem = service.GetPublicKeyPem();
|
|
|
|
// Assert
|
|
pem.ShouldStartWith("-----BEGIN PUBLIC KEY-----");
|
|
pem.ShouldEndWith("-----END PUBLIC KEY-----");
|
|
}
|
|
|
|
[Fact]
|
|
public void Decrypt_WithValidCiphertext_ReturnsPlaintext()
|
|
{
|
|
// Arrange
|
|
using var service = CreateService();
|
|
var plaintext = "Hello, World!"u8.ToArray();
|
|
|
|
// Encrypt using public key (simulating what client does)
|
|
using var rsa = RSA.Create();
|
|
rsa.ImportFromPem(service.GetPublicKeyPem());
|
|
var ciphertext = rsa.Encrypt(plaintext, RSAEncryptionPadding.OaepSHA256);
|
|
|
|
// Act
|
|
var decrypted = service.Decrypt(ciphertext);
|
|
|
|
// Assert
|
|
decrypted.ShouldBe(plaintext);
|
|
}
|
|
|
|
[Fact]
|
|
public void Decrypt_WithInvalidCiphertext_ThrowsCryptographicException()
|
|
{
|
|
// Arrange
|
|
using var service = CreateService();
|
|
var invalidCiphertext = new byte[] { 1, 2, 3, 4, 5 };
|
|
|
|
// Act & Assert
|
|
Should.Throw<CryptographicException>(() => service.Decrypt(invalidCiphertext));
|
|
}
|
|
|
|
[Fact]
|
|
public void MultipleInstances_WithSameStore_UseSameKey()
|
|
{
|
|
// Arrange & Act
|
|
using var service1 = CreateService();
|
|
var publicKey1 = service1.GetPublicKeyPem();
|
|
|
|
// Create second instance with same store
|
|
using var service2 = new SecureStoreRsaKeyService(
|
|
_secureStore,
|
|
NullLogger<SecureStoreRsaKeyService>.Instance);
|
|
var publicKey2 = service2.GetPublicKeyPem();
|
|
|
|
// Assert
|
|
publicKey2.ShouldBe(publicKey1);
|
|
}
|
|
|
|
[Fact]
|
|
public void EncryptDecrypt_RoundTrip_Succeeds()
|
|
{
|
|
// Arrange
|
|
using var service = CreateService();
|
|
var originalMessage = "Sensitive password data 123!"u8.ToArray();
|
|
|
|
// Act - encrypt with public key
|
|
using var clientRsa = RSA.Create();
|
|
clientRsa.ImportFromPem(service.GetPublicKeyPem());
|
|
var encrypted = clientRsa.Encrypt(originalMessage, RSAEncryptionPadding.OaepSHA256);
|
|
|
|
// Act - decrypt with service (which has private key)
|
|
var decrypted = service.Decrypt(encrypted);
|
|
|
|
// Assert
|
|
decrypted.ShouldBe(originalMessage);
|
|
}
|
|
|
|
[Fact]
|
|
public void Operations_AfterDispose_ThrowObjectDisposedException()
|
|
{
|
|
// Arrange
|
|
var service = CreateService();
|
|
service.Dispose();
|
|
|
|
// Act & Assert
|
|
Should.Throw<ObjectDisposedException>(() => service.GetPublicKeyPem());
|
|
Should.Throw<ObjectDisposedException>(() => service.Decrypt(Array.Empty<byte>()));
|
|
}
|
|
}
|