using Google.Protobuf.WellKnownTypes;
using Shouldly;
using ZB.MOM.WW.OtOpcUa.Commons.Protos;
using ZB.MOM.WW.OtOpcUa.Commons.Protos.Telemetry.V1;
using ZB.MOM.WW.OtOpcUa.ControlPlane.Telemetry;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Telemetry;
///
/// Unit tests for — the central-side proto→domain
/// projection. Each kind asserts every field round-trips, including the null-vs-absent presence
/// handling for nullable Timestamps, optional strings, and the tri-state optional bool. A coverage
/// guard proves every value has a converter.
///
public sealed class TelemetryProtoMapCentralTests
{
private static readonly DateTime SampleUtc =
new(2026, 7, 23, 10, 30, 45, DateTimeKind.Utc);
private static readonly DateTime OtherUtc =
new(2026, 7, 23, 9, 15, 0, DateTimeKind.Utc);
[Fact]
public void ToAlarm_transcribes_every_field_with_nullables_present()
{
var proto = new AlarmTransition
{
AlarmId = "Plant/Modbus/dev1/Speed",
EquipmentPath = "Area/Line/Equip",
AlarmName = "HighSpeed",
TransitionKind = "Activated",
Severity = 700,
Message = "Speed high",
User = "operator1",
TimestampUtc = Timestamp.FromDateTime(SampleUtc),
AlarmTypeName = "LimitAlarm",
Comment = "ack comment",
HistorizeToAveva = false,
};
proto.ReferencingEquipmentPaths.AddRange(["Area/Line/E1", "Area/Line/E2"]);
var e = TelemetryProtoMapCentral.ToAlarm(proto);
e.AlarmId.ShouldBe("Plant/Modbus/dev1/Speed");
e.EquipmentPath.ShouldBe("Area/Line/Equip");
e.AlarmName.ShouldBe("HighSpeed");
e.TransitionKind.ShouldBe("Activated");
e.Severity.ShouldBe(700);
e.Message.ShouldBe("Speed high");
e.User.ShouldBe("operator1");
e.TimestampUtc.ShouldBe(SampleUtc);
e.TimestampUtc.Kind.ShouldBe(DateTimeKind.Utc);
e.AlarmTypeName.ShouldBe("LimitAlarm");
e.Comment.ShouldBe("ack comment");
e.HistorizeToAveva.ShouldBe(false);
e.ReferencingEquipmentPaths.ShouldBe(["Area/Line/E1", "Area/Line/E2"]);
}
[Fact]
public void ToAlarm_absent_optionals_map_to_null_and_empty_repeated_to_empty_list()
{
var proto = new AlarmTransition
{
AlarmId = "a",
EquipmentPath = "p",
AlarmName = "n",
TransitionKind = "Cleared",
Severity = 1,
Message = "m",
User = "system",
TimestampUtc = Timestamp.FromDateTime(SampleUtc),
AlarmTypeName = "AlarmCondition",
// Comment, HistorizeToAveva unset; no referencing paths added.
};
var e = TelemetryProtoMapCentral.ToAlarm(proto);
e.Comment.ShouldBeNull();
e.HistorizeToAveva.ShouldBeNull();
e.ReferencingEquipmentPaths.ShouldNotBeNull();
e.ReferencingEquipmentPaths.ShouldBeEmpty();
}
[Fact]
public void ToAlarm_historize_present_true_round_trips()
{
var proto = new AlarmTransition
{
AlarmId = "a", EquipmentPath = "p", AlarmName = "n", TransitionKind = "Activated",
Severity = 1, Message = "m", User = "system",
TimestampUtc = Timestamp.FromDateTime(SampleUtc), AlarmTypeName = "AlarmCondition",
HistorizeToAveva = true,
};
TelemetryProtoMapCentral.ToAlarm(proto).HistorizeToAveva.ShouldBe(true);
}
[Fact]
public void ToScript_transcribes_every_field_with_optionals_present()
{
var proto = new ScriptLog
{
ScriptId = "script-1",
Level = "Information",
Message = "hello",
TimestampUtc = Timestamp.FromDateTime(SampleUtc),
VirtualTagId = "vt-1",
AlarmId = "al-1",
EquipmentId = "eq-1",
};
var e = TelemetryProtoMapCentral.ToScript(proto);
e.ScriptId.ShouldBe("script-1");
e.Level.ShouldBe("Information");
e.Message.ShouldBe("hello");
e.TimestampUtc.ShouldBe(SampleUtc);
e.TimestampUtc.Kind.ShouldBe(DateTimeKind.Utc);
e.VirtualTagId.ShouldBe("vt-1");
e.AlarmId.ShouldBe("al-1");
e.EquipmentId.ShouldBe("eq-1");
}
[Fact]
public void ToScript_absent_optionals_map_to_null()
{
var proto = new ScriptLog
{
ScriptId = "script-1",
Level = "Error",
Message = "boom",
TimestampUtc = Timestamp.FromDateTime(SampleUtc),
// VirtualTagId, AlarmId, EquipmentId unset.
};
var e = TelemetryProtoMapCentral.ToScript(proto);
e.VirtualTagId.ShouldBeNull();
e.AlarmId.ShouldBeNull();
e.EquipmentId.ShouldBeNull();
}
[Fact]
public void ToHealth_transcribes_every_field_with_nullables_present()
{
var proto = new DriverHealth
{
ClusterId = "c1",
DriverInstanceId = "d1",
State = "Faulted",
LastSuccessfulReadUtc = Timestamp.FromDateTime(OtherUtc),
LastError = "timeout",
ErrorCount5Min = 3,
PublishedUtc = Timestamp.FromDateTime(SampleUtc),
};
var e = TelemetryProtoMapCentral.ToHealth(proto);
e.ClusterId.ShouldBe("c1");
e.DriverInstanceId.ShouldBe("d1");
e.State.ShouldBe("Faulted");
e.LastSuccessfulReadUtc.ShouldBe(OtherUtc);
e.LastSuccessfulReadUtc!.Value.Kind.ShouldBe(DateTimeKind.Utc);
e.LastError.ShouldBe("timeout");
e.ErrorCount5Min.ShouldBe(3);
e.PublishedUtc.ShouldBe(SampleUtc);
e.PublishedUtc.Kind.ShouldBe(DateTimeKind.Utc);
}
[Fact]
public void ToHealth_absent_nullable_timestamp_and_optional_string_map_to_null()
{
var proto = new DriverHealth
{
ClusterId = "c1",
DriverInstanceId = "d1",
State = "Healthy",
ErrorCount5Min = 0,
PublishedUtc = Timestamp.FromDateTime(SampleUtc),
// LastSuccessfulReadUtc, LastError unset.
};
var e = TelemetryProtoMapCentral.ToHealth(proto);
e.LastSuccessfulReadUtc.ShouldBeNull();
e.LastError.ShouldBeNull();
}
[Fact]
public void ToResilience_transcribes_every_field_with_nullable_present()
{
var proto = new DriverResilienceStatus
{
DriverInstanceId = "d1",
HostName = "host-a",
BreakerOpen = true,
ConsecutiveFailures = 5,
CurrentInFlight = 2,
LastBreakerOpenUtc = Timestamp.FromDateTime(OtherUtc),
LastSampledUtc = Timestamp.FromDateTime(SampleUtc),
PublishedUtc = Timestamp.FromDateTime(SampleUtc),
};
var e = TelemetryProtoMapCentral.ToResilience(proto);
e.DriverInstanceId.ShouldBe("d1");
e.HostName.ShouldBe("host-a");
e.BreakerOpen.ShouldBeTrue();
e.ConsecutiveFailures.ShouldBe(5);
e.CurrentInFlight.ShouldBe(2);
e.LastBreakerOpenUtc.ShouldBe(OtherUtc);
e.LastBreakerOpenUtc!.Value.Kind.ShouldBe(DateTimeKind.Utc);
e.LastSampledUtc.ShouldBe(SampleUtc);
e.PublishedUtc.ShouldBe(SampleUtc);
}
[Fact]
public void ToResilience_absent_nullable_timestamp_maps_to_null()
{
var proto = new DriverResilienceStatus
{
DriverInstanceId = "d1",
HostName = "host-a",
BreakerOpen = false,
ConsecutiveFailures = 0,
CurrentInFlight = 0,
LastSampledUtc = Timestamp.FromDateTime(SampleUtc),
PublishedUtc = Timestamp.FromDateTime(SampleUtc),
// LastBreakerOpenUtc unset.
};
TelemetryProtoMapCentral.ToResilience(proto).LastBreakerOpenUtc.ShouldBeNull();
}
[Fact]
public void ToAlarm_missing_required_timestamp_throws_clear_InvalidOperation()
{
var proto = new AlarmTransition
{
AlarmId = "a", EquipmentPath = "p", AlarmName = "n", TransitionKind = "Activated",
Severity = 1, Message = "m", User = "system", AlarmTypeName = "AlarmCondition",
// TimestampUtc deliberately unset — an out-of-contract sender.
};
var ex = Should.Throw(() => TelemetryProtoMapCentral.ToAlarm(proto));
ex.Message.ShouldContain("AlarmTransition");
ex.Message.ShouldContain("timestamp_utc");
}
[Fact]
public void ToScript_missing_required_timestamp_throws_clear_InvalidOperation()
{
var proto = new ScriptLog { ScriptId = "s", Level = "Information", Message = "m" };
var ex = Should.Throw(() => TelemetryProtoMapCentral.ToScript(proto));
ex.Message.ShouldContain("ScriptLog");
ex.Message.ShouldContain("timestamp_utc");
}
[Fact]
public void ToHealth_missing_required_timestamp_throws_clear_InvalidOperation()
{
var proto = new DriverHealth
{
ClusterId = "c", DriverInstanceId = "d", State = "Healthy", ErrorCount5Min = 0,
// PublishedUtc unset.
};
var ex = Should.Throw(() => TelemetryProtoMapCentral.ToHealth(proto));
ex.Message.ShouldContain("DriverHealth");
ex.Message.ShouldContain("published_utc");
}
[Fact]
public void ToResilience_missing_required_timestamp_throws_clear_InvalidOperation()
{
var proto = new DriverResilienceStatus
{
DriverInstanceId = "d", HostName = "h", BreakerOpen = false,
ConsecutiveFailures = 0, CurrentInFlight = 0,
// LastSampledUtc + PublishedUtc unset.
};
var ex = Should.Throw(() => TelemetryProtoMapCentral.ToResilience(proto));
ex.Message.ShouldContain("DriverResilienceStatus");
}
[Fact]
public void MapEvent_routes_each_case_to_its_record_type()
{
var alarm = new TelemetryEvent { AlarmTransition = MinimalAlarm() };
var script = new TelemetryEvent { ScriptLog = MinimalScript() };
var health = new TelemetryEvent { DriverHealth = MinimalHealth() };
var resilience = new TelemetryEvent { DriverResilience = MinimalResilience() };
TelemetryProtoMapCentral.MapEvent(alarm).ShouldBeOfType();
TelemetryProtoMapCentral.MapEvent(script).ShouldBeOfType();
TelemetryProtoMapCentral.MapEvent(health).ShouldBeOfType();
TelemetryProtoMapCentral.MapEvent(resilience).ShouldBeOfType();
}
[Fact]
public void MapEvent_unset_case_throws_NotSupported()
{
Should.Throw(() => TelemetryProtoMapCentral.MapEvent(new TelemetryEvent()));
}
///
/// Coverage guard: every value must be routed
/// by to a non-null domain record. Adding a
/// fifth oneof case (and thus a fifth HandledCases entry) without a converter arm fails here.
///
[Fact]
public void MapEvent_covers_every_handled_case()
{
foreach (var handled in TelemetryProtoContract.HandledCases)
{
var evt = BuildFor(handled);
evt.EventCase.ShouldBe(handled);
TelemetryProtoMapCentral.MapEvent(evt).ShouldNotBeNull();
}
}
private static TelemetryEvent BuildFor(TelemetryEvent.EventOneofCase handled) => handled switch
{
TelemetryEvent.EventOneofCase.AlarmTransition => new TelemetryEvent { AlarmTransition = MinimalAlarm() },
TelemetryEvent.EventOneofCase.ScriptLog => new TelemetryEvent { ScriptLog = MinimalScript() },
TelemetryEvent.EventOneofCase.DriverHealth => new TelemetryEvent { DriverHealth = MinimalHealth() },
TelemetryEvent.EventOneofCase.DriverResilience => new TelemetryEvent { DriverResilience = MinimalResilience() },
_ => throw new InvalidOperationException($"Test does not know how to build case {handled}."),
};
private static AlarmTransition MinimalAlarm() => new()
{
AlarmId = "a", EquipmentPath = "p", AlarmName = "n", TransitionKind = "Activated",
Severity = 1, Message = "m", User = "system",
TimestampUtc = Timestamp.FromDateTime(SampleUtc), AlarmTypeName = "AlarmCondition",
};
private static ScriptLog MinimalScript() => new()
{
ScriptId = "s", Level = "Information", Message = "m",
TimestampUtc = Timestamp.FromDateTime(SampleUtc),
};
private static DriverHealth MinimalHealth() => new()
{
ClusterId = "c", DriverInstanceId = "d", State = "Healthy",
ErrorCount5Min = 0, PublishedUtc = Timestamp.FromDateTime(SampleUtc),
};
private static DriverResilienceStatus MinimalResilience() => new()
{
DriverInstanceId = "d", HostName = "h", BreakerOpen = false,
ConsecutiveFailures = 0, CurrentInFlight = 0,
LastSampledUtc = Timestamp.FromDateTime(SampleUtc),
PublishedUtc = Timestamp.FromDateTime(SampleUtc),
};
}