using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Health;
using ZB.MOM.WW.MxGateway.Server;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Diagnostics;
namespace ZB.MOM.WW.MxGateway.Tests.Diagnostics;
/// Readiness coverage for the worker executable (#206).
public sealed class WorkerExecutableHealthCheckTests : IDisposable
{
private readonly string _directory = Path.Combine(Path.GetTempPath(), $"mxgw-workercheck-{Guid.NewGuid():N}");
/// Initializes a new instance of the class.
public WorkerExecutableHealthCheckTests() => Directory.CreateDirectory(_directory);
///
public void Dispose() => Directory.Delete(_directory, recursive: true);
/// A present x86 worker image is healthy.
/// A task that represents the asynchronous operation.
[Fact]
public async Task Healthy_WhenWorkerExecutableIsLaunchable()
{
string path = WritePeStub(machine: 0x014c);
HealthCheckResult result = await CheckAsync(path, WorkerArchitecture.X86);
Assert.Equal(HealthStatus.Healthy, result.Status);
}
/// A missing worker executable is unhealthy and reports only the error code, never the path.
/// A task that represents the asynchronous operation.
[Fact]
public async Task Unhealthy_WhenWorkerExecutableIsMissing()
{
string path = Path.Combine(_directory, "missing", "ZB.MOM.WW.MxGateway.Worker.exe");
HealthCheckResult result = await CheckAsync(path, WorkerArchitecture.X86);
Assert.Equal(HealthStatus.Unhealthy, result.Status);
Assert.Equal("ExecutableNotFound", result.Data["error_code"]);
Assert.DoesNotContain(_directory, result.Description, StringComparison.OrdinalIgnoreCase);
}
/// A worker image of the wrong architecture is unhealthy, as the launcher would reject it.
/// A task that represents the asynchronous operation.
[Fact]
public async Task Unhealthy_WhenWorkerExecutableHasWrongArchitecture()
{
string path = WritePeStub(machine: 0x8664);
HealthCheckResult result = await CheckAsync(path, WorkerArchitecture.X86);
Assert.Equal(HealthStatus.Unhealthy, result.Status);
Assert.Equal("InvalidExecutable", result.Data["error_code"]);
}
///
/// The gateway registers the check on the ready tier, and the session check stays on the active
/// tier so zero sessions keeps readiness green.
///
/// A task that represents the asynchronous operation.
[Fact]
public async Task Build_RegistersWorkerExecutableCheckOnReadyTier()
{
await using WebApplication app = GatewayApplication.Build([]);
HealthCheckServiceOptions options =
app.Services.GetRequiredService>().Value;
HealthCheckRegistration worker = Assert.Single(options.Registrations, r => r.Name == "worker-executable");
Assert.Contains(ZbHealthTags.Ready, worker.Tags);
HealthCheckRegistration sessions = Assert.Single(options.Registrations, r => r.Name == "mxaccess-sessions");
Assert.DoesNotContain(ZbHealthTags.Ready, sessions.Tags);
}
private static Task CheckAsync(string path, WorkerArchitecture architecture)
{
WorkerExecutableHealthCheck check = new(Options.Create(new GatewayOptions
{
Worker = new WorkerOptions
{
ExecutablePath = path,
RequiredArchitecture = architecture,
},
}));
return check.CheckHealthAsync(new HealthCheckContext());
}
private string WritePeStub(ushort machine)
{
string path = Path.Combine(_directory, "ZB.MOM.WW.MxGateway.Worker.exe");
byte[] bytes = new byte[0x100];
bytes[0] = (byte)'M';
bytes[1] = (byte)'Z';
BitConverter.GetBytes(0x80).CopyTo(bytes, 0x3c);
bytes[0x80] = (byte)'P';
bytes[0x81] = (byte)'E';
BitConverter.GetBytes(machine).CopyTo(bytes, 0x84);
File.WriteAllBytes(path, bytes);
return path;
}
}