// 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.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Xunit; namespace Microsoft.Extensions.Diagnostics.HealthChecks { public class HealthCheckServiceTests { [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(new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy()))) .AddCheck(new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy()))) .AddCheck(new HealthCheck("Bar", _ => Task.FromResult(HealthCheckResult.Healthy()))) .AddCheck(new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy()))) .AddCheck(new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy()))); var services = serviceCollection.BuildServiceProvider(); var scopeFactory = services.GetRequiredService(); var logger = services.GetRequiredService>(); // Act var exception = Assert.Throws(() => new HealthCheckService(scopeFactory, logger)); // Assert Assert.Equal($"Duplicate health checks were registered with the name(s): Foo, Baz{Environment.NewLine}Parameter name: healthChecks", 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.AddCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data))); b.AddCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage))); b.AddCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception))); }); // Act var results = await service.CheckHealthAsync(); // Assert Assert.Collection(results.Results, actual => { Assert.Equal("HealthyCheck", actual.Key); Assert.Equal(HealthyMessage, actual.Value.Description); Assert.Equal(HealthCheckStatus.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("DegradedCheck", actual.Key); Assert.Equal(DegradedMessage, actual.Value.Description); Assert.Equal(HealthCheckStatus.Degraded, actual.Value.Status); Assert.Null(actual.Value.Exception); Assert.Empty(actual.Value.Data); }, actual => { Assert.Equal("UnhealthyCheck", actual.Key); Assert.Equal(UnhealthyMessage, actual.Value.Description); Assert.Equal(HealthCheckStatus.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.AddCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data))); b.AddCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage))); b.AddCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception))); }); // Act var results = await service.CheckHealthAsync(c => c.Name == "HealthyCheck"); // Assert Assert.Collection(results.Results, actual => { Assert.Equal("HealthyCheck", actual.Key); Assert.Equal(HealthyMessage, actual.Value.Description); Assert.Equal(HealthCheckStatus.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_ConvertsExceptionInHealthCheckerToFailedResultAsync() { // Arrange var thrownException = new InvalidOperationException("Whoops!"); var faultedException = new InvalidOperationException("Ohnoes!"); var service = CreateHealthChecksService(b => { b.AddCheck("Throws", ct => throw thrownException); b.AddCheck("Faults", ct => Task.FromException(faultedException)); b.AddCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy())); }); // Act var results = await service.CheckHealthAsync(); // Assert Assert.Collection(results.Results, actual => { Assert.Equal("Throws", actual.Key); Assert.Equal(thrownException.Message, actual.Value.Description); Assert.Equal(HealthCheckStatus.Failed, actual.Value.Status); Assert.Same(thrownException, actual.Value.Exception); }, actual => { Assert.Equal("Faults", actual.Key); Assert.Equal(faultedException.Message, actual.Value.Description); Assert.Equal(HealthCheckStatus.Failed, actual.Value.Status); Assert.Same(faultedException, actual.Value.Exception); }, actual => { Assert.Equal("Succeeds", actual.Key); Assert.Empty(actual.Value.Description); Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); Assert.Null(actual.Value.Exception); }); } [Fact] public async Task CheckHealthAsync_SetsUpALoggerScopeForEachCheck() { // Arrange var sink = new TestSink(); var check = new HealthCheck("TestScope", cancellationToken => { Assert.Collection(sink.Scopes, actual => { Assert.Equal(actual.LoggerName, typeof(HealthCheckService).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(check); }); // Act var results = await service.CheckHealthAsync(); // Assert Assert.Collection(results.Results, actual => { Assert.Equal("TestScope", actual.Key); Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); }); } [Fact] public async Task CheckHealthAsync_ThrowsIfCheckReturnsUnknownStatusResult() { // Arrange var service = CreateHealthChecksService(b => { b.AddCheck("Kaboom", ct => Task.FromResult(default(HealthCheckResult))); }); // Act var ex = await Assert.ThrowsAsync(() => service.CheckHealthAsync()); // Assert Assert.Equal("Health check 'Kaboom' returned a result with a status of Unknown", ex.Message); } [Fact] public async Task CheckHealthAsync_CheckCanDependOnTransientService() { // Arrange var service = CreateHealthChecksService(b => { b.Services.AddTransient(); b.AddCheck(); }); // Act var results = await service.CheckHealthAsync(); // Assert Assert.Collection( results.Results, actual => { Assert.Equal("Test", actual.Key); Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); }); } [Fact] public async Task CheckHealthAsync_CheckCanDependOnScopedService() { // Arrange var service = CreateHealthChecksService(b => { b.Services.AddScoped(); b.AddCheck(); }); // Act var results = await service.CheckHealthAsync(); // Assert Assert.Collection( results.Results, actual => { Assert.Equal("Test", actual.Key); Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); }); } [Fact] public async Task CheckHealthAsync_CheckCanDependOnSingletonService() { // Arrange var service = CreateHealthChecksService(b => { b.Services.AddSingleton(); b.AddCheck(); }); // Act var results = await service.CheckHealthAsync(); // Assert Assert.Collection( results.Results, actual => { Assert.Equal("Test", actual.Key); Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); }); } private static HealthCheckService CreateHealthChecksService(Action configure) { var services = new ServiceCollection(); services.AddLogging(); services.AddOptions(); var builder = services.AddHealthChecks(); if (configure != null) { configure(builder); } return (HealthCheckService)services.BuildServiceProvider(validateScopes: true).GetRequiredService(); } private class AnotherService { } private class CheckWithServiceDependency : IHealthCheck { public CheckWithServiceDependency(AnotherService _) { } public string Name => "Test"; public Task CheckHealthAsync(CancellationToken cancellationToken = default) { return Task.FromResult(HealthCheckResult.Healthy()); } } } }