From d4424e0153dbc4d641d6811bf3ef9fa36a0b4a01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:47:16 +0000 Subject: [PATCH 1/5] Initial plan From 66b22b84d89e01bb96191502a4b5c266a62e0dac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:51:57 +0000 Subject: [PATCH 2/5] Fix PostgreSQL health check connection string handling Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com> --- src/Service/HealthCheck/HealthCheckHelper.cs | 6 +++--- src/Service/HealthCheck/HttpUtilities.cs | 4 ++-- src/Service/HealthCheck/Utilities.cs | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Service/HealthCheck/HealthCheckHelper.cs b/src/Service/HealthCheck/HealthCheckHelper.cs index ab19756195..991e39983f 100644 --- a/src/Service/HealthCheck/HealthCheckHelper.cs +++ b/src/Service/HealthCheck/HealthCheckHelper.cs @@ -162,7 +162,7 @@ private async Task UpdateDataSourceHealthCheckResultsAsync(ComprehensiveHealthCh if (comprehensiveHealthCheckReport.Checks != null && runtimeConfig.DataSource.IsDatasourceHealthEnabled) { string query = Utilities.GetDatSourceQuery(runtimeConfig.DataSource.DatabaseType); - (int, string?) response = await ExecuteDatasourceQueryCheckAsync(query, runtimeConfig.DataSource.ConnectionString, Utilities.GetDbProviderFactory(runtimeConfig.DataSource.DatabaseType)); + (int, string?) response = await ExecuteDatasourceQueryCheckAsync(query, runtimeConfig.DataSource.ConnectionString, Utilities.GetDbProviderFactory(runtimeConfig.DataSource.DatabaseType), runtimeConfig.DataSource.DatabaseType); bool isResponseTimeWithinThreshold = response.Item1 >= 0 && response.Item1 < runtimeConfig.DataSource.DatasourceThresholdMs; // Add DataSource Health Check Results @@ -182,14 +182,14 @@ private async Task UpdateDataSourceHealthCheckResultsAsync(ComprehensiveHealthCh } // Executes the DB Query and keeps track of the response time and error message. - private async Task<(int, string?)> ExecuteDatasourceQueryCheckAsync(string query, string connectionString, DbProviderFactory dbProviderFactory) + private async Task<(int, string?)> ExecuteDatasourceQueryCheckAsync(string query, string connectionString, DbProviderFactory dbProviderFactory, DatabaseType databaseType) { string? errorMessage = null; if (!string.IsNullOrEmpty(query) && !string.IsNullOrEmpty(connectionString)) { Stopwatch stopwatch = new(); stopwatch.Start(); - errorMessage = await _httpUtility.ExecuteDbQueryAsync(query, connectionString, dbProviderFactory); + errorMessage = await _httpUtility.ExecuteDbQueryAsync(query, connectionString, dbProviderFactory, databaseType); stopwatch.Stop(); return string.IsNullOrEmpty(errorMessage) ? ((int)stopwatch.ElapsedMilliseconds, errorMessage) : (HealthCheckConstants.ERROR_RESPONSE_TIME_MS, errorMessage); } diff --git a/src/Service/HealthCheck/HttpUtilities.cs b/src/Service/HealthCheck/HttpUtilities.cs index 9da596ae30..d2d31ae1b4 100644 --- a/src/Service/HealthCheck/HttpUtilities.cs +++ b/src/Service/HealthCheck/HttpUtilities.cs @@ -49,7 +49,7 @@ public HttpUtilities( } // Executes the DB query by establishing a connection to the DB. - public async Task ExecuteDbQueryAsync(string query, string connectionString, DbProviderFactory providerFactory) + public async Task ExecuteDbQueryAsync(string query, string connectionString, DbProviderFactory providerFactory, DatabaseType databaseType) { string? errorMessage = null; // Execute the query on DB and return the response time. @@ -65,7 +65,7 @@ public HttpUtilities( { try { - connection.ConnectionString = connectionString; + connection.ConnectionString = Utilities.NormalizeConnectionString(connectionString, databaseType); using (DbCommand command = connection.CreateCommand()) { command.CommandText = query; diff --git a/src/Service/HealthCheck/Utilities.cs b/src/Service/HealthCheck/Utilities.cs index 290410291e..a395886f1f 100644 --- a/src/Service/HealthCheck/Utilities.cs +++ b/src/Service/HealthCheck/Utilities.cs @@ -69,5 +69,19 @@ public static string CreateHttpRestQuery(string entityName, int first) // "EntityName?$first=4" return $"/{entityName}?$first={first}"; } + + public static string NormalizeConnectionString(string connectionString, DatabaseType dbType) + { + switch (dbType) + { + case DatabaseType.PostgreSQL: + return new NpgsqlConnectionStringBuilder(connectionString).ToString(); + case DatabaseType.MSSQL: + case DatabaseType.DWSQL: + return new SqlConnectionStringBuilder(connectionString).ToString(); + default: + return connectionString; + } + } } } From d858f2d973d008fa40ed5cb1a6956d04b67eea3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:28:34 +0000 Subject: [PATCH 3/5] Add exception handling to NormalizeConnectionString method Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com> --- src/Service/HealthCheck/Utilities.cs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/Service/HealthCheck/Utilities.cs b/src/Service/HealthCheck/Utilities.cs index a395886f1f..8ef5036430 100644 --- a/src/Service/HealthCheck/Utilities.cs +++ b/src/Service/HealthCheck/Utilities.cs @@ -72,15 +72,24 @@ public static string CreateHttpRestQuery(string entityName, int first) public static string NormalizeConnectionString(string connectionString, DatabaseType dbType) { - switch (dbType) + try { - case DatabaseType.PostgreSQL: - return new NpgsqlConnectionStringBuilder(connectionString).ToString(); - case DatabaseType.MSSQL: - case DatabaseType.DWSQL: - return new SqlConnectionStringBuilder(connectionString).ToString(); - default: - return connectionString; + switch (dbType) + { + case DatabaseType.PostgreSQL: + return new NpgsqlConnectionStringBuilder(connectionString).ToString(); + case DatabaseType.MSSQL: + case DatabaseType.DWSQL: + return new SqlConnectionStringBuilder(connectionString).ToString(); + default: + return connectionString; + } + } + catch (Exception) + { + // If the connection string cannot be parsed by the builder, + // return the original string to avoid failing the health check. + return connectionString; } } } From d94b703798eaffb20be9847c56e3f44451c2c774 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 08:29:11 +0000 Subject: [PATCH 4/5] Add MySQL support, logging, and unit tests for NormalizeConnectionString Co-authored-by: souvikghosh04 <210500244+souvikghosh04@users.noreply.github.com> --- .../HealthCheckUtilitiesUnitTests.cs | 195 ++++++++++++++++++ src/Service/HealthCheck/HttpUtilities.cs | 2 +- src/Service/HealthCheck/Utilities.cs | 10 +- 3 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 src/Service.Tests/UnitTests/HealthCheckUtilitiesUnitTests.cs diff --git a/src/Service.Tests/UnitTests/HealthCheckUtilitiesUnitTests.cs b/src/Service.Tests/UnitTests/HealthCheckUtilitiesUnitTests.cs new file mode 100644 index 0000000000..10ee7ebed1 --- /dev/null +++ b/src/Service.Tests/UnitTests/HealthCheckUtilitiesUnitTests.cs @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Service.HealthCheck; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Azure.DataApiBuilder.Service.Tests.UnitTests +{ + /// + /// Unit tests for health check utility methods. + /// + [TestClass] + public class HealthCheckUtilitiesUnitTests + { + /// + /// Tests that PostgreSQL connection strings are properly normalized. + /// + [TestMethod] + public void NormalizeConnectionString_PostgreSQL_Success() + { + // Arrange + string connectionString = "Host=localhost;Port=5432;Database=testdb;Username=testuser;Password=testpass"; + DatabaseType dbType = DatabaseType.PostgreSQL; + + // Act + string result = HealthCheck.Utilities.NormalizeConnectionString(connectionString, dbType); + + // Assert + Assert.IsNotNull(result); + Assert.IsTrue(result.Contains("Host=localhost")); + Assert.IsTrue(result.Contains("Database=testdb")); + } + + /// + /// Tests that MSSQL connection strings are properly normalized. + /// + [TestMethod] + public void NormalizeConnectionString_MSSQL_Success() + { + // Arrange + string connectionString = "Server=localhost;Database=testdb;User Id=testuser;Password=testpass"; + DatabaseType dbType = DatabaseType.MSSQL; + + // Act + string result = HealthCheck.Utilities.NormalizeConnectionString(connectionString, dbType); + + // Assert + Assert.IsNotNull(result); + Assert.IsTrue(result.Contains("Data Source=localhost")); + Assert.IsTrue(result.Contains("Initial Catalog=testdb")); + } + + /// + /// Tests that DWSQL connection strings are properly normalized. + /// + [TestMethod] + public void NormalizeConnectionString_DWSQL_Success() + { + // Arrange + string connectionString = "Server=localhost;Database=testdb;User Id=testuser;Password=testpass"; + DatabaseType dbType = DatabaseType.DWSQL; + + // Act + string result = HealthCheck.Utilities.NormalizeConnectionString(connectionString, dbType); + + // Assert + Assert.IsNotNull(result); + Assert.IsTrue(result.Contains("Data Source=localhost")); + Assert.IsTrue(result.Contains("Initial Catalog=testdb")); + } + + /// + /// Tests that MySQL connection strings are properly normalized. + /// + [TestMethod] + public void NormalizeConnectionString_MySQL_Success() + { + // Arrange + string connectionString = "Server=localhost;Port=3306;Database=testdb;Uid=testuser;Pwd=testpass"; + DatabaseType dbType = DatabaseType.MySQL; + + // Act + string result = HealthCheck.Utilities.NormalizeConnectionString(connectionString, dbType); + + // Assert + Assert.IsNotNull(result); + Assert.IsTrue(result.Contains("Server=localhost")); + Assert.IsTrue(result.Contains("Database=testdb")); + } + + /// + /// Tests that unsupported database types return the original connection string. + /// + [TestMethod] + public void NormalizeConnectionString_UnsupportedType_ReturnsOriginal() + { + // Arrange + string connectionString = "AccountEndpoint=https://test.documents.azure.com:443/;AccountKey=test"; + DatabaseType dbType = DatabaseType.CosmosDB_NoSQL; + + // Act + string result = HealthCheck.Utilities.NormalizeConnectionString(connectionString, dbType); + + // Assert + Assert.AreEqual(connectionString, result); + } + + /// + /// Tests that malformed connection strings are handled gracefully. + /// + [TestMethod] + public void NormalizeConnectionString_MalformedString_ReturnsOriginalAndLogs() + { + // Arrange + string malformedConnectionString = "InvalidConnectionString;NoEquals"; + DatabaseType dbType = DatabaseType.PostgreSQL; + Mock mockLogger = new Mock(); + + // Act + string result = HealthCheck.Utilities.NormalizeConnectionString(malformedConnectionString, dbType, mockLogger.Object); + + // Assert + Assert.AreEqual(malformedConnectionString, result); + mockLogger.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => true), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + /// + /// Tests that null logger is handled gracefully. + /// + [TestMethod] + public void NormalizeConnectionString_MalformedString_NullLogger_ReturnsOriginal() + { + // Arrange + string malformedConnectionString = "InvalidConnectionString;NoEquals"; + DatabaseType dbType = DatabaseType.MSSQL; + + // Act + string result = HealthCheck.Utilities.NormalizeConnectionString(malformedConnectionString, dbType, null); + + // Assert + Assert.AreEqual(malformedConnectionString, result); + } + + /// + /// Tests that PostgreSQL connection strings with lowercase keywords are normalized correctly. + /// This is the specific bug that was reported - lowercase 'host' was not supported. + /// + [TestMethod] + public void NormalizeConnectionString_PostgreSQL_LowercaseKeywords_Success() + { + // Arrange + string connectionString = "host=localhost;port=5432;database=mydb;username=myuser;password=mypass"; + DatabaseType dbType = DatabaseType.PostgreSQL; + + // Act + string result = HealthCheck.Utilities.NormalizeConnectionString(connectionString, dbType); + + // Assert + Assert.IsNotNull(result); + // NpgsqlConnectionStringBuilder should normalize lowercase keywords to proper format + Assert.IsTrue(result.Contains("Host=localhost") || result.Contains("host=localhost")); + Assert.IsTrue(result.Contains("Database=mydb") || result.Contains("database=mydb")); + } + + /// + /// Tests that empty connection strings are handled gracefully. + /// + [TestMethod] + public void NormalizeConnectionString_EmptyString_ReturnsEmpty() + { + // Arrange + string connectionString = string.Empty; + DatabaseType dbType = DatabaseType.PostgreSQL; + + // Act + string result = HealthCheck.Utilities.NormalizeConnectionString(connectionString, dbType); + + // Assert + Assert.AreEqual(string.Empty, result); + } + } +} diff --git a/src/Service/HealthCheck/HttpUtilities.cs b/src/Service/HealthCheck/HttpUtilities.cs index d2d31ae1b4..2a8d7b9f3e 100644 --- a/src/Service/HealthCheck/HttpUtilities.cs +++ b/src/Service/HealthCheck/HttpUtilities.cs @@ -65,7 +65,7 @@ public HttpUtilities( { try { - connection.ConnectionString = Utilities.NormalizeConnectionString(connectionString, databaseType); + connection.ConnectionString = Utilities.NormalizeConnectionString(connectionString, databaseType, _logger); using (DbCommand command = connection.CreateCommand()) { command.CommandText = query; diff --git a/src/Service/HealthCheck/Utilities.cs b/src/Service/HealthCheck/Utilities.cs index 8ef5036430..888ffbca91 100644 --- a/src/Service/HealthCheck/Utilities.cs +++ b/src/Service/HealthCheck/Utilities.cs @@ -7,6 +7,8 @@ using System.Text.Json; using Azure.DataApiBuilder.Config.ObjectModel; using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; +using MySqlConnector; using Npgsql; namespace Azure.DataApiBuilder.Service.HealthCheck @@ -70,7 +72,7 @@ public static string CreateHttpRestQuery(string entityName, int first) return $"/{entityName}?$first={first}"; } - public static string NormalizeConnectionString(string connectionString, DatabaseType dbType) + public static string NormalizeConnectionString(string connectionString, DatabaseType dbType, ILogger? logger = null) { try { @@ -78,6 +80,8 @@ public static string NormalizeConnectionString(string connectionString, Database { case DatabaseType.PostgreSQL: return new NpgsqlConnectionStringBuilder(connectionString).ToString(); + case DatabaseType.MySQL: + return new MySqlConnectionStringBuilder(connectionString).ToString(); case DatabaseType.MSSQL: case DatabaseType.DWSQL: return new SqlConnectionStringBuilder(connectionString).ToString(); @@ -85,8 +89,10 @@ public static string NormalizeConnectionString(string connectionString, Database return connectionString; } } - catch (Exception) + catch (Exception ex) { + // Log the exception if a logger is provided + logger?.LogWarning(ex, "Failed to parse connection string for database type {DatabaseType}. Returning original connection string.", dbType); // If the connection string cannot be parsed by the builder, // return the original string to avoid failing the health check. return connectionString; From 4a83f332e93578f68259e836025237cfc222f5d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:49:03 +0000 Subject: [PATCH 5/5] Refactor tests to use DataRow attributes and mask passwords Co-authored-by: Aniruddh25 <3513779+Aniruddh25@users.noreply.github.com> --- .../HealthCheckUtilitiesUnitTests.cs | 150 +++++++----------- 1 file changed, 55 insertions(+), 95 deletions(-) diff --git a/src/Service.Tests/UnitTests/HealthCheckUtilitiesUnitTests.cs b/src/Service.Tests/UnitTests/HealthCheckUtilitiesUnitTests.cs index 10ee7ebed1..db9e2d9409 100644 --- a/src/Service.Tests/UnitTests/HealthCheckUtilitiesUnitTests.cs +++ b/src/Service.Tests/UnitTests/HealthCheckUtilitiesUnitTests.cs @@ -19,79 +19,46 @@ namespace Azure.DataApiBuilder.Service.Tests.UnitTests public class HealthCheckUtilitiesUnitTests { /// - /// Tests that PostgreSQL connection strings are properly normalized. + /// Tests that connection strings are properly normalized for supported database types. /// [TestMethod] - public void NormalizeConnectionString_PostgreSQL_Success() + [DataRow( + DatabaseType.PostgreSQL, + "Host=localhost;Port=5432;Database=testdb;Username=testuser;Password=XXXX", + "Host=localhost", + "Database=testdb", + DisplayName = "PostgreSQL connection string normalization")] + [DataRow( + DatabaseType.MSSQL, + "Server=localhost;Database=testdb;User Id=testuser;Password=XXXX", + "Data Source=localhost", + "Initial Catalog=testdb", + DisplayName = "MSSQL connection string normalization")] + [DataRow( + DatabaseType.DWSQL, + "Server=localhost;Database=testdb;User Id=testuser;Password=XXXX", + "Data Source=localhost", + "Initial Catalog=testdb", + DisplayName = "DWSQL connection string normalization")] + [DataRow( + DatabaseType.MySQL, + "Server=localhost;Port=3306;Database=testdb;Uid=testuser;Pwd=XXXX", + "Server=localhost", + "Database=testdb", + DisplayName = "MySQL connection string normalization")] + public void NormalizeConnectionString_SupportedDatabases_Success( + DatabaseType dbType, + string connectionString, + string expectedServerPart, + string expectedDatabasePart) { - // Arrange - string connectionString = "Host=localhost;Port=5432;Database=testdb;Username=testuser;Password=testpass"; - DatabaseType dbType = DatabaseType.PostgreSQL; - - // Act - string result = HealthCheck.Utilities.NormalizeConnectionString(connectionString, dbType); - - // Assert - Assert.IsNotNull(result); - Assert.IsTrue(result.Contains("Host=localhost")); - Assert.IsTrue(result.Contains("Database=testdb")); - } - - /// - /// Tests that MSSQL connection strings are properly normalized. - /// - [TestMethod] - public void NormalizeConnectionString_MSSQL_Success() - { - // Arrange - string connectionString = "Server=localhost;Database=testdb;User Id=testuser;Password=testpass"; - DatabaseType dbType = DatabaseType.MSSQL; - - // Act - string result = HealthCheck.Utilities.NormalizeConnectionString(connectionString, dbType); - - // Assert - Assert.IsNotNull(result); - Assert.IsTrue(result.Contains("Data Source=localhost")); - Assert.IsTrue(result.Contains("Initial Catalog=testdb")); - } - - /// - /// Tests that DWSQL connection strings are properly normalized. - /// - [TestMethod] - public void NormalizeConnectionString_DWSQL_Success() - { - // Arrange - string connectionString = "Server=localhost;Database=testdb;User Id=testuser;Password=testpass"; - DatabaseType dbType = DatabaseType.DWSQL; - - // Act - string result = HealthCheck.Utilities.NormalizeConnectionString(connectionString, dbType); - - // Assert - Assert.IsNotNull(result); - Assert.IsTrue(result.Contains("Data Source=localhost")); - Assert.IsTrue(result.Contains("Initial Catalog=testdb")); - } - - /// - /// Tests that MySQL connection strings are properly normalized. - /// - [TestMethod] - public void NormalizeConnectionString_MySQL_Success() - { - // Arrange - string connectionString = "Server=localhost;Port=3306;Database=testdb;Uid=testuser;Pwd=testpass"; - DatabaseType dbType = DatabaseType.MySQL; - // Act string result = HealthCheck.Utilities.NormalizeConnectionString(connectionString, dbType); // Assert Assert.IsNotNull(result); - Assert.IsTrue(result.Contains("Server=localhost")); - Assert.IsTrue(result.Contains("Database=testdb")); + Assert.IsTrue(result.Contains(expectedServerPart)); + Assert.IsTrue(result.Contains(expectedDatabasePart)); } /// @@ -115,43 +82,36 @@ public void NormalizeConnectionString_UnsupportedType_ReturnsOriginal() /// Tests that malformed connection strings are handled gracefully. /// [TestMethod] - public void NormalizeConnectionString_MalformedString_ReturnsOriginalAndLogs() - { - // Arrange - string malformedConnectionString = "InvalidConnectionString;NoEquals"; - DatabaseType dbType = DatabaseType.PostgreSQL; - Mock mockLogger = new Mock(); - - // Act - string result = HealthCheck.Utilities.NormalizeConnectionString(malformedConnectionString, dbType, mockLogger.Object); - - // Assert - Assert.AreEqual(malformedConnectionString, result); - mockLogger.Verify( - x => x.Log( - LogLevel.Warning, - It.IsAny(), - It.Is((v, t) => true), - It.IsAny(), - It.Is>((v, t) => true)), - Times.Once); - } - - /// - /// Tests that null logger is handled gracefully. - /// - [TestMethod] - public void NormalizeConnectionString_MalformedString_NullLogger_ReturnsOriginal() + [DataRow(DatabaseType.PostgreSQL, true, DisplayName = "PostgreSQL malformed string with logger")] + [DataRow(DatabaseType.MSSQL, true, DisplayName = "MSSQL malformed string with logger")] + [DataRow(DatabaseType.MySQL, false, DisplayName = "MySQL malformed string without logger")] + public void NormalizeConnectionString_MalformedString_ReturnsOriginal( + DatabaseType dbType, + bool useLogger) { // Arrange string malformedConnectionString = "InvalidConnectionString;NoEquals"; - DatabaseType dbType = DatabaseType.MSSQL; + Mock? mockLogger = useLogger ? new Mock() : null; // Act - string result = HealthCheck.Utilities.NormalizeConnectionString(malformedConnectionString, dbType, null); + string result = HealthCheck.Utilities.NormalizeConnectionString( + malformedConnectionString, + dbType, + mockLogger?.Object); // Assert Assert.AreEqual(malformedConnectionString, result); + if (useLogger && mockLogger != null) + { + mockLogger.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => true), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } } /// @@ -162,7 +122,7 @@ public void NormalizeConnectionString_MalformedString_NullLogger_ReturnsOriginal public void NormalizeConnectionString_PostgreSQL_LowercaseKeywords_Success() { // Arrange - string connectionString = "host=localhost;port=5432;database=mydb;username=myuser;password=mypass"; + string connectionString = "host=localhost;port=5432;database=mydb;username=myuser;password=XXXX"; DatabaseType dbType = DatabaseType.PostgreSQL; // Act