using System.Text.Json; using System.Text.Json.Serialization; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Forms; using ZB.MOM.WW.OtOpcUa.Driver.Sql; using ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; /// /// Guards the AdminUI SqlDriverForm's driver-config output against the runtime factory that consumes /// it. The form is razor (no bUnit here), so these tests pin two things the live verify then confirms: /// (1) the exact JSON bytes the form emits for a minimal + fuller config are accepted by the authoritative /// reader, ; and (2) the /// serialization contract — provider as a NAME string (never an ordinal), camelCase keys, and no /// composer-owned rawTags or an unrequested allowWrites leaking into the authored blob. A numeric enum /// or a stray connection string is the "authors fine, never reads / leaks a secret" defect class this guards. /// public sealed class SqlDriverFormContractTests { // The JsonSerializerOptions SqlDriverForm serializes with (camelCase + string enums + omit-nulls). private static readonly JsonSerializerOptions FormOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = false, Converters = { new JsonStringEnumConverter() }, }; // ---- LOAD-BEARING: the exact bytes SqlDriverForm emits must construct a driver through the factory ---- [Theory] // Minimal valid config: provider + required connectionStringRef only (all timeouts left to the factory). [InlineData("""{"provider":"SqlServer","connectionStringRef":"AdminUiFormTest"}""")] // Fuller config with every optional the form authors (timeouts satisfy operationTimeout > commandTimeout). [InlineData("""{"provider":"SqlServer","connectionStringRef":"AdminUiFormTest","defaultPollInterval":"00:00:05","operationTimeout":"00:00:20","commandTimeout":"00:00:10","maxConcurrentGroups":8,"nullIsBad":true}""")] public void FormEmittedJson_constructs_a_driver_through_the_factory(string formEmittedJson) { var envVar = SqlConnectionStringResolver.EnvironmentVariableFor("AdminUiFormTest"); var previous = Environment.GetEnvironmentVariable(envVar); Environment.SetEnvironmentVariable(envVar, "Server=localhost;Database=Test;Trusted_Connection=True;"); try { var driver = SqlDriverFactoryExtensions.CreateInstance("adminui-form-test", formEmittedJson); driver.ShouldNotBeNull(); } finally { Environment.SetEnvironmentVariable(envVar, previous); } } [Fact] public void Factory_rejects_a_blob_missing_connectionStringRef() { // The form marks connectionStringRef required; the factory is the authoritative enforcement. var ex = Should.Throw( () => SqlDriverFactoryExtensions.CreateInstance("adminui-form-test", """{"provider":"SqlServer"}""")); ex.Message.ShouldContain("connectionStringRef"); } // ---- serialization contract: enum-by-name, camelCase, no rawTags / allowWrites / null optionals -------- [Fact] public void Minimal_config_serializes_provider_as_a_name_and_omits_owned_and_null_fields() { // Mirrors SqlDriverForm.FormModel.ToDto() for a minimal config (only the required ref set). var dto = new SqlDriverConfigDto { Provider = SqlProvider.SqlServer, ConnectionStringRef = "MesStaging" }; var json = JsonSerializer.Serialize(dto, FormOptions); json.ShouldBe("""{"provider":"SqlServer","connectionStringRef":"MesStaging"}"""); json.ShouldNotContain("\"provider\":0"); // never an ordinal — that faults the string-typed factory read json.ShouldNotContain("rawTags"); // composer-owned; never authored by the form json.ShouldNotContain("allowWrites"); // off ⇒ omitted ⇒ read-only json.ShouldNotContain("defaultPollInterval"); // null optional omitted ⇒ factory default applies } [Fact] public void Provider_round_trips_as_the_SqlServer_name() { var json = JsonSerializer.Serialize( new SqlDriverConfigDto { Provider = SqlProvider.SqlServer, ConnectionStringRef = "R" }, FormOptions); json.ShouldContain("\"SqlServer\""); JsonSerializer.Deserialize(json, FormOptions)!.Provider.ShouldBe(SqlProvider.SqlServer); } // ---- the multi-provider dropdown: every offered option survives the form model's own round-trip ---- /// /// The dropdown offers SqlServer + Postgres + Oracle, so each must survive /// FormModel.ToDto() → JSON → FormModel.FromDto() as a NAME string. The razor markup /// itself is not reachable (no bUnit), so this pins the model half of the change: a provider the /// dropdown can select but the form model cannot carry back would silently reset to SqlServer on /// the next modal open. /// [Theory] [InlineData(SqlProvider.SqlServer, "SqlServer")] [InlineData(SqlProvider.Postgres, "Postgres")] [InlineData(SqlProvider.Oracle, "Oracle")] [InlineData(SqlProvider.MySql, "MySql")] [InlineData(SqlProvider.Odbc, "Odbc")] public void Every_dropdown_provider_round_trips_through_the_form_model(SqlProvider provider, string name) { var form = new SqlDriverForm.FormModel { Provider = provider, ConnectionStringRef = "MesStaging" }; var json = JsonSerializer.Serialize(form.ToDto(), FormOptions); json.ShouldContain($"\"provider\":\"{name}\""); json.ShouldNotContain($"\"provider\":{(int)provider}"); // never an ordinal — that faults the factory read var reloaded = SqlDriverForm.FormModel.FromDto( JsonSerializer.Deserialize(json, FormOptions)!); reloaded.Provider.ShouldBe(provider); reloaded.ConnectionStringRef.ShouldBe("MesStaging"); } /// /// The dropdown offers every provider the build constructs (lmxopcua#753 made all five enum members real), /// so the form-model theory above must cover exactly — a provider added /// to the map without a dropdown option (or the reverse) has to argue with this test. /// [Fact] public void Every_constructed_provider_is_covered_by_the_dropdown_theory() => SqlDialects.Supported.ShouldBe( [SqlProvider.SqlServer, SqlProvider.Postgres, SqlProvider.MySql, SqlProvider.Odbc, SqlProvider.Oracle], ignoreOrder: true); // ---- named queries (lmxopcua#753): the form refuses exactly what the factory refuses ---- /// /// The driver form's queries editor consults the runtime's own rules /// (), so what the form keeps is what the factory constructs and /// what it refuses the factory refuses too. Driven through the real factory entry point, so a rule added /// on one side only breaks this test. /// [Theory] [InlineData("""{"q":{"sql":"SELECT a FROM t WHERE x = @x","params":{"x":1}}}""", true)] [InlineData("""{"q":{"sql":"WITH l AS (SELECT a FROM t) SELECT a FROM l;","maxRows":10}}""", true)] [InlineData("""{"q":{"sql":"UPDATE t SET a = 1"}}""", false)] [InlineData("""{"q":{"sql":"SELECT a INTO t2 FROM t"}}""", false)] [InlineData("""{"q":{"sql":"SELECT 1; SELECT 2"}}""", false)] [InlineData("""{"q":{"sql":"SELECT a FROM t","maxRows":0}}""", false)] [InlineData("""{"q":{"sql":"SELECT a FROM t WHERE x = @x","params":{"x":{"nested":1}}}}""", false)] [InlineData("""{"q":{"sql":""}}""", false)] public void Form_queries_editor_and_factory_agree(string queriesJson, bool accepted) { var formError = SqlDriverForm.FormModel.TryParseQueries(queriesJson, out _); (formError is null).ShouldBe(accepted, formError); var envVar = SqlConnectionStringResolver.EnvironmentVariableFor("AdminUiFormTest"); var previous = Environment.GetEnvironmentVariable(envVar); Environment.SetEnvironmentVariable(envVar, "Server=localhost;Database=Test;Trusted_Connection=True;"); try { var blob = $$"""{"provider":"SqlServer","connectionStringRef":"AdminUiFormTest","queries":{{queriesJson}}}"""; if (accepted) { SqlDriverFactoryExtensions.CreateInstance("adminui-form-test", blob).ShouldNotBeNull(); } else { Should.Throw( () => SqlDriverFactoryExtensions.CreateInstance("adminui-form-test", blob)); } } finally { Environment.SetEnvironmentVariable(envVar, previous); } } [Fact] public void AllowWrites_is_emitted_only_when_switched_on_and_the_factory_honours_it() { var off = JsonSerializer.Serialize(new SqlDriverForm.FormModel { ConnectionStringRef = "R" }.ToDto(), FormOptions); off.ShouldNotContain("allowWrites"); var form = new SqlDriverForm.FormModel { ConnectionStringRef = "AdminUiFormTest", AllowWrites = true }; var on = JsonSerializer.Serialize(form.ToDto(), FormOptions); on.ShouldContain("\"allowWrites\":true"); SqlDriverForm.FormModel.FromDto(JsonSerializer.Deserialize(on, FormOptions)!) .AllowWrites.ShouldBeTrue(); var envVar = SqlConnectionStringResolver.EnvironmentVariableFor("AdminUiFormTest"); var previous = Environment.GetEnvironmentVariable(envVar); Environment.SetEnvironmentVariable(envVar, "Server=localhost;Database=Test;Trusted_Connection=True;"); try { SqlDriverFactoryExtensions.CreateInstance("adminui-form-test", on) .ShouldBeAssignableTo(); } finally { Environment.SetEnvironmentVariable(envVar, previous); } } [Fact] public void Queries_survive_the_form_model_round_trip_and_a_typo_keeps_the_last_valid_set() { var dto = JsonSerializer.Deserialize( """{"provider":"SqlServer","connectionStringRef":"R","queries":{"q":{"sql":"SELECT a FROM t"}}}""", FormOptions)!; var form = SqlDriverForm.FormModel.FromDto(dto); form.SetQueriesJson("{ not json"); form.QueriesError.ShouldNotBeNull(); var json = JsonSerializer.Serialize(form.ToDto(), FormOptions); json.ShouldContain("\"queries\":{\"q\":{\"sql\":\"SELECT a FROM t\"}}"); } }