diff --git a/tests/Vantage.Core.Tests/Health/HealthCheckerTests.cs b/tests/Vantage.Core.Tests/Health/HealthCheckerTests.cs new file mode 100644 index 0000000..8e5cbd4 --- /dev/null +++ b/tests/Vantage.Core.Tests/Health/HealthCheckerTests.cs @@ -0,0 +1,94 @@ +using System.Net; +using System.Net.Sockets; +using Vantage.Core.Health; +using Vantage.Core.Models; +using Vantage.Core.Tests.Fakes; +using Xunit; + +namespace Vantage.Core.Tests.Health; + +public class HealthCheckerTests +{ + private static HealthCheckConfig HttpCheck() => + new() { Kind = HealthKind.Http, Target = "https://service.local/health", TimeoutMs = 2000 }; + + [Fact] + public async Task Http_ExpectedStatusIsUp() + { + var checker = new HealthChecker(new HttpClient(StubHttpMessageHandler.WithStatus(HttpStatusCode.OK))); + var result = await checker.CheckAsync(HttpCheck(), CancellationToken.None); + + Assert.Equal(HealthStatus.Up, result.Status); + Assert.NotNull(result.LatencyMs); + } + + [Fact] + public async Task Http_UnexpectedStatusIsDegraded() + { + var checker = new HealthChecker(new HttpClient(StubHttpMessageHandler.WithStatus(HttpStatusCode.InternalServerError))); + var result = await checker.CheckAsync(HttpCheck(), CancellationToken.None); + + Assert.Equal(HealthStatus.Degraded, result.Status); + } + + [Fact] + public async Task Http_ConnectionErrorIsDown() + { + var checker = new HealthChecker(new HttpClient(StubHttpMessageHandler.Throwing(new HttpRequestException("refused")))); + var result = await checker.CheckAsync(HttpCheck(), CancellationToken.None); + + Assert.Equal(HealthStatus.Down, result.Status); + Assert.Contains("refused", result.Message); + } + + [Fact] + public async Task Http_TimeoutIsDown() + { + var checker = new HealthChecker(new HttpClient(StubHttpMessageHandler.Delayed(TimeSpan.FromSeconds(10), HttpStatusCode.OK))); + var check = HttpCheck(); + check.TimeoutMs = 100; + + var result = await checker.CheckAsync(check, CancellationToken.None); + + Assert.Equal(HealthStatus.Down, result.Status); + Assert.Contains("Timed out", result.Message); + } + + [Fact] + public async Task Tcp_OpenPortIsUp() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + try + { + var checker = new HealthChecker(new HttpClient()); + var check = new HealthCheckConfig { Kind = HealthKind.Tcp, Target = $"127.0.0.1:{port}", TimeoutMs = 2000 }; + + var result = await checker.CheckAsync(check, CancellationToken.None); + + Assert.Equal(HealthStatus.Up, result.Status); + Assert.NotNull(result.LatencyMs); + } + finally + { + listener.Stop(); + } + } + + [Fact] + public async Task Tcp_ClosedPortIsDown() + { + var probe = new TcpListener(IPAddress.Loopback, 0); + probe.Start(); + var closedPort = ((IPEndPoint)probe.LocalEndpoint).Port; + probe.Stop(); + + var checker = new HealthChecker(new HttpClient()); + var check = new HealthCheckConfig { Kind = HealthKind.Tcp, Target = $"127.0.0.1:{closedPort}", TimeoutMs = 2000 }; + + var result = await checker.CheckAsync(check, CancellationToken.None); + + Assert.Equal(HealthStatus.Down, result.Status); + } +} diff --git a/tests/Vantage.Core.Tests/Health/TcpEndpointTests.cs b/tests/Vantage.Core.Tests/Health/TcpEndpointTests.cs new file mode 100644 index 0000000..50eb715 --- /dev/null +++ b/tests/Vantage.Core.Tests/Health/TcpEndpointTests.cs @@ -0,0 +1,33 @@ +using Vantage.Core.Health; +using Xunit; + +namespace Vantage.Core.Tests.Health; + +public class TcpEndpointTests +{ + [Fact] + public void Parse_HostAndPort() + { + var endpoint = TcpEndpoint.Parse("db.internal:5432"); + Assert.Equal("db.internal", endpoint.Host); + Assert.Equal(5432, endpoint.Port); + } + + [Fact] + public void Parse_IPv6InBrackets() + { + var endpoint = TcpEndpoint.Parse("[::1]:8080"); + Assert.Equal("::1", endpoint.Host); + Assert.Equal(8080, endpoint.Port); + } + + [Theory] + [InlineData("nohost")] + [InlineData("host:")] + [InlineData("host:70000")] + [InlineData("host:abc")] + public void Parse_RejectsMalformedTargets(string target) + { + Assert.Throws(() => TcpEndpoint.Parse(target)); + } +}