using Microsoft.EntityFrameworkCore; using Shouldly; using Xunit; 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.Configuration.Tests; /// /// v3 WP1 — exercises the DB-enforceable integrity + sibling-name uniqueness rules of the Raw tree /// against the real SQL schema (the filtered unique indexes over nullable parent columns and the /// UnsTagReference (Equipment, Tag) uniqueness). Runs against the shared SchemaComplianceFixture DB; /// every row uses a GUID-suffixed logical id so tests do not clobber one another. /// [Trait("Category", "SchemaCompliance")] [Collection(nameof(SchemaComplianceCollection))] public sealed class RawSchemaIntegrityTests { private readonly SchemaComplianceFixture _fixture; /// Initializes a new instance of the class. /// The schema compliance fixture (real SQL DB). public RawSchemaIntegrityTests(SchemaComplianceFixture fixture) => _fixture = fixture; private OtOpcUaConfigDbContext NewContext() { var options = new DbContextOptionsBuilder() .UseSqlServer(_fixture.ConnectionString) .Options; return new OtOpcUaConfigDbContext(options); } // Logical ids are nvarchar(64); prefix + '-' + 32 hex stays well under that. private static string Id(string prefix) => $"{prefix}-{Guid.NewGuid():N}"; private string SeedCluster() { var clusterId = Id("cl"); using var db = NewContext(); db.ServerClusters.Add(new ServerCluster { ClusterId = clusterId, Name = Id("Cluster"), Enterprise = "zb", Site = "dev", NodeCount = 1, RedundancyMode = RedundancyMode.None, CreatedBy = "wp1-test", }); db.SaveChanges(); return clusterId; } [Fact] public void RawFolder_root_sibling_name_must_be_unique_within_cluster() { var clusterId = SeedCluster(); using var db = NewContext(); db.RawFolders.Add(new RawFolder { RawFolderId = Id("rf"), ClusterId = clusterId, Name = "plc-area" }); db.RawFolders.Add(new RawFolder { RawFolderId = Id("rf"), ClusterId = clusterId, Name = "plc-area" }); Should.Throw(() => db.SaveChanges()); } [Fact] public void RawFolder_same_root_name_in_different_cluster_is_allowed() { var clusterA = SeedCluster(); var clusterB = SeedCluster(); using var db = NewContext(); db.RawFolders.Add(new RawFolder { RawFolderId = Id("rf"), ClusterId = clusterA, Name = "shared-name" }); db.RawFolders.Add(new RawFolder { RawFolderId = Id("rf"), ClusterId = clusterB, Name = "shared-name" }); Should.NotThrow(() => db.SaveChanges()); } [Fact] public void RawFolder_nested_sibling_name_must_be_unique_within_parent() { var clusterId = SeedCluster(); var parentId = Id("rf-parent"); using var db = NewContext(); db.RawFolders.Add(new RawFolder { RawFolderId = parentId, ClusterId = clusterId, Name = "parent" }); db.RawFolders.Add(new RawFolder { RawFolderId = Id("rf"), ClusterId = clusterId, ParentRawFolderId = parentId, Name = "child" }); db.RawFolders.Add(new RawFolder { RawFolderId = Id("rf"), ClusterId = clusterId, ParentRawFolderId = parentId, Name = "child" }); Should.Throw(() => db.SaveChanges()); } [Fact] public void DriverInstance_cluster_root_sibling_name_must_be_unique() { var clusterId = SeedCluster(); using var db = NewContext(); db.DriverInstances.Add(new DriverInstance { DriverInstanceId = Id("di"), ClusterId = clusterId, Name = "modbus", DriverType = "Modbus", DriverConfig = "{}" }); db.DriverInstances.Add(new DriverInstance { DriverInstanceId = Id("di"), ClusterId = clusterId, Name = "modbus", DriverType = "Modbus", DriverConfig = "{}" }); Should.Throw(() => db.SaveChanges()); } [Fact] public void Device_sibling_name_must_be_unique_within_driver() { var driverId = Id("di"); using var db = NewContext(); db.Devices.Add(new Device { DeviceId = Id("dev"), DriverInstanceId = driverId, Name = "plc-1", DeviceConfig = "{}" }); db.Devices.Add(new Device { DeviceId = Id("dev"), DriverInstanceId = driverId, Name = "plc-1", DeviceConfig = "{}" }); Should.Throw(() => db.SaveChanges()); } [Fact] public void TagGroup_device_root_sibling_name_must_be_unique() { var deviceId = Id("dev"); using var db = NewContext(); db.TagGroups.Add(new TagGroup { TagGroupId = Id("tg"), DeviceId = deviceId, Name = "motors" }); db.TagGroups.Add(new TagGroup { TagGroupId = Id("tg"), DeviceId = deviceId, Name = "motors" }); Should.Throw(() => db.SaveChanges()); } [Fact] public void Tag_device_root_sibling_name_must_be_unique() { var deviceId = Id("dev"); using var db = NewContext(); db.Tags.Add(NewTag(deviceId, tagGroupId: null, name: "speed")); db.Tags.Add(NewTag(deviceId, tagGroupId: null, name: "speed")); Should.Throw(() => db.SaveChanges()); } [Fact] public void Tag_grouped_sibling_name_must_be_unique_within_group() { var deviceId = Id("dev"); var groupId = Id("tg"); using var db = NewContext(); db.Tags.Add(NewTag(deviceId, tagGroupId: groupId, name: "speed")); db.Tags.Add(NewTag(deviceId, tagGroupId: groupId, name: "speed")); Should.Throw(() => db.SaveChanges()); } [Fact] public void Tag_same_name_in_different_groups_is_allowed() { var deviceId = Id("dev"); using var db = NewContext(); db.Tags.Add(NewTag(deviceId, tagGroupId: Id("tg"), name: "speed")); db.Tags.Add(NewTag(deviceId, tagGroupId: Id("tg"), name: "speed")); Should.NotThrow(() => db.SaveChanges()); } [Fact] public void UnsTagReference_equipment_tag_pair_must_be_unique() { var equipmentId = Id("eq"); var tagId = Id("tag"); using var db = NewContext(); db.UnsTagReferences.Add(new UnsTagReference { UnsTagReferenceId = Id("utr"), EquipmentId = equipmentId, TagId = tagId }); db.UnsTagReferences.Add(new UnsTagReference { UnsTagReferenceId = Id("utr"), EquipmentId = equipmentId, TagId = tagId }); Should.Throw(() => db.SaveChanges()); } [Fact] public void UnsTagReference_same_tag_different_equipment_is_allowed() { var tagId = Id("tag"); using var db = NewContext(); db.UnsTagReferences.Add(new UnsTagReference { UnsTagReferenceId = Id("utr"), EquipmentId = Id("eq"), TagId = tagId }); db.UnsTagReferences.Add(new UnsTagReference { UnsTagReferenceId = Id("utr"), EquipmentId = Id("eq"), TagId = tagId }); Should.NotThrow(() => db.SaveChanges()); } private static Tag NewTag(string deviceId, string? tagGroupId, string name) => new() { TagId = Id("tag"), DeviceId = deviceId, TagGroupId = tagGroupId, Name = name, DataType = "Float", AccessLevel = TagAccessLevel.Read, TagConfig = "{}", }; }