Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/Commands/TemplateTriggerKindTests.cs
T
Joseph Doherty 0bd5e0986f fix(m9/T28b): forward --trigger-kind on alarm-update/script-add/script-update; fix default-selector test
Three dead-code bugs: --trigger-kind was registered but never read or forwarded on the
alarm-update, script-add, and script-update paths. Introduced TriggerConfigJson.InjectAnalysisKind
helper that rewrites any raw --trigger-config JSON blob, writing "analysisKind":"Strict" when
the flag is strict (case-insensitive) and stripping the key for any other value. Wired the
helper into all three handlers alongside the existing alarm-add path (which already used
AlarmTriggerConfigJson.Build). Added 6 unit tests for the new helper in TemplateTriggerKindTests.

Also fixed a false-positive bUnit test (AlarmTriggerEditor_Expression_NoAnalysisKindInConfig_
SelectorDefaultsAdvisory) that passed because "Advisory" appeared anywhere in the HTML; now
asserts select.GetAttribute("value") == "Advisory". Added the missing equivalent test for
ScriptTriggerEditor (ScriptTriggerEditor_Expression_NoAnalysisKindInConfig_SelectorDefaultsAdvisory).
2026-06-18 10:54:42 -04:00

231 lines
9.0 KiB
C#

using System.CommandLine;
using System.Text.Json;
using ZB.MOM.WW.ScadaBridge.CLI;
using ZB.MOM.WW.ScadaBridge.CLI.Commands;
namespace ZB.MOM.WW.ScadaBridge.CLI.Tests.Commands;
/// <summary>
/// M9-T28b: <c>template alarm add/update</c> and <c>template script add/update</c>
/// must expose a <c>--trigger-kind</c> option (advisory|strict) that writes
/// <c>"analysisKind":"Strict"</c> into the trigger-config JSON for expression
/// triggers, matching the T28a backend contract exactly.
/// </summary>
public class TemplateTriggerKindTests
{
// ── TriggerConfigJson.InjectAnalysisKind — the shared helper used by
// alarm-update / script-add / script-update ──────────────────────────────
// Inject strict into an Expression config that has no prior analysisKind
[Fact]
public void InjectAnalysisKind_Strict_AddsKey()
{
var json = "{\"expression\":\"x > 0\"}";
var result = TriggerConfigJson.InjectAnalysisKind(json, "strict");
Assert.NotNull(result);
using var doc = JsonDocument.Parse(result!);
Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString());
}
// Inject advisory (null) into config that had Strict → key removed
[Fact]
public void InjectAnalysisKind_Advisory_RemovesKey()
{
var json = "{\"expression\":\"x > 0\",\"analysisKind\":\"Strict\"}";
var result = TriggerConfigJson.InjectAnalysisKind(json, null);
Assert.NotNull(result);
using var doc = JsonDocument.Parse(result!);
Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
}
// Inject strict into config that already had Strict → still Strict (idempotent)
[Fact]
public void InjectAnalysisKind_Strict_Idempotent()
{
var json = "{\"expression\":\"x > 0\",\"analysisKind\":\"Strict\"}";
var result = TriggerConfigJson.InjectAnalysisKind(json, "strict");
Assert.NotNull(result);
using var doc = JsonDocument.Parse(result!);
Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString());
}
// Null config → returns null (no config to inject into)
[Fact]
public void InjectAnalysisKind_NullConfig_ReturnsNull()
{
var result = TriggerConfigJson.InjectAnalysisKind(null, "strict");
Assert.Null(result);
}
// Case-insensitive: "STRICT" → canonical "Strict"
[Fact]
public void InjectAnalysisKind_Strict_CaseInsensitive()
{
var json = "{\"expression\":\"x > 0\"}";
var result = TriggerConfigJson.InjectAnalysisKind(json, "STRICT");
Assert.NotNull(result);
using var doc = JsonDocument.Parse(result!);
Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString());
}
// Other keys preserved when injecting
[Fact]
public void InjectAnalysisKind_OtherKeysPreserved()
{
var json = "{\"expression\":\"x > 0\",\"mode\":\"OnTrue\"}";
var result = TriggerConfigJson.InjectAnalysisKind(json, "strict");
Assert.NotNull(result);
using var doc = JsonDocument.Parse(result!);
Assert.Equal("x > 0", doc.RootElement.GetProperty("expression").GetString());
Assert.Equal("OnTrue", doc.RootElement.GetProperty("mode").GetString());
Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString());
}
// ── Option surface: alarm add + update have --trigger-kind ───────────────
[Fact]
public void AlarmAdd_HasTriggerKindOption()
{
var add = AlarmGroup().Subcommands.Single(c => c.Name == "add");
Assert.Contains("--trigger-kind", add.Options.Select(o => o.Name));
}
[Fact]
public void AlarmUpdate_HasTriggerKindOption()
{
var update = AlarmGroup().Subcommands.Single(c => c.Name == "update");
Assert.Contains("--trigger-kind", update.Options.Select(o => o.Name));
}
// ── Option surface: script add + update have --trigger-kind ─────────────
[Fact]
public void ScriptAdd_HasTriggerKindOption()
{
var add = ScriptGroup().Subcommands.Single(c => c.Name == "add");
Assert.Contains("--trigger-kind", add.Options.Select(o => o.Name));
}
[Fact]
public void ScriptUpdate_HasTriggerKindOption()
{
var update = ScriptGroup().Subcommands.Single(c => c.Name == "update");
Assert.Contains("--trigger-kind", update.Options.Select(o => o.Name));
}
// ── AlarmTriggerConfigJson.Build — analysisKind injection ────────────────
[Fact]
public void AlarmConfigBuild_Expression_StrictKind_InjectsAnalysisKindStrict()
{
// --trigger-kind strict with --expression set → config carries analysisKind:"Strict"
var json = AlarmTriggerConfigJson.Build(
triggerType: "Expression",
attribute: null,
matchValue: null, notEquals: false,
min: null, max: null,
thresholdPerSecond: null, windowSeconds: null, direction: null,
loLo: null, lo: null, hi: null, hiHi: null,
expression: "Temp > 80",
analysisKind: "strict");
Assert.NotNull(json);
using var doc = JsonDocument.Parse(json!);
Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString());
}
[Fact]
public void AlarmConfigBuild_Expression_AdvisoryKind_OmitsAnalysisKindKey()
{
// --trigger-kind advisory (default) → key omitted
var json = AlarmTriggerConfigJson.Build(
triggerType: "Expression",
attribute: null,
matchValue: null, notEquals: false,
min: null, max: null,
thresholdPerSecond: null, windowSeconds: null, direction: null,
loLo: null, lo: null, hi: null, hiHi: null,
expression: "Temp > 80",
analysisKind: "advisory");
Assert.NotNull(json);
using var doc = JsonDocument.Parse(json!);
Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
}
[Fact]
public void AlarmConfigBuild_Expression_NullKind_OmitsAnalysisKindKey()
{
// Omitted --trigger-kind → null → Advisory default → key omitted
var json = AlarmTriggerConfigJson.Build(
triggerType: "Expression",
attribute: null,
matchValue: null, notEquals: false,
min: null, max: null,
thresholdPerSecond: null, windowSeconds: null, direction: null,
loLo: null, lo: null, hi: null, hiHi: null,
expression: "Temp > 80",
analysisKind: null);
Assert.NotNull(json);
using var doc = JsonDocument.Parse(json!);
Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
}
[Fact]
public void AlarmConfigBuild_NonExpression_StrictKind_DoesNotEmitAnalysisKind()
{
// analysisKind is meaningless for non-expression triggers — must be suppressed
var json = AlarmTriggerConfigJson.Build(
triggerType: "RangeViolation",
attribute: "Temp",
matchValue: null, notEquals: false,
min: 0, max: 100,
thresholdPerSecond: null, windowSeconds: null, direction: null,
loLo: null, lo: null, hi: null, hiHi: null,
expression: null,
analysisKind: "strict");
Assert.NotNull(json);
using var doc = JsonDocument.Parse(json!);
Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
}
[Fact]
public void AlarmConfigBuild_Expression_StrictKind_CaseInsensitive()
{
// "STRICT" (uppercase) must also produce the canonical "Strict" value
var json = AlarmTriggerConfigJson.Build(
triggerType: "Expression",
attribute: null,
matchValue: null, notEquals: false,
min: null, max: null,
thresholdPerSecond: null, windowSeconds: null, direction: null,
loLo: null, lo: null, hi: null, hiHi: null,
expression: "x > 0",
analysisKind: "STRICT");
Assert.NotNull(json);
using var doc = JsonDocument.Parse(json!);
Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString());
}
private static readonly Option<string> Url = new("--url") { Recursive = true };
private static readonly Option<string> Username = new("--username") { Recursive = true };
private static readonly Option<string> Password = new("--password") { Recursive = true };
private static readonly Option<string> Format = CliOptions.CreateFormatOption();
private static Command AlarmGroup()
=> TemplateCommands.Build(Url, Format, Username, Password)
.Subcommands.Single(c => c.Name == "alarm");
private static Command ScriptGroup()
=> TemplateCommands.Build(Url, Format, Username, Password)
.Subcommands.Single(c => c.Name == "script");
}