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:
+412
@@ -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);
|
||||
}
|
||||
}
|
||||
+115
@@ -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();
|
||||
}
|
||||
}
|
||||
+327
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user