From 79fe098886ed4d4f3c47ad2b0b2bef91e6ed9fd0 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 19 Jun 2026 03:06:10 -0400 Subject: [PATCH] =?UTF-8?q?test(transport):=20regression=20=E2=80=94=20emp?= =?UTF-8?q?ty-ConnectionName=20binding=20skipped=20on=20import=20(#229)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Import/SiteInstanceImportTests.cs | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/Import/SiteInstanceImportTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/Import/SiteInstanceImportTests.cs index aedbf179..5a99803f 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/Import/SiteInstanceImportTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/Import/SiteInstanceImportTests.cs @@ -1109,4 +1109,117 @@ public sealed class SiteInstanceImportTests : IDisposable Assert.Equal(1, result.Overwritten); Assert.Equal(3, result.Skipped); } + + // ────────────────────────────────────────────────────────────────────── + // #229 — empty-ConnectionName binding is silently SKIPPED (not written) + // ────────────────────────────────────────────────────────────────────── + + [Fact] + public async Task ApplyAsync_empty_ConnectionName_binding_is_skipped_not_written() + { + // Regression guard for commit 3782ebda: an InstanceConnectionBindingDto + // whose ConnectionName is empty represents an orphan binding — the source + // connection FK was excluded from the export. Before the fix the importer + // wrote DataConnectionId = 0 (an invalid FK, FK-violating at commit on a + // relational provider). After the fix the binding row is silently skipped. + // Assert: (a) the import SUCCEEDS (no exception), (b) NO binding row is + // written, and (c) a non-empty binding in the SAME instance IS written so + // we know the loop executed and only the empty-name entry was dropped. + int targetConnId; + await using (var scope = _provider.CreateAsyncScope()) + { + var ctx = scope.ServiceProvider.GetRequiredService(); + ctx.Templates.Add(new Template("Pump") { Description = "pump tpl" }); + var site = new Site("Plant 1", "plant-1"); + ctx.Sites.Add(site); + await ctx.SaveChangesAsync(); + var conn = new DataConnection("OpcUaPrimary", "OpcUa", site.Id) + { + PrimaryConfiguration = "{\"endpoint\":\"opc.tcp://primary\"}", + }; + ctx.DataConnections.Add(conn); + await ctx.SaveChangesAsync(); + targetConnId = conn.Id; + } + + // Hand-pack a bundle that carries two bindings for the same instance: + // - "Flow" → ConnectionName = "" (orphan / empty — must be SKIPPED) + // - "Status" → ConnectionName = "OpcUaPrimary" (valid — must be WRITTEN) + var content = new BundleContentDto( + TemplateFolders: Array.Empty(), + Templates: Array.Empty(), + SharedScripts: Array.Empty(), + ExternalSystems: Array.Empty(), + DatabaseConnections: Array.Empty(), + NotificationLists: Array.Empty(), + SmtpConfigs: Array.Empty(), + ApiMethods: Array.Empty()) + { + Sites = new[] + { + new SiteDto("plant-1", "Plant 1", null, null, null, null, null), + }, + DataConnections = Array.Empty(), + Instances = new[] + { + new InstanceDto( + UniqueName: "Pump-01", + TemplateName: "Pump", + SiteIdentifier: "plant-1", + AreaName: null, + State: InstanceState.Enabled, + AttributeOverrides: Array.Empty(), + AlarmOverrides: Array.Empty(), + NativeAlarmSourceOverrides: Array.Empty(), + ConnectionBindings: new[] + { + // Empty ConnectionName — orphan binding; must be SKIPPED. + new InstanceConnectionBindingDto( + AttributeName: "Flow", + ConnectionName: "", + DataSourceReferenceOverride: "ns=3;s=Pump.Flow"), + // Valid ConnectionName — must be written with the correct FK. + new InstanceConnectionBindingDto( + AttributeName: "Status", + ConnectionName: "OpcUaPrimary", + DataSourceReferenceOverride: "ns=3;s=Pump.Status"), + }), + }, + }; + var sessionId = await PackAndLoadAsync(content); + + var nameMap = new BundleNameMap( + Sites: new[] { new SiteMapping("plant-1", MappingAction.MapToExisting, "plant-1") }, + Connections: Array.Empty()); + + // (a) Import must SUCCEED — no exception thrown. + var result = await ApplyAsync( + sessionId, + new List + { + new("Site", "plant-1", ResolutionAction.Skip, null), + new("Instance", "Pump-01", ResolutionAction.Add, null), + }, + nameMap); + + await using (var scope = _provider.CreateAsyncScope()) + { + var ctx = scope.ServiceProvider.GetRequiredService(); + + var inst = await ctx.Instances + .Include(i => i.ConnectionBindings) + .SingleAsync(i => i.UniqueName == "Pump-01"); + + // (b) The empty-ConnectionName binding for "Flow" must NOT be written. + Assert.DoesNotContain(inst.ConnectionBindings, b => b.AttributeName == "Flow"); + + // (c) The valid "Status" binding was written with the correct FK — proves + // the loop ran and it was only the empty-name entry that was dropped. + var statusBinding = Assert.Single(inst.ConnectionBindings, b => b.AttributeName == "Status"); + Assert.Equal(targetConnId, statusBinding.DataConnectionId); + Assert.NotEqual(0, statusBinding.DataConnectionId); + } + + Assert.Equal(1, result.Added); + } }