4d39b98564
DbContext (OtOpcUaConfigDbContext): - Drop Namespace/EquipmentImportBatch/EquipmentImportRow DbSets + Configure methods. - Add RawFolder/TagGroup/UnsTagReference DbSets + Configure methods with sibling-name filtered unique indexes over nullable parents (two-filtered pattern per level). - Reshape DriverInstance (RawFolderId + UX_DriverInstance_Folder_Name/_ClusterRoot_Name), Tag (required DeviceId, nullable TagGroupId, UX_Tag_Group_Name/_Device_Name, IX_Tag_Device), Equipment (drop DriverInstanceId/DeviceId + IX_Equipment_Driver), Device (UX_Device_Driver_Name). Migration squash -> V3Initial: - Delete the whole Migrations/ folder; scaffold one V3Initial (applies clean to a fresh DB). - Port the hand-written procs + AuthorizationGrants into V3Initial.StoredProcedures.cs. The v1 generation-model procs (ConfigGeneration/GenerationId, dead since V2HostingAlignment) are rewritten as v3-schema-valid retirement stubs that preserve the two-role EXECUTE-grant surface + AuthorizationTests behaviors; sp_ComputeGenerationDiff now references the v3 tables; sp_ReleaseExternalIdReservation ported verbatim. Compile-fixes for the build gate (Wave-C-owned, minimal + TODO(v3 WP4) markers): - DraftSnapshot/DraftSnapshotFactory: drop the retired Namespaces list. - DraftValidator: delete namespace-binding + Galaxy-FullName rules; collision rule now VirtualTag-only. Seeds -> v3 schema: - seed-clusters.sql summary SELECTs; all 5 scripts/smoke/seed-*.sql rewritten (DriverInstance RawFolderId, Device+DeviceConfig endpoint, Tag DeviceId, UnsTagReference; no ConfigGeneration/ Namespace/sp_PublishGeneration). Verified: all seeds apply clean against a fresh V3Initial DB. Tests (Configuration.Tests, 107 pass): - SchemaComplianceTests rewritten to v3 tables + filtered indexes. - New RawSchemaIntegrityTests: DB-enforced sibling-name uniqueness per raw level + UnsTagReference (Equipment,Tag) uniqueness. - Delete DraftValidatorGalaxyFullNameCorpusTests (WP2/WP6 replaces); fix DraftValidatorTests + DraftSnapshotFactoryTests to v3 shapes; repoint scripting-migration existence test to V3Initial.
192 lines
7.4 KiB
C#
192 lines
7.4 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[Trait("Category", "SchemaCompliance")]
|
|
[Collection(nameof(SchemaComplianceCollection))]
|
|
public sealed class RawSchemaIntegrityTests
|
|
{
|
|
private readonly SchemaComplianceFixture _fixture;
|
|
|
|
/// <summary>Initializes a new instance of the <see cref="RawSchemaIntegrityTests"/> class.</summary>
|
|
/// <param name="fixture">The schema compliance fixture (real SQL DB).</param>
|
|
public RawSchemaIntegrityTests(SchemaComplianceFixture fixture) => _fixture = fixture;
|
|
|
|
private OtOpcUaConfigDbContext NewContext()
|
|
{
|
|
var options = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
|
|
.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<DbUpdateException>(() => 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<DbUpdateException>(() => 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<DbUpdateException>(() => 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<DbUpdateException>(() => 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<DbUpdateException>(() => 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<DbUpdateException>(() => 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<DbUpdateException>(() => 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<DbUpdateException>(() => 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 = "{}",
|
|
};
|
|
}
|