using Serilog.Events;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Logging;
namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests;
///
/// Verifies converts Serilog
/// instances into records and hands them to the
/// . Covers level filtering, property extraction,
/// fallback for missing identity props, and message template rendering.
///
[Trait("Category", "Unit")]
public sealed class ScriptLogTopicSinkTests
{
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
private sealed class FakePublisher : IScriptLogPublisher
{
/// Gets the entries that have been published.
public List Published { get; } = [];
///
public void Publish(ScriptLogEntry entry) => Published.Add(entry);
}
/// Builds a with an optional set of string properties.
private static LogEvent Evt(
LogEventLevel level,
string template,
params (string Key, string Val)[] props)
{
var parser = new Serilog.Parsing.MessageTemplateParser();
var tmpl = parser.Parse(template);
var logProps = props.Select(p => new LogEventProperty(p.Key, new ScalarValue(p.Val)));
return new LogEvent(DateTimeOffset.UtcNow, level, exception: null, tmpl, logProps);
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
///
/// An Information event with all identity props present → publisher receives
/// exactly one entry with correctly mapped fields and null AlarmId.
///
[Fact]
public void Information_event_with_all_identity_props_is_published_correctly()
{
var publisher = new FakePublisher();
var sink = new ScriptLogTopicSink(publisher);
var evt = Evt(LogEventLevel.Information, "hello",
(ScriptLoggerFactory.ScriptIdProperty, "S1"),
(ScriptLoggerFactory.VirtualTagIdProperty, "V1"),
(ScriptLoggerFactory.EquipmentIdProperty, "EQ1"));
sink.Emit(evt);
publisher.Published.Count.ShouldBe(1);
var entry = publisher.Published[0];
entry.ScriptId.ShouldBe("S1");
entry.Level.ShouldBe("Information");
entry.Message.ShouldBe("hello");
entry.VirtualTagId.ShouldBe("V1");
entry.EquipmentId.ShouldBe("EQ1");
entry.AlarmId.ShouldBeNull();
}
///
/// An AlarmId property on the event is mapped to .
///
[Fact]
public void AlarmId_prop_is_mapped_to_entry_AlarmId()
{
var publisher = new FakePublisher();
var sink = new ScriptLogTopicSink(publisher);
var evt = Evt(LogEventLevel.Information, "alarm fired",
(ScriptLoggerFactory.ScriptIdProperty, "S2"),
(ScriptLoggerFactory.AlarmIdProperty, "A1"));
sink.Emit(evt);
publisher.Published[0].AlarmId.ShouldBe("A1");
}
///
/// When no identity properties are present all nullable fields are null and
/// ScriptId falls back to "unknown".
///
[Fact]
public void Absent_identity_props_yield_null_fields_and_unknown_ScriptId()
{
var publisher = new FakePublisher();
var sink = new ScriptLogTopicSink(publisher);
var evt = Evt(LogEventLevel.Warning, "bare message");
sink.Emit(evt);
publisher.Published.Count.ShouldBe(1);
var entry = publisher.Published[0];
entry.ScriptId.ShouldBe("unknown");
entry.VirtualTagId.ShouldBeNull();
entry.AlarmId.ShouldBeNull();
entry.EquipmentId.ShouldBeNull();
}
///
/// When only a ScriptName property (no ScriptId) is present,
/// falls back to the ScriptName value.
///
[Fact]
public void ScriptName_fallback_used_when_ScriptId_absent()
{
var publisher = new FakePublisher();
var sink = new ScriptLogTopicSink(publisher);
var evt = Evt(LogEventLevel.Information, "using name fallback",
(ScriptLoggerFactory.ScriptNameProperty, "NameFallback"));
sink.Emit(evt);
publisher.Published[0].ScriptId.ShouldBe("NameFallback");
}
///
/// A Debug event is filtered by the default minimum level (Information) and
/// the publisher receives nothing.
///
[Fact]
public void Debug_event_filtered_by_default_minimum_level()
{
var publisher = new FakePublisher();
var sink = new ScriptLogTopicSink(publisher); // default min = Information
var evt = Evt(LogEventLevel.Debug, "debug noise",
(ScriptLoggerFactory.ScriptIdProperty, "S3"));
sink.Emit(evt);
publisher.Published.ShouldBeEmpty();
}
///
/// When the sink is constructed with min: Debug, a Debug event reaches
/// the publisher with Level=="Debug".
///
[Fact]
public void Debug_event_published_when_sink_min_level_is_Debug()
{
var publisher = new FakePublisher();
var sink = new ScriptLogTopicSink(publisher, min: LogEventLevel.Debug);
var evt = Evt(LogEventLevel.Debug, "debug detail",
(ScriptLoggerFactory.ScriptIdProperty, "S4"));
sink.Emit(evt);
publisher.Published.Count.ShouldBe(1);
publisher.Published[0].Level.ShouldBe("Debug");
}
///
/// A templated message is fully rendered before being stored so the consumer
/// sees a plain string rather than a raw template.
///
[Fact]
public void Templated_message_is_rendered_before_publishing()
{
var publisher = new FakePublisher();
var sink = new ScriptLogTopicSink(publisher);
// Build the event manually to include a numeric property (can't be passed as string
// in the helper since we need an integer ScalarValue for realistic rendering).
var parser = new Serilog.Parsing.MessageTemplateParser();
var tmpl = parser.Parse("v={V}");
var props = new[] { new LogEventProperty("V", new ScalarValue(3)) };
var evt = new LogEvent(DateTimeOffset.UtcNow, LogEventLevel.Information, null, tmpl, props);
sink.Emit(evt);
publisher.Published[0].Message.ShouldBe("v=3");
}
/// Passing a null publisher to the constructor throws .
[Fact]
public void Null_publisher_throws_ArgumentNullException()
{
Should.Throw(() => new ScriptLogTopicSink(null!));
}
}