193 lines
7.4 KiB
C#
193 lines
7.4 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Historian;
|
|
|
|
/// <summary>
|
|
/// Verifies the config-gated <c>AddAlarmHistorian</c> registration: when the
|
|
/// <c>AlarmHistorian</c> section is absent or disabled the <see cref="NullAlarmHistorianSink"/>
|
|
/// default survives; when it is enabled a real <see cref="SqliteStoreAndForwardSink"/> wins
|
|
/// (last-registration-wins over the <c>TryAddSingleton</c> Null default).
|
|
/// </summary>
|
|
public sealed class AlarmHistorianRegistrationTests
|
|
{
|
|
/// <summary>A no-op writer the factory hands the Sqlite sink; never actually invoked in these tests.</summary>
|
|
private sealed class FakeWriter : IAlarmHistorianWriter
|
|
{
|
|
public Task<IReadOnlyList<HistorianWriteOutcome>> WriteBatchAsync(
|
|
IReadOnlyList<AlarmHistorianEvent> batch, CancellationToken cancellationToken)
|
|
=> Task.FromResult<IReadOnlyList<HistorianWriteOutcome>>(
|
|
batch.Select(_ => HistorianWriteOutcome.Ack).ToList());
|
|
}
|
|
|
|
/// <summary>Seed the Null default exactly the way <c>AddOtOpcUaRuntime</c> does, then add logging.</summary>
|
|
private static ServiceCollection BaseServices()
|
|
{
|
|
var services = new ServiceCollection();
|
|
services.AddLogging();
|
|
services.TryAddSingleton<IAlarmHistorianSink>(NullAlarmHistorianSink.Instance);
|
|
return services;
|
|
}
|
|
|
|
private static IConfiguration ConfigFrom(Dictionary<string, string?> values)
|
|
=> new ConfigurationBuilder().AddInMemoryCollection(values).Build();
|
|
|
|
[Fact]
|
|
public void Section_absent_keeps_null_sink()
|
|
{
|
|
var services = BaseServices();
|
|
var config = ConfigFrom(new Dictionary<string, string?>());
|
|
|
|
services.AddAlarmHistorian(config, (_, _) => new FakeWriter());
|
|
|
|
using var provider = services.BuildServiceProvider();
|
|
provider.GetRequiredService<IAlarmHistorianSink>().ShouldBeOfType<NullAlarmHistorianSink>();
|
|
}
|
|
|
|
[Fact]
|
|
public void Section_disabled_keeps_null_sink()
|
|
{
|
|
var services = BaseServices();
|
|
var config = ConfigFrom(new Dictionary<string, string?>
|
|
{
|
|
["AlarmHistorian:Enabled"] = "false",
|
|
});
|
|
|
|
services.AddAlarmHistorian(config, (_, _) => new FakeWriter());
|
|
|
|
using var provider = services.BuildServiceProvider();
|
|
provider.GetRequiredService<IAlarmHistorianSink>().ShouldBeOfType<NullAlarmHistorianSink>();
|
|
}
|
|
|
|
[Fact]
|
|
public void Section_enabled_registers_sqlite_sink()
|
|
{
|
|
var tempDir = Path.Combine(Path.GetTempPath(), "otopcua-alarmhist-test-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(tempDir);
|
|
var dbPath = Path.Combine(tempDir, "alarm-historian-test.db");
|
|
|
|
try
|
|
{
|
|
var services = BaseServices();
|
|
var config = ConfigFrom(new Dictionary<string, string?>
|
|
{
|
|
["AlarmHistorian:Enabled"] = "true",
|
|
["AlarmHistorian:DatabasePath"] = dbPath,
|
|
});
|
|
|
|
services.AddAlarmHistorian(config, (_, _) => new FakeWriter());
|
|
|
|
using (var provider = services.BuildServiceProvider())
|
|
{
|
|
provider.GetRequiredService<IAlarmHistorianSink>().ShouldBeOfType<SqliteStoreAndForwardSink>();
|
|
} // dispose stops the drain loop + releases the SQLite file handle
|
|
}
|
|
finally
|
|
{
|
|
try { Directory.Delete(tempDir, recursive: true); } catch (IOException) { /* best effort */ }
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Section_binds_drain_capacity_and_retention_knobs()
|
|
{
|
|
var config = ConfigFrom(new Dictionary<string, string?>
|
|
{
|
|
["AlarmHistorian:Enabled"] = "true",
|
|
["AlarmHistorian:DrainIntervalSeconds"] = "11",
|
|
["AlarmHistorian:Capacity"] = "500",
|
|
["AlarmHistorian:DeadLetterRetentionDays"] = "7",
|
|
});
|
|
|
|
var opts = config.GetSection(AlarmHistorianOptions.SectionName).Get<AlarmHistorianOptions>();
|
|
|
|
opts.ShouldNotBeNull();
|
|
opts.DrainIntervalSeconds.ShouldBe(11);
|
|
opts.Capacity.ShouldBe(500);
|
|
opts.DeadLetterRetentionDays.ShouldBe(7);
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_warns_on_empty_shared_secret_when_enabled()
|
|
{
|
|
var opts = new AlarmHistorianOptions { Enabled = true, SharedSecret = "", DatabasePath = "/var/h.db" };
|
|
opts.Validate().ShouldContain(w => w.Contains("SharedSecret"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_warns_on_relative_database_path_when_enabled()
|
|
{
|
|
var opts = new AlarmHistorianOptions { Enabled = true, SharedSecret = "s", DatabasePath = "alarm-historian.db" };
|
|
opts.Validate().ShouldContain(w => w.Contains("DatabasePath"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_is_silent_when_correctly_configured()
|
|
{
|
|
new AlarmHistorianOptions { Enabled = true, SharedSecret = "s", DatabasePath = "/abs/h.db" }.Validate().ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_is_silent_when_disabled()
|
|
{
|
|
new AlarmHistorianOptions { Enabled = false, SharedSecret = "" }.Validate().ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_warns_on_non_positive_drain_interval()
|
|
{
|
|
var opts = new AlarmHistorianOptions { Enabled = true, SharedSecret = "s", DatabasePath = "/abs/h.db", DrainIntervalSeconds = 0 };
|
|
opts.Validate().ShouldContain(w => w.Contains("DrainIntervalSeconds"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_warns_on_non_positive_capacity()
|
|
{
|
|
var opts = new AlarmHistorianOptions { Enabled = true, SharedSecret = "s", DatabasePath = "/abs/h.db", Capacity = 0 };
|
|
opts.Validate().ShouldContain(w => w.Contains("Capacity"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_warns_on_non_positive_retention()
|
|
{
|
|
var opts = new AlarmHistorianOptions { Enabled = true, SharedSecret = "s", DatabasePath = "/abs/h.db", DeadLetterRetentionDays = 0 };
|
|
opts.Validate().ShouldContain(w => w.Contains("DeadLetterRetentionDays"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_accumulates_multiple_warnings()
|
|
{
|
|
// relative path + empty secret ⇒ both warnings, not short-circuited on the first.
|
|
var opts = new AlarmHistorianOptions { Enabled = true, SharedSecret = "", DatabasePath = "alarm-historian.db" };
|
|
var warnings = opts.Validate();
|
|
warnings.ShouldContain(w => w.Contains("SharedSecret"));
|
|
warnings.ShouldContain(w => w.Contains("DatabasePath"));
|
|
warnings.Count.ShouldBeGreaterThanOrEqualTo(2);
|
|
}
|
|
|
|
[Fact]
|
|
public void Section_binds_tcp_host_port_tls_fields()
|
|
{
|
|
var config = ConfigFrom(new Dictionary<string, string?>
|
|
{
|
|
["AlarmHistorian:Host"] = "historian.example.com",
|
|
["AlarmHistorian:Port"] = "12345",
|
|
["AlarmHistorian:UseTls"] = "true",
|
|
["AlarmHistorian:ServerCertThumbprint"] = "AABBCCDDEEFF",
|
|
});
|
|
|
|
var opts = config.GetSection(AlarmHistorianOptions.SectionName).Get<AlarmHistorianOptions>();
|
|
|
|
opts.ShouldNotBeNull();
|
|
opts.Host.ShouldBe("historian.example.com");
|
|
opts.Port.ShouldBe(12345);
|
|
opts.UseTls.ShouldBeTrue();
|
|
opts.ServerCertThumbprint.ShouldBe("AABBCCDDEEFF");
|
|
}
|
|
}
|