diff --git a/web/backend/Shared/Shared.Application.Tests/Behaviors/ValidationBehaviorTests.cs b/web/backend/Shared/Shared.Application.Tests/Behaviors/ValidationBehaviorTests.cs new file mode 100644 index 00000000..db91f2da --- /dev/null +++ b/web/backend/Shared/Shared.Application.Tests/Behaviors/ValidationBehaviorTests.cs @@ -0,0 +1,189 @@ +using FluentAssertions; +using FluentValidation; +using FluentValidation.Results; +using MediatR; +using Moq; +using Shared.Application.Behaviors; + +namespace Shared.Application.Tests.Behaviors; + +public class ValidationBehaviorTests +{ + private static ValidationBehavior CreateBehavior( + params IValidator[] validators + ) => new(validators); + + [Fact] + public async Task Handle_WhenRequestPassesAllValidators_InvokesNextUnchanged() + { + Mock> validatorMock = new(); + validatorMock + .Setup(v => + v.ValidateAsync( + It.IsAny>(), + It.IsAny() + ) + ) + .ReturnsAsync(new ValidationResult()); + ValidationBehavior behavior = CreateBehavior( + validatorMock.Object + ); + ValidationTestRequest request = new("aaron"); + ValidationTestResponse response = new("ok"); + bool nextInvoked = false; + RequestHandlerDelegate next = () => + { + nextInvoked = true; + return Task.FromResult(response); + }; + + ValidationTestResponse result = await behavior.Handle( + request, + next, + CancellationToken.None + ); + + nextInvoked.Should().BeTrue(); + result.Should().BeSameAs(response); + } + + [Fact] + public async Task Handle_WhenValidatorReportsFailures_ThrowsValidationExceptionAndNeverInvokesNext() + { + ValidationFailure failure = new("Name", "is required"); + Mock> validatorMock = new(); + validatorMock + .Setup(v => + v.ValidateAsync( + It.IsAny>(), + It.IsAny() + ) + ) + .ReturnsAsync(new ValidationResult([failure])); + ValidationBehavior behavior = CreateBehavior( + validatorMock.Object + ); + ValidationTestRequest request = new(""); + bool nextInvoked = false; + RequestHandlerDelegate next = () => + { + nextInvoked = true; + return Task.FromResult(new ValidationTestResponse("ok")); + }; + + Func act = async () => await behavior.Handle(request, next, CancellationToken.None); + + (await act.Should().ThrowAsync()).Which.Errors.Should() + .ContainSingle() + .Which.Should() + .BeSameAs(failure); + nextInvoked.Should().BeFalse(); + } + + [Fact] + public async Task Handle_WhenMultipleValidatorsAreRegistered_RunsAllOfThem() + { + Mock> firstValidatorMock = new(); + firstValidatorMock + .Setup(v => + v.ValidateAsync( + It.IsAny>(), + It.IsAny() + ) + ) + .ReturnsAsync(new ValidationResult()); + Mock> secondValidatorMock = new(); + secondValidatorMock + .Setup(v => + v.ValidateAsync( + It.IsAny>(), + It.IsAny() + ) + ) + .ReturnsAsync(new ValidationResult()); + ValidationBehavior behavior = CreateBehavior( + firstValidatorMock.Object, + secondValidatorMock.Object + ); + ValidationTestRequest request = new("aaron"); + RequestHandlerDelegate next = () => + Task.FromResult(new ValidationTestResponse("ok")); + + await behavior.Handle(request, next, CancellationToken.None); + + firstValidatorMock.Verify( + v => + v.ValidateAsync( + It.IsAny>(), + It.IsAny() + ), + Times.Once() + ); + secondValidatorMock.Verify( + v => + v.ValidateAsync( + It.IsAny>(), + It.IsAny() + ), + Times.Once() + ); + } + + [Fact] + public async Task Handle_WhenMultipleValidatorsReportFailures_AggregatesErrorsFromAll() + { + ValidationFailure firstFailure = new("Name", "is required"); + ValidationFailure secondFailure = new("Name", "must be at least 3 characters"); + Mock> firstValidatorMock = new(); + firstValidatorMock + .Setup(v => + v.ValidateAsync( + It.IsAny>(), + It.IsAny() + ) + ) + .ReturnsAsync(new ValidationResult([firstFailure])); + Mock> secondValidatorMock = new(); + secondValidatorMock + .Setup(v => + v.ValidateAsync( + It.IsAny>(), + It.IsAny() + ) + ) + .ReturnsAsync(new ValidationResult([secondFailure])); + ValidationBehavior behavior = CreateBehavior( + firstValidatorMock.Object, + secondValidatorMock.Object + ); + ValidationTestRequest request = new(""); + RequestHandlerDelegate next = () => + Task.FromResult(new ValidationTestResponse("ok")); + + Func act = async () => await behavior.Handle(request, next, CancellationToken.None); + + (await act.Should().ThrowAsync()).Which.Errors.Should() + .BeEquivalentTo([firstFailure, secondFailure]); + } + + [Fact] + public async Task Handle_WhenNoValidatorsAreRegistered_InvokesNext() + { + ValidationBehavior behavior = CreateBehavior(); + ValidationTestRequest request = new("aaron"); + ValidationTestResponse response = new("ok"); + RequestHandlerDelegate next = () => Task.FromResult(response); + + ValidationTestResponse result = await behavior.Handle( + request, + next, + CancellationToken.None + ); + + result.Should().BeSameAs(response); + } +} + +public record ValidationTestRequest(string Name) : IRequest; + +public record ValidationTestResponse(string Value);