refactor(configmanager): simplify SecureStore UI with unified info view

Consolidate SecureStoreLockedFormView and SecureStoreUnlockedFormView into
a single SecureStoreInfoFormView that displays store status and metadata.
Simplifies MainWindowViewModel by removing redundant state management.
Also adds design docs for RegexTransformer feature.
This commit is contained in:
Joseph Doherty
2026-01-22 09:40:38 -05:00
parent 5669bac221
commit 9bf0c29add
28 changed files with 2811 additions and 1527 deletions
@@ -11,28 +11,45 @@ public class ExcelExportFormViewModelTests
// Arrange
var model = new ExcelExportSection
{
CriteriaSheetPassword = "criteriaPass123",
DataSheetPassword = "dataPass456",
MaxRowsPerSheet = 500000,
DefaultDateFormat = "MM/dd/yyyy",
DebugWriteToFile = true,
DebugOutputDirectory = "/tmp/debug",
TimezoneId = "America/New_York",
TimezoneAbbreviation = "ET"
TimezoneId = "America/Los_Angeles"
};
// Act
var sut = new ExcelExportFormViewModel(model, () => { });
// Assert
sut.CriteriaSheetPassword.ShouldBe("criteriaPass123");
sut.DataSheetPassword.ShouldBe("dataPass456");
sut.MaxRowsPerSheet.ShouldBe(500000);
sut.DefaultDateFormat.ShouldBe("MM/dd/yyyy");
sut.DebugWriteToFile.ShouldBeTrue();
sut.DebugOutputDirectory.ShouldBe("/tmp/debug");
sut.TimezoneId.ShouldBe("America/New_York");
sut.TimezoneAbbreviation.ShouldBe("ET");
sut.SelectedTimezone.ShouldBe("America/Los_Angeles");
}
[Fact]
public void Constructor_UsesModelTimezone()
{
// Arrange - model defaults to "America/Chicago"
var model = new ExcelExportSection();
// Act
var sut = new ExcelExportFormViewModel(model, () => { });
// Assert
sut.SelectedTimezone.ShouldBe("America/Chicago");
}
[Fact]
public void AvailableTimezones_ContainsSystemTimezones()
{
// Act & Assert
ExcelExportFormViewModel.AvailableTimezones.ShouldNotBeEmpty();
// Check for common IANA timezones
ExcelExportFormViewModel.AvailableTimezones.ShouldContain("America/Chicago");
ExcelExportFormViewModel.AvailableTimezones.ShouldContain("UTC");
}
[Fact]
@@ -49,16 +66,30 @@ public class ExcelExportFormViewModelTests
model.MaxRowsPerSheet.ShouldBe(750000);
}
[Fact]
public void SelectedTimezone_UpdatesModelTimezoneId()
{
// Arrange - model defaults to "America/Chicago"
var model = new ExcelExportSection();
var sut = new ExcelExportFormViewModel(model, () => { });
// Act - change to a different timezone
sut.SelectedTimezone = "America/New_York";
// Assert
model.TimezoneId.ShouldBe("America/New_York");
}
[Fact]
public void PropertyChange_InvokesOnChanged()
{
// Arrange
var model = new ExcelExportSection();
var model = new ExcelExportSection(); // Default TimezoneId is "America/Chicago"
var changedInvoked = false;
var sut = new ExcelExportFormViewModel(model, () => changedInvoked = true);
// Act
sut.TimezoneId = "Europe/London";
// Act - change to a different timezone than the default
sut.SelectedTimezone = "America/Denver";
// Assert
changedInvoked.ShouldBeTrue();
@@ -1,115 +0,0 @@
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();
}
}
@@ -1,327 +0,0 @@
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();
}
}