feat(configmanager): add ConnectionStrings editor with test connection support

Adds a new ConnectionStrings section to ConfigManager allowing users to manage
database connection strings with provider selection, connection testing, and
visual feedback for connection state.
This commit is contained in:
Joseph Doherty
2026-01-22 11:12:08 -05:00
parent 9bf0c29add
commit db663cc82d
20 changed files with 2508 additions and 2 deletions
@@ -0,0 +1,182 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
public class ConnectionStringEntryViewModelTests
{
[Fact]
public void Constructor_InitializesFromModel()
{
// Arrange
var model = new ConnectionStringEntry
{
Name = "TestConnection",
Provider = ConnectionProvider.SqlServer,
Server = "localhost",
Database = "TestDb",
UserId = "testuser",
Password = "testpass",
Encrypt = "Strict",
TrustServerCertificate = true,
ConnectionTimeout = 45,
ApplicationName = "TestApp"
};
// Act
var sut = new ConnectionStringEntryViewModel(model, () => { });
// Assert
sut.Name.ShouldBe("TestConnection");
sut.Provider.ShouldBe(ConnectionProvider.SqlServer);
sut.Server.ShouldBe("localhost");
sut.Database.ShouldBe("TestDb");
sut.UserId.ShouldBe("testuser");
sut.Password.ShouldBe("testpass");
sut.Encrypt.ShouldBe("Strict");
sut.TrustServerCertificate.ShouldBeTrue();
sut.ConnectionTimeout.ShouldBe(45);
sut.ApplicationName.ShouldBe("TestApp");
}
[Fact]
public void Constructor_ThrowsOnNullModel()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() => new ConnectionStringEntryViewModel(null!, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullOnChanged()
{
// Arrange
var model = new ConnectionStringEntry();
// Act & Assert
Should.Throw<ArgumentNullException>(() => new ConnectionStringEntryViewModel(model, null!));
}
[Fact]
public void PropertyChange_UpdatesModel()
{
// Arrange
var model = new ConnectionStringEntry();
var sut = new ConnectionStringEntryViewModel(model, () => { });
// Act
sut.Name = "UpdatedName";
sut.Server = "newserver";
sut.Database = "newdb";
// Assert
model.Name.ShouldBe("UpdatedName");
model.Server.ShouldBe("newserver");
model.Database.ShouldBe("newdb");
}
[Fact]
public void PropertyChange_InvokesOnChanged()
{
// Arrange
var model = new ConnectionStringEntry();
var changedInvoked = false;
var sut = new ConnectionStringEntryViewModel(model, () => changedInvoked = true);
// Act
sut.Name = "NewName";
// Assert
changedInvoked.ShouldBeTrue();
}
[Fact]
public void TogglePasswordVisibility_TogglesIsPasswordVisible()
{
// Arrange
var model = new ConnectionStringEntry();
var sut = new ConnectionStringEntryViewModel(model, () => { });
// Assert initial state
sut.IsPasswordVisible.ShouldBeFalse();
// Act - first toggle
sut.TogglePasswordVisibilityCommand.Execute(null);
// Assert
sut.IsPasswordVisible.ShouldBeTrue();
// Act - second toggle
sut.TogglePasswordVisibilityCommand.Execute(null);
// Assert
sut.IsPasswordVisible.ShouldBeFalse();
}
[Fact]
public void ProviderDisplay_ReturnsCorrectString()
{
// Arrange
var model = new ConnectionStringEntry { Provider = ConnectionProvider.SqlServer };
var sut = new ConnectionStringEntryViewModel(model, () => { });
// Assert
sut.ProviderDisplay.ShouldBe("SqlServer");
// Act
sut.Provider = ConnectionProvider.Oracle;
// Assert
sut.ProviderDisplay.ShouldBe("Oracle");
// Act
sut.Provider = ConnectionProvider.Generic;
// Assert
sut.ProviderDisplay.ShouldBe("Generic");
}
[Fact]
public void ServerDisplay_ReturnsServerForSqlServer()
{
// Arrange
var model = new ConnectionStringEntry
{
Provider = ConnectionProvider.SqlServer,
Server = "sql-server-host"
};
var sut = new ConnectionStringEntryViewModel(model, () => { });
// Assert
sut.ServerDisplay.ShouldBe("sql-server-host");
}
[Fact]
public void ServerDisplay_ReturnsHostForOracle()
{
// Arrange
var model = new ConnectionStringEntry
{
Provider = ConnectionProvider.Oracle,
Host = "oracle-host"
};
var sut = new ConnectionStringEntryViewModel(model, () => { });
// Assert
sut.ServerDisplay.ShouldBe("oracle-host");
}
[Fact]
public void ServerDisplay_ReturnsDashForGeneric()
{
// Arrange
var model = new ConnectionStringEntry
{
Provider = ConnectionProvider.Generic,
RawConnectionString = "some connection string"
};
var sut = new ConnectionStringEntryViewModel(model, () => { });
// Assert
sut.ServerDisplay.ShouldBe("-");
}
}
@@ -0,0 +1,167 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
public class ConnectionStringsFormViewModelTests
{
private readonly IDialogService _dialogService;
private readonly IConnectionTestService _connectionTestService;
public ConnectionStringsFormViewModelTests()
{
_dialogService = Substitute.For<IDialogService>();
_connectionTestService = Substitute.For<IConnectionTestService>();
}
[Fact]
public void Constructor_InitializesFromModel()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry
{
Name = "Connection1",
Provider = ConnectionProvider.SqlServer,
Server = "server1"
},
new ConnectionStringEntry
{
Name = "Connection2",
Provider = ConnectionProvider.Oracle,
Host = "oracle-host"
}
}
};
// Act
var sut = new ConnectionStringsFormViewModel(model, () => { }, _dialogService, _connectionTestService);
// Assert
sut.Connections.Count.ShouldBe(2);
sut.Connections[0].Name.ShouldBe("Connection1");
sut.Connections[0].Provider.ShouldBe(ConnectionProvider.SqlServer);
sut.Connections[0].Server.ShouldBe("server1");
sut.Connections[1].Name.ShouldBe("Connection2");
sut.Connections[1].Provider.ShouldBe(ConnectionProvider.Oracle);
sut.Connections[1].Host.ShouldBe("oracle-host");
}
[Fact]
public void Constructor_ThrowsOnNullModel()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new ConnectionStringsFormViewModel(null!, () => { }, _dialogService, _connectionTestService));
}
[Fact]
public void Constructor_ThrowsOnNullOnChanged()
{
// Arrange
var model = new ConnectionStringsSection();
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new ConnectionStringsFormViewModel(model, null!, _dialogService, _connectionTestService));
}
[Fact]
public void Constructor_ThrowsOnNullConnectionTestService()
{
// Arrange
var model = new ConnectionStringsSection();
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new ConnectionStringsFormViewModel(model, () => { }, _dialogService, null!));
}
[Fact]
public void AddConnection_CreatesNewEntryAndSelectsIt()
{
// Arrange
var model = new ConnectionStringsSection();
var changedInvoked = false;
var sut = new ConnectionStringsFormViewModel(model, () => changedInvoked = true, _dialogService, _connectionTestService);
// Act
sut.AddConnectionCommand.Execute(null);
// Assert
sut.Connections.Count.ShouldBe(1);
sut.Connections[0].Name.ShouldBe("NewConnection");
sut.SelectedConnection.ShouldBe(sut.Connections[0]);
model.Entries.Count.ShouldBe(1);
changedInvoked.ShouldBeTrue();
}
[Fact]
public void HasSelection_IsFalseWhenNothingSelected()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "Conn1" }
}
};
var sut = new ConnectionStringsFormViewModel(model, () => { }, _dialogService, _connectionTestService);
// Assert - no selection by default
sut.SelectedConnection.ShouldBeNull();
sut.HasSelection.ShouldBeFalse();
}
[Fact]
public void HasSelection_IsTrueWhenConnectionSelected()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "Conn1" }
}
};
var sut = new ConnectionStringsFormViewModel(model, () => { }, _dialogService, _connectionTestService);
// Act
sut.SelectedConnection = sut.Connections[0];
// Assert
sut.HasSelection.ShouldBeTrue();
}
[Fact]
public void ConnectionCount_ReflectsCollectionSize()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "Conn1" },
new ConnectionStringEntry { Name = "Conn2" },
new ConnectionStringEntry { Name = "Conn3" }
}
};
// Act
var sut = new ConnectionStringsFormViewModel(model, () => { }, _dialogService, _connectionTestService);
// Assert
sut.ConnectionCount.ShouldBe(3);
// Act - add another
sut.AddConnectionCommand.Execute(null);
// Assert
sut.ConnectionCount.ShouldBe(4);
}
}
@@ -19,6 +19,7 @@ public class MainWindowViewModelTests
private readonly ISecureStoreManager _secureStoreManager;
private readonly IClipboardService _clipboardService;
private readonly IRuntimeConfigValidationService _runtimeValidationService;
private readonly IConnectionTestService _connectionTestService;
private readonly ILogger<MainWindowViewModel> _logger;
public MainWindowViewModelTests()
@@ -32,6 +33,7 @@ public class MainWindowViewModelTests
_secureStoreManager = Substitute.For<ISecureStoreManager>();
_clipboardService = Substitute.For<IClipboardService>();
_runtimeValidationService = Substitute.For<IRuntimeConfigValidationService>();
_connectionTestService = Substitute.For<IConnectionTestService>();
_logger = Substitute.For<ILogger<MainWindowViewModel>>();
_validationService.ValidateAppSettings(Arg.Any<ConfigModel>())
@@ -284,7 +286,7 @@ public class MainWindowViewModelTests
// Without a configured/open SecureStore, only Settings and Pipelines appear
sut.TreeNodes.Count.ShouldBe(2); // Settings, Pipelines (no Secure Store when not configured)
sut.TreeNodes[0].Name.ShouldBe("Settings");
sut.TreeNodes[0].Children.Count.ShouldBe(6); // DataSync, DataAccess, Auth, Ldap, Search, ExcelExport
sut.TreeNodes[0].Children.Count.ShouldBe(7); // ConnectionStrings, DataSync, DataAccess, Auth, Ldap, Search, ExcelExport
sut.TreeNodes[1].Name.ShouldBe("Pipelines");
}
@@ -382,6 +384,7 @@ public class MainWindowViewModelTests
_secureStoreManager,
_clipboardService,
_runtimeValidationService,
_connectionTestService,
_logger);
}
}