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.
This commit is contained in:
@@ -6,9 +6,10 @@ namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests;
|
||||
public class CollisionDetectorTests
|
||||
{
|
||||
// ========================================================================
|
||||
// WP-12: Naming Collision Detection
|
||||
// Naming Collision Detection
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that no collisions returns an empty result.</summary>
|
||||
[Fact]
|
||||
public void DetectCollisions_NoCollisions_ReturnsEmpty()
|
||||
{
|
||||
@@ -22,6 +23,7 @@ public class CollisionDetectorTests
|
||||
Assert.Empty(collisions);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that different modules with no prefix collision return an empty result.</summary>
|
||||
[Fact]
|
||||
public void DetectCollisions_DifferentModulesNoPrefixCollision_ReturnsEmpty()
|
||||
{
|
||||
@@ -43,6 +45,7 @@ public class CollisionDetectorTests
|
||||
Assert.Empty(collisions);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a collision between a direct and a composed name is reported.</summary>
|
||||
[Fact]
|
||||
public void DetectCollisions_DirectAndComposedNameCollision_ReturnsCollision()
|
||||
{
|
||||
@@ -67,6 +70,7 @@ public class CollisionDetectorTests
|
||||
Assert.Contains(collisions, c => c.Contains("modA.Value"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that nested composition yields the correct canonical names.</summary>
|
||||
[Fact]
|
||||
public void DetectCollisions_NestedComposition_ReturnsCorrectCanonicalNames()
|
||||
{
|
||||
@@ -89,6 +93,7 @@ public class CollisionDetectorTests
|
||||
Assert.Empty(collisions);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that inherited members that collide with composed members are reported as a collision.</summary>
|
||||
[Fact]
|
||||
public void DetectCollisions_InheritedMembersCollideWithComposed_ReturnsCollision()
|
||||
{
|
||||
@@ -113,13 +118,14 @@ public class CollisionDetectorTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Follow-up #3: inherited placeholder rows must not self-collide with the
|
||||
// Inherited placeholder rows must not self-collide with the
|
||||
// parent rows the inheritance walk re-adds. Before the fix, every derived
|
||||
// template reported a collision for each IsInherited row (child origin vs
|
||||
// "parent 'X'" origin) — so there was no supported path to add an attribute
|
||||
// or composition to a derived template.
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that a derived template with inherited placeholder rows produces no false collision.</summary>
|
||||
[Fact]
|
||||
public void DetectCollisions_DerivedTemplateWithInheritedPlaceholders_NoFalseCollision()
|
||||
{
|
||||
@@ -142,6 +148,7 @@ public class CollisionDetectorTests
|
||||
Assert.Empty(collisions);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that multi-level inherited placeholder rows produce no false collision.</summary>
|
||||
[Fact]
|
||||
public void DetectCollisions_MultiLevelInheritedPlaceholders_NoFalseCollision()
|
||||
{
|
||||
@@ -163,6 +170,7 @@ public class CollisionDetectorTests
|
||||
Assert.Empty(collisions);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a genuine collision on a derived template is still detected despite an inherited placeholder.</summary>
|
||||
[Fact]
|
||||
public void DetectCollisions_DerivedTemplate_GenuineCollisionStillDetected_DespiteInheritedPlaceholder()
|
||||
{
|
||||
@@ -190,7 +198,7 @@ public class CollisionDetectorTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Task 10: repeated composition blindness. A template composed into TWO
|
||||
// Repeated composition blindness. A template composed into TWO
|
||||
// different slots was added to the global visited-set on the first slot, so
|
||||
// the SECOND slot's subtree was never collected — the detector went blind to
|
||||
// any collision reachable only through the second slot. The visited-set must
|
||||
@@ -198,6 +206,7 @@ public class CollisionDetectorTests
|
||||
// siblings/other slots can re-descend the same template.
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that a template composed twice within a subtree reports the collision under the second slot.</summary>
|
||||
[Fact]
|
||||
public void DetectCollisions_TemplateComposedTwiceWithinSubtree_SeesCollisionUnderSecondSlot()
|
||||
{
|
||||
@@ -230,6 +239,7 @@ public class CollisionDetectorTests
|
||||
Assert.Contains(collisions, c => c.Contains("w.y2.z.Val"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a cyclic composition does not loop forever.</summary>
|
||||
[Fact]
|
||||
public void DetectCollisions_CyclicComposition_DoesNotLoopForever()
|
||||
{
|
||||
|
||||
@@ -5,9 +5,10 @@ namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests;
|
||||
public class CycleDetectorTests
|
||||
{
|
||||
// ========================================================================
|
||||
// WP-13: Inheritance cycle detection
|
||||
// Inheritance cycle detection
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that self-inheritance is reported as a cycle.</summary>
|
||||
[Fact]
|
||||
public void DetectInheritanceCycle_SelfInheritance_ReturnsCycle()
|
||||
{
|
||||
@@ -20,6 +21,7 @@ public class CycleDetectorTests
|
||||
Assert.Contains("itself", result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a direct inheritance cycle is reported.</summary>
|
||||
[Fact]
|
||||
public void DetectInheritanceCycle_DirectCycle_ReturnsCycle()
|
||||
{
|
||||
@@ -34,6 +36,7 @@ public class CycleDetectorTests
|
||||
Assert.Contains("cycle", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a three-node inheritance cycle is reported.</summary>
|
||||
[Fact]
|
||||
public void DetectInheritanceCycle_ThreeNodeCycle_ReturnsCycle()
|
||||
{
|
||||
@@ -49,6 +52,7 @@ public class CycleDetectorTests
|
||||
Assert.Contains("cycle", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an inheritance chain with no cycle returns null.</summary>
|
||||
[Fact]
|
||||
public void DetectInheritanceCycle_NoCycle_ReturnsNull()
|
||||
{
|
||||
@@ -62,9 +66,10 @@ public class CycleDetectorTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// WP-13: Composition cycle detection
|
||||
// Composition cycle detection
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that self-composition is reported as a cycle.</summary>
|
||||
[Fact]
|
||||
public void DetectCompositionCycle_SelfComposition_ReturnsCycle()
|
||||
{
|
||||
@@ -77,6 +82,7 @@ public class CycleDetectorTests
|
||||
Assert.Contains("compose itself", result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a direct composition cycle is reported.</summary>
|
||||
[Fact]
|
||||
public void DetectCompositionCycle_DirectCycle_ReturnsCycle()
|
||||
{
|
||||
@@ -92,6 +98,7 @@ public class CycleDetectorTests
|
||||
Assert.Contains("cycle", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a transitive composition cycle is reported.</summary>
|
||||
[Fact]
|
||||
public void DetectCompositionCycle_TransitiveCycle_ReturnsCycle()
|
||||
{
|
||||
@@ -108,6 +115,7 @@ public class CycleDetectorTests
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a composition graph with no cycle returns null.</summary>
|
||||
[Fact]
|
||||
public void DetectCompositionCycle_NoCycle_ReturnsNull()
|
||||
{
|
||||
@@ -121,9 +129,10 @@ public class CycleDetectorTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// WP-13: Cross-graph cycle detection (inheritance + composition)
|
||||
// Cross-graph cycle detection (inheritance + composition)
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that a cycle spanning inheritance and composition is reported as a cross-graph cycle.</summary>
|
||||
[Fact]
|
||||
public void DetectCrossGraphCycle_InheritanceCompositionCross_ReturnsCycle()
|
||||
{
|
||||
@@ -141,6 +150,7 @@ public class CycleDetectorTests
|
||||
Assert.Contains("Cross-graph cycle", result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a graph with no cross-graph cycle returns null.</summary>
|
||||
[Fact]
|
||||
public void DetectCrossGraphCycle_NoCycle_ReturnsNull()
|
||||
{
|
||||
@@ -155,9 +165,10 @@ public class CycleDetectorTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// TemplateEngine-013: robustness against duplicate Ids and Id 0
|
||||
// Robustness against duplicate Ids and Id 0
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that duplicate ids in the list do not cause an exception when detecting an inheritance cycle.</summary>
|
||||
[Fact]
|
||||
public void DetectInheritanceCycle_DuplicateIdsInList_DoesNotThrow()
|
||||
{
|
||||
@@ -173,6 +184,7 @@ public class CycleDetectorTests
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that duplicate ids in the list do not cause an exception when detecting a composition cycle.</summary>
|
||||
[Fact]
|
||||
public void DetectCompositionCycle_DuplicateIdsInList_DoesNotThrow()
|
||||
{
|
||||
@@ -185,6 +197,7 @@ public class CycleDetectorTests
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that duplicate ids in the list do not cause an exception when detecting a cross-graph cycle.</summary>
|
||||
[Fact]
|
||||
public void DetectCrossGraphCycle_DuplicateIdsInList_DoesNotThrow()
|
||||
{
|
||||
@@ -197,6 +210,7 @@ public class CycleDetectorTests
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an inheritance cycle through a real id of zero is still detected.</summary>
|
||||
[Fact]
|
||||
public void DetectInheritanceCycle_RealIdZero_StillDetectsCycle()
|
||||
{
|
||||
@@ -212,6 +226,7 @@ public class CycleDetectorTests
|
||||
Assert.Contains("itself", result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a parent chain passing through id zero is detected as a cycle.</summary>
|
||||
[Fact]
|
||||
public void DetectInheritanceCycle_ParentChainThroughIdZero_DetectsCycle()
|
||||
{
|
||||
|
||||
+3
@@ -35,6 +35,7 @@ public class ConnectionBindingOverrideTests
|
||||
[id] = new("OPC-Server1", "OpcUa", 1) { Id = id, PrimaryConfiguration = "opc.tcp://localhost:4840" }
|
||||
};
|
||||
|
||||
/// <summary>Verifies that a set override replaces the template's data source reference.</summary>
|
||||
[Fact]
|
||||
public void Override_replaces_template_DataSourceReference_when_set()
|
||||
{
|
||||
@@ -58,6 +59,7 @@ public class ConnectionBindingOverrideTests
|
||||
Assert.Equal("ns=2;s=Pump1.Speed", attr.DataSourceReference);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null override falls back to the template default.</summary>
|
||||
[Fact]
|
||||
public void Null_override_falls_back_to_template_default()
|
||||
{
|
||||
@@ -81,6 +83,7 @@ public class ConnectionBindingOverrideTests
|
||||
Assert.Equal("TemplateDefault", attr.DataSourceReference);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that changing an override changes the revision hash.</summary>
|
||||
[Fact]
|
||||
public void Override_change_changes_revision_hash()
|
||||
{
|
||||
|
||||
+3
@@ -15,6 +15,7 @@ namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.Flattening;
|
||||
/// </summary>
|
||||
public class DefaultDataSourceReferenceTests
|
||||
{
|
||||
/// <summary>Verifies that the revision hash ignores the default data source reference.</summary>
|
||||
[Fact]
|
||||
public void RevisionHash_IgnoresDefaultDataSourceReference()
|
||||
{
|
||||
@@ -32,6 +33,7 @@ public class DefaultDataSourceReferenceTests
|
||||
Assert.Equal(svc.ComputeHash(without), svc.ComputeHash(with));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a resolved attribute's default data source reference is null by default.</summary>
|
||||
[Fact]
|
||||
public void ResolvedAttribute_DefaultDataSourceReference_IsNullByDefault()
|
||||
{
|
||||
@@ -39,6 +41,7 @@ public class DefaultDataSourceReferenceTests
|
||||
Assert.Null(a.DefaultDataSourceReference);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that flattening populates the default data source reference for both bound and unbound data-sourced attributes.</summary>
|
||||
[Fact]
|
||||
public void Flatten_PopulatesDefault_ForBoundAndUnboundDataSourcedAttributes()
|
||||
{
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.Flattening;
|
||||
|
||||
public class DeploymentPackageTests
|
||||
{
|
||||
/// <summary>Verifies that a deployment package round-trips through JSON serialization.</summary>
|
||||
/// <returns>The result.</returns>
|
||||
[Fact]
|
||||
public void DeploymentPackage_JsonSerializable()
|
||||
{
|
||||
@@ -67,6 +69,8 @@ public class DeploymentPackageTests
|
||||
Assert.Equal("Temperature", deserialized.Configuration.Attributes[0].CanonicalName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a deployment package carrying a configuration diff round-trips through JSON serialization.</summary>
|
||||
/// <returns>The result.</returns>
|
||||
[Fact]
|
||||
public void DeploymentPackage_WithDiff_Serializable()
|
||||
{
|
||||
@@ -105,6 +109,8 @@ public class DeploymentPackageTests
|
||||
Assert.Equal("sha256:old", deserialized.PreviousRevisionHash);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a new flattened configuration has empty default values.</summary>
|
||||
/// <returns>The result.</returns>
|
||||
[Fact]
|
||||
public void FlattenedConfiguration_DefaultValues()
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ public class DiffServiceTests
|
||||
{
|
||||
private readonly DiffService _sut = new();
|
||||
|
||||
/// <summary>Verifies that a null old configuration reports everything as added.</summary>
|
||||
[Fact]
|
||||
public void ComputeDiff_NullOldConfig_AllAdded()
|
||||
{
|
||||
@@ -37,6 +38,7 @@ public class DiffServiceTests
|
||||
Assert.Single(diff.ScriptChanges);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that identical configurations produce no changes.</summary>
|
||||
[Fact]
|
||||
public void ComputeDiff_IdenticalConfigs_NoChanges()
|
||||
{
|
||||
@@ -54,6 +56,7 @@ public class DiffServiceTests
|
||||
Assert.Empty(diff.AttributeChanges);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a removed attribute is detected as removed.</summary>
|
||||
[Fact]
|
||||
public void ComputeDiff_AttributeRemoved_DetectedAsRemoved()
|
||||
{
|
||||
@@ -80,6 +83,7 @@ public class DiffServiceTests
|
||||
Assert.Equal("Removed", diff.AttributeChanges[0].CanonicalName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a changed attribute is detected as changed.</summary>
|
||||
[Fact]
|
||||
public void ComputeDiff_AttributeChanged_DetectedAsChanged()
|
||||
{
|
||||
@@ -103,6 +107,7 @@ public class DiffServiceTests
|
||||
Assert.Equal("50", diff.AttributeChanges[0].NewValue?.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the revision hashes are included in the diff.</summary>
|
||||
[Fact]
|
||||
public void ComputeDiff_RevisionHashes_Included()
|
||||
{
|
||||
@@ -113,6 +118,7 @@ public class DiffServiceTests
|
||||
Assert.Equal("sha256:new", diff.NewRevisionHash);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a script code change is detected.</summary>
|
||||
[Fact]
|
||||
public void ComputeDiff_ScriptCodeChange_Detected()
|
||||
{
|
||||
@@ -134,10 +140,11 @@ public class DiffServiceTests
|
||||
Assert.Equal(DiffChangeType.Changed, diff.ScriptChanges[0].ChangeType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an attribute description change is detected as changed.</summary>
|
||||
[Fact]
|
||||
public void ComputeDiff_AttributeDescriptionChange_DetectedAsChanged()
|
||||
{
|
||||
// TemplateEngine-017: AttributesEqual must compare Description.
|
||||
// AttributesEqual must compare Description.
|
||||
var oldConfig = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
@@ -162,10 +169,11 @@ public class DiffServiceTests
|
||||
Assert.Equal(DiffChangeType.Changed, diff.AttributeChanges[0].ChangeType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an attribute element data type change is detected as changed.</summary>
|
||||
[Fact]
|
||||
public void ComputeDiff_AttributeElementDataTypeChange_DetectedAsChanged()
|
||||
{
|
||||
// #290: ElementDataType is part of the revision hash, so AttributesEqual
|
||||
// ElementDataType is part of the revision hash, so AttributesEqual
|
||||
// must also compare it — a List element-type change (Int32 → Double) with
|
||||
// identical JSON-encoded values must surface as a Changed attribute,
|
||||
// keeping the structured diff in parity with the staleness hash.
|
||||
@@ -193,10 +201,11 @@ public class DiffServiceTests
|
||||
Assert.Equal(DiffChangeType.Changed, diff.AttributeChanges[0].ChangeType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an alarm description change is detected as changed.</summary>
|
||||
[Fact]
|
||||
public void ComputeDiff_AlarmDescriptionChange_DetectedAsChanged()
|
||||
{
|
||||
// TemplateEngine-017: AlarmsEqual must compare Description.
|
||||
// AlarmsEqual must compare Description.
|
||||
var oldConfig = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
@@ -221,10 +230,11 @@ public class DiffServiceTests
|
||||
Assert.Equal(DiffChangeType.Changed, diff.AlarmChanges[0].ChangeType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a native alarm source reference change is detected as changed.</summary>
|
||||
[Fact]
|
||||
public void ComputeDiff_NativeAlarmSourceReferenceChange_DetectedAsChanged()
|
||||
{
|
||||
// PLAN-05 Task 12: native alarm source edits must surface in the diff so
|
||||
// Native alarm source edits must surface in the diff so
|
||||
// the Deployments diff view is not blind to native-alarm binding changes.
|
||||
var oldConfig = new FlattenedConfiguration
|
||||
{
|
||||
@@ -251,6 +261,7 @@ public class DiffServiceTests
|
||||
Assert.Equal("PumpAlarms", diff.NativeAlarmSourceChanges[0].CanonicalName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an added native alarm source is detected as added.</summary>
|
||||
[Fact]
|
||||
public void ComputeDiff_NativeAlarmSourceAdded_DetectedAsAdded()
|
||||
{
|
||||
@@ -271,6 +282,7 @@ public class DiffServiceTests
|
||||
Assert.Equal(DiffChangeType.Added, diff.NativeAlarmSourceChanges[0].ChangeType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a removed native alarm source is detected as removed.</summary>
|
||||
[Fact]
|
||||
public void ComputeDiff_NativeAlarmSourceRemoved_DetectedAsRemoved()
|
||||
{
|
||||
@@ -291,6 +303,7 @@ public class DiffServiceTests
|
||||
Assert.Equal(DiffChangeType.Removed, diff.NativeAlarmSourceChanges[0].ChangeType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a native alarm source lock change is detected as changed.</summary>
|
||||
[Fact]
|
||||
public void ComputeDiff_NativeAlarmSourceLockChange_DetectedAsChanged()
|
||||
{
|
||||
@@ -317,6 +330,7 @@ public class DiffServiceTests
|
||||
Assert.Equal(DiffChangeType.Changed, diff.NativeAlarmSourceChanges[0].ChangeType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unchanged native alarm source produces no diff.</summary>
|
||||
[Fact]
|
||||
public void ComputeDiff_NativeAlarmSourceUnchanged_NoDiff()
|
||||
{
|
||||
@@ -329,12 +343,12 @@ public class DiffServiceTests
|
||||
Assert.Empty(diff.NativeAlarmSourceChanges);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that identical connection configurations compare as equal.</summary>
|
||||
[Fact]
|
||||
public void ConnectionsEqual_IdenticalConfigs_ReturnsTrue()
|
||||
{
|
||||
// TemplateEngine-017: ConnectionsEqual is the comparator callers use
|
||||
// to detect connection-endpoint drift (the diff-view extension that
|
||||
// surfaces this in the UI is tracked under TemplateEngine-018).
|
||||
// ConnectionsEqual is the comparator callers use
|
||||
// to detect connection-endpoint drift.
|
||||
var a = new ConnectionConfig
|
||||
{
|
||||
Protocol = "OpcUa",
|
||||
@@ -347,10 +361,11 @@ public class DiffServiceTests
|
||||
Assert.True(DiffService.ConnectionsEqual(a, b));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an endpoint edit makes the connection configurations compare as unequal.</summary>
|
||||
[Fact]
|
||||
public void ConnectionsEqual_EndpointEdit_ReturnsFalse()
|
||||
{
|
||||
// TemplateEngine-017: primary endpoint JSON edit must surface as a
|
||||
// A primary endpoint JSON edit must surface as a
|
||||
// change. Without this, deployment redeploys ship a different
|
||||
// ConnectionConfig with no visible drift signal.
|
||||
var a = new ConnectionConfig
|
||||
@@ -364,6 +379,7 @@ public class DiffServiceTests
|
||||
Assert.False(DiffService.ConnectionsEqual(a, b));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a backup configuration edit makes the connection configurations compare as unequal.</summary>
|
||||
[Fact]
|
||||
public void ConnectionsEqual_BackupConfigurationEdit_ReturnsFalse()
|
||||
{
|
||||
@@ -373,6 +389,7 @@ public class DiffServiceTests
|
||||
Assert.False(DiffService.ConnectionsEqual(a, b));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a failover retry count edit makes the connection configurations compare as unequal.</summary>
|
||||
[Fact]
|
||||
public void ConnectionsEqual_FailoverRetryCountEdit_ReturnsFalse()
|
||||
{
|
||||
@@ -382,6 +399,7 @@ public class DiffServiceTests
|
||||
Assert.False(DiffService.ConnectionsEqual(a, b));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a protocol edit makes the connection configurations compare as unequal.</summary>
|
||||
[Fact]
|
||||
public void ConnectionsEqual_ProtocolEdit_ReturnsFalse()
|
||||
{
|
||||
@@ -391,14 +409,15 @@ public class DiffServiceTests
|
||||
Assert.False(DiffService.ConnectionsEqual(a, b));
|
||||
}
|
||||
|
||||
// ── TemplateEngine-018: ComputeConnectionsDiff produces Added/Removed/Changed entries ──
|
||||
// ── ComputeConnectionsDiff produces Added/Removed/Changed entries ──
|
||||
|
||||
/// <summary>Verifies that a newly added binding is reported as added in the connections diff.</summary>
|
||||
[Fact]
|
||||
public void ComputeConnectionsDiff_NewBindingAdded_ReportedAsAdded()
|
||||
{
|
||||
// First-time binding (or instance gains its first data-sourced
|
||||
// attribute) — old config has no Connections map, new config does.
|
||||
// The pre-018 diff shape silently dropped this so operators saw
|
||||
// The earlier diff shape silently dropped this so operators saw
|
||||
// "no changes" when the deployment package was structurally larger.
|
||||
var oldConfig = new FlattenedConfiguration { InstanceUniqueName = "Instance1" };
|
||||
var newConfig = new FlattenedConfiguration
|
||||
@@ -425,6 +444,7 @@ public class DiffServiceTests
|
||||
Assert.Equal("OpcUa", diff[0].NewValue!.Protocol);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null old configuration reports all connections as added.</summary>
|
||||
[Fact]
|
||||
public void ComputeConnectionsDiff_NullOldConfig_AllReportedAsAdded()
|
||||
{
|
||||
@@ -455,6 +475,7 @@ public class DiffServiceTests
|
||||
Assert.Equal("OpcUa", diff[0].NewValue!.Protocol);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a cleared binding is reported as removed in the connections diff.</summary>
|
||||
[Fact]
|
||||
public void ComputeConnectionsDiff_BindingCleared_ReportedAsRemoved()
|
||||
{
|
||||
@@ -479,6 +500,7 @@ public class DiffServiceTests
|
||||
Assert.Null(diff[0].NewValue);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an endpoint edit is reported as changed in the connections diff.</summary>
|
||||
[Fact]
|
||||
public void ComputeConnectionsDiff_EndpointEdit_ReportedAsChanged()
|
||||
{
|
||||
@@ -521,6 +543,7 @@ public class DiffServiceTests
|
||||
Assert.Contains("host-b", diff[0].NewValue!.ConfigurationJson);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that identical connections produce no diff entries.</summary>
|
||||
[Fact]
|
||||
public void ComputeConnectionsDiff_IdenticalConnections_NoEntries()
|
||||
{
|
||||
@@ -539,10 +562,11 @@ public class DiffServiceTests
|
||||
Assert.Empty(diff);
|
||||
}
|
||||
|
||||
// ── TemplateEngine-018: ComputeDiff wires ComputeConnectionsDiff into the
|
||||
// ── ComputeDiff wires ComputeConnectionsDiff into the
|
||||
// public ConfigurationDiff.ConnectionChanges slot so standalone connection
|
||||
// protocol/endpoint/failover drift surfaces in the deployment diff (#10). ──
|
||||
// protocol/endpoint/failover drift surfaces in the deployment diff. ──
|
||||
|
||||
/// <summary>Verifies that protocol, endpoint and failover changes populate the connection changes of the diff.</summary>
|
||||
[Fact]
|
||||
public void ComputeDiff_ConnectionProtocolAndEndpointAndFailoverChange_PopulatesConnectionChanges()
|
||||
{
|
||||
@@ -591,6 +615,7 @@ public class DiffServiceTests
|
||||
Assert.Equal(5, entry.NewValue!.FailoverRetryCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a diff that differs only in its connections reports changes.</summary>
|
||||
[Fact]
|
||||
public void ComputeDiff_OnlyConnectionDiffers_HasChangesIsTrue()
|
||||
{
|
||||
|
||||
+20
@@ -12,6 +12,7 @@ public class FlatteningServiceMergeTests
|
||||
{
|
||||
// ── MergeHiLoConfig ────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that merging prefers derived keys while inherited keys that are not overridden survive.</summary>
|
||||
[Fact]
|
||||
public void MergeHiLoConfig_DerivedKeysWin_InheritedKeysSurvive()
|
||||
{
|
||||
@@ -29,6 +30,7 @@ public class FlatteningServiceMergeTests
|
||||
Assert.Equal(100, doc.RootElement.GetProperty("hiHi").GetDouble()); // inherited
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a derived config can override the attribute name.</summary>
|
||||
[Fact]
|
||||
public void MergeHiLoConfig_DerivedCanOverrideAttribute()
|
||||
{
|
||||
@@ -42,6 +44,7 @@ public class FlatteningServiceMergeTests
|
||||
Assert.Equal(50, doc.RootElement.GetProperty("hi").GetDouble());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that merging returns the inherited config when the derived config is null.</summary>
|
||||
[Fact]
|
||||
public void MergeHiLoConfig_DerivedNull_ReturnsInherited()
|
||||
{
|
||||
@@ -52,6 +55,7 @@ public class FlatteningServiceMergeTests
|
||||
Assert.Equal(inherited, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that merging returns the derived config when the inherited config is null.</summary>
|
||||
[Fact]
|
||||
public void MergeHiLoConfig_InheritedNull_ReturnsDerived()
|
||||
{
|
||||
@@ -62,12 +66,14 @@ public class FlatteningServiceMergeTests
|
||||
Assert.Equal(derived, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that merging returns null when both configs are null.</summary>
|
||||
[Fact]
|
||||
public void MergeHiLoConfig_BothNull_ReturnsNull()
|
||||
{
|
||||
Assert.Null(FlatteningService.MergeHiLoConfig(null, null));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that merging falls back to the derived config when the inherited config is malformed.</summary>
|
||||
[Fact]
|
||||
public void MergeHiLoConfig_MalformedInherited_FallsBackToDerived()
|
||||
{
|
||||
@@ -79,6 +85,7 @@ public class FlatteningServiceMergeTests
|
||||
Assert.Equal(derived, result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a new derived key is added while the remaining inherited keys are preserved.</summary>
|
||||
[Fact]
|
||||
public void MergeHiLoConfig_DerivedAddsNewKey_PreservesInheritedRest()
|
||||
{
|
||||
@@ -96,6 +103,7 @@ public class FlatteningServiceMergeTests
|
||||
|
||||
// ── DiffHiLoConfig ─────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that diffing identical configs returns null.</summary>
|
||||
[Fact]
|
||||
public void DiffHiLoConfig_NoChanges_ReturnsNull()
|
||||
{
|
||||
@@ -103,6 +111,7 @@ public class FlatteningServiceMergeTests
|
||||
Assert.Null(FlatteningService.DiffHiLoConfig(both, both));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that diffing returns only the key that changed.</summary>
|
||||
[Fact]
|
||||
public void DiffHiLoConfig_ChangedKey_ReturnsOnlyChangedKey()
|
||||
{
|
||||
@@ -118,6 +127,7 @@ public class FlatteningServiceMergeTests
|
||||
Assert.Equal(90, prop.Value.GetDouble());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a newly added key appears in the diff.</summary>
|
||||
[Fact]
|
||||
public void DiffHiLoConfig_NewKey_AddedToDiff()
|
||||
{
|
||||
@@ -132,6 +142,7 @@ public class FlatteningServiceMergeTests
|
||||
Assert.False(doc.RootElement.TryGetProperty("hi", out _));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that diffing against a null inherited config returns the edited config verbatim.</summary>
|
||||
[Fact]
|
||||
public void DiffHiLoConfig_NullInherited_ReturnsEditedVerbatim()
|
||||
{
|
||||
@@ -139,12 +150,14 @@ public class FlatteningServiceMergeTests
|
||||
Assert.Equal(edited, FlatteningService.DiffHiLoConfig(null, edited));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that diffing a null edited config returns null.</summary>
|
||||
[Fact]
|
||||
public void DiffHiLoConfig_NullEdited_ReturnsNull()
|
||||
{
|
||||
Assert.Null(FlatteningService.DiffHiLoConfig(@"{""hi"":80}", null));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that diffing ignores differences in string escaping.</summary>
|
||||
[Fact]
|
||||
public void DiffHiLoConfig_IgnoresStringEscapeDifferences()
|
||||
{
|
||||
@@ -158,6 +171,7 @@ public class FlatteningServiceMergeTests
|
||||
Assert.Null(diff); // no real change once values are decoded
|
||||
}
|
||||
|
||||
/// <summary>Verifies that diffing ignores differences in numeric formatting.</summary>
|
||||
[Fact]
|
||||
public void DiffHiLoConfig_IgnoresNumericFormatDifferences()
|
||||
{
|
||||
@@ -167,6 +181,7 @@ public class FlatteningServiceMergeTests
|
||||
Assert.Null(FlatteningService.DiffHiLoConfig(inherited, edited));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a diff merged back onto the inherited config reproduces the edited config.</summary>
|
||||
[Fact]
|
||||
public void DiffHiLoConfig_RoundTripsThroughMerge()
|
||||
{
|
||||
@@ -227,6 +242,7 @@ public class FlatteningServiceMergeTests
|
||||
return result.Value!;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an instance alarm override merges its high/low setpoints into the flattened alarm.</summary>
|
||||
[Fact]
|
||||
public void Flatten_InstanceAlarmOverride_HiLo_MergesSetpoints()
|
||||
{
|
||||
@@ -250,6 +266,7 @@ public class FlatteningServiceMergeTests
|
||||
Assert.Equal("Override", alarm.Source);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an instance alarm override replaces the alarm priority.</summary>
|
||||
[Fact]
|
||||
public void Flatten_InstanceAlarmOverride_OverridesPriority()
|
||||
{
|
||||
@@ -268,6 +285,7 @@ public class FlatteningServiceMergeTests
|
||||
Assert.Equal("Override", alarm.Source);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an override targeting a locked alarm is silently ignored.</summary>
|
||||
[Fact]
|
||||
public void Flatten_InstanceAlarmOverride_LockedAlarm_OverrideSilentlyIgnored()
|
||||
{
|
||||
@@ -291,6 +309,7 @@ public class FlatteningServiceMergeTests
|
||||
Assert.Equal(80, doc.RootElement.GetProperty("hi").GetDouble()); // not overridden
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an override naming an unknown alarm has no effect.</summary>
|
||||
[Fact]
|
||||
public void Flatten_InstanceAlarmOverride_UnknownName_DoesNothing()
|
||||
{
|
||||
@@ -312,6 +331,7 @@ public class FlatteningServiceMergeTests
|
||||
Assert.NotEqual("Override", alarm.Source);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an override on a binary-trigger alarm replaces the whole trigger config.</summary>
|
||||
[Fact]
|
||||
public void Flatten_InstanceAlarmOverride_BinaryTrigger_ReplacesWholeConfig()
|
||||
{
|
||||
|
||||
+46
-10
@@ -19,6 +19,7 @@ public class FlatteningServiceTests
|
||||
return t;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that flattening an empty template chain returns a failure.</summary>
|
||||
[Fact]
|
||||
public void Flatten_EmptyTemplateChain_ReturnsFailure()
|
||||
{
|
||||
@@ -34,6 +35,7 @@ public class FlatteningServiceTests
|
||||
Assert.Contains("empty", result.Error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that flattening a single template resolves its attributes.</summary>
|
||||
[Fact]
|
||||
public void Flatten_SingleTemplate_ResolvesAttributes()
|
||||
{
|
||||
@@ -56,6 +58,7 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("Status", result.Value.Attributes[0].CanonicalName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that in an inheritance chain the derived template's value overrides the base.</summary>
|
||||
[Fact]
|
||||
public void Flatten_InheritanceChain_DerivedOverridesBase()
|
||||
{
|
||||
@@ -83,6 +86,7 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("200.0", speed.Value); // Child's value wins
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a locked attribute is not overridden by a derived template.</summary>
|
||||
[Fact]
|
||||
public void Flatten_LockedAttribute_NotOverriddenByDerived()
|
||||
{
|
||||
@@ -105,6 +109,7 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("locked", locked.Value); // Base locked value preserved
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an instance override is applied to an unlocked attribute.</summary>
|
||||
[Fact]
|
||||
public void Flatten_InstanceOverride_AppliedToUnlockedAttribute()
|
||||
{
|
||||
@@ -127,6 +132,7 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("Override", attr.Source);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an instance override is skipped for a locked attribute.</summary>
|
||||
[Fact]
|
||||
public void Flatten_InstanceOverride_SkippedForLockedAttribute()
|
||||
{
|
||||
@@ -148,6 +154,7 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("original", attr.Value); // Lock honored
|
||||
}
|
||||
|
||||
/// <summary>Verifies that members of a composed module receive path-qualified names.</summary>
|
||||
[Fact]
|
||||
public void Flatten_ComposedModule_PathQualifiedNames()
|
||||
{
|
||||
@@ -185,6 +192,7 @@ public class FlatteningServiceTests
|
||||
Assert.Contains(result.Value.Scripts, s => s.CanonicalName == "MainPump.Start");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that connection bindings are resolved correctly.</summary>
|
||||
[Fact]
|
||||
public void Flatten_ConnectionBindings_ResolvedCorrectly()
|
||||
{
|
||||
@@ -217,6 +225,7 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("OpcUa", attr.BoundDataConnectionProtocol);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that alarms are resolved from the template chain.</summary>
|
||||
[Fact]
|
||||
public void Flatten_Alarms_ResolvedFromChain()
|
||||
{
|
||||
@@ -242,6 +251,7 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("RangeViolation", result.Value.Alarms[0].TriggerType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that instance metadata is set correctly on the flattened configuration.</summary>
|
||||
[Fact]
|
||||
public void Flatten_InstanceMetadata_SetCorrectly()
|
||||
{
|
||||
@@ -263,6 +273,7 @@ public class FlatteningServiceTests
|
||||
Assert.Equal(3, result.Value.AreaId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an inherited attribute on a derived template keeps the base value.</summary>
|
||||
[Fact]
|
||||
public void Flatten_InheritedAttributeOnDerived_BaseValueWins()
|
||||
{
|
||||
@@ -290,6 +301,7 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("100.0", setPoint.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an attribute overridden on a derived template takes the derived value.</summary>
|
||||
[Fact]
|
||||
public void Flatten_OverriddenAttributeOnDerived_DerivedValueWins()
|
||||
{
|
||||
@@ -317,6 +329,7 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("150.0", setPoint.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that overriding an attribute locked in the derived template fails.</summary>
|
||||
[Fact]
|
||||
public void Flatten_LockedInDerivedOverride_Fails()
|
||||
{
|
||||
@@ -349,6 +362,7 @@ public class FlatteningServiceTests
|
||||
Assert.Contains("SetPoint", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an inherited script on a derived template keeps the base code.</summary>
|
||||
[Fact]
|
||||
public void Flatten_InheritedScriptOnDerived_BaseCodeWins()
|
||||
{
|
||||
@@ -371,8 +385,9 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("return base;", script.Code);
|
||||
}
|
||||
|
||||
// ── M2.5 (#9): per-script execution timeout threads to ResolvedScript ───
|
||||
// ── Per-script execution timeout threads to ResolvedScript ───
|
||||
|
||||
/// <summary>Verifies that a script's execution timeout carries through to the resolved script.</summary>
|
||||
[Fact]
|
||||
public void Flatten_SingleTemplate_ScriptExecutionTimeoutSecondsThreadsThrough()
|
||||
{
|
||||
@@ -395,6 +410,7 @@ public class FlatteningServiceTests
|
||||
Assert.Null(dflt.ExecutionTimeoutSeconds);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a derived script override's execution timeout follows the winning row.</summary>
|
||||
[Fact]
|
||||
public void Flatten_DerivedScriptOverride_ExecutionTimeoutFollowsWinningRow()
|
||||
{
|
||||
@@ -427,6 +443,7 @@ public class FlatteningServiceTests
|
||||
Assert.Equal(3, script.ExecutionTimeoutSeconds);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an inherited script's execution timeout follows the base row.</summary>
|
||||
[Fact]
|
||||
public void Flatten_InheritedScriptOnDerived_ExecutionTimeoutFollowsBaseRow()
|
||||
{
|
||||
@@ -459,8 +476,9 @@ public class FlatteningServiceTests
|
||||
Assert.Equal(10, script.ExecutionTimeoutSeconds);
|
||||
}
|
||||
|
||||
// ── TemplateEngine-002: per-slot alarm override ────────────────────────
|
||||
// ── Per-slot alarm override ────────────────────────
|
||||
|
||||
/// <summary>Verifies that an inherited alarm on a derived template keeps the base value.</summary>
|
||||
[Fact]
|
||||
public void Flatten_InheritedAlarmOnDerived_BaseValueWins()
|
||||
{
|
||||
@@ -495,6 +513,7 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("{\"attributeName\":\"Temp\",\"high\":100}", alarm.TriggerConfiguration);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the MES-reportable flag keeps the base value through a derived chain.</summary>
|
||||
[Fact]
|
||||
public void Flatten_MesReportable_DerivedChain_BaseValueWins()
|
||||
{
|
||||
@@ -532,6 +551,7 @@ public class FlatteningServiceTests
|
||||
Assert.True(alarm.IsMesReportable);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an alarm overridden on a derived template takes the derived value.</summary>
|
||||
[Fact]
|
||||
public void Flatten_OverriddenAlarmOnDerived_DerivedValueWins()
|
||||
{
|
||||
@@ -566,6 +586,7 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("{\"attributeName\":\"Temp\",\"high\":120}", alarm.TriggerConfiguration);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that overriding an alarm locked in the derived template fails.</summary>
|
||||
[Fact]
|
||||
public void Flatten_LockedInDerivedAlarmOverride_Fails()
|
||||
{
|
||||
@@ -602,6 +623,7 @@ public class FlatteningServiceTests
|
||||
|
||||
// ── MES reportable flag: template value + instance override ────────────
|
||||
|
||||
/// <summary>Verifies that the template's MES-reportable value is carried to the resolved alarm.</summary>
|
||||
[Fact]
|
||||
public void Flatten_MesReportable_TemplateValueCarried()
|
||||
{
|
||||
@@ -633,6 +655,7 @@ public class FlatteningServiceTests
|
||||
Assert.False(result.Value.Alarms.First(a => a.CanonicalName == "LowTemp").IsMesReportable);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an instance override of false wins for the MES-reportable flag.</summary>
|
||||
[Fact]
|
||||
public void Flatten_MesReportable_OverrideFalseWins()
|
||||
{
|
||||
@@ -664,6 +687,7 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("Override", alarm.Source);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null instance override inherits the template's MES-reportable value.</summary>
|
||||
[Fact]
|
||||
public void Flatten_MesReportable_OverrideNullInherits()
|
||||
{
|
||||
@@ -695,8 +719,9 @@ public class FlatteningServiceTests
|
||||
Assert.True(alarm.IsMesReportable);
|
||||
}
|
||||
|
||||
// ── TemplateEngine-001: deep composition nesting ───────────────────────
|
||||
// ── Deep composition nesting ───────────────────────
|
||||
|
||||
/// <summary>Verifies that attributes, alarms and scripts all resolve through a three-level composition.</summary>
|
||||
[Fact]
|
||||
public void Flatten_ThreeLevelComposition_AttributesAlarmsScriptsAllResolved()
|
||||
{
|
||||
@@ -749,6 +774,7 @@ public class FlatteningServiceTests
|
||||
s => s.CanonicalName == "MainPump.DriveMotor.FrontBearing.MonitorBearing");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a nested composed alarm's trigger attribute is prefixed with the module path.</summary>
|
||||
[Fact]
|
||||
public void Flatten_NestedComposedAlarm_TriggerAttributePrefixed()
|
||||
{
|
||||
@@ -786,8 +812,9 @@ public class FlatteningServiceTests
|
||||
Assert.Contains("MainPump.DriveMotor.FrontBearing.Vibration", alarm.TriggerConfiguration);
|
||||
}
|
||||
|
||||
// ── TemplateEngine-004: alarm on-trigger script resolution ─────────────
|
||||
// ── Alarm on-trigger script resolution ─────────────
|
||||
|
||||
/// <summary>Verifies that an alarm's on-trigger script resolves to its canonical name.</summary>
|
||||
[Fact]
|
||||
public void Flatten_AlarmOnTriggerScript_ResolvedToCanonicalName()
|
||||
{
|
||||
@@ -812,6 +839,7 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("HandleAlarm", alarm.OnTriggerScriptCanonicalName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a composed alarm's on-trigger script resolves with the module prefix.</summary>
|
||||
[Fact]
|
||||
public void Flatten_ComposedAlarmOnTriggerScript_ResolvedWithPrefix()
|
||||
{
|
||||
@@ -845,14 +873,15 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("MainPump.PumpAlarmHandler", alarm.OnTriggerScriptCanonicalName);
|
||||
}
|
||||
|
||||
// ── TemplateEngine-016: composed-script ScriptScope.ParentPath ─────────
|
||||
// ── Composed-script ScriptScope.ParentPath ─────────
|
||||
|
||||
/// <summary>Verifies that a nested composed script's scope carries the correct parent path.</summary>
|
||||
[Fact]
|
||||
public void Flatten_NestedComposedScript_ScopeCarriesCorrectParentPath()
|
||||
{
|
||||
// Station composes Pump (level 1); Pump composes Motor (level 2).
|
||||
// The depth-1 script's parent is the root template (ParentPath "");
|
||||
// the depth-2 script's parent is the Pump module (ParentPath "MainPump").
|
||||
// The first-level script's parent is the root template (ParentPath "");
|
||||
// the second-level script's parent is the Pump module (ParentPath "MainPump").
|
||||
var motor = CreateTemplate(3, "Motor");
|
||||
motor.Scripts.Add(new TemplateScript("MonitorMotor", "// m") { Id = 70 });
|
||||
|
||||
@@ -883,10 +912,11 @@ public class FlatteningServiceTests
|
||||
|
||||
var depth2 = result.Value.Scripts.First(s => s.CanonicalName == "MainPump.DriveMotor.MonitorMotor");
|
||||
Assert.Equal("MainPump.DriveMotor", depth2.Scope.SelfPath);
|
||||
// Parent module of a depth-2 script is the enclosing Pump module.
|
||||
// Parent module of a second-level script is the enclosing Pump module.
|
||||
Assert.Equal("MainPump", depth2.Scope.ParentPath);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that native alarm sources are resolved from the template.</summary>
|
||||
[Fact]
|
||||
public void Flatten_ResolvesNativeAlarmSources_FromTemplate()
|
||||
{
|
||||
@@ -903,6 +933,7 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("ns=2;s=P1", result.Value.NativeAlarmSources[0].SourceReference);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an instance override replaces a native alarm source reference.</summary>
|
||||
[Fact]
|
||||
public void Flatten_InstanceOverride_ReplacesNativeAlarmSourceReference()
|
||||
{
|
||||
@@ -921,10 +952,11 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("Override", result.Value.NativeAlarmSources[0].Source);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a locked native alarm source ignores an instance override.</summary>
|
||||
[Fact]
|
||||
public void Flatten_LockedNativeAlarmSource_IgnoresInstanceOverride()
|
||||
{
|
||||
// #05-T13: a native alarm source locked at the template level must not be
|
||||
// A native alarm source locked at the template level must not be
|
||||
// repointed by an instance override — the resolved source keeps the template
|
||||
// values and its Source stays "Template" (mirrors attribute/alarm lock rules).
|
||||
var template = CreateTemplate(1, "Base");
|
||||
@@ -947,8 +979,9 @@ public class FlatteningServiceTests
|
||||
Assert.True(src.IsLocked);
|
||||
}
|
||||
|
||||
// ── MV-4: ElementDataType carried through flatten/override ────────────
|
||||
// ── ElementDataType carried through flatten/override ────────────
|
||||
|
||||
/// <summary>Verifies that a list attribute's element data type is carried to the resolved attribute.</summary>
|
||||
[Fact]
|
||||
public void Flatten_ListAttribute_ElementDataTypeCarriedToResolvedAttribute()
|
||||
{
|
||||
@@ -976,6 +1009,7 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("String", attr.ElementDataType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a list attribute's element data type is preserved through an instance override.</summary>
|
||||
[Fact]
|
||||
public void Flatten_ListAttributeWithInstanceOverride_ElementDataTypePreservedThroughOverride()
|
||||
{
|
||||
@@ -1012,6 +1046,7 @@ public class FlatteningServiceTests
|
||||
|
||||
// ── Repeated composition: same template under multiple slots ──────────
|
||||
|
||||
/// <summary>Verifies that a template composed twice resolves its nested members under both slots.</summary>
|
||||
[Fact]
|
||||
public void Flatten_SameTemplateComposedTwice_ResolvesNestedMembersUnderBothSlots()
|
||||
{
|
||||
@@ -1076,6 +1111,7 @@ public class FlatteningServiceTests
|
||||
Assert.Contains(result.Value.NativeAlarmSources, s => s.CanonicalName == "x.y2.z.Src");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that flattening a composition cycle still terminates.</summary>
|
||||
[Fact]
|
||||
public void Flatten_CompositionCycle_StillTerminates()
|
||||
{
|
||||
|
||||
+32
-13
@@ -7,6 +7,7 @@ public class RevisionHashServiceTests
|
||||
{
|
||||
private readonly RevisionHashService _sut = new();
|
||||
|
||||
/// <summary>Verifies that identical content produces the same hash.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_SameContent_SameHash()
|
||||
{
|
||||
@@ -19,6 +20,7 @@ public class RevisionHashServiceTests
|
||||
Assert.Equal(hash1, hash2);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that different content produces a different hash.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_DifferentContent_DifferentHash()
|
||||
{
|
||||
@@ -31,6 +33,7 @@ public class RevisionHashServiceTests
|
||||
Assert.NotEqual(hash1, hash2);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the computed hash starts with the sha256 prefix.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_StartsWithSha256Prefix()
|
||||
{
|
||||
@@ -40,6 +43,7 @@ public class RevisionHashServiceTests
|
||||
Assert.StartsWith("sha256:", hash);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the hash is deterministic across runs.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_DeterministicAcrossRuns()
|
||||
{
|
||||
@@ -64,12 +68,14 @@ public class RevisionHashServiceTests
|
||||
Assert.Equal(_sut.ComputeHash(config1), _sut.ComputeHash(config2));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null configuration throws an argument-null exception.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_NullConfig_ThrowsArgumentNull()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => _sut.ComputeHash(null!));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that attribute order does not affect the hash.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_AttributeOrder_DoesNotAffectHash()
|
||||
{
|
||||
@@ -99,10 +105,11 @@ public class RevisionHashServiceTests
|
||||
Assert.Equal(_sut.ComputeHash(config1), _sut.ComputeHash(config2));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the hashable records declare their properties in alphabetical order.</summary>
|
||||
[Fact]
|
||||
public void HashableRecords_PropertiesDeclaredAlphabetically()
|
||||
{
|
||||
// TemplateEngine-011: revision-hash determinism depends entirely on the
|
||||
// Revision-hash determinism depends entirely on the
|
||||
// private Hashable* records declaring their properties in alphabetical
|
||||
// order (System.Text.Json emits properties in CLR declaration order and
|
||||
// does not sort). This guards against a contributor silently changing
|
||||
@@ -131,10 +138,11 @@ public class RevisionHashServiceTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that editing an attribute description changes the hash.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_AttributeDescriptionEdit_ChangesHash()
|
||||
{
|
||||
// TemplateEngine-017: Description must be folded into the hash so that
|
||||
// Description must be folded into the hash so that
|
||||
// edits to authoring-time documentation (which still travels in the
|
||||
// deployed payload) flow through the staleness indicator.
|
||||
var baseAttr = new ResolvedAttribute
|
||||
@@ -158,10 +166,11 @@ public class RevisionHashServiceTests
|
||||
Assert.NotEqual(_sut.ComputeHash(configBefore), _sut.ComputeHash(configAfter));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that editing an alarm description changes the hash.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_AlarmDescriptionEdit_ChangesHash()
|
||||
{
|
||||
// TemplateEngine-017: same Description contract applies to alarms.
|
||||
// Same Description contract applies to alarms.
|
||||
var baseAlarm = new ResolvedAlarm
|
||||
{
|
||||
CanonicalName = "HighTemp",
|
||||
@@ -182,10 +191,11 @@ public class RevisionHashServiceTests
|
||||
Assert.NotEqual(_sut.ComputeHash(configBefore), _sut.ComputeHash(configAfter));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that editing a connection endpoint changes the hash.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_ConnectionEndpointEdit_ChangesHash()
|
||||
{
|
||||
// TemplateEngine-017: a Deployment user editing the primary endpoint
|
||||
// A Deployment user editing the primary endpoint
|
||||
// JSON of a data connection bound to an instance must produce a
|
||||
// different revision hash. The connection's protocol, primary/backup
|
||||
// configuration JSON, and failover retry count are all part of the
|
||||
@@ -220,10 +230,11 @@ public class RevisionHashServiceTests
|
||||
Assert.NotEqual(_sut.ComputeHash(configBefore), _sut.ComputeHash(configAfter));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that editing a connection protocol changes the hash.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_ConnectionProtocolEdit_ChangesHash()
|
||||
{
|
||||
// TemplateEngine-017: changing protocol must change the hash.
|
||||
// Changing protocol must change the hash.
|
||||
var connectionsBefore = new Dictionary<string, ConnectionConfig>
|
||||
{
|
||||
["plc1"] = new ConnectionConfig
|
||||
@@ -250,10 +261,11 @@ public class RevisionHashServiceTests
|
||||
Assert.NotEqual(_sut.ComputeHash(configBefore), _sut.ComputeHash(configAfter));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that connections with the same content produce the same hash.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_ConnectionsSameContent_SameHash()
|
||||
{
|
||||
// TemplateEngine-017: equal Connections maps must yield the same hash,
|
||||
// Equal Connections maps must yield the same hash,
|
||||
// regardless of dictionary iteration order (the SortedDictionary
|
||||
// projection guards this).
|
||||
var connections1 = new Dictionary<string, ConnectionConfig>
|
||||
@@ -279,10 +291,11 @@ public class RevisionHashServiceTests
|
||||
Assert.Equal(_sut.ComputeHash(config1), _sut.ComputeHash(config2));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that changing a list element data type changes the hash.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_ListElementDataTypeChange_ChangesHash()
|
||||
{
|
||||
// #290: a List attribute whose element type changes (Int32 → Double)
|
||||
// A List attribute whose element type changes (Int32 → Double)
|
||||
// while the JSON-encoded values stay identical MUST produce a different
|
||||
// revision hash, so the deployment is flagged stale. ElementDataType is
|
||||
// folded into the hashable projection precisely for this case.
|
||||
@@ -307,10 +320,11 @@ public class RevisionHashServiceTests
|
||||
Assert.NotEqual(_sut.ComputeHash(configBefore), _sut.ComputeHash(configAfter));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unchanged list element data type produces the same hash.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_SameListElementDataType_SameHash()
|
||||
{
|
||||
// #290: two List attributes identical in every field — including
|
||||
// Two List attributes identical in every field — including
|
||||
// ElementDataType — must hash the same.
|
||||
var attr = new ResolvedAttribute
|
||||
{
|
||||
@@ -332,10 +346,11 @@ public class RevisionHashServiceTests
|
||||
Assert.Equal(_sut.ComputeHash(config1), _sut.ComputeHash(config2));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a scalar attribute hashes the same whether its element data type is null or unset.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_ScalarAttribute_NullVsUnsetElementDataType_SameHash()
|
||||
{
|
||||
// #290 no-op guard: a scalar attribute has a null ElementDataType, and
|
||||
// No-op guard: a scalar attribute has a null ElementDataType, and
|
||||
// the canonical serializer omits null properties
|
||||
// (DefaultIgnoreCondition = WhenWritingNull). Explicitly setting
|
||||
// ElementDataType = null must therefore hash identically to leaving it
|
||||
@@ -367,10 +382,11 @@ public class RevisionHashServiceTests
|
||||
Assert.Equal(_sut.ComputeHash(config1), _sut.ComputeHash(config2));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that changing a native alarm source changes the hash.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_NativeAlarmSourceChange_ChangesHash()
|
||||
{
|
||||
// TemplateEngine-011: native alarm source bindings are part of the
|
||||
// Native alarm source bindings are part of the
|
||||
// deployment package, so an edit to a source's binding (here the
|
||||
// SourceReference) MUST change the revision hash and flag the instance
|
||||
// stale. Before this migration native sources were absent from the hash
|
||||
@@ -397,10 +413,11 @@ public class RevisionHashServiceTests
|
||||
Assert.NotEqual(_sut.ComputeHash(configBefore), _sut.ComputeHash(configAfter));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that changing a native alarm source's locked flag changes the hash.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_NativeAlarmSourceIsLockedChange_ChangesHash()
|
||||
{
|
||||
// TemplateEngine-011: IsLocked participates in the hash too.
|
||||
// IsLocked participates in the hash too.
|
||||
var baseSource = new ResolvedNativeAlarmSource
|
||||
{
|
||||
CanonicalName = "Boiler.Alarms",
|
||||
@@ -422,10 +439,11 @@ public class RevisionHashServiceTests
|
||||
Assert.NotEqual(_sut.ComputeHash(configBefore), _sut.ComputeHash(configAfter));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that native alarm source order does not affect the hash.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_NativeAlarmSourceOrder_DoesNotAffectHash()
|
||||
{
|
||||
// TemplateEngine-011: sources are sorted by CanonicalName before hashing,
|
||||
// Sources are sorted by CanonicalName before hashing,
|
||||
// mirroring attributes/alarms/scripts — authoring order is irrelevant.
|
||||
var a = new ResolvedNativeAlarmSource
|
||||
{
|
||||
@@ -448,10 +466,11 @@ public class RevisionHashServiceTests
|
||||
Assert.Equal(_sut.ComputeHash(config1), _sut.ComputeHash(config2));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a configuration with no native alarm sources keeps its baseline hash.</summary>
|
||||
[Fact]
|
||||
public void ComputeHash_NoNativeAlarmSources_HashUnchangedFromBaseline()
|
||||
{
|
||||
// TemplateEngine-011 migration surgicality: adding the (null-when-empty)
|
||||
// Migration surgicality: adding the (null-when-empty)
|
||||
// NativeAlarmSources property to the hashable projection must NOT change
|
||||
// the hash of any native-source-free configuration. The literal below is
|
||||
// the pre-migration hash of CreateConfig("Instance1", "25.0"); pinning it
|
||||
|
||||
@@ -6,9 +6,10 @@ namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests;
|
||||
public class LockEnforcerTests
|
||||
{
|
||||
// ========================================================================
|
||||
// WP-8: Override Granularity
|
||||
// Override Granularity
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that overriding a locked attribute returns an error.</summary>
|
||||
[Fact]
|
||||
public void ValidateAttributeOverride_LockedAttribute_ReturnsError()
|
||||
{
|
||||
@@ -27,6 +28,7 @@ public class LockEnforcerTests
|
||||
Assert.Contains("locked", result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that changing the data type in an attribute override returns an error.</summary>
|
||||
[Fact]
|
||||
public void ValidateAttributeOverride_DataTypeChanged_ReturnsError()
|
||||
{
|
||||
@@ -45,6 +47,7 @@ public class LockEnforcerTests
|
||||
Assert.Contains("DataType", result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that changing the data source reference in an attribute override returns an error.</summary>
|
||||
[Fact]
|
||||
public void ValidateAttributeOverride_DataSourceReferenceChanged_ReturnsError()
|
||||
{
|
||||
@@ -63,6 +66,7 @@ public class LockEnforcerTests
|
||||
Assert.Contains("DataSourceReference", result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that changing only the value and description in an attribute override returns null.</summary>
|
||||
[Fact]
|
||||
public void ValidateAttributeOverride_ValueAndDescriptionChanged_ReturnsNull()
|
||||
{
|
||||
@@ -80,10 +84,11 @@ public class LockEnforcerTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that changing the element data type in an attribute override returns an error.</summary>
|
||||
[Fact]
|
||||
public void ValidateAttributeOverride_ElementDataTypeChanged_ReturnsError()
|
||||
{
|
||||
// MV-10 review fix: ElementDataType is the element scalar type of a List
|
||||
// ElementDataType is the element scalar type of a List
|
||||
// attribute and is fixed by the defining level, exactly like DataType.
|
||||
// TemplateService.UpdateAttributeAsync never copies it onto the persisted
|
||||
// row, so a mismatch must be rejected before the Value (validated against
|
||||
@@ -103,6 +108,7 @@ public class LockEnforcerTests
|
||||
Assert.Contains("ElementDataType", result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a matching element data type returns null.</summary>
|
||||
[Fact]
|
||||
public void ValidateAttributeOverride_ElementDataTypeMatches_ReturnsNull()
|
||||
{
|
||||
@@ -120,6 +126,7 @@ public class LockEnforcerTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that both element data types being null returns null.</summary>
|
||||
[Fact]
|
||||
public void ValidateAttributeOverride_ElementDataTypeBothNull_ReturnsNull()
|
||||
{
|
||||
@@ -138,6 +145,7 @@ public class LockEnforcerTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that overriding a locked alarm returns an error.</summary>
|
||||
[Fact]
|
||||
public void ValidateAlarmOverride_LockedAlarm_ReturnsError()
|
||||
{
|
||||
@@ -156,6 +164,7 @@ public class LockEnforcerTests
|
||||
Assert.Contains("locked", result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that changing the trigger type in an alarm override returns an error.</summary>
|
||||
[Fact]
|
||||
public void ValidateAlarmOverride_TriggerTypeChanged_ReturnsError()
|
||||
{
|
||||
@@ -174,6 +183,7 @@ public class LockEnforcerTests
|
||||
Assert.Contains("TriggerType", result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that changing the overridable fields of an alarm returns null.</summary>
|
||||
[Fact]
|
||||
public void ValidateAlarmOverride_OverridableFieldsChanged_ReturnsNull()
|
||||
{
|
||||
@@ -193,6 +203,7 @@ public class LockEnforcerTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that overriding a locked script returns an error.</summary>
|
||||
[Fact]
|
||||
public void ValidateScriptOverride_LockedScript_ReturnsError()
|
||||
{
|
||||
@@ -205,6 +216,7 @@ public class LockEnforcerTests
|
||||
Assert.Contains("locked", result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that changing the name in a script override returns an error.</summary>
|
||||
[Fact]
|
||||
public void ValidateScriptOverride_NameChanged_ReturnsError()
|
||||
{
|
||||
@@ -217,6 +229,7 @@ public class LockEnforcerTests
|
||||
Assert.Contains("Name", result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that changing the overridable fields of a script returns null.</summary>
|
||||
[Fact]
|
||||
public void ValidateScriptOverride_OverridableFieldsChanged_ReturnsNull()
|
||||
{
|
||||
@@ -234,9 +247,10 @@ public class LockEnforcerTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// WP-9: Locking Rules
|
||||
// Locking Rules
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that unlocking a locked member returns an error.</summary>
|
||||
[Fact]
|
||||
public void ValidateLockChange_UnlockLocked_ReturnsError()
|
||||
{
|
||||
@@ -246,6 +260,7 @@ public class LockEnforcerTests
|
||||
Assert.Contains("cannot be unlocked", result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that locking an unlocked member returns null.</summary>
|
||||
[Fact]
|
||||
public void ValidateLockChange_LockUnlocked_ReturnsNull()
|
||||
{
|
||||
@@ -254,6 +269,7 @@ public class LockEnforcerTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that keeping a member locked returns null.</summary>
|
||||
[Fact]
|
||||
public void ValidateLockChange_KeepLocked_ReturnsNull()
|
||||
{
|
||||
@@ -262,6 +278,7 @@ public class LockEnforcerTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that keeping a member unlocked returns null.</summary>
|
||||
[Fact]
|
||||
public void ValidateLockChange_KeepUnlocked_ReturnsNull()
|
||||
{
|
||||
@@ -271,9 +288,10 @@ public class LockEnforcerTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// TemplateEngine-022: LockedInDerived one-way ratchet
|
||||
// LockedInDerived one-way ratchet
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that clearing LockedInDerived returns an error.</summary>
|
||||
[Fact]
|
||||
public void ValidateLockedInDerivedChange_ClearLocked_ReturnsError()
|
||||
{
|
||||
@@ -287,6 +305,7 @@ public class LockEnforcerTests
|
||||
Assert.Contains("cannot be cleared", result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that setting LockedInDerived on an unlocked member returns null.</summary>
|
||||
[Fact]
|
||||
public void ValidateLockedInDerivedChange_LockUnlocked_ReturnsNull()
|
||||
{
|
||||
@@ -296,6 +315,7 @@ public class LockEnforcerTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that keeping LockedInDerived set returns null.</summary>
|
||||
[Fact]
|
||||
public void ValidateLockedInDerivedChange_KeepLocked_ReturnsNull()
|
||||
{
|
||||
@@ -304,6 +324,7 @@ public class LockEnforcerTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that keeping LockedInDerived unset returns null.</summary>
|
||||
[Fact]
|
||||
public void ValidateLockedInDerivedChange_KeepUnlocked_ReturnsNull()
|
||||
{
|
||||
|
||||
@@ -12,11 +12,14 @@ public class AreaServiceTests
|
||||
private readonly Mock<IAuditService> _auditMock = new();
|
||||
private readonly AreaService _sut;
|
||||
|
||||
/// <summary>Initializes the test class with a mocked repository and the service under test.</summary>
|
||||
public AreaServiceTests()
|
||||
{
|
||||
_sut = new AreaService(_repoMock.Object, _auditMock.Object);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating an area with valid input succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateArea_ValidInput_ReturnsSuccess()
|
||||
{
|
||||
@@ -32,6 +35,8 @@ public class AreaServiceTests
|
||||
Assert.Equal(1, result.Value.SiteId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating an area with a duplicate name fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateArea_DuplicateName_ReturnsFailure()
|
||||
{
|
||||
@@ -47,6 +52,8 @@ public class AreaServiceTests
|
||||
Assert.Contains("already exists", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating an area with a parent validates that the parent belongs to the site.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateArea_WithParent_ValidatesParentBelongsToSite()
|
||||
{
|
||||
@@ -59,6 +66,8 @@ public class AreaServiceTests
|
||||
Assert.Contains("does not belong", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting an area with assigned instances fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteArea_WithAssignedInstances_ReturnsFailure()
|
||||
{
|
||||
@@ -78,6 +87,8 @@ public class AreaServiceTests
|
||||
Assert.Contains("instance(s) are assigned", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting an area with child areas fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteArea_WithChildAreas_ReturnsFailure()
|
||||
{
|
||||
@@ -98,6 +109,8 @@ public class AreaServiceTests
|
||||
Assert.Contains("child areas", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting an unconstrained area succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteArea_NoConstraints_Success()
|
||||
{
|
||||
@@ -116,6 +129,8 @@ public class AreaServiceTests
|
||||
_repoMock.Verify(r => r.DeleteAreaAsync(1, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that moving an area under another area succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveArea_ToOtherArea_Succeeds()
|
||||
{
|
||||
@@ -141,6 +156,8 @@ public class AreaServiceTests
|
||||
_auditMock.Verify(a => a.LogAsync("admin", "Move", "Area", "3", "Leaf", It.IsAny<object>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that moving an area to the site root succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveArea_ToSiteRoot_Succeeds()
|
||||
{
|
||||
@@ -161,6 +178,8 @@ public class AreaServiceTests
|
||||
Assert.Null(result.Value.ParentAreaId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that moving an area under itself fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveArea_ToSelf_Fails()
|
||||
{
|
||||
@@ -173,6 +192,8 @@ public class AreaServiceTests
|
||||
Assert.Contains("its own parent", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that moving an area under one of its descendants fails with a cycle error.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveArea_ToDescendant_FailsWithCycleError()
|
||||
{
|
||||
@@ -195,6 +216,8 @@ public class AreaServiceTests
|
||||
Assert.Contains("descendants", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that moving an area to a different site fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveArea_DifferentSite_Fails()
|
||||
{
|
||||
@@ -209,6 +232,8 @@ public class AreaServiceTests
|
||||
Assert.Contains("same site", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that moving an area fails when its name collides at the new parent.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveArea_NameCollidesAtNewParent_Fails()
|
||||
{
|
||||
@@ -232,6 +257,8 @@ public class AreaServiceTests
|
||||
Assert.Contains("already exists", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that moving an area to its current parent succeeds as a no-op.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveArea_SameParent_NoOpSuccess()
|
||||
{
|
||||
@@ -253,6 +280,8 @@ public class AreaServiceTests
|
||||
_auditMock.Verify(a => a.LogAsync(It.IsAny<string>(), "Move", It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<object>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that moving an area fails when the target parent does not exist.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveArea_TargetParentMissing_Fails()
|
||||
{
|
||||
@@ -267,6 +296,8 @@ public class AreaServiceTests
|
||||
Assert.Contains("not found", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting an area is blocked when its descendants have assigned instances.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteArea_InstancesInDescendants_Blocked()
|
||||
{
|
||||
|
||||
@@ -15,11 +15,14 @@ public class InstanceServiceTests
|
||||
private readonly Mock<IAuditService> _auditMock = new();
|
||||
private readonly InstanceService _sut;
|
||||
|
||||
/// <summary>Initializes the test with a mocked repository and audit service and the service under test.</summary>
|
||||
public InstanceServiceTests()
|
||||
{
|
||||
_sut = new InstanceService(_repoMock.Object, _auditMock.Object);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating an instance with valid input returns success.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateInstance_ValidInput_ReturnsSuccess()
|
||||
{
|
||||
@@ -38,6 +41,8 @@ public class InstanceServiceTests
|
||||
_repoMock.Verify(r => r.AddInstanceAsync(It.IsAny<Instance>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating an instance with a duplicate name returns a failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateInstance_DuplicateName_ReturnsFailure()
|
||||
{
|
||||
@@ -52,6 +57,8 @@ public class InstanceServiceTests
|
||||
Assert.Contains("already exists", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating an instance from a missing template returns a failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateInstance_MissingTemplate_ReturnsFailure()
|
||||
{
|
||||
@@ -64,6 +71,8 @@ public class InstanceServiceTests
|
||||
Assert.Contains("not found", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that overriding a locked attribute returns a failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetAttributeOverride_LockedAttribute_ReturnsFailure()
|
||||
{
|
||||
@@ -82,6 +91,8 @@ public class InstanceServiceTests
|
||||
Assert.Contains("locked", result.Error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that overriding a non-existent attribute returns a failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetAttributeOverride_NonExistentAttribute_ReturnsFailure()
|
||||
{
|
||||
@@ -97,6 +108,8 @@ public class InstanceServiceTests
|
||||
Assert.Contains("does not exist", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that overriding an unlocked attribute returns success.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetAttributeOverride_UnlockedAttribute_ReturnsSuccess()
|
||||
{
|
||||
@@ -120,7 +133,7 @@ public class InstanceServiceTests
|
||||
Assert.Equal("99", result.Value.OverrideValue);
|
||||
}
|
||||
|
||||
// --- TemplateEngine-008 regression: SetAlarmOverrideAsync validation ---
|
||||
// --- SetAlarmOverrideAsync validation regression ---
|
||||
|
||||
private static Template TemplateWithAlarms(int id, params TemplateAlarm[] alarms)
|
||||
{
|
||||
@@ -133,6 +146,8 @@ public class InstanceServiceTests
|
||||
return t;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that overriding a non-existent alarm returns a failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetAlarmOverride_NonExistentAlarm_ReturnsFailure()
|
||||
{
|
||||
@@ -153,6 +168,8 @@ public class InstanceServiceTests
|
||||
It.IsAny<InstanceAlarmOverride>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that overriding a locked alarm on a composed template returns a failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetAlarmOverride_ComposedLockedAlarm_ReturnsFailure()
|
||||
{
|
||||
@@ -177,6 +194,8 @@ public class InstanceServiceTests
|
||||
It.IsAny<InstanceAlarmOverride>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that overriding an unlocked alarm on a composed template returns success.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetAlarmOverride_ComposedUnlockedAlarm_ReturnsSuccess()
|
||||
{
|
||||
@@ -202,6 +221,8 @@ public class InstanceServiceTests
|
||||
It.IsAny<InstanceAlarmOverride>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that overriding a locked alarm declared directly on the template returns a failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetAlarmOverride_DirectLockedAlarm_ReturnsFailure()
|
||||
{
|
||||
@@ -220,6 +241,8 @@ public class InstanceServiceTests
|
||||
Assert.Contains("locked", result.Error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that setting an alarm override stores the MES-reportable override.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetAlarmOverrideAsync_StoresIsMesReportableOverride()
|
||||
{
|
||||
@@ -250,6 +273,8 @@ public class InstanceServiceTests
|
||||
Assert.True(result.Value.IsMesReportableOverride);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null MES-reportable override clears the existing override.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetAlarmOverrideAsync_NullIsMesReportableOverride_ClearsExistingOverride()
|
||||
{
|
||||
@@ -284,6 +309,8 @@ public class InstanceServiceTests
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that setting an alarm override updates the MES-reportable override on an existing override.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetAlarmOverrideAsync_UpdatesIsMesReportableOverrideOnExisting()
|
||||
{
|
||||
@@ -315,6 +342,8 @@ public class InstanceServiceTests
|
||||
Assert.Equal(3, existingOverride.PriorityLevelOverride);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that enabling an existing instance sets its state to enabled.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Enable_ExistingInstance_SetsEnabled()
|
||||
{
|
||||
@@ -330,6 +359,8 @@ public class InstanceServiceTests
|
||||
Assert.Equal(InstanceState.Enabled, result.Value.State);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disabling an existing instance sets its state to disabled.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Disable_ExistingInstance_SetsDisabled()
|
||||
{
|
||||
@@ -345,6 +376,8 @@ public class InstanceServiceTests
|
||||
Assert.Equal(InstanceState.Disabled, result.Value.State);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a bulk connection-binding assignment succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetConnectionBindings_BulkAssignment_Success()
|
||||
{
|
||||
@@ -364,8 +397,10 @@ public class InstanceServiceTests
|
||||
_repoMock.Verify(r => r.AddInstanceConnectionBindingAsync(It.IsAny<InstanceConnectionBinding>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||
}
|
||||
|
||||
// --- NJ-2: ElementDataType stamping on SetAttributeOverrideAsync ---
|
||||
// --- ElementDataType stamping on SetAttributeOverrideAsync ---
|
||||
|
||||
/// <summary>Verifies that creating an override for a list attribute stamps the element data type.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetAttributeOverride_CreatePath_ListAttribute_StampsElementDataType()
|
||||
{
|
||||
@@ -398,6 +433,8 @@ public class InstanceServiceTests
|
||||
Assert.Equal(DataType.String, captured.ElementDataType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that updating an override for a list attribute stamps the element data type.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetAttributeOverride_UpdatePath_ListAttribute_StampsElementDataType()
|
||||
{
|
||||
@@ -432,6 +469,8 @@ public class InstanceServiceTests
|
||||
Assert.Equal(DataType.String, captured.ElementDataType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating an override for a scalar attribute leaves the element data type null.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SetAttributeOverride_CreatePath_ScalarAttribute_ElementDataTypeIsNull()
|
||||
{
|
||||
@@ -464,6 +503,8 @@ public class InstanceServiceTests
|
||||
Assert.Null(captured.ElementDataType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that assigning an instance to an area in a different site returns a failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AssignToArea_AreaInDifferentSite_ReturnsFailure()
|
||||
{
|
||||
|
||||
@@ -13,11 +13,14 @@ public class SiteServiceTests
|
||||
private readonly Mock<IAuditService> _auditMock = new();
|
||||
private readonly SiteService _sut;
|
||||
|
||||
/// <summary>Initializes the test class with a mocked repository and the service under test.</summary>
|
||||
public SiteServiceTests()
|
||||
{
|
||||
_sut = new SiteService(_repoMock.Object, _auditMock.Object);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a site with valid input succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateSite_ValidInput_ReturnsSuccess()
|
||||
{
|
||||
@@ -33,6 +36,8 @@ public class SiteServiceTests
|
||||
Assert.Equal("SITE-001", result.Value.SiteIdentifier);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a site with a duplicate identifier fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateSite_DuplicateIdentifier_ReturnsFailure()
|
||||
{
|
||||
@@ -45,6 +50,8 @@ public class SiteServiceTests
|
||||
Assert.Contains("already exists", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a site with an empty name fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateSite_EmptyName_ReturnsFailure()
|
||||
{
|
||||
@@ -54,6 +61,8 @@ public class SiteServiceTests
|
||||
Assert.Contains("required", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting a site that has instances fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteSite_WithInstances_ReturnsFailure()
|
||||
{
|
||||
@@ -72,6 +81,8 @@ public class SiteServiceTests
|
||||
Assert.Contains("2 instance(s)", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting a site without instances succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteSite_NoInstances_Success()
|
||||
{
|
||||
@@ -87,6 +98,8 @@ public class SiteServiceTests
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a data connection with valid input succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateDataConnection_ValidInput_Success()
|
||||
{
|
||||
@@ -101,6 +114,8 @@ public class SiteServiceTests
|
||||
Assert.Equal(1, result.Value.SiteId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that updating a site with valid input succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateSite_ValidInput_Success()
|
||||
{
|
||||
|
||||
+18
-1
@@ -11,11 +11,14 @@ public class TemplateDeletionServiceTests
|
||||
private readonly Mock<ITemplateEngineRepository> _repoMock = new();
|
||||
private readonly TemplateDeletionService _sut;
|
||||
|
||||
/// <summary>Initializes the test class with a mocked repository and the deletion service under test.</summary>
|
||||
public TemplateDeletionServiceTests()
|
||||
{
|
||||
_sut = new TemplateDeletionService(_repoMock.Object);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a template with no references can be deleted.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CanDeleteTemplate_NoReferences_ReturnsSuccess()
|
||||
{
|
||||
@@ -33,6 +36,8 @@ public class TemplateDeletionServiceTests
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a template with instances cannot be deleted.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CanDeleteTemplate_WithInstances_ReturnsFailure()
|
||||
{
|
||||
@@ -56,6 +61,8 @@ public class TemplateDeletionServiceTests
|
||||
Assert.Contains("Inst1", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a template with child templates cannot be deleted.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CanDeleteTemplate_WithChildTemplates_ReturnsFailure()
|
||||
{
|
||||
@@ -79,6 +86,8 @@ public class TemplateDeletionServiceTests
|
||||
Assert.Contains("Child", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a template composed by other templates cannot be deleted.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CanDeleteTemplate_ComposedByOthers_ReturnsFailure()
|
||||
{
|
||||
@@ -103,10 +112,12 @@ public class TemplateDeletionServiceTests
|
||||
Assert.Contains("Composer", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that checking deletability does not issue a composition query per template.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CanDeleteTemplate_DoesNotIssuePerTemplateCompositionQuery()
|
||||
{
|
||||
// TemplateEngine-009: Check 3 must read the Compositions navigation
|
||||
// The composition check must read the Compositions navigation
|
||||
// already loaded by GetAllTemplatesAsync rather than issuing one
|
||||
// GetCompositionsByTemplateIdAsync round-trip per template.
|
||||
var templates = new List<Template>
|
||||
@@ -131,6 +142,8 @@ public class TemplateDeletionServiceTests
|
||||
It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that checking a missing template returns a failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CanDeleteTemplate_NotFound_ReturnsFailure()
|
||||
{
|
||||
@@ -143,6 +156,8 @@ public class TemplateDeletionServiceTests
|
||||
Assert.Contains("not found", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a template is deleted when all constraints are met.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteTemplate_AllConstraintsMet_Deletes()
|
||||
{
|
||||
@@ -163,6 +178,8 @@ public class TemplateDeletionServiceTests
|
||||
_repoMock.Verify(r => r.DeleteTemplateAsync(1, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that all errors are reported when multiple constraints are violated.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CanDeleteTemplate_MultipleConstraints_AllErrorsReported()
|
||||
{
|
||||
|
||||
+69
-4
@@ -13,11 +13,14 @@ public class TemplateFolderServiceTests
|
||||
private readonly Mock<IAuditService> _auditMock = new();
|
||||
private readonly TemplateFolderService _sut;
|
||||
|
||||
/// <summary>Initializes the test class with a mocked repository and the service under test.</summary>
|
||||
public TemplateFolderServiceTests()
|
||||
{
|
||||
_sut = new TemplateFolderService(_repoMock.Object, _auditMock.Object);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a folder with valid input succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateFolder_ValidInput_ReturnsSuccess()
|
||||
{
|
||||
@@ -30,10 +33,12 @@ public class TemplateFolderServiceTests
|
||||
Assert.Equal("Dev", result.Value.Name);
|
||||
Assert.Null(result.Value.ParentFolderId);
|
||||
_repoMock.Verify(r => r.AddFolderAsync(It.IsAny<TemplateFolder>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
// Two saves: the folder entity, then the staged audit row (TemplateEngine-024).
|
||||
// Two saves: the folder entity, then the staged audit row.
|
||||
_repoMock.Verify(r => r.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a folder with an empty name fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateFolder_EmptyName_ReturnsFailure()
|
||||
{
|
||||
@@ -43,6 +48,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.Contains("required", result.Error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a folder whose name duplicates a sibling's, ignoring case, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateFolder_DuplicateSiblingName_CaseInsensitive_ReturnsFailure()
|
||||
{
|
||||
@@ -58,6 +65,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.Contains("already exists", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a folder under a missing parent fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateFolder_ParentNotFound_ReturnsFailure()
|
||||
{
|
||||
@@ -70,6 +79,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.Contains("not found", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that renaming a folder with valid input succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RenameFolder_ValidInput_ReturnsSuccess()
|
||||
{
|
||||
@@ -84,6 +95,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.Equal("New", result.Value.Name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that renaming a missing folder fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RenameFolder_NotFound_ReturnsFailure()
|
||||
{
|
||||
@@ -95,6 +108,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.True(result.IsFailure);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that renaming a folder to a sibling's name fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RenameFolder_DuplicateSibling_ReturnsFailure()
|
||||
{
|
||||
@@ -110,6 +125,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.Contains("already exists", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that moving a folder under a valid parent succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveFolder_ValidParent_ReturnsSuccess()
|
||||
{
|
||||
@@ -126,6 +143,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.Equal(2, result.Value.ParentFolderId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that moving a folder onto itself fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveFolder_OntoSelf_ReturnsFailure()
|
||||
{
|
||||
@@ -138,6 +157,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.Contains("cycle", result.Error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that moving a folder onto one of its descendants fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveFolder_OntoDescendant_ReturnsFailure()
|
||||
{
|
||||
@@ -156,6 +177,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.Contains("cycle", result.Error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that moving a folder to the root succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveFolder_ToRoot_ReturnsSuccess()
|
||||
{
|
||||
@@ -170,6 +193,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.Null(result.Value.ParentFolderId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that moving a folder in a graph with a pre-existing cycle fails without looping forever.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveFolder_PreExistingCycleInGraph_ReturnsFailure_DoesNotInfiniteLoop()
|
||||
{
|
||||
@@ -189,6 +214,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.Contains("cycle", result.Error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting an empty folder succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteFolder_Empty_ReturnsSuccess()
|
||||
{
|
||||
@@ -205,6 +232,8 @@ public class TemplateFolderServiceTests
|
||||
_repoMock.Verify(r => r.DeleteFolderAsync(1, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting a folder with child folders fails and reports the counts.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteFolder_HasChildFolders_ReturnsFailure_WithCounts()
|
||||
{
|
||||
@@ -222,6 +251,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.Contains("1 subfolder", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting a folder that contains templates fails and reports the counts.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteFolder_HasTemplates_ReturnsFailure_WithCounts()
|
||||
{
|
||||
@@ -243,6 +274,8 @@ public class TemplateFolderServiceTests
|
||||
// ReorderFolderAsync — sibling reorder by SortOrder (Up = lower, Down = higher)
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that reordering a folder up swaps its sort order with the previous sibling.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReorderFolder_Up_SwapsSortOrderWithPreviousSibling()
|
||||
{
|
||||
@@ -262,6 +295,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.Equal(2, c.SortOrder);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reordering a folder down swaps its sort order with the next sibling.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReorderFolder_Down_SwapsSortOrderWithNextSibling()
|
||||
{
|
||||
@@ -281,6 +316,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.Equal(1, c.SortOrder);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reordering the top folder up is a no-op.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReorderFolder_UpAtTop_IsNoOp()
|
||||
{
|
||||
@@ -302,6 +339,8 @@ public class TemplateFolderServiceTests
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<object?>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reordering the bottom folder down is a no-op.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReorderFolder_DownAtBottom_IsNoOp()
|
||||
{
|
||||
@@ -320,6 +359,8 @@ public class TemplateFolderServiceTests
|
||||
_repoMock.Verify(r => r.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reordering only affects folders within the same parent.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReorderFolder_OnlyReordersWithinSameParent()
|
||||
{
|
||||
@@ -343,6 +384,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.Equal(1, b.SortOrder);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that moving one of three siblings down and then up yields the expected order.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReorderFolder_ThreeSiblings_DownThenUp_YieldsExpectedOrder()
|
||||
{
|
||||
@@ -367,6 +410,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.Equal(new[] { "B", "C", "A" }, ordered2);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reordering a missing folder fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReorderFolder_NotFound_ReturnsFailure()
|
||||
{
|
||||
@@ -383,6 +428,8 @@ public class TemplateFolderServiceTests
|
||||
// CreateFolderAsync — distinct SortOrder assignment (max+1 among siblings)
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that the first sibling folder gets sort order zero.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateFolder_FirstSibling_GetsSortOrderZero()
|
||||
{
|
||||
@@ -401,6 +448,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.Equal(0, captured!.SortOrder);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a second sibling folder under the same parent gets sort order one.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateFolder_SecondSiblingUnderSameParent_GetsSortOrderOne()
|
||||
{
|
||||
@@ -420,6 +469,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.Equal(1, captured!.SortOrder);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that folder sort orders do not interfere across different parents.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateFolder_SortOrdersDoNotInterferAcrossParents()
|
||||
{
|
||||
@@ -442,6 +493,8 @@ public class TemplateFolderServiceTests
|
||||
Assert.Equal(0, captured!.SortOrder);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reordering two sibling folders visibly changes their order.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateFolder_TwoSiblingsReorderIsVisiblyEffective()
|
||||
{
|
||||
@@ -471,6 +524,8 @@ public class TemplateFolderServiceTests
|
||||
_repoMock.Verify(r => r.UpdateFolderAsync(second, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reordering a folder persists the change and writes an audit entry.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReorderFolder_Persists_And_WritesAudit()
|
||||
{
|
||||
@@ -486,7 +541,7 @@ public class TemplateFolderServiceTests
|
||||
// Both swapped siblings persisted.
|
||||
_repoMock.Verify(r => r.UpdateFolderAsync(a, It.IsAny<CancellationToken>()), Times.Once);
|
||||
_repoMock.Verify(r => r.UpdateFolderAsync(b, It.IsAny<CancellationToken>()), Times.Once);
|
||||
// Two saves: the swapped siblings, then the staged audit row (TemplateEngine-024).
|
||||
// Two saves: the swapped siblings, then the staged audit row.
|
||||
_repoMock.Verify(r => r.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||
// Audit entry written (mirrors Move/Rename audit assertions).
|
||||
_auditMock.Verify(au => au.LogAsync("admin", "Reorder", "TemplateFolder", "2", "B",
|
||||
@@ -494,11 +549,13 @@ public class TemplateFolderServiceTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// TemplateEngine-024 — each folder mutator must SaveChanges *after* LogAsync
|
||||
// Each folder mutator must SaveChanges *after* LogAsync
|
||||
// so the staged audit row is persisted (and not discarded when the scope is
|
||||
// disposed). Verified by tracking call order across the mutators.
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that creating a folder persists the audit row by saving after logging.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateFolder_PersistsAuditRow_SaveFollowsLog()
|
||||
{
|
||||
@@ -508,6 +565,8 @@ public class TemplateFolderServiceTests
|
||||
.ReturnsAsync(new List<TemplateFolder>())));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that renaming a folder persists the audit row by saving after logging.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RenameFolder_PersistsAuditRow_SaveFollowsLog()
|
||||
{
|
||||
@@ -522,6 +581,8 @@ public class TemplateFolderServiceTests
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that moving a folder persists the audit row by saving after logging.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveFolder_PersistsAuditRow_SaveFollowsLog()
|
||||
{
|
||||
@@ -538,6 +599,8 @@ public class TemplateFolderServiceTests
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reordering a folder persists the audit row by saving after logging.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReorderFolder_PersistsAuditRow_SaveFollowsLog()
|
||||
{
|
||||
@@ -553,6 +616,8 @@ public class TemplateFolderServiceTests
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting a folder persists the audit row by saving after logging.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteFolder_PersistsAuditRow_SaveFollowsLog()
|
||||
{
|
||||
@@ -593,7 +658,7 @@ public class TemplateFolderServiceTests
|
||||
/// <summary>
|
||||
/// Asserts the mutator logged an audit entry and then issued a
|
||||
/// <c>SaveChangesAsync</c> after it — proving the staged audit row is
|
||||
/// persisted rather than discarded (TemplateEngine-024).
|
||||
/// persisted rather than discarded.
|
||||
/// </summary>
|
||||
private static void AssertAuditRowPersisted(List<string> calls)
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ public class SharedScriptServiceTests
|
||||
private readonly Mock<IAuditService> _auditMock;
|
||||
private readonly SharedScriptService _service;
|
||||
|
||||
/// <summary>Initializes the test class with mocked repository and audit services.</summary>
|
||||
public SharedScriptServiceTests()
|
||||
{
|
||||
_repoMock = new Mock<ITemplateEngineRepository>();
|
||||
@@ -19,6 +20,9 @@ public class SharedScriptServiceTests
|
||||
_service = new SharedScriptService(_repoMock.Object, _auditMock.Object);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that create and update both reject invalid code before saving.</summary>
|
||||
/// <param name="code">The invalid script code.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("return System.IO.File.ReadAllText(\"x\");")]
|
||||
[InlineData("return noSuchVariable;")]
|
||||
@@ -34,6 +38,8 @@ public class SharedScriptServiceTests
|
||||
_repoMock.Verify(r => r.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a shared script succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateSharedScript_Success()
|
||||
{
|
||||
@@ -48,6 +54,8 @@ public class SharedScriptServiceTests
|
||||
_repoMock.Verify(r => r.AddSharedScriptAsync(It.IsAny<SharedScript>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a shared script with an empty name fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateSharedScript_EmptyName_Fails()
|
||||
{
|
||||
@@ -56,6 +64,8 @@ public class SharedScriptServiceTests
|
||||
Assert.Contains("name is required", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a shared script with empty code fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateSharedScript_EmptyCode_Fails()
|
||||
{
|
||||
@@ -64,6 +74,8 @@ public class SharedScriptServiceTests
|
||||
Assert.Contains("code is required", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a shared script with a duplicate name fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateSharedScript_DuplicateName_Fails()
|
||||
{
|
||||
@@ -76,6 +88,8 @@ public class SharedScriptServiceTests
|
||||
Assert.Contains("already exists", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that creating a shared script with unbalanced braces fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateSharedScript_UnbalancedBraces_Fails()
|
||||
{
|
||||
@@ -88,6 +102,8 @@ public class SharedScriptServiceTests
|
||||
Assert.Contains("Syntax error", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that updating a shared script succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateSharedScript_Success()
|
||||
{
|
||||
@@ -100,6 +116,8 @@ public class SharedScriptServiceTests
|
||||
Assert.Equal("return 42;", result.Value.Code);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that updating a shared script that does not exist fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateSharedScript_NotFound_Fails()
|
||||
{
|
||||
@@ -111,6 +129,8 @@ public class SharedScriptServiceTests
|
||||
Assert.Contains("not found", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting a shared script succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteSharedScript_Success()
|
||||
{
|
||||
@@ -123,6 +143,8 @@ public class SharedScriptServiceTests
|
||||
_repoMock.Verify(r => r.DeleteSharedScriptAsync(1, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting a shared script that does not exist fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteSharedScript_NotFound_Fails()
|
||||
{
|
||||
@@ -136,6 +158,9 @@ public class SharedScriptServiceTests
|
||||
|
||||
// Syntax validation unit tests
|
||||
|
||||
/// <summary>Verifies that syntax validation of valid code returns the expected result.</summary>
|
||||
/// <param name="code">The script code to validate.</param>
|
||||
/// <param name="expected">The expected validation result, or null when the code is valid.</param>
|
||||
[Theory]
|
||||
[InlineData("return 42;", null)]
|
||||
[InlineData("public void Run() { }", null)]
|
||||
@@ -146,6 +171,8 @@ public class SharedScriptServiceTests
|
||||
Assert.Equal(expected, SharedScriptService.ValidateSyntax(code));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that syntax validation of invalid code returns an error.</summary>
|
||||
/// <param name="code">The invalid script code.</param>
|
||||
[Theory]
|
||||
[InlineData("public void Run() {")]
|
||||
[InlineData("return a + b);")]
|
||||
@@ -157,8 +184,10 @@ public class SharedScriptServiceTests
|
||||
Assert.Contains("Syntax error", result);
|
||||
}
|
||||
|
||||
// --- TemplateEngine-007 regression: string/comment-literal awareness ---
|
||||
// --- Regression: string/comment-literal awareness ---
|
||||
|
||||
/// <summary>Verifies that delimiters inside strings or comments do not cause a syntax validation error.</summary>
|
||||
/// <param name="code">The script code containing delimiters inside a string or comment.</param>
|
||||
[Theory]
|
||||
[InlineData("var s = \"a } brace\"; { }")] // brace inside a normal string
|
||||
[InlineData("var s = \"a ) paren ] bracket\";")] // paren/bracket inside a string
|
||||
@@ -172,14 +201,16 @@ public class SharedScriptServiceTests
|
||||
Assert.Null(SharedScriptService.ValidateSyntax(code));
|
||||
}
|
||||
|
||||
// --- TemplateEngine-020 regression: audit row carries the real script Id ---
|
||||
// --- Regression: audit row carries the real script Id ---
|
||||
|
||||
/// <summary>Verifies that the create audit row carries the real script id rather than a literal zero.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateSharedScript_AuditRowCarriesRealScriptIdNotLiteralZero()
|
||||
{
|
||||
// Pre-020: AddSharedScriptAsync → LogAsync("0", ...) → SaveChangesAsync.
|
||||
// Before the fix: AddSharedScriptAsync → LogAsync("0", ...) → SaveChangesAsync.
|
||||
// The audit row was queued with EntityId = "0" because EF Core had
|
||||
// not yet populated the auto-generated key. Post-020: save first,
|
||||
// not yet populated the auto-generated key. After the fix: save first,
|
||||
// then log with the real Id, then save the audit row.
|
||||
_repoMock.Setup(r => r.GetSharedScriptByNameAsync("Helpers", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((SharedScript?)null);
|
||||
|
||||
+19
-1
@@ -7,7 +7,7 @@ using ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening;
|
||||
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the read-only authoring resolve service (M9/T26a). The resolved
|
||||
/// Tests for the read-only authoring resolve service. The resolved
|
||||
/// inherited member set MUST agree with what <see cref="FlatteningService"/>
|
||||
/// produces on deploy — the precedence is exercised both directly and via a
|
||||
/// cross-check against the flattener.
|
||||
@@ -29,6 +29,7 @@ public class TemplateInheritanceResolverTests
|
||||
|
||||
// ── (a) A→B→C chain: resolving C returns members inherited transitively from A ──
|
||||
|
||||
/// <summary>Verifies that a three-level chain includes a transitively inherited grandparent member.</summary>
|
||||
[Fact]
|
||||
public void Resolve_ThreeLevelChain_IncludesTransitivelyInheritedGrandparentMember()
|
||||
{
|
||||
@@ -62,6 +63,7 @@ public class TemplateInheritanceResolverTests
|
||||
|
||||
// ── (b) a base member ADDED to A after C was created appears in C's resolved set ──
|
||||
|
||||
/// <summary>Verifies that a base member added after the derived template was created appears in the resolved set.</summary>
|
||||
[Fact]
|
||||
public void Resolve_BaseMemberAddedAfterDerivedCreated_AppearsInResolvedSet()
|
||||
{
|
||||
@@ -86,6 +88,7 @@ public class TemplateInheritanceResolverTests
|
||||
|
||||
// ── (c) a locked member is flagged ──
|
||||
|
||||
/// <summary>Verifies that locked and base-locked members are flagged.</summary>
|
||||
[Fact]
|
||||
public void Resolve_LockedAndBaseLockedMembers_Flagged()
|
||||
{
|
||||
@@ -107,6 +110,7 @@ public class TemplateInheritanceResolverTests
|
||||
|
||||
// ── (d) own override wins over inherited ──
|
||||
|
||||
/// <summary>Verifies that an own override wins over an inherited member.</summary>
|
||||
[Fact]
|
||||
public void Resolve_OwnOverride_WinsOverInherited()
|
||||
{
|
||||
@@ -125,6 +129,7 @@ public class TemplateInheritanceResolverTests
|
||||
Assert.False(speed.IsInherited);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an inherited placeholder does not shadow the live base value.</summary>
|
||||
[Fact]
|
||||
public void Resolve_InheritedPlaceholder_DoesNotShadowLiveBaseValue()
|
||||
{
|
||||
@@ -146,6 +151,7 @@ public class TemplateInheritanceResolverTests
|
||||
|
||||
// ── (e) staleness summary true when stored rows differ; false when in sync ──
|
||||
|
||||
/// <summary>Verifies that the staleness summary is true when a base-added member is missing from the stored rows.</summary>
|
||||
[Fact]
|
||||
public void Resolve_StalenessSummary_TrueWhenBaseAddedMemberMissingFromStoredRows()
|
||||
{
|
||||
@@ -163,6 +169,7 @@ public class TemplateInheritanceResolverTests
|
||||
Assert.Equal(1, resolved.Staleness.DifferingMemberCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the staleness summary is false when the stored rows are in sync.</summary>
|
||||
[Fact]
|
||||
public void Resolve_StalenessSummary_FalseWhenStoredRowsInSync()
|
||||
{
|
||||
@@ -179,6 +186,7 @@ public class TemplateInheritanceResolverTests
|
||||
Assert.Equal(0, resolved.Staleness.DifferingMemberCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the staleness summary is true when a stored inherited value is stale.</summary>
|
||||
[Fact]
|
||||
public void Resolve_StalenessSummary_TrueWhenStoredInheritedValueIsStale()
|
||||
{
|
||||
@@ -196,6 +204,7 @@ public class TemplateInheritanceResolverTests
|
||||
|
||||
// ── (f) a composition-derived template resolves sanely ──
|
||||
|
||||
/// <summary>Verifies that a composition-derived template resolves its own and inherited members.</summary>
|
||||
[Fact]
|
||||
public void Resolve_CompositionDerivedTemplate_ResolvesOwnAndInheritedMembers()
|
||||
{
|
||||
@@ -216,6 +225,7 @@ public class TemplateInheritanceResolverTests
|
||||
Assert.Contains(resolved.Attributes, m => m.Name == "LocalTweak" && !m.IsInherited);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that resolving an unknown template returns an empty set.</summary>
|
||||
[Fact]
|
||||
public void Resolve_UnknownTemplate_ReturnsEmptySet()
|
||||
{
|
||||
@@ -226,6 +236,7 @@ public class TemplateInheritanceResolverTests
|
||||
|
||||
// ── Cross-member-type coverage: alarms, scripts, native alarm sources ──
|
||||
|
||||
/// <summary>Verifies that all member types are transitively inherited.</summary>
|
||||
[Fact]
|
||||
public void Resolve_AllMemberTypes_TransitivelyInherited()
|
||||
{
|
||||
@@ -250,6 +261,7 @@ public class TemplateInheritanceResolverTests
|
||||
// output for a no-override instance over the same chain. This is the
|
||||
// guarantee that the editor preview agrees with deploy. ──
|
||||
|
||||
/// <summary>Verifies that the resolver agrees with the flattening service for an instance with no overrides.</summary>
|
||||
[Fact]
|
||||
public void Resolve_AgreesWithFlatteningService_ForNoOverrideInstance()
|
||||
{
|
||||
@@ -295,6 +307,7 @@ public class TemplateInheritanceResolverTests
|
||||
// TriggerConfiguration MUST equal the FlatteningService-merged result for
|
||||
// a derived template that overrides only one setpoint (preview = deploy). ──
|
||||
|
||||
/// <summary>Verifies that the resolver agrees with the flattening service for a partial high/low override.</summary>
|
||||
[Fact]
|
||||
public void Resolve_AgreesWithFlatteningService_ForPartialHiLoOverride()
|
||||
{
|
||||
@@ -359,6 +372,7 @@ public class TemplateInheritanceResolverTests
|
||||
// must NOT attempt the per-setpoint merge (guard matches the flattener's
|
||||
// both-HiLo gate). Derived config must win verbatim (whole-replace). ──
|
||||
|
||||
/// <summary>Verifies that when the base is not high/low and the derived template is, the derived config wins verbatim.</summary>
|
||||
[Fact]
|
||||
public void Resolve_BaseNonHiLo_DerivedHiLo_DerivedConfigWinsVerbatim()
|
||||
{
|
||||
@@ -418,6 +432,7 @@ public class TemplateInheritanceResolverTests
|
||||
// ── I2: staleness must flag drift on the fields that actually matter, not
|
||||
// just one scalar per type. ──
|
||||
|
||||
/// <summary>Verifies that the staleness summary is true when a base alarm trigger config changed but the priority did not.</summary>
|
||||
[Fact]
|
||||
public void Resolve_StalenessSummary_TrueWhenBaseAlarmTriggerConfigChanged_PriorityUnchanged()
|
||||
{
|
||||
@@ -451,6 +466,7 @@ public class TemplateInheritanceResolverTests
|
||||
Assert.Equal(1, resolved.Staleness.DifferingMemberCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the staleness summary is true when a native source connection name changed but the source reference did not.</summary>
|
||||
[Fact]
|
||||
public void Resolve_StalenessSummary_TrueWhenNativeSourceConnectionNameChanged_SourceReferenceUnchanged()
|
||||
{
|
||||
@@ -481,6 +497,7 @@ public class TemplateInheritanceResolverTests
|
||||
Assert.Equal(1, resolved.Staleness.DifferingMemberCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the staleness summary is true when a native source condition filter changed but the source reference did not.</summary>
|
||||
[Fact]
|
||||
public void Resolve_StalenessSummary_TrueWhenNativeSourceConditionFilterChanged_SourceReferenceUnchanged()
|
||||
{
|
||||
@@ -511,6 +528,7 @@ public class TemplateInheritanceResolverTests
|
||||
Assert.Equal(1, resolved.Staleness.DifferingMemberCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the staleness summary is false when the alarm and native source are in sync.</summary>
|
||||
[Fact]
|
||||
public void Resolve_StalenessSummary_FalseWhenAlarmAndNativeSourceInSync()
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@ public class TemplateNamingTests
|
||||
return (byId, compById);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a base template's qualified name is just its own name.</summary>
|
||||
[Fact]
|
||||
public void QualifiedName_BaseTemplate_IsJustItsName()
|
||||
{
|
||||
@@ -29,6 +30,7 @@ public class TemplateNamingTests
|
||||
Assert.Equal("Motor Controller", TemplateNaming.QualifiedName(motorController, byId, compById));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a one-level derived template's qualified name is prefixed with its owner.</summary>
|
||||
[Fact]
|
||||
public void QualifiedName_OneLevelDerived_PrefixesTheOwner()
|
||||
{
|
||||
@@ -45,6 +47,7 @@ public class TemplateNamingTests
|
||||
Assert.Equal("Motor Controller.Pump", TemplateNaming.QualifiedName(derivedPump, byId, compById));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a nested derived template's qualified name walks the whole chain.</summary>
|
||||
[Fact]
|
||||
public void QualifiedName_NestedDerived_WalksTheWholeChain()
|
||||
{
|
||||
@@ -71,6 +74,7 @@ public class TemplateNamingTests
|
||||
TemplateNaming.QualifiedName(derivedTempSensor, byId, compById));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a derived template with a missing owner link falls back to its stored name.</summary>
|
||||
[Fact]
|
||||
public void QualifiedName_DerivedWithMissingOwnerLink_FallsBackToStoredName()
|
||||
{
|
||||
|
||||
@@ -6,9 +6,10 @@ namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests;
|
||||
public class TemplateResolverTests
|
||||
{
|
||||
// ========================================================================
|
||||
// WP-7: Path-Qualified Canonical Naming
|
||||
// Path-Qualified Canonical Naming
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that direct members are resolved without a prefix.</summary>
|
||||
[Fact]
|
||||
public void ResolveAllMembers_DirectMembers_NoPrefix()
|
||||
{
|
||||
@@ -26,6 +27,7 @@ public class TemplateResolverTests
|
||||
Assert.Contains(members, m => m.CanonicalName == "OnStart" && m.MemberType == "Script");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that members of a composed module are resolved with prefixed names.</summary>
|
||||
[Fact]
|
||||
public void ResolveAllMembers_ComposedModule_PrefixedNames()
|
||||
{
|
||||
@@ -42,6 +44,7 @@ public class TemplateResolverTests
|
||||
Assert.Equal("sensor1.Pressure", members[0].CanonicalName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that nested composition produces multi-level prefixes.</summary>
|
||||
[Fact]
|
||||
public void ResolveAllMembers_NestedComposition_MultiLevelPrefix()
|
||||
{
|
||||
@@ -62,9 +65,10 @@ public class TemplateResolverTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// WP-10: Inheritance Override Scope
|
||||
// Inheritance Override Scope
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that inherited members are included.</summary>
|
||||
[Fact]
|
||||
public void ResolveAllMembers_InheritedMembers_Included()
|
||||
{
|
||||
@@ -82,6 +86,7 @@ public class TemplateResolverTests
|
||||
Assert.Contains(members, m => m.CanonicalName == "ExtraAttr");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a child override of a parent member uses the child's version.</summary>
|
||||
[Fact]
|
||||
public void ResolveAllMembers_ChildOverridesParentMember_UsesChildVersion()
|
||||
{
|
||||
@@ -105,6 +110,7 @@ public class TemplateResolverTests
|
||||
Assert.Equal(2, speedMember.SourceTemplateId); // Child's version
|
||||
}
|
||||
|
||||
/// <summary>Verifies that inherited composed modules are included.</summary>
|
||||
[Fact]
|
||||
public void ResolveAllMembers_InheritedComposedModules_Included()
|
||||
{
|
||||
@@ -123,9 +129,10 @@ public class TemplateResolverTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// WP-11: Composition Override Scope
|
||||
// Composition Override Scope
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that a composed member is found by its canonical name.</summary>
|
||||
[Fact]
|
||||
public void FindMemberByCanonicalName_ComposedMember_Found()
|
||||
{
|
||||
@@ -143,6 +150,7 @@ public class TemplateResolverTests
|
||||
Assert.False(member.IsLocked);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a locked composed member is reported as locked.</summary>
|
||||
[Fact]
|
||||
public void FindMemberByCanonicalName_LockedComposedMember_ReturnsLocked()
|
||||
{
|
||||
@@ -159,6 +167,7 @@ public class TemplateResolverTests
|
||||
Assert.True(member.IsLocked);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a three-level inheritance chain is returned root first.</summary>
|
||||
[Fact]
|
||||
public void BuildInheritanceChain_ThreeLevel_RootFirst()
|
||||
{
|
||||
@@ -175,16 +184,17 @@ public class TemplateResolverTests
|
||||
Assert.Equal("C", chain[2].Name);
|
||||
}
|
||||
|
||||
// ── TemplateEngine-019: a real Id of 0 must walk the inheritance chain ──
|
||||
// ── A real Id of 0 must walk the inheritance chain ──
|
||||
|
||||
/// <summary>Verifies that a real template id of zero is treated as a parent reference rather than as no parent.</summary>
|
||||
[Fact]
|
||||
public void BuildInheritanceChain_RealIdZero_IsTreatedAsParentReferenceNotAsNoParent()
|
||||
{
|
||||
// TemplateEngine-013 removed the 0-as-no-parent sentinel from
|
||||
// CycleDetector; the same fix had not propagated into the resolver,
|
||||
// The 0-as-no-parent sentinel was removed from
|
||||
// CycleDetector, but the same fix had not propagated into the resolver,
|
||||
// so seeding BuildInheritanceChain with templateId == 0 returned an
|
||||
// empty chain even when a template with Id 0 existed (e.g. an
|
||||
// import-staging row, or any in-memory test setup). Post-019: only a
|
||||
// import-staging row, or any in-memory test setup). Now only a
|
||||
// null ParentTemplateId means "no parent", and an Id of 0 walks the
|
||||
// chain like any other node.
|
||||
var orphaned = new Template("OrphanedId0") { Id = 0 };
|
||||
@@ -198,19 +208,20 @@ public class TemplateResolverTests
|
||||
var lookup = new Dictionary<int, Template> { [0] = orphaned };
|
||||
var chain = TemplateResolver.BuildInheritanceChain(0, lookup);
|
||||
|
||||
// Pre-019: while (currentId != 0 && ...) was false on the first
|
||||
// Previously, while (currentId != 0 && ...) was false on the first
|
||||
// iteration, so the chain was empty and the orphaned template's
|
||||
// members were silently dropped from every flatten/resolve through
|
||||
// it. Post-019: the orphan is the chain.
|
||||
// it. Now the orphan is the chain.
|
||||
Assert.Single(chain);
|
||||
Assert.Equal("OrphanedId0", chain[0].Name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a parent chain passing through id zero is not truncated at zero.</summary>
|
||||
[Fact]
|
||||
public void BuildInheritanceChain_ParentChainThroughIdZero_DoesNotTruncateChainAtZero()
|
||||
{
|
||||
// A template whose real parent has Id 0 must include the Id 0 parent
|
||||
// in the chain. Pre-019: `current.ParentTemplateId ?? 0` paired with
|
||||
// in the chain. Previously `current.ParentTemplateId ?? 0` paired with
|
||||
// `currentId != 0` exited the loop as soon as the walk reached a real
|
||||
// Id of 0 — silently truncating the inheritance contribution from the
|
||||
// root template.
|
||||
@@ -231,12 +242,13 @@ public class TemplateResolverTests
|
||||
Assert.Equal("Child", chain[1].Name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a template with a real id of zero still resolves its members.</summary>
|
||||
[Fact]
|
||||
public void ResolveAllMembers_TemplateWithRealIdZero_StillResolvesItsMembers()
|
||||
{
|
||||
// End-to-end: ResolveAllMembers piggybacks on BuildInheritanceChain,
|
||||
// so the chain truncation regression also dropped every member of an
|
||||
// Id-0 template from the resolved-member set. Lock this in too.
|
||||
// template with an Id of 0 from the resolved-member set. Lock this in too.
|
||||
var orphan = new Template("OrphanedId0") { Id = 0 };
|
||||
orphan.Attributes.Add(new TemplateAttribute("Speed")
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ public class TemplateServiceTests
|
||||
private readonly Mock<IAuditService> _auditMock;
|
||||
private readonly TemplateService _service;
|
||||
|
||||
/// <summary>Initializes the test with mocked repositories and the service under test.</summary>
|
||||
public TemplateServiceTests()
|
||||
{
|
||||
_repoMock = new Mock<ITemplateEngineRepository>();
|
||||
@@ -23,9 +24,11 @@ public class TemplateServiceTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// WP-1: Template CRUD with Inheritance
|
||||
// Template CRUD with Inheritance
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that create template, success.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateTemplate_Success()
|
||||
{
|
||||
@@ -34,7 +37,7 @@ public class TemplateServiceTests
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal("Pump", result.Value.Name);
|
||||
_repoMock.Verify(r => r.AddTemplateAsync(It.IsAny<Template>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
// TemplateEngine-020: SaveChangesAsync runs twice — once after the
|
||||
// SaveChangesAsync runs twice — once after the
|
||||
// AddTemplateAsync to populate the auto-generated key (so the audit
|
||||
// row can carry the real Id), and once after LogAsync to persist the
|
||||
// audit row itself.
|
||||
@@ -42,6 +45,8 @@ public class TemplateServiceTests
|
||||
_auditMock.Verify(a => a.LogAsync("admin", "Create", "Template", It.IsAny<string>(), "Pump", It.IsAny<object?>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that create template, empty name, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateTemplate_EmptyName_Fails()
|
||||
{
|
||||
@@ -50,6 +55,8 @@ public class TemplateServiceTests
|
||||
Assert.Contains("required", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that create template, with parent, success.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateTemplate_WithParent_Success()
|
||||
{
|
||||
@@ -66,6 +73,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal(1, result.Value.ParentTemplateId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that create template, with parent, does not run dead collision query.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateTemplate_WithParent_DoesNotRunDeadCollisionQuery()
|
||||
{
|
||||
@@ -87,6 +96,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that create template, nonexistent parent, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateTemplate_NonexistentParent_Fails()
|
||||
{
|
||||
@@ -97,6 +108,8 @@ public class TemplateServiceTests
|
||||
Assert.Contains("not found", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that create template, with folder id, sets folder id.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateTemplate_WithFolderId_SetsFolderId()
|
||||
{
|
||||
@@ -109,6 +122,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal(7, result.Value.FolderId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that delete template, success.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteTemplate_Success()
|
||||
{
|
||||
@@ -127,6 +142,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.DeleteTemplateAsync(1, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that delete template, referenced by instances, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteTemplate_ReferencedByInstances_Fails()
|
||||
{
|
||||
@@ -145,6 +162,8 @@ public class TemplateServiceTests
|
||||
Assert.Contains("instance(s) reference it", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that delete template, has children, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteTemplate_HasChildren_Fails()
|
||||
{
|
||||
@@ -164,6 +183,8 @@ public class TemplateServiceTests
|
||||
Assert.Contains("child template(s) inherit from it", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that delete template, composed by other, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteTemplate_ComposedByOther_Fails()
|
||||
{
|
||||
@@ -185,10 +206,12 @@ public class TemplateServiceTests
|
||||
Assert.Contains("template(s) compose it", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that delete template, multiple constraints, reports all not just first.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteTemplate_MultipleConstraints_ReportsAllNotJustFirst()
|
||||
{
|
||||
// TemplateEngine-014: DeleteTemplateAsync delegates its constraint check
|
||||
// DeleteTemplateAsync delegates its constraint check
|
||||
// to the single TemplateDeletionService implementation, which accumulates
|
||||
// every blocking reason instead of returning on the first failing category.
|
||||
var template = new Template("Busy") { Id = 1 };
|
||||
@@ -222,9 +245,11 @@ public class TemplateServiceTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// WP-2: Attribute Definitions with Lock Flags
|
||||
// Attribute Definitions with Lock Flags
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that add attribute, success.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddAttribute_Success()
|
||||
{
|
||||
@@ -243,10 +268,12 @@ public class TemplateServiceTests
|
||||
Assert.Equal(1, result.Value.TemplateId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add attribute, to derived template with inherited placeholders, succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddAttribute_ToDerivedTemplateWithInheritedPlaceholders_Succeeds()
|
||||
{
|
||||
// Follow-up #3 (end-to-end). Adding a member to a derived template used to
|
||||
// End-to-end: adding a member to a derived template used to
|
||||
// fail: CloneTemplateWithNewAttribute carries the child's IsInherited
|
||||
// placeholder rows into DetectCollisions, which counted each one twice (child
|
||||
// origin + the parent the inheritance walk re-adds) and reported a spurious
|
||||
@@ -276,6 +303,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal(8, result.Value.TemplateId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add attribute, duplicate name, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddAttribute_DuplicateName_Fails()
|
||||
{
|
||||
@@ -290,10 +319,12 @@ public class TemplateServiceTests
|
||||
Assert.Contains("already exists", result.Error);
|
||||
}
|
||||
|
||||
// ── #92: (DataType, ElementDataType, Value) validation hoisted into the
|
||||
// ── (DataType, ElementDataType, Value) validation hoisted into the
|
||||
// service so the Central UI's direct write path shares the same
|
||||
// server-side guard as ManagementActor.ValidateAttributeTypes. ──
|
||||
|
||||
/// <summary>Verifies that add attribute, invalid data type, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddAttribute_InvalidDataType_Fails()
|
||||
{
|
||||
@@ -312,6 +343,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.AddTemplateAttributeAsync(It.IsAny<TemplateAttribute>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add attribute, list missing element type, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddAttribute_ListMissingElementType_Fails()
|
||||
{
|
||||
@@ -326,6 +359,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.AddTemplateAttributeAsync(It.IsAny<TemplateAttribute>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add attribute, list invalid element type, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddAttribute_ListInvalidElementType_Fails()
|
||||
{
|
||||
@@ -341,6 +376,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.AddTemplateAttributeAsync(It.IsAny<TemplateAttribute>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add attribute, scalar with element type, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddAttribute_ScalarWithElementType_Fails()
|
||||
{
|
||||
@@ -355,6 +392,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.AddTemplateAttributeAsync(It.IsAny<TemplateAttribute>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add attribute, list value does not decode, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddAttribute_ListValueDoesNotDecode_Fails()
|
||||
{
|
||||
@@ -373,6 +412,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.AddTemplateAttributeAsync(It.IsAny<TemplateAttribute>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add attribute, scalar value does not parse, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddAttribute_ScalarValueDoesNotParse_Fails()
|
||||
{
|
||||
@@ -396,6 +437,8 @@ public class TemplateServiceTests
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add attribute, valid typed list, succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddAttribute_ValidTypedList_Succeeds()
|
||||
{
|
||||
@@ -418,6 +461,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal(DataType.Int32, result.Value.ElementDataType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update attribute, invalid list value, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateAttribute_InvalidListValue_Fails()
|
||||
{
|
||||
@@ -444,6 +489,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.UpdateTemplateAttributeAsync(It.IsAny<TemplateAttribute>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update attribute, valid typed list value, succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateAttribute_ValidTypedListValue_Succeeds()
|
||||
{
|
||||
@@ -466,9 +513,11 @@ public class TemplateServiceTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// WP-3: Alarm Definitions
|
||||
// Alarm Definitions
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that add alarm, success.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddAlarm_Success()
|
||||
{
|
||||
@@ -491,6 +540,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal("HighTemp", result.Value.Name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add alarm, invalid priority, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddAlarm_InvalidPriority_Fails()
|
||||
{
|
||||
@@ -504,6 +555,8 @@ public class TemplateServiceTests
|
||||
Assert.Contains("priority", result.Error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update alarm, trigger type fixed, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateAlarm_TriggerTypeFixed_Fails()
|
||||
{
|
||||
@@ -530,6 +583,8 @@ public class TemplateServiceTests
|
||||
Assert.Contains("TriggerType", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update alarm async, copies is mes reportable.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateAlarmAsync_CopiesIsMesReportable()
|
||||
{
|
||||
@@ -561,6 +616,8 @@ public class TemplateServiceTests
|
||||
It.Is<TemplateAlarm>(a => a.IsMesReportable), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update alarm async, clears is mes reportable.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateAlarmAsync_ClearsIsMesReportable()
|
||||
{
|
||||
@@ -590,9 +647,11 @@ public class TemplateServiceTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// WP-4: Script Definitions
|
||||
// Script Definitions
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that add script, success.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddScript_Success()
|
||||
{
|
||||
@@ -610,6 +669,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal("OnStart", result.Value.Name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update script, name fixed, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateScript_NameFixed_Fails()
|
||||
{
|
||||
@@ -625,10 +686,12 @@ public class TemplateServiceTests
|
||||
Assert.Contains("Name", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update script, ui edit path, preserves existing execution timeout seconds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateScript_UiEditPath_PreservesExistingExecutionTimeoutSeconds()
|
||||
{
|
||||
// #54: the UI now has an ExecutionTimeoutSeconds input, but it still loads
|
||||
// The UI now has an ExecutionTimeoutSeconds input, but it still loads
|
||||
// (round-trips) the existing value into that input on edit, so leaving it
|
||||
// untouched re-sends the same value rather than null. This test proves the
|
||||
// service preserves the timeout end-to-end when the loaded value is re-sent
|
||||
@@ -655,10 +718,12 @@ public class TemplateServiceTests
|
||||
Assert.Equal(30, result.Value.ExecutionTimeoutSeconds);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update script, explicit null timeout, clears execution timeout seconds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateScript_ExplicitNullTimeout_ClearsExecutionTimeoutSeconds()
|
||||
{
|
||||
// M2.5 (#9): a deliberate clear (e.g. via Transport or CLI setting null
|
||||
// A deliberate clear (e.g. via Transport or CLI setting null
|
||||
// explicitly) must still work — the service must not guard against null.
|
||||
var existing = new TemplateScript("OnStart", "return true;")
|
||||
{
|
||||
@@ -683,13 +748,15 @@ public class TemplateServiceTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// WP-5: Shared Script CRUD (see SharedScriptServiceTests)
|
||||
// Shared Script CRUD (see SharedScriptServiceTests)
|
||||
// ========================================================================
|
||||
|
||||
// ========================================================================
|
||||
// WP-6: Composition with Recursive Nesting
|
||||
// Composition with Recursive Nesting
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that add composition, success, derives template.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddComposition_Success_DerivesTemplate()
|
||||
{
|
||||
@@ -726,6 +793,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.AddTemplateCompositionAsync(It.IsAny<TemplateComposition>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add composition, cascades child compositions.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddComposition_CascadesChildCompositions()
|
||||
{
|
||||
@@ -771,6 +840,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal("Probe1", capturedCompositions[1].InstanceName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add composition, base is derived, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddComposition_BaseIsDerived_Fails()
|
||||
{
|
||||
@@ -786,6 +857,8 @@ public class TemplateServiceTests
|
||||
Assert.Contains("derived template", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add composition, derived contained name, need not be globally unique.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddComposition_DerivedContainedName_NeedNotBeGloballyUnique()
|
||||
{
|
||||
@@ -816,6 +889,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal("myModule", captured[0].Name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that rename composition, renames slot and derived template.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RenameComposition_RenamesSlotAndDerivedTemplate()
|
||||
{
|
||||
@@ -837,6 +912,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal("NewSlot", derived.Name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that rename composition, renames only the slots own derived template.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RenameComposition_RenamesOnlyTheSlotsOwnDerivedTemplate()
|
||||
{
|
||||
@@ -867,6 +944,8 @@ public class TemplateServiceTests
|
||||
It.Is<Template>(t => t.Id == 78), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that rename composition, duplicate name, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RenameComposition_DuplicateName_Fails()
|
||||
{
|
||||
@@ -885,6 +964,8 @@ public class TemplateServiceTests
|
||||
Assert.Contains("already exists", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that delete composition, cascades nested derived templates.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteComposition_CascadesNestedDerivedTemplates()
|
||||
{
|
||||
@@ -912,6 +993,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.DeleteTemplateAsync(78, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that delete composition, cascades derived template.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteComposition_CascadesDerivedTemplate()
|
||||
{
|
||||
@@ -928,6 +1011,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.DeleteTemplateAsync(77, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that delete composition, legacy direct base, does not cascade.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteComposition_LegacyDirectBase_DoesNotCascade()
|
||||
{
|
||||
@@ -944,6 +1029,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.DeleteTemplateAsync(5, It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that delete template, derived template, direct delete blocked.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteTemplate_DerivedTemplate_DirectDeleteBlocked()
|
||||
{
|
||||
@@ -956,6 +1043,8 @@ public class TemplateServiceTests
|
||||
Assert.Contains("derived template", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update attribute, locked in derived base, rejects on derived.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateAttribute_LockedInDerivedBase_RejectsOnDerived()
|
||||
{
|
||||
@@ -979,6 +1068,8 @@ public class TemplateServiceTests
|
||||
Assert.Contains("locked by base template 'Sensor'", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update script, locked in derived base, rejects on derived.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateScript_LockedInDerivedBase_RejectsOnDerived()
|
||||
{
|
||||
@@ -1002,6 +1093,8 @@ public class TemplateServiceTests
|
||||
Assert.Contains("locked by base template 'Sensor'", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add composition, copies alarms as inherited.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddComposition_CopiesAlarmsAsInherited()
|
||||
{
|
||||
@@ -1043,6 +1136,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal("Too hot", copied.Description);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update alarm, locked in derived base, rejects on derived.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateAlarm_LockedInDerivedBase_RejectsOnDerived()
|
||||
{
|
||||
@@ -1084,6 +1179,8 @@ public class TemplateServiceTests
|
||||
Assert.Contains("locked by base template 'Sensor'", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update alarm, derived override, persists is inherited false.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateAlarm_DerivedOverride_PersistsIsInheritedFalse()
|
||||
{
|
||||
@@ -1125,6 +1222,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal(99, result.Value.PriorityLevel);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update attribute, derived override, persists is inherited false.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateAttribute_DerivedOverride_PersistsIsInheritedFalse()
|
||||
{
|
||||
@@ -1149,6 +1248,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal("99", result.Value.Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that delete template, base with derivatives, blocked with slot names.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteTemplate_BaseWithDerivatives_BlockedWithSlotNames()
|
||||
{
|
||||
@@ -1172,6 +1273,8 @@ public class TemplateServiceTests
|
||||
Assert.Contains("'Pump' (as 'TempSensor')", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add composition, duplicate instance name, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddComposition_DuplicateInstanceName_Fails()
|
||||
{
|
||||
@@ -1188,6 +1291,8 @@ public class TemplateServiceTests
|
||||
Assert.Contains("already exists", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add composition, self composition, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddComposition_SelfComposition_Fails()
|
||||
{
|
||||
@@ -1205,9 +1310,11 @@ public class TemplateServiceTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// WP-9: Locking Rules
|
||||
// Locking Rules
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that update attribute, locked member, cannot unlock.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateAttribute_LockedMember_CannotUnlock()
|
||||
{
|
||||
@@ -1229,6 +1336,8 @@ public class TemplateServiceTests
|
||||
Assert.Contains("cannot be unlocked", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update attribute, lock unlocked member, succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateAttribute_LockUnlockedMember_Succeeds()
|
||||
{
|
||||
@@ -1250,6 +1359,8 @@ public class TemplateServiceTests
|
||||
Assert.True(result.Value.IsLocked);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update attribute, unlocked attribute, data type change rejected.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateAttribute_UnlockedAttribute_DataTypeChangeRejected()
|
||||
{
|
||||
@@ -1274,6 +1385,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal(DataType.Float, existing.DataType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update attribute, unlocked attribute, data source reference change rejected.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateAttribute_UnlockedAttribute_DataSourceReferenceChangeRejected()
|
||||
{
|
||||
@@ -1298,6 +1411,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal("/Motor/Temp", existing.DataSourceReference);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update attribute, parent locked, cannot override.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateAttribute_ParentLocked_CannotOverride()
|
||||
{
|
||||
@@ -1334,9 +1449,11 @@ public class TemplateServiceTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// WP-10: Inheritance Override Scope — Cannot remove parent members
|
||||
// Inheritance Override Scope — Cannot remove parent members
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that delete attribute, inherited from parent, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteAttribute_InheritedFromParent_Fails()
|
||||
{
|
||||
@@ -1366,6 +1483,8 @@ public class TemplateServiceTests
|
||||
Assert.Contains("inherited from parent", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that delete attribute, own member, succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteAttribute_OwnMember_Succeeds()
|
||||
{
|
||||
@@ -1385,9 +1504,11 @@ public class TemplateServiceTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// WP-13: Graph Acyclicity
|
||||
// Graph Acyclicity
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that update template, change parent, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateTemplate_ChangeParent_Fails()
|
||||
{
|
||||
@@ -1403,10 +1524,12 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.UpdateTemplateAsync(It.IsAny<Template>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update template, omitted parent and description, leaves unchanged.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateTemplate_OmittedParentAndDescription_LeavesUnchanged()
|
||||
{
|
||||
// Followup #5: a null parent/description means "leave unchanged" (an omitted
|
||||
// A null parent/description means "leave unchanged" (an omitted
|
||||
// CLI/API field), not an attempt to clear. Renaming without re-passing the
|
||||
// description must not wipe it, and must not trip the parent-immutability guard.
|
||||
var child = new Template("Child") { Id = 2, ParentTemplateId = 1, Description = "keep me" };
|
||||
@@ -1425,6 +1548,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.UpdateTemplateAsync(It.IsAny<Template>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update template, empty description, clears it.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateTemplate_EmptyDescription_ClearsIt()
|
||||
{
|
||||
@@ -1443,6 +1568,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.UpdateTemplateAsync(It.IsAny<Template>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update template, same parent, succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateTemplate_SameParent_Succeeds()
|
||||
{
|
||||
@@ -1463,6 +1590,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.UpdateTemplateAsync(It.IsAny<Template>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add composition, circular chain, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddComposition_CircularChain_Fails()
|
||||
{
|
||||
@@ -1490,6 +1619,8 @@ public class TemplateServiceTests
|
||||
// Move template between folders
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that move template, to folder, returns success.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveTemplate_ToFolder_ReturnsSuccess()
|
||||
{
|
||||
@@ -1508,6 +1639,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal(7, result.Value.FolderId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that move template, to root, returns success.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveTemplate_ToRoot_ReturnsSuccess()
|
||||
{
|
||||
@@ -1524,6 +1657,8 @@ public class TemplateServiceTests
|
||||
Assert.Null(result.Value.FolderId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that move template, target folder missing, returns failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveTemplate_TargetFolderMissing_ReturnsFailure()
|
||||
{
|
||||
@@ -1537,10 +1672,12 @@ public class TemplateServiceTests
|
||||
Assert.Contains("not found", result.Error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that move template, name collision at destination, fails.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveTemplate_NameCollisionAtDestination_Fails()
|
||||
{
|
||||
// TemplateEngine-021: moving "Pump" into a folder that already contains a
|
||||
// Moving "Pump" into a folder that already contains a
|
||||
// template named "Pump" (case-insensitive) must be rejected before the
|
||||
// FolderId is persisted. Mirrors TemplateFolderService.MoveFolderAsync
|
||||
// sibling-name uniqueness, and pins the design rule that naming
|
||||
@@ -1564,10 +1701,12 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.UpdateTemplateAsync(It.IsAny<Template>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that move template, no collision at destination, succeeds.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task MoveTemplate_NoCollisionAtDestination_Succeeds()
|
||||
{
|
||||
// TemplateEngine-021 companion: a sibling with the same name in a
|
||||
// Companion case: a sibling with the same name in a
|
||||
// *different* folder is not a collision. The move must succeed.
|
||||
var moving = new Template("Pump") { Id = 1, FolderId = null };
|
||||
var unrelated = new Template("Pump") { Id = 2, FolderId = 8 }; // different folder
|
||||
@@ -1585,10 +1724,12 @@ public class TemplateServiceTests
|
||||
Assert.Equal(7, result.Value.FolderId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that update attribute, locked in derived downgrade, on base, rejected.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateAttribute_LockedInDerivedDowngrade_OnBase_Rejected()
|
||||
{
|
||||
// TemplateEngine-022: LockedInDerived is a one-way ratchet on base
|
||||
// LockedInDerived is a one-way ratchet on base
|
||||
// templates. Once true, it cannot be cleared — otherwise existing
|
||||
// derived overrides that were previously blocked would become
|
||||
// retroactively legal.
|
||||
@@ -1616,20 +1757,22 @@ public class TemplateServiceTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// TemplateEngine-020: Create* audit rows must carry the real entity Id,
|
||||
// Create* audit rows must carry the real entity Id,
|
||||
// not the literal "0" that EF Core leaves on the entity until
|
||||
// SaveChangesAsync populates the auto-generated key.
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>Verifies that create template, audit row carries real template id not literal zero.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateTemplate_AuditRowCarriesRealTemplateIdNotLiteralZero()
|
||||
{
|
||||
// Simulate EF Core's identity-key population: the repository's
|
||||
// AddTemplateAsync stamps Id during SaveChangesAsync. The pre-020
|
||||
// AddTemplateAsync stamps Id during SaveChangesAsync. The previous
|
||||
// order (Add → LogAsync → Save) queued the audit row before the Id
|
||||
// existed, so every Create audit entry persisted with EntityId = "0"
|
||||
// and the audit trail lost its link back to the created template.
|
||||
// Post-020: save first, then log with the real Id, then save the
|
||||
// Current order: save first, then log with the real Id, then save the
|
||||
// audit row.
|
||||
Template? added = null;
|
||||
_repoMock.Setup(r => r.AddTemplateAsync(It.IsAny<Template>(), It.IsAny<CancellationToken>()))
|
||||
@@ -1663,6 +1806,8 @@ public class TemplateServiceTests
|
||||
Assert.NotEqual("0", auditedEntityId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add attribute, audit row carries real attribute id not literal zero.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddAttribute_AuditRowCarriesRealAttributeIdNotLiteralZero()
|
||||
{
|
||||
@@ -1705,6 +1850,8 @@ public class TemplateServiceTests
|
||||
Assert.NotEqual("0", auditedEntityId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add alarm, audit row carries real alarm id not literal zero.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddAlarm_AuditRowCarriesRealAlarmIdNotLiteralZero()
|
||||
{
|
||||
@@ -1752,6 +1899,8 @@ public class TemplateServiceTests
|
||||
Assert.NotEqual("0", auditedEntityId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add script, audit row carries real script id not literal zero.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddScript_AuditRowCarriesRealScriptIdNotLiteralZero()
|
||||
{
|
||||
@@ -1795,17 +1944,19 @@ public class TemplateServiceTests
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Follow-up #1/#2: inherited-member propagation & resync
|
||||
// Inherited-member propagation & resync
|
||||
// ========================================================================
|
||||
|
||||
private static Template BaseTemplate(int id, string name) => new(name) { Id = id };
|
||||
private static Template DerivedTemplate(int id, string name, int parentId) =>
|
||||
new(name) { Id = id, ParentTemplateId = parentId, IsDerived = true };
|
||||
|
||||
/// <summary>Verifies that resync, materializes missing inherited attribute.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Resync_MaterializesMissingInheritedAttribute()
|
||||
{
|
||||
// The reported #1/#2 shape: a base member added after the child was derived
|
||||
// The reported shape: a base member added after the child was derived
|
||||
// never reached the child's stored rows. Resync materializes it.
|
||||
var baseT = BaseTemplate(7, "Base");
|
||||
baseT.Attributes.Add(new TemplateAttribute("A") { Id = 70, TemplateId = 7, DataType = DataType.Float, Value = "1" });
|
||||
@@ -1836,6 +1987,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal(DataType.String, b.DataType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that resync, removes orphaned inherited row.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Resync_RemovesOrphanedInheritedRow()
|
||||
{
|
||||
@@ -1863,6 +2016,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal(81, Assert.Single(deleted));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that resync, updates drifted inherited row.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Resync_UpdatesDriftedInheritedRow()
|
||||
{
|
||||
@@ -1890,6 +2045,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal("NEW", childA.Value); // re-synced in place to the live base value
|
||||
}
|
||||
|
||||
/// <summary>Verifies that resync, leaves authored overrides untouched.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Resync_LeavesAuthoredOverridesUntouched()
|
||||
{
|
||||
@@ -1915,6 +2072,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.DeleteTemplateAttributeAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that resync, on base, cascades to all derived children.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Resync_OnBase_CascadesToAllDerivedChildren()
|
||||
{
|
||||
@@ -1946,6 +2105,8 @@ public class TemplateServiceTests
|
||||
Assert.Contains(added, a => a.TemplateId == 9 && a.Name == "B" && a.IsInherited);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that resync, materializes missing inherited script and native source.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Resync_MaterializesMissingInheritedScriptAndNativeSource()
|
||||
{
|
||||
@@ -1984,6 +2145,8 @@ public class TemplateServiceTests
|
||||
Assert.Equal(8, src.TemplateId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reconcile descendants, propagates new base member to child.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReconcileDescendants_PropagatesNewBaseMemberToChild()
|
||||
{
|
||||
@@ -2012,11 +2175,13 @@ public class TemplateServiceTests
|
||||
Assert.True(c.IsInherited);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add attribute, propagates new member to derived child.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddAttribute_PropagatesNewMemberToDerivedChild()
|
||||
{
|
||||
// End-to-end: adding a member to a base auto-propagates an inherited
|
||||
// placeholder to its derived children (#1/#2).
|
||||
// placeholder to its derived children.
|
||||
var baseT = BaseTemplate(7, "Base");
|
||||
baseT.Attributes.Add(new TemplateAttribute("A") { Id = 70, TemplateId = 7, DataType = DataType.Float, Value = "1" });
|
||||
var child = DerivedTemplate(8, "Child", 7);
|
||||
@@ -2047,6 +2212,8 @@ public class TemplateServiceTests
|
||||
Assert.Contains(added, a => a.TemplateId == 8 && a.Name == "C" && a.IsInherited); // child placeholder
|
||||
}
|
||||
|
||||
/// <summary>Verifies that create derived template async, copies is mes reportable onto placeholder.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CreateDerivedTemplateAsync_CopiesIsMesReportableOntoPlaceholder()
|
||||
{
|
||||
@@ -2088,6 +2255,8 @@ public class TemplateServiceTests
|
||||
Assert.True(copied.IsMesReportable);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reconcile descendants, propagates is mes reportable change.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReconcileDescendants_PropagatesIsMesReportableChange()
|
||||
{
|
||||
|
||||
+7
-1
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;
|
||||
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// M9-T32b deploy-time validation: a dangling <c>{"$ref":"lib:Name"}</c> in any
|
||||
/// Deploy-time validation: a dangling <c>{"$ref":"lib:Name"}</c> in any
|
||||
/// validated script parameter/return schema must BLOCK deployment (a
|
||||
/// <see cref="ValidationCategory.SchemaReference"/> error naming the missing ref).
|
||||
/// A valid ref resolved through the supplied seam passes. The check is inert for
|
||||
@@ -15,6 +15,7 @@ public class SchemaRefValidationTests
|
||||
{
|
||||
private readonly ValidationService _sut = new();
|
||||
|
||||
/// <summary>Verifies that a dangling reference in the parameter definitions blocks deployment.</summary>
|
||||
[Fact]
|
||||
public void Validate_DanglingRefInParameterDefinitions_BlocksDeploy()
|
||||
{
|
||||
@@ -40,6 +41,7 @@ public class SchemaRefValidationTests
|
||||
Assert.Contains("MissingLib", error.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a dangling reference in the return definition blocks deployment.</summary>
|
||||
[Fact]
|
||||
public void Validate_DanglingRefInReturnDefinition_BlocksDeploy()
|
||||
{
|
||||
@@ -65,6 +67,7 @@ public class SchemaRefValidationTests
|
||||
&& e.Message.Contains("GoneReturn", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a valid schema reference passes validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_ValidRef_Passes()
|
||||
{
|
||||
@@ -91,6 +94,7 @@ public class SchemaRefValidationTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.SchemaReference);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a schema without references produces no schema reference error.</summary>
|
||||
[Fact]
|
||||
public void Validate_NoRefSchema_NoSchemaReferenceError()
|
||||
{
|
||||
@@ -114,6 +118,7 @@ public class SchemaRefValidationTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.SchemaReference);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that validating without a schema resolver causes no regression.</summary>
|
||||
[Fact]
|
||||
public void Validate_NoResolverSupplied_NoRegression()
|
||||
{
|
||||
@@ -140,6 +145,7 @@ public class SchemaRefValidationTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.SchemaReference);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a cyclic reference blocks deployment without overflowing the stack.</summary>
|
||||
[Fact]
|
||||
public void Validate_CyclicRef_BlocksDeployWithoutOverflow()
|
||||
{
|
||||
|
||||
+5
-1
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;
|
||||
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// WP2.5: the verdict cache's eviction policy. Overflow used to <c>Clear()</c> the
|
||||
/// The verdict cache's eviction policy. Overflow used to <c>Clear()</c> the
|
||||
/// whole cache, which re-opened the non-collectible <c>InteractiveAssemblyLoader</c>
|
||||
/// leak the cache exists to bound — every hot script had to be recompiled, and every
|
||||
/// recompile loads another assembly that can never be unloaded. These tests pin the
|
||||
@@ -26,6 +26,7 @@ public class ScriptCompileVerdictCacheEvictionTests
|
||||
private static (bool Ok, string? Error) Lookup(string code, Func<(bool, string?)> factory) =>
|
||||
ScriptCompileVerdictCache.GetOrAdd(Surface, code, factory);
|
||||
|
||||
/// <summary>Verifies that an overflow keeps the hot entry instead of clearing the whole cache.</summary>
|
||||
[Fact]
|
||||
public void Overflow_KeepsHotEntry_AndDoesNotClearEverything()
|
||||
{
|
||||
@@ -66,6 +67,7 @@ public class ScriptCompileVerdictCacheEvictionTests
|
||||
Assert.Equal(compilesBefore, hotCompiles);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an overflow retains recent entries rather than dropping all of them.</summary>
|
||||
[Fact]
|
||||
public void Overflow_RetainsRecentEntries_RatherThanDroppingAll()
|
||||
{
|
||||
@@ -91,6 +93,7 @@ public class ScriptCompileVerdictCacheEvictionTests
|
||||
$"cache retained only {ScriptCompileVerdictCache.Count} entries after eviction");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an overflow keeps the cache within its size bound.</summary>
|
||||
[Fact]
|
||||
public void Overflow_KeepsCacheBounded()
|
||||
{
|
||||
@@ -135,6 +138,7 @@ public class ScriptCompileVerdictCacheEvictionTests
|
||||
$"cache grew to {ScriptCompileVerdictCache.Count} entries under promotion, exceeding its two-generation bound");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the compile surface remains part of the cache key across eviction.</summary>
|
||||
[Fact]
|
||||
public void SurfaceIsPartOfTheKey_AcrossEviction()
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;
|
||||
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// M3.2: the design-time deploy gate now delegates to the shared
|
||||
/// The design-time deploy gate now delegates to the shared
|
||||
/// <c>ZB.MOM.WW.ScadaBridge.ScriptAnalysis</c> analyzer — a real Roslyn semantic
|
||||
/// forbidden-API verdict plus a real CSharpScript compile against the
|
||||
/// runtime-mirroring <c>ScriptCompileSurface</c>. These tests assert the
|
||||
@@ -18,6 +18,7 @@ public class ScriptCompilerTests
|
||||
{
|
||||
private readonly ScriptCompiler _sut = new();
|
||||
|
||||
/// <summary>Verifies that compiling empty code returns a failure.</summary>
|
||||
[Fact]
|
||||
public void TryCompile_EmptyCode_ReturnsFailure()
|
||||
{
|
||||
@@ -26,6 +27,7 @@ public class ScriptCompilerTests
|
||||
Assert.Contains("empty", result.Error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that compiling whitespace-only code returns a failure.</summary>
|
||||
[Fact]
|
||||
public void TryCompile_WhitespaceOnly_ReturnsFailure()
|
||||
{
|
||||
@@ -36,6 +38,7 @@ public class ScriptCompilerTests
|
||||
|
||||
// --- Forbidden-API gate (authoritative) ---
|
||||
|
||||
/// <summary>Verifies that compiling code that uses a forbidden API returns a failure.</summary>
|
||||
[Fact]
|
||||
public void TryCompile_ForbiddenApi_ReturnsFailure()
|
||||
{
|
||||
@@ -44,10 +47,11 @@ public class ScriptCompilerTests
|
||||
Assert.Contains("forbidden", result.Error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that compiling code that uses several forbidden APIs reports all of them.</summary>
|
||||
[Fact]
|
||||
public void TryCompile_MultipleForbiddenApis_ReportsAll()
|
||||
{
|
||||
// #05-T24: the failure message must enumerate EVERY forbidden API, not
|
||||
// The failure message must enumerate EVERY forbidden API, not
|
||||
// just the first, so an operator fixing one isn't surprised by the next.
|
||||
var result = _sut.TryCompile(
|
||||
"System.IO.File.ReadAllText(\"x\"); System.Diagnostics.Process.Start(\"y\");",
|
||||
@@ -57,6 +61,8 @@ public class ScriptCompilerTests
|
||||
Assert.Contains("Process", result.Error, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that adversarial attempts to bypass the forbidden-API check are still rejected.</summary>
|
||||
/// <param name="code">The adversarial script source under test.</param>
|
||||
[Theory]
|
||||
// Namespace alias — the old substring scan never saw "System.IO." spelled out.
|
||||
[InlineData("using IO = System.IO; IO.File.ReadAllText(\"x\");")]
|
||||
@@ -73,6 +79,7 @@ public class ScriptCompilerTests
|
||||
Assert.Contains("forbidden", result.Error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a forbidden type in an otherwise allowed namespace is rejected as forbidden.</summary>
|
||||
[Fact]
|
||||
public void TryCompile_ForbiddenTypeInAllowedNamespace_RejectedAsForbidden()
|
||||
{
|
||||
@@ -87,6 +94,7 @@ public class ScriptCompilerTests
|
||||
Assert.Contains("forbidden", result.Error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that using the stopwatch from the allowed diagnostics namespace compiles successfully.</summary>
|
||||
[Fact]
|
||||
public void TryCompile_StopwatchInAllowedDiagnostics_ReturnsSuccess()
|
||||
{
|
||||
@@ -100,6 +108,7 @@ public class ScriptCompilerTests
|
||||
|
||||
// --- Real-compile gate (the win over the old structural-only scan) ---
|
||||
|
||||
/// <summary>Verifies that compiling code with an undefined symbol returns a failure.</summary>
|
||||
[Fact]
|
||||
public void TryCompile_UndefinedSymbol_ReturnsFailure()
|
||||
{
|
||||
@@ -111,6 +120,7 @@ public class ScriptCompilerTests
|
||||
Assert.Contains("compile", result.Error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that compiling code with a syntax error returns a failure.</summary>
|
||||
[Fact]
|
||||
public void TryCompile_SyntaxError_ReturnsFailure()
|
||||
{
|
||||
@@ -121,6 +131,7 @@ public class ScriptCompilerTests
|
||||
|
||||
// --- Valid real script against the script API surface ---
|
||||
|
||||
/// <summary>Verifies that a valid script using the API surface compiles successfully.</summary>
|
||||
[Fact]
|
||||
public void TryCompile_ValidScriptUsingApiSurface_ReturnsSuccess()
|
||||
{
|
||||
@@ -137,6 +148,7 @@ public class ScriptCompilerTests
|
||||
|
||||
// --- Trigger-expression gate via ValidationService.CheckExpressionSyntax ---
|
||||
|
||||
/// <summary>Verifies that checking a valid expression returns null.</summary>
|
||||
[Fact]
|
||||
public void CheckExpressionSyntax_ValidExpression_ReturnsNull()
|
||||
{
|
||||
@@ -145,6 +157,7 @@ public class ScriptCompilerTests
|
||||
Assert.Null(error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that checking an expression that uses a forbidden API returns an error message.</summary>
|
||||
[Fact]
|
||||
public void CheckExpressionSyntax_ForbiddenApi_ReturnsMessage()
|
||||
{
|
||||
@@ -153,6 +166,7 @@ public class ScriptCompilerTests
|
||||
Assert.Contains("forbidden", error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that checking the same expression twice serves the second call from the cache.</summary>
|
||||
[Fact]
|
||||
public void CheckExpressionSyntax_SameExpressionTwice_SecondCallIsCacheHit()
|
||||
{
|
||||
@@ -164,6 +178,7 @@ public class ScriptCompilerTests
|
||||
Assert.Equal(hitsBefore + 1, ScriptCompileVerdictCache.Hits); // FAILS today: no cache use
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a verdict cached for the script surface is not reused for the trigger surface.</summary>
|
||||
[Fact]
|
||||
public void CheckExpressionSyntax_ScriptSurfaceVerdict_NotReusedForTriggerSurface()
|
||||
{
|
||||
@@ -176,6 +191,7 @@ public class ScriptCompilerTests
|
||||
Assert.NotNull(error); // green today (no cache), and MUST STAY green after T3 — the T2 key makes it structural
|
||||
}
|
||||
|
||||
/// <summary>Verifies that checking an expression with several forbidden APIs reports all of them.</summary>
|
||||
[Fact]
|
||||
public void CheckExpressionSyntax_MultipleForbiddenApis_ReportsAll()
|
||||
{
|
||||
@@ -186,6 +202,7 @@ public class ScriptCompilerTests
|
||||
Assert.Contains("Process", error);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that checking an expression with several compile errors reports all of them.</summary>
|
||||
[Fact]
|
||||
public void CheckExpressionSyntax_MultipleCompileErrors_ReportsAll()
|
||||
{
|
||||
@@ -197,6 +214,7 @@ public class ScriptCompilerTests
|
||||
|
||||
// --- Compile-verdict cache: unchanged code compiles once per process ---
|
||||
|
||||
/// <summary>Verifies that compiling the same code twice serves the second call from the cache.</summary>
|
||||
[Fact]
|
||||
public void TryCompile_SameCodeTwice_SecondCallIsCacheHit()
|
||||
{
|
||||
@@ -209,6 +227,7 @@ public class ScriptCompilerTests
|
||||
Assert.Equal(hitsBefore + 1, ScriptCompileVerdictCache.Hits);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the same code on different surfaces computes separate verdicts.</summary>
|
||||
[Fact]
|
||||
public void GetOrAdd_SameCodeDifferentSurface_ComputesSeparateVerdicts()
|
||||
{
|
||||
@@ -223,6 +242,7 @@ public class ScriptCompilerTests
|
||||
Assert.Equal(2, ScriptCompileVerdictCache.Count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a cached failure formats the script name separately for each caller.</summary>
|
||||
[Fact]
|
||||
public void TryCompile_CachedFailure_FormatsNameForEachCaller()
|
||||
{
|
||||
|
||||
+65
-7
@@ -7,6 +7,7 @@ public class SemanticValidatorTests
|
||||
{
|
||||
private readonly SemanticValidator _sut = new();
|
||||
|
||||
/// <summary>Verifies that a native alarm source referencing an unknown connection returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_NativeAlarmSource_UnknownConnection_ReturnsError()
|
||||
{
|
||||
@@ -19,6 +20,7 @@ public class SemanticValidatorTests
|
||||
Assert.Contains(result.Errors, e => e.Category == ValidationCategory.NativeAlarmSourceInvalid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a native alarm source with an empty source reference returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_NativeAlarmSource_EmptySourceRef_ReturnsError()
|
||||
{
|
||||
@@ -31,6 +33,7 @@ public class SemanticValidatorTests
|
||||
Assert.Contains(result.Errors, e => e.Category == ValidationCategory.NativeAlarmSourceInvalid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a native alarm source with a valid binding produces no error.</summary>
|
||||
[Fact]
|
||||
public void Validate_NativeAlarmSource_ValidBinding_NoError()
|
||||
{
|
||||
@@ -43,6 +46,7 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.NativeAlarmSourceInvalid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a CallScript target that cannot be found returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_CallScriptTargetNotFound_ReturnsError()
|
||||
{
|
||||
@@ -65,6 +69,7 @@ public class SemanticValidatorTests
|
||||
e.Message.Contains("NonExistent"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a CallScript target that exists produces no error.</summary>
|
||||
[Fact]
|
||||
public void Validate_CallScriptTargetExists_NoError()
|
||||
{
|
||||
@@ -82,6 +87,7 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.CallTargetNotFound);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a CallScript to a composition script referenced by its leaf name produces no error.</summary>
|
||||
[Fact]
|
||||
public void Validate_CallScriptCompositionDelegatedByLeafName_NoError()
|
||||
{
|
||||
@@ -105,10 +111,11 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.CallTargetNotFound);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a CallScript to a composition script referenced by its leaf name emits an advisory warning.</summary>
|
||||
[Fact]
|
||||
public void Validate_CallScriptCompositionDelegatedByLeafName_EmitsAdvisoryWarning()
|
||||
{
|
||||
// #05-T24: a call target accepted ONLY by the composed leaf-name fallback
|
||||
// A call target accepted ONLY by the composed leaf-name fallback
|
||||
// (dynamic child path, not statically verifiable) must surface an advisory
|
||||
// warning rather than being silently accepted.
|
||||
var config = new FlattenedConfiguration
|
||||
@@ -128,6 +135,7 @@ public class SemanticValidatorTests
|
||||
&& w.Message.Contains("leaf name only", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a CallScript to a sibling in the same scope emits no leaf-name warning.</summary>
|
||||
[Fact]
|
||||
public void Validate_CallScriptSameScopeSibling_EmitsNoLeafWarning()
|
||||
{
|
||||
@@ -149,6 +157,7 @@ public class SemanticValidatorTests
|
||||
w.Message.Contains("leaf name only", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a CallScript to an unknown leaf name still returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_CallScriptUnknownLeafName_StillReturnsError()
|
||||
{
|
||||
@@ -169,6 +178,7 @@ public class SemanticValidatorTests
|
||||
e.Category == ValidationCategory.CallTargetNotFound && e.Message.Contains("Typo"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a CallShared target that cannot be found returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_CallSharedTargetNotFound_ReturnsError()
|
||||
{
|
||||
@@ -191,6 +201,7 @@ public class SemanticValidatorTests
|
||||
e.Message.Contains("MissingShared"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a CallShared target that exists produces no error.</summary>
|
||||
[Fact]
|
||||
public void Validate_CallSharedTargetExists_NoError()
|
||||
{
|
||||
@@ -212,6 +223,7 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.CallTargetNotFound);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a parameter count mismatch returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ParameterCountMismatch_ReturnsError()
|
||||
{
|
||||
@@ -238,8 +250,9 @@ public class SemanticValidatorTests
|
||||
Assert.Contains(result.Errors, e => e.Category == ValidationCategory.ParameterMismatch);
|
||||
}
|
||||
|
||||
// ── #21 Argument-type validation ────────────────────────────────────────
|
||||
// ── Argument-type validation ────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that passing a string for an integer parameter returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ArgumentTypeMismatch_StringForInteger_ReturnsError()
|
||||
{
|
||||
@@ -269,6 +282,7 @@ public class SemanticValidatorTests
|
||||
e.Message.Contains("type", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that passing a number for a string parameter returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ArgumentTypeMismatch_NumberForString_ReturnsError()
|
||||
{
|
||||
@@ -298,6 +312,7 @@ public class SemanticValidatorTests
|
||||
e.Message.Contains("type", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that passing a boolean for an integer parameter returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ArgumentTypeMismatch_BooleanForInteger_ReturnsError()
|
||||
{
|
||||
@@ -326,6 +341,7 @@ public class SemanticValidatorTests
|
||||
e.Message.Contains("type", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that literals of the correct types produce no error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ArgumentTypeMatch_CorrectLiterals_NoError()
|
||||
{
|
||||
@@ -354,6 +370,7 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.ParameterMismatch);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that passing an integer literal for a float parameter produces no error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ArgumentType_IntegerLiteralForFloat_NoError()
|
||||
{
|
||||
@@ -381,6 +398,7 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.ParameterMismatch);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an expression of unknown type does not produce a false positive.</summary>
|
||||
[Fact]
|
||||
public void Validate_ArgumentType_UnknownExpression_NoFalsePositive()
|
||||
{
|
||||
@@ -409,6 +427,7 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.ParameterMismatch);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an object initializer argument does not produce a false positive.</summary>
|
||||
[Fact]
|
||||
public void Validate_ArgumentType_ObjectInitializerArgument_NoFalsePositive()
|
||||
{
|
||||
@@ -437,6 +456,7 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.ParameterMismatch);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an untyped parameter does not produce a false positive.</summary>
|
||||
[Fact]
|
||||
public void Validate_ArgumentType_UntypedParameter_NoFalsePositive()
|
||||
{
|
||||
@@ -464,6 +484,7 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.ParameterMismatch);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a compound expression starting with a literal does not produce a false positive.</summary>
|
||||
[Fact]
|
||||
public void Validate_ArgumentType_CompoundExpressionStartingWithLiteral_NoFalsePositive()
|
||||
{
|
||||
@@ -492,6 +513,7 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.ParameterMismatch);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a concatenated string expression does not produce a false positive.</summary>
|
||||
[Fact]
|
||||
public void Validate_ArgumentType_ConcatenatedStringExpression_NoFalsePositive()
|
||||
{
|
||||
@@ -520,8 +542,9 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.ParameterMismatch);
|
||||
}
|
||||
|
||||
// ── #20 Return-type validation ──────────────────────────────────────────
|
||||
// ── Return-type validation ──────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that assigning a boolean result to an integer returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ReturnTypeMismatch_BooleanResultIntoInt_ReturnsError()
|
||||
{
|
||||
@@ -549,6 +572,7 @@ public class SemanticValidatorTests
|
||||
Assert.Contains(result.Errors, e => e.Category == ValidationCategory.ReturnTypeMismatch);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that assigning a string result to a boolean returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ReturnTypeMismatch_StringResultIntoBool_ReturnsError()
|
||||
{
|
||||
@@ -575,6 +599,7 @@ public class SemanticValidatorTests
|
||||
Assert.Contains(result.Errors, e => e.Category == ValidationCategory.ReturnTypeMismatch);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a compatible assignment of a return value produces no error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ReturnTypeMatch_CompatibleAssignment_NoError()
|
||||
{
|
||||
@@ -602,6 +627,7 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.ReturnTypeMismatch);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that assigning a return value to a var local does not produce a false positive.</summary>
|
||||
[Fact]
|
||||
public void Validate_ReturnType_VarAssignment_NoFalsePositive()
|
||||
{
|
||||
@@ -629,6 +655,7 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.ReturnTypeMismatch);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unused return value does not produce a false positive.</summary>
|
||||
[Fact]
|
||||
public void Validate_ReturnType_UnusedResult_NoFalsePositive()
|
||||
{
|
||||
@@ -656,6 +683,7 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.ReturnTypeMismatch);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a call to a script with no declared return type does not produce a false positive.</summary>
|
||||
[Fact]
|
||||
public void Validate_ReturnType_UndeclaredReturn_NoFalsePositive()
|
||||
{
|
||||
@@ -678,11 +706,12 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.ReturnTypeMismatch);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a return type mismatch on a qualified instance call returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ReturnTypeMismatch_QualifiedInstanceCall_ReturnsError()
|
||||
{
|
||||
// Real-code form: `Instance.CallScript(...)`. The receiver prefix must
|
||||
// be skipped so #20 still sees the typed-local assignment.
|
||||
// be skipped so return-type validation still sees the typed-local assignment.
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
@@ -706,6 +735,7 @@ public class SemanticValidatorTests
|
||||
Assert.Contains(result.Errors, e => e.Category == ValidationCategory.ReturnTypeMismatch);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a return type mismatch on a qualified shared call returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ReturnTypeMismatch_QualifiedSharedCall_ReturnsError()
|
||||
{
|
||||
@@ -737,6 +767,7 @@ public class SemanticValidatorTests
|
||||
Assert.Contains(result.Errors, e => e.Category == ValidationCategory.ReturnTypeMismatch);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a cast expression on a return value does not produce a false positive.</summary>
|
||||
[Fact]
|
||||
public void Validate_ReturnType_CastExpression_NoFalsePositive()
|
||||
{
|
||||
@@ -765,6 +796,7 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.ReturnTypeMismatch);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a range violation on a non-numeric attribute returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_RangeViolationOnNonNumeric_ReturnsError()
|
||||
{
|
||||
@@ -790,6 +822,7 @@ public class SemanticValidatorTests
|
||||
Assert.Contains(result.Errors, e => e.Category == ValidationCategory.TriggerOperandType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a range violation on a numeric attribute produces no error.</summary>
|
||||
[Fact]
|
||||
public void Validate_RangeViolationOnNumeric_NoError()
|
||||
{
|
||||
@@ -815,6 +848,7 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.TriggerOperandType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an on-trigger script that cannot be found returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_OnTriggerScriptNotFound_ReturnsError()
|
||||
{
|
||||
@@ -837,6 +871,7 @@ public class SemanticValidatorTests
|
||||
Assert.Contains(result.Errors, e => e.Category == ValidationCategory.OnTriggerScriptNotFound);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an instance script that calls an alarm on-trigger script returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_InstanceScriptCallsAlarmOnTrigger_ReturnsError()
|
||||
{
|
||||
@@ -867,6 +902,7 @@ public class SemanticValidatorTests
|
||||
Assert.Contains(result.Errors, e => e.Category == ValidationCategory.CrossCallViolation);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that call target extraction handles multiple call types.</summary>
|
||||
[Fact]
|
||||
public void ExtractCallTargets_MultipleCallTypes()
|
||||
{
|
||||
@@ -902,6 +938,7 @@ public class SemanticValidatorTests
|
||||
]
|
||||
};
|
||||
|
||||
/// <summary>Verifies that a hi/lo alarm on a non-numeric attribute returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_HiLoOnNonNumericAttribute_ReturnsError()
|
||||
{
|
||||
@@ -916,6 +953,7 @@ public class SemanticValidatorTests
|
||||
&& e.Message.Contains("non-numeric"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a hi/lo alarm on a numeric attribute produces no operand type error.</summary>
|
||||
[Fact]
|
||||
public void Validate_HiLoOnNumericAttribute_NoOperandTypeError()
|
||||
{
|
||||
@@ -927,6 +965,7 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.TriggerOperandType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a hi/lo alarm with no setpoints returns a warning.</summary>
|
||||
[Fact]
|
||||
public void Validate_HiLoNoSetpoints_ReturnsWarning()
|
||||
{
|
||||
@@ -941,6 +980,7 @@ public class SemanticValidatorTests
|
||||
&& w.Message.Contains("no setpoints"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a hi/lo alarm whose LoLo setpoint exceeds its Lo setpoint returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_HiLoLoLoGreaterThanLo_ReturnsError()
|
||||
{
|
||||
@@ -955,6 +995,7 @@ public class SemanticValidatorTests
|
||||
&& e.Message.Contains("Lo"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a hi/lo alarm whose Hi setpoint exceeds its HiHi setpoint returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_HiLoHiGreaterThanHiHi_ReturnsError()
|
||||
{
|
||||
@@ -969,6 +1010,7 @@ public class SemanticValidatorTests
|
||||
&& e.Message.Contains("HiHi"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a hi/lo alarm whose low side overlaps its high side returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_HiLoLowSideOverlapsHighSide_ReturnsError()
|
||||
{
|
||||
@@ -983,6 +1025,7 @@ public class SemanticValidatorTests
|
||||
&& e.Message.Contains("overlap"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a hi/lo alarm with only the high side configured produces no ordering error.</summary>
|
||||
[Fact]
|
||||
public void Validate_HiLoOnlyHighSideConfigured_NoOrderingError()
|
||||
{
|
||||
@@ -996,6 +1039,7 @@ public class SemanticValidatorTests
|
||||
e => e.Category == ValidationCategory.TriggerOperandType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a hi/lo alarm with a negative deadband returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_HiLoNegativeDeadband_ReturnsError()
|
||||
{
|
||||
@@ -1010,6 +1054,7 @@ public class SemanticValidatorTests
|
||||
&& e.Message.Contains("non-negative"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a hi/lo alarm with a zero deadband produces no error.</summary>
|
||||
[Fact]
|
||||
public void Validate_HiLoZeroDeadband_NoError()
|
||||
{
|
||||
@@ -1023,6 +1068,7 @@ public class SemanticValidatorTests
|
||||
e => e.Category == ValidationCategory.TriggerOperandType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a hi/lo alarm with valid setpoint ordering produces no errors.</summary>
|
||||
[Fact]
|
||||
public void Validate_HiLoValidOrdering_NoErrors()
|
||||
{
|
||||
@@ -1038,8 +1084,9 @@ public class SemanticValidatorTests
|
||||
w => w.Category == ValidationCategory.TriggerOperandType);
|
||||
}
|
||||
|
||||
// ── M2.7 review nits — comment-aware arg tokenizer ─────────────────────
|
||||
// ── Review nits — comment-aware arg tokenizer ─────────────────────
|
||||
|
||||
/// <summary>Verifies that a line comment containing a comma inside the arguments does not produce a false positive.</summary>
|
||||
[Fact]
|
||||
public void Validate_ArgSplit_LineCommentWithCommaInsideArgs_NoFalsePositive()
|
||||
{
|
||||
@@ -1068,6 +1115,7 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.ParameterMismatch);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a block comment containing a comma inside the arguments does not produce a false positive.</summary>
|
||||
[Fact]
|
||||
public void Validate_ArgSplit_BlockCommentWithCommaInsideArgs_NoFalsePositive()
|
||||
{
|
||||
@@ -1096,8 +1144,9 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.ParameterMismatch);
|
||||
}
|
||||
|
||||
// ── M2.7 review nits — stricter numeric-literal inference ───────────────
|
||||
// ── Review nits — stricter numeric-literal inference ───────────────
|
||||
|
||||
/// <summary>Verifies that an identifier with a leading underscore does not produce a false positive.</summary>
|
||||
[Fact]
|
||||
public void Validate_ArgumentType_UnderscoreLeadingIdentifier_NoFalsePositive()
|
||||
{
|
||||
@@ -1126,8 +1175,9 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.ParameterMismatch);
|
||||
}
|
||||
|
||||
// ── MV-5 List attribute semantics ────────────────────────────────────────
|
||||
// ── List attribute semantics ────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that a list attribute with a valid element type and default produces no error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ListAttribute_ValidElementTypeAndDefault_NoError()
|
||||
{
|
||||
@@ -1151,6 +1201,7 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.MissingMetadata);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a list attribute with a null element type returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ListAttribute_NullElementType_ReturnsError()
|
||||
{
|
||||
@@ -1171,6 +1222,7 @@ public class SemanticValidatorTests
|
||||
e.Message.Contains("element type", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a scalar attribute that has an element type returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ScalarAttribute_WithElementType_ReturnsError()
|
||||
{
|
||||
@@ -1191,6 +1243,7 @@ public class SemanticValidatorTests
|
||||
e.Message.Contains("element type", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a list attribute with a non-scalar element type returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ListAttribute_NonScalarElementType_ReturnsError()
|
||||
{
|
||||
@@ -1213,6 +1266,7 @@ public class SemanticValidatorTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a list attribute with a malformed JSON default returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ListAttribute_MalformedJsonDefault_ReturnsError()
|
||||
{
|
||||
@@ -1239,6 +1293,7 @@ public class SemanticValidatorTests
|
||||
e.Message.Contains("default", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a list attribute whose default contains an unparseable element returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ListAttribute_DefaultElementNotParseable_ReturnsError()
|
||||
{
|
||||
@@ -1265,6 +1320,7 @@ public class SemanticValidatorTests
|
||||
e.Message.Contains("Int32"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a list attribute with an empty default produces no default error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ListAttribute_EmptyDefault_NoDefaultError()
|
||||
{
|
||||
@@ -1282,6 +1338,7 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.MissingMetadata);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a list attribute used as a range violation operand returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ListAttribute_AsRangeViolationOperand_ReturnsError()
|
||||
{
|
||||
@@ -1311,6 +1368,7 @@ public class SemanticValidatorTests
|
||||
e.Message.Contains("non-numeric"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a list attribute used as a hi/lo operand returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ListAttribute_AsHiLoOperand_ReturnsError()
|
||||
{
|
||||
|
||||
+28
-7
@@ -7,6 +7,7 @@ public class ValidationServiceTests
|
||||
{
|
||||
private readonly ValidationService _sut = new();
|
||||
|
||||
/// <summary>Verifies that a valid configuration returns success.</summary>
|
||||
[Fact]
|
||||
public void Validate_ValidConfig_ReturnsSuccess()
|
||||
{
|
||||
@@ -21,6 +22,7 @@ public class ValidationServiceTests
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty instance name returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_EmptyInstanceName_ReturnsError()
|
||||
{
|
||||
@@ -35,6 +37,7 @@ public class ValidationServiceTests
|
||||
Assert.Contains(result.Errors, e => e.Category == ValidationCategory.FlatteningFailure);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a naming collision returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_NamingCollision_ReturnsError()
|
||||
{
|
||||
@@ -53,6 +56,7 @@ public class ValidationServiceTests
|
||||
Assert.Contains(result.Errors, e => e.Category == ValidationCategory.NamingCollision);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a forbidden API returns a compilation error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ForbiddenApi_ReturnsCompilationError()
|
||||
{
|
||||
@@ -74,6 +78,7 @@ public class ValidationServiceTests
|
||||
Assert.Contains(result.Errors, e => e.Category == ValidationCategory.ScriptCompilation);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that mismatched braces return a compilation error.</summary>
|
||||
[Fact]
|
||||
public void Validate_MismatchedBraces_ReturnsCompilationError()
|
||||
{
|
||||
@@ -91,6 +96,7 @@ public class ValidationServiceTests
|
||||
Assert.Contains(result.Errors, e => e.Category == ValidationCategory.ScriptCompilation);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an alarm referencing a missing attribute returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_AlarmReferencesMissingAttribute_ReturnsError()
|
||||
{
|
||||
@@ -114,6 +120,7 @@ public class ValidationServiceTests
|
||||
Assert.Contains(result.Errors, e => e.Category == ValidationCategory.AlarmTriggerReference);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an alarm referencing an existing attribute produces no error.</summary>
|
||||
[Fact]
|
||||
public void Validate_AlarmReferencesExistingAttribute_NoError()
|
||||
{
|
||||
@@ -137,6 +144,7 @@ public class ValidationServiceTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.AlarmTriggerReference);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a script trigger referencing a missing attribute returns an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ScriptTriggerReferencesMissingAttribute_ReturnsError()
|
||||
{
|
||||
@@ -160,10 +168,11 @@ public class ValidationServiceTests
|
||||
Assert.Contains(result.Errors, e => e.Category == ValidationCategory.ScriptTriggerReference);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unbound data-sourced attribute at design time returns a warning rather than an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_UnboundDataSourceAttribute_DesignTime_ReturnsWarningNotError()
|
||||
{
|
||||
// M2.8 (#23): at template design time (the default, enforceConnectionBindings:false)
|
||||
// At template design time (the default, enforceConnectionBindings:false)
|
||||
// a data-sourced attribute is legitimately unbound — bindings are set later at
|
||||
// instance/deploy time. So this must stay a non-blocking WARNING and IsValid true.
|
||||
var config = new FlattenedConfiguration
|
||||
@@ -187,10 +196,11 @@ public class ValidationServiceTests
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unbound data-sourced attribute at deploy time returns an error and blocks.</summary>
|
||||
[Fact]
|
||||
public void Validate_UnboundDataSourceAttribute_DeployTime_ReturnsErrorAndBlocks()
|
||||
{
|
||||
// M2.8 (#23): the deploy path opts in (enforceConnectionBindings:true). A data-sourced
|
||||
// The deploy path opts in (enforceConnectionBindings:true). A data-sourced
|
||||
// attribute with no binding now gates the deployment as an ERROR (IsValid false).
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
@@ -212,10 +222,11 @@ public class ValidationServiceTests
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a static attribute without a binding produces no binding error at deploy time.</summary>
|
||||
[Fact]
|
||||
public void Validate_StaticAttributeWithoutBinding_DeployTime_NoBindingError()
|
||||
{
|
||||
// M2.8 (#23): only DATA-SOURCED attributes require a binding. A static attribute
|
||||
// Only DATA-SOURCED attributes require a binding. A static attribute
|
||||
// (DataSourceReference == null) must remain OK even under deploy-time enforcement.
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
@@ -238,10 +249,11 @@ public class ValidationServiceTests
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an attribute bound to an existing site connection produces no binding error at deploy time.</summary>
|
||||
[Fact]
|
||||
public void Validate_BoundToExistingSiteConnection_DeployTime_NoBindingError()
|
||||
{
|
||||
// M2.8 (#23): a data-sourced attribute bound to a connection that exists at the
|
||||
// A data-sourced attribute bound to a connection that exists at the
|
||||
// target site passes the binding gate.
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
@@ -268,10 +280,11 @@ public class ValidationServiceTests
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an attribute bound to a non-existent site connection returns an error at deploy time.</summary>
|
||||
[Fact]
|
||||
public void Validate_BoundToNonExistentSiteConnection_DeployTime_ReturnsError()
|
||||
{
|
||||
// M2.8 (#23): a binding pointing at a connection that does NOT exist on the
|
||||
// A binding pointing at a connection that does NOT exist on the
|
||||
// target site is an ERROR that blocks deployment.
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
@@ -298,10 +311,11 @@ public class ValidationServiceTests
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the exists-at-site check is inert for a bound attribute when no site is set at deploy time.</summary>
|
||||
[Fact]
|
||||
public void Validate_BoundAttributeWithNoSiteSet_DeployTime_ExistsAtSiteCheckIsInert()
|
||||
{
|
||||
// M2.8 (#23): when siteConnectionNames is null the "exists at site" half of the
|
||||
// When siteConnectionNames is null the "exists at site" half of the
|
||||
// binding check stays inert — a properly-bound data-sourced attribute must NOT
|
||||
// produce a ConnectionBinding error, even under deploy-time enforcement.
|
||||
// This pins the contract: passing enforce:true + siteConnectionNames:null is safe
|
||||
@@ -328,6 +342,7 @@ public class ValidationServiceTests
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty configuration returns a warning.</summary>
|
||||
[Fact]
|
||||
public void Validate_EmptyConfig_ReturnsWarning()
|
||||
{
|
||||
@@ -337,12 +352,13 @@ public class ValidationServiceTests
|
||||
Assert.Contains(result.Warnings, w => w.Category == ValidationCategory.FlatteningFailure);
|
||||
}
|
||||
|
||||
// --- M9-T28a: per-trigger AnalysisKind (Advisory default | Strict escalates) ---
|
||||
// --- Per-trigger AnalysisKind (Advisory default | Strict escalates) ---
|
||||
// The only currently-advisory finding in CheckExpressionTrigger is the blank/empty
|
||||
// expression (which "will never fire"). Advisory keeps it a non-blocking warning;
|
||||
// Strict promotes it to a deploy-blocking error. The kind rides the existing
|
||||
// trigger-config JSON ({"expression":"...","analysisKind":"Strict"}) — no migration.
|
||||
|
||||
/// <summary>Verifies that a blank expression trigger under the default advisory kind warns but passes.</summary>
|
||||
[Fact]
|
||||
public void Validate_ExpressionTrigger_BlankExpression_AdvisoryDefault_WarnsButPasses()
|
||||
{
|
||||
@@ -368,6 +384,7 @@ public class ValidationServiceTests
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a blank expression trigger under the strict kind fails with an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ExpressionTrigger_BlankExpression_StrictKind_FailsWithError()
|
||||
{
|
||||
@@ -392,6 +409,7 @@ public class ValidationServiceTests
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a blank script expression trigger under the strict kind fails with an error.</summary>
|
||||
[Fact]
|
||||
public void Validate_ScriptExpressionTrigger_BlankExpression_StrictKind_FailsWithError()
|
||||
{
|
||||
@@ -416,6 +434,7 @@ public class ValidationServiceTests
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a valid expression trigger passes under both kinds.</summary>
|
||||
[Fact]
|
||||
public void Validate_ExpressionTrigger_ValidExpression_PassesUnderBothKinds()
|
||||
{
|
||||
@@ -463,6 +482,7 @@ public class ValidationServiceTests
|
||||
Assert.DoesNotContain(strictResult.Warnings, w => w.Category == ValidationCategory.AlarmTriggerReference);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that skipping compilation skips the expression trigger syntax check.</summary>
|
||||
[Fact]
|
||||
public void Validate_SkipCompilation_SkipsExpressionTriggerSyntaxCheck()
|
||||
{
|
||||
@@ -473,6 +493,7 @@ public class ValidationServiceTests
|
||||
Assert.Contains(full.Errors, e => e.Message.Contains("failed validation")); // deploy gate unchanged
|
||||
}
|
||||
|
||||
/// <summary>Verifies that skipping compilation still checks the expression attribute references.</summary>
|
||||
[Fact]
|
||||
public void Validate_SkipCompilation_StillChecksExpressionAttributeReferences()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user