diff --git a/Blazored.FluentValidation.sln b/Blazored.FluentValidation.sln index 5b8ce3e..bc1abab 100644 --- a/Blazored.FluentValidation.sln +++ b/Blazored.FluentValidation.sln @@ -20,6 +20,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorServer", "samples\Bla EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedModels", "samples\Shared\SharedModels\SharedModels.csproj", "{42276235-5139-41D6-923D-18B7EB5E3E44}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{DACAA0DB-2B93-4FE1-9D21-F45A4E63A640}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blazored.FluentValidation.Tests", "tests\Blazored.FluentValidation.Tests\Blazored.FluentValidation.Tests.csproj", "{C92DF59B-B760-4FCC-A34C-A4007529BCC5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -78,6 +82,18 @@ Global {42276235-5139-41D6-923D-18B7EB5E3E44}.Release|x64.Build.0 = Release|Any CPU {42276235-5139-41D6-923D-18B7EB5E3E44}.Release|x86.ActiveCfg = Release|Any CPU {42276235-5139-41D6-923D-18B7EB5E3E44}.Release|x86.Build.0 = Release|Any CPU + {C92DF59B-B760-4FCC-A34C-A4007529BCC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C92DF59B-B760-4FCC-A34C-A4007529BCC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C92DF59B-B760-4FCC-A34C-A4007529BCC5}.Debug|x64.ActiveCfg = Debug|Any CPU + {C92DF59B-B760-4FCC-A34C-A4007529BCC5}.Debug|x64.Build.0 = Debug|Any CPU + {C92DF59B-B760-4FCC-A34C-A4007529BCC5}.Debug|x86.ActiveCfg = Debug|Any CPU + {C92DF59B-B760-4FCC-A34C-A4007529BCC5}.Debug|x86.Build.0 = Debug|Any CPU + {C92DF59B-B760-4FCC-A34C-A4007529BCC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C92DF59B-B760-4FCC-A34C-A4007529BCC5}.Release|Any CPU.Build.0 = Release|Any CPU + {C92DF59B-B760-4FCC-A34C-A4007529BCC5}.Release|x64.ActiveCfg = Release|Any CPU + {C92DF59B-B760-4FCC-A34C-A4007529BCC5}.Release|x64.Build.0 = Release|Any CPU + {C92DF59B-B760-4FCC-A34C-A4007529BCC5}.Release|x86.ActiveCfg = Release|Any CPU + {C92DF59B-B760-4FCC-A34C-A4007529BCC5}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -86,6 +102,7 @@ Global {8BC1065A-A71E-4568-8A67-9C3AF039F73A} = {D5C6DCA9-C2BD-4117-BCCC-19E36E8406AB} {2459CF4B-6548-4031-B784-43E943E270A9} = {D5C6DCA9-C2BD-4117-BCCC-19E36E8406AB} {42276235-5139-41D6-923D-18B7EB5E3E44} = {D5C6DCA9-C2BD-4117-BCCC-19E36E8406AB} + {C92DF59B-B760-4FCC-A34C-A4007529BCC5} = {DACAA0DB-2B93-4FE1-9D21-F45A4E63A640} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {42B22D99-6E59-4B30-88AD-B9CC07E0DA49} diff --git a/README.md b/README.md index 2a95284..00f9ce5 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ The second is when manually validating the model using the `Validate` or `Valida ``` ## Access to full `ValidationFailure` -If you need details about the specifics of a validation result (e.g. its `Severity), you can access the result of the +If you need details about the specifics of a validation result (e.g. its `Severity`), you can access the result of the last validation by calling the `GetFailuresFromLastValidation` method on the `FluentValidationValidator` component. ```razor diff --git a/tests/Blazored.FluentValidation.Tests/AssemblyScanning/Component.razor b/tests/Blazored.FluentValidation.Tests/AssemblyScanning/Component.razor new file mode 100644 index 0000000..e501004 --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/AssemblyScanning/Component.razor @@ -0,0 +1,33 @@ + + + @if (DisableAssemblyScanning is null) + { + + } + else + { + + } + + + +

+ + +

+ + +
+ +@code { + [Parameter] public bool? DisableAssemblyScanning { get; set; } + private readonly Person _person = new(); + + internal ValidationResultType Result { get; private set; } = ValidationResultType.Valid; + + private void ValidSubmit() => Result = ValidationResultType.Valid; + private void InvalidSubmit() => Result = ValidationResultType.Error; +} \ No newline at end of file diff --git a/tests/Blazored.FluentValidation.Tests/AssemblyScanning/Readme.md b/tests/Blazored.FluentValidation.Tests/AssemblyScanning/Readme.md new file mode 100644 index 0000000..4540b8d --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/AssemblyScanning/Readme.md @@ -0,0 +1,9 @@ +### What does this test? +This test checks if the assembly scanning works. It leverages, that this test +assembly does not register any `AbstractValidator` by default. + + - Setting the `DisableAssemblyScanning` to `true` should not find any validators and ignore errors. + - Setting the `DisableAssemblyScanning` to `false` or not setting the attribute at all, should + find the validators in the assembly and validate normally. + - Setting the `DisableAssemblyScanning` to `true` and registering the validators manually should + find the validators and validate normally. diff --git a/tests/Blazored.FluentValidation.Tests/AssemblyScanning/Tests.cs b/tests/Blazored.FluentValidation.Tests/AssemblyScanning/Tests.cs new file mode 100644 index 0000000..32be18a --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/AssemblyScanning/Tests.cs @@ -0,0 +1,80 @@ +using Blazored.FluentValidation.Tests.Model; + +namespace Blazored.FluentValidation.Tests.AssemblyScanning; + +public class Tests : TestContext +{ + private readonly Fixture _fixture = new(); + + [Fact] + public void DisableAssemblyScanning_SetToTrue_NoValidationHappens() + { + // Arrange + var cut = RenderComponent(p => p.Add(c => c.DisableAssemblyScanning, true)); + var person = _fixture.InvalidPerson(); + + // Act + cut.Find($"input[name={nameof(Person.FirstName)}]").Change(person.FirstName); + cut.Find("button").Click(); + + // Assert + cut.Instance.Result.Should().Be(ValidationResultType.Valid); + } + + [Fact] + public void DisableAssemblyScanning_SetToFalse_ValidationHappens() + { + // Arrange + var cut = RenderComponent(p => p.Add(c => c.DisableAssemblyScanning, false)); + var person = _fixture.InvalidPerson(); + + // Act + cut.Find($"input[name={nameof(Person.FirstName)}]").Change(person.FirstName); + cut.Find("button").Click(); + + // Assert + cut.Instance.Result.Should().Be(ValidationResultType.Error); + } + + [Fact] + public void DisableAssemblyScanning_NotSet_ValidationHappens() + { + // Arrange + var cut = RenderComponent(p => p.Add(c => c.DisableAssemblyScanning, null)); + var person = _fixture.InvalidPerson(); + + // Act + cut.Find($"input[name={nameof(Person.FirstName)}]").Change(person.FirstName); + cut.Find("button").Click(); + + // Assert + cut.Instance.Result.Should().Be(ValidationResultType.Error); + } + + [Fact] + public void DisableAssemblyScanning_SetToTrueButValidatorsRegistered_ValidationHappens() + { + // Arrange + Services.AddTransient, PersonValidator>(); + var cut = RenderComponent(p => p.Add(c => c.DisableAssemblyScanning, null)); + var person = _fixture.InvalidPerson(); + + // Act + cut.Find($"input[name={nameof(Person.FirstName)}]").Change(person.FirstName); + cut.Find("button").Click(); + + // Assert + cut.Instance.Result.Should().Be(ValidationResultType.Error); + } + + private class Fixture + { + public Person InvalidPerson() => new() + { + FirstName = "", + LastName = "Doe", + EmailAddress = "john.doe@blazored.org", + Age = 30 + }; + } +} \ No newline at end of file diff --git a/tests/Blazored.FluentValidation.Tests/BasicValidation/Component.razor b/tests/Blazored.FluentValidation.Tests/BasicValidation/Component.razor new file mode 100644 index 0000000..17b2f0f --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/BasicValidation/Component.razor @@ -0,0 +1,43 @@ + + + + + +

+ + +

+ +

+ + +

+ +

+ + +

+ +

+ + +

+ +

+ + +

+ + +
+ +@code { + private readonly Person _person = new() { Address = new() }; + internal ValidationResultType Result { get; private set; } = ValidationResultType.Valid; + + private void ValidSubmit() => Result = ValidationResultType.Valid; + private void InvalidSubmit() => Result = ValidationResultType.Error; +} \ No newline at end of file diff --git a/tests/Blazored.FluentValidation.Tests/BasicValidation/Readme.md b/tests/Blazored.FluentValidation.Tests/BasicValidation/Readme.md new file mode 100644 index 0000000..4ab205e --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/BasicValidation/Readme.md @@ -0,0 +1,7 @@ +### What does this test? +This test checks if the basic validation works. + + - Does a valid model pass the validation? + - Do basic validation rules get picked up? + - Do validation errors get displayed correctly in the UI? + - Are nested rules validated correctly? diff --git a/tests/Blazored.FluentValidation.Tests/BasicValidation/Tests.cs b/tests/Blazored.FluentValidation.Tests/BasicValidation/Tests.cs new file mode 100644 index 0000000..fdcef88 --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/BasicValidation/Tests.cs @@ -0,0 +1,110 @@ +using Blazored.FluentValidation.Tests.Model; + +namespace Blazored.FluentValidation.Tests.BasicValidation; + +public class Tests : TestContext +{ + private readonly Fixture _fixture = new(); + + [Fact] + public void Validate_DataIsValid_ValidSubmit() + { + // Arrange + var cut = RenderComponent(); + var person = _fixture.ValidPerson(); + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + + // Assert + cut.Instance.Result.Should().Be(ValidationResultType.Valid); + } + + [Fact] + public void Validate_FirstNameMissing_InvalidSubmit() + { + // Arrange + var cut = RenderComponent(); + var person = _fixture.ValidPerson() with { FirstName = string.Empty }; + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + + // Assert + cut.Instance.Result.Should().Be(ValidationResultType.Error); + } + + [Fact] + public void Validate_FirstNameMissing_ValidationErrorsPresent() + { + // Arrange + var cut = RenderComponent(); + var person = _fixture.ValidPerson() with { FirstName = string.Empty }; + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + + // Assert + cut.Find(".validation-errors>.validation-message").TextContent.Should().Contain(PersonValidator.FirstNameRequired); + cut.Find("li.validation-message").TextContent.Should().Contain(PersonValidator.FirstNameRequired); + } + + [Fact] + public void Validate_AgeTooOld_ValidationErrorsPresent() + { + // Arrange + var cut = RenderComponent(); + var person = _fixture.ValidPerson() with { Age = 250 }; + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + + // Assert + cut.Find(".validation-errors>.validation-message").TextContent.Should().Contain(PersonValidator.AgeMax); + } + + [Fact] + public void Validate_AddressLine1Missing_ValidationErrorsPresent() + { + // Arrange + var cut = RenderComponent(); + var person = _fixture.ValidPerson() with { Address = new() { Line1 = string.Empty } }; + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + + // Assert + cut.Find(".validation-errors>.validation-message").TextContent.Should().Contain(AddressValidator.Line1Required); + } + + private static void FillForm(IRenderedComponent cut, Person person) + { + cut.Find($"input[name={nameof(Person.FirstName)}]").Change(person.FirstName); + cut.Find($"input[name={nameof(Person.LastName)}]").Change(person.LastName); + cut.Find($"input[name={nameof(Person.EmailAddress)}]").Change(person.EmailAddress); + cut.Find($"input[name={nameof(Person.Age)}]").Change(person.Age.ToString()); + cut.Find($"input[name={nameof(Person.Address.Line1)}]").Change(person.Address!.Line1); + } + + private class Fixture + { + public Person ValidPerson() => new() + { + FirstName = "John", + LastName = "Doe", + EmailAddress = "john.doe@blazored.org", + Age = 30, + Address = new() + { + Line1 = "123 Main St", + Town = "Springfield", + Postcode = "12345" + } + }; + } +} \ No newline at end of file diff --git a/tests/Blazored.FluentValidation.Tests/Blazored.FluentValidation.Tests.csproj b/tests/Blazored.FluentValidation.Tests/Blazored.FluentValidation.Tests.csproj new file mode 100644 index 0000000..8269342 --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/Blazored.FluentValidation.Tests.csproj @@ -0,0 +1,41 @@ + + + + net7.0 + enable + false + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + diff --git a/tests/Blazored.FluentValidation.Tests/DirectValidation/AsyncComponent.razor b/tests/Blazored.FluentValidation.Tests/DirectValidation/AsyncComponent.razor new file mode 100644 index 0000000..c0cafe7 --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/DirectValidation/AsyncComponent.razor @@ -0,0 +1,39 @@ + + + + +

+ + +

+ +

+ + +

+ +

+ + +

+ +

+ + +

+ + +
+ +@code { + private readonly Person _person = new(); + private FluentValidationValidator? _fluentValidationValidator; + + public ValidationResultType? Result { get; private set; } + + private async Task SubmitAsync() + { + var result = await _fluentValidationValidator!.ValidateAsync(); + Result = result ? ValidationResultType.Valid : ValidationResultType.Error; + } +} \ No newline at end of file diff --git a/tests/Blazored.FluentValidation.Tests/DirectValidation/AsyncTests.cs b/tests/Blazored.FluentValidation.Tests/DirectValidation/AsyncTests.cs new file mode 100644 index 0000000..634efe3 --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/DirectValidation/AsyncTests.cs @@ -0,0 +1,76 @@ +using Blazored.FluentValidation.Tests.Model; + +namespace Blazored.FluentValidation.Tests.DirectValidation; + +public class AsyncTests : TestContext +{ + private readonly Fixture _fixture = new(); + + [Fact] + public void ValidateAsync_PersonIsValid_ResultIsValid() + { + // Arrange + var cut = RenderComponent(); + var person = _fixture.ValidPerson(); + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + cut.WaitForState(() => cut.Instance.Result is not null); + + // Assert + cut.Instance.Result.Should().Be(ValidationResultType.Valid); + } + + [Fact] + public void ValidateAsync_AgeNegative_ResultIsError() + { + // Arrange + var cut = RenderComponent(); + var person = _fixture.ValidPerson() with { Age = -5 }; + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + cut.WaitForState(() => cut.Instance.Result is not null); + + // Assert + cut.Instance.Result.Should().Be(ValidationResultType.Error); + } + + [Fact] + public void ValidateAsync_AgeNegative_ValidationMessagesPresent() + { + // Arrange + var cut = RenderComponent(); + var person = _fixture.ValidPerson() with { Age = -5 }; + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + cut.WaitForState(() => cut.Instance.Result is not null); + + // Assert + cut.Find(".validation-errors>.validation-message").TextContent.Should().Contain(PersonValidator.AgeMin); + cut.Find("li.validation-message").TextContent.Should().Contain(PersonValidator.AgeMin); + } + + private void FillForm(IRenderedComponent cut, Person person) + { + cut.Find("input[name=FirstName]").Change(person.FirstName); + cut.Find("input[name=LastName]").Change(person.LastName); + cut.Find("input[name=Age]").Change(person.Age.ToString()); + cut.Find("input[name=EmailAddress]").Change(person.EmailAddress); + } + + private class Fixture + { + public Person ValidPerson() => new() + { + FirstName = "John", + LastName = "Doe", + Age = 30, + EmailAddress = "john.doe@blazored.com" + }; + } +} \ No newline at end of file diff --git a/tests/Blazored.FluentValidation.Tests/DirectValidation/Readme.md b/tests/Blazored.FluentValidation.Tests/DirectValidation/Readme.md new file mode 100644 index 0000000..e1363a0 --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/DirectValidation/Readme.md @@ -0,0 +1,4 @@ +### What does this test? +This test checks if the `ValidateAsync` method works correctly, +specifically that the `bool` returned is `false` when validation fails, +and `true` otherwise. \ No newline at end of file diff --git a/tests/Blazored.FluentValidation.Tests/DirectValidation/SyncComponent.razor b/tests/Blazored.FluentValidation.Tests/DirectValidation/SyncComponent.razor new file mode 100644 index 0000000..00a73e9 --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/DirectValidation/SyncComponent.razor @@ -0,0 +1,39 @@ + + + + +

+ + +

+ +

+ + +

+ +

+ + +

+ +

+ + +

+ + +
+ +@code { + private readonly Person _person = new(); + private FluentValidationValidator? _fluentValidationValidator; + + public ValidationResultType? Result { get; private set; } + + private void Submit() + { + var result = _fluentValidationValidator!.Validate(); + Result = result ? ValidationResultType.Valid : ValidationResultType.Error; + } +} \ No newline at end of file diff --git a/tests/Blazored.FluentValidation.Tests/DirectValidation/SyncTests.cs b/tests/Blazored.FluentValidation.Tests/DirectValidation/SyncTests.cs new file mode 100644 index 0000000..cd48fcb --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/DirectValidation/SyncTests.cs @@ -0,0 +1,73 @@ +using Blazored.FluentValidation.Tests.Model; + +namespace Blazored.FluentValidation.Tests.DirectValidation; + +public class SyncTests : TestContext +{ + private readonly Fixture _fixture = new(); + + [Fact] + public void Validate_PersonIsValid_ResultIsValid() + { + // Arrange + var cut = RenderComponent(); + var person = _fixture.ValidPerson(); + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + + // Assert + cut.Instance.Result.Should().Be(ValidationResultType.Valid); + } + + [Fact] + public void Validate_AgeNegative_ResultIsError() + { + // Arrange + var cut = RenderComponent(); + var person = _fixture.ValidPerson() with { Age = -5 }; + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + + // Assert + cut.Instance.Result.Should().Be(ValidationResultType.Error); + } + + [Fact] + public void Validate_AgeNegative_ValidationMessagesPresent() + { + // Arrange + var cut = RenderComponent(); + var person = _fixture.ValidPerson() with { Age = -5 }; + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + + // Assert + cut.Find(".validation-errors>.validation-message").TextContent.Should().Contain(PersonValidator.AgeMin); + cut.Find("li.validation-message").TextContent.Should().Contain(PersonValidator.AgeMin); + } + + private void FillForm(IRenderedComponent cut, Person person) + { + cut.Find("input[name=FirstName]").Change(person.FirstName); + cut.Find("input[name=LastName]").Change(person.LastName); + cut.Find("input[name=Age]").Change(person.Age.ToString()); + cut.Find("input[name=EmailAddress]").Change(person.EmailAddress); + } + + private class Fixture + { + public Person ValidPerson() => new() + { + FirstName = "John", + LastName = "Doe", + Age = 30, + EmailAddress = "john.doe@blazored.com" + }; + } +} \ No newline at end of file diff --git a/tests/Blazored.FluentValidation.Tests/FullFailureAccess/AsyncComponent.razor b/tests/Blazored.FluentValidation.Tests/FullFailureAccess/AsyncComponent.razor new file mode 100644 index 0000000..144144b --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/FullFailureAccess/AsyncComponent.razor @@ -0,0 +1,55 @@ + + + + + +

+ + +

+ +

+ + +

+ +

+ + +

+ +

+ + +

+ + +
+ +@code { + private readonly Person _person = new(); + private FluentValidationValidator? _fluentValidationValidator; + + public ValidationResultType? Result { get; private set; } + + protected async Task Submit() + { + await _fluentValidationValidator!.ValidateAsync(); + var lastResult = _fluentValidationValidator!.GetFailuresFromLastValidation(); + if (!lastResult.Any()) + { + Result = ValidationResultType.Valid; + } + else if (lastResult.Any(failure => failure.Severity == Severity.Error)) + { + Result = ValidationResultType.Error; + } + else + { + Result = ValidationResultType.Warning; + } + } + +} \ No newline at end of file diff --git a/tests/Blazored.FluentValidation.Tests/FullFailureAccess/AsyncTests.cs b/tests/Blazored.FluentValidation.Tests/FullFailureAccess/AsyncTests.cs new file mode 100644 index 0000000..8491b24 --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/FullFailureAccess/AsyncTests.cs @@ -0,0 +1,77 @@ +using System; +using Blazored.FluentValidation.Tests.Model; + +namespace Blazored.FluentValidation.Tests.FullFailureAccess; + +public class AsyncTests : TestContext +{ + private readonly Fixture _fixture = new(); + + [Fact] + public void GetFailuresFromLastValidation_PersonValid_ResultIsValid() + { + // Arrange + var person = _fixture.ValidPerson(); + var cut = RenderComponent(); + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + cut.WaitForState(() => cut.Instance.Result is not null); + + // Assert + cut.Instance.Result.Should().Be(ValidationResultType.Valid); + } + + [Fact] + public void GetFailuresFromLastValidation_EmailInvalid_ResultIsError() + { + // Arrange + var person = _fixture.ValidPerson() with { EmailAddress = "invalid-email" }; + var cut = RenderComponent(); + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + cut.WaitForState(() => cut.Instance.Result is not null); + + // Assert + cut.Instance.Result.Should().Be(ValidationResultType.Error); + } + + [Fact] + public void GetFailuresFromLastValidation_AgeSuspect_ResultIsWarning() + { + // Arrange + var person = _fixture.ValidPerson() with { Age = 69 }; + var cut = RenderComponent(); + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + cut.WaitForState(() => cut.Instance.Result is not null); + + // Assert + cut.Instance.Result.Should().Be(ValidationResultType.Warning); + } + + + private static void FillForm(IRenderedComponent cut, Person person) + { + cut.Find($"input[name={nameof(Person.FirstName)}]").Change(person.FirstName); + cut.Find($"input[name={nameof(Person.LastName)}]").Change(person.LastName); + cut.Find($"input[name={nameof(Person.EmailAddress)}]").Change(person.EmailAddress); + cut.Find($"input[name={nameof(Person.Age)}]").Change(person.Age.ToString()); + } + + private class Fixture + { + public Person ValidPerson() => new() + { + FirstName = "John", + LastName = "Doe", + EmailAddress = "john.doe@blazored.com", + Age = 30 + }; + } +} \ No newline at end of file diff --git a/tests/Blazored.FluentValidation.Tests/FullFailureAccess/Readme.md b/tests/Blazored.FluentValidation.Tests/FullFailureAccess/Readme.md new file mode 100644 index 0000000..aff880a --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/FullFailureAccess/Readme.md @@ -0,0 +1,5 @@ +### What does this test? +This test checks if the `GetFailuresFromLastValidation` method works correctly. It does so by both using `Validate` +and `ValidateAsync`. The failures given back are then checked for severity. + +To test warnings, the age of a person can be set to 69. diff --git a/tests/Blazored.FluentValidation.Tests/FullFailureAccess/SyncComponent.razor b/tests/Blazored.FluentValidation.Tests/FullFailureAccess/SyncComponent.razor new file mode 100644 index 0000000..4958393 --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/FullFailureAccess/SyncComponent.razor @@ -0,0 +1,54 @@ + + + + + +

+ + +

+ +

+ + +

+ +

+ + +

+ +

+ + +

+ + +
+ +@code { + private readonly Person _person = new(); + private FluentValidationValidator? _fluentValidationValidator; + + public ValidationResultType? Result { get; private set; } + + protected void Submit() + { + _fluentValidationValidator!.Validate(); + var lastResult = _fluentValidationValidator!.GetFailuresFromLastValidation(); + if (!lastResult.Any()) + { + Result = ValidationResultType.Valid; + } + else if (lastResult.Any(failure => failure.Severity == Severity.Error)) + { + Result = ValidationResultType.Error; + } + else + { + Result = ValidationResultType.Warning; + } + } +} \ No newline at end of file diff --git a/tests/Blazored.FluentValidation.Tests/FullFailureAccess/SyncTests.cs b/tests/Blazored.FluentValidation.Tests/FullFailureAccess/SyncTests.cs new file mode 100644 index 0000000..78abfe9 --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/FullFailureAccess/SyncTests.cs @@ -0,0 +1,74 @@ +using System; +using Blazored.FluentValidation.Tests.Model; + +namespace Blazored.FluentValidation.Tests.FullFailureAccess; + +public class SyncTests : TestContext +{ + private readonly Fixture _fixture = new(); + + [Fact] + public void GetFailuresFromLastValidation_PersonValid_ResultIsValid() + { + // Arrange + var person = _fixture.ValidPerson(); + var cut = RenderComponent(); + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + + // Assert + cut.Instance.Result.Should().Be(ValidationResultType.Valid); + } + + [Fact] + public void GetFailuresFromLastValidation_EmailInvalid_ResultIsError() + { + // Arrange + var person = _fixture.ValidPerson() with { EmailAddress = "invalid-email" }; + var cut = RenderComponent(); + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + + // Assert + cut.Instance.Result.Should().Be(ValidationResultType.Error); + } + + [Fact] + public void GetFailuresFromLastValidation_AgeSuspect_ResultIsWarning() + { + // Arrange + var person = _fixture.ValidPerson() with { Age = 69 }; + var cut = RenderComponent(); + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + + // Assert + cut.Instance.Result.Should().Be(ValidationResultType.Warning); + } + + + private static void FillForm(IRenderedComponent cut, Person person) + { + cut.Find($"input[name={nameof(Person.FirstName)}]").Change(person.FirstName); + cut.Find($"input[name={nameof(Person.LastName)}]").Change(person.LastName); + cut.Find($"input[name={nameof(Person.EmailAddress)}]").Change(person.EmailAddress); + cut.Find($"input[name={nameof(Person.Age)}]").Change(person.Age.ToString()); + } + + private class Fixture + { + public Person ValidPerson() => new() + { + FirstName = "John", + LastName = "Doe", + EmailAddress = "john.doe@blazored.com", + Age = 30 + }; + } +} \ No newline at end of file diff --git a/tests/Blazored.FluentValidation.Tests/GlobalUsings.cs b/tests/Blazored.FluentValidation.Tests/GlobalUsings.cs new file mode 100644 index 0000000..6b9b9b4 --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using System.Threading.Tasks; +global using FluentAssertions; +global using FluentValidation; \ No newline at end of file diff --git a/tests/Blazored.FluentValidation.Tests/Model/Address.cs b/tests/Blazored.FluentValidation.Tests/Model/Address.cs new file mode 100644 index 0000000..d7a5332 --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/Model/Address.cs @@ -0,0 +1,27 @@ +namespace Blazored.FluentValidation.Tests.Model +{ + public record Address + { + public string? Line1 { get; set; } + public string? Line2 { get; set; } + public string? Town { get; set; } + public string? County { get; set; } + public string? Postcode { get; set; } + } + + public class AddressValidator : AbstractValidator
+ { + public const string Line1Required = "You must enter Line 1"; + public const string TownRequired = "You must enter a town"; + public const string CountyRequired = "You must enter a county"; + public const string PostcodeRequired = "You must enter a postcode"; + + public AddressValidator() + { + RuleFor(p => p.Line1).NotEmpty().WithMessage(Line1Required); + RuleFor(p => p.Town).NotEmpty().WithMessage(TownRequired); + RuleFor(p => p.County).NotEmpty().WithMessage(CountyRequired); + RuleFor(p => p.Postcode).NotEmpty().WithMessage(PostcodeRequired); + } + } +} diff --git a/tests/Blazored.FluentValidation.Tests/Model/Person.cs b/tests/Blazored.FluentValidation.Tests/Model/Person.cs new file mode 100644 index 0000000..fc8c0f5 --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/Model/Person.cs @@ -0,0 +1,66 @@ +namespace Blazored.FluentValidation.Tests.Model +{ + public record Person + { + public string? FirstName { get; set; } + public string? LastName { get; set; } + public int? Age { get; set; } + public string? EmailAddress { get; set; } + public Address? Address { get; set; } + } + + public class PersonValidator : AbstractValidator + { + public const string FirstNameRequired = "You must enter your first name"; + public const string FirstNameMaxLength = "First name cannot be longer than 50 characters"; + public const string LastNameRequired = "You must enter your last name"; + public const string LastNameMaxLength = "Last name cannot be longer than 50 characters"; + public const string AgeRequired = "You must enter your age"; + public const string AgeMin = "Age must be greater than 0"; + public const string AgeMax = "Age cannot be greater than 150"; + public const string AgeSuspect = "Age is suspect. Troll?"; + public const string EmailRequired = "You must enter an email address"; + public const string EmailValid = "You must provide a valid email address"; + public const string EmailUnique = "Email address must be unique"; + public const string DuplicateEmail = "mail@my.com"; + + public PersonValidator() + { + RuleSet("Names", () => + { + RuleFor(p => p.FirstName) + .NotEmpty().WithMessage(FirstNameRequired) + .MaximumLength(50).WithMessage(FirstNameMaxLength); + + RuleFor(p => p.LastName) + .NotEmpty().WithMessage(LastNameRequired) + .MaximumLength(50).WithMessage(LastNameMaxLength); + }); + + RuleFor(p => p.Age) + .NotNull().WithMessage(AgeRequired) + .GreaterThanOrEqualTo(0).WithMessage(AgeMin) + .LessThan(150).WithMessage(AgeMax); + + RuleFor(p => p.EmailAddress) + .NotEmpty().WithMessage(EmailRequired) + .EmailAddress().WithMessage(EmailValid) + .MustAsync(async (email, _) => await IsUniqueAsync(email)).WithMessage(EmailUnique); + + RuleFor(p => p.Address!) + .SetValidator(new AddressValidator()) + .When(p => p.Address is not null); + + RuleFor(p => p.Age) + .NotEqual(69) + .WithMessage(AgeSuspect) + .WithSeverity(Severity.Warning); + } + + private static async Task IsUniqueAsync(string? email) + { + await Task.Delay(300); + return email?.ToLower() != DuplicateEmail; + } + } +} diff --git a/tests/Blazored.FluentValidation.Tests/Model/ValidationResultType.cs b/tests/Blazored.FluentValidation.Tests/Model/ValidationResultType.cs new file mode 100644 index 0000000..e35c667 --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/Model/ValidationResultType.cs @@ -0,0 +1,8 @@ +namespace Blazored.FluentValidation.Tests.Model; + +public enum ValidationResultType +{ + Valid, + Warning, + Error +} \ No newline at end of file diff --git a/tests/Blazored.FluentValidation.Tests/RuleSets/Component.razor b/tests/Blazored.FluentValidation.Tests/RuleSets/Component.razor new file mode 100644 index 0000000..fa209c7 --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/RuleSets/Component.razor @@ -0,0 +1,53 @@ + + + @if (IncludeWithAttribute) + { + @*

Can't implement.. bugged?

*@ + } + else + { + + } + + +

+ + +

+ +

+ + +

+ +

+ + +

+ +

+ + +

+ + +
+ +@code { + [Parameter] public bool IncludeWithAttribute { get; set; } + [Parameter] public bool IncludeWithCode { get; set; } + + private readonly Person _person = new(); + private FluentValidationValidator? _fluentValidationValidator; + + public ValidationResultType? Result { get; private set; } + + protected void Submit() + { + var result = _fluentValidationValidator!.Validate(o => o.IncludeRuleSets("Names")); + Result = result ? ValidationResultType.Valid : ValidationResultType.Error; + } + +} \ No newline at end of file diff --git a/tests/Blazored.FluentValidation.Tests/RuleSets/Readme.md b/tests/Blazored.FluentValidation.Tests/RuleSets/Readme.md new file mode 100644 index 0000000..afc393f --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/RuleSets/Readme.md @@ -0,0 +1,17 @@ +### What does this test? +This test checks if using the `IncludeRuleSets..` method work, once by attribute + +```html + +``` + +and once by code + +```csharp +@code { + private FluentValidationValidator? _fluentValidationValidator; + + private void PartialValidate() + => _fluentValidationValidator?.Validate(options => options.IncludeRuleSets("Names")); +} +``` \ No newline at end of file diff --git a/tests/Blazored.FluentValidation.Tests/RuleSets/Tests.cs b/tests/Blazored.FluentValidation.Tests/RuleSets/Tests.cs new file mode 100644 index 0000000..d0f7ab5 --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/RuleSets/Tests.cs @@ -0,0 +1,79 @@ +using Blazored.FluentValidation.Tests.Model; + +namespace Blazored.FluentValidation.Tests.RuleSets; + +public class Tests : TestContext +{ + private readonly Fixture _fixture = new(); + + [Fact] + public void AddedByAttribute_PersonValid_ValidationPasses() + { + // Arrange + var person = _fixture.ValidPerson(); + var cut = RenderComponent(p => p.Add(c => c.IncludeWithCode, true)); + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + + // Assert + cut.Instance.Result.Should().Be(ValidationResultType.Valid); + } + + [Fact] + public void AddedByAttribute_PersonFirstNameTooLong_ValidationFails() + { + // Arrange + var person = _fixture.ValidPerson() with + { + FirstName = "This name is clearly longer than 50 characters and thus should fail." + }; + var cut = RenderComponent(p => p.Add(c => c.IncludeWithCode, true)); + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + + // Assert + cut.Instance.Result.Should().Be(ValidationResultType.Error); + } + + [Fact] + public void AddedByAttribute_PersonFirstNameTooLong_ValidationMessagesPresent() + { + // Arrange + var person = _fixture.ValidPerson() with + { + FirstName = "This name is clearly longer than 50 characters and thus should fail." + }; + var cut = RenderComponent(p => p.Add(c => c.IncludeWithCode, true)); + + // Act + FillForm(cut, person); + cut.Find("button").Click(); + + // Assert + cut.Find(".validation-errors>.validation-message").TextContent.Should().Contain(PersonValidator.FirstNameMaxLength); + cut.Find("li.validation-message").TextContent.Should().Contain(PersonValidator.FirstNameMaxLength); + } + + private static void FillForm(IRenderedComponent cut, Person person) + { + cut.Find($"input[name={nameof(Person.FirstName)}]").Change(person.FirstName); + cut.Find($"input[name={nameof(Person.LastName)}]").Change(person.LastName); + cut.Find($"input[name={nameof(Person.EmailAddress)}]").Change(person.EmailAddress); + cut.Find($"input[name={nameof(Person.Age)}]").Change(person.Age.ToString()); + } + + private class Fixture + { + public Person ValidPerson() => new() + { + FirstName = "John", + LastName = "Doe", + EmailAddress = "john.doe@blazored.com", + Age = 30 + }; + } +} \ No newline at end of file diff --git a/tests/Blazored.FluentValidation.Tests/_Imports.razor b/tests/Blazored.FluentValidation.Tests/_Imports.razor new file mode 100644 index 0000000..b5e1119 --- /dev/null +++ b/tests/Blazored.FluentValidation.Tests/_Imports.razor @@ -0,0 +1,15 @@ +@using Blazored.FluentValidation +@using Blazored.FluentValidation.Tests.Model + +@using Bunit +@using Bunit.TestDoubles + +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.Extensions.DependencyInjection +@using Microsoft.JSInterop +@using System.Net.Http +@using System.Net.Http.Json + +@using Xunit \ No newline at end of file