using FluentAssertions; using FluentValidation; using FluentValidation.Results; using MediatR; using Microsoft.Extensions.Logging; using Moq; using Shared.Application.Behaviors; namespace Shared.Application.Tests.Behaviors; public class UnhandledExceptionLoggingBehaviorTests { private readonly Mock< ILogger> > _loggerMock = new(); private UnhandledExceptionLoggingBehavior CreateBehavior() => new(_loggerMock.Object); [Fact] public async Task Handle_WhenHandlerThrowsGenericException_LogsOnceAndRethrows() { UnhandledExceptionLoggingBehavior behavior = CreateBehavior(); TestRequest request = new("aaron", "hunter2"); InvalidOperationException thrown = new("boom"); RequestHandlerDelegate next = () => throw thrown; Func act = async () => await behavior.Handle(request, next, CancellationToken.None); (await act.Should().ThrowAsync()) .Which.Should() .BeSameAs(thrown); VerifyLogError(Times.Once()); } [Fact] public async Task Handle_WhenValidationExceptionThrown_DoesNotLogAndRethrows() { UnhandledExceptionLoggingBehavior behavior = CreateBehavior(); TestRequest request = new("aaron", "hunter2"); ValidationException thrown = new([new ValidationFailure("Username", "is required")]); RequestHandlerDelegate next = () => throw thrown; Func act = async () => await behavior.Handle(request, next, CancellationToken.None); (await act.Should().ThrowAsync()).Which.Should().BeSameAs(thrown); VerifyLogError(Times.Never()); } [Fact] public async Task Handle_WhenHandlerSucceeds_DoesNotLogAndReturnsResponse() { UnhandledExceptionLoggingBehavior behavior = CreateBehavior(); TestRequest request = new("aaron", "hunter2"); TestResponse response = new("ok"); RequestHandlerDelegate next = () => Task.FromResult(response); TestResponse result = await behavior.Handle(request, next, CancellationToken.None); result.Should().BeSameAs(response); VerifyLogError(Times.Never()); } [Fact] public async Task Handle_WhenHandlerThrows_RedactsSensitiveFieldsInLoggedPayload() { UnhandledExceptionLoggingBehavior behavior = CreateBehavior(); TestRequest request = new("aaron", "hunter2"); RequestHandlerDelegate next = () => throw new InvalidOperationException("boom"); Func act = async () => await behavior.Handle(request, next, CancellationToken.None); await act.Should().ThrowAsync(); string loggedMessage = CaptureLoggedMessage(); loggedMessage.Should().Contain("aaron").And.NotContain("hunter2"); } private void VerifyLogError(Times times) => _loggerMock.Verify( l => l.Log( LogLevel.Error, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>() ), times ); private string CaptureLoggedMessage() { var invocation = _loggerMock .Invocations.Where(i => i.Method.Name == nameof(ILogger.Log)) .Should() .ContainSingle() .Subject; object state = invocation.Arguments[2]; object? exception = invocation.Arguments[3]; object formatter = invocation.Arguments[4]; System.Reflection.MethodInfo invokeMethod = formatter.GetType().GetMethod("Invoke")!; return (string)invokeMethod.Invoke(formatter, [state, exception])!; } } public record TestRequest(string Username, string Password) : IRequest; public record TestResponse(string Value);