using System.Collections.Concurrent; using Mbproxy; using Mbproxy.Options; using Mbproxy.Proxy; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting.Systemd; using Microsoft.Extensions.Hosting.WindowsServices; using Serilog; using Serilog.Core; using Serilog.Events; using Shouldly; using Xunit; namespace Mbproxy.Tests; /// /// Smoke tests: host starts, logs mbproxy.startup.ready, and shuts down cleanly. /// [Trait("Category", "Unit")] public sealed class HostSmokeTests { [Fact] public async Task HostSmoke_StartsAndStops_Cleanly_AndLogs_StartupReady() { // Arrange: build a host with an in-memory Serilog sink. var sink = new CapturingSink(); var serilogLogger = new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.Sink(sink) .CreateLogger(); using var host = Host.CreateApplicationBuilder() .ConfigureForTest(serilogLogger) .Build(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); // Act await host.StartAsync(cts.Token); // Give ProxyWorker time to fire (it binds 0 listeners and logs startup.ready). await Task.Delay(500, cts.Token); await host.StopAsync(cts.Token); // Assert: the startup.ready event was logged at Information. var readyEvents = sink.Events .Where(e => e.Level == LogEventLevel.Information && e.MessageTemplate.Text.Contains("mbproxy service ready")) .ToList(); readyEvents.ShouldNotBeEmpty("ProxyWorker should have logged mbproxy.startup.ready"); } [Fact] public async Task HostSmoke_ShutdownIsOrdered() { // Arrange using var host = Host.CreateApplicationBuilder() .ConfigureForTest(new LoggerConfiguration().CreateLogger()) .Build(); using var startCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await host.StartAsync(startCts.Token); // Act: stop must complete well within 2 s. using var stopCts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); var stopTask = host.StopAsync(stopCts.Token); // Assert: does not throw / time out. await stopTask.ShouldCompleteWithinAsync(TimeSpan.FromSeconds(3)); } [Fact] public async Task HostSmoke_BothInitSystemIntegrations_CoRegister_AndHostRunsCleanly() { // Arrange: register BOTH init-system integrations. Each is a no-op off its // own init system, so on a test run (neither) the default console lifetime // applies — they must co-register without conflict and leave the host // startable and stoppable. var builder = Host.CreateApplicationBuilder(); builder.Services.AddWindowsService(); builder.Services.AddSystemd(); builder.ConfigureForTest(new LoggerConfiguration().CreateLogger()); using var host = builder.Build(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); // Act + Assert: start/stop do not throw or time out. await host.StartAsync(cts.Token); await host.StopAsync(cts.Token); } } /// /// Helper to configure a for smoke tests, /// wiring in an in-memory config and the workers under test. /// internal static class TestHostBuilderExtensions { public static HostApplicationBuilder ConfigureForTest( this HostApplicationBuilder builder, Serilog.ILogger serilogLogger) { // Minimal in-memory config so AddMbproxyOptions doesn't fail. builder.Configuration.AddInMemoryCollection(new Dictionary { ["Mbproxy:AdminPort"] = "8080", }); builder.Services.AddSerilog(serilogLogger, dispose: false); builder.AddMbproxyOptions(); // Register the no-op pipeline and ProxyWorker. builder.Services.AddSingleton(); builder.Services.AddHostedService(); return builder; } } /// Serilog that stores events for assertion. internal sealed class CapturingSink : ILogEventSink { private readonly ConcurrentQueue _events = new(); public IEnumerable Events => _events; public void Emit(LogEvent logEvent) => _events.Enqueue(logEvent); } internal static class TaskExtensions { public static async Task ShouldCompleteWithinAsync(this Task task, TimeSpan timeout) { var completed = await Task.WhenAny(task, Task.Delay(timeout)); completed.ShouldBe(task, $"Task did not complete within {timeout}"); await task; // propagate any exception } }