b330faff03
Make the service build, run, and install on Linux as a first-class target while keeping the Windows Service + Event Log behaviour intact. - Build: drop the hardcoded win-x64 RID — single-file publish now works for any RID. publish.ps1 gains -Rid; new publish.sh for Linux hosts. - Diagnostics: DiagnosticSinkSelector picks the Error+ sink per host — Windows Event Log under the SCM, local syslog under systemd (Serilog.Sinks.SyslogMessages), none for interactive runs. The EventLog truncation helper is extracted so it is testable cross-OS. - Host: Program.cs registers AddSystemd() alongside AddWindowsService(). - Config: a RID-conditioned appsettings template ships Windows or Unix paths; both templates are schema-validated by a test. - Install: systemd unit (Type=exec) plus install.sh / uninstall.sh. Also fixes two cross-platform bugs found while testing: install.ps1 and uninstall.ps1 used New-EventLog / Remove-EventLog (absent in PowerShell 7), and the E2E sim launcher hardcoded Windows venv paths. - Docs updated across README, CLAUDE.md, and docs/ for dual-platform. 413 tests pass on Windows; 374 (all non-simulator) on Linux. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
142 lines
4.8 KiB
C#
142 lines
4.8 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Smoke tests: host starts, logs <c>mbproxy.startup.ready</c>, and shuts down cleanly.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to configure a <see cref="HostApplicationBuilder"/> for smoke tests,
|
|
/// wiring in an in-memory config and the workers under test.
|
|
/// </summary>
|
|
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<string, string?>
|
|
{
|
|
["Mbproxy:AdminPort"] = "8080",
|
|
});
|
|
|
|
builder.Services.AddSerilog(serilogLogger, dispose: false);
|
|
builder.AddMbproxyOptions();
|
|
|
|
// Register the no-op pipeline and ProxyWorker.
|
|
builder.Services.AddSingleton<IPduPipeline, NoopPduPipeline>();
|
|
builder.Services.AddHostedService<ProxyWorker>();
|
|
|
|
return builder;
|
|
}
|
|
}
|
|
|
|
/// <summary>Serilog <see cref="ILogEventSink"/> that stores events for assertion.</summary>
|
|
internal sealed class CapturingSink : ILogEventSink
|
|
{
|
|
private readonly ConcurrentQueue<LogEvent> _events = new();
|
|
public IEnumerable<LogEvent> 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
|
|
}
|
|
}
|