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 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)); } } /// /// 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(); // Phase 03: register the no-op pipeline and ProxyWorker (replaces HeartbeatWorker). 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 } }