getWithMeals(@AuthenticationPrincipal AuthUser authUser) {
+ return super.getWithMeals(authUser.id());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/topjava/user/web/UniqueMailValidator.java b/src/main/java/ru/javaops/topjava/user/web/UniqueMailValidator.java
new file mode 100644
index 000000000..6d09f9325
--- /dev/null
+++ b/src/main/java/ru/javaops/topjava/user/web/UniqueMailValidator.java
@@ -0,0 +1,48 @@
+package ru.javaops.topjava.user.web;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.AllArgsConstructor;
+import org.jspecify.annotations.NonNull;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.validation.Errors;
+import ru.javaops.topjava.app.AuthUtil;
+import ru.javaops.topjava.common.HasIdAndEmail;
+import ru.javaops.topjava.user.repository.UserRepository;
+
+@Component
+@AllArgsConstructor
+public class UniqueMailValidator implements org.springframework.validation.Validator {
+ public static final String EXCEPTION_DUPLICATE_EMAIL = "User with this email already exists";
+
+ private final UserRepository repository;
+ private final HttpServletRequest request;
+
+ @Override
+ public boolean supports(@NonNull Class> clazz) {
+ return HasIdAndEmail.class.isAssignableFrom(clazz);
+ }
+
+ @Override
+ public void validate(@NonNull Object target, @NonNull Errors errors) {
+ HasIdAndEmail user = ((HasIdAndEmail) target);
+ if (StringUtils.hasText(user.getEmail())) {
+ repository.findByEmailIgnoreCase(user.getEmail())
+ .ifPresent(dbUser -> {
+ if (request.getMethod().equals("PUT")) { // UPDATE
+ int dbId = dbUser.id();
+
+ // it is ok, if update ourselves
+ if (user.getId() != null && dbId == user.id()) return;
+
+ // Workaround for update with user.id=null in request body
+ // ValidationUtil.assureIdConsistent called after this validation
+ String requestURI = request.getRequestURI();
+ if (requestURI.endsWith("/" + dbId) || (dbId == AuthUtil.get().id() && requestURI.contains("/profile")))
+ return;
+ }
+ errors.rejectValue("email", "", EXCEPTION_DUPLICATE_EMAIL);
+ });
+ }
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/Main.java b/src/main/java/ru/javawebinar/topjava/Main.java
deleted file mode 100644
index 723742bac..000000000
--- a/src/main/java/ru/javawebinar/topjava/Main.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package ru.javawebinar.topjava;
-
-/**
- * @see Demo application
- * @see Initial project
- */
-public class Main {
- public static void main(String[] args) {
- System.out.format("Hello TopJava Enterprise!");
- }
-}
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
new file mode 100644
index 000000000..8c069ba4f
--- /dev/null
+++ b/src/main/resources/application.yaml
@@ -0,0 +1,48 @@
+# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
+spring:
+ jpa:
+ show-sql: true
+ open-in-view: false
+ # https://stackoverflow.com/a/67678945/548473
+ defer-datasource-initialization: true
+ hibernate:
+ ddl-auto: create
+ properties:
+ # http://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#configurations
+ hibernate:
+ format_sql: true
+ default_batch_fetch_size: 20
+ # https://stackoverflow.com/questions/21257819/what-is-the-difference-between-hibernate-jdbc-fetch-size-and-hibernate-jdbc-batc
+ jdbc.batch_size: 20
+ datasource:
+ # ImMemory
+ url: jdbc:h2:mem:topjava
+ # tcp: jdbc:h2:tcp://localhost:9092/mem:topjava
+ # Absolute path
+ # url: jdbc:h2:C:/projects/bootjava/db/topjava
+ # tcp: jdbc:h2:tcp://localhost:9092/C:/projects/bootjava/db/topjava
+ # Relative path form current dir
+ # url: jdbc:h2:./db/topjava
+ # Relative path from home
+ # url: jdbc:h2:~/topjava
+ # tcp: jdbc:h2:tcp://localhost:9092/~/topjava
+ username: sa
+ password:
+
+ # https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#common-application-properties-cache
+ cache:
+# cache-names: users
+# caffeine.spec: maximumSize=5000,expireAfterAccess=60s
+ servlet:
+ encoding:
+ charset: UTF-8
+ enabled: true
+ force: true
+
+logging:
+ level:
+ root: WARN
+ ru.javaops.topjava: DEBUG
+ org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver: DEBUG
+
+springdoc.swagger-ui.path: /
diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql
new file mode 100644
index 000000000..84e67bb9e
--- /dev/null
+++ b/src/main/resources/data.sql
@@ -0,0 +1,20 @@
+INSERT INTO USERS (name, email, password, calories_per_day)
+VALUES ('User', 'user@yandex.ru', '{noop}password', 2005),
+ ('Admin', 'admin@gmail.com', '{noop}admin', 1900),
+ ('Guest', 'guest@gmail.com', '{noop}guest', 2000);
+
+INSERT INTO USER_ROLE (role, user_id)
+VALUES ('USER', 1),
+ ('ADMIN', 2),
+ ('USER', 2);
+
+INSERT INTO MEAL (date_time, description, calories, user_id)
+VALUES ('2020-01-30 10:00:00', 'Завтрак', 500, 1),
+ ('2020-01-30 13:00:00', 'Обед', 1000, 1),
+ ('2020-01-30 20:00:00', 'Ужин', 500, 1),
+ ('2020-01-31 0:00:00', 'Еда на граничное значение', 100, 1),
+ ('2020-01-31 10:00:00', 'Завтрак', 500, 1),
+ ('2020-01-31 13:00:00', 'Обед', 1000, 1),
+ ('2020-01-31 20:00:00', 'Ужин', 510, 1),
+ ('2020-01-31 14:00:00', 'Админ ланч', 510, 2),
+ ('2020-01-31 21:00:00', 'Админ ужин', 1500, 2);
\ No newline at end of file
diff --git a/src/test/java/ru/javaops/topjava/AbstractControllerTest.java b/src/test/java/ru/javaops/topjava/AbstractControllerTest.java
new file mode 100644
index 000000000..5dd1528ed
--- /dev/null
+++ b/src/test/java/ru/javaops/topjava/AbstractControllerTest.java
@@ -0,0 +1,26 @@
+package ru.javaops.topjava;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+import org.springframework.transaction.annotation.Transactional;
+
+//https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications
+@SpringBootTest
+@Transactional
+@AutoConfigureMockMvc
+@ActiveProfiles("test")
+//https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-mock-environment
+public abstract class AbstractControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ protected ResultActions perform(MockHttpServletRequestBuilder builder) throws Exception {
+ return mockMvc.perform(builder);
+ }
+}
diff --git a/src/test/java/ru/javaops/topjava/MatcherFactory.java b/src/test/java/ru/javaops/topjava/MatcherFactory.java
new file mode 100644
index 000000000..de8ee9d1a
--- /dev/null
+++ b/src/test/java/ru/javaops/topjava/MatcherFactory.java
@@ -0,0 +1,83 @@
+package ru.javaops.topjava;
+
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.ResultMatcher;
+import ru.javaops.topjava.common.util.JsonUtil;
+
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+import java.util.function.BiConsumer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Factory for creating test matchers.
+ *
+ * Comparing actual and expected objects via AssertJ
+ * Support converting json MvcResult to objects for comparation.
+ */
+public class MatcherFactory {
+
+ public static Matcher usingAssertions(Class clazz, BiConsumer assertion, BiConsumer, Iterable> iterableAssertion) {
+ return new Matcher<>(clazz, assertion, iterableAssertion);
+ }
+
+ public static Matcher usingEqualsComparator(Class clazz) {
+ return usingAssertions(clazz,
+ (a, e) -> assertThat(a).isEqualTo(e),
+ (a, e) -> assertThat(a).isEqualTo(e));
+ }
+
+ public static Matcher usingIgnoringFieldsComparator(Class clazz, String... fieldsToIgnore) {
+ return usingAssertions(clazz,
+ (a, e) -> assertThat(a).usingRecursiveComparison().ignoringFields(fieldsToIgnore).isEqualTo(e),
+ (a, e) -> assertThat(a).usingRecursiveFieldByFieldElementComparatorIgnoringFields(fieldsToIgnore).isEqualTo(e));
+ }
+
+ public static class Matcher {
+ private final Class clazz;
+ private final BiConsumer assertion;
+ private final BiConsumer, Iterable> iterableAssertion;
+
+ private Matcher(Class clazz, BiConsumer assertion, BiConsumer, Iterable> iterableAssertion) {
+ this.clazz = clazz;
+ this.assertion = assertion;
+ this.iterableAssertion = iterableAssertion;
+ }
+
+ public void assertMatch(T actual, T expected) {
+ assertion.accept(actual, expected);
+ }
+
+ @SafeVarargs
+ public final void assertMatch(Iterable actual, T... expected) {
+ assertMatch(actual, List.of(expected));
+ }
+
+ public void assertMatch(Iterable actual, Iterable expected) {
+ iterableAssertion.accept(actual, expected);
+ }
+
+ public ResultMatcher contentJson(T expected) {
+ return result -> assertMatch(JsonUtil.readValue(getContent(result), clazz), expected);
+ }
+
+ @SafeVarargs
+ public final ResultMatcher contentJson(T... expected) {
+ return contentJson(List.of(expected));
+ }
+
+ public ResultMatcher contentJson(Iterable expected) {
+ return result -> assertMatch(JsonUtil.readValues(getContent(result), clazz), expected);
+ }
+
+ public T readFromJson(ResultActions action) throws UnsupportedEncodingException {
+ return JsonUtil.readValue(getContent(action.andReturn()), clazz);
+ }
+
+ private static String getContent(MvcResult result) throws UnsupportedEncodingException {
+ return result.getResponse().getContentAsString();
+ }
+ }
+}
diff --git a/src/test/java/ru/javaops/topjava/user/MealTestData.java b/src/test/java/ru/javaops/topjava/user/MealTestData.java
new file mode 100644
index 000000000..36ba2d773
--- /dev/null
+++ b/src/test/java/ru/javaops/topjava/user/MealTestData.java
@@ -0,0 +1,35 @@
+package ru.javaops.topjava.user;
+
+import ru.javaops.topjava.MatcherFactory;
+import ru.javaops.topjava.user.model.Meal;
+import ru.javaops.topjava.user.to.MealTo;
+
+import java.time.Month;
+import java.util.List;
+
+import static java.time.LocalDateTime.of;
+
+public class MealTestData {
+ public static final MatcherFactory.Matcher MEAL_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(Meal.class, "user");
+ public static final int MEAL1_ID = 1;
+ public static final int ADMIN_MEAL_ID = 8;
+ public static final Meal meal1 = new Meal(MEAL1_ID, of(2020, Month.JANUARY, 30, 10, 0), "Завтрак", 500);
+ public static final Meal meal2 = new Meal(MEAL1_ID + 1, of(2020, Month.JANUARY, 30, 13, 0), "Обед", 1000);
+ public static final Meal meal3 = new Meal(MEAL1_ID + 2, of(2020, Month.JANUARY, 30, 20, 0), "Ужин", 500);
+ public static final Meal meal4 = new Meal(MEAL1_ID + 3, of(2020, Month.JANUARY, 31, 0, 0), "Еда на граничное значение", 100);
+ public static final Meal meal5 = new Meal(MEAL1_ID + 4, of(2020, Month.JANUARY, 31, 10, 0), "Завтрак", 500);
+ public static final Meal meal6 = new Meal(MEAL1_ID + 5, of(2020, Month.JANUARY, 31, 13, 0), "Обед", 1000);
+ public static final Meal meal7 = new Meal(MEAL1_ID + 6, of(2020, Month.JANUARY, 31, 20, 0), "Ужин", 510);
+ public static final List meals = List.of(meal7, meal6, meal5, meal4, meal3, meal2, meal1);
+ public static final Meal adminMeal1 = new Meal(ADMIN_MEAL_ID, of(2020, Month.JANUARY, 31, 14, 0), "Админ ланч", 510);
+ public static final Meal adminMeal2 = new Meal(ADMIN_MEAL_ID + 1, of(2020, Month.JANUARY, 31, 21, 0), "Админ ужин", 1500);
+ public static MatcherFactory.Matcher MEAL_TO_MATCHER = MatcherFactory.usingEqualsComparator(MealTo.class);
+
+ public static Meal getNew() {
+ return new Meal(null, of(2020, Month.FEBRUARY, 1, 18, 0), "Созданный ужин", 300);
+ }
+
+ public static Meal getUpdated() {
+ return new Meal(MEAL1_ID, meal1.getDateTime().plusMinutes(2), "Обновленный завтрак", 200);
+ }
+}
diff --git a/src/test/java/ru/javaops/topjava/user/UserTestData.java b/src/test/java/ru/javaops/topjava/user/UserTestData.java
new file mode 100644
index 000000000..a195a053e
--- /dev/null
+++ b/src/test/java/ru/javaops/topjava/user/UserTestData.java
@@ -0,0 +1,54 @@
+package ru.javaops.topjava.user;
+
+import ru.javaops.topjava.MatcherFactory;
+import ru.javaops.topjava.common.util.JsonUtil;
+import ru.javaops.topjava.user.model.Role;
+import ru.javaops.topjava.user.model.User;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static ru.javaops.topjava.user.MealTestData.*;
+
+public class UserTestData {
+ public static final MatcherFactory.Matcher USER_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(User.class, "registered", "meals", "password");
+ public static MatcherFactory.Matcher USER_WITH_MEALS_MATCHER =
+ MatcherFactory.usingAssertions(User.class,
+ // No need use ignoringAllOverriddenEquals, see https://assertj.github.io/doc/#breaking-changes
+ (a, e) -> assertThat(a).usingRecursiveComparison()
+ .ignoringFields("registered", "meals.user", "password").isEqualTo(e),
+ (a, e) -> {
+ throw new UnsupportedOperationException();
+ });
+
+ public static final int USER_ID = 1;
+ public static final int ADMIN_ID = 2;
+ public static final int GUEST_ID = 3;
+ public static final int NOT_FOUND = 100;
+ public static final String USER_MAIL = "user@yandex.ru";
+ public static final String ADMIN_MAIL = "admin@gmail.com";
+ public static final String GUEST_MAIL = "guest@gmail.com";
+
+ public static final User user = new User(USER_ID, "User", USER_MAIL, "password", 2005, Role.USER);
+ public static final User admin = new User(ADMIN_ID, "Admin", ADMIN_MAIL, "admin", 1900, Role.ADMIN, Role.USER);
+ public static final User guest = new User(GUEST_ID, "Guest", GUEST_MAIL, "guest", 2000);
+
+ static {
+ user.setMeals(meals);
+ admin.setMeals(List.of(adminMeal2, adminMeal1));
+ }
+
+ public static User getNew() {
+ return new User(null, "New", "new@gmail.com", "newPass", 1555, false, new Date(), Collections.singleton(Role.USER));
+ }
+
+ public static User getUpdated() {
+ return new User(USER_ID, "UpdatedName", USER_MAIL, "newPass", 330, false, new Date(), List.of(Role.ADMIN));
+ }
+
+ public static String jsonWithPassword(User user, String passw) {
+ return JsonUtil.writeAdditionProps(user, "password", passw);
+ }
+}
diff --git a/src/test/java/ru/javaops/topjava/user/web/AdminUserControllerTest.java b/src/test/java/ru/javaops/topjava/user/web/AdminUserControllerTest.java
new file mode 100644
index 000000000..e4beacc98
--- /dev/null
+++ b/src/test/java/ru/javaops/topjava/user/web/AdminUserControllerTest.java
@@ -0,0 +1,219 @@
+package ru.javaops.topjava.user.web;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.security.test.context.support.WithUserDetails;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import ru.javaops.topjava.AbstractControllerTest;
+import ru.javaops.topjava.user.model.Role;
+import ru.javaops.topjava.user.model.User;
+import ru.javaops.topjava.user.repository.UserRepository;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static ru.javaops.topjava.user.UserTestData.*;
+import static ru.javaops.topjava.user.web.AdminUserController.REST_URL;
+import static ru.javaops.topjava.user.web.UniqueMailValidator.EXCEPTION_DUPLICATE_EMAIL;
+
+class AdminUserControllerTest extends AbstractControllerTest {
+
+ private static final String REST_URL_SLASH = REST_URL + '/';
+
+ @Autowired
+ private UserRepository repository;
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void get() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL_SLASH + ADMIN_ID))
+ .andExpect(status().isOk())
+ .andDo(print())
+ // https://jira.spring.io/browse/SPR-14472
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(USER_MATCHER.contentJson(admin));
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void getNotFound() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL_SLASH + NOT_FOUND))
+ .andDo(print())
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void getByEmail() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL_SLASH + "by-email?email=" + admin.getEmail()))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(USER_MATCHER.contentJson(admin));
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void delete() throws Exception {
+ perform(MockMvcRequestBuilders.delete(REST_URL_SLASH + USER_ID))
+ .andDo(print())
+ .andExpect(status().isNoContent());
+ assertFalse(repository.findById(USER_ID).isPresent());
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void deleteNotFound() throws Exception {
+ perform(MockMvcRequestBuilders.delete(REST_URL_SLASH + NOT_FOUND))
+ .andDo(print())
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void enableNotFound() throws Exception {
+ perform(MockMvcRequestBuilders.patch(REST_URL_SLASH + NOT_FOUND)
+ .param("enabled", "false")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andDo(print())
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ void getUnAuth() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL))
+ .andDo(print())
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void getForbidden() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL))
+ .andExpect(status().isForbidden());
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void update() throws Exception {
+ User updated = getUpdated();
+ updated.setId(null);
+ perform(MockMvcRequestBuilders.put(REST_URL_SLASH + USER_ID)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(jsonWithPassword(updated, "newPass")))
+ .andDo(print())
+ .andExpect(status().isNoContent());
+
+ USER_MATCHER.assertMatch(repository.getExisted(USER_ID), getUpdated());
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void createWithLocation() throws Exception {
+ User newUser = getNew();
+ ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(jsonWithPassword(newUser, "newPass")))
+ .andExpect(status().isCreated());
+
+ User created = USER_MATCHER.readFromJson(action);
+ int newId = created.id();
+ newUser.setId(newId);
+ USER_MATCHER.assertMatch(created, newUser);
+ USER_MATCHER.assertMatch(repository.getExisted(newId), newUser);
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void getAll() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(USER_MATCHER.contentJson(admin, guest, user));
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void enable() throws Exception {
+ perform(MockMvcRequestBuilders.patch(REST_URL_SLASH + USER_ID)
+ .param("enabled", "false")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andDo(print())
+ .andExpect(status().isNoContent());
+
+ assertFalse(repository.getExisted(USER_ID).isEnabled());
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void getWithMeals() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL_SLASH + ADMIN_ID + "/with-meals"))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(USER_WITH_MEALS_MATCHER.contentJson(admin));
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void createInvalid() throws Exception {
+ User invalid = new User(null, null, "", "newPass", 7300, Role.USER, Role.ADMIN);
+ perform(MockMvcRequestBuilders.post(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(jsonWithPassword(invalid, "newPass")))
+ .andDo(print())
+ .andExpect(status().isUnprocessableContent());
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void updateInvalid() throws Exception {
+ User invalid = new User(user);
+ invalid.setName("");
+ perform(MockMvcRequestBuilders.put(REST_URL_SLASH + USER_ID)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(jsonWithPassword(invalid, "password")))
+ .andDo(print())
+ .andExpect(status().isUnprocessableContent());
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void updateHtmlUnsafe() throws Exception {
+ User updated = new User(user);
+ updated.setName("");
+ perform(MockMvcRequestBuilders.put(REST_URL_SLASH + USER_ID)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(jsonWithPassword(updated, "password")))
+ .andDo(print())
+ .andExpect(status().isUnprocessableContent());
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void updateDuplicate() throws Exception {
+ User updated = new User(user);
+ updated.setEmail(ADMIN_MAIL);
+ perform(MockMvcRequestBuilders.put(REST_URL_SLASH + USER_ID)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(jsonWithPassword(updated, "password")))
+ .andDo(print())
+ .andExpect(status().isUnprocessableContent())
+ .andExpect(content().string(containsString(EXCEPTION_DUPLICATE_EMAIL)));
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void createDuplicate() throws Exception {
+ User expected = new User(null, "New", USER_MAIL, "newPass", 2300, Role.USER, Role.ADMIN);
+ perform(MockMvcRequestBuilders.post(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(jsonWithPassword(expected, "newPass")))
+ .andDo(print())
+ .andExpect(status().isUnprocessableContent())
+ .andExpect(content().string(containsString(EXCEPTION_DUPLICATE_EMAIL)));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javaops/topjava/user/web/MealControllerTest.java b/src/test/java/ru/javaops/topjava/user/web/MealControllerTest.java
new file mode 100644
index 000000000..dd1c49f75
--- /dev/null
+++ b/src/test/java/ru/javaops/topjava/user/web/MealControllerTest.java
@@ -0,0 +1,188 @@
+package ru.javaops.topjava.user.web;
+
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.security.test.context.support.WithUserDetails;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+import ru.javaops.topjava.AbstractControllerTest;
+import ru.javaops.topjava.common.util.JsonUtil;
+import ru.javaops.topjava.user.UserTestData;
+import ru.javaops.topjava.user.model.Meal;
+import ru.javaops.topjava.user.repository.MealRepository;
+
+import java.time.LocalDateTime;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static ru.javaops.topjava.user.MealTestData.*;
+import static ru.javaops.topjava.user.UserTestData.ADMIN_MAIL;
+import static ru.javaops.topjava.user.UserTestData.USER_MAIL;
+import static ru.javaops.topjava.user.util.MealsUtil.createTo;
+import static ru.javaops.topjava.user.util.MealsUtil.getTos;
+import static ru.javaops.topjava.user.web.MealController.REST_URL;
+
+class MealControllerTest extends AbstractControllerTest {
+
+ private static final String REST_URL_SLASH = REST_URL + '/';
+
+ @Autowired
+ private MealRepository mealRepository;
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void get() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL_SLASH + MEAL1_ID))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(MEAL_MATCHER.contentJson(meal1));
+ }
+
+ @Test
+ void getUnauth() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL_SLASH + MEAL1_ID))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void getNotFound() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL_SLASH + ADMIN_MEAL_ID))
+ .andDo(print())
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void delete() throws Exception {
+ perform(MockMvcRequestBuilders.delete(REST_URL_SLASH + MEAL1_ID))
+ .andExpect(status().isNoContent());
+ assertFalse(mealRepository.get(UserTestData.USER_ID, MEAL1_ID).isPresent());
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void deleteDataConflict() throws Exception {
+ perform(MockMvcRequestBuilders.delete(REST_URL_SLASH + ADMIN_MEAL_ID))
+ .andExpect(status().isConflict());
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void update() throws Exception {
+ Meal updated = getUpdated();
+ perform(MockMvcRequestBuilders.put(REST_URL_SLASH + MEAL1_ID)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(updated)))
+ .andExpect(status().isNoContent());
+
+ MEAL_MATCHER.assertMatch(mealRepository.getExisted(MEAL1_ID), updated);
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void createWithLocation() throws Exception {
+ Meal newMeal = getNew();
+ ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(newMeal)));
+
+ Meal created = MEAL_MATCHER.readFromJson(action);
+ int newId = created.id();
+ newMeal.setId(newId);
+ MEAL_MATCHER.assertMatch(created, newMeal);
+ MEAL_MATCHER.assertMatch(mealRepository.getExisted(newId), newMeal);
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void getAll() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(MEAL_TO_MATCHER.contentJson(getTos(meals, UserTestData.user.getCaloriesPerDay())));
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void getBetween() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL_SLASH + "filter")
+ .param("startDate", "2020-01-30").param("startTime", "07:00")
+ .param("endDate", "2020-01-31").param("endTime", "11:00"))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andExpect(MEAL_TO_MATCHER.contentJson(createTo(meal5, true), createTo(meal1, false)));
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void getBetweenAll() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL_SLASH + "filter?startDate=&endTime="))
+ .andExpect(status().isOk())
+ .andExpect(MEAL_TO_MATCHER.contentJson(getTos(meals, UserTestData.user.getCaloriesPerDay())));
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void createInvalid() throws Exception {
+ Meal invalid = new Meal(null, null, "Dummy", 200);
+ perform(MockMvcRequestBuilders.post(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(invalid)))
+ .andDo(print())
+ .andExpect(status().isUnprocessableContent());
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void updateInvalid() throws Exception {
+ Meal invalid = new Meal(MEAL1_ID, null, null, 6000);
+ perform(MockMvcRequestBuilders.put(REST_URL_SLASH + MEAL1_ID)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(invalid)))
+ .andDo(print())
+ .andExpect(status().isUnprocessableContent());
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void updateHtmlUnsafe() throws Exception {
+ Meal invalid = new Meal(MEAL1_ID, LocalDateTime.now(), "", 200);
+ perform(MockMvcRequestBuilders.put(REST_URL_SLASH + MEAL1_ID)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(invalid)))
+ .andDo(print())
+ .andExpect(status().isUnprocessableContent());
+ }
+
+ @Test
+ @Transactional(propagation = Propagation.NEVER)
+ @WithUserDetails(value = USER_MAIL)
+ void updateDuplicate() throws Exception {
+ Meal invalid = new Meal(MEAL1_ID, meal2.getDateTime(), "Dummy", 200);
+ perform(MockMvcRequestBuilders.put(REST_URL_SLASH + MEAL1_ID)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(invalid)))
+ .andDo(print())
+ .andExpect(status().isConflict());
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void createDuplicate() throws Exception {
+ Meal invalid = new Meal(null, adminMeal1.getDateTime(), "Dummy", 200);
+ perform(MockMvcRequestBuilders.post(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(invalid)))
+ .andDo(print())
+ .andExpect(status().isConflict());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javaops/topjava/user/web/ProfileControllerTest.java b/src/test/java/ru/javaops/topjava/user/web/ProfileControllerTest.java
new file mode 100644
index 000000000..a105bd70c
--- /dev/null
+++ b/src/test/java/ru/javaops/topjava/user/web/ProfileControllerTest.java
@@ -0,0 +1,121 @@
+package ru.javaops.topjava.user.web;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.security.test.context.support.WithUserDetails;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import ru.javaops.topjava.AbstractControllerTest;
+import ru.javaops.topjava.common.util.JsonUtil;
+import ru.javaops.topjava.user.model.User;
+import ru.javaops.topjava.user.repository.UserRepository;
+import ru.javaops.topjava.user.to.UserTo;
+import ru.javaops.topjava.user.util.UsersUtil;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static ru.javaops.topjava.user.UserTestData.*;
+import static ru.javaops.topjava.user.web.ProfileController.REST_URL;
+
+class ProfileControllerTest extends AbstractControllerTest {
+
+ @Autowired
+ private UserRepository repository;
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void get() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(USER_MATCHER.contentJson(user));
+ }
+
+ @Test
+ void getUnAuth() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void delete() throws Exception {
+ perform(MockMvcRequestBuilders.delete(REST_URL))
+ .andExpect(status().isNoContent());
+ USER_MATCHER.assertMatch(repository.findAll(), admin, guest);
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void getWithMeals() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL + "/with-meals"))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(USER_WITH_MEALS_MATCHER.contentJson(user));
+ }
+
+ @Test
+ void register() throws Exception {
+ UserTo newTo = new UserTo(null, "newName", "newemail@ya.ru", "newPassword", 1500);
+ User newUser = UsersUtil.createNewFromTo(newTo);
+ ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(newTo)))
+ .andDo(print())
+ .andExpect(status().isCreated());
+
+ User created = USER_MATCHER.readFromJson(action);
+ int newId = created.id();
+ newUser.setId(newId);
+ USER_MATCHER.assertMatch(created, newUser);
+ USER_MATCHER.assertMatch(repository.getExisted(newId), newUser);
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void update() throws Exception {
+ UserTo updatedTo = new UserTo(null, "newName", USER_MAIL, "newPassword", 1500);
+ perform(MockMvcRequestBuilders.put(REST_URL).contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(updatedTo)))
+ .andDo(print())
+ .andExpect(status().isNoContent());
+
+ USER_MATCHER.assertMatch(repository.getExisted(USER_ID), UsersUtil.updateFromTo(new User(user), updatedTo));
+ }
+
+ @Test
+ void registerInvalid() throws Exception {
+ UserTo newTo = new UserTo(null, null, null, null, 1);
+ perform(MockMvcRequestBuilders.post(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(newTo)))
+ .andDo(print())
+ .andExpect(status().isUnprocessableContent());
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void updateInvalid() throws Exception {
+ UserTo updatedTo = new UserTo(null, null, "password", null, 1);
+ perform(MockMvcRequestBuilders.put(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(updatedTo)))
+ .andDo(print())
+ .andExpect(status().isUnprocessableContent());
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void updateDuplicate() throws Exception {
+ UserTo updatedTo = new UserTo(null, "newName", ADMIN_MAIL, "newPassword", 1500);
+ perform(MockMvcRequestBuilders.put(REST_URL).contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(updatedTo)))
+ .andDo(print())
+ .andExpect(status().isUnprocessableContent())
+ .andExpect(content().string(containsString(UniqueMailValidator.EXCEPTION_DUPLICATE_EMAIL)));
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml
new file mode 100644
index 000000000..be1663221
--- /dev/null
+++ b/src/test/resources/application-test.yaml
@@ -0,0 +1 @@
+spring.cache.type: none
\ No newline at end of file