I was confused about the validation (which can be defined by annotation) on the javax side and the validation test method on the Spring side, so I will summarize it as follows. By the way, you may think that you should make everything with custom annotations on the javax side, but I could not test it as I expected, so I had to make it on the Spring side.
How to DI to the field in the validation that I made when executing the test
First, check the target class.
PasswordForm.java
package com.ssp_engine.user.domain.model;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.Length;
import com.ssp_engine.user.domain.model.validation.ConfirmPassword;
import com.ssp_engine.user.domain.model.validation.ValidGroup1;
import com.ssp_engine.user.domain.model.validation.ValidGroup2;
import com.ssp_engine.user.domain.model.validation.ValidGroup3;
import com.ssp_engine.user.domain.model.validation.ValidGroup4;
import lombok.Data;
@Data
@ConfirmPassword(field = "password", groups = ValidGroup4.class)
public class PasswordForm {
@NotBlank(groups = ValidGroup1.class)
private String currentPassword;
@NotBlank(groups = ValidGroup1.class)
@Length(min = 4, max = 8, groups = ValidGroup2.class)
@Pattern(regexp="^[a-zA-Z0-9]+$", groups = ValidGroup3.class)
private String password;
@NotBlank(groups = ValidGroup1.class)
private String confirmPassword;
}
It is a form class when editing your own password on the management screen of a general Web service like this.
currentPassword
is the password you are currently logged in to
password
is the password to change
confirmPassword
is the confirmation password
So, each has a regular expression validation for @ NotBlank
and @Pattern
.
There is a validation in currentPassword
to compare with the currently logged in password, which is as follows.
LoginPassAndFormPassValidator.java
package com.ssp_engine.user.domain.model.validation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import com.ssp_engine.user.domain.model.PasswordForm;
@Component
public class LoginPassAndFormPassValidator implements Validator {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public boolean supports(Class<?> clazz) {
return PasswordForm.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
PasswordForm form = (PasswordForm) target;
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
UserDetails principal = (UserDetails) auth.getPrincipal();
String userPass = principal.getPassword();
if (form.getCurrentPassword() == null) {
return;
}
if (!this.passwordEncoder.matches(form.getCurrentPassword(), userPass)) {
errors.rejectValue("currentPassword",
"LoginPassAndFormPassValidator.PasswordForm.currentPassword",
"It is different from the password you are logged in to.");
}
}
}
On the controller side, I use it after @InitBinder
.
We will test these Spring and Javax validators.
And as for the test, it is not long to separate the test on the controller side and the test of the form class, and it is easier to separate the test contents, so it became like this.
PasswordFormTests.java
@RunWith(SpringRunner.class)
@SpringBootTest
public class PasswordFormTests {
private PasswordForm passwordForm = new PasswordForm();
private BindingResult bindingResult = new BindException(passwordForm, "PasswordForm"); //①
@Autowired
@Qualifier("loginPassAndFormPassValidator") //②
/*Spring side*/
private org.springframework.validation.Validator loginPassAndFormPassValidator;
/*javax side*/
private static Validator validator; //③
@BeforeClass
public static void initialization process() { //④
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
validator = validatorFactory.getValidator();
}
@Before
public void Set value() throws Exception{ //⑤
this.passwordForm.setCurrentPassword("currentpassword");
this.passwordForm.setConfirmPassword("password");
this.passwordForm.setPassword("password");
}
}
① ・ ・ ・ Field for receiving the result after executing the validator
② ・ ・ ・ Since it was necessary to explicitly specify which class, it was specified with @Qualifier
.
③ ・ ④ ・ ・ ・ I wanted to get a bean by specifying it with @AutoWired
, but it didn't work, so I'm explicitly creating a bean.
⑤ ・ ・ ・ A value is set for the target object.
When I'm ready, I'm going to do a gorigori test, and it looks like this. First, check that there are no errors.
PasswordFormTests.java
@Test
@WithMockUser(username = "username",
password ="$2a$10$p3/Malw3/KWyfOlPwWoUCulx4iDb2C/nmo6x8P2svXjfJQ5ETLhG2",
roles = "USER")
public void no error() throws Exception{
loginPassAndFormPassValidator.validate(this.passwordForm, bindingResult); //①
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup1.class,ValidGroup2.class,ValidGroup3.class,ValidGroup4.class); //②
assertThat(bindingResult.getFieldError(), is(nullValue())); //③
assertThat(violations.size(), is(0)); //④
}
① ・ ・ ・ Validation on the Spring side is being executed. The target object is specified in the first argument, and the object bindingResult
that stores the result is specified in the second argument.
② ・ ・ ・ ConstraintViolation
returns a set of objects that store the contents of the constraint violation, and the target object is the first argument of validate. Since ValidGroup was specified in the second argument, which validation is enabled is specified.
② ・ ・ ・ Receives the result on the Spring side with bindingResult
and checks whether it is Null.
③ ・ ・ ・ The result on the Javax side is sized with violations.size ()
and checked to see if it is 0.
@WithMockUser
is logged in because it was necessary to get login information with loginPassAndFormPassValidator
.
When I found out that there was no error with this, I wrote it like this.
PasswordFormTests.java
@Test
@WithMockUser(username = "username",
password ="currentpassword",
roles = "USER")
public void Login path and input path are different() throws Exception{
loginPassAndFormPassValidator.validate(this.passwordForm, bindingResult);
assertThat(bindingResult.getFieldError("currentPassword"), is(bindingResult.getFieldError()));
assertThat(bindingResult.getFieldError().getDefaultMessage(), is("It is different from the password you are logged in to."));
}
@Test
public void The current password is Blank() throws Exception{
this.passwordForm.setCurrentPassword("");
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup1.class);
assertThat(violations.size(), is(1));
assertThat(getAnnotation(violations, "currentPassword"), is(instanceOf(NotBlank.class))); //①
}
private Annotation getAnnotation(Set<ConstraintViolation<PasswordForm>> violations, String path) { //②
return violations.stream()
.filter(cv -> cv.getPropertyPath().toString().equals(path))
.findFirst()
.map(cv -> cv.getConstraintDescriptor().getAnnotation())
.get();
}
① ・ ・ ・ Check which annotation causes the error. (2) ... I am creating a method to get an instance of the annotation that was played with an error.
The whole picture looks like this.
PasswordFormTests.java
package com.ssp_engine.user.domain.model;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import java.lang.annotation.Annotation;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.Length;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import com.ssp_engine.user.domain.model.validation.ConfirmPassword;
import com.ssp_engine.user.domain.model.validation.ValidGroup1;
import com.ssp_engine.user.domain.model.validation.ValidGroup2;
import com.ssp_engine.user.domain.model.validation.ValidGroup3;
import com.ssp_engine.user.domain.model.validation.ValidGroup4;
@RunWith(SpringRunner.class)
@SpringBootTest
public class PasswordFormTests {
private PasswordForm passwordForm = new PasswordForm();
private BindingResult bindingResult = new BindException(passwordForm, "PasswordForm");
@Autowired
@Qualifier("loginPassAndFormPassValidator")
/*Spring side*/
private org.springframework.validation.Validator loginPassAndFormPassValidator;
/*javax side*/
private static Validator validator;
@BeforeClass
public static void initialization process() {
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
validator = validatorFactory.getValidator();
}
@Before
public void Set value() throws Exception{
this.passwordForm.setCurrentPassword("currentpassword");
this.passwordForm.setConfirmPassword("password");
this.passwordForm.setPassword("password");
}
@Test
@WithMockUser(username = "username",
password ="$2a$10$p3/Malw3/KWyfOlPwWoUCulx4iDb2C/nmo6x8P2svXjfJQ5ETLhG2",
roles = "USER")
public void no error() throws Exception{
loginPassAndFormPassValidator.validate(this.passwordForm, bindingResult);
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup1.class,ValidGroup2.class,ValidGroup3.class,ValidGroup4.class);
assertThat(bindingResult.getFieldError(), is(nullValue()));
assertThat(violations.size(), is(0));
}
@Test
@WithMockUser(username = "username",
password ="currentpassword",
roles = "USER")
public void Login path and input path are different() throws Exception{
loginPassAndFormPassValidator.validate(this.passwordForm, bindingResult);
assertThat(bindingResult.getFieldError("currentPassword"), is(bindingResult.getFieldError()));
assertThat(bindingResult.getFieldError().getDefaultMessage(), is("It is different from the password you are logged in to."));
}
@Test
public void The current password is Blank() throws Exception{
this.passwordForm.setCurrentPassword("");
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup1.class);
assertThat(violations.size(), is(1));
assertThat(getAnnotation(violations, "currentPassword"), is(instanceOf(NotBlank.class)));
}
@Test
public void Password is Blank() throws Exception{
this.passwordForm.setPassword("");
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup1.class);
assertThat(violations.size(), is(1));
assertThat(getAnnotation(violations, "password"), is(instanceOf(NotBlank.class)));
}
@Test
public void Confirmation password is Blank() throws Exception{
this.passwordForm.setConfirmPassword("");
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup1.class);
assertThat(violations.size(), is(1));
assertThat(getAnnotation(violations, "confirmPassword"), is(instanceOf(NotBlank.class)));
}
@Test
public void When the confirmation password and the input password are different() throws Exception{
this.passwordForm.setPassword("aiueo");
this.passwordForm.setConfirmPassword("kakikukeko");
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup4.class);
assertThat(violations.size(), is(1));
assertThat(getAnnotation(violations, "password"), is(instanceOf(ConfirmPassword.class)));
}
@Test
public void When the password is 8 characters or more() throws Exception{
this.passwordForm.setPassword("aiueokakikukeko");
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup2.class);
assertThat(violations.size(), is(1));
assertThat(getAnnotation(violations, "password"), is(instanceOf(Length.class)));
}
@Test
public void When the password is 4 characters or less() throws Exception{
this.passwordForm.setPassword("aiu");
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup2.class);
assertThat(violations.size(), is(1));
assertThat(getAnnotation(violations, "password"), is(instanceOf(Length.class)));
}
@Test
public void When the password is other than half-width alphanumeric characters() throws Exception{
this.passwordForm.setPassword("It's a test");
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup3.class);
assertThat(violations.size(), is(1));
assertThat(getAnnotation(violations, "password"), is(instanceOf(Pattern.class)));
}
private Annotation getAnnotation(Set<ConstraintViolation<PasswordForm>> violations, String path) {
return violations.stream()
.filter(cv -> cv.getPropertyPath().toString().equals(path))
.findFirst()
.map(cv -> cv.getConstraintDescriptor().getAnnotation())
.get();
}
}
I would be grateful if anyone could find it helpful.
Recommended Posts