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");
+ }
+}