From 9f25184cd226f74d2c26fa05623e30b7f5e9aa1d Mon Sep 17 00:00:00 2001 From: Borychev Andrey Date: Sun, 23 Jun 2024 22:23:01 +0300 Subject: [PATCH] [#260] unified error display format and add instancio, faker dependency for tests --- build.gradle.kts | 4 +- .../account/constraint/AccountUsername.java | 2 +- .../web/model/SignupAccountModel.java | 3 +- .../web/model/WorkspaceUserModel.java | 2 +- src/main/resources/messages_en.properties | 3 + src/main/resources/messages_ru.properties | 2 + .../resources/templates/account/signup.html | 12 +-- .../resources/templates/error-general.html | 2 +- src/main/resources/templates/login.html | 2 +- .../templates/widget/report-typo-error.html | 2 +- .../resources/templates/widget/typo-form.html | 2 +- .../templates/workspace/wks-settings.html | 2 +- .../templates/workspace/wks-users.html | 2 +- .../typoreporter/config/TestConfig.java | 18 ++++ .../test/factory/AccountModelGenerator.java | 46 ++++++++ .../typoreporter/utils/BundleSourceUtils.java | 75 +++++++++++++ .../hexlet/typoreporter/utils/ModelUtils.java | 32 ++++++ .../typoreporter/web/AccountControllerIT.java | 101 ++++++++++-------- 18 files changed, 249 insertions(+), 63 deletions(-) create mode 100644 src/test/java/io/hexlet/typoreporter/config/TestConfig.java create mode 100644 src/test/java/io/hexlet/typoreporter/test/factory/AccountModelGenerator.java create mode 100644 src/test/java/io/hexlet/typoreporter/utils/BundleSourceUtils.java create mode 100644 src/test/java/io/hexlet/typoreporter/utils/ModelUtils.java diff --git a/build.gradle.kts b/build.gradle.kts index 11b3e6ec..10ef00f0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -48,7 +48,9 @@ dependencies { implementation("org.mapstruct:mapstruct:1.5.3.Final") // Annotation processors annotationProcessor("org.mapstruct:mapstruct-processor:1.5.3.Final") - + //Generating models for tests + implementation("org.instancio:instancio-junit:3.6.0") + implementation("net.datafaker:datafaker:2.0.2") // Testing testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.security:spring-security-test") diff --git a/src/main/java/io/hexlet/typoreporter/domain/account/constraint/AccountUsername.java b/src/main/java/io/hexlet/typoreporter/domain/account/constraint/AccountUsername.java index 7520cca1..370d3d02 100644 --- a/src/main/java/io/hexlet/typoreporter/domain/account/constraint/AccountUsername.java +++ b/src/main/java/io/hexlet/typoreporter/domain/account/constraint/AccountUsername.java @@ -13,7 +13,7 @@ import java.lang.annotation.Target; @NotBlank -@Pattern(regexp = "^[-_A-Za-z0-9]*$") +@Pattern(regexp = "^[-_A-Za-z0-9]*$", message = "{validation.alert.wrong-username-pattern}") @Size(min = 2, max = 20) @Constraint(validatedBy = {}) @Target({ElementType.FIELD, ElementType.PARAMETER}) diff --git a/src/main/java/io/hexlet/typoreporter/web/model/SignupAccountModel.java b/src/main/java/io/hexlet/typoreporter/web/model/SignupAccountModel.java index b475a10b..00d7a39e 100644 --- a/src/main/java/io/hexlet/typoreporter/web/model/SignupAccountModel.java +++ b/src/main/java/io/hexlet/typoreporter/web/model/SignupAccountModel.java @@ -27,7 +27,8 @@ public class SignupAccountModel { private String username; @Email(regexp = "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$", - message = "The email \"${validatedValue}\" is not valid") + //message = "The email \"${validatedValue}\" is not valid") + message = "{validation.alert.wrong-email}") private String email; @AccountPassword diff --git a/src/main/java/io/hexlet/typoreporter/web/model/WorkspaceUserModel.java b/src/main/java/io/hexlet/typoreporter/web/model/WorkspaceUserModel.java index 17d088a7..1b424db1 100644 --- a/src/main/java/io/hexlet/typoreporter/web/model/WorkspaceUserModel.java +++ b/src/main/java/io/hexlet/typoreporter/web/model/WorkspaceUserModel.java @@ -13,6 +13,6 @@ public class WorkspaceUserModel { @Email(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", - message = "The email \"${validatedValue}\" is not valid") + message = "{validation.alert.wrong-email}") private String email; } diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index 892b32df..eadf47ff 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -104,3 +104,6 @@ btn.delete-from-wks=Delete from workspace alert.password-wrong-format=Password must be between 8 and 20 characters \ and contain only latin letters, digits and symbols ~`!@#$%^&*()_-+={[}]|\:;"'<,>.?/ + +validation.alert.wrong-email=The email "${validatedValue}" is not valid +validation.alert.wrong-username-pattern=The pattern of username must be "{regexp}" diff --git a/src/main/resources/messages_ru.properties b/src/main/resources/messages_ru.properties index 65312c97..d36b20fe 100644 --- a/src/main/resources/messages_ru.properties +++ b/src/main/resources/messages_ru.properties @@ -102,3 +102,5 @@ text.wks-delete-confirm=Удалить пространство? alert.password-wrong-format=Пароль должен быть от 8 до 20 символов\ \ и содержать только буквы латинского алфавита,\ \ цифры и символы ~`!@#$%^&*()_-+={[}]|\:;"'<,>.?/ +validation.alert.wrong-email=Электронная почта "${validatedValue}" указана некорректно +validation.alert.wrong-username-pattern=Имя пользователя должен быть подходить под шаблон "{regexp}" diff --git a/src/main/resources/templates/account/signup.html b/src/main/resources/templates/account/signup.html index 600dfb40..328c8fd2 100644 --- a/src/main/resources/templates/account/signup.html +++ b/src/main/resources/templates/account/signup.html @@ -13,7 +13,7 @@ th:classappend="${!#fields.hasErrors('username') && formModified}? 'is-valid'" th:errorclass="is-invalid"> -
+

@@ -24,7 +24,7 @@ th:classappend="${!#fields.hasErrors('email') && formModified}? 'is-valid'" th:errorclass="is-invalid"> -
+

@@ -35,7 +35,7 @@ th:classappend="${!#fields.hasErrors('firstName') && formModified}? 'is-valid'" th:errorclass="is-invalid"> -
+

@@ -46,7 +46,7 @@ th:classappend="${!#fields.hasErrors('lastName') && formModified}? 'is-valid'" th:errorclass="is-invalid"> -
+

@@ -55,7 +55,7 @@ -
+

@@ -64,7 +64,7 @@ -
+

diff --git a/src/main/resources/templates/error-general.html b/src/main/resources/templates/error-general.html index 9fbfdc1c..e8b7e77e 100644 --- a/src/main/resources/templates/error-general.html +++ b/src/main/resources/templates/error-general.html @@ -2,7 +2,7 @@ -
+
diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index 5905aa22..a81f88dc 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -6,7 +6,7 @@
- +
diff --git a/src/main/resources/templates/widget/report-typo-error.html b/src/main/resources/templates/widget/report-typo-error.html index 2d524213..b9aeb711 100755 --- a/src/main/resources/templates/widget/report-typo-error.html +++ b/src/main/resources/templates/widget/report-typo-error.html @@ -2,7 +2,7 @@ -
+
diff --git a/src/main/resources/templates/widget/typo-form.html b/src/main/resources/templates/widget/typo-form.html index a56b617f..02ea2f1b 100755 --- a/src/main/resources/templates/widget/typo-form.html +++ b/src/main/resources/templates/widget/typo-form.html @@ -17,7 +17,7 @@ type="text" > -
+

diff --git a/src/main/resources/templates/workspace/wks-settings.html b/src/main/resources/templates/workspace/wks-settings.html index 0881b0d1..1c4ac2e3 100644 --- a/src/main/resources/templates/workspace/wks-settings.html +++ b/src/main/resources/templates/workspace/wks-settings.html @@ -49,7 +49,7 @@
-
    +
diff --git a/src/main/resources/templates/workspace/wks-users.html b/src/main/resources/templates/workspace/wks-users.html index 271b1b8f..b7023c12 100644 --- a/src/main/resources/templates/workspace/wks-users.html +++ b/src/main/resources/templates/workspace/wks-users.html @@ -27,7 +27,7 @@ placeholder="Enter user email. For example: hexlet@gmail.com" th:field="*{email}" th:classappend="${!#fields.hasErrors('email') && formModified}? 'is-valid'" th:errorclass="is-invalid" required> -
+

diff --git a/src/test/java/io/hexlet/typoreporter/config/TestConfig.java b/src/test/java/io/hexlet/typoreporter/config/TestConfig.java new file mode 100644 index 00000000..c9485f64 --- /dev/null +++ b/src/test/java/io/hexlet/typoreporter/config/TestConfig.java @@ -0,0 +1,18 @@ +package io.hexlet.typoreporter.config; + +import net.datafaker.Faker; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; + +@Configuration +public class TestConfig { + @Bean + public Faker getFaker() { + return new Faker(); + } + @Bean + public ObjectMapper getObjectMapper() { + return new ObjectMapper(); + } +} diff --git a/src/test/java/io/hexlet/typoreporter/test/factory/AccountModelGenerator.java b/src/test/java/io/hexlet/typoreporter/test/factory/AccountModelGenerator.java new file mode 100644 index 00000000..43aa6942 --- /dev/null +++ b/src/test/java/io/hexlet/typoreporter/test/factory/AccountModelGenerator.java @@ -0,0 +1,46 @@ +package io.hexlet.typoreporter.test.factory; + +import io.hexlet.typoreporter.web.model.SignupAccountModel; +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import net.datafaker.Faker; +import org.instancio.Instancio; +import org.instancio.Model; +import org.instancio.Select; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.Serializable; + +import static io.hexlet.typoreporter.test.factory.EntitiesFactory.ACCOUNT_INCORRECT_EMAIL; + +@Component +@Getter +public class AccountModelGenerator { + private Model correctAccountModel; + private Model incorrectAccountModel; + @Autowired + private Faker faker; + + @PostConstruct + public void init() { + final String password = faker.internet().password(8, 20); + final String incorrectPassword = faker.internet().password(1, 7); + correctAccountModel = Instancio.of(SignupAccountModel.class) + .supply(Select.field(SignupAccountModel::getUsername), () -> faker.name().firstName()) + .supply(Select.field(SignupAccountModel::getEmail), () -> faker.internet().emailAddress()) + .supply(Select.field(SignupAccountModel::getPassword), () -> password) + .supply(Select.field(SignupAccountModel::getConfirmPassword), () -> password) + .supply(Select.field(SignupAccountModel::getFirstName), () -> faker.name().firstName()) + .supply(Select.field(SignupAccountModel::getLastName), () -> faker.name().lastName()) + .toModel(); + incorrectAccountModel = Instancio.of(SignupAccountModel.class) + .supply(Select.field(SignupAccountModel::getUsername), () -> faker.name().firstName() + ".") + .supply(Select.field(SignupAccountModel::getEmail), () -> ACCOUNT_INCORRECT_EMAIL) + .supply(Select.field(SignupAccountModel::getPassword), () -> incorrectPassword) + .supply(Select.field(SignupAccountModel::getConfirmPassword), () -> incorrectPassword) + .supply(Select.field(SignupAccountModel::getFirstName), () -> "") + .supply(Select.field(SignupAccountModel::getLastName), () -> "") + .toModel(); + } +} diff --git a/src/test/java/io/hexlet/typoreporter/utils/BundleSourceUtils.java b/src/test/java/io/hexlet/typoreporter/utils/BundleSourceUtils.java new file mode 100644 index 00000000..bcbf0e50 --- /dev/null +++ b/src/test/java/io/hexlet/typoreporter/utils/BundleSourceUtils.java @@ -0,0 +1,75 @@ +package io.hexlet.typoreporter.utils; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.stereotype.Component; + +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Component +public class BundleSourceUtils { + @Autowired + private MessageSource messageSource; + + /** + * @param key - Ключ из messages_LANG.properties, по которому необходимо получить значение + * Для RU необходимо использовать new Locale("ru") + * @param locale - Параметр, по которому определяется язык (локализация) + * @param isReplaceToQuot - Признак необходимости замены " на " + * @param args - Аргументы с типом String, которые необходимо подставить при интерполяции + * @return Строка из resource bundle изменениями + */ + public String getValueByKey(String key, Locale locale, boolean isReplaceToQuot, String[] args) { + String value = messageSource.getMessage(key, null, locale); + if (args != null) { + Pattern pattern = Pattern.compile("(\\$\\{(\\w+)\\})|(\\{(\\w+)\\})"); + Matcher matcher = pattern.matcher(value); + while (matcher.find()) { + value = value.replace(matcher.group(), "%s"); + } + value = isReplaceToQuot + ? value.replace("\"", """) + : value; + return String.format(value, args); + } + return isReplaceToQuot ? value.replace("\"", """) : value; + } + + /** + * @param key - Ключ из messages_LANG.properties, по которому необходимо получить значение + * @param isReplaceToQuot - Признак необходимости замены " на " + * @param args - Аргументы с типом String, которые необходимо подставить при интерполяции + * @return - Строка из resource bundle изменениями, по умолчанию на английском языке + */ + public String getValueByKey(String key, boolean isReplaceToQuot, String[] args) { + return getValueByKey(key, Locale.ENGLISH, isReplaceToQuot, args); + } + + /** + * @param key - Ключ из messages_LANGUAGE.properties, по которому необходимо получить значение. + * @param isReplaceToQuot - Признак необходимости замены " на " + * @return - Строка из resource bundle изменениями, по умолчанию на английском языке + */ + public String getValueByKey(String key, boolean isReplaceToQuot) { + return getValueByKey(key, isReplaceToQuot, null); + } + + /** + * @param key - Ключ из messages_LANG.properties, по которому необходимо получить значение + * @param args - Аргументы с типом String, которые необходимо подставить при интерполяции + * @return - Строка из resource bundle, по умолчанию на английском языке + */ + public String getValueByKey(String key, String[] args) { + return getValueByKey(key, false, args); + } + + /** + * @param key - Ключ из messages_LANG.properties, по которому необходимо получить значение + * @return - Строка из resource bundle, по умолчанию на английском языке + */ + public String getValueByKey(String key) { + return getValueByKey(key, null); + } +} diff --git a/src/test/java/io/hexlet/typoreporter/utils/ModelUtils.java b/src/test/java/io/hexlet/typoreporter/utils/ModelUtils.java new file mode 100644 index 00000000..98ee2042 --- /dev/null +++ b/src/test/java/io/hexlet/typoreporter/utils/ModelUtils.java @@ -0,0 +1,32 @@ +package io.hexlet.typoreporter.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import java.util.Map; +import java.util.Set; + +@Component +public class ModelUtils { + @Autowired + private ObjectMapper objectMapper; + + public MultiValueMap toFormParams(Object dto) throws Exception { + return toFormParams(dto, Set.of()); + } + + public MultiValueMap toFormParams(Object dto, Set excludeFields) throws Exception { + ObjectReader reader = objectMapper.readerFor(Map.class); + Map map = reader.readValue(objectMapper.writeValueAsString(dto)); + + MultiValueMap multiValueMap = new LinkedMultiValueMap<>(); + map.entrySet().stream() + .filter(e -> !excludeFields.contains(e.getKey())) + .forEach(e -> multiValueMap.add(e.getKey(), (e.getValue() == null ? "" : e.getValue()))); + return multiValueMap; + } +} diff --git a/src/test/java/io/hexlet/typoreporter/web/AccountControllerIT.java b/src/test/java/io/hexlet/typoreporter/web/AccountControllerIT.java index e2773fd5..2e91ee63 100644 --- a/src/test/java/io/hexlet/typoreporter/web/AccountControllerIT.java +++ b/src/test/java/io/hexlet/typoreporter/web/AccountControllerIT.java @@ -4,10 +4,16 @@ import com.github.database.rider.spring.api.DBRider; import io.hexlet.typoreporter.repository.AccountRepository; import io.hexlet.typoreporter.test.DBUnitEnumPostgres; +import io.hexlet.typoreporter.test.factory.AccountModelGenerator; +import io.hexlet.typoreporter.utils.BundleSourceUtils; +import io.hexlet.typoreporter.utils.ModelUtils; +import io.hexlet.typoreporter.web.model.SignupAccountModel; +import org.instancio.Instancio; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.web.servlet.MockMvc; @@ -51,68 +57,69 @@ static void dataSourceProperties(DynamicPropertyRegistry registry) { @Autowired private AccountRepository accountRepository; + @Autowired + private AccountModelGenerator accountGenerator; + + @Autowired + private ModelUtils modelUtils; + @Autowired + private BundleSourceUtils bundleSourceUtils; + @Test void updateAccountWithWrongEmailDomain() throws Exception { - String userName = "testUser"; - String correctEmailDomain = "test@test.test"; - String password = "_Qwe1234"; + SignupAccountModel correctAccount = Instancio.of(accountGenerator.getCorrectAccountModel()).create(); + var correctFormParams = modelUtils.toFormParams(correctAccount); mockMvc.perform(post("/signup") - .param("username", userName) - .param("email", correctEmailDomain) - .param("password", password) - .param("confirmPassword", password) - .param("firstName", userName) - .param("lastName", userName) - .with(csrf())); - assertThat(accountRepository.findAccountByEmail(correctEmailDomain)).isNotEmpty(); - assertThat(accountRepository.findAccountByEmail(correctEmailDomain).orElseThrow().getEmail()) - .isEqualTo(correctEmailDomain); - - String wrongEmailDomain = "test@test"; + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .params(correctFormParams) + .with(csrf())) + .andExpect(status().isFound()); + assertThat(accountRepository.findAccountByEmail(correctAccount.getEmail())).isNotEmpty(); + assertThat(accountRepository.findAccountByEmail(correctAccount.getEmail()).orElseThrow().getEmail()) + .isEqualTo(correctAccount.getEmail()); + SignupAccountModel incorrectAccount = Instancio.of(accountGenerator.getIncorrectAccountModel()).create(); + var incorrectFormParams = modelUtils.toFormParams(incorrectAccount); var response = mockMvc.perform(put("/account/update") - .param("username", userName) - .param("email", wrongEmailDomain) - .param("firstName", userName) - .param("lastName", userName) - .with(user(correctEmailDomain)) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .params(incorrectFormParams) + .with(user(correctAccount.getEmail())) .with(csrf())) .andExpect(status().isOk()) .andReturn(); + var body = response.getResponse().getContentAsString(); - assertThat(accountRepository.findAccountByEmail(wrongEmailDomain)).isEmpty(); - assertThat(accountRepository.findAccountByEmail(correctEmailDomain).orElseThrow().getEmail()) - .isEqualTo(correctEmailDomain); - assertThat(body).contains(String.format("The email "%s" is not valid", wrongEmailDomain)); + assertThat(accountRepository.findAccountByEmail(incorrectAccount.getEmail())).isEmpty(); + assertThat(accountRepository.findAccountByEmail(correctAccount.getEmail()).orElseThrow().getEmail()) + .isEqualTo(correctAccount.getEmail()); + assertThat(body).contains(bundleSourceUtils.getValueByKey( + "validation.alert.wrong-email", true, new String[]{incorrectAccount.getEmail()})); + assertThat(body).contains(bundleSourceUtils.getValueByKey( + "validation.alert.wrong-username-pattern", true, new String[]{"^[-_A-Za-z0-9]*$"})); } @Test void updateAccountEmailUsingDifferentCase() throws Exception { - final String username = "testUser"; - final String emailUpperCase = "TEST@TEST.RU"; - final String emailMixedCase = "TEST@test.Ru"; - final String emailLowerCase = "test@test.ru"; - final String password = "_Qwe1234"; - + final SignupAccountModel account = Instancio.of(accountGenerator.getCorrectAccountModel()).create(); + account.setEmail(account.getEmail().toUpperCase()); + var accountFormParams = modelUtils.toFormParams(account); mockMvc.perform(post("/signup") - .param("username", username) - .param("email", emailMixedCase) - .param("password", password) - .param("confirmPassword", password) - .param("firstName", username) - .param("lastName", username) - .with(csrf())); - assertThat(accountRepository.findAccountByEmail(emailLowerCase)).isNotEmpty(); + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .params(accountFormParams) + .with(csrf())) + .andExpect(status().isFound()); + assertThat(accountRepository.findAccountByEmail(account.getEmail().toLowerCase())).isNotEmpty(); + final SignupAccountModel accountToUpdate = Instancio.of(accountGenerator.getCorrectAccountModel()).create(); + accountToUpdate.setEmail(account.getEmail().toLowerCase()); + accountFormParams = modelUtils.toFormParams(accountToUpdate); mockMvc.perform(put("/account/update") - .param("firstName", username) - .param("lastName", username) - .param("username", username) - .param("email", emailUpperCase) - .with(user(emailLowerCase)) - .with(csrf())) - .andExpect(status().isFound()); - assertThat(accountRepository.findAccountByEmail(emailUpperCase)).isEmpty(); - assertThat(accountRepository.findAccountByEmail(emailLowerCase)).isNotEmpty(); + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .params(accountFormParams) + .with(user(account.getEmail())) + .with(csrf())) + .andExpect(status().isOk()); + assertThat(accountRepository.findAccountByEmail(accountToUpdate.getEmail().toUpperCase())).isEmpty(); + assertThat(accountRepository.findAccountByEmail(accountToUpdate.getEmail())).isNotEmpty(); } }