using System; using System.Collections.Generic; using ZB.MOM.WW.MxGateway.Contracts.Proto; using ZB.MOM.WW.MxGateway.Worker.Conversion; namespace ZB.MOM.WW.MxGateway.Worker.Tests.Conversion; public sealed class MxStatusProxyConverterTests { private readonly MxStatusProxyConverter _converter = new(); /// Verifies that status struct fields are preserved during conversion. [Fact] public void Convert_WithStatusStruct_PreservesStatusFields() { FakeMxStatusProxy status = new() { success = 1, category = 5, detectedBy = 3, detail = 21, }; MxStatusProxy converted = _converter.Convert(status); Assert.Equal(1, converted.Success); Assert.Equal(MxStatusCategory.OperationalError, converted.Category); Assert.Equal(MxStatusSource.RespondingNmx, converted.DetectedBy); Assert.Equal(21, converted.Detail); Assert.Equal(5, converted.RawCategory); Assert.Equal(3, converted.RawDetectedBy); Assert.Equal("Invalid reference", converted.DiagnosticText); } /// Verifies that status arrays are converted without collapsing duplicate entries. [Fact] public void ConvertMany_WithStatusArray_DoesNotCollapseEntries() { FakeMxStatusProxy[] statuses = [ new() { success = 1, category = 0, detectedBy = 0, detail = 0, }, new() { success = 0, category = 6, detectedBy = 5, detail = 33, }, ]; IReadOnlyList converted = _converter.ConvertMany(statuses); Assert.Equal(2, converted.Count); Assert.Equal(MxStatusCategory.Ok, converted[0].Category); Assert.Equal(MxStatusCategory.SecurityError, converted[1].Category); Assert.Equal(MxStatusSource.RespondingAutomationObject, converted[1].DetectedBy); Assert.Equal("Write access denied", converted[1].DiagnosticText); } /// Verifies that unknown category and source values preserve raw field values. [Fact] public void Convert_WithUnknownCategoryAndSource_PreservesRawFields() { FakeMxStatusProxy status = new() { success = -1, category = 99, detectedBy = 42, detail = 1234, }; MxStatusProxy converted = _converter.Convert(status); Assert.Equal(-1, converted.Success); Assert.Equal(MxStatusCategory.Unknown, converted.Category); Assert.Equal(MxStatusSource.Unknown, converted.DetectedBy); Assert.Equal(99, converted.RawCategory); Assert.Equal(42, converted.RawDetectedBy); Assert.Equal(1234, converted.Detail); Assert.Equal(string.Empty, converted.DiagnosticText); } /// Verifies that completion-only status bytes are preserved as hex metadata. [Fact] public void PreserveCompletionOnlyStatusBytes_ReturnsRawHexMetadata() { string rawStatus = _converter.PreserveCompletionOnlyStatusBytes( [0x00, 0x00, 0x50, 0x80, 0x00]); Assert.Equal("completion_only_status_hex=0000508000", rawStatus); } /// Verifies that missing status fields throw a conversion exception. [Fact] public void Convert_WithMissingStatusField_ThrowsConversionException() { MxStatusConversionException exception = Assert.Throws(() => _converter.Convert(new MissingFields())); Assert.Contains("success", exception.Message); } /// /// Verifies that repeated conversions of the same status type produce /// identical results. WRK-06 caches the resolved FieldInfo objects per /// type after the first conversion; the cached path must yield the same /// message the uncached first call produced. /// [Fact] public void Convert_RepeatedForSameType_ProducesIdenticalResults() { FakeMxStatusProxy status = new() { success = 0, category = 3, detectedBy = 1, detail = 21, }; // First call populates the per-type FieldInfo cache; the second reuses it. MxStatusProxy first = _converter.Convert(status); MxStatusProxy second = _converter.Convert(status); Assert.Equal(first, second); Assert.Equal(MxStatusCategory.CommunicationError, second.Category); Assert.Equal(MxStatusSource.RespondingLmx, second.DetectedBy); Assert.Equal(0, second.Success); Assert.Equal(21, second.Detail); Assert.Equal("Invalid reference", second.DiagnosticText); } /// /// Verifies every accessor strategy converts the same status values /// identically. PERF-20 reads a public field of a publicly visible type /// through a delegate compiled from an expression tree instead of /// FieldInfo.GetValue; a field type whose conversion to int is not /// a lossless widening () and a type the /// compiled delegate may not touch (, not /// visible outside this assembly) keep the reflection read. All four /// doubles below carry the same logical status, so all four messages must /// be equal. /// [Fact] public void Convert_AcrossAccessorStrategies_ProducesIdenticalMessages() { // Struct, widening field types: the compiled accessor unboxes in place. MxStatusProxy compiledStruct = _converter.Convert(new FakeMxStatusProxy { success = 1, category = 5, detectedBy = 3, detail = 21, }); // Reference type, widening field types: the compiled accessor casts. MxStatusProxy compiledClass = _converter.Convert(new FakeStatusProxyClass { success = 1, category = 5, detectedBy = 3, detail = 21, }); // Non-widening field types: reflection + Convert.ToInt32 is kept. MxStatusProxy reflectedWide = _converter.Convert(new FakeWideStatusProxy { success = 1L, category = 5L, detectedBy = 3L, detail = 21L, }); // Type not visible outside the assembly: reflection is kept. MxStatusProxy reflectedHidden = _converter.Convert(new HiddenStatusProxy { success = 1, category = 5, detectedBy = 3, detail = 21, }); Assert.Equal(compiledStruct, compiledClass); Assert.Equal(compiledStruct, reflectedWide); Assert.Equal(compiledStruct, reflectedHidden); Assert.Equal(MxStatusCategory.OperationalError, compiledStruct.Category); Assert.Equal(MxStatusSource.RespondingNmx, compiledStruct.DetectedBy); Assert.Equal("Invalid reference", compiledStruct.DiagnosticText); } /// /// Verifies the compiled accessor widens a negative 16-bit field the way /// Convert.ToInt32 did — sign-extended, not reinterpreted. The /// interop MXSTATUS_PROXY declares 16-bit fields, so a sign-extension /// mistake here would silently change every failing status the gateway /// reports. /// [Fact] public void Convert_WithExtremeInt16FieldValues_SignExtendsLikeConvertToInt32() { FakeMxStatusProxy status = new() { success = short.MinValue, category = 2, detectedBy = 0, detail = short.MaxValue, }; MxStatusProxy converted = _converter.Convert(status); Assert.Equal((int)short.MinValue, converted.Success); Assert.Equal((int)short.MaxValue, converted.Detail); Assert.Equal(MxStatusCategory.Warning, converted.Category); Assert.Equal(MxStatusSource.RequestingLmx, converted.DetectedBy); } public struct FakeMxStatusProxy { public short success; public int category; public int detectedBy; public short detail; } public struct FakeWideStatusProxy { public long success; public long category; public long detectedBy; public long detail; } public sealed class FakeStatusProxyClass { public int success; public int category; public int detectedBy; public int detail; } private struct HiddenStatusProxy { public short success; public int category; public int detectedBy; public short detail; } private sealed class MissingFields { } }