From 75a057f268ff6ef6519e6ac5c13588641a06791f Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 5 Aug 2026 07:43:31 -0400 Subject: [PATCH] fix(sql): Test Connect must construct Postgres and Oracle dialects too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SqlDriverProbe.DefaultDialectFor was left SqlServer-only when the provider gates were widened, so AdminUI Test Connect failed for both new providers with InvalidOperationException while Deploy and browse worked. Found by the live chrome/AdminUI gate the plan deferred, not by any test: every case in SqlDriverProbeTests builds its probe through ForTest(), which injects the dialect and never reaches the production switch. Widen the switch arm-for-arm with SqlDriverFactoryExtensions.CreateDialect, and cover the gate directly via an internal seam — plus a reserved-provider falsification control and an end-to-end production-constructor case. --- .../SqlDriverProbe.cs | 30 +++++++++++- .../SqlDriverProbeTests.cs | 49 +++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriverProbe.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriverProbe.cs index 95222661..fe28c346 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriverProbe.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriverProbe.cs @@ -164,11 +164,37 @@ public sealed class SqlDriverProbe : IDriverProbe } } - /// Constructs the production dialect for a provider; v1 supports SQL Server only. + /// + /// Test seam over . The parameterless constructor is the only + /// production path into that switch, and deliberately bypasses it — so without + /// this seam the gate has no direct coverage at all, which is how it kept a SqlServer-only switch + /// after the factory and browser were widened. + /// + /// The provider whose dialect to construct. + /// The dialect this build constructs for . + /// The provider is reserved and not constructed. + internal static ISqlDialect DefaultDialectForTest(SqlProvider provider) => DefaultDialectFor(provider); + + /// + /// Constructs the production dialect for a provider — the same set + /// constructs, because Test Connect must exercise the + /// backend a Deploy would actually use. This build constructs , + /// and ; + /// and stay reserved. + /// Keep this arm-for-arm with the factory's CreateDialect. When the factory and the + /// browser were widened to Postgres + Oracle this switch was left SqlServer-only, so a config that + /// deployed and browsed fine failed Test Connect with an — and + /// no test noticed, because every probe test injects its dialect through . + /// private static ISqlDialect DefaultDialectFor(SqlProvider provider) => provider switch { SqlProvider.SqlServer => new SqlServerDialect(), + SqlProvider.Postgres => new PostgresDialect(), + SqlProvider.Oracle => new OracleDialect(), _ => throw new InvalidOperationException( - $"provider '{provider}' is not available in this build (only SqlServer is supported)."), + $"provider '{provider}' is not available in this build. Supported: " + + $"'{nameof(SqlProvider.SqlServer)}', '{nameof(SqlProvider.Postgres)}', " + + $"'{nameof(SqlProvider.Oracle)}'. Reserved (declared on the enum but not implemented): " + + $"'{nameof(SqlProvider.MySql)}', '{nameof(SqlProvider.Odbc)}'."), }; } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlDriverProbeTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlDriverProbeTests.cs index 7092695d..e8eb97a7 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlDriverProbeTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlDriverProbeTests.cs @@ -3,6 +3,7 @@ using System.Data.Common; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; +using ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts; namespace ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests; @@ -125,6 +126,54 @@ public sealed class SqlDriverProbeTests result.Ok.ShouldBeFalse(); } + /// + /// The production dialect gate — the one seam every other test in this file bypasses. + /// Each case above builds its probe with , which injects + /// _ => dialect and therefore never reaches DefaultDialectFor. That is exactly how the + /// probe kept a SqlServer-only gate while the factory and the browser were widened to Postgres + + /// Oracle: the whole suite stayed green, and AdminUI "Test Connect" failed for two of the three + /// shipped providers with InvalidOperationException. Caught by the live chrome/AdminUI gate, + /// not by any test — so this asserts the gate directly. + /// + [Theory] + [InlineData(SqlProvider.SqlServer)] + [InlineData(SqlProvider.Postgres)] + [InlineData(SqlProvider.Oracle)] + public void ProductionDialectGate_constructsEveryShippedProvider(SqlProvider provider) + => SqlDriverProbe.DefaultDialectForTest(provider).Provider.ShouldBe(provider); + + /// + /// The falsification control for the Theory above: the two reserved members must still be + /// rejected. Without this, widening the gate to "return a dialect for anything" would pass. + /// + [Theory] + [InlineData(SqlProvider.MySql)] + [InlineData(SqlProvider.Odbc)] + public void ProductionDialectGate_stillRejectsReservedProviders(SqlProvider provider) + => Should.Throw(() => SqlDriverProbe.DefaultDialectForTest(provider)) + .Message.ShouldContain(provider.ToString()); + + /// + /// End-to-end through the production constructor: a Postgres config must get PAST dialect + /// selection and fail at the connection attempt instead. Both outcomes are a red result whose message + /// names only an exception type, so the type is the discriminator — with the SqlServer-only gate this + /// is InvalidOperationException thrown before any socket is opened. + /// + [Fact] + public async Task ProductionProbe_postgresConfig_reachesTheConnectionAttempt_notTheProviderGate() + { + // Port 1 on the loopback: nothing listens, so the connect fails fast and locally — no fixture, no + // network egress, and a failure that is the PROVIDER's, which is the point of the assertion. + var name = SetRef("Host=127.0.0.1;Port=1;Database=x;Username=u;Password=p;Timeout=2"); + var result = await new SqlDriverProbe().ProbeAsync( + $$"""{"provider":"Postgres","connectionStringRef":"{{name}}"}""", + TimeSpan.FromSeconds(10), CancellationToken.None); + + result.Ok.ShouldBeFalse(); + result.Message.ShouldNotBeNull(); + result.Message!.ShouldNotContain(nameof(InvalidOperationException)); + } + // ---- helpers ---- /// A config blob whose connectionStringRef resolves (for the life of the returned scope) to the