Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/NativeAlarmSourceOverrideCsvParserTests.cs
T
Joseph Doherty f480a56737 feat(instance): native-alarm-source-override CSV bulk import (deferred #12) + doc fixes
Closes the operator parity gap the deferred-work register tracked as #12: native
alarm source overrides could only be set one-at-a-time, while attribute overrides
had a CSV bulk path. Adds an all-or-nothing CSV import for native sources.

- Commons: extract the RFC-4180 line splitter into a shared CsvLineSplitter
  (refactor OverrideCsvParser onto it — no behavior change, pinned by its tests);
  new NativeAlarmSourceOverrideCsvParser (header SourceName,Connection,
  SourceReference,Filter; blank = inherited).
- Commons: NativeAlarmSourceOverrideEntry + bulk SetInstanceNativeAlarmSource-
  OverridesCommand (auto-registered via the reflection command registry).
- ManagementService: Deployer-gated handler — flattens once, validates every
  source resolves + is unlocked + no duplicates up front, then upserts the whole
  batch under a single SaveChanges (true all-or-nothing txn). Added to the frozen
  authorization matrix; dispatch-coverage guard passes.
- CLI: `instance native-alarm-source import --instance-id --file` (parity with
  `instance import-overrides`) + README.
- Tests: native parser (Commons), CLI parse/entry mapping, 3 bulk-handler tests
  (happy path single-commit, locked-source reject, unresolved-source reject).

Also corrects a stale XML-doc line in ScriptRuntimeContext (WaitForAttribute
quality-gated mode is shipped, not "planned") and updates the deferred-work
register: marks #7/#13/#15/#16/#20 verified-resolved, #12 as CLI/API-shipped with
only the Central UI upload affordance still pending.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 08:52:12 -04:00

149 lines
4.6 KiB
C#

using ZB.MOM.WW.ScadaBridge.Commons.Types;
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests;
public class NativeAlarmSourceOverrideCsvParserTests
{
[Fact]
public void Parse_SimpleFile_ReturnsRowsNoErrors()
{
const string csv =
"SourceName,Connection,SourceReference,Filter\n" +
"Pressure,ConnA,ns=2;s=Pump.Alarm,Active\n" +
"Module.Temp,ConnB,ns=2;s=Temp,\n";
var result = NativeAlarmSourceOverrideCsvParser.Parse(csv);
Assert.Empty(result.Errors);
Assert.Equal(2, result.Rows.Count);
Assert.Equal("Pressure", result.Rows[0].SourceName);
Assert.Equal("ConnA", result.Rows[0].Connection);
Assert.Equal("ns=2;s=Pump.Alarm", result.Rows[0].SourceReference);
Assert.Equal("Active", result.Rows[0].Filter);
Assert.Equal(2, result.Rows[0].LineNumber);
Assert.Equal("Module.Temp", result.Rows[1].SourceName);
Assert.Equal("ConnB", result.Rows[1].Connection);
Assert.Equal("ns=2;s=Temp", result.Rows[1].SourceReference);
Assert.Null(result.Rows[1].Filter);
Assert.Equal(3, result.Rows[1].LineNumber);
}
[Fact]
public void Parse_BlankOverrideFields_BecomeNullInherited()
{
const string csv =
"SourceName,Connection,SourceReference,Filter\n" +
"Pressure,,,\n";
var result = NativeAlarmSourceOverrideCsvParser.Parse(csv);
Assert.Empty(result.Errors);
var row = Assert.Single(result.Rows);
Assert.Equal("Pressure", row.SourceName);
Assert.Null(row.Connection);
Assert.Null(row.SourceReference);
Assert.Null(row.Filter);
}
[Fact]
public void Parse_HeaderCaseInsensitive_Accepted()
{
const string csv =
"sourcename,CONNECTION,SourceReference,filter\n" +
"Pressure,ConnA,ref,\n";
var result = NativeAlarmSourceOverrideCsvParser.Parse(csv);
Assert.Empty(result.Errors);
Assert.Single(result.Rows);
}
[Fact]
public void Parse_QuotedFieldWithComma_PreservesEmbeddedComma()
{
const string csv =
"SourceName,Connection,SourceReference,Filter\n" +
"Pressure,ConnA,\"ns=2;s=a,b,c\",Active\n";
var result = NativeAlarmSourceOverrideCsvParser.Parse(csv);
Assert.Empty(result.Errors);
var row = Assert.Single(result.Rows);
Assert.Equal("ns=2;s=a,b,c", row.SourceReference);
}
[Fact]
public void Parse_MissingHeader_ReturnsHeaderError()
{
const string csv = "Pressure,ConnA,ref,Active\n";
var result = NativeAlarmSourceOverrideCsvParser.Parse(csv);
Assert.Empty(result.Rows);
var error = Assert.Single(result.Errors);
Assert.Contains("SourceName,Connection,SourceReference,Filter", error);
}
[Fact]
public void Parse_WrongColumnCount_ReportsPerLineErrorAndSkipsRow()
{
const string csv =
"SourceName,Connection,SourceReference,Filter\n" +
"Pressure,ConnA\n" +
"Temp,ConnB,ref,Active\n";
var result = NativeAlarmSourceOverrideCsvParser.Parse(csv);
var error = Assert.Single(result.Errors);
Assert.Contains("Line 2", error);
Assert.Contains("expected 4 columns", error);
// Valid rows still flow through.
var row = Assert.Single(result.Rows);
Assert.Equal("Temp", row.SourceName);
}
[Fact]
public void Parse_BlankSourceName_ReportsError()
{
const string csv =
"SourceName,Connection,SourceReference,Filter\n" +
",ConnA,ref,Active\n";
var result = NativeAlarmSourceOverrideCsvParser.Parse(csv);
Assert.Empty(result.Rows);
var error = Assert.Single(result.Errors);
Assert.Contains("SourceName must not be blank", error);
}
[Fact]
public void Parse_UnterminatedQuote_ReportsError()
{
const string csv =
"SourceName,Connection,SourceReference,Filter\n" +
"Pressure,ConnA,\"ns=2;s=open,Active\n";
var result = NativeAlarmSourceOverrideCsvParser.Parse(csv);
Assert.Empty(result.Rows);
Assert.Contains(result.Errors, e => e.Contains("Unterminated quoted field"));
}
[Fact]
public void Parse_BlankLinesSkippedWithoutError()
{
const string csv =
"SourceName,Connection,SourceReference,Filter\n" +
"\n" +
"Pressure,ConnA,ref,Active\n" +
" \n";
var result = NativeAlarmSourceOverrideCsvParser.Parse(csv);
Assert.Empty(result.Errors);
Assert.Single(result.Rows);
}
}