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