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 connection = "OpcUaPrimary",
string sourceRef = "ns=3;s=Boiler.Alarm", string sourceRef = "ns=3;s=Boiler.Alarm",
string? filter = "severity>500", string? filter = "severity>500",
bool locked = false) => new(name) bool locked = false,
bool inherited = false,
bool lockedInDerived = false) => new(name)
{ {
Description = "boiler native alarms", Description = "boiler native alarms",
ConnectionName = connection, ConnectionName = connection,
SourceReference = sourceRef, SourceReference = sourceRef,
ConditionFilter = filter, ConditionFilter = filter,
IsLocked = locked, IsLocked = locked,
IsInherited = false, IsInherited = inherited,
LockedInDerived = false, LockedInDerived = lockedInDerived,
}; };
[Fact] [Fact]
@@ -121,7 +123,8 @@ public sealed class NativeAlarmSourceImportTests : IDisposable
{ {
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>(); var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
var t = new Template("Pump") { Description = "tpl" }; 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); ctx.Templates.Add(t);
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
} }
@@ -155,6 +158,39 @@ public sealed class NativeAlarmSourceImportTests : IDisposable
Assert.Equal("ns=3;s=Boiler.Alarm", src.SourceReference); Assert.Equal("ns=3;s=Boiler.Alarm", src.SourceReference);
Assert.Equal("severity>500", src.ConditionFilter); Assert.Equal("severity>500", src.ConditionFilter);
Assert.True(src.IsLocked); 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", SourceReference = "ns=3;s=Boiler.Alarm",
ConditionFilter = "severity>500", ConditionFilter = "severity>500",
IsLocked = true, IsLocked = true,
IsInherited = false, IsInherited = true,
LockedInDerived = true, LockedInDerived = true,
}); });
@@ -230,6 +230,7 @@ public sealed class EntitySerializerTests
Assert.Equal("ns=3;s=Boiler.Alarm", dtoSrc.SourceReference); Assert.Equal("ns=3;s=Boiler.Alarm", dtoSrc.SourceReference);
Assert.Equal("severity>500", dtoSrc.ConditionFilter); Assert.Equal("severity>500", dtoSrc.ConditionFilter);
Assert.True(dtoSrc.IsLocked); Assert.True(dtoSrc.IsLocked);
Assert.True(dtoSrc.IsInherited);
Assert.True(dtoSrc.LockedInDerived); Assert.True(dtoSrc.LockedInDerived);
var rtSrc = Assert.Single(Assert.Single(sut.FromBundleContent(dto).Templates).NativeAlarmSources); 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("ns=3;s=Boiler.Alarm", rtSrc.SourceReference);
Assert.Equal("severity>500", rtSrc.ConditionFilter); Assert.Equal("severity>500", rtSrc.ConditionFilter);
Assert.True(rtSrc.IsLocked); Assert.True(rtSrc.IsLocked);
Assert.True(rtSrc.IsInherited);
Assert.True(rtSrc.LockedInDerived); Assert.True(rtSrc.LockedInDerived);
} }