test(transport): harden T5 native-source coverage — no-change zero-audit + IsInherited/LockedInDerived fidelity

Follow-up from the T5 code review: assert an idempotent Overwrite emits zero
TemplateNativeAlarmSource* audit rows, and round-trip IsInherited/LockedInDerived
as true through both the serializer and the importer BuildTemplate path.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-09 17:05:02 -04:00
parent e4136cb920
commit 7b6d6bb805
2 changed files with 43 additions and 5 deletions
@@ -103,15 +103,17 @@ public sealed class NativeAlarmSourceImportTests : IDisposable
string connection = "OpcUaPrimary",
string sourceRef = "ns=3;s=Boiler.Alarm",
string? filter = "severity>500",
bool locked = false) => new(name)
bool locked = false,
bool inherited = false,
bool lockedInDerived = false) => new(name)
{
Description = "boiler native alarms",
ConnectionName = connection,
SourceReference = sourceRef,
ConditionFilter = filter,
IsLocked = locked,
IsInherited = false,
LockedInDerived = false,
IsInherited = inherited,
LockedInDerived = lockedInDerived,
};
[Fact]
@@ -121,7 +123,8 @@ public sealed class NativeAlarmSourceImportTests : IDisposable
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
var t = new Template("Pump") { Description = "tpl" };
t.NativeAlarmSources.Add(MakeSource(locked: true));
// Set every flag non-default so BuildTemplate's fidelity is exercised.
t.NativeAlarmSources.Add(MakeSource(locked: true, inherited: true, lockedInDerived: true));
ctx.Templates.Add(t);
await ctx.SaveChangesAsync();
}
@@ -155,6 +158,39 @@ public sealed class NativeAlarmSourceImportTests : IDisposable
Assert.Equal("ns=3;s=Boiler.Alarm", src.SourceReference);
Assert.Equal("severity>500", src.ConditionFilter);
Assert.True(src.IsLocked);
Assert.True(src.IsInherited);
Assert.True(src.LockedInDerived);
}
}
[Fact]
public async Task Import_Overwrite_NoChange_EmitsNoNativeSourceAudit()
{
// An idempotent Overwrite (bundle == target) must produce ZERO
// TemplateNativeAlarmSource* audit rows — the "no audit noise" invariant.
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
var t = new Template("Pump") { Description = "same" };
t.NativeAlarmSources.Add(MakeSource("Boiler", locked: true, lockedInDerived: true));
ctx.Templates.Add(t);
await ctx.SaveChangesAsync();
}
var sessionId = await ExportAllTemplatesAndLoadAsync();
await using (var scope = _provider.CreateAsyncScope())
{
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
await importer.ApplyAsync(sessionId,
new List<ImportResolution> { new("Template", "Pump", ResolutionAction.Overwrite, null) },
user: "bob");
}
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
Assert.False(await ctx.AuditLogEntries.AnyAsync(a =>
a.Action.StartsWith("TemplateNativeAlarmSource")));
}
}
@@ -215,7 +215,7 @@ public sealed class EntitySerializerTests
SourceReference = "ns=3;s=Boiler.Alarm",
ConditionFilter = "severity>500",
IsLocked = true,
IsInherited = false,
IsInherited = true,
LockedInDerived = true,
});
@@ -230,6 +230,7 @@ public sealed class EntitySerializerTests
Assert.Equal("ns=3;s=Boiler.Alarm", dtoSrc.SourceReference);
Assert.Equal("severity>500", dtoSrc.ConditionFilter);
Assert.True(dtoSrc.IsLocked);
Assert.True(dtoSrc.IsInherited);
Assert.True(dtoSrc.LockedInDerived);
var rtSrc = Assert.Single(Assert.Single(sut.FromBundleContent(dto).Templates).NativeAlarmSources);
@@ -239,6 +240,7 @@ public sealed class EntitySerializerTests
Assert.Equal("ns=3;s=Boiler.Alarm", rtSrc.SourceReference);
Assert.Equal("severity>500", rtSrc.ConditionFilter);
Assert.True(rtSrc.IsLocked);
Assert.True(rtSrc.IsInherited);
Assert.True(rtSrc.LockedInDerived);
}