using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.Extensions.DependencyInjection; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests; /// /// Verifies the Phase 7 Stream E entities (, , /// ) register correctly in the EF model, map to the expected /// tables/columns/indexes, and carry the check constraints the plan decisions call for. /// Introspection only — no SQL Server required. /// [Trait("Category", "Unit")] public sealed class ScriptingEntitiesTests { private static OtOpcUaConfigDbContext BuildCtx() { var options = new DbContextOptionsBuilder() .UseSqlServer("Server=(local);Database=dummy;Integrated Security=true") // not connected .Options; return new OtOpcUaConfigDbContext(options); } private static Microsoft.EntityFrameworkCore.Metadata.IModel DesignModel(OtOpcUaConfigDbContext ctx) => ctx.GetService().Model; /// Verifies that the Script entity is registered with the expected table name and columns. [Fact] public void Script_entity_registered_with_expected_table_and_columns() { using var ctx = BuildCtx(); var entity = ctx.Model.FindEntityType(typeof(Script)).ShouldNotBeNull(); entity.GetTableName().ShouldBe("Script"); entity.FindProperty(nameof(Script.ScriptRowId)).ShouldNotBeNull(); entity.FindProperty(nameof(Script.ScriptId)).ShouldNotBeNull() .GetMaxLength().ShouldBe(64); entity.FindProperty(nameof(Script.SourceCode)).ShouldNotBeNull() .GetColumnType().ShouldBe("nvarchar(max)"); entity.FindProperty(nameof(Script.SourceHash)).ShouldNotBeNull() .GetMaxLength().ShouldBe(64); entity.FindProperty(nameof(Script.Language)).ShouldNotBeNull() .GetMaxLength().ShouldBe(16); } /// Verifies that the Script entity has a unique logical ID constraint. [Fact] public void Script_has_unique_logical_id() { // v2 live-edit dropped the per-generation qualifier; Script.ScriptId is globally unique. using var ctx = BuildCtx(); var entity = ctx.Model.FindEntityType(typeof(Script)).ShouldNotBeNull(); entity.GetIndexes().Any(i => i.IsUnique).ShouldBeTrue("Script needs at least one unique index"); } /// Verifies that the VirtualTag entity is registered with the expected trigger check constraint. [Fact] public void VirtualTag_entity_registered_with_trigger_check_constraint() { using var ctx = BuildCtx(); var entity = DesignModel(ctx).FindEntityType(typeof(VirtualTag)).ShouldNotBeNull(); entity.GetTableName().ShouldBe("VirtualTag"); var checks = entity.GetCheckConstraints().Select(c => c.Name).ToArray(); checks.ShouldContain("CK_VirtualTag_Trigger_AtLeastOne"); checks.ShouldContain("CK_VirtualTag_TimerInterval_Min"); } /// Verifies that the VirtualTag entity has a unique index on its logical key. [Fact] public void VirtualTag_has_unique_index_on_logical_key() { // v2 live-edit dropped the per-generation qualifier on VirtualTag's uniqueness. using var ctx = BuildCtx(); var entity = ctx.Model.FindEntityType(typeof(VirtualTag)).ShouldNotBeNull(); entity.GetIndexes().Any(i => i.IsUnique).ShouldBeTrue("VirtualTag needs at least one unique index"); } /// Verifies that the VirtualTag entity has ChangeTriggered, Historize, and TimerIntervalMs properties. [Fact] public void VirtualTag_has_ChangeTriggered_and_Historize_flags() { using var ctx = BuildCtx(); var entity = ctx.Model.FindEntityType(typeof(VirtualTag)).ShouldNotBeNull(); entity.FindProperty(nameof(VirtualTag.ChangeTriggered)).ShouldNotBeNull() .ClrType.ShouldBe(typeof(bool)); entity.FindProperty(nameof(VirtualTag.Historize)).ShouldNotBeNull() .ClrType.ShouldBe(typeof(bool)); entity.FindProperty(nameof(VirtualTag.TimerIntervalMs)).ShouldNotBeNull() .ClrType.ShouldBe(typeof(int?)); } /// Verifies that the ScriptedAlarm entity is registered with severity and type check constraints. [Fact] public void ScriptedAlarm_entity_registered_with_severity_and_type_checks() { using var ctx = BuildCtx(); var entity = DesignModel(ctx).FindEntityType(typeof(ScriptedAlarm)).ShouldNotBeNull(); entity.GetTableName().ShouldBe("ScriptedAlarm"); var checks = entity.GetCheckConstraints().Select(c => c.Name).ToArray(); checks.ShouldContain("CK_ScriptedAlarm_Severity_Range"); checks.ShouldContain("CK_ScriptedAlarm_AlarmType"); } /// Verifies that ScriptedAlarm has HistorizeToAveva defaulted to true per plan decision 15. [Fact] public void ScriptedAlarm_has_HistorizeToAveva_default_true_per_plan_decision_15() { // Defaults live on the CLR default assignment — verify the initializer. var alarm = new ScriptedAlarm { ScriptedAlarmId = "a1", EquipmentId = "eq1", Name = "n", AlarmType = "LimitAlarm", MessageTemplate = "m", PredicateScriptId = "s1", }; alarm.HistorizeToAveva.ShouldBeTrue(); alarm.Retain.ShouldBeTrue(); alarm.Severity.ShouldBe(500); alarm.Enabled.ShouldBeTrue(); } // The ConfigDb-backed ScriptedAlarmState entity + table were retired in per-cluster mesh Phase 4 // (Task 9): scripted-alarm condition state lives in the replicated LocalDb store. The entity-model // tests that covered it are gone with it. /// Verifies that all new Phase 7 entities are exposed via DbSet properties. [Fact] public void All_new_entities_exposed_via_DbSet() { using var ctx = BuildCtx(); ctx.Scripts.ShouldNotBeNull(); ctx.VirtualTags.ShouldNotBeNull(); ctx.ScriptedAlarms.ShouldNotBeNull(); } /// Verifies that the squashed V3Initial migration exists in the assembly. [Fact] public void V3Initial_migration_exists_in_assembly() { // v3 WP1 squashed every prior migration into V3Initial. The migration type carries the // design-time snapshot + Up/Down (incl. the scripting tables) EF uses to apply the schema. var t = typeof(Migrations.V3Initial); t.ShouldNotBeNull(); } }