micronaut-projects / micronaut-validation

Validation support for Micronaut
Apache License 2.0
6 stars 6 forks source link

Calling addConstraintViolation on a ConstraintValidatorContext results in a wrong validation message #397

Open tbashour opened 3 months ago

tbashour commented 3 months ago

Expected Behavior

message and messageTemplate are filled correctly

Actual Behaviour

message is filled with the custome template and message template is filled with the interpolated message.

Steps To Reproduce

The Problem is in the addConstraintViolation method in the DefaultConstraintViolationBuilder class. The mehods adds a violation by calling:


new DefaultConstraintViolation(this.constraintValidatorContext.getRootBean(), this.constraintValidatorContext.getRootClass(), (Object)null, (Object)null, this.messageTemplate, this.messageInterpolator.interpolate(this.messageTemplate, new MessageInterpolator.Context() {
            public ConstraintDescriptor<?> getConstraintDescriptor() {
                return DefaultConstraintViolationBuilder.this.constraintValidatorContext.constraint;
            }

            public Object getValidatedValue() {
                return null;
            }

            public <T> T unwrap(Class<T> type) {
                throw new ValidationException("Not supported!");
            }
        })

It should be:

new DefaultConstraintViolation(this.constraintValidatorContext.getRootBean(), this.constraintValidatorContext.getRootClass(), (Object)null, (Object)null, this.messageInterpolator.interpolate(this.messageTemplate, new MessageInterpolator.Context() {
            public ConstraintDescriptor<?> getConstraintDescriptor() {
                return DefaultConstraintViolationBuilder.this.constraintValidatorContext.constraint;
            }

            public Object getValidatedValue() {
                return null;
            }

            public <T> T unwrap(Class<T> type) {
                throw new ValidationException("Not supported!");
            }
        }, this.messageTemplate)

Environment Information

No response

Example Application

package com.micronaut.test.constraints;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import jakarta.validation.constraints.Pattern;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ FIELD, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { CollectionPatternValidator.class })
public @interface CollectionPattern {

    String regexp();

    Pattern.Flag[] flags() default {};

    String message() default "{jakarta.validation.constraints.Pattern.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
package com.micronaut.test.constraints;

import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.validation.validator.constraints.ConstraintValidator;
import io.micronaut.validation.validator.constraints.ConstraintValidatorContext;
import jakarta.inject.Singleton;

import java.util.Collection;

@Singleton
public class CollectionPatternValidator implements ConstraintValidator<CollectionPattern, Collection<String>> {

    @Override
    public boolean isValid(@Nullable Collection<String> values,
                           @NonNull AnnotationValue<CollectionPattern> annotationMetadata,
                           @NonNull ConstraintValidatorContext context) {

        if (values == null || values.isEmpty()) {
            return true;
        }

        final String regexp = annotationMetadata.getRequiredValue("regexp", String.class);
        final java.util.regex.Pattern regex = java.util.regex.Pattern.compile(regexp);

        context.disableDefaultConstraintViolation();

        boolean valid = true;
        for (String value : values) {
            if (value != null && !regex.matcher(value).matches()) {

                context.buildConstraintViolationWithTemplate("must match  \"{regexp}\"")
                        .addPropertyNode("[" + value + "]")
                        .addConstraintViolation();

                valid = false;
            }
        }
        return valid;
    }
}
package com.micronaut.test.constraints;

import io.micronaut.core.annotation.Introspected;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import io.micronaut.validation.validator.Validator;
import jakarta.inject.Inject;
import jakarta.validation.ConstraintViolation;
import lombok.Getter;
import lombok.Setter;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.not;

@MicronautTest(startApplication = false)
public class CustomerConstraintTest {

    @Introspected
    @Getter
    @Setter
    public class ExampleBean {
        @CollectionPattern(regexp = "[a-z]")
        private List<String> list = new ArrayList<>();
    }

    @Inject
    Validator validator;

    @Test
    void should_return_interpolated_message()
    {
        // Given
        ExampleBean bean = new ExampleBean();
        bean.getList().add("11111");

        // When
        Set<ConstraintViolation<ExampleBean>> violations = validator.validate(bean);

        // Then
        assertThat("violations", violations, not(empty()));
        assertThat("violations[0].message", violations.iterator().next().getMessage(), containsString("[a-z]"));
        assertThat("violations[0].message", violations.iterator().next().getMessageTemplate(), containsString("regexp"));
    }

}

Version

4.6.1

dstepanov commented 3 months ago

Please create a reproducer

tbashour commented 3 months ago

Please create a reproducer

I have written a junit testcase which should reporduce the issue

dstepanov commented 3 months ago

Can you just create a PR with a fix and a test?

tbashour commented 3 months ago

Can you just create a PR with a fix and a test?

I really don't have time to do so ... not in the coming two weeks

Are you still missing some information? Or you also don't have time to fix the issue?

dstepanov commented 3 months ago

It’s faster to create a PR directly, I will have time only next week.