using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; using ZB.MOM.WW.OtOpcUa.Configuration.Validation; namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests; [Trait("Category", "Unit")] public sealed class DraftValidatorTests { [Theory] [InlineData("valid-name", true)] [InlineData("line-01", true)] [InlineData("_default", true)] [InlineData("UPPER", false)] [InlineData("with space", false)] [InlineData("", false)] public void UnsSegment_rule_accepts_lowercase_or_default_only(string name, bool shouldPass) { var uuid = Guid.NewGuid(); var draft = new DraftSnapshot { GenerationId = 1, ClusterId = "c", Equipment = [ new Equipment { EquipmentUuid = uuid, EquipmentId = DraftValidator.DeriveEquipmentId(uuid), Name = name, DriverInstanceId = "d", UnsLineId = "line-a", MachineCode = "m", }, ], }; var errors = DraftValidator.Validate(draft); var hasUnsError = errors.Any(e => e.Code == "UnsSegmentInvalid"); hasUnsError.ShouldBe(!shouldPass); } [Fact] public void Cross_cluster_namespace_binding_is_rejected() { var draft = new DraftSnapshot { GenerationId = 1, ClusterId = "c-A", Namespaces = [new Namespace { NamespaceId = "ns-1", ClusterId = "c-B", NamespaceUri = "urn:x", Kind = NamespaceKind.Equipment }], DriverInstances = [new DriverInstance { DriverInstanceId = "d-1", ClusterId = "c-A", NamespaceId = "ns-1", Name = "drv", DriverType = "ModbusTcp", DriverConfig = "{}" }], }; var errors = DraftValidator.Validate(draft); errors.ShouldContain(e => e.Code == "BadCrossClusterNamespaceBinding"); } [Fact] public void Same_cluster_namespace_binding_is_accepted() { var draft = new DraftSnapshot { GenerationId = 1, ClusterId = "c-A", Namespaces = [new Namespace { NamespaceId = "ns-1", ClusterId = "c-A", NamespaceUri = "urn:x", Kind = NamespaceKind.Equipment }], DriverInstances = [new DriverInstance { DriverInstanceId = "d-1", ClusterId = "c-A", NamespaceId = "ns-1", Name = "drv", DriverType = "ModbusTcp", DriverConfig = "{}" }], }; DraftValidator.Validate(draft).ShouldNotContain(e => e.Code == "BadCrossClusterNamespaceBinding"); } [Fact] public void EquipmentUuid_change_across_generations_is_rejected() { var oldUuid = Guid.Parse("11111111-1111-1111-1111-111111111111"); var newUuid = Guid.Parse("22222222-2222-2222-2222-222222222222"); var eid = DraftValidator.DeriveEquipmentId(oldUuid); var draft = new DraftSnapshot { GenerationId = 2, ClusterId = "c", Equipment = [new Equipment { EquipmentUuid = newUuid, EquipmentId = eid, Name = "eq", DriverInstanceId = "d", UnsLineId = "line-a", MachineCode = "m" }], PriorEquipment = [new Equipment { EquipmentUuid = oldUuid, EquipmentId = eid, Name = "eq", DriverInstanceId = "d", UnsLineId = "line-a", MachineCode = "m" }], }; DraftValidator.Validate(draft).ShouldContain(e => e.Code == "EquipmentUuidImmutable"); } [Fact] public void ZTag_reserved_by_different_uuid_is_rejected() { var uuid = Guid.NewGuid(); var otherUuid = Guid.NewGuid(); var draft = new DraftSnapshot { GenerationId = 1, ClusterId = "c", Equipment = [new Equipment { EquipmentUuid = uuid, EquipmentId = DraftValidator.DeriveEquipmentId(uuid), Name = "eq", DriverInstanceId = "d", UnsLineId = "line-a", MachineCode = "m", ZTag = "ZT-001" }], ActiveReservations = [new ExternalIdReservation { Kind = ReservationKind.ZTag, Value = "ZT-001", EquipmentUuid = otherUuid, ClusterId = "c", FirstPublishedBy = "t" }], }; DraftValidator.Validate(draft).ShouldContain(e => e.Code == "BadDuplicateExternalIdentifier"); } [Fact] public void EquipmentId_that_does_not_match_derivation_is_rejected() { var uuid = Guid.NewGuid(); var draft = new DraftSnapshot { GenerationId = 1, ClusterId = "c", Equipment = [new Equipment { EquipmentUuid = uuid, EquipmentId = "EQ-operator-typed", Name = "eq", DriverInstanceId = "d", UnsLineId = "line-a", MachineCode = "m" }], }; DraftValidator.Validate(draft).ShouldContain(e => e.Code == "EquipmentIdNotDerived"); } [Fact] public void Galaxy_driver_in_Equipment_namespace_is_rejected() { var draft = new DraftSnapshot { GenerationId = 1, ClusterId = "c", Namespaces = [new Namespace { NamespaceId = "ns-1", ClusterId = "c", NamespaceUri = "urn:x", Kind = NamespaceKind.Equipment }], DriverInstances = [new DriverInstance { DriverInstanceId = "d-1", ClusterId = "c", NamespaceId = "ns-1", Name = "drv", DriverType = "Galaxy", DriverConfig = "{}" }], }; DraftValidator.Validate(draft).ShouldContain(e => e.Code == "DriverNamespaceKindMismatch"); } [Fact] public void Draft_with_three_violations_surfaces_all_three() { var uuid = Guid.NewGuid(); var draft = new DraftSnapshot { GenerationId = 1, ClusterId = "c-A", Namespaces = [new Namespace { NamespaceId = "ns-1", ClusterId = "c-B", NamespaceUri = "urn:x", Kind = NamespaceKind.Equipment }], DriverInstances = [new DriverInstance { DriverInstanceId = "d-1", ClusterId = "c-A", NamespaceId = "ns-1", Name = "drv", DriverType = "Galaxy", DriverConfig = "{}" }], Equipment = [new Equipment { EquipmentUuid = uuid, EquipmentId = "EQ-wrong", Name = "BAD NAME", DriverInstanceId = "d-1", UnsLineId = "line-a", MachineCode = "m" }], }; var errors = DraftValidator.Validate(draft); errors.ShouldContain(e => e.Code == "BadCrossClusterNamespaceBinding"); errors.ShouldContain(e => e.Code == "DriverNamespaceKindMismatch"); errors.ShouldContain(e => e.Code == "EquipmentIdNotDerived"); errors.ShouldContain(e => e.Code == "UnsSegmentInvalid"); } }