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; } }