Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/TriggerAnalysisKindTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

414 lines
18 KiB
C#

using System.Text.Json;
using Bunit;
using Bunit.JSInterop;
using Microsoft.AspNetCore.Components;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;
/// <summary>
/// The <c>AlarmTriggerEditor</c> and <c>ScriptTriggerEditor</c>
/// components must surface an Advisory|Strict selector for Expression triggers
/// and round-trip the <c>"analysisKind"</c> key into the trigger config JSON
/// that the ValidationService backend reads.
/// </summary>
// ── AlarmTriggerConfigCodec — analysisKind parse / serialize ────────────────
public class AlarmTriggerAnalysisKindCodecTests
{
// Codec: parse — absent key defaults to Advisory (false)
/// <summary>Verifies that parsing an Expression trigger with no analysis kind defaults to Advisory.</summary>
[Fact]
public void Parse_Expression_NoAnalysisKind_DefaultsToAdvisory()
{
var json = @"{""expression"":""Attributes[""Temp""] > 50""}";
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.Expression);
Assert.False(model.IsStrictAnalysisKind);
}
// Codec: parse — "Advisory" explicit → false
/// <summary>Verifies that parsing an Expression trigger with the Advisory key returns false.</summary>
[Fact]
public void Parse_Expression_AdvisoryKey_ReturnsFalse()
{
var json = @"{""expression"":""x > 0"",""analysisKind"":""Advisory""}";
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.Expression);
Assert.False(model.IsStrictAnalysisKind);
}
// Codec: parse — "Strict" → true
/// <summary>Verifies that parsing an Expression trigger with the Strict key returns true.</summary>
[Fact]
public void Parse_Expression_StrictKey_ReturnsTrue()
{
var json = @"{""expression"":""x > 0"",""analysisKind"":""Strict""}";
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.Expression);
Assert.True(model.IsStrictAnalysisKind);
}
// Codec: parse — case-insensitive
/// <summary>Verifies that the Strict key is parsed case-insensitively.</summary>
[Fact]
public void Parse_Expression_StrictKeyCaseInsensitive()
{
var json = @"{""expression"":""x > 0"",""analysisKind"":""strict""}";
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.Expression);
Assert.True(model.IsStrictAnalysisKind);
}
// Codec: serialize — Advisory (false) → "analysisKind" key omitted
/// <summary>Verifies that serializing an Advisory Expression trigger omits the analysisKind key.</summary>
[Fact]
public void Serialize_Expression_Advisory_OmitsAnalysisKindKey()
{
var model = new AlarmTriggerModel { Expression = "x > 0", IsStrictAnalysisKind = false };
var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.Expression);
using var doc = JsonDocument.Parse(json);
Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
}
// Codec: serialize — Strict (true) → "analysisKind":"Strict"
/// <summary>Verifies that serializing a Strict Expression trigger writes analysisKind as Strict.</summary>
[Fact]
public void Serialize_Expression_Strict_WritesAnalysisKindStrict()
{
var model = new AlarmTriggerModel { Expression = "x > 0", IsStrictAnalysisKind = true };
var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.Expression);
using var doc = JsonDocument.Parse(json);
Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString());
}
// Codec: serialize — Strict not emitted for non-Expression trigger types
/// <summary>Verifies that serializing a non-Expression trigger does not emit analysisKind.</summary>
[Fact]
public void Serialize_NonExpression_DoesNotEmitAnalysisKind()
{
var model = new AlarmTriggerModel
{
AttributeName = "Temp",
Min = 0,
Max = 100,
IsStrictAnalysisKind = true // should be ignored for non-Expression
};
var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.RangeViolation);
using var doc = JsonDocument.Parse(json);
Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
}
// Codec: round-trip Strict
/// <summary>Verifies that a Strict Expression trigger is preserved through a round trip.</summary>
[Fact]
public void RoundTrip_Expression_Strict_Preserved()
{
var original = new AlarmTriggerModel { Expression = "Temp > 80", IsStrictAnalysisKind = true };
var json = AlarmTriggerConfigCodec.Serialize(original, AlarmTriggerType.Expression);
var round = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.Expression);
Assert.True(round.IsStrictAnalysisKind);
Assert.Equal("Temp > 80", round.Expression);
}
// Codec: round-trip Advisory (omit + re-parse → still false)
/// <summary>Verifies that an Advisory Expression trigger is preserved through a round trip.</summary>
[Fact]
public void RoundTrip_Expression_Advisory_Preserved()
{
var original = new AlarmTriggerModel { Expression = "Temp > 80", IsStrictAnalysisKind = false };
var json = AlarmTriggerConfigCodec.Serialize(original, AlarmTriggerType.Expression);
var round = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.Expression);
Assert.False(round.IsStrictAnalysisKind);
}
}
// ── ScriptTriggerConfigCodec — analysisKind parse / serialize ───────────────
public class ScriptTriggerAnalysisKindCodecTests
{
// Codec: parse — absent key defaults to Advisory (false)
/// <summary>Verifies that parsing an Expression trigger with no analysis kind defaults to Advisory.</summary>
[Fact]
public void Parse_Expression_NoAnalysisKind_DefaultsToAdvisory()
{
var json = @"{""expression"":""x > 0"",""mode"":""OnTrue""}";
var model = ScriptTriggerConfigCodec.Parse(json, ScriptTriggerKind.Expression);
Assert.False(model.IsStrictAnalysisKind);
}
// Codec: parse — "Strict" → true
/// <summary>Verifies that parsing an Expression trigger with the Strict key returns true.</summary>
[Fact]
public void Parse_Expression_StrictKey_ReturnsTrue()
{
var json = @"{""expression"":""x > 0"",""mode"":""OnTrue"",""analysisKind"":""Strict""}";
var model = ScriptTriggerConfigCodec.Parse(json, ScriptTriggerKind.Expression);
Assert.True(model.IsStrictAnalysisKind);
}
// Codec: serialize — Advisory → key omitted
/// <summary>Verifies that serializing an Advisory Expression trigger omits the analysisKind key.</summary>
[Fact]
public void Serialize_Expression_Advisory_OmitsAnalysisKindKey()
{
var model = new ScriptTriggerModel { Expression = "x > 0", IsStrictAnalysisKind = false };
var json = ScriptTriggerConfigCodec.Serialize(model, ScriptTriggerKind.Expression)!;
using var doc = JsonDocument.Parse(json);
Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
}
// Codec: serialize — Strict → "analysisKind":"Strict"
/// <summary>Verifies that serializing a Strict Expression trigger writes analysisKind as Strict.</summary>
[Fact]
public void Serialize_Expression_Strict_WritesAnalysisKindStrict()
{
var model = new ScriptTriggerModel { Expression = "x > 0", IsStrictAnalysisKind = true };
var json = ScriptTriggerConfigCodec.Serialize(model, ScriptTriggerKind.Expression)!;
using var doc = JsonDocument.Parse(json);
Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString());
}
// Codec: serialize — Strict not emitted for non-Expression triggers
/// <summary>Verifies that serializing a non-Expression trigger does not emit analysisKind.</summary>
[Fact]
public void Serialize_NonExpression_DoesNotEmitAnalysisKind()
{
var model = new ScriptTriggerModel
{
AttributeName = "Temp",
Operator = ">",
Threshold = 50,
IsStrictAnalysisKind = true // must be ignored for Conditional
};
var json = ScriptTriggerConfigCodec.Serialize(model, ScriptTriggerKind.Conditional)!;
using var doc = JsonDocument.Parse(json);
Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
}
// Codec: round-trip Strict
/// <summary>Verifies that a Strict Expression trigger is preserved through a round trip.</summary>
[Fact]
public void RoundTrip_Expression_Strict_Preserved()
{
var original = new ScriptTriggerModel
{
Expression = "Temp > 80",
Mode = ScriptTriggerMode.WhileTrue,
IsStrictAnalysisKind = true
};
var json = ScriptTriggerConfigCodec.Serialize(original, ScriptTriggerKind.Expression)!;
var round = ScriptTriggerConfigCodec.Parse(json, ScriptTriggerKind.Expression);
Assert.True(round.IsStrictAnalysisKind);
Assert.Equal(ScriptTriggerMode.WhileTrue, round.Mode);
}
}
// ── AlarmTriggerEditor — UI selector (bUnit component tests) ─────────────────
public class AlarmTriggerEditorAnalysisKindTests : BunitContext
{
/// <summary>Initializes the test context for the alarm trigger editor.</summary>
public AlarmTriggerEditorAnalysisKindTests()
{
// MonacoEditor calls MonacoBlazor.createEditor via JS interop. Use Loose
// mode so the editor renders without requiring a full setup for each call.
JSInterop.Mode = JSRuntimeMode.Loose;
}
// Selector is visible for Expression trigger type
/// <summary>Verifies that the alarm trigger editor shows the analysis kind selector for an Expression trigger.</summary>
[Fact]
public void AlarmTriggerEditor_Expression_ShowsAnalysisKindSelector()
{
var cut = Render<AlarmTriggerEditor>(ps => ps
.Add(p => p.TriggerType, AlarmTriggerType.Expression)
.Add(p => p.Value, @"{""expression"":""""}"));
// The selector must exist and have id="alarm-trigger-kind"
var select = cut.Find("#alarm-trigger-kind");
Assert.NotNull(select);
}
// Selector is NOT visible for non-Expression trigger types
/// <summary>Verifies that the alarm trigger editor does not show the analysis kind selector for a non-Expression trigger.</summary>
[Fact]
public void AlarmTriggerEditor_NonExpression_DoesNotShowAnalysisKindSelector()
{
var cut = Render<AlarmTriggerEditor>(ps => ps
.Add(p => p.TriggerType, AlarmTriggerType.RangeViolation)
.Add(p => p.Value, @"{""attributeName"":""Temp"",""min"":0,""max"":100}"));
Assert.Throws<Bunit.ElementNotFoundException>(() => cut.Find("#alarm-trigger-kind"));
}
// Selector defaults to Advisory
/// <summary>Verifies that the alarm trigger editor selector defaults to Advisory for an Expression trigger.</summary>
[Fact]
public void AlarmTriggerEditor_Expression_NoAnalysisKindInConfig_SelectorDefaultsAdvisory()
{
var cut = Render<AlarmTriggerEditor>(ps => ps
.Add(p => p.TriggerType, AlarmTriggerType.Expression)
.Add(p => p.Value, @"{""expression"":""x > 0""}"));
var select = cut.Find("#alarm-trigger-kind");
// The bound value must be "Advisory" — not just present in the HTML
Assert.Equal("Advisory", select.GetAttribute("value"));
}
// Selector defaults to Advisory when config has no analysisKind (ScriptTriggerEditor)
/// <summary>Verifies that the trigger editor selector defaults to Advisory when the config has no analysis kind.</summary>
[Fact]
public void ScriptTriggerEditor_Expression_NoAnalysisKindInConfig_SelectorDefaultsAdvisory()
{
var cut = Render<ScriptTriggerEditor>(ps => ps
.Add(p => p.TriggerType, "Expression")
.Add(p => p.TriggerConfig, @"{""expression"":""x > 0"",""mode"":""OnTrue""}"));
var select = cut.Find("#script-trigger-kind");
// The bound value must be "Advisory" — not just present in the HTML
Assert.Equal("Advisory", select.GetAttribute("value"));
}
// Choosing Strict writes analysisKind:"Strict" into emitted config
/// <summary>Verifies that choosing Strict in the alarm trigger editor emits analysisKind as Strict.</summary>
[Fact]
public void AlarmTriggerEditor_Expression_ChoosingStrict_EmitsAnalysisKindStrict()
{
string? emitted = null;
var cut = Render<AlarmTriggerEditor>(ps => ps
.Add(p => p.TriggerType, AlarmTriggerType.Expression)
.Add(p => p.Value, @"{""expression"":""x > 0""}")
.Add(p => p.ValueChanged,
EventCallback.Factory.Create<string?>(this, v => emitted = v)));
cut.Find("#alarm-trigger-kind").Change("Strict");
Assert.NotNull(emitted);
using var doc = JsonDocument.Parse(emitted!);
Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString());
}
// Loaded Strict config reflects in selector, emits Strict on next change
/// <summary>Verifies that a loaded Strict config is retained when the alarm trigger is edited.</summary>
[Fact]
public void AlarmTriggerEditor_Expression_LoadedStrictConfig_RetainedOnEdit()
{
string? emitted = null;
var cut = Render<AlarmTriggerEditor>(ps => ps
.Add(p => p.TriggerType, AlarmTriggerType.Expression)
.Add(p => p.Value, @"{""expression"":""x > 0"",""analysisKind"":""Strict""}")
.Add(p => p.ValueChanged,
EventCallback.Factory.Create<string?>(this, v => emitted = v)));
// Switch to Advisory — config must no longer carry analysisKind
cut.Find("#alarm-trigger-kind").Change("Advisory");
Assert.NotNull(emitted);
using var doc = JsonDocument.Parse(emitted!);
Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
}
}
// ── ScriptTriggerEditor — UI selector (bUnit component tests) ────────────────
public class ScriptTriggerEditorAnalysisKindTests : BunitContext
{
/// <summary>Initializes the test context for the script trigger editor.</summary>
public ScriptTriggerEditorAnalysisKindTests()
{
// MonacoEditor calls MonacoBlazor.createEditor via JS interop. Use Loose
// mode so the editor renders without requiring a full setup for each call.
JSInterop.Mode = JSRuntimeMode.Loose;
}
// Selector is visible for Expression trigger type
/// <summary>Verifies that the script trigger editor shows the analysis kind selector for an Expression trigger.</summary>
[Fact]
public void ScriptTriggerEditor_Expression_ShowsAnalysisKindSelector()
{
var cut = Render<ScriptTriggerEditor>(ps => ps
.Add(p => p.TriggerType, "Expression")
.Add(p => p.TriggerConfig, @"{""expression"":"""",""mode"":""OnTrue""}"));
var select = cut.Find("#script-trigger-kind");
Assert.NotNull(select);
}
// Selector is NOT visible for non-Expression trigger types
/// <summary>Verifies that the script trigger editor does not show the analysis kind selector for a Conditional trigger.</summary>
[Fact]
public void ScriptTriggerEditor_Conditional_DoesNotShowAnalysisKindSelector()
{
var cut = Render<ScriptTriggerEditor>(ps => ps
.Add(p => p.TriggerType, "Conditional")
.Add(p => p.TriggerConfig, @"{""attributeName"":""Temp"",""operator"":"">"",""threshold"":50,""mode"":""OnTrue""}"));
Assert.Throws<Bunit.ElementNotFoundException>(() => cut.Find("#script-trigger-kind"));
}
// Choosing Strict writes analysisKind:"Strict" into emitted config
/// <summary>Verifies that choosing Strict in the script trigger editor emits analysisKind as Strict.</summary>
[Fact]
public void ScriptTriggerEditor_Expression_ChoosingStrict_EmitsAnalysisKindStrict()
{
ScriptTriggerValue? captured = null;
var cut = Render<ScriptTriggerEditor>(ps => ps
.Add(p => p.TriggerType, "Expression")
.Add(p => p.TriggerConfig, @"{""expression"":""x > 0"",""mode"":""OnTrue""}")
.Add(p => p.Changed,
EventCallback.Factory.Create<ScriptTriggerValue>(this, v => captured = v)));
cut.Find("#script-trigger-kind").Change("Strict");
Assert.NotNull(captured);
using var doc = JsonDocument.Parse(captured!.Config!);
Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString());
}
// Choosing Advisory emits config without analysisKind key
/// <summary>Verifies that choosing Advisory in the script trigger editor omits the analysisKind key.</summary>
[Fact]
public void ScriptTriggerEditor_Expression_ChoosingAdvisory_OmitsAnalysisKindKey()
{
ScriptTriggerValue? captured = null;
var cut = Render<ScriptTriggerEditor>(ps => ps
.Add(p => p.TriggerType, "Expression")
.Add(p => p.TriggerConfig, @"{""expression"":""x > 0"",""mode"":""OnTrue"",""analysisKind"":""Strict""}")
.Add(p => p.Changed,
EventCallback.Factory.Create<ScriptTriggerValue>(this, v => captured = v)));
cut.Find("#script-trigger-kind").Change("Advisory");
Assert.NotNull(captured);
using var doc = JsonDocument.Parse(captured!.Config!);
Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
}
// Loaded Strict is retained on unrelated edit (fire mode change)
/// <summary>Verifies that a loaded Strict setting is retained when the trigger mode changes.</summary>
[Fact]
public void ScriptTriggerEditor_Expression_LoadedStrict_RetainedOnModeChange()
{
ScriptTriggerValue? captured = null;
var cut = Render<ScriptTriggerEditor>(ps => ps
.Add(p => p.TriggerType, "Expression")
.Add(p => p.TriggerConfig, @"{""expression"":""x > 0"",""mode"":""OnTrue"",""analysisKind"":""Strict""}")
.Add(p => p.Changed,
EventCallback.Factory.Create<ScriptTriggerValue>(this, v => captured = v)));
// Change fire mode — Strict kind must survive
cut.Find("#script-trigger-mode").Change("WhileTrue");
Assert.NotNull(captured);
Assert.Contains("\"analysisKind\":\"Strict\"", captured!.Config);
}
}