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 = "{}",
};
}