// Copyright (c) The Geekeey Authors // SPDX-License-Identifier: EUPL-1.2 using System.Text.RegularExpressions; namespace Geekeey.Request.Validation.Tests; internal sealed class RuleBuilderExtensionsTests { [Test] public async Task I_can_validate_not_null_for_reference_types() { var validator = new PropertyValidator(model => model.Value, rule => rule.NotNull()); var invalid = validator.Validate(new ReferenceValueModel()); var valid = validator.Validate(new ReferenceValueModel { Value = new object() }); await AssertSingleProblem(invalid, nameof(ReferenceValueModel.Value), "Value is required."); await Assert.That(valid.IsValid).IsTrue(); } [Test] public async Task I_can_validate_not_null_for_nullable_value_types() { var validator = new PropertyValidator(model => model.Value, rule => rule.NotNull()); var invalid = validator.Validate(new NullableIntValueModel()); var valid = validator.Validate(new NullableIntValueModel { Value = 1 }); await AssertSingleProblem(invalid, nameof(NullableIntValueModel.Value), "Value is required."); await Assert.That(valid.IsValid).IsTrue(); } [Test] public async Task I_can_validate_not_empty_for_strings() { var validator = new PropertyValidator(model => model.Value, rule => rule.NotEmpty()); var invalid = validator.Validate(new StringValueModel { Value = " " }); var valid = validator.Validate(new StringValueModel { Value = "abc" }); await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value is required."); await Assert.That(valid.IsValid).IsTrue(); } [Test] public async Task I_can_validate_not_empty_for_collections() { var validator = new PropertyValidator?>(model => model.Value, rule => rule.NotEmpty()); var invalid = validator.Validate(new CollectionValueModel { Value = [] }); var valid = validator.Validate(new CollectionValueModel { Value = ["item"] }); await AssertSingleProblem(invalid, nameof(CollectionValueModel.Value), "Value is required."); await Assert.That(valid.IsValid).IsTrue(); } [Test] public async Task I_can_validate_min_length() { var validator = new PropertyValidator(model => model.Value, rule => rule.MinLength(3)); var invalid = validator.Validate(new StringValueModel { Value = "ab" }); var valid = validator.Validate(new StringValueModel { Value = "abc" }); var ignoredNull = validator.Validate(new StringValueModel()); await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value must be at least 3 characters long."); await Assert.That(valid.IsValid).IsTrue(); await Assert.That(ignoredNull.IsValid).IsTrue(); } [Test] public async Task I_can_validate_max_length() { var validator = new PropertyValidator(model => model.Value, rule => rule.MaxLength(5)); var invalid = validator.Validate(new StringValueModel { Value = "abcdef" }); var valid = validator.Validate(new StringValueModel { Value = "abcde" }); var ignoredNull = validator.Validate(new StringValueModel()); await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value must be at most 5 characters long."); await Assert.That(valid.IsValid).IsTrue(); await Assert.That(ignoredNull.IsValid).IsTrue(); } [Test] public async Task I_can_validate_length() { var validator = new PropertyValidator(model => model.Value, rule => rule.Length(2, 4)); var invalid = validator.Validate(new StringValueModel { Value = "a" }); var valid = validator.Validate(new StringValueModel { Value = "ab" }); var ignoredNull = validator.Validate(new StringValueModel()); await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value must be between 2 and 4 characters long."); await Assert.That(valid.IsValid).IsTrue(); await Assert.That(ignoredNull.IsValid).IsTrue(); } [Test] public async Task I_can_validate_greater_than() { var validator = new PropertyValidator(model => model.Value, rule => rule.GreaterThan(18)); var invalid = validator.Validate(new IntValueModel { Value = 18 }); var valid = validator.Validate(new IntValueModel { Value = 19 }); await AssertSingleProblem(invalid, nameof(IntValueModel.Value), "Value must be greater than 18."); await Assert.That(valid.IsValid).IsTrue(); } [Test] public async Task I_can_validate_greater_than_or_equal_to() { var validator = new PropertyValidator(model => model.Value, rule => rule.GreaterThanOrEqualTo(1)); var invalid = validator.Validate(new IntValueModel { Value = 0 }); var valid = validator.Validate(new IntValueModel { Value = 1 }); await AssertSingleProblem(invalid, nameof(IntValueModel.Value), "Value must be greater than or equal to 1."); await Assert.That(valid.IsValid).IsTrue(); } [Test] public async Task I_can_validate_less_than() { var validator = new PropertyValidator(model => model.Value, rule => rule.LessThan(10)); var invalid = validator.Validate(new IntValueModel { Value = 10 }); var valid = validator.Validate(new IntValueModel { Value = 9 }); await AssertSingleProblem(invalid, nameof(IntValueModel.Value), "Value must be less than 10."); await Assert.That(valid.IsValid).IsTrue(); } [Test] public async Task I_can_validate_less_than_or_equal_to() { var validator = new PropertyValidator(model => model.Value, rule => rule.LessThanOrEqualTo(5)); var invalid = validator.Validate(new IntValueModel { Value = 6 }); var valid = validator.Validate(new IntValueModel { Value = 5 }); await AssertSingleProblem(invalid, nameof(IntValueModel.Value), "Value must be less than or equal to 5."); await Assert.That(valid.IsValid).IsTrue(); } [Test] public async Task I_can_validate_between() { var validator = new PropertyValidator(model => model.Value, rule => rule.Between(0, 100)); var invalid = validator.Validate(new IntValueModel { Value = 101 }); var valid = validator.Validate(new IntValueModel { Value = 100 }); await AssertSingleProblem(invalid, nameof(IntValueModel.Value), "Value must be between 0 and 100."); await Assert.That(valid.IsValid).IsTrue(); } [Test] public async Task I_can_validate_equal() { var validator = new PropertyValidator(model => model.Value, rule => rule.Equal("ACTIVE")); var invalid = validator.Validate(new StringValueModel { Value = "INACTIVE" }); var valid = validator.Validate(new StringValueModel { Value = "ACTIVE" }); await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value must be equal to ACTIVE."); await Assert.That(valid.IsValid).IsTrue(); } [Test] public async Task I_can_validate_not_equal() { var validator = new PropertyValidator(model => model.Value, rule => rule.NotEqual("BANNED")); var invalid = validator.Validate(new StringValueModel { Value = "BANNED" }); var valid = validator.Validate(new StringValueModel { Value = "ALLOWED" }); await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value must not be equal to BANNED."); await Assert.That(valid.IsValid).IsTrue(); } [Test] public async Task I_can_validate_equal_with_null_reference_values() { var validator = new PropertyValidator(model => model.Value, rule => rule.Equal("ACTIVE")); var invalid = validator.Validate(new StringValueModel { Value = "INACTIVE" }); var valid = validator.Validate(new StringValueModel { Value = "ACTIVE" }); var nullValue = validator.Validate(new StringValueModel()); await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value must be equal to ACTIVE."); await Assert.That(valid.IsValid).IsTrue(); await Assert.That(nullValue.IsValid).IsFalse(); } [Test] public async Task I_can_validate_not_equal_with_null_reference_values() { var validator = new PropertyValidator(model => model.Value, rule => rule.NotEqual("BANNED")); var invalid = validator.Validate(new StringValueModel { Value = "BANNED" }); var valid = validator.Validate(new StringValueModel { Value = "ALLOWED" }); var nullValue = validator.Validate(new StringValueModel()); await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value must not be equal to BANNED."); await Assert.That(valid.IsValid).IsTrue(); await Assert.That(nullValue.IsValid).IsTrue(); } [Test] public async Task I_can_skip_null_values_for_reference_greater_than() { var validator = new PropertyValidator(model => model.Value, rule => rule.GreaterThan("B")); var invalid = validator.Validate(new StringValueModel { Value = "A" }); var valid = validator.Validate(new StringValueModel { Value = "C" }); var ignoredNull = validator.Validate(new StringValueModel()); await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value must be greater than B."); await Assert.That(valid.IsValid).IsTrue(); await Assert.That(ignoredNull.IsValid).IsTrue(); } [Test] public async Task I_can_skip_null_values_for_reference_between() { var validator = new PropertyValidator(model => model.Value, rule => rule.Between("A", "C")); var invalid = validator.Validate(new StringValueModel { Value = "D" }); var valid = validator.Validate(new StringValueModel { Value = "C" }); var ignoredNull = validator.Validate(new StringValueModel()); await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value must be between A and C."); await Assert.That(valid.IsValid).IsTrue(); await Assert.That(ignoredNull.IsValid).IsTrue(); } [Test] public async Task I_can_validate_matches_with_a_pattern() { var validator = new PropertyValidator(model => model.Value, rule => rule.Matches("^[A-Z]+$")); var invalid = validator.Validate(new StringValueModel { Value = "Abc" }); var valid = validator.Validate(new StringValueModel { Value = "ABC" }); var ignoredNull = validator.Validate(new StringValueModel()); await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value is not in the correct format."); await Assert.That(valid.IsValid).IsTrue(); await Assert.That(ignoredNull.IsValid).IsTrue(); } [Test] public async Task I_can_validate_matches_with_a_regex() { var validator = new PropertyValidator(model => model.Value, rule => rule.Matches(new Regex(@"^\d{4}$", RegexOptions.CultureInvariant))); var invalid = validator.Validate(new StringValueModel { Value = "12AB" }); var valid = validator.Validate(new StringValueModel { Value = "1234" }); var ignoredNull = validator.Validate(new StringValueModel()); await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value is not in the correct format."); await Assert.That(valid.IsValid).IsTrue(); await Assert.That(ignoredNull.IsValid).IsTrue(); } [Test] public async Task I_see_it_throw_for_invalid_length_configuration() { await Assert.That(() => new PropertyValidator(model => model.Value, rule => rule.Length(5, 4))) .Throws(); } [Test] public async Task I_see_it_throw_for_negative_min_length_configuration() { await Assert.That(() => new PropertyValidator(model => model.Value, rule => rule.MinLength(-1))) .Throws(); } [Test] public async Task I_see_it_throw_for_invalid_between_configuration() { await Assert.That(() => new PropertyValidator(model => model.Value, rule => rule.Between(10, 0))) .Throws(); } private static async Task AssertSingleProblem(Validation validation, string propertyPath, string message) { await Assert.That(validation.Problems).Count().IsEqualTo(1); var problem = validation.Problems.Single(); using (Assert.Multiple()) { await Assert.That(problem.PropertyPath).IsEqualTo(propertyPath); await Assert.That(problem.Message).IsEqualTo(message); } } }