diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3ccc8f6..ec9a169 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,15 +18,16 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: aboutbits/github-actions-java/setup-and-install@v3 + - uses: aboutbits/github-actions-java/setup@v3 - name: Set Version run: sed -i 's|BUILD-SNAPSHOT|${{ github.event.inputs.version }}|g' pom.xml - name: Publish package - run: mvn --batch-mode deploy + run: mvn -s $GITHUB_WORKSPACE/.github/workflows/maven-settings.xml --batch-mode deploy env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_USER_NAME: ${{ github.actor }} + GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} tag: timeout-minutes: 5 diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..63e9001 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 8df6b57..dd9bd70 100644 --- a/pom.xml +++ b/pom.xml @@ -3,6 +3,13 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + it.aboutbits spring-boot-testing BUILD-SNAPSHOT @@ -13,7 +20,29 @@ + + it.aboutbits + spring-boot-toolbox + 1.0.0-RC1 + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + diff --git a/readme.md b/readme.md index c570d6b..cd9e16d 100644 --- a/readme.md +++ b/readme.md @@ -7,6 +7,7 @@ Testing library for Spring Boot projects. Add this library to the classpath by adding the following maven dependency. Versions can be found [here](../../packages) ```xml + it.aboutbits spring-boot-testing @@ -14,6 +15,248 @@ Add this library to the classpath by adding the following maven dependency. Vers ``` +## Usage + +### Validation + +The validation tester allows us to quickly test simple validation constraints. Most commonly we use bean validation for this. + +#### Configuration + +To use the validation tester in your project you need to extend both the [BaseValidationAssert.java](src/main/java/it/aboutbits/springboot/testing/validation/core/BaseValidationAssert.java) and the [BaseRuleBuilder.java](src/main/java/it/aboutbits/springboot/testing/validation/core/BaseRuleBuilder.java). + +```java +public class ValidationAssert extends BaseValidationAssert> { + protected ValidationAssert() { + super(new RuleBuilder()); + } + + public static ValidationAssert assertThatValidation() { + return new ValidationAssert(); + } + + public static final class RuleBuilder extends BaseRuleBuilder { + + } +} +``` + +By default, the validation tester will assume that all properties of type `Record` are substructures. Therefore, using the `@Valid` annotation is required to make sure that validation for those records is triggered. +You can add a class to a whitelist to disable this behavior: + +```java + +public class ValidationConfig { + public static void configure() { + ValidationAssert.registerNonBeanType(NotValidated.class); + } +} + +public class ValidationAssert extends BaseValidationAssert> { + static { + ValidationConfig.configure(); + } + + // ... +} +``` + +#### Usage + +Each property is required to have at least one rule defined. You can add multiple rules for the same property as needed to combine more complex rulesets. +The tester will fail if not all properties have rules. In case you have properties without any restrictions, use the `notValidated` rule. + +The validation tester works by taking in a **valid** parameter. It will then mutate the parameter internally and test each property with an invalid value. Then a check is done if a validation violation is raised as expected. + +In any case, the call to `isCompliant` is required at the end and then triggers the actual assertion. + +You can use plain bean validation to verify a Record: + +```java +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Past; +import org.springframework.lang.Nullable; + +public record SomeParameter( + @NotBlank + String name, + @Min(18) + int age, + @NotNull + @Past + LocalDate birthDay, + @Nullable + String something, + String notValidatedAtAll +) { +} + + +@Test +void testValidation() { + var validParameter = new SomeParameter("Sepp", 32); + + assertThatValidation().of(validParameter) + .usingBeanValidation() + .notBlank("name") + .min("age", 18) + .notNull("birthDay") + .past("birthDay") + .nullable("something") + .notValidated("notValidatedAtAll") + .isCompliant(); +} +``` + +Alternatively you can use a method call to a service function to verify the validation. This is the preferred way as it makes sure that the bean validation is both triggered and also valid. + +```java +import org.springframework.beans.factory.annotation.Autowired; + +public record SomeParameter( + @NotBlank + String name, + @Min(18) + int age +) { +} + +@Autowired +private MyService myService; + + +@Test +void testValidation() { + var validParameter = new SomeParameter("Sepp", 32); + + assertThatValidation().of(validParameter) + .calling(myService::create) + .notBlank("name") + .min("age", 18) + .isCompliant(); +} + +@Test +void testValidationWithIdParameter() { + var validParameter = new SomeParameter("Sepp", 32); + + assertThatValidation().of(validParameter) + .calling(myService::update, new User.ID(3L)) + .notBlank("name") + .min("age", 18) + .isCompliant(); +} +``` + +#### Adding custom validation rules + +You can add new rules by creating a new interface: + +```java +public interface MyShinyNewRule> extends ValidationRulesData { + default V shiny(@NonNull String property) { + this.addRule(new Rule(property, InertValueSource.class, new Object[0])); + return (BaseRuleBuilder) this; + } +} +``` + +To use the newly created rule, we can simply have our `RuleBuilder` implement the interface: + +```java +public class ValidationAssert extends BaseValidationAssert> { + // ... + + public static final class RuleBuilder extends BaseRuleBuilder implements MyShinyNewRule { + + } +} +``` + +The `Rule` requires the property name, a value-source and an array of optional parameters. For example `min(property, minValue)` takes in the additional parameter for the value. +Note that the value-source must return **invalid** values. This is required because the tool is actively trying to violate the rules to check if an error is raised. + +#### Adding custom value sources + +You can add custom values sources by implementing the `ValueSource` interface. +While the interface can not enforce the static function `registerType`, it is best practice to implement it in a way that keeps this extensible. +This way we can use the same logical value-source for multiple property types. + +Here is an example: + +```java +public class EmptyValueSource implements ValueSource { + private static final Map, Function>> TYPE_SOURCES = new HashMap<>(); + + static { + TYPE_SOURCES.put( + String.class, + (Object[] args) -> Stream.of("") + ); + TYPE_SOURCES.put( + Set.class, + (Object[] args) -> Stream.of(new HashSet<>()) + ); + TYPE_SOURCES.put( + List.class, + (Object[] args) -> Stream.of(new ArrayList<>()) + ); + } + + public static void registerType(Class type, Function> source) { + TYPE_SOURCES.put(type, source); + } + + @Override + @SuppressWarnings("unchecked") + public Stream values(Class propertyClass, Object... args) { + var sourceFunction = TYPE_SOURCES.get(propertyClass); + if (sourceFunction != null) { + return (Stream) sourceFunction.apply(args); + } + + throw new IllegalArgumentException("Property class not supported!"); + } +} +``` + +#### Adding support for custom types + +_Note: CustomType wrappers from the `toolbox` are currently not natively supported._ + +Adding custom types will require some extension to the existing value-sources. Those need to become aware of the new type in order to produce values of said type. + +This can be done by extending the configuration: + +```java +public record SurnameType( + String value +) { +} + +public class ValidationConfig { + public static void configure() { + EmptyValueSource.registerType( + SurnameType.class, + (args) -> { + return Stream.of(new SurnameType("")); + } + ); + + // ... + } +} + +public class ValidationAssert extends BaseValidationAssert> { + static { + ValidationConfig.configure(); + } + + // ... +} +``` + ## Local development: To use this library as a local development dependency, you can simply refer to the version `BUILD-SNAPSHOT`. diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/core/BaseRuleBuilder.java b/src/main/java/it/aboutbits/springboot/testing/validation/core/BaseRuleBuilder.java new file mode 100644 index 0000000..fda5fed --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/core/BaseRuleBuilder.java @@ -0,0 +1,59 @@ +package it.aboutbits.springboot.testing.validation.core; + +import it.aboutbits.springboot.testing.validation.rule.BetweenRule; +import it.aboutbits.springboot.testing.validation.rule.FutureRule; +import it.aboutbits.springboot.testing.validation.rule.MaxRule; +import it.aboutbits.springboot.testing.validation.rule.MinRule; +import it.aboutbits.springboot.testing.validation.rule.NegativeOrZeroRule; +import it.aboutbits.springboot.testing.validation.rule.NegativeRule; +import it.aboutbits.springboot.testing.validation.rule.NotBlankRule; +import it.aboutbits.springboot.testing.validation.rule.NotEmptyRule; +import it.aboutbits.springboot.testing.validation.rule.NotNullRule; +import it.aboutbits.springboot.testing.validation.rule.NotValidatedRule; +import it.aboutbits.springboot.testing.validation.rule.NullableRule; +import it.aboutbits.springboot.testing.validation.rule.PastRule; +import it.aboutbits.springboot.testing.validation.rule.PositiveOrZeroRule; +import it.aboutbits.springboot.testing.validation.rule.PositiveRule; +import it.aboutbits.springboot.testing.validation.rule.ValidBeanRule; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +public abstract class BaseRuleBuilder> implements + ValidationRulesData, + BetweenRule, + FutureRule, + MaxRule, + MinRule, + NegativeOrZeroRule, + NegativeRule, + NotBlankRule, + NotEmptyRule, + NotNullRule, + NullableRule, + PastRule, + PositiveOrZeroRule, + PositiveRule, + NotValidatedRule, + ValidBeanRule { + @Getter(AccessLevel.PACKAGE) + private final List rules = new ArrayList<>(); + + @Setter(AccessLevel.PACKAGE) + private Runnable triggerValidation; + + @Override + public void addRule(@NonNull Rule rule) { + rules.add(rule); + } + + public void isCompliant() { + triggerValidation.run(); + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/core/BaseValidationAssert.java b/src/main/java/it/aboutbits/springboot/testing/validation/core/BaseValidationAssert.java new file mode 100644 index 0000000..6edee7b --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/core/BaseValidationAssert.java @@ -0,0 +1,82 @@ +package it.aboutbits.springboot.testing.validation.core; + +import it.aboutbits.springboot.toolbox.type.CustomType; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +public abstract class BaseValidationAssert> { + @Getter(AccessLevel.PROTECTED) + private final R ruleBuilder; + + // This keeps track of classes that are not required to have a @Valid annotation. + protected static final Set> NON_BEAN_TYPES = new HashSet<>( + Set.of( + CustomType.class + ) + ); + + private Object parameterUnderTest; + + @Setter(AccessLevel.PRIVATE) + private Consumer functionToCallWithParameter = null; + + /** + * Configure a class that is not required to have a @Valid annotation. Sub-structures are assumed to always require @Valid. + * + * @param type The class to whitelist. + */ + public static void registerNonBeanType(Class type) { + NON_BEAN_TYPES.add(type); + } + + public

CallBuilder of(@NonNull P parameterUnderTest) { + this.parameterUnderTest = parameterUnderTest; + ruleBuilder.setTriggerValidation(this::assertValidation); + return new CallBuilder<>(this); + } + + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + public static final class CallBuilder, P> { + private final BaseValidationAssert parent; + + public R calling(@NonNull Consumer

functionToCallWithParameter) { + parent.setFunctionToCallWithParameter(functionToCallWithParameter); + return parent.ruleBuilder; + } + + public R usingBeanValidation() { + return parent.ruleBuilder; + } + + @SuppressWarnings("unchecked") + public R calling( + @NonNull BiConsumer functionToCallWithParameter, + @NonNull ID id + ) { + parent.setFunctionToCallWithParameter( + p -> functionToCallWithParameter.accept(id, (P) p) + ); + return parent.ruleBuilder; + } + } + + private void assertValidation() { + new RuleValidator<>().assertValidation( + new RuleValidator.AssertionParameter<>( + parameterUnderTest, + functionToCallWithParameter, + ruleBuilder.getRules(), + NON_BEAN_TYPES + ) + ); + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/core/Rule.java b/src/main/java/it/aboutbits/springboot/testing/validation/core/Rule.java new file mode 100644 index 0000000..48cdbdf --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/core/Rule.java @@ -0,0 +1,52 @@ +package it.aboutbits.springboot.testing.validation.core; + +import it.aboutbits.springboot.testing.validation.source.InertValueSource; +import lombok.AccessLevel; +import lombok.Getter; + +public final class Rule { + @Getter(AccessLevel.PACKAGE) + private final String property; + + @Getter(AccessLevel.PACKAGE) + private final Class valueSource; + + @Getter(AccessLevel.PACKAGE) + private final Object[] args; + + @Getter(AccessLevel.PACKAGE) + private boolean requireValid = false; + + @Getter(AccessLevel.PACKAGE) + private boolean requireNullable = false; + + public Rule(String property, Class valueSource, Object... args) { + checkPropertyName(property); + + this.property = property; + this.valueSource = valueSource; + this.args = args; + } + + public static Rule validAnnotated(String property) { + checkPropertyName(property); + + var rule = new Rule(property, InertValueSource.class); + rule.requireValid = true; + return rule; + } + + public static Rule nullableAnnotated(String property) { + checkPropertyName(property); + + var rule = new Rule(property, InertValueSource.class); + rule.requireNullable = true; + return rule; + } + + private static void checkPropertyName(String property) { + if (property.contains(".")) { + throw new IllegalArgumentException("Referencing sub-objects using dot notation is not supported."); + } + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/core/RuleValidationException.java b/src/main/java/it/aboutbits/springboot/testing/validation/core/RuleValidationException.java new file mode 100644 index 0000000..3260b67 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/core/RuleValidationException.java @@ -0,0 +1,11 @@ +package it.aboutbits.springboot.testing.validation.core; + +class RuleValidationException extends RuntimeException { + RuleValidationException(String message) { + super(message); + } + + RuleValidationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/core/RuleValidator.java b/src/main/java/it/aboutbits/springboot/testing/validation/core/RuleValidator.java new file mode 100644 index 0000000..48bd79c --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/core/RuleValidator.java @@ -0,0 +1,317 @@ +package it.aboutbits.springboot.testing.validation.core; + +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Valid; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.NonNull; +import lombok.SneakyThrows; +import org.springframework.lang.Nullable; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * The main idea of the RuleValidator is to take in a valid parameter and then mutate it. + * Each mutation changes exactly one property`s value. + * Then we check if an exception is raised. + * We repeat this for each defined rule. + *

+ * Additionally, we also check if @Valid or @Nullable is present where required according to the rules. + * Also, we enforce that all properties must have at least one rule (with a rule existing that says "no-rule"). + *

+ * + * @parameterUnderTest A valid parameter that we can use as the basis for our mutations. Validation for the unmodified parameter MUST succeed. + * @functionToCallWithParameter Optional. Instead of directly using bean validation, we can also validate a real function call. This makes sure the parameter is actually annotated with @Valid as well and that the class is using @Validated. + * @rules The list of rules to validate. + * @nonBeanTypes This is a whitelist that holds classes that don't implicitly require @Valid. We assume that @Valid is required + * for all substructures. + */ +final class RuleValidator

{ + private static final ValidatorFactory VALIDATOR_FACTORY = Validation.buildDefaultValidatorFactory(); + + record AssertionParameter

( + @NonNull + P parameterUnderTest, + @Nullable + Consumer functionToCallWithParameter, + @NonNull + List rules, + @NonNull + Set> nonBeanTypes + ) { + } + + void assertValidation(AssertionParameter

assertionParameter) { + var parameterUnderTest = assertionParameter.parameterUnderTest(); + @SuppressWarnings("unchecked") + var functionToCallWithParameter = (Consumer

) assertionParameter.functionToCallWithParameter(); + var rules = assertionParameter.rules(); + var nonBeanTypes = assertionParameter.nonBeanTypes(); + + assertThat(rules) + .withFailMessage("Validation failed: no rules were defined.") + .isNotEmpty(); + + var validator = VALIDATOR_FACTORY.getValidator(); + + var propertiesWithRules = getPropertyNamesThatHaveRules(rules); + + assertThatValidationIsCompliantForEachProperty( + rules, + parameterUnderTest, + functionToCallWithParameter, + validator + ); + + assertThatValidAnnotationsArePresent(rules, propertiesWithRules, parameterUnderTest); + + assertThatNullableAnnotationsArePresent(rules, propertiesWithRules, parameterUnderTest); + + assertThatAllPropertiesHaveRules(parameterUnderTest, propertiesWithRules); + + checkIfNestedValidationIsEnabledForNestedRecords(parameterUnderTest, nonBeanTypes); + } + + private static HashSet getPropertyNamesThatHaveRules(List rules) { + // Create a set to keep track of properties that have validation rules + var propertiesWithRules = new HashSet(); + for (var rule : rules) { + propertiesWithRules.add(rule.getProperty()); + } + return propertiesWithRules; + } + + private static

void assertThatValidationIsCompliantForEachProperty( + List rules, + P parameterUnderTest, + Consumer

functionToCallWithParameter, + Validator validator + ) { + // Iterate through the rules and validate each property + for (var rule : rules) { + var values = getValues(rule, parameterUnderTest); + values.forEach(alteredValue -> { + // Create a copy of the original object + var copy = createCopyWithAlteredProperty(parameterUnderTest, rule.getProperty(), alteredValue); + + if (functionToCallWithParameter != null) { + assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy( + () -> functionToCallWithParameter.accept(copy) + ).withMessageContaining(rule.getProperty()); + } else { + + // Use Bean Validation to validate the copy + var violations = validator.validate(copy); + + // Check if there are any violations + assertThat(violations) + .withFailMessage( + "More than one property failed to validate during mutation. The supplied parameter is possibly contains invalid values.") + .hasSizeLessThan(2); + + assertThat(violations) + .withFailMessage("Validation failed for property: " + rule.getProperty() + " [" + alteredValue + "]") + .hasSize(1); + } + }); + } + } + + private static

void assertThatValidAnnotationsArePresent( + List rules, + HashSet propertiesWithRules, + P parameterUnderTest + ) { + var propertiesWithValid = rules.stream() + .filter(Rule::isRequireValid) + .map(Rule::getProperty) + .collect(Collectors.toSet()); + + for (var property : propertiesWithValid) { + propertiesWithRules.add(property); + + assertThat(hasValidAnnotation(property, parameterUnderTest)) + .withFailMessage("Missing @Valid annotation for property: " + property) + .isTrue(); + } + } + + private static

void assertThatNullableAnnotationsArePresent( + List rules, + HashSet propertiesWithRules, + P parameterUnderTest + ) { + var propertiesWithNullable = rules.stream() + .filter(Rule::isRequireNullable) + .map(Rule::getProperty) + .collect(Collectors.toSet()); + + for (var property : propertiesWithNullable) { + propertiesWithRules.add(property); + + assertThat(hasNullableAnnotation(property, parameterUnderTest)) + .withFailMessage("Missing @Nullable annotation for property: " + property + ". Note: This does not work with `org.jetbrains.annotations.Nullable` because of their retention policy. Use `org.springframework.lang.Nullable` or `jakarta.annotation.Nullable` instead.") + .isTrue(); + } + } + + private static

void assertThatAllPropertiesHaveRules( + P parameterUnderTest, + HashSet propertiesWithRules + ) { + // Check if all properties have rules (you can also handle this differently based on your needs) + var allProperties = getAllPropertiesOf(parameterUnderTest); + + var missingProperties = new HashSet<>(allProperties); + missingProperties.removeAll(propertiesWithRules); + + assertThat(missingProperties) + .withFailMessage("Not all properties have validation rules: " + "[" + String.join( + ", ", + missingProperties + ) + "]") + .isEmpty(); + } + + private static

void checkIfNestedValidationIsEnabledForNestedRecords( + P parameterUnderTest, + Set> nonBeanTypes + ) { + var clazz = parameterUnderTest.getClass(); + + for (var field : clazz.getDeclaredFields()) { + var isRecord = Record.class.isAssignableFrom(field.getType()); + if (isRecord) { + var isBeanType = nonBeanTypes.stream().noneMatch(type -> type.isAssignableFrom(field.getType())); + + if (isBeanType) { + var maybeAnnotation = field.getAnnotation(Valid.class); + assertThat(maybeAnnotation).withFailMessage("Missing @Valid annotation for property: " + field.getName() + ". Note: This is implicitly assumed for nested records. You can configure `BaseValidationAssert.registerNonBeanType` to add exceptions.") + .isNotNull(); + } + } + } + } + + @NonNull + @SneakyThrows(ReflectiveOperationException.class) + private static

Stream getValues(Rule rule, P parameterUnderTest) { + var source = (ValueSource) rule.getValueSource().getDeclaredConstructors()[0].newInstance(); + + return source.values(getPropertyType(rule.getProperty(), parameterUnderTest), rule.getArgs()); + } + + @SuppressWarnings("unchecked") + private static T createCopyWithAlteredProperty(T original, String property, Object alteredValue) { + try { + // Get the class of the original object + var clazz = original.getClass(); + + // Get all the declared fields of the class + var fields = clazz.getDeclaredFields(); + + // Create an array to hold the values of the original object's properties + var propertyValues = new Object[fields.length]; + + // Get the types of the fields + var parameterTypes = new Class[fields.length]; + + // Populate the array with the current property values + for (var i = 0; i < fields.length; i++) { + fields[i].setAccessible(true); + propertyValues[i] = fields[i].get(original); + parameterTypes[i] = fields[i].getType(); + } + + // Get the constructor that accepts all properties as arguments + var constructor = clazz.getDeclaredConstructor(parameterTypes); + constructor.setAccessible(true); + + // Create an array to hold the new property values + var newPropertyValues = new Object[propertyValues.length]; + + // Find the index of the property to be altered + var propertyIndex = -1; + for (var i = 0; i < fields.length; i++) { + if (fields[i].getName().equals(property)) { + propertyIndex = i; + break; + } + } + + // Replace the value of the altered property + newPropertyValues[propertyIndex] = alteredValue; + + // Copy the other property values + for (var i = 0; i < propertyValues.length; i++) { + if (i != propertyIndex) { + newPropertyValues[i] = propertyValues[i]; + } + } + + // Create a copy with the altered property value + return (T) constructor.newInstance(newPropertyValues); + } catch (NoSuchMethodException e) { + throw new RuleValidationException( + "Error creating copy with altered property. Maybe there is no all-args-constructor?", + e + ); + } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { + throw new RuleValidationException("Error creating copy with altered property: " + property, e); + } + } + + private static Set getAllPropertiesOf(T object) { + var clazz = object.getClass(); + + var properties = new HashSet(); + for (var field : clazz.getDeclaredFields()) { + properties.add(field.getName()); + } + + return properties; + } + + private static Class getPropertyType(String propertyName, Object object) { + var field = getFieldOrFail(propertyName, object); + + return field.getType(); + } + + private static boolean hasValidAnnotation(String propertyName, T object) { + var field = getFieldOrFail(propertyName, object); + + return field.getAnnotation(Valid.class) != null; + } + + private static boolean hasNullableAnnotation(String propertyName, Object object) { + var field = getFieldOrFail(propertyName, object); + + return field.getAnnotation(org.springframework.lang.Nullable.class) != null + || field.getAnnotation(jakarta.annotation.Nullable.class) != null; + } + + @NonNull + private static Field getFieldOrFail(String propertyName, Object object) { + var clazz = object.getClass(); + + Field field = null; + try { + field = clazz.getDeclaredField(propertyName); + } catch (NoSuchFieldException e) { + throw new RuleValidationException("Property does not exist: " + propertyName, e); + } + return field; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/core/ValidationRulesData.java b/src/main/java/it/aboutbits/springboot/testing/validation/core/ValidationRulesData.java new file mode 100644 index 0000000..2af0435 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/core/ValidationRulesData.java @@ -0,0 +1,7 @@ +package it.aboutbits.springboot.testing.validation.core; + +import lombok.NonNull; + +public interface ValidationRulesData { + void addRule(@NonNull Rule rule); +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/core/ValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/core/ValueSource.java new file mode 100644 index 0000000..290191f --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/core/ValueSource.java @@ -0,0 +1,7 @@ +package it.aboutbits.springboot.testing.validation.core; + +import java.util.stream.Stream; + +public interface ValueSource { + Stream values(Class propertyClass, Object... args); +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/BetweenRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/BetweenRule.java new file mode 100644 index 0000000..285cfe3 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/BetweenRule.java @@ -0,0 +1,21 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.Rule; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import it.aboutbits.springboot.testing.validation.source.BiggerThanValueSource; +import it.aboutbits.springboot.testing.validation.source.LessThanValueSource; +import lombok.NonNull; + +@SuppressWarnings("unchecked") +public interface BetweenRule> extends ValidationRulesData { + default V between(@NonNull String property, long min, long max) { + addRule( + new Rule(property, BiggerThanValueSource.class, max) + ); + addRule( + new Rule(property, LessThanValueSource.class, min) + ); + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/FutureRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/FutureRule.java new file mode 100644 index 0000000..7da2a2d --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/FutureRule.java @@ -0,0 +1,17 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.Rule; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import it.aboutbits.springboot.testing.validation.source.PastValueSource; +import lombok.NonNull; + +@SuppressWarnings("unchecked") +public interface FutureRule> extends ValidationRulesData { + default V future(@NonNull String property) { + addRule( + new Rule(property, PastValueSource.class) + ); + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/MaxRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/MaxRule.java new file mode 100644 index 0000000..4858067 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/MaxRule.java @@ -0,0 +1,17 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.Rule; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import it.aboutbits.springboot.testing.validation.source.BiggerThanValueSource; +import lombok.NonNull; + +@SuppressWarnings("unchecked") +public interface MaxRule> extends ValidationRulesData { + default V max(@NonNull String property, long max) { + addRule( + new Rule(property, BiggerThanValueSource.class, max) + ); + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/MinRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/MinRule.java new file mode 100644 index 0000000..5484f08 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/MinRule.java @@ -0,0 +1,17 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.Rule; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import it.aboutbits.springboot.testing.validation.source.LessThanValueSource; +import lombok.NonNull; + +@SuppressWarnings("unchecked") +public interface MinRule> extends ValidationRulesData { + default V min(@NonNull String property, long min) { + addRule( + new Rule(property, LessThanValueSource.class, min) + ); + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/NegativeOrZeroRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NegativeOrZeroRule.java new file mode 100644 index 0000000..4852867 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NegativeOrZeroRule.java @@ -0,0 +1,17 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.Rule; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import it.aboutbits.springboot.testing.validation.source.BiggerThanValueSource; +import lombok.NonNull; + +@SuppressWarnings("unchecked") +public interface NegativeOrZeroRule> extends ValidationRulesData { + default V negativeOrZero(@NonNull String property) { + addRule( + new Rule(property, BiggerThanValueSource.class, 0L) + ); + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/NegativeRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NegativeRule.java new file mode 100644 index 0000000..e420f2e --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NegativeRule.java @@ -0,0 +1,21 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.Rule; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import it.aboutbits.springboot.testing.validation.source.BiggerThanValueSource; +import it.aboutbits.springboot.testing.validation.source.ZeroValueSource; +import lombok.NonNull; + +@SuppressWarnings("unchecked") +public interface NegativeRule> extends ValidationRulesData { + default V negative(@NonNull String property) { + addRule( + new Rule(property, BiggerThanValueSource.class, 0L) + ); + addRule( + new Rule(property, ZeroValueSource.class) + ); + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotBlankRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotBlankRule.java new file mode 100644 index 0000000..052cbb3 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotBlankRule.java @@ -0,0 +1,17 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.Rule; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import it.aboutbits.springboot.testing.validation.source.BlankValueSource; +import lombok.NonNull; + +@SuppressWarnings("unchecked") +public interface NotBlankRule> extends ValidationRulesData { + default V notBlank(@NonNull String property) { + addRule( + new Rule(property, BlankValueSource.class) + ); + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotEmptyRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotEmptyRule.java new file mode 100644 index 0000000..53ba30d --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotEmptyRule.java @@ -0,0 +1,17 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.Rule; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import it.aboutbits.springboot.testing.validation.source.EmptyValueSource; +import lombok.NonNull; + +@SuppressWarnings("unchecked") +public interface NotEmptyRule> extends ValidationRulesData { + default V notEmpty(@NonNull String property) { + addRule( + new Rule(property, EmptyValueSource.class) + ); + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotNullRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotNullRule.java new file mode 100644 index 0000000..5346ae9 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotNullRule.java @@ -0,0 +1,17 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.Rule; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import it.aboutbits.springboot.testing.validation.source.NullValueSource; +import lombok.NonNull; + +@SuppressWarnings("unchecked") +public interface NotNullRule> extends ValidationRulesData { + default V notNull(@NonNull String property) { + addRule( + new Rule(property, NullValueSource.class) + ); + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotValidatedRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotValidatedRule.java new file mode 100644 index 0000000..3e00e20 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NotValidatedRule.java @@ -0,0 +1,17 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.Rule; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import it.aboutbits.springboot.testing.validation.source.InertValueSource; +import lombok.NonNull; + +@SuppressWarnings("unchecked") +public interface NotValidatedRule> extends ValidationRulesData { + default V notValidated(@NonNull String property) { + addRule( + new Rule(property, InertValueSource.class) + ); + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/NullableRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NullableRule.java new file mode 100644 index 0000000..69a33c4 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/NullableRule.java @@ -0,0 +1,16 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.Rule; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import lombok.NonNull; + +@SuppressWarnings("unchecked") +public interface NullableRule> extends ValidationRulesData { + default V nullable(@NonNull String property) { + addRule( + Rule.nullableAnnotated(property) + ); + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/PastRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/PastRule.java new file mode 100644 index 0000000..b61dd7d --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/PastRule.java @@ -0,0 +1,17 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.Rule; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import it.aboutbits.springboot.testing.validation.source.FutureValueSource; +import lombok.NonNull; + +@SuppressWarnings("unchecked") +public interface PastRule> extends ValidationRulesData { + default V past(@NonNull String property) { + addRule( + new Rule(property, FutureValueSource.class) + ); + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/PositiveOrZeroRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/PositiveOrZeroRule.java new file mode 100644 index 0000000..ba9a3fe --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/PositiveOrZeroRule.java @@ -0,0 +1,17 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.Rule; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import it.aboutbits.springboot.testing.validation.source.LessThanValueSource; +import lombok.NonNull; + +@SuppressWarnings("unchecked") +public interface PositiveOrZeroRule> extends ValidationRulesData { + default V positiveOrZero(@NonNull String property) { + addRule( + new Rule(property, LessThanValueSource.class, 0L) + ); + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/PositiveRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/PositiveRule.java new file mode 100644 index 0000000..bf9b25b --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/PositiveRule.java @@ -0,0 +1,21 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.Rule; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import it.aboutbits.springboot.testing.validation.source.LessThanValueSource; +import it.aboutbits.springboot.testing.validation.source.ZeroValueSource; +import lombok.NonNull; + +@SuppressWarnings("unchecked") +public interface PositiveRule> extends ValidationRulesData { + default V positive(@NonNull String property) { + addRule( + new Rule(property, LessThanValueSource.class, 0L) + ); + addRule( + new Rule(property, ZeroValueSource.class) + ); + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/rule/ValidBeanRule.java b/src/main/java/it/aboutbits/springboot/testing/validation/rule/ValidBeanRule.java new file mode 100644 index 0000000..7c3e7ae --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/rule/ValidBeanRule.java @@ -0,0 +1,16 @@ +package it.aboutbits.springboot.testing.validation.rule; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.Rule; +import it.aboutbits.springboot.testing.validation.core.ValidationRulesData; +import lombok.NonNull; + +@SuppressWarnings("unchecked") +public interface ValidBeanRule> extends ValidationRulesData { + default V validBean(@NonNull String property) { + addRule( + Rule.validAnnotated(property) + ); + return (V) this; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/BiggerThanValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/BiggerThanValueSource.java new file mode 100644 index 0000000..4343980 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/BiggerThanValueSource.java @@ -0,0 +1,117 @@ +package it.aboutbits.springboot.testing.validation.source; + +import it.aboutbits.springboot.testing.validation.core.ValueSource; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; +import lombok.NonNull; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.function.Function; +import java.util.stream.Stream; + +public class BiggerThanValueSource implements ValueSource { + private static final Map, Function>> TYPE_SOURCES = new HashMap<>(); + private static final Random RANDOM = new Random(); + + static { + TYPE_SOURCES.put(Integer.class, BiggerThanValueSource::getIntegerStream); + TYPE_SOURCES.put(int.class, BiggerThanValueSource::getIntegerStream); + + TYPE_SOURCES.put(Long.class, BiggerThanValueSource::getLongStream); + TYPE_SOURCES.put(long.class, BiggerThanValueSource::getLongStream); + + TYPE_SOURCES.put(Float.class, BiggerThanValueSource::getFloatStream); + TYPE_SOURCES.put(float.class, BiggerThanValueSource::getFloatStream); + + TYPE_SOURCES.put(Double.class, BiggerThanValueSource::getDoubleStream); + TYPE_SOURCES.put(double.class, BiggerThanValueSource::getDoubleStream); + + TYPE_SOURCES.put(BigDecimal.class, BiggerThanValueSource::getBigDecimalStream); + TYPE_SOURCES.put(ScaledBigDecimal.class, BiggerThanValueSource::getScaledBigDecimalStream); + } + + public static void registerType(Class type, Function> source) { + TYPE_SOURCES.put(type, source); + } + + @Override + @SuppressWarnings("unchecked") + public Stream values(Class propertyClass, Object... args) { + var sourceFunction = TYPE_SOURCES.get(propertyClass); + if (sourceFunction != null) { + return (Stream) sourceFunction.apply(args); + } + + throw new IllegalArgumentException("Property class not supported!"); + } + + @NonNull + private static Stream getIntegerStream(Object[] args) { + var minValue = Long.valueOf((long) args[0]).intValue() + 1; + var maxValue = Integer.MAX_VALUE; + + return Stream.concat( + Stream.of(minValue, maxValue), + RANDOM.ints(minValue, maxValue).limit(1).boxed() + ); + } + + @NonNull + private static Stream getLongStream(Object[] args) { + var minValue = (long) args[0] + 1; + var maxValue = Long.MAX_VALUE; + + return Stream.concat( + Stream.of(minValue, maxValue), + RANDOM.longs(minValue, maxValue).limit(1).boxed() + ); + } + + @NonNull + private static Stream getFloatStream(Object[] args) { + var minValue = Long.valueOf((long) args[0]).floatValue() + 0.1f; + var maxValue = Float.MAX_VALUE; + + return Stream.concat( + Stream.of(minValue, maxValue), + RANDOM.doubles(minValue, maxValue).limit(1).boxed().map( + Double::floatValue + ) + ); + } + + @NonNull + private static Stream getDoubleStream(Object[] args) { + var minValue = Long.valueOf((long) args[0]).doubleValue() + 0.1d; + var maxValue = Double.MAX_VALUE; + + return Stream.concat( + Stream.of(minValue, maxValue), + RANDOM.doubles(minValue, maxValue).limit(1).boxed() + ); + } + + @NonNull + private static Stream getScaledBigDecimalStream(Object[] args) { + var minValue = Long.valueOf((long) args[0]).doubleValue() + 0.1d; + var maxValue = Double.MAX_VALUE; + + return Stream.concat( + Stream.of(ScaledBigDecimal.valueOf(minValue), ScaledBigDecimal.valueOf(maxValue)), + RANDOM.doubles(minValue, maxValue).limit(1).boxed().map(ScaledBigDecimal::valueOf) + ); + } + + @NonNull + private static Stream getBigDecimalStream(Object[] args) { + var minValue = Long.valueOf((long) args[0]).doubleValue() + 0.1d; + var maxValue = Double.MAX_VALUE; + + return Stream.concat( + Stream.of(BigDecimal.valueOf(minValue), BigDecimal.valueOf(maxValue)), + RANDOM.doubles(minValue, maxValue).limit(1).boxed().map(BigDecimal::valueOf) + ); + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/BlankValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/BlankValueSource.java new file mode 100644 index 0000000..035d6d4 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/BlankValueSource.java @@ -0,0 +1,34 @@ +package it.aboutbits.springboot.testing.validation.source; + +import it.aboutbits.springboot.testing.validation.core.ValueSource; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; + +public class BlankValueSource implements ValueSource { + private static final Map, Function>> TYPE_SOURCES = new HashMap<>(); + + static { + TYPE_SOURCES.put( + String.class, + (Object[] args) -> Stream.of("", " ", " ", "\t", "\r", "\n", "\r\n") + ); + } + + public static void registerType(Class type, Function> source) { + TYPE_SOURCES.put(type, source); + } + + @Override + @SuppressWarnings("unchecked") + public Stream values(Class propertyClass, Object... args) { + var sourceFunction = TYPE_SOURCES.get(propertyClass); + if (sourceFunction != null) { + return (Stream) sourceFunction.apply(args); + } + + throw new IllegalArgumentException("Property class not supported!"); + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/EmptyValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/EmptyValueSource.java new file mode 100644 index 0000000..318c13d --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/EmptyValueSource.java @@ -0,0 +1,46 @@ +package it.aboutbits.springboot.testing.validation.source; + +import it.aboutbits.springboot.testing.validation.core.ValueSource; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; + +public class EmptyValueSource implements ValueSource { + private static final Map, Function>> TYPE_SOURCES = new HashMap<>(); + + static { + TYPE_SOURCES.put( + String.class, + (Object[] args) -> Stream.of("") + ); + TYPE_SOURCES.put( + Set.class, + (Object[] args) -> Stream.of(new HashSet<>()) + ); + TYPE_SOURCES.put( + List.class, + (Object[] args) -> Stream.of(new ArrayList<>()) + ); + } + + public static void registerType(Class type, Function> source) { + TYPE_SOURCES.put(type, source); + } + + @Override + @SuppressWarnings("unchecked") + public Stream values(Class propertyClass, Object... args) { + var sourceFunction = TYPE_SOURCES.get(propertyClass); + if (sourceFunction != null) { + return (Stream) sourceFunction.apply(args); + } + + throw new IllegalArgumentException("Property class not supported!"); + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/FutureValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/FutureValueSource.java new file mode 100644 index 0000000..2441a06 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/FutureValueSource.java @@ -0,0 +1,58 @@ +package it.aboutbits.springboot.testing.validation.source; + +import it.aboutbits.springboot.testing.validation.core.ValueSource; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; + +public class FutureValueSource implements ValueSource { + private static final Map, Function>> TYPE_SOURCES = new HashMap<>(); + + static { + TYPE_SOURCES.put(LocalDate.class, FutureValueSource::getLocalDateStream); + TYPE_SOURCES.put(LocalDateTime.class, FutureValueSource::getLocalDatetimeStream); + TYPE_SOURCES.put(OffsetDateTime.class, FutureValueSource::getOffsetDateTimeStream); + } + + public static void registerType(Class type, Function> source) { + TYPE_SOURCES.put(type, source); + } + + @Override + @SuppressWarnings("unchecked") + public Stream values(Class propertyClass, Object... args) { + var sourceFunction = TYPE_SOURCES.get(propertyClass); + if (sourceFunction != null) { + return (Stream) sourceFunction.apply(args); + } + + throw new IllegalArgumentException("Property class not supported!"); + } + + private static Stream getLocalDateStream(Object[] args) { + var currentDate = LocalDate.now(); + var largestDate = LocalDate.MAX; + + return Stream.of(currentDate.plusDays(1), largestDate); + } + + private static Stream getLocalDatetimeStream(Object[] args) { + var currentDateTime = LocalDateTime.now(); + var largestDateTime = LocalDateTime.MAX; + + return Stream.of(currentDateTime.plusSeconds(1), largestDateTime); + } + + private static Stream getOffsetDateTimeStream(Object[] args) { + var currentOffsetDateTime = OffsetDateTime.now(ZoneOffset.UTC); + var largestOffsetDateTime = OffsetDateTime.MAX; + + return Stream.of(currentOffsetDateTime.plusSeconds(1), largestOffsetDateTime); + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/InertValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/InertValueSource.java new file mode 100644 index 0000000..7e895b6 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/InertValueSource.java @@ -0,0 +1,12 @@ +package it.aboutbits.springboot.testing.validation.source; + +import it.aboutbits.springboot.testing.validation.core.ValueSource; + +import java.util.stream.Stream; + +public class InertValueSource implements ValueSource { + @Override + public Stream values(Class propertyClass, Object... args) { + return Stream.empty(); + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/LessThanValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/LessThanValueSource.java new file mode 100644 index 0000000..df02ab9 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/LessThanValueSource.java @@ -0,0 +1,118 @@ +package it.aboutbits.springboot.testing.validation.source; + +import it.aboutbits.springboot.testing.validation.core.ValueSource; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; +import lombok.NonNull; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.function.Function; +import java.util.stream.Stream; + +// For floating point values: The negative max value is the minimum, MIN_VALUE is the smallest positive +public class LessThanValueSource implements ValueSource { + private static final Map, Function>> TYPE_SOURCES = new HashMap<>(); + private static final Random RANDOM = new Random(); + + static { + TYPE_SOURCES.put(Integer.class, LessThanValueSource::getIntegerStream); + TYPE_SOURCES.put(int.class, LessThanValueSource::getIntegerStream); + + TYPE_SOURCES.put(Long.class, LessThanValueSource::getLongStream); + TYPE_SOURCES.put(long.class, LessThanValueSource::getLongStream); + + TYPE_SOURCES.put(Float.class, LessThanValueSource::getFloatStream); + TYPE_SOURCES.put(float.class, LessThanValueSource::getFloatStream); + + TYPE_SOURCES.put(Double.class, LessThanValueSource::getDoubleStream); + TYPE_SOURCES.put(double.class, LessThanValueSource::getDoubleStream); + + TYPE_SOURCES.put(BigDecimal.class, LessThanValueSource::getBigDecimalStream); + TYPE_SOURCES.put(ScaledBigDecimal.class, LessThanValueSource::getScaledBigDecimalStream); + } + + public static void registerType(Class type, Function> source) { + TYPE_SOURCES.put(type, source); + } + + @Override + @SuppressWarnings("unchecked") + public Stream values(Class propertyClass, Object... args) { + var sourceFunction = TYPE_SOURCES.get(propertyClass); + if (sourceFunction != null) { + return (Stream) sourceFunction.apply(args); + } + + throw new IllegalArgumentException("Property class not supported!"); + } + + @NonNull + private static Stream getIntegerStream(Object[] args) { + var minValue = Integer.MIN_VALUE; + var maxValue = Long.valueOf((long) args[0]).intValue() - 1; + + return Stream.concat( + Stream.of(minValue, maxValue), + RANDOM.ints(minValue, maxValue).limit(1).boxed() + ); + } + + @NonNull + private static Stream getLongStream(Object[] args) { + var minValue = Long.MIN_VALUE; + var maxValue = (long) args[0] - 1; + + return Stream.concat( + Stream.of(minValue, maxValue), + RANDOM.longs(minValue, maxValue).limit(1).boxed() + ); + } + + @NonNull + private static Stream getFloatStream(Object[] args) { + var minValue = Float.MAX_VALUE * -1; + var maxValue = Long.valueOf((long) args[0]).floatValue() - 0.1f; + + return Stream.concat( + Stream.of(minValue, maxValue), + RANDOM.doubles(1).map(d -> minValue + (maxValue - minValue) * d).boxed().map( + Double::floatValue + ) + ); + } + + @NonNull + private static Stream getDoubleStream(Object[] args) { + var minValue = Double.MAX_VALUE * -1; + var maxValue = Long.valueOf((long) args[0]).doubleValue() - 0.1d; + + return Stream.concat( + Stream.of(minValue, maxValue), + RANDOM.doubles(1).map(d -> minValue + (maxValue - minValue) * d).boxed() + ); + } + + @NonNull + private static Stream getBigDecimalStream(Object[] args) { + var minValue = Double.MAX_VALUE * -1; + var maxValue = Long.valueOf((long) args[0]).doubleValue() - 0.1d; + + return Stream.concat( + Stream.of(BigDecimal.valueOf(minValue), BigDecimal.valueOf(maxValue)), + RANDOM.doubles(1).map(d -> minValue + (maxValue - minValue) * d).boxed().map(BigDecimal::valueOf) + ); + } + + @NonNull + private static Stream getScaledBigDecimalStream(Object[] args) { + var minValue = Double.MAX_VALUE * -1; + var maxValue = Long.valueOf((long) args[0]).doubleValue() - 0.1d; + + return Stream.concat( + Stream.of(ScaledBigDecimal.valueOf(minValue), ScaledBigDecimal.valueOf(maxValue)), + RANDOM.doubles(1).map(d -> minValue + (maxValue - minValue) * d).boxed().map(ScaledBigDecimal::valueOf) + ); + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/NullValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/NullValueSource.java new file mode 100644 index 0000000..0656bdf --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/NullValueSource.java @@ -0,0 +1,27 @@ +package it.aboutbits.springboot.testing.validation.source; + +import it.aboutbits.springboot.testing.validation.core.ValueSource; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; + +public class NullValueSource implements ValueSource { + private static final Map, Function>> TYPE_SOURCES = new HashMap<>(); + + public static void registerType(Class type, Function> source) { + TYPE_SOURCES.put(type, source); + } + + @Override + @SuppressWarnings("unchecked") + public Stream values(Class propertyClass, Object... args) { + var sourceFunction = TYPE_SOURCES.get(propertyClass); + if (sourceFunction != null) { + return (Stream) sourceFunction.apply(args); + } + + return Stream.of((T) null); + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/PastValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/PastValueSource.java new file mode 100644 index 0000000..f8b2271 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/PastValueSource.java @@ -0,0 +1,59 @@ +package it.aboutbits.springboot.testing.validation.source; + +import it.aboutbits.springboot.testing.validation.core.ValueSource; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; + +public class PastValueSource implements ValueSource { + private static final Map, Function>> TYPE_SOURCES = new HashMap<>(); + + static { + TYPE_SOURCES.put(LocalDate.class, PastValueSource::getLocalDateStream); + TYPE_SOURCES.put(LocalDateTime.class, PastValueSource::getLocalDatetimeStream); + TYPE_SOURCES.put(OffsetDateTime.class, PastValueSource::getOffsetDateTimeStream); + } + + public static void registerType(Class type, Function> source) { + TYPE_SOURCES.put(type, source); + } + + @Override + @SuppressWarnings("unchecked") + public Stream values(Class propertyClass, Object... args) { + var sourceFunction = TYPE_SOURCES.get(propertyClass); + if (sourceFunction != null) { + return (Stream) sourceFunction.apply(args); + } + + throw new IllegalArgumentException("Property class not supported!"); + } + + + private static Stream getLocalDateStream(Object[] args) { + var currentDate = LocalDate.now(); + var smallestDate = LocalDate.MIN; + + return Stream.of(smallestDate, currentDate.minusDays(1)); + } + + private static Stream getLocalDatetimeStream(Object[] args) { + var currentDateTime = LocalDateTime.now(); + var smallestDateTime = LocalDateTime.MIN; + + return Stream.of(smallestDateTime, currentDateTime.minusSeconds(1)); + } + + private static Stream getOffsetDateTimeStream(Object[] args) { + var currentOffsetDateTime = OffsetDateTime.now(ZoneOffset.UTC); + var smallestOffsetDateTime = OffsetDateTime.MIN; + + return Stream.of(smallestOffsetDateTime, currentOffsetDateTime.minusSeconds(1)); + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/validation/source/ZeroValueSource.java b/src/main/java/it/aboutbits/springboot/testing/validation/source/ZeroValueSource.java new file mode 100644 index 0000000..b0a7c02 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/validation/source/ZeroValueSource.java @@ -0,0 +1,46 @@ +package it.aboutbits.springboot.testing.validation.source; + +import it.aboutbits.springboot.testing.validation.core.ValueSource; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; + +public class ZeroValueSource implements ValueSource { + private static final Map, Function>> TYPE_SOURCES = new HashMap<>(); + + static { + TYPE_SOURCES.put(Integer.class, (Object[] args) -> Stream.of(0)); + TYPE_SOURCES.put(int.class, (Object[] args) -> Stream.of(0)); + + TYPE_SOURCES.put(Long.class, (Object[] args) -> Stream.of(0L)); + TYPE_SOURCES.put(long.class, (Object[] args) -> Stream.of(0L)); + + TYPE_SOURCES.put(Float.class, (Object[] args) -> Stream.of(0F)); + TYPE_SOURCES.put(float.class, (Object[] args) -> Stream.of(0F)); + + TYPE_SOURCES.put(Double.class, (Object[] args) -> Stream.of(0D)); + TYPE_SOURCES.put(double.class, (Object[] args) -> Stream.of(0D)); + + TYPE_SOURCES.put(BigDecimal.class, (Object[] args) -> Stream.of(BigDecimal.valueOf(0))); + TYPE_SOURCES.put(ScaledBigDecimal.class, (Object[] args) -> Stream.of(ScaledBigDecimal.valueOf(0))); + } + + public static void registerType(Class type, Function> source) { + TYPE_SOURCES.put(type, source); + } + + @Override + @SuppressWarnings("unchecked") + public Stream values(Class propertyClass, Object... args) { + var sourceFunction = TYPE_SOURCES.get(propertyClass); + if (sourceFunction != null) { + return (Stream) sourceFunction.apply(args); + } + + throw new IllegalArgumentException("Property class not supported!"); + } +} diff --git a/src/test/java/it/aboutbits/springboot/testing/validation/ValidationAssertTest.java b/src/test/java/it/aboutbits/springboot/testing/validation/ValidationAssertTest.java new file mode 100644 index 0000000..e59d945 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/testing/validation/ValidationAssertTest.java @@ -0,0 +1,436 @@ +package it.aboutbits.springboot.testing.validation; + +import it.aboutbits.springboot.testing.validation.core.BaseRuleBuilder; +import it.aboutbits.springboot.testing.validation.core.BaseValidationAssert; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Negative; +import jakarta.validation.constraints.NegativeOrZero; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Past; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.With; +import org.junit.jupiter.api.Test; +import org.springframework.lang.Nullable; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; + +import static it.aboutbits.springboot.testing.validation.ValidationAssertTest.TestValidationAssert.assertThatValidation; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +class ValidationAssertTest { + @With + public record SomeValidParameter( + @NotNull + String notNullable, + @NotBlank + String notBlank, + @Min(5) + int biggerThanInt, + @Min(5) + long biggerThanLong, + @Min(5) + float biggerThanFloat, + @Min(5) + double biggerThanDouble, + @Min(5) + BigDecimal biggerThanBigDecimal, + @Min(5) + ScaledBigDecimal biggerThanScaledBigDecimal, + @Max(5) + int lessThanInt, + @Max(5) + long lessThanLong, + @Max(5) + float lessThanFloat, + @Max(5) + double lessThanDouble, + @Max(5) + BigDecimal lessThanBigDecimal, + @Max(5) + ScaledBigDecimal lessThanScaledBigDecimal, + @Min(-3) + @Max(5) + int betweenInt, + @Min(-3) + @Max(5) + long betweenLong, + @Min(-3) + @Max(5) + float betweenFloat, + @Min(-3) + @Max(5) + double betweenDouble, + @Min(-3) + @Max(5) + BigDecimal betweenBigDecimal, + @Min(-3) + @Max(5) + ScaledBigDecimal betweenScaledBigDecimal, + @Positive + int positiveInt, + @Positive + long positiveLong, + @Positive + float positiveFloat, + @Positive + double positiveDouble, + @Positive + BigDecimal positiveBigDecimal, + @Positive + ScaledBigDecimal positiveScaledBigDecimal, + @Negative + int negativeInt, + @Negative + long negativeLong, + @Negative + float negativeFloat, + @Negative + double negativeDouble, + @Negative + BigDecimal negativeBigDecimal, + @Negative + ScaledBigDecimal negativeScaledBigDecimal, + @PositiveOrZero + int positiveOrZeroInt, + @PositiveOrZero + long positiveOrZeroLong, + @PositiveOrZero + float positiveOrZeroFloat, + @PositiveOrZero + double positiveOrZeroDouble, + @PositiveOrZero + BigDecimal positiveOrZeroBigDecimal, + @PositiveOrZero + ScaledBigDecimal positiveOrZeroScaledBigDecimal, + @NegativeOrZero + int negativeOrZeroInt, + @NegativeOrZero + long negativeOrZeroLong, + @NegativeOrZero + float negativeOrZeroFloat, + @NegativeOrZero + double negativeOrZeroDouble, + @NegativeOrZero + BigDecimal negativeOrZeroBigDecimal, + @NegativeOrZero + ScaledBigDecimal negativeOrZeroScaledBigDecimal, + @Future + LocalDate futureDate, + @Future + LocalDateTime futureDateTime, + @Future + OffsetDateTime futureOffsetDateTime, + @Past + LocalDate pastDate, + @Past + LocalDateTime pastDateTime, + @Past + OffsetDateTime pastOffsetDateTime, + @Valid + Object validObject, + @Nullable + Object nullable, + Object notValidated + ) { + + } + + @Test + @SuppressWarnings("checkstyle:MethodLength") + void testWithBeanValidation() { + var validParameter = getSomeValidParameter(); + + assertThatValidation().of(validParameter) + .usingBeanValidation() + .notNull("notNullable") + .notBlank("notBlank") + .min("biggerThanInt", 5) + .min("biggerThanLong", 5) + .min("biggerThanFloat", 5) + .min("biggerThanDouble", 5) + .min("biggerThanBigDecimal", 5) + .min("biggerThanScaledBigDecimal", 5) + .max("lessThanInt", 5) + .max("lessThanLong", 5) + .max("lessThanFloat", 5) + .max("lessThanDouble", 5) + .max("lessThanBigDecimal", 5) + .max("lessThanScaledBigDecimal", 5) + .positive("positiveInt") + .positive("positiveLong") + .positive("positiveFloat") + .positive("positiveDouble") + .positive("positiveBigDecimal") + .positive("positiveScaledBigDecimal") + .negative("negativeInt") + .negative("negativeLong") + .negative("negativeFloat") + .negative("negativeDouble") + .negative("negativeBigDecimal") + .negative("negativeScaledBigDecimal") + .between("betweenInt", -3, 5) + .between("betweenLong", -3, 5) + .between("betweenFloat", -3, 5) + .between("betweenDouble", -3, 5) + .between("betweenBigDecimal", -3, 5) + .between("betweenScaledBigDecimal", -3, 5) + .positiveOrZero("positiveOrZeroInt") + .positiveOrZero("positiveOrZeroLong") + .positiveOrZero("positiveOrZeroFloat") + .positiveOrZero("positiveOrZeroDouble") + .positiveOrZero("positiveOrZeroBigDecimal") + .positiveOrZero("positiveOrZeroScaledBigDecimal") + .negativeOrZero("negativeOrZeroInt") + .negativeOrZero("negativeOrZeroLong") + .negativeOrZero("negativeOrZeroFloat") + .negativeOrZero("negativeOrZeroDouble") + .negativeOrZero("negativeOrZeroBigDecimal") + .negativeOrZero("negativeOrZeroScaledBigDecimal") + .future("futureDate") + .future("futureDateTime") + .future("futureOffsetDateTime") + .past("pastDate") + .past("pastDateTime") + .past("pastOffsetDateTime") + .validBean("validObject") + .nullable("nullable") + .notValidated("notValidated") + .isCompliant(); + } + + @Test + void invalidParameter_shouldFail() { + var validParameter = getSomeValidParameter(); + + var invalidParameter = validParameter.withFutureDate(LocalDate.EPOCH); + + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThatValidation().of(invalidParameter) + .usingBeanValidation() + .notNull("notNullable") + .notBlank("notBlank") + .min("biggerThanInt", 5) + .min("biggerThanLong", 5) + .min("biggerThanFloat", 5) + .min("biggerThanDouble", 5) + .min("biggerThanBigDecimal", 5) + .min("biggerThanScaledBigDecimal", 5) + .max("lessThanInt", 5) + .max("lessThanLong", 5) + .max("lessThanFloat", 5) + .max("lessThanDouble", 5) + .max("lessThanBigDecimal", 5) + .max("lessThanScaledBigDecimal", 5) + .positive("positiveInt") + .positive("positiveLong") + .positive("positiveFloat") + .positive("positiveDouble") + .positive("positiveBigDecimal") + .positive("positiveScaledBigDecimal") + .negative("negativeInt") + .negative("negativeLong") + .negative("negativeFloat") + .negative("negativeDouble") + .negative("negativeBigDecimal") + .negative("negativeScaledBigDecimal") + .between("betweenInt", -3, 5) + .between("betweenLong", -3, 5) + .between("betweenFloat", -3, 5) + .between("betweenDouble", -3, 5) + .between("betweenBigDecimal", -3, 5) + .between("betweenScaledBigDecimal", -3, 5) + .positiveOrZero("positiveOrZeroInt") + .positiveOrZero("positiveOrZeroLong") + .positiveOrZero("positiveOrZeroFloat") + .positiveOrZero("positiveOrZeroDouble") + .positiveOrZero("positiveOrZeroBigDecimal") + .positiveOrZero("positiveOrZeroScaledBigDecimal") + .negativeOrZero("negativeOrZeroInt") + .negativeOrZero("negativeOrZeroLong") + .negativeOrZero("negativeOrZeroFloat") + .negativeOrZero("negativeOrZeroDouble") + .negativeOrZero("negativeOrZeroBigDecimal") + .negativeOrZero("negativeOrZeroScaledBigDecimal") + .future("futureDate") + .future("futureDateTime") + .future("futureOffsetDateTime") + .past("pastDate") + .past("pastDateTime") + .past("pastOffsetDateTime") + .validBean("validObject") + .nullable("nullable") + .notValidated("notValidated") + .isCompliant()); + } + + @Test + void propertyMissingRule_shouldFail() { + var validParameter = getSomeValidParameter(); + + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThatValidation().of(validParameter) + .usingBeanValidation() + .notNull("notNullable") + // this is now missing: .notBlank("notBlank") + .min("biggerThanInt", 5) + .min("biggerThanLong", 5) + .min("biggerThanFloat", 5) + .min("biggerThanDouble", 5) + .min("biggerThanBigDecimal", 5) + .min("biggerThanScaledBigDecimal", 5) + .max("lessThanInt", 5) + .max("lessThanLong", 5) + .max("lessThanFloat", 5) + .max("lessThanDouble", 5) + .max("lessThanBigDecimal", 5) + .max("lessThanScaledBigDecimal", 5) + .positive("positiveInt") + .positive("positiveLong") + .positive("positiveFloat") + .positive("positiveDouble") + .positive("positiveBigDecimal") + .positive("positiveScaledBigDecimal") + .negative("negativeInt") + .negative("negativeLong") + .negative("negativeFloat") + .negative("negativeDouble") + .negative("negativeBigDecimal") + .negative("negativeScaledBigDecimal") + .between("betweenInt", -3, 5) + .between("betweenLong", -3, 5) + .between("betweenFloat", -3, 5) + .between("betweenDouble", -3, 5) + .between("betweenBigDecimal", -3, 5) + .between("betweenScaledBigDecimal", -3, 5) + .positiveOrZero("positiveOrZeroInt") + .positiveOrZero("positiveOrZeroLong") + .positiveOrZero("positiveOrZeroFloat") + .positiveOrZero("positiveOrZeroDouble") + .positiveOrZero("positiveOrZeroBigDecimal") + .positiveOrZero("positiveOrZeroScaledBigDecimal") + .negativeOrZero("negativeOrZeroInt") + .negativeOrZero("negativeOrZeroLong") + .negativeOrZero("negativeOrZeroFloat") + .negativeOrZero("negativeOrZeroDouble") + .negativeOrZero("negativeOrZeroBigDecimal") + .negativeOrZero("negativeOrZeroScaledBigDecimal") + .future("futureDate") + .future("futureDateTime") + .future("futureOffsetDateTime") + .past("pastDate") + .past("pastDateTime") + .past("pastOffsetDateTime") + .validBean("validObject") + .nullable("nullable") + .notValidated("notValidated") + .isCompliant()); + } + + private static SomeValidParameter getSomeValidParameter() { + return new SomeValidParameter( + // notNull + "", + + // notBlank + "something", + + // min + 6, + 6, + 6, + 6, + BigDecimal.valueOf(6), + ScaledBigDecimal.valueOf(6), + + // max + 4, + 4, + 4, + 4, + BigDecimal.valueOf(4), + ScaledBigDecimal.valueOf(4), + + // positive + 4, + 4, + 4, + 4, + BigDecimal.valueOf(4), + ScaledBigDecimal.valueOf(4), + + // negative + 1, + 1, + 1, + 1, + BigDecimal.valueOf(1), + ScaledBigDecimal.valueOf(1), + + // between + -1, + -1, + -1, + -1, + BigDecimal.valueOf(-1), + ScaledBigDecimal.valueOf(-1), + + // positiveOrZero + 0, + 0, + 0, + 0, + BigDecimal.valueOf(0), + ScaledBigDecimal.valueOf(0), + + // negativeOrZero + 0, + 0, + 0, + 0, + BigDecimal.valueOf(0), + ScaledBigDecimal.valueOf(0), + + // future + LocalDate.now().plusDays(1), + LocalDateTime.now().plusDays(1), + OffsetDateTime.now().plusDays(1), + + // past + LocalDate.now().minusDays(1), + LocalDateTime.now().minusDays(1), + OffsetDateTime.now().minusDays(1), + + // valid + null, + + // nullable + null, + + // not validated + null + ); + } + + public static final class TestValidationAssert extends BaseValidationAssert> { + private TestValidationAssert() { + super(new TestRuleBuilder()); + } + + public static TestValidationAssert assertThatValidation() { + return new TestValidationAssert(); + } + + public static final class TestRuleBuilder extends BaseRuleBuilder { + + } + } +}