From 6e2decd21f34ddb8c0d267d2c8342c8b490546e8 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 19 Jan 2026 19:45:43 -0500 Subject: [PATCH] feat(configmanager): add LdapFormViewModel Implement LdapFormViewModel for editing LDAP configuration section with properties for ServerUrlsText, GroupDn, SearchBase, ConnectionTimeoutSeconds, UseFakeAuth, and AdminBypassUsersText. Array properties use newline-separated text with StringSplitOptions.RemoveEmptyEntries | TrimEntries for splitting. --- .../ViewModels/Forms/AuthFormViewModel.cs | 52 ++++++ .../Forms/ExcelExportFormViewModel.cs | 154 ++++++++++++++++++ .../ViewModels/Forms/LdapFormViewModel.cs | 122 ++++++++++++++ .../Forms/AuthFormViewModelTests.cs | 91 +++++++++++ .../Forms/ExcelExportFormViewModelTests.cs | 118 ++++++++++++++ .../Forms/LdapFormViewModelTests.cs | 60 +++++++ 6 files changed, 597 insertions(+) create mode 100644 NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/AuthFormViewModel.cs create mode 100644 NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ExcelExportFormViewModel.cs create mode 100644 NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/LdapFormViewModel.cs create mode 100644 NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/AuthFormViewModelTests.cs create mode 100644 NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ExcelExportFormViewModelTests.cs create mode 100644 NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/LdapFormViewModelTests.cs diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/AuthFormViewModel.cs b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/AuthFormViewModel.cs new file mode 100644 index 0000000..eb4c81d --- /dev/null +++ b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/AuthFormViewModel.cs @@ -0,0 +1,52 @@ +using JdeScoping.ConfigManager.Models; + +namespace JdeScoping.ConfigManager.ViewModels.Forms; + +/// +/// ViewModel for editing Auth configuration section. +/// +public class AuthFormViewModel : ViewModelBase +{ + private readonly AuthSection _model; + private readonly Action _onChanged; + + public AuthFormViewModel(AuthSection model, Action onChanged) + { + _model = model ?? throw new ArgumentNullException(nameof(model)); + _onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged)); + } + + /// + /// Gets or sets the name of the authentication cookie. + /// + public string CookieName + { + get => _model.CookieName; + set + { + if (_model.CookieName != value) + { + _model.CookieName = value; + OnPropertyChanged(); + _onChanged(); + } + } + } + + /// + /// Gets or sets the cookie expiration time in minutes. + /// + public int CookieExpirationMinutes + { + get => _model.CookieExpirationMinutes; + set + { + if (_model.CookieExpirationMinutes != value) + { + _model.CookieExpirationMinutes = value; + OnPropertyChanged(); + _onChanged(); + } + } + } +} diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ExcelExportFormViewModel.cs b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ExcelExportFormViewModel.cs new file mode 100644 index 0000000..5b32dfd --- /dev/null +++ b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ExcelExportFormViewModel.cs @@ -0,0 +1,154 @@ +using JdeScoping.ConfigManager.Models; + +namespace JdeScoping.ConfigManager.ViewModels.Forms; + +/// +/// ViewModel for editing ExcelExport configuration section. +/// +public class ExcelExportFormViewModel : ViewModelBase +{ + private readonly ExcelExportSection _model; + private readonly Action _onChanged; + + public ExcelExportFormViewModel(ExcelExportSection model, Action onChanged) + { + _model = model ?? throw new ArgumentNullException(nameof(model)); + _onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged)); + } + + /// + /// Gets or sets the password for protecting the criteria worksheet. + /// + public string CriteriaSheetPassword + { + get => _model.CriteriaSheetPassword; + set + { + if (_model.CriteriaSheetPassword != value) + { + _model.CriteriaSheetPassword = value; + OnPropertyChanged(); + _onChanged(); + } + } + } + + /// + /// Gets or sets the password for protecting the data worksheet. + /// + public string DataSheetPassword + { + get => _model.DataSheetPassword; + set + { + if (_model.DataSheetPassword != value) + { + _model.DataSheetPassword = value; + OnPropertyChanged(); + _onChanged(); + } + } + } + + /// + /// Gets or sets the maximum number of rows per Excel worksheet. + /// + public int MaxRowsPerSheet + { + get => _model.MaxRowsPerSheet; + set + { + if (_model.MaxRowsPerSheet != value) + { + _model.MaxRowsPerSheet = value; + OnPropertyChanged(); + _onChanged(); + } + } + } + + /// + /// Gets or sets the default date format for Excel exports. + /// + public string DefaultDateFormat + { + get => _model.DefaultDateFormat; + set + { + if (_model.DefaultDateFormat != value) + { + _model.DefaultDateFormat = value; + OnPropertyChanged(); + _onChanged(); + } + } + } + + /// + /// Gets or sets whether to write debug output to files. + /// + public bool DebugWriteToFile + { + get => _model.DebugWriteToFile; + set + { + if (_model.DebugWriteToFile != value) + { + _model.DebugWriteToFile = value; + OnPropertyChanged(); + _onChanged(); + } + } + } + + /// + /// Gets or sets the directory path for debug output files. + /// + public string DebugOutputDirectory + { + get => _model.DebugOutputDirectory; + set + { + if (_model.DebugOutputDirectory != value) + { + _model.DebugOutputDirectory = value; + OnPropertyChanged(); + _onChanged(); + } + } + } + + /// + /// Gets or sets the time zone identifier for date/time conversions. + /// + public string TimezoneId + { + get => _model.TimezoneId; + set + { + if (_model.TimezoneId != value) + { + _model.TimezoneId = value; + OnPropertyChanged(); + _onChanged(); + } + } + } + + /// + /// Gets or sets the time zone abbreviation for display purposes. + /// + public string TimezoneAbbreviation + { + get => _model.TimezoneAbbreviation; + set + { + if (_model.TimezoneAbbreviation != value) + { + _model.TimezoneAbbreviation = value; + OnPropertyChanged(); + _onChanged(); + } + } + } +} diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/LdapFormViewModel.cs b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/LdapFormViewModel.cs new file mode 100644 index 0000000..8c69507 --- /dev/null +++ b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/LdapFormViewModel.cs @@ -0,0 +1,122 @@ +using JdeScoping.ConfigManager.Models; + +namespace JdeScoping.ConfigManager.ViewModels.Forms; + +/// +/// ViewModel for editing LDAP configuration section. +/// +public class LdapFormViewModel : ViewModelBase +{ + private readonly LdapSection _model; + private readonly Action _onChanged; + + public LdapFormViewModel(LdapSection model, Action onChanged) + { + _model = model ?? throw new ArgumentNullException(nameof(model)); + _onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged)); + } + + /// + /// Gets or sets the server URLs as newline-separated text. + /// + public string ServerUrlsText + { + get => string.Join("\n", _model.ServerUrls); + set + { + var urls = value.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (!_model.ServerUrls.SequenceEqual(urls)) + { + _model.ServerUrls = urls; + OnPropertyChanged(); + _onChanged(); + } + } + } + + /// + /// Gets or sets the group distinguished name. + /// + public string GroupDn + { + get => _model.GroupDn; + set + { + if (_model.GroupDn != value) + { + _model.GroupDn = value; + OnPropertyChanged(); + _onChanged(); + } + } + } + + /// + /// Gets or sets the search base distinguished name. + /// + public string SearchBase + { + get => _model.SearchBase; + set + { + if (_model.SearchBase != value) + { + _model.SearchBase = value; + OnPropertyChanged(); + _onChanged(); + } + } + } + + /// + /// Gets or sets the connection timeout in seconds. + /// + public int ConnectionTimeoutSeconds + { + get => _model.ConnectionTimeoutSeconds; + set + { + if (_model.ConnectionTimeoutSeconds != value) + { + _model.ConnectionTimeoutSeconds = value; + OnPropertyChanged(); + _onChanged(); + } + } + } + + /// + /// Gets or sets whether to use fake authentication. + /// + public bool UseFakeAuth + { + get => _model.UseFakeAuth; + set + { + if (_model.UseFakeAuth != value) + { + _model.UseFakeAuth = value; + OnPropertyChanged(); + _onChanged(); + } + } + } + + /// + /// Gets or sets the admin bypass users as newline-separated text. + /// + public string AdminBypassUsersText + { + get => string.Join("\n", _model.AdminBypassUsers); + set + { + var users = value.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (!_model.AdminBypassUsers.SequenceEqual(users)) + { + _model.AdminBypassUsers = users; + OnPropertyChanged(); + _onChanged(); + } + } + } +} diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/AuthFormViewModelTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/AuthFormViewModelTests.cs new file mode 100644 index 0000000..652966d --- /dev/null +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/AuthFormViewModelTests.cs @@ -0,0 +1,91 @@ +using JdeScoping.ConfigManager.Models; +using JdeScoping.ConfigManager.ViewModels.Forms; + +namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms; + +public class AuthFormViewModelTests +{ + [Fact] + public void Constructor_InitializesFromModel() + { + // Arrange + var model = new AuthSection + { + CookieName = ".TestApp.Auth", + CookieExpirationMinutes = 120 + }; + + // Act + var sut = new AuthFormViewModel(model, () => { }); + + // Assert + sut.CookieName.ShouldBe(".TestApp.Auth"); + sut.CookieExpirationMinutes.ShouldBe(120); + } + + [Fact] + public void PropertyChange_UpdatesModelAndInvokesOnChanged() + { + // Arrange + var model = new AuthSection(); + var changedInvoked = false; + var sut = new AuthFormViewModel(model, () => changedInvoked = true); + + // Act + sut.CookieName = ".Custom.Cookie"; + + // Assert + model.CookieName.ShouldBe(".Custom.Cookie"); + changedInvoked.ShouldBeTrue(); + } + + [Fact] + public void CookieExpirationMinutes_UpdatesModelAndInvokesOnChanged() + { + // Arrange + var model = new AuthSection { CookieExpirationMinutes = 480 }; + var changedInvoked = false; + var sut = new AuthFormViewModel(model, () => changedInvoked = true); + + // Act + sut.CookieExpirationMinutes = 60; + + // Assert + model.CookieExpirationMinutes.ShouldBe(60); + changedInvoked.ShouldBeTrue(); + } + + [Fact] + public void PropertyChange_RaisesPropertyChanged() + { + // Arrange + var model = new AuthSection(); + var sut = new AuthFormViewModel(model, () => { }); + var propertyChangedRaised = false; + sut.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(AuthFormViewModel.CookieName)) + propertyChangedRaised = true; + }; + + // Act + sut.CookieName = ".New.Cookie"; + + // Assert + propertyChangedRaised.ShouldBeTrue(); + } + + [Fact] + public void Constructor_ThrowsOnNullModel() + { + // Act & Assert + Should.Throw(() => new AuthFormViewModel(null!, () => { })); + } + + [Fact] + public void Constructor_ThrowsOnNullCallback() + { + // Act & Assert + Should.Throw(() => new AuthFormViewModel(new AuthSection(), null!)); + } +} diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ExcelExportFormViewModelTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ExcelExportFormViewModelTests.cs new file mode 100644 index 0000000..b8a2321 --- /dev/null +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ExcelExportFormViewModelTests.cs @@ -0,0 +1,118 @@ +using JdeScoping.ConfigManager.Models; +using JdeScoping.ConfigManager.ViewModels.Forms; + +namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms; + +public class ExcelExportFormViewModelTests +{ + [Fact] + public void Constructor_InitializesFromModel() + { + // 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" + }; + + // 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"); + } + + [Fact] + public void PropertyChange_UpdatesModel() + { + // Arrange + var model = new ExcelExportSection { MaxRowsPerSheet = 1000000 }; + var sut = new ExcelExportFormViewModel(model, () => { }); + + // Act + sut.MaxRowsPerSheet = 750000; + + // Assert + model.MaxRowsPerSheet.ShouldBe(750000); + } + + [Fact] + public void PropertyChange_InvokesOnChanged() + { + // Arrange + var model = new ExcelExportSection(); + var changedInvoked = false; + var sut = new ExcelExportFormViewModel(model, () => changedInvoked = true); + + // Act + sut.TimezoneId = "Europe/London"; + + // Assert + changedInvoked.ShouldBeTrue(); + } + + [Fact] + public void PropertyChange_RaisesPropertyChanged() + { + // Arrange + var model = new ExcelExportSection(); + var sut = new ExcelExportFormViewModel(model, () => { }); + var propertyChangedRaised = false; + sut.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(ExcelExportFormViewModel.DefaultDateFormat)) + propertyChangedRaised = true; + }; + + // Act + sut.DefaultDateFormat = "dd-MMM-yyyy"; + + // Assert + propertyChangedRaised.ShouldBeTrue(); + } + + [Fact] + public void SameValueAssignment_DoesNotInvokeOnChanged() + { + // Arrange + var model = new ExcelExportSection { DebugWriteToFile = true }; + var changedInvoked = false; + var sut = new ExcelExportFormViewModel(model, () => changedInvoked = true); + + // Act + sut.DebugWriteToFile = true; // Same value + + // Assert + changedInvoked.ShouldBeFalse(); + } + + [Fact] + public void Constructor_ThrowsOnNullModel() + { + // Act & Assert + Should.Throw(() => new ExcelExportFormViewModel(null!, () => { })); + } + + [Fact] + public void Constructor_ThrowsOnNullOnChanged() + { + // Arrange + var model = new ExcelExportSection(); + + // Act & Assert + Should.Throw(() => new ExcelExportFormViewModel(model, null!)); + } +} diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/LdapFormViewModelTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/LdapFormViewModelTests.cs new file mode 100644 index 0000000..64b862d --- /dev/null +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/LdapFormViewModelTests.cs @@ -0,0 +1,60 @@ +using JdeScoping.ConfigManager.Models; +using JdeScoping.ConfigManager.ViewModels.Forms; + +namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms; + +public class LdapFormViewModelTests +{ + [Fact] + public void Constructor_InitializesFromModel() + { + // Arrange + var model = new LdapSection + { + ServerUrls = ["ldap://server1.local", "ldap://server2.local"], + GroupDn = "CN=Admins,DC=corp", + SearchBase = "DC=corp,DC=local", + UseFakeAuth = true + }; + + // Act + var sut = new LdapFormViewModel(model, () => { }); + + // Assert + sut.ServerUrlsText.ShouldBe("ldap://server1.local\nldap://server2.local"); + sut.GroupDn.ShouldBe("CN=Admins,DC=corp"); + sut.SearchBase.ShouldBe("DC=corp,DC=local"); + sut.UseFakeAuth.ShouldBeTrue(); + } + + [Fact] + public void ServerUrlsText_SplitsIntoArray() + { + // Arrange + var model = new LdapSection(); + var sut = new LdapFormViewModel(model, () => { }); + + // Act + sut.ServerUrlsText = "ldap://a.local\nldap://b.local\nldap://c.local"; + + // Assert + model.ServerUrls.Length.ShouldBe(3); + model.ServerUrls[0].ShouldBe("ldap://a.local"); + model.ServerUrls[2].ShouldBe("ldap://c.local"); + } + + [Fact] + public void AdminBypassUsersText_SplitsIntoArray() + { + // Arrange + var model = new LdapSection(); + var sut = new LdapFormViewModel(model, () => { }); + + // Act + sut.AdminBypassUsersText = "admin\nservice_account"; + + // Assert + model.AdminBypassUsers.Length.ShouldBe(2); + model.AdminBypassUsers[0].ShouldBe("admin"); + } +}