test(transport): regression — empty-ConnectionName binding skipped on import (#229)
This commit is contained in:
+113
@@ -1109,4 +1109,117 @@ public sealed class SiteInstanceImportTests : IDisposable
|
|||||||
Assert.Equal(1, result.Overwritten);
|
Assert.Equal(1, result.Overwritten);
|
||||||
Assert.Equal(3, result.Skipped);
|
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<ScadaBridgeDbContext>();
|
||||||
|
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<TemplateFolderDto>(),
|
||||||
|
Templates: Array.Empty<TemplateDto>(),
|
||||||
|
SharedScripts: Array.Empty<SharedScriptDto>(),
|
||||||
|
ExternalSystems: Array.Empty<ExternalSystemDto>(),
|
||||||
|
DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
|
||||||
|
NotificationLists: Array.Empty<NotificationListDto>(),
|
||||||
|
SmtpConfigs: Array.Empty<SmtpConfigDto>(),
|
||||||
|
ApiMethods: Array.Empty<ApiMethodDto>())
|
||||||
|
{
|
||||||
|
Sites = new[]
|
||||||
|
{
|
||||||
|
new SiteDto("plant-1", "Plant 1", null, null, null, null, null),
|
||||||
|
},
|
||||||
|
DataConnections = Array.Empty<DataConnectionDto>(),
|
||||||
|
Instances = new[]
|
||||||
|
{
|
||||||
|
new InstanceDto(
|
||||||
|
UniqueName: "Pump-01",
|
||||||
|
TemplateName: "Pump",
|
||||||
|
SiteIdentifier: "plant-1",
|
||||||
|
AreaName: null,
|
||||||
|
State: InstanceState.Enabled,
|
||||||
|
AttributeOverrides: Array.Empty<InstanceAttributeOverrideDto>(),
|
||||||
|
AlarmOverrides: Array.Empty<InstanceAlarmOverrideDto>(),
|
||||||
|
NativeAlarmSourceOverrides: Array.Empty<InstanceNativeAlarmSourceOverrideDto>(),
|
||||||
|
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<ConnectionMapping>());
|
||||||
|
|
||||||
|
// (a) Import must SUCCEED — no exception thrown.
|
||||||
|
var result = await ApplyAsync(
|
||||||
|
sessionId,
|
||||||
|
new List<ImportResolution>
|
||||||
|
{
|
||||||
|
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<ScadaBridgeDbContext>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user