// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.Extensions.Diagnostics.HealthChecks { public class DefaultHealthCheckServiceTest { [Fact] public void Constructor_ThrowsUsefulExceptionForDuplicateNames() { // Arrange // // Doing this the old fashioned way so we can verify that the exception comes // from the constructor. var serviceCollection = new ServiceCollection(); serviceCollection.AddLogging(); serviceCollection.AddOptions(); serviceCollection.AddHealthChecks() .AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy()))) .AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy()))) .AddCheck("Bar", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy()))) .AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy()))) .AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy()))); var services = serviceCollection.BuildServiceProvider(); var scopeFactory = services.GetRequiredService(); var options = services.GetRequiredService>(); var logger = services.GetRequiredService>(); // Act var exception = Assert.Throws(() => new DefaultHealthCheckService(scopeFactory, options, logger)); // Assert Assert.StartsWith($"Duplicate health checks were registered with the name(s): Foo, Baz", exception.Message); } [Fact] public async Task CheckAsync_RunsAllChecksAndAggregatesResultsAsync() { const string DataKey = "Foo"; const string DataValue = "Bar"; const string DegradedMessage = "I'm not feeling so good"; const string UnhealthyMessage = "Halp!"; const string HealthyMessage = "Everything is A-OK"; var exception = new Exception("Things are pretty bad!"); // Arrange var data = new Dictionary() { { DataKey, DataValue } }; var service = CreateHealthChecksService(b => { b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data))); b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage))); b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception))); }); // Act var results = await service.CheckHealthAsync(); // Assert Assert.Collection( results.Entries.OrderBy(kvp => kvp.Key), actual => { Assert.Equal("DegradedCheck", actual.Key); Assert.Equal(DegradedMessage, actual.Value.Description); Assert.Equal(HealthStatus.Degraded, actual.Value.Status); Assert.Null(actual.Value.Exception); Assert.Empty(actual.Value.Data); }, actual => { Assert.Equal("HealthyCheck", actual.Key); Assert.Equal(HealthyMessage, actual.Value.Description); Assert.Equal(HealthStatus.Healthy, actual.Value.Status); Assert.Null(actual.Value.Exception); Assert.Collection(actual.Value.Data, item => { Assert.Equal(DataKey, item.Key); Assert.Equal(DataValue, item.Value); }); }, actual => { Assert.Equal("UnhealthyCheck", actual.Key); Assert.Equal(UnhealthyMessage, actual.Value.Description); Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status); Assert.Same(exception, actual.Value.Exception); Assert.Empty(actual.Value.Data); }); } [Fact] public async Task CheckAsync_RunsFilteredChecksAndAggregatesResultsAsync() { const string DataKey = "Foo"; const string DataValue = "Bar"; const string DegradedMessage = "I'm not feeling so good"; const string UnhealthyMessage = "Halp!"; const string HealthyMessage = "Everything is A-OK"; var exception = new Exception("Things are pretty bad!"); // Arrange var data = new Dictionary { { DataKey, DataValue } }; var service = CreateHealthChecksService(b => { b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data))); b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage))); b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception))); }); // Act var results = await service.CheckHealthAsync(c => c.Name == "HealthyCheck"); // Assert Assert.Collection(results.Entries, actual => { Assert.Equal("HealthyCheck", actual.Key); Assert.Equal(HealthyMessage, actual.Value.Description); Assert.Equal(HealthStatus.Healthy, actual.Value.Status); Assert.Null(actual.Value.Exception); Assert.Collection(actual.Value.Data, item => { Assert.Equal(DataKey, item.Key); Assert.Equal(DataValue, item.Value); }); }); } [Fact] public async Task CheckHealthAsync_SetsRegistrationForEachCheck() { // Arrange var thrownException = new InvalidOperationException("Whoops!"); var faultedException = new InvalidOperationException("Ohnoes!"); var service = CreateHealthChecksService(b => { b.AddCheck("A"); b.AddCheck("B"); b.AddCheck("C"); }); // Act var results = await service.CheckHealthAsync(); // Assert Assert.Collection( results.Entries, actual => { Assert.Equal("A", actual.Key); Assert.Collection( actual.Value.Data, kvp => Assert.Equal(kvp, new KeyValuePair("name", "A"))); }, actual => { Assert.Equal("B", actual.Key); Assert.Collection( actual.Value.Data, kvp => Assert.Equal(kvp, new KeyValuePair("name", "B"))); }, actual => { Assert.Equal("C", actual.Key); Assert.Collection( actual.Value.Data, kvp => Assert.Equal(kvp, new KeyValuePair("name", "C"))); }); } [Fact] public async Task CheckHealthAsync_Cancellation_CanPropagate() { // Arrange var insideCheck = new TaskCompletionSource(); var service = CreateHealthChecksService(b => { b.AddAsyncCheck("cancels", async ct => { insideCheck.SetResult(null); await Task.Delay(10000, ct); return HealthCheckResult.Unhealthy(); }); }); var cancel = new CancellationTokenSource(); var task = service.CheckHealthAsync(cancel.Token); // After this returns we know the check has started await insideCheck.Task; cancel.Cancel(); // Act & Assert await Assert.ThrowsAsync(async () => await task); } [Fact] public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckToUnhealthyResultAsync() { // Arrange var thrownException = new InvalidOperationException("Whoops!"); var faultedException = new InvalidOperationException("Ohnoes!"); var service = CreateHealthChecksService(b => { b.AddAsyncCheck("Throws", ct => throw thrownException); b.AddAsyncCheck("Faults", ct => Task.FromException(faultedException)); b.AddAsyncCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy())); }); // Act var results = await service.CheckHealthAsync(); // Assert Assert.Collection( results.Entries, actual => { Assert.Equal("Throws", actual.Key); Assert.Equal(thrownException.Message, actual.Value.Description); Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status); Assert.Same(thrownException, actual.Value.Exception); }, actual => { Assert.Equal("Faults", actual.Key); Assert.Equal(faultedException.Message, actual.Value.Description); Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status); Assert.Same(faultedException, actual.Value.Exception); }, actual => { Assert.Equal("Succeeds", actual.Key); Assert.Null(actual.Value.Description); Assert.Equal(HealthStatus.Healthy, actual.Value.Status); Assert.Null(actual.Value.Exception); }); } [Fact] public async Task CheckHealthAsync_SetsUpALoggerScopeForEachCheck() { // Arrange var sink = new TestSink(); var check = new DelegateHealthCheck(cancellationToken => { Assert.Collection(sink.Scopes, actual => { Assert.Equal(actual.LoggerName, typeof(DefaultHealthCheckService).FullName); Assert.Collection((IEnumerable>)actual.Scope, item => { Assert.Equal("HealthCheckName", item.Key); Assert.Equal("TestScope", item.Value); }); }); return Task.FromResult(HealthCheckResult.Healthy()); }); var loggerFactory = new TestLoggerFactory(sink, enabled: true); var service = CreateHealthChecksService(b => { // Override the logger factory for testing b.Services.AddSingleton(loggerFactory); b.AddCheck("TestScope", check); }); // Act var results = await service.CheckHealthAsync(); // Assert Assert.Collection(results.Entries, actual => { Assert.Equal("TestScope", actual.Key); Assert.Equal(HealthStatus.Healthy, actual.Value.Status); }); } [Fact] public async Task CheckHealthAsync_CheckCanDependOnTransientService() { // Arrange var service = CreateHealthChecksService(b => { b.Services.AddTransient(); b.AddCheck("Test"); }); // Act var results = await service.CheckHealthAsync(); // Assert Assert.Collection( results.Entries, actual => { Assert.Equal("Test", actual.Key); Assert.Equal(HealthStatus.Healthy, actual.Value.Status); }); } [Fact] public async Task CheckHealthAsync_CheckCanDependOnScopedService() { // Arrange var service = CreateHealthChecksService(b => { b.Services.AddScoped(); b.AddCheck("Test"); }); // Act var results = await service.CheckHealthAsync(); // Assert Assert.Collection( results.Entries, actual => { Assert.Equal("Test", actual.Key); Assert.Equal(HealthStatus.Healthy, actual.Value.Status); }); } [Fact] public async Task CheckHealthAsync_CheckCanDependOnSingletonService() { // Arrange var service = CreateHealthChecksService(b => { b.Services.AddSingleton(); b.AddCheck("Test"); }); // Act var results = await service.CheckHealthAsync(); // Assert Assert.Collection( results.Entries, actual => { Assert.Equal("Test", actual.Key); Assert.Equal(HealthStatus.Healthy, actual.Value.Status); }); } private static DefaultHealthCheckService CreateHealthChecksService(Action configure) { var services = new ServiceCollection(); services.AddLogging(); services.AddOptions(); var builder = services.AddHealthChecks(); if (configure != null) { configure(builder); } return (DefaultHealthCheckService)services.BuildServiceProvider(validateScopes: true).GetRequiredService(); } private class AnotherService { } private class CheckWithServiceDependency : IHealthCheck { public CheckWithServiceDependency(AnotherService _) { } public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { return Task.FromResult(HealthCheckResult.Healthy()); } } private class NameCapturingCheck : IHealthCheck { public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { var data = new Dictionary() { { "name", context.Registration.Name }, }; return Task.FromResult(HealthCheckResult.Healthy(data: data)); } } } }