Files
lmxopcua/tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/RawSchemaIntegrityTests.cs
T
Joseph Doherty 4d39b98564 v3 batch1 WP1: greenfield DbContext + V3Initial squash + seeds + tests
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.
2026-07-15 19:19:21 -04:00

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