From 5ee710d330815535ca5798d820551defc81f2339 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 28 Jan 2026 09:24:49 -0500 Subject: [PATCH] feat(configmanager): add dedicated SQL Server port control Add SqlServerPort property to connection string editor for explicit port configuration, replacing the need to embed port in server name (e.g., localhost,1434). The port control generates the comma-separated format in the connection string but does not parse it back when loading existing connection strings. --- .../Models/ConnectionStringEntry.cs | 12 +- .../ConnectionStringsSectionConverter.cs | 155 +++++++++--------- .../Forms/ConnectionStringEntryViewModel.cs | 18 ++ .../Forms/ConnectionStringsFormView.axaml | 68 ++++---- .../Models/ConnectionStringEntryTests.cs | 67 ++++++++ .../ConnectionStringsSectionConverterTests.cs | 82 ++++++++- .../ConnectionStringEntryViewModelTests.cs | 37 +++++ .../ConnectionStringsFormViewModelTests.cs | 3 +- 8 files changed, 334 insertions(+), 108 deletions(-) diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringEntry.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringEntry.cs index f64bbde..6c61f82 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringEntry.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringEntry.cs @@ -55,6 +55,11 @@ public class ConnectionStringEntry /// public string? ApplicationName { get; set; } + /// + /// Gets or sets the SQL Server port number. When null, no port is specified. + /// + public int? SqlServerPort { get; set; } + /// /// Gets or sets the Oracle server host name or address. /// @@ -94,7 +99,12 @@ public class ConnectionStringEntry var parts = new List(); if (!string.IsNullOrWhiteSpace(Server)) - parts.Add($"Server={Server}"); + { + var serverValue = SqlServerPort.HasValue && SqlServerPort.Value > 0 + ? $"{Server},{SqlServerPort.Value}" + : Server; + parts.Add($"Server={serverValue}"); + } if (!string.IsNullOrWhiteSpace(Database)) parts.Add($"Database={Database}"); if (!string.IsNullOrWhiteSpace(UserId)) diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringsSectionConverter.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringsSectionConverter.cs index 5c4f922..5d81755 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringsSectionConverter.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringsSectionConverter.cs @@ -62,11 +62,11 @@ public class ConnectionStringsSectionConverter : JsonConverter /// Parses a connection string and attempts to detect the provider type. /// - internal static ConnectionStringEntry ParseConnectionString(string name, string connectionString) - { - var entry = new ConnectionStringEntry - { - Name = name, - RawConnectionString = connectionString - }; - - // Try to detect provider and parse structured fields - var parts = ParseConnectionStringParts(connectionString); - - // Detect Oracle using HOST/Service Name/Port pattern (DDTek.Oracle style) - var hasHost = parts.TryGetValue("host", out var host); - var hasServiceName = parts.TryGetValue("service name", out var serviceName); - var hasPort = parts.TryGetValue("port", out var portText); - - if (hasHost || hasServiceName || hasPort) - { - entry.Provider = ConnectionProvider.Oracle; - - if (hasHost && !string.IsNullOrEmpty(host)) - { - entry.Host = host; - } - - if (hasServiceName && !string.IsNullOrEmpty(serviceName)) - { - entry.ServiceName = serviceName; - } - - if (hasPort && !string.IsNullOrEmpty(portText) && int.TryParse(portText, out var port)) - { - entry.Port = port; - } - - if (parts.TryGetValue("user id", out var oraUserId)) - { - entry.UserId = oraUserId; - } - - if (parts.TryGetValue("password", out var oraPassword)) - { - entry.Password = oraPassword; - } - } - // Detect Oracle first (Data Source with host:port/service pattern) - else if (parts.TryGetValue("data source", out var dataSource) && - IsOracleDataSource(dataSource)) - { - entry.Provider = ConnectionProvider.Oracle; - ParseOracleDataSource(entry, dataSource); + internal static ConnectionStringEntry ParseConnectionString(string name, string connectionString) + { + var entry = new ConnectionStringEntry + { + Name = name, + RawConnectionString = connectionString + }; + + // Try to detect provider and parse structured fields + var parts = ParseConnectionStringParts(connectionString); + + // Detect Oracle using HOST/Service Name/Port pattern (DDTek.Oracle style) + var hasHost = parts.TryGetValue("host", out var host); + var hasServiceName = parts.TryGetValue("service name", out var serviceName); + var hasPort = parts.TryGetValue("port", out var portText); + + if (hasHost || hasServiceName || hasPort) + { + entry.Provider = ConnectionProvider.Oracle; + + if (hasHost && !string.IsNullOrEmpty(host)) + { + entry.Host = host; + } + + if (hasServiceName && !string.IsNullOrEmpty(serviceName)) + { + entry.ServiceName = serviceName; + } + + if (hasPort && !string.IsNullOrEmpty(portText) && int.TryParse(portText, out var port)) + { + entry.Port = port; + } + + if (parts.TryGetValue("user id", out var oraUserId)) + { + entry.UserId = oraUserId; + } + + if (parts.TryGetValue("password", out var oraPassword)) + { + entry.Password = oraPassword; + } + } + // Detect Oracle first (Data Source with host:port/service pattern) + else if (parts.TryGetValue("data source", out var dataSource) && + IsOracleDataSource(dataSource)) + { + entry.Provider = ConnectionProvider.Oracle; + ParseOracleDataSource(entry, dataSource); if (parts.TryGetValue("user id", out var oraUserId)) { @@ -212,27 +212,28 @@ public class ConnectionStringsSectionConverter : JsonConverter /// Parses a connection string into key-value pairs. diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ConnectionStringEntryViewModel.cs b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ConnectionStringEntryViewModel.cs index c0d5d02..021c2ec 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ConnectionStringEntryViewModel.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ConnectionStringEntryViewModel.cs @@ -208,6 +208,24 @@ public class ConnectionStringEntryViewModel : ViewModelBase } } + /// + /// Gets or sets the SQL Server port number. + /// + public int? SqlServerPort + { + get => _model.SqlServerPort; + set + { + if (_model.SqlServerPort != value) + { + _model.SqlServerPort = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(GeneratedConnectionString)); + _onChanged(); + } + } + } + /// /// Gets or sets the Oracle host name. /// diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/ConnectionStringsFormView.axaml b/NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/ConnectionStringsFormView.axaml index 3c02fd5..fd34eeb 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/ConnectionStringsFormView.axaml +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/ConnectionStringsFormView.axaml @@ -10,8 +10,8 @@ - - + + - - + + + + + + + + + @@ -279,26 +291,26 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/Models/ConnectionStringEntryTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/Models/ConnectionStringEntryTests.cs index ef4329a..f38163e 100644 --- a/NEW/tests/JdeScoping.ConfigManager.Tests/Models/ConnectionStringEntryTests.cs +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/Models/ConnectionStringEntryTests.cs @@ -107,6 +107,7 @@ public class ConnectionStringEntryTests entry.Name.ShouldBe(string.Empty); entry.Provider.ShouldBe(ConnectionProvider.Generic); entry.Server.ShouldBeNull(); + entry.SqlServerPort.ShouldBeNull(); entry.Database.ShouldBeNull(); entry.UserId.ShouldBeNull(); entry.Password.ShouldBeNull(); @@ -119,4 +120,70 @@ public class ConnectionStringEntryTests entry.ServiceName.ShouldBeNull(); entry.RawConnectionString.ShouldBeNull(); } + + [Fact] + public void GenerateConnectionString_SqlServer_WithPort_IncludesPortInServer() + { + // Arrange + var entry = new ConnectionStringEntry + { + Provider = ConnectionProvider.SqlServer, + Server = "localhost", + SqlServerPort = 1434, + Database = "TestDb", + UserId = "sa", + Password = "secret123" + }; + + // Act + var result = entry.GenerateConnectionString(); + + // Assert + result.ShouldContain("Server=localhost,1434"); + result.ShouldContain("Database=TestDb"); + } + + [Fact] + public void GenerateConnectionString_SqlServer_WithoutPort_NoPortInServer() + { + // Arrange + var entry = new ConnectionStringEntry + { + Provider = ConnectionProvider.SqlServer, + Server = "localhost", + SqlServerPort = null, + Database = "TestDb", + UserId = "sa", + Password = "secret123" + }; + + // Act + var result = entry.GenerateConnectionString(); + + // Assert + result.ShouldContain("Server=localhost;"); + result.ShouldNotContain("Server=localhost,"); + } + + [Fact] + public void GenerateConnectionString_SqlServer_WithZeroPort_NoPortInServer() + { + // Arrange + var entry = new ConnectionStringEntry + { + Provider = ConnectionProvider.SqlServer, + Server = "localhost", + SqlServerPort = 0, + Database = "TestDb", + UserId = "sa", + Password = "secret123" + }; + + // Act + var result = entry.GenerateConnectionString(); + + // Assert + result.ShouldContain("Server=localhost;"); + result.ShouldNotContain("Server=localhost,"); + } } diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/Models/ConnectionStringsSectionConverterTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/Models/ConnectionStringsSectionConverterTests.cs index 5265a09..af669da 100644 --- a/NEW/tests/JdeScoping.ConfigManager.Tests/Models/ConnectionStringsSectionConverterTests.cs +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/Models/ConnectionStringsSectionConverterTests.cs @@ -34,7 +34,8 @@ public class ConnectionStringsSectionConverterTests var lotFinder = section.Entries.First(e => e.Name == "LotFinder"); lotFinder.Provider.ShouldBe(ConnectionProvider.SqlServer); - lotFinder.Server.ShouldBe("localhost,1434"); + lotFinder.Server.ShouldBe("localhost,1434"); // Port embedded in server, not parsed separately + lotFinder.SqlServerPort.ShouldBeNull(); lotFinder.Database.ShouldBe("ScopingTool"); lotFinder.UserId.ShouldBe("sa"); @@ -195,4 +196,83 @@ public class ConnectionStringsSectionConverterTests secondary.Port.ShouldBe(1521); secondary.ServiceName.ShouldBe("ORCL"); } + + [Fact] + public void Deserialize_SqlServerWithEmbeddedPort_KeepsPortInServer() + { + // Arrange - connection string with port embedded in server name (legacy format) + var json = """ + { + "TestDb": "Server=localhost,1434;Database=TestDB;User Id=testuser;Password=testpass" + } + """; + + // Act + var section = JsonSerializer.Deserialize(json, JsonOptions); + + // Assert - port is NOT parsed out, stays embedded in server name + section.ShouldNotBeNull(); + section.Entries.Count.ShouldBe(1); + + var entry = section.Entries[0]; + entry.Name.ShouldBe("TestDb"); + entry.Provider.ShouldBe(ConnectionProvider.SqlServer); + entry.Server.ShouldBe("localhost,1434"); // Port stays in server name + entry.SqlServerPort.ShouldBeNull(); // Port control not populated from parsing + entry.Database.ShouldBe("TestDB"); + } + + [Fact] + public void Deserialize_SqlServerWithoutPort_ServerHasNoPort() + { + // Arrange + var json = """ + { + "TestDb": "Server=myserver;Database=TestDB;User Id=testuser;Password=testpass" + } + """; + + // Act + var section = JsonSerializer.Deserialize(json, JsonOptions); + + // Assert + section.ShouldNotBeNull(); + section.Entries.Count.ShouldBe(1); + + var entry = section.Entries[0]; + entry.Server.ShouldBe("myserver"); + entry.SqlServerPort.ShouldBeNull(); + } + + [Fact] + public void RoundTrip_SqlServerWithPort_EmbedsPortInServer() + { + // Arrange - entry with separate port control + var original = new ConnectionStringsSection + { + Entries = new List + { + new() + { + Name = "WithPort", + Provider = ConnectionProvider.SqlServer, + Server = "localhost", + SqlServerPort = 1434, + Database = "DB1", + UserId = "user1", + Password = "pass1" + } + } + }; + + // Act + var json = JsonSerializer.Serialize(original, JsonOptions); + var deserialized = JsonSerializer.Deserialize(json, JsonOptions); + + // Assert - after round trip, port is embedded in server name (not parsed back out) + deserialized.ShouldNotBeNull(); + var entry = deserialized.Entries.First(); + entry.Server.ShouldBe("localhost,1434"); // Port embedded in server after round trip + entry.SqlServerPort.ShouldBeNull(); // Port control not populated from parsing + } } diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringEntryViewModelTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringEntryViewModelTests.cs index 68dba00..e50e782 100644 --- a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringEntryViewModelTests.cs +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringEntryViewModelTests.cs @@ -14,6 +14,7 @@ public class ConnectionStringEntryViewModelTests Name = "TestConnection", Provider = ConnectionProvider.SqlServer, Server = "localhost", + SqlServerPort = 1434, Database = "TestDb", UserId = "testuser", Password = "testpass", @@ -30,6 +31,7 @@ public class ConnectionStringEntryViewModelTests sut.Name.ShouldBe("TestConnection"); sut.Provider.ShouldBe(ConnectionProvider.SqlServer); sut.Server.ShouldBe("localhost"); + sut.SqlServerPort.ShouldBe(1434); sut.Database.ShouldBe("TestDb"); sut.UserId.ShouldBe("testuser"); sut.Password.ShouldBe("testpass"); @@ -179,4 +181,39 @@ public class ConnectionStringEntryViewModelTests // Assert sut.ServerDisplay.ShouldBe("-"); } + + [Fact] + public void SqlServerPort_PropertyChange_UpdatesModel() + { + // Arrange + var model = new ConnectionStringEntry(); + var changedInvoked = false; + var sut = new ConnectionStringEntryViewModel(model, () => changedInvoked = true); + + // Act + sut.SqlServerPort = 1434; + + // Assert + model.SqlServerPort.ShouldBe(1434); + changedInvoked.ShouldBeTrue(); + } + + [Fact] + public void SqlServerPort_PropertyChange_UpdatesGeneratedConnectionString() + { + // Arrange + var model = new ConnectionStringEntry + { + Provider = ConnectionProvider.SqlServer, + Server = "localhost", + Database = "TestDb" + }; + var sut = new ConnectionStringEntryViewModel(model, () => { }); + + // Act + sut.SqlServerPort = 1434; + + // Assert + sut.GeneratedConnectionString.ShouldContain("Server=localhost,1434"); + } } diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringsFormViewModelTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringsFormViewModelTests.cs index c8edfde..f56c287 100644 --- a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringsFormViewModelTests.cs +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringsFormViewModelTests.cs @@ -75,10 +75,11 @@ public class ConnectionStringsFormViewModelTests // Act var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService); - // Assert + // Assert - port stays embedded in server name, not parsed into SqlServerPort sut.Connections.Count.ShouldBe(1); sut.Connections[0].Provider.ShouldBe(ConnectionProvider.SqlServer); sut.Connections[0].Server.ShouldBe("localhost,1434"); + sut.Connections[0].SqlServerPort.ShouldBeNull(); sut.Connections[0].Database.ShouldBe("ScopingTool"); sut.Connections[0].UserId.ShouldBe("scopingapp"); sut.Connections[0].Password.ShouldBe("pass");