feat(configmanager): integrate SecureStore for credential management

Add SecureStore integration to ConfigManager for secure handling of connection
strings and sensitive configuration values. Includes store/secret management
UI, encrypted .store file support, and comprehensive test coverage.
This commit is contained in:
Joseph Doherty
2026-01-20 02:51:16 -05:00
parent d49330e697
commit 94d5a864e0
44 changed files with 6220 additions and 4 deletions
@@ -0,0 +1,412 @@
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
public class SecretFormViewModelTests
{
private readonly IClipboardService _clipboardService;
public SecretFormViewModelTests()
{
_clipboardService = Substitute.For<IClipboardService>();
}
[Fact]
public void Constructor_SetsPropertiesCorrectly()
{
// Arrange & Act
var sut = new SecretFormViewModel(
"API_KEY",
"secret-value-123",
_clipboardService,
_ => { },
() => { });
// Assert
sut.Key.ShouldBe("API_KEY");
sut.Value.ShouldBe("secret-value-123");
sut.IsValueVisible.ShouldBeFalse();
}
[Fact]
public void Constructor_WithNullValue_SetsEmptyString()
{
// Arrange & Act
var sut = new SecretFormViewModel(
"API_KEY",
null!,
_clipboardService,
_ => { },
() => { });
// Assert
sut.Value.ShouldBe(string.Empty);
}
[Fact]
public void Value_Setter_InvokesCallback()
{
// Arrange
string? changedValue = null;
var sut = new SecretFormViewModel(
"API_KEY",
"initial",
_clipboardService,
v => changedValue = v,
() => { });
// Act
sut.Value = "new-value";
// Assert
changedValue.ShouldBe("new-value");
}
[Fact]
public void Value_Setter_RaisesPropertyChanged()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"initial",
_clipboardService,
_ => { },
() => { });
var propertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SecretFormViewModel.Value))
propertyChangedRaised = true;
};
// Act
sut.Value = "new-value";
// Assert
propertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void Value_Setter_RaisesDisplayValuePropertyChanged()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"initial",
_clipboardService,
_ => { },
() => { });
var displayValueChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SecretFormViewModel.DisplayValue))
displayValueChangedRaised = true;
};
// Act
sut.Value = "new-value";
// Assert
displayValueChangedRaised.ShouldBeTrue();
}
[Fact]
public void IsValueVisible_Toggle_Works()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"secret",
_clipboardService,
_ => { },
() => { });
// Act & Assert - Initial state
sut.IsValueVisible.ShouldBeFalse();
// Act - Toggle on
sut.IsValueVisible = true;
sut.IsValueVisible.ShouldBeTrue();
// Act - Toggle off
sut.IsValueVisible = false;
sut.IsValueVisible.ShouldBeFalse();
}
[Fact]
public void IsValueVisible_RaisesPropertyChanged()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"secret",
_clipboardService,
_ => { },
() => { });
var propertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SecretFormViewModel.IsValueVisible))
propertyChangedRaised = true;
};
// Act
sut.IsValueVisible = true;
// Assert
propertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void IsValueVisible_RaisesDisplayValueAndVisibilityButtonTextPropertyChanged()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"secret",
_clipboardService,
_ => { },
() => { });
var displayValueChangedRaised = false;
var visibilityButtonTextChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SecretFormViewModel.DisplayValue))
displayValueChangedRaised = true;
if (e.PropertyName == nameof(SecretFormViewModel.VisibilityButtonText))
visibilityButtonTextChangedRaised = true;
};
// Act
sut.IsValueVisible = true;
// Assert
displayValueChangedRaised.ShouldBeTrue();
visibilityButtonTextChangedRaised.ShouldBeTrue();
}
[Fact]
public void DisplayValue_ShowsMaskedValue_WhenNotVisible()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"secret123",
_clipboardService,
_ => { },
() => { });
// Act & Assert
sut.IsValueVisible.ShouldBeFalse();
sut.DisplayValue.ShouldBe("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"); // 9 bullet points
}
[Fact]
public void DisplayValue_ShowsActualValue_WhenVisible()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"secret123",
_clipboardService,
_ => { },
() => { });
// Act
sut.IsValueVisible = true;
// Assert
sut.DisplayValue.ShouldBe("secret123");
}
[Fact]
public void DisplayValue_LimitsMaskedLength_ToTwentyCharacters()
{
// Arrange
var longValue = new string('x', 50);
var sut = new SecretFormViewModel(
"API_KEY",
longValue,
_clipboardService,
_ => { },
() => { });
// Act & Assert
sut.DisplayValue.Length.ShouldBe(20);
sut.DisplayValue.ShouldBe(new string('\u2022', 20));
}
[Fact]
public void DisplayValue_HandlesEmptyValue()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"",
_clipboardService,
_ => { },
() => { });
// Act & Assert
sut.DisplayValue.ShouldBe("");
}
[Fact]
public void VisibilityButtonText_ShowsShow_WhenHidden()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"secret",
_clipboardService,
_ => { },
() => { });
// Act & Assert
sut.VisibilityButtonText.ShouldBe("Show");
}
[Fact]
public void VisibilityButtonText_ShowsHide_WhenVisible()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"secret",
_clipboardService,
_ => { },
() => { });
// Act
sut.IsValueVisible = true;
// Assert
sut.VisibilityButtonText.ShouldBe("Hide");
}
[Fact]
public void ToggleVisibilityCommand_TogglesVisibility()
{
// Arrange
var sut = new SecretFormViewModel(
"API_KEY",
"secret",
_clipboardService,
_ => { },
() => { });
// Act & Assert - Toggle on
sut.ToggleVisibilityCommand.Execute(null);
sut.IsValueVisible.ShouldBeTrue();
// Act & Assert - Toggle off
sut.ToggleVisibilityCommand.Execute(null);
sut.IsValueVisible.ShouldBeFalse();
}
[Fact]
public async Task CopyToClipboardCommand_CopiesValueToClipboard()
{
// Arrange
_clipboardService.SetTextAsync(Arg.Any<string>()).Returns(Task.CompletedTask);
var sut = new SecretFormViewModel(
"API_KEY",
"secret-to-copy",
_clipboardService,
_ => { },
() => { });
// Act
sut.CopyToClipboardCommand.Execute(null);
// Small delay to allow async command to complete
await Task.Delay(50);
// Assert
await _clipboardService.Received(1).SetTextAsync("secret-to-copy");
}
[Fact]
public void DeleteCommand_InvokesCallback()
{
// Arrange
var deleteCalled = false;
var sut = new SecretFormViewModel(
"API_KEY",
"secret",
_clipboardService,
_ => { },
() => deleteCalled = true);
// Act
sut.DeleteCommand.Execute(null);
// Assert
deleteCalled.ShouldBeTrue();
}
[Fact]
public void Constructor_ThrowsOnNullKey()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new SecretFormViewModel(null!, "value", _clipboardService, _ => { }, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullClipboardService()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new SecretFormViewModel("key", "value", null!, _ => { }, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullValueChangedCallback()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new SecretFormViewModel("key", "value", _clipboardService, null!, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullDeleteCallback()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new SecretFormViewModel("key", "value", _clipboardService, _ => { }, null!));
}
[Fact]
public void Key_IsReadOnly()
{
// Assert - Verify Key property is get-only
var keyProperty = typeof(SecretFormViewModel).GetProperty(nameof(SecretFormViewModel.Key));
keyProperty!.CanWrite.ShouldBeFalse();
}
[Fact]
public void Value_DoesNotInvokeCallback_WhenValueUnchanged()
{
// Arrange
var callbackCount = 0;
var sut = new SecretFormViewModel(
"API_KEY",
"same-value",
_clipboardService,
_ => callbackCount++,
() => { });
// Act
sut.Value = "same-value"; // Same as initial value
// Assert
callbackCount.ShouldBe(0);
}
}
@@ -0,0 +1,115 @@
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
public class SecureStoreLockedFormViewModelTests
{
[Fact]
public void Constructor_SetsPropertiesCorrectly()
{
// Arrange
var lastModified = DateTime.Now;
// Act
var sut = new SecureStoreLockedFormViewModel(
"test.secrets",
"/path/to/test.secrets",
lastModified,
() => { });
// Assert
sut.StoreName.ShouldBe("test.secrets");
sut.StorePath.ShouldBe("/path/to/test.secrets");
sut.LastModified.ShouldBe(lastModified);
}
[Fact]
public void Constructor_WithNullLastModified_SetsNullLastModified()
{
// Arrange & Act
var sut = new SecureStoreLockedFormViewModel(
"test.secrets",
"/path/to/test.secrets",
null,
() => { });
// Assert
sut.LastModified.ShouldBeNull();
}
[Fact]
public void UnlockCommand_InvokesCallback()
{
// Arrange
var unlockCalled = false;
var sut = new SecureStoreLockedFormViewModel(
"test.secrets",
"/path/to/test.secrets",
null,
() => unlockCalled = true);
// Act
sut.UnlockCommand.Execute(null);
// Assert
unlockCalled.ShouldBeTrue();
}
[Fact]
public void UnlockCommand_CanExecute_ReturnsTrue()
{
// Arrange
var sut = new SecureStoreLockedFormViewModel(
"test.secrets",
"/path/to/test.secrets",
null,
() => { });
// Act & Assert
sut.UnlockCommand.CanExecute(null).ShouldBeTrue();
}
[Fact]
public void Constructor_ThrowsOnNullStoreName()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new SecureStoreLockedFormViewModel(null!, "/path", null, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullStorePath()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new SecureStoreLockedFormViewModel("test", null!, null, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullUnlockCallback()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new SecureStoreLockedFormViewModel("test", "/path", null, null!));
}
[Fact]
public void Properties_AreReadOnly()
{
// Arrange
var sut = new SecureStoreLockedFormViewModel(
"test.secrets",
"/path/to/test.secrets",
DateTime.Now,
() => { });
// Assert - Verify properties are get-only (no setters)
var storeNameProperty = typeof(SecureStoreLockedFormViewModel).GetProperty(nameof(SecureStoreLockedFormViewModel.StoreName));
var storePathProperty = typeof(SecureStoreLockedFormViewModel).GetProperty(nameof(SecureStoreLockedFormViewModel.StorePath));
var lastModifiedProperty = typeof(SecureStoreLockedFormViewModel).GetProperty(nameof(SecureStoreLockedFormViewModel.LastModified));
storeNameProperty!.CanWrite.ShouldBeFalse();
storePathProperty!.CanWrite.ShouldBeFalse();
lastModifiedProperty!.CanWrite.ShouldBeFalse();
}
}
@@ -0,0 +1,327 @@
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
public class SecureStoreUnlockedFormViewModelTests
{
[Fact]
public void Constructor_SetsPropertiesCorrectly()
{
// Arrange & Act
var sut = new SecureStoreUnlockedFormViewModel(
"test.secrets",
"/path/to/test.secrets",
5,
false,
() => { },
() => { },
() => { });
// Assert
sut.StoreName.ShouldBe("test.secrets");
sut.StorePath.ShouldBe("/path/to/test.secrets");
sut.SecretCount.ShouldBe(5);
sut.HasUnsavedChanges.ShouldBeFalse();
}
[Fact]
public void Constructor_WithUnsavedChanges_SetsHasUnsavedChanges()
{
// Arrange & Act
var sut = new SecureStoreUnlockedFormViewModel(
"test.secrets",
"/path/to/test.secrets",
3,
true,
() => { },
() => { },
() => { });
// Assert
sut.HasUnsavedChanges.ShouldBeTrue();
}
[Fact]
public void HasUnsavedChanges_RaisesPropertyChanged()
{
// Arrange
var sut = new SecureStoreUnlockedFormViewModel(
"test.secrets",
"/path/to/test.secrets",
0,
false,
() => { },
() => { },
() => { });
var propertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SecureStoreUnlockedFormViewModel.HasUnsavedChanges))
propertyChangedRaised = true;
};
// Act
sut.HasUnsavedChanges = true;
// Assert
propertyChangedRaised.ShouldBeTrue();
}
[Fact]
public void HasUnsavedChanges_DoesNotRaisePropertyChanged_WhenValueUnchanged()
{
// Arrange
var sut = new SecureStoreUnlockedFormViewModel(
"test.secrets",
"/path/to/test.secrets",
0,
false,
() => { },
() => { },
() => { });
var propertyChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(SecureStoreUnlockedFormViewModel.HasUnsavedChanges))
propertyChangedRaised = true;
};
// Act
sut.HasUnsavedChanges = false; // Same as initial value
// Assert
propertyChangedRaised.ShouldBeFalse();
}
[Fact]
public void LockCommand_InvokesCallback()
{
// Arrange
var lockCalled = false;
var sut = new SecureStoreUnlockedFormViewModel(
"test.secrets",
"/path/to/test.secrets",
0,
false,
() => lockCalled = true,
() => { },
() => { });
// Act
sut.LockCommand.Execute(null);
// Assert
lockCalled.ShouldBeTrue();
}
[Fact]
public void AddSecretCommand_InvokesCallback()
{
// Arrange
var addSecretCalled = false;
var sut = new SecureStoreUnlockedFormViewModel(
"test.secrets",
"/path/to/test.secrets",
0,
false,
() => { },
() => addSecretCalled = true,
() => { });
// Act
sut.AddSecretCommand.Execute(null);
// Assert
addSecretCalled.ShouldBeTrue();
}
[Fact]
public void SaveCommand_InvokesCallback()
{
// Arrange
var saveCalled = false;
var sut = new SecureStoreUnlockedFormViewModel(
"test.secrets",
"/path/to/test.secrets",
0,
true, // Must have unsaved changes for save to be enabled
() => { },
() => { },
() => saveCalled = true);
// Act
sut.SaveCommand.Execute(null);
// Assert
saveCalled.ShouldBeTrue();
}
[Fact]
public void SaveCommand_CanExecute_ReturnsFalse_WhenNoUnsavedChanges()
{
// Arrange
var sut = new SecureStoreUnlockedFormViewModel(
"test.secrets",
"/path/to/test.secrets",
0,
false,
() => { },
() => { },
() => { });
// Act & Assert
sut.SaveCommand.CanExecute(null).ShouldBeFalse();
}
[Fact]
public void SaveCommand_CanExecute_ReturnsTrue_WhenHasUnsavedChanges()
{
// Arrange
var sut = new SecureStoreUnlockedFormViewModel(
"test.secrets",
"/path/to/test.secrets",
0,
true,
() => { },
() => { },
() => { });
// Act & Assert
sut.SaveCommand.CanExecute(null).ShouldBeTrue();
}
[Fact]
public void SaveCommand_CanExecute_Updates_WhenHasUnsavedChangesChanges()
{
// Arrange
var sut = new SecureStoreUnlockedFormViewModel(
"test.secrets",
"/path/to/test.secrets",
0,
false,
() => { },
() => { },
() => { });
// Initial state - can't save
sut.SaveCommand.CanExecute(null).ShouldBeFalse();
// Act
sut.HasUnsavedChanges = true;
// Assert
sut.SaveCommand.CanExecute(null).ShouldBeTrue();
}
[Fact]
public void SaveCommand_RaisesCanExecuteChanged_WhenHasUnsavedChangesChanges()
{
// Arrange
var sut = new SecureStoreUnlockedFormViewModel(
"test.secrets",
"/path/to/test.secrets",
0,
false,
() => { },
() => { },
() => { });
var canExecuteChangedRaised = false;
sut.SaveCommand.CanExecuteChanged += (s, e) => canExecuteChangedRaised = true;
// Act
sut.HasUnsavedChanges = true;
// Assert
canExecuteChangedRaised.ShouldBeTrue();
}
[Fact]
public void LockCommand_CanExecute_ReturnsTrue()
{
// Arrange
var sut = new SecureStoreUnlockedFormViewModel(
"test.secrets",
"/path/to/test.secrets",
0,
false,
() => { },
() => { },
() => { });
// Act & Assert
sut.LockCommand.CanExecute(null).ShouldBeTrue();
}
[Fact]
public void AddSecretCommand_CanExecute_ReturnsTrue()
{
// Arrange
var sut = new SecureStoreUnlockedFormViewModel(
"test.secrets",
"/path/to/test.secrets",
0,
false,
() => { },
() => { },
() => { });
// Act & Assert
sut.AddSecretCommand.CanExecute(null).ShouldBeTrue();
}
[Fact]
public void Constructor_ThrowsOnNullStoreName()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new SecureStoreUnlockedFormViewModel(null!, "/path", 0, false, () => { }, () => { }, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullStorePath()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new SecureStoreUnlockedFormViewModel("test", null!, 0, false, () => { }, () => { }, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullLockCallback()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new SecureStoreUnlockedFormViewModel("test", "/path", 0, false, null!, () => { }, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullAddSecretCallback()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new SecureStoreUnlockedFormViewModel("test", "/path", 0, false, () => { }, null!, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullSaveCallback()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new SecureStoreUnlockedFormViewModel("test", "/path", 0, false, () => { }, () => { }, null!));
}
[Fact]
public void ReadOnlyProperties_CannotBeModified()
{
// Assert - Verify StoreName, StorePath, and SecretCount are get-only
var storeNameProperty = typeof(SecureStoreUnlockedFormViewModel).GetProperty(nameof(SecureStoreUnlockedFormViewModel.StoreName));
var storePathProperty = typeof(SecureStoreUnlockedFormViewModel).GetProperty(nameof(SecureStoreUnlockedFormViewModel.StorePath));
var secretCountProperty = typeof(SecureStoreUnlockedFormViewModel).GetProperty(nameof(SecureStoreUnlockedFormViewModel.SecretCount));
storeNameProperty!.CanWrite.ShouldBeFalse();
storePathProperty!.CanWrite.ShouldBeFalse();
secretCountProperty!.CanWrite.ShouldBeFalse();
}
}