using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using ScadaLink.AuditLog.Configuration; using ScadaLink.AuditLog.Site; using ScadaLink.AuditLog.Site.Telemetry; using ScadaLink.Commons.Interfaces.Services; namespace ScadaLink.AuditLog.Tests; /// /// Bundle E (M2 Task E1) DI surface tests for AddAuditLog. M1 shipped /// the options-only scaffold; M2 extends it with the site writer chain /// ( + + /// ) and the telemetry collaborators /// (, , /// ). /// public class AddAuditLogTests { private static ServiceProvider BuildProvider(IDictionary? settings = null) { var config = new ConfigurationBuilder() .AddInMemoryCollection(settings ?? new Dictionary()) .Build(); var services = new ServiceCollection(); services.AddSingleton(); services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); services.AddAuditLog(config); return services.BuildServiceProvider(); } [Fact] public void AddAuditLog_RegistersAuditLogOptions() { using var provider = BuildProvider(); var opts = provider.GetService>(); Assert.NotNull(opts); Assert.NotNull(opts!.Value); } [Fact] public void AddAuditLog_NullServices_Throws() { var config = new ConfigurationBuilder().Build(); Assert.Throws( () => ServiceCollectionExtensions.AddAuditLog(null!, config)); } [Fact] public void AddAuditLog_NullConfig_Throws() { var services = new ServiceCollection(); Assert.Throws( () => services.AddAuditLog(null!)); } // -- Bundle E (M2 Task E1) --------------------------------------------- [Fact] public void AddAuditLog_Registers_SqliteAuditWriter_Singleton_FromDI() { using var provider = BuildProvider(new Dictionary { // In-memory database keeps the writer's owned connection portable // across tests; the per-instance Cache=Shared in the writer's // default connection string ensures no on-disk file is touched. ["AuditLog:SiteWriter:DatabasePath"] = ":memory:", }); var writer = provider.GetService(); Assert.NotNull(writer); // Singleton — same instance on a second resolve. Assert.Same(writer, provider.GetService()); } [Fact] public void AddAuditLog_Registers_IAuditWriter_AsFallbackAuditWriter() { using var provider = BuildProvider(new Dictionary { ["AuditLog:SiteWriter:DatabasePath"] = ":memory:", }); var writer = provider.GetService(); Assert.NotNull(writer); Assert.IsType(writer); } [Fact] public void AddAuditLog_Registers_ISiteAuditQueue_AsSameInstance_As_SqliteAuditWriter() { // The telemetry actor reads from ISiteAuditQueue while scripts write // through IAuditWriter → SqliteAuditWriter. Both surfaces MUST resolve // to the same instance or pending rows will never be visible. using var provider = BuildProvider(new Dictionary { ["AuditLog:SiteWriter:DatabasePath"] = ":memory:", }); var queue = provider.GetService(); var writer = provider.GetService(); Assert.NotNull(queue); Assert.NotNull(writer); Assert.Same(writer, queue); } [Fact] public void AddAuditLog_Registers_RingBufferFallback_Singleton() { using var provider = BuildProvider(new Dictionary { ["AuditLog:SiteWriter:DatabasePath"] = ":memory:", }); var ring = provider.GetService(); Assert.NotNull(ring); Assert.Same(ring, provider.GetService()); } [Fact] public void AddAuditLog_Registers_AuditWriteFailureCounter_AsNoOpDefault() { using var provider = BuildProvider(new Dictionary { ["AuditLog:SiteWriter:DatabasePath"] = ":memory:", }); var counter = provider.GetService(); Assert.NotNull(counter); Assert.IsType(counter); } [Fact] public void AddAuditLog_Registers_SiteStreamAuditClient_AsNoOpDefault() { using var provider = BuildProvider(new Dictionary { ["AuditLog:SiteWriter:DatabasePath"] = ":memory:", }); var client = provider.GetService(); Assert.NotNull(client); Assert.IsType(client); } [Fact] public void AddAuditLog_Options_Bind_RoundTrip_SqliteWriter() { using var provider = BuildProvider(new Dictionary { ["AuditLog:SiteWriter:DatabasePath"] = "/tmp/test-audit.db", ["AuditLog:SiteWriter:ChannelCapacity"] = "8192", ["AuditLog:SiteWriter:BatchSize"] = "128", ["AuditLog:SiteWriter:FlushIntervalMs"] = "75", }); var opts = provider.GetRequiredService>().Value; Assert.Equal("/tmp/test-audit.db", opts.DatabasePath); Assert.Equal(8192, opts.ChannelCapacity); Assert.Equal(128, opts.BatchSize); Assert.Equal(75, opts.FlushIntervalMs); } [Fact] public void AddAuditLog_Options_Bind_RoundTrip_SiteTelemetry() { using var provider = BuildProvider(new Dictionary { ["AuditLog:SiteTelemetry:BatchSize"] = "512", ["AuditLog:SiteTelemetry:BusyIntervalSeconds"] = "3", ["AuditLog:SiteTelemetry:IdleIntervalSeconds"] = "60", }); var opts = provider.GetRequiredService>().Value; Assert.Equal(512, opts.BatchSize); Assert.Equal(3, opts.BusyIntervalSeconds); Assert.Equal(60, opts.IdleIntervalSeconds); } }