using System.Diagnostics.Metrics;
using System.Net;
using System.Text;
using System.Text.Json;
using ZB.MOM.WW.Overview.Polling;
namespace ZB.MOM.WW.Overview.Tests;
/// A meter factory with no listener attached, for tests that only need the poller to run.
internal sealed class DummyMeterFactory : IMeterFactory
{
private readonly List _meters = [];
public Meter Create(MeterOptions options)
{
ArgumentNullException.ThrowIfNull(options);
// Stamp ourselves as the scope, exactly as the real factory does. Meter names are global,
// so a MeterListener that matched on name alone would pick up meters created by tests
// running in parallel; the scope is what makes one test's instruments identifiable.
options.Scope = this;
var meter = new Meter(options);
_meters.Add(meter);
return meter;
}
public void Dispose()
{
foreach (var meter in _meters)
meter.Dispose();
_meters.Clear();
}
}
/// A clock the test drives by hand.
///
/// Only is overridden: the poller's sweep — the part with rules worth
/// pinning — is exercised directly, so no test here needs a fake timer.
///
internal sealed class FakeTimeProvider(DateTimeOffset start) : TimeProvider
{
private DateTimeOffset _now = start;
public override DateTimeOffset GetUtcNow() => _now;
public void Advance(TimeSpan by) => _now += by;
}
/// Serves canned responses per absolute URL and counts what was requested.
internal sealed class RoutingHandler : HttpMessageHandler
{
private readonly Dictionary> _routes = new(StringComparer.OrdinalIgnoreCase);
public List Requests { get; } = [];
public RoutingHandler Json(string url, HttpStatusCode status, string body)
{
_routes[url] = () => new HttpResponseMessage(status)
{
Content = new StringContent(body, Encoding.UTF8, "application/json"),
};
return this;
}
///
/// A status-code-only response with an EMPTY body — what the active tier actually returns when
/// it is a bare gate endpoint rather than a full health report.
///
public RoutingHandler Status(string url, HttpStatusCode status)
{
_routes[url] = () => new HttpResponseMessage(status)
{
Content = new StringContent(string.Empty, Encoding.UTF8, "application/json"),
};
return this;
}
public RoutingHandler Fails(string url, string message)
{
_routes[url] = () => throw new HttpRequestException(message);
return this;
}
/// Lets a wrapping handler delegate here without re-entering the protected member.
public Task HandleAsync(HttpRequestMessage request) =>
SendAsync(request, CancellationToken.None);
protected override Task SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var url = request.RequestUri!.ToString();
lock (Requests)
Requests.Add(url);
if (!_routes.TryGetValue(url, out var factory))
return Task.FromException(new HttpRequestException($"no route for {url}"));
try
{
return Task.FromResult(factory());
}
catch (HttpRequestException ex)
{
return Task.FromException(ex);
}
}
}
/// Hands every named client the same test handler.
internal sealed class StubHttpClientFactory(HttpMessageHandler handler) : IHttpClientFactory
{
public HttpClient CreateClient(string name) =>
new(handler, disposeHandler: false) { Timeout = Timeout.InfiniteTimeSpan };
}
/// Builds canonical health bodies so tests state intent, not JSON.
internal static class HealthBody
{
public static string Ready(string status, params (string Name, string Status, string? Leader)[] entries)
{
var payload = new
{
status,
totalDurationMs = 1.5,
entries = entries.ToDictionary(
e => e.Name,
e => (object)new
{
status = e.Status,
description = $"{e.Name} says {e.Status}",
durationMs = 0.5,
data = e.Leader is null ? null : new Dictionary { ["leader"] = e.Leader },
},
StringComparer.Ordinal),
};
return JsonSerializer.Serialize(payload);
}
///
/// Builds a body whose entries carry arbitrary data, for the cases where the key under
/// test is not leader — notably the active tier's activeNode.
///
public static string WithData(string status, params (string Name, string Status, Dictionary? Data)[] entries)
{
var payload = new
{
status,
totalDurationMs = 1.5,
entries = entries.ToDictionary(
e => e.Name,
e => (object)new
{
status = e.Status,
description = $"{e.Name} says {e.Status}",
durationMs = 0.5,
data = e.Data,
},
StringComparer.Ordinal),
};
return JsonSerializer.Serialize(payload);
}
}
/// Builds values for the pure-logic tests.
internal static class Snapshots
{
public static InstanceSnapshot Instance(
string name,
string? group = null,
string baseUrl = "http://host:8080",
bool hasActiveRole = true,
InstanceStatus? status = InstanceStatus.Up,
string? leader = null,
string clusterCheckName = "akka-cluster") =>
new()
{
Application = "App",
Instance = name,
Group = group,
BaseUrl = baseUrl,
HasActiveRole = hasActiveRole,
Status = status,
Report = leader is null ? null : ReportWithLeader(leader, clusterCheckName),
StaleAfter = TimeSpan.FromSeconds(45),
};
private static ZbHealthReport ReportWithLeader(string leader, string clusterCheckName)
{
var body = HealthBody.Ready("Healthy", (clusterCheckName, "Healthy", leader));
return JsonSerializer.Deserialize(body)!;
}
}