using Microsoft.EntityFrameworkCore; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Uns; using ZB.MOM.WW.OtOpcUa.Configuration; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; /// /// Unit tests for — the authoring-time UNS effective-name /// uniqueness guard. Backed by an InMemory EF database seeded with one equipment carrying a /// reference (override + backing raw tag), a VirtualTag, and a ScriptedAlarm. /// public sealed class EffectiveNameGuardTests { private const string EquipmentId = "EQ-000000000001"; // Reference REF-1 → raw tag TAG-1 (Name "speed"), NO override → effective "speed". // Reference REF-2 → raw tag TAG-2 (Name "raw-temp"), override "temperature" → effective "temperature". // VirtualTag VT-1 (Name "computed"). ScriptedAlarm AL-1 (Name "highpress"). private const string RefNoOverrideId = "REF-1"; private const string RefWithOverrideId = "REF-2"; private const string VirtualTagId = "VT-1"; private const string ScriptedAlarmId = "AL-1"; private static IDbContextFactory SeedFactory() { var name = $"engname-{Guid.NewGuid():N}"; var factory = new NamedFactory(name); using var db = factory.CreateDbContext(); db.Tags.Add(new Tag { TagId = "TAG-1", DeviceId = "DEV-1", Name = "speed", DataType = "Float", AccessLevel = TagAccessLevel.Read, TagConfig = "{}", }); db.Tags.Add(new Tag { TagId = "TAG-2", DeviceId = "DEV-1", Name = "raw-temp", DataType = "Float", AccessLevel = TagAccessLevel.Read, TagConfig = "{}", }); db.UnsTagReferences.Add(new UnsTagReference { UnsTagReferenceId = RefNoOverrideId, EquipmentId = EquipmentId, TagId = "TAG-1", DisplayNameOverride = null, // effective = raw tag Name "speed" }); db.UnsTagReferences.Add(new UnsTagReference { UnsTagReferenceId = RefWithOverrideId, EquipmentId = EquipmentId, TagId = "TAG-2", DisplayNameOverride = "temperature", // effective = "temperature" }); db.VirtualTags.Add(new VirtualTag { VirtualTagId = VirtualTagId, EquipmentId = EquipmentId, Name = "computed", DataType = "Float", ScriptId = "SCR-1", }); db.ScriptedAlarms.Add(new ScriptedAlarm { ScriptedAlarmId = ScriptedAlarmId, EquipmentId = EquipmentId, Name = "highpress", AlarmType = "AlarmCondition", MessageTemplate = "{TagPath} high", PredicateScriptId = "SCR-2", }); db.SaveChanges(); return factory; } private static EffectiveNameGuard Guard(IDbContextFactory factory) => new(factory); [Fact] public async Task Free_name_returns_null() { var guard = Guard(SeedFactory()); var result = await guard.CheckAsync( EquipmentId, "brand-new-name", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null); result.ShouldBeNull(); } [Fact] public async Task Reference_vs_new_virtualtag_clash_returns_error_naming_the_reference() { var guard = Guard(SeedFactory()); // A new VirtualTag proposing "speed" collides with reference REF-1's effective name. var result = await guard.CheckAsync( EquipmentId, "speed", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null); result.ShouldNotBeNull(); result.ShouldBe($"effective name 'speed' already used by reference '{RefNoOverrideId}' in equipment '{EquipmentId}'"); } [Fact] public async Task Override_effective_name_vs_new_reference_clash_returns_error() { var guard = Guard(SeedFactory()); // A new reference proposing "temperature" collides with REF-2's OVERRIDE effective name. var result = await guard.CheckAsync( EquipmentId, "temperature", EffectiveNameSourceKind.Reference, excludeSourceId: null); result.ShouldNotBeNull(); result.ShouldBe($"effective name 'temperature' already used by reference '{RefWithOverrideId}' in equipment '{EquipmentId}'"); } [Fact] public async Task New_virtualtag_vs_scripted_alarm_clash_returns_error_naming_the_alarm() { var guard = Guard(SeedFactory()); var result = await guard.CheckAsync( EquipmentId, "highpress", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null); result.ShouldNotBeNull(); result.ShouldBe($"effective name 'highpress' already used by ScriptedAlarm '{ScriptedAlarmId}' in equipment '{EquipmentId}'"); } [Fact] public async Task Self_exclude_on_override_change_returns_null() { var guard = Guard(SeedFactory()); // Editing REF-2 to change its override to "temperature" (its own current effective name) // must NOT collide with itself. var result = await guard.CheckAsync( EquipmentId, "temperature", EffectiveNameSourceKind.Reference, excludeSourceId: RefWithOverrideId); result.ShouldBeNull(); } [Fact] public async Task Self_exclude_only_applies_to_same_kind() { var guard = Guard(SeedFactory()); // Excluding a VirtualTag whose id happens to equal a reference id must NOT excuse the // reference collision — the self-row is identified by (kind, id) together. var result = await guard.CheckAsync( EquipmentId, "speed", EffectiveNameSourceKind.VirtualTag, excludeSourceId: RefNoOverrideId); result.ShouldNotBeNull(); result.ShouldBe($"effective name 'speed' already used by reference '{RefNoOverrideId}' in equipment '{EquipmentId}'"); } [Fact] public async Task Ordinal_case_sensitivity_Speed_does_not_collide_with_speed() { var guard = Guard(SeedFactory()); // "Speed" (capital S) must NOT collide with the existing "speed" (ordinal comparison). var result = await guard.CheckAsync( EquipmentId, "Speed", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null); result.ShouldBeNull(); } [Fact] public async Task Collision_is_scoped_to_the_equipment() { var guard = Guard(SeedFactory()); // Same name, different equipment → no collision (per-equipment NodeId space). var result = await guard.CheckAsync( "EQ-000000000002", "speed", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null); result.ShouldBeNull(); } private sealed class NamedFactory(string name) : IDbContextFactory { public OtOpcUaConfigDbContext CreateDbContext() => new(new DbContextOptionsBuilder() .UseInMemoryDatabase(name) .Options); public Task CreateDbContextAsync(CancellationToken cancellationToken = default) => Task.FromResult(CreateDbContext()); } }