From d5856d6bf18858eee3a4187268ee01d9a208691f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Tue, 12 Aug 2025 22:27:15 +0300 Subject: [PATCH 01/15] =?UTF-8?q?=D0=9D=D0=B0=D1=87=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D1=8B=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20API=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=BA=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=B3=D0=BE=D1=80=D0=B8=D1=8F=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/pom.xml | 16 ++++++++++ .../controller/CategoryAdminController.java | 29 +++++++++++++++++++ .../practicum/category/dto/CategoryDto.java | 19 ++++++++++++ .../category/dto/NewCategoryDto.java | 18 ++++++++++++ .../category/dto/mapper/CategoryMapper.java | 23 +++++++++++++++ .../ru/practicum/category/model/Category.java | 22 ++++++++++++++ .../repository/CategoryRepository.java | 8 +++++ .../category/service/CategoryService.java | 8 +++++ .../category/service/CategoryServiceImpl.java | 22 ++++++++++++++ 9 files changed, 165 insertions(+) create mode 100644 main/src/main/java/ru/practicum/category/controller/CategoryAdminController.java create mode 100644 main/src/main/java/ru/practicum/category/dto/CategoryDto.java create mode 100644 main/src/main/java/ru/practicum/category/dto/NewCategoryDto.java create mode 100644 main/src/main/java/ru/practicum/category/dto/mapper/CategoryMapper.java create mode 100644 main/src/main/java/ru/practicum/category/model/Category.java create mode 100644 main/src/main/java/ru/practicum/category/repository/CategoryRepository.java create mode 100644 main/src/main/java/ru/practicum/category/service/CategoryService.java create mode 100644 main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java diff --git a/main/pom.xml b/main/pom.xml index f2f5f44..0a8006f 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -39,5 +39,21 @@ org.springframework.boot spring-boot-starter-actuator + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-data-jpa + \ No newline at end of file diff --git a/main/src/main/java/ru/practicum/category/controller/CategoryAdminController.java b/main/src/main/java/ru/practicum/category/controller/CategoryAdminController.java new file mode 100644 index 0000000..e783d5d --- /dev/null +++ b/main/src/main/java/ru/practicum/category/controller/CategoryAdminController.java @@ -0,0 +1,29 @@ +package ru.practicum.category.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; +import ru.practicum.category.service.CategoryService; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/admin/categories") +public class CategoryAdminController { + + private final CategoryService categoryService; + + @PostMapping + public CategoryDto createCategory(@Valid @RequestBody NewCategoryDto newCategoryDto) { + log.info("POST /admin/categories"); + return categoryService.createCategory(newCategoryDto); + } + + +} diff --git a/main/src/main/java/ru/practicum/category/dto/CategoryDto.java b/main/src/main/java/ru/practicum/category/dto/CategoryDto.java new file mode 100644 index 0000000..fba4ffb --- /dev/null +++ b/main/src/main/java/ru/practicum/category/dto/CategoryDto.java @@ -0,0 +1,19 @@ +package ru.practicum.category.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class CategoryDto { + private Long id; + @NotBlank(message = "Название категории не может быть пустым") + @Size(min = 1, max = 50, message = "Название категории должно быть в границах от {min} до {max} символов") + private String name; +} diff --git a/main/src/main/java/ru/practicum/category/dto/NewCategoryDto.java b/main/src/main/java/ru/practicum/category/dto/NewCategoryDto.java new file mode 100644 index 0000000..5a768c8 --- /dev/null +++ b/main/src/main/java/ru/practicum/category/dto/NewCategoryDto.java @@ -0,0 +1,18 @@ +package ru.practicum.category.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class NewCategoryDto { + @NotBlank(message = "Название категории не может быть пустым") + @Size(min = 1, max = 50, message = "Название категории должно быть в границах от {min} до {max} символов") + private String name; +} diff --git a/main/src/main/java/ru/practicum/category/dto/mapper/CategoryMapper.java b/main/src/main/java/ru/practicum/category/dto/mapper/CategoryMapper.java new file mode 100644 index 0000000..d9b571c --- /dev/null +++ b/main/src/main/java/ru/practicum/category/dto/mapper/CategoryMapper.java @@ -0,0 +1,23 @@ +package ru.practicum.category.dto.mapper; + +import lombok.experimental.UtilityClass; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; +import ru.practicum.category.model.Category; + +@UtilityClass +public class CategoryMapper { + public static CategoryDto toCategoryDto(Category category) { + return new CategoryDto( + category.getId(), + category.getName() + ); + } + + public static Category toCategory(NewCategoryDto newCategoryDto) { + return new Category( + null, + newCategoryDto.getName() + ); + } +} diff --git a/main/src/main/java/ru/practicum/category/model/Category.java b/main/src/main/java/ru/practicum/category/model/Category.java new file mode 100644 index 0000000..f8841dc --- /dev/null +++ b/main/src/main/java/ru/practicum/category/model/Category.java @@ -0,0 +1,22 @@ +package ru.practicum.category.model; + +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Table(name = "categories") +public class Category { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; +} diff --git a/main/src/main/java/ru/practicum/category/repository/CategoryRepository.java b/main/src/main/java/ru/practicum/category/repository/CategoryRepository.java new file mode 100644 index 0000000..eb8e111 --- /dev/null +++ b/main/src/main/java/ru/practicum/category/repository/CategoryRepository.java @@ -0,0 +1,8 @@ +package ru.practicum.category.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.category.model.Category; + +public interface CategoryRepository extends JpaRepository { + +} diff --git a/main/src/main/java/ru/practicum/category/service/CategoryService.java b/main/src/main/java/ru/practicum/category/service/CategoryService.java new file mode 100644 index 0000000..4c74027 --- /dev/null +++ b/main/src/main/java/ru/practicum/category/service/CategoryService.java @@ -0,0 +1,8 @@ +package ru.practicum.category.service; + +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; + +public interface CategoryService { + CategoryDto createCategory(NewCategoryDto newCategoryDto); +} diff --git a/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java b/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java new file mode 100644 index 0000000..e741390 --- /dev/null +++ b/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java @@ -0,0 +1,22 @@ +package ru.practicum.category.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; +import ru.practicum.category.dto.mapper.CategoryMapper; +import ru.practicum.category.repository.CategoryRepository; + +@Slf4j +@RequiredArgsConstructor +@Service +public class CategoryServiceImpl implements CategoryService { + + private final CategoryRepository categoryRepository; + + @Override + public CategoryDto createCategory(NewCategoryDto newCategoryDto) { + return CategoryMapper.toCategoryDto(categoryRepository.save(CategoryMapper.toCategory(newCategoryDto))); + } +} From 5bea6e4c8d4e511e419b3edaf74c9bf4c1085f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Wed, 13 Aug 2025 09:16:57 +0300 Subject: [PATCH 02/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=87?= =?UTF-8?q?=D0=B8=D0=BA=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ru/practicum/error/ApiError.java | 19 ++++++ .../java/ru/practicum/error/ErrorHandler.java | 63 +++++++++++++++++++ .../exception/ConflictException.java | 7 +++ .../exception/NotFoundException.java | 7 +++ .../exception/ValidationException.java | 7 +++ 5 files changed, 103 insertions(+) create mode 100644 main/src/main/java/ru/practicum/error/ApiError.java create mode 100644 main/src/main/java/ru/practicum/error/ErrorHandler.java create mode 100644 main/src/main/java/ru/practicum/exception/ConflictException.java create mode 100644 main/src/main/java/ru/practicum/exception/NotFoundException.java create mode 100644 main/src/main/java/ru/practicum/exception/ValidationException.java diff --git a/main/src/main/java/ru/practicum/error/ApiError.java b/main/src/main/java/ru/practicum/error/ApiError.java new file mode 100644 index 0000000..327ff47 --- /dev/null +++ b/main/src/main/java/ru/practicum/error/ApiError.java @@ -0,0 +1,19 @@ +package ru.practicum.error; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +@AllArgsConstructor +@Getter +public class ApiError { + private final List errors; + private final String message; + private final String reason; + private final String status; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private final LocalDateTime timestamp; +} diff --git a/main/src/main/java/ru/practicum/error/ErrorHandler.java b/main/src/main/java/ru/practicum/error/ErrorHandler.java new file mode 100644 index 0000000..6e67c7e --- /dev/null +++ b/main/src/main/java/ru/practicum/error/ErrorHandler.java @@ -0,0 +1,63 @@ +package ru.practicum.error; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.NotFoundException; +import ru.practicum.exception.ValidationException; + +import java.time.LocalDateTime; + +@RestControllerAdvice +public class ErrorHandler { + + @ExceptionHandler(NotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ApiError handleNotFound(NotFoundException e) { + return new ApiError( + null, + e.getMessage(), + "Не найдено", + "NOT_FOUND", + LocalDateTime.now() + ); + } + + @ExceptionHandler(ValidationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError handleValidation(ValidationException e) { + return new ApiError( + null, + e.getMessage(), + "Cервер не смог обработать запроc", + "BAD_REQUEST", + LocalDateTime.now() + ); + } + + @ExceptionHandler(ConflictException.class) + @ResponseStatus(HttpStatus.CONFLICT) + public ApiError handleConflict(ConflictException e) { + return new ApiError( + null, + e.getMessage(), + "Конфликт запроса", + "CONFLICT", + LocalDateTime.now() + ); + } + + @ExceptionHandler(Throwable.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiError handleThrowable(Throwable e) { + return new ApiError( + null, + e.getMessage(), + "Внутренняя ошибка сервера", + "INTERNAL_SERVER_ERROR", + LocalDateTime.now() + ); + } +} diff --git a/main/src/main/java/ru/practicum/exception/ConflictException.java b/main/src/main/java/ru/practicum/exception/ConflictException.java new file mode 100644 index 0000000..e2fddf1 --- /dev/null +++ b/main/src/main/java/ru/practicum/exception/ConflictException.java @@ -0,0 +1,7 @@ +package ru.practicum.exception; + +public class ConflictException extends RuntimeException { + public ConflictException(String message) { + super(message); + } +} diff --git a/main/src/main/java/ru/practicum/exception/NotFoundException.java b/main/src/main/java/ru/practicum/exception/NotFoundException.java new file mode 100644 index 0000000..628c788 --- /dev/null +++ b/main/src/main/java/ru/practicum/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package ru.practicum.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} diff --git a/main/src/main/java/ru/practicum/exception/ValidationException.java b/main/src/main/java/ru/practicum/exception/ValidationException.java new file mode 100644 index 0000000..d751d21 --- /dev/null +++ b/main/src/main/java/ru/practicum/exception/ValidationException.java @@ -0,0 +1,7 @@ +package ru.practicum.exception; + +public class ValidationException extends RuntimeException { + public ValidationException(String message) { + super(message); + } +} From 3fdbebcc5b38de3f498a9506ec7dbd6a9ec4ce9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Fri, 15 Aug 2025 01:35:07 +0300 Subject: [PATCH 03/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20API=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=BA=D0=B0=D1=82=D0=B5=D0=B3?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D1=8F=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/pom.xml | 6 ++ .../controller/CategoryAdminController.java | 18 +++-- .../controller/CategoryPublicController.java | 35 ++++++++++ .../practicum/category/dto/CategoryDto.java | 4 -- .../ru/practicum/category/model/Category.java | 6 +- .../repository/CategoryRepository.java | 4 ++ .../category/service/CategoryService.java | 11 +++ .../category/service/CategoryServiceImpl.java | 69 +++++++++++++++++++ .../java/ru/practicum/error/ErrorHandler.java | 54 ++++++++++----- main/src/main/resources/schema.sql | 6 ++ 10 files changed, 185 insertions(+), 28 deletions(-) create mode 100644 main/src/main/java/ru/practicum/category/controller/CategoryPublicController.java create mode 100644 main/src/main/resources/schema.sql diff --git a/main/pom.xml b/main/pom.xml index 0a8006f..e465080 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -55,5 +55,11 @@ org.springframework.boot spring-boot-starter-data-jpa + + + org.postgresql + postgresql + runtime + \ No newline at end of file diff --git a/main/src/main/java/ru/practicum/category/controller/CategoryAdminController.java b/main/src/main/java/ru/practicum/category/controller/CategoryAdminController.java index e783d5d..cf4dc0a 100644 --- a/main/src/main/java/ru/practicum/category/controller/CategoryAdminController.java +++ b/main/src/main/java/ru/practicum/category/controller/CategoryAdminController.java @@ -3,10 +3,8 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; import ru.practicum.category.dto.CategoryDto; import ru.practicum.category.dto.NewCategoryDto; import ru.practicum.category.service.CategoryService; @@ -19,11 +17,23 @@ public class CategoryAdminController { private final CategoryService categoryService; + @ResponseStatus(HttpStatus.CREATED) @PostMapping public CategoryDto createCategory(@Valid @RequestBody NewCategoryDto newCategoryDto) { log.info("POST /admin/categories"); return categoryService.createCategory(newCategoryDto); } + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping("/{catId}") + public void deleteCategory(@PathVariable("catId") Long id) { + log.info("DELETE /admin/categories/{}", id); + categoryService.deleteCategory(id); + } + @PatchMapping("/{catId}") + public CategoryDto updateCategory(@PathVariable("catId") Long id, @Valid @RequestBody NewCategoryDto newCategoryDto) { + log.info("PATCH /admin/categories/{}", id); + return categoryService.updateCategory(id, newCategoryDto); + } } diff --git a/main/src/main/java/ru/practicum/category/controller/CategoryPublicController.java b/main/src/main/java/ru/practicum/category/controller/CategoryPublicController.java new file mode 100644 index 0000000..8ab7b2e --- /dev/null +++ b/main/src/main/java/ru/practicum/category/controller/CategoryPublicController.java @@ -0,0 +1,35 @@ +package ru.practicum.category.controller; + +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.service.CategoryService; + +import java.util.List; + +@Validated +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/categories") +public class CategoryPublicController { + + private final CategoryService categoryService; + + @GetMapping + public List getCategories(@RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size) { + log.info("GET /categories?from={}&size={}", from, size); + return categoryService.getCategories(from, size); + } + + @GetMapping("/{catId}") + public CategoryDto getCategory(@PathVariable(value = "catId") Long id) { + log.info("GET /categories/{}", id); + return categoryService.getCategory(id); + } +} diff --git a/main/src/main/java/ru/practicum/category/dto/CategoryDto.java b/main/src/main/java/ru/practicum/category/dto/CategoryDto.java index fba4ffb..0423033 100644 --- a/main/src/main/java/ru/practicum/category/dto/CategoryDto.java +++ b/main/src/main/java/ru/practicum/category/dto/CategoryDto.java @@ -1,7 +1,5 @@ package ru.practicum.category.dto; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -13,7 +11,5 @@ @Setter public class CategoryDto { private Long id; - @NotBlank(message = "Название категории не может быть пустым") - @Size(min = 1, max = 50, message = "Название категории должно быть в границах от {min} до {max} символов") private String name; } diff --git a/main/src/main/java/ru/practicum/category/model/Category.java b/main/src/main/java/ru/practicum/category/model/Category.java index f8841dc..32e123d 100644 --- a/main/src/main/java/ru/practicum/category/model/Category.java +++ b/main/src/main/java/ru/practicum/category/model/Category.java @@ -1,9 +1,6 @@ package ru.practicum.category.model; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -14,6 +11,7 @@ @Getter @Setter @Table(name = "categories") +@Entity public class Category { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/main/src/main/java/ru/practicum/category/repository/CategoryRepository.java b/main/src/main/java/ru/practicum/category/repository/CategoryRepository.java index eb8e111..5af656e 100644 --- a/main/src/main/java/ru/practicum/category/repository/CategoryRepository.java +++ b/main/src/main/java/ru/practicum/category/repository/CategoryRepository.java @@ -3,6 +3,10 @@ import org.springframework.data.jpa.repository.JpaRepository; import ru.practicum.category.model.Category; +import java.util.Optional; + public interface CategoryRepository extends JpaRepository { + boolean existsByName(String name); + Optional findByName(String name); } diff --git a/main/src/main/java/ru/practicum/category/service/CategoryService.java b/main/src/main/java/ru/practicum/category/service/CategoryService.java index 4c74027..486ead5 100644 --- a/main/src/main/java/ru/practicum/category/service/CategoryService.java +++ b/main/src/main/java/ru/practicum/category/service/CategoryService.java @@ -1,8 +1,19 @@ package ru.practicum.category.service; +import org.springframework.web.bind.annotation.PathVariable; import ru.practicum.category.dto.CategoryDto; import ru.practicum.category.dto.NewCategoryDto; +import java.util.List; + public interface CategoryService { CategoryDto createCategory(NewCategoryDto newCategoryDto); + + void deleteCategory(Long id); + + CategoryDto updateCategory(Long id, NewCategoryDto newCategoryDto); + + List getCategories(Integer from, Integer size); + + CategoryDto getCategory(Long id); } diff --git a/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java b/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java index e741390..46b7001 100644 --- a/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java +++ b/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java @@ -1,12 +1,22 @@ package ru.practicum.category.service; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import ru.practicum.category.dto.CategoryDto; import ru.practicum.category.dto.NewCategoryDto; import ru.practicum.category.dto.mapper.CategoryMapper; +import ru.practicum.category.model.Category; import ru.practicum.category.repository.CategoryRepository; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.NotFoundException; + +import java.util.List; +import java.util.stream.Collectors; @Slf4j @RequiredArgsConstructor @@ -15,8 +25,67 @@ public class CategoryServiceImpl implements CategoryService { private final CategoryRepository categoryRepository; + @Transactional @Override public CategoryDto createCategory(NewCategoryDto newCategoryDto) { + if (categoryRepository.existsByName(newCategoryDto.getName())) { + log.warn("Категория - {} уже существует", newCategoryDto.getName()); + throw new ConflictException("Категория - " + newCategoryDto.getName() + " уже существует"); + } + return CategoryMapper.toCategoryDto(categoryRepository.save(CategoryMapper.toCategory(newCategoryDto))); } + + @Transactional + @Override + public void deleteCategory(Long id) { + findCategoryById(id); + categoryRepository.deleteById(id); + } + + @Transactional + @Override + public CategoryDto updateCategory(Long id, NewCategoryDto newCategoryDto) { + Category category = findCategoryById(id); + + if (newCategoryDto.getName() != null && !category.getName().equals(newCategoryDto.getName())) { + categoryRepository.findByName(newCategoryDto.getName()) + .ifPresent(existing -> { + if (!existing.getId().equals(id)) { + log.warn("Категория с именем '{}' уже существует", newCategoryDto.getName()); + throw new ConflictException("Категория с именем '" + newCategoryDto.getName() + "' уже существует"); + } + }); + + category.setName(newCategoryDto.getName()); + } + + return CategoryMapper.toCategoryDto(categoryRepository.save(category)); + } + + @Override + public List getCategories(Integer from, Integer size) { + Pageable pageable = PageRequest.of(from / size, size, Sort.by("id").ascending()); + + return categoryRepository.findAll(pageable) + .stream() + .map(CategoryMapper::toCategoryDto) + .collect(Collectors.toList()); + } + + @Override + public CategoryDto getCategory(Long id) { + return CategoryMapper.toCategoryDto(findCategoryById(id)); + } + + private Category findCategoryById(Long categoryId) { + return categoryRepository.findById(categoryId) + .orElseThrow(() -> new NotFoundException("Категория с данным id: " + categoryId + " не найдена")); + } + + // Продолжить реализацию после написания events + private void validateCategoryIsEmpty(Long categoryId) { + log.warn("Категория с данным id: {} используется", categoryId); + throw new ConflictException("Категория используется"); + } } diff --git a/main/src/main/java/ru/practicum/error/ErrorHandler.java b/main/src/main/java/ru/practicum/error/ErrorHandler.java index 6e67c7e..aee4743 100644 --- a/main/src/main/java/ru/practicum/error/ErrorHandler.java +++ b/main/src/main/java/ru/practicum/error/ErrorHandler.java @@ -1,49 +1,71 @@ package ru.practicum.error; +import jakarta.validation.ConstraintViolationException; import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import ru.practicum.exception.ConflictException; import ru.practicum.exception.NotFoundException; -import ru.practicum.exception.ValidationException; import java.time.LocalDateTime; +import java.util.Collections; +import java.util.stream.Collectors; @RestControllerAdvice public class ErrorHandler { @ExceptionHandler(NotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) - public ApiError handleNotFound(NotFoundException e) { + public ApiError handleNotFoundException(NotFoundException e) { return new ApiError( - null, + Collections.emptyList(), e.getMessage(), - "Не найдено", + "Not found.", "NOT_FOUND", LocalDateTime.now() ); } - @ExceptionHandler(ValidationException.class) + @ExceptionHandler({MethodArgumentNotValidException.class}) @ResponseStatus(HttpStatus.BAD_REQUEST) - public ApiError handleValidation(ValidationException e) { + public ApiError handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + String message = e.getBindingResult().getFieldErrors().stream() + .map(fe -> String.format("Field: %s. Error: %s. Value: %s", + fe.getField(), + fe.getDefaultMessage(), + fe.getRejectedValue() != null ? fe.getRejectedValue().toString() : "null")) + .collect(Collectors.joining("; ")); + return new ApiError( - null, - e.getMessage(), - "Cервер не смог обработать запроc", + Collections.emptyList(), + message, + "Incorrectly made request.", "BAD_REQUEST", LocalDateTime.now() ); } - @ExceptionHandler(ConflictException.class) + @ExceptionHandler({ConflictException.class}) @ResponseStatus(HttpStatus.CONFLICT) - public ApiError handleConflict(ConflictException e) { + public ApiError handleConflictException(ConflictException e) { return new ApiError( - null, + Collections.emptyList(), e.getMessage(), - "Конфликт запроса", + "Integrity constraint has been violated.", + "CONFLICT", + LocalDateTime.now() + ); + } + + @ExceptionHandler({ConstraintViolationException.class}) + @ResponseStatus(HttpStatus.CONFLICT) + public ApiError handleConstraintViolationException(ConstraintViolationException e) { + return new ApiError( + Collections.emptyList(), + "Категория с таким названием уже существует.", + "Integrity constraint has been violated.", "CONFLICT", LocalDateTime.now() ); @@ -53,9 +75,9 @@ public ApiError handleConflict(ConflictException e) { @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ApiError handleThrowable(Throwable e) { return new ApiError( - null, - e.getMessage(), - "Внутренняя ошибка сервера", + Collections.emptyList(), + "Произошла непредвиденная ошибка.", + "Internal server error.", "INTERNAL_SERVER_ERROR", LocalDateTime.now() ); diff --git a/main/src/main/resources/schema.sql b/main/src/main/resources/schema.sql new file mode 100644 index 0000000..7296e33 --- /dev/null +++ b/main/src/main/resources/schema.sql @@ -0,0 +1,6 @@ +create table categories ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(50) NOT NULL, + CONSTRAINT uq_category_name UNIQUE (name), + CONSTRAINT pk_category_id PRIMARY KEY (id) +); From e1ffabb842184992f13472eed1e7ea2da8797295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Fri, 15 Aug 2025 15:21:44 +0300 Subject: [PATCH 04/15] =?UTF-8?q?=D0=9F=D0=BE=D0=BA=D1=80=D1=8B=D1=82?= =?UTF-8?q?=D1=8C=20=D1=82=D0=B5=D1=81=D1=82=D0=B0=D0=BC=D0=B8=20API=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20?= =?UTF-8?q?=D1=81=20=D0=BA=D0=B0=D1=82=D0=B5=D0=B3=D0=BE=D1=80=D0=B8=D1=8F?= =?UTF-8?q?=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/pom.xml | 12 ++ main/src/main/resources/application.yml | 33 +++++ .../CategoryAdminControllerTest.java | 92 ++++++++++++++ .../java/ru/practicum/CategoryDtoTest.java | 40 ++++++ .../java/ru/practicum/CategoryMapperTest.java | 31 +++++ .../CategoryPublicControllerTest.java | 83 +++++++++++++ .../ru/practicum/CategoryServiceImplTest.java | 116 ++++++++++++++++++ 7 files changed, 407 insertions(+) create mode 100644 main/src/test/java/ru/practicum/CategoryAdminControllerTest.java create mode 100644 main/src/test/java/ru/practicum/CategoryDtoTest.java create mode 100644 main/src/test/java/ru/practicum/CategoryMapperTest.java create mode 100644 main/src/test/java/ru/practicum/CategoryPublicControllerTest.java create mode 100644 main/src/test/java/ru/practicum/CategoryServiceImplTest.java diff --git a/main/pom.xml b/main/pom.xml index e465080..c4c902b 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -61,5 +61,17 @@ postgresql runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.h2database + h2 + runtime + \ No newline at end of file diff --git a/main/src/main/resources/application.yml b/main/src/main/resources/application.yml index 443332b..eca3458 100644 --- a/main/src/main/resources/application.yml +++ b/main/src/main/resources/application.yml @@ -1,4 +1,13 @@ spring: + profiles: + active: dev + +--- + +spring: + config: + activate: + on-profile: dev datasource: driver-class-name: org.postgresql.Driver url: jdbc:postgresql://localhost:6542/maindb @@ -15,5 +24,29 @@ spring: server: port: 8080 +client: + url: http://localhost:9090 + +--- + +spring: + config: + activate: + on-profile: test + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:maindb + username: dbuser2 + password: 1234567 + jpa: + hibernate: + ddl-auto: create-drop + properties: + hibernate: + dialect: org.hibernate.dialect.H2Dialect + +server: + port: 8080 + client: url: http://localhost:9090 \ No newline at end of file diff --git a/main/src/test/java/ru/practicum/CategoryAdminControllerTest.java b/main/src/test/java/ru/practicum/CategoryAdminControllerTest.java new file mode 100644 index 0000000..cced04e --- /dev/null +++ b/main/src/test/java/ru/practicum/CategoryAdminControllerTest.java @@ -0,0 +1,92 @@ +package ru.practicum; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.category.controller.CategoryAdminController; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; +import ru.practicum.category.service.CategoryService; +import ru.practicum.error.ErrorHandler; + +import java.nio.charset.StandardCharsets; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.hamcrest.Matchers.is; + +@ExtendWith(MockitoExtension.class) +class CategoryAdminControllerTest { + + private final ObjectMapper mapper = new ObjectMapper(); + @Mock + private CategoryService categoryService; + @InjectMocks + private CategoryAdminController categoryAdminController; + private MockMvc mvc; + private CategoryDto categoryDto; + private NewCategoryDto newCategoryDto; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + mvc = MockMvcBuilders + .standaloneSetup(categoryAdminController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + categoryDto = new CategoryDto(1L, "Концерты"); + newCategoryDto = new NewCategoryDto("Концерты"); + } + + @Test + void mustCreateCategory() throws Exception { + when(categoryService.createCategory(any())) + .thenReturn(categoryDto); + + mvc.perform(post("/admin/categories") + .content(mapper.writeValueAsString(newCategoryDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.name", is("Концерты"))); + } + + @Test + void mustDeleteCategory() throws Exception { + mvc.perform(delete("/admin/categories/1")) + .andExpect(status().isNoContent()); + + verify(categoryService).deleteCategory(1L); + } + + @Test + void mustUpdateCategory() throws Exception { + CategoryDto updated = new CategoryDto(1L, "Фестивали"); + when(categoryService.updateCategory(eq(1L), any(NewCategoryDto.class))) + .thenReturn(updated); + + mvc.perform(patch("/admin/categories/1") + .content(mapper.writeValueAsString(newCategoryDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.name", is("Фестивали"))); + } +} diff --git a/main/src/test/java/ru/practicum/CategoryDtoTest.java b/main/src/test/java/ru/practicum/CategoryDtoTest.java new file mode 100644 index 0000000..54d08d5 --- /dev/null +++ b/main/src/test/java/ru/practicum/CategoryDtoTest.java @@ -0,0 +1,40 @@ +package ru.practicum; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@ContextConfiguration(classes = MainServiceApp.class) +@JsonTest +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class CategoryDtoTest { + private final JacksonTester jsonCategoryDto; + private final JacksonTester jsonNewCategoryDto; + + @Test + void shouldReturnCategoryDto() throws Exception { + CategoryDto dto = new CategoryDto(1L, "Выставки"); + + JsonContent result = jsonCategoryDto.write(dto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Выставки"); + } + + @Test + void shouldReturnNewCategoryDto() throws Exception { + NewCategoryDto dto = new NewCategoryDto("Выставки"); + + JsonContent result = jsonNewCategoryDto.write(dto); + + assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Выставки"); + } +} diff --git a/main/src/test/java/ru/practicum/CategoryMapperTest.java b/main/src/test/java/ru/practicum/CategoryMapperTest.java new file mode 100644 index 0000000..2da4043 --- /dev/null +++ b/main/src/test/java/ru/practicum/CategoryMapperTest.java @@ -0,0 +1,31 @@ +package ru.practicum; + +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.Test; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; +import ru.practicum.category.dto.mapper.CategoryMapper; +import ru.practicum.category.model.Category; + + +public class CategoryMapperTest { + @Test + void shouldMapCategoryToCategoryDto() { + Category category = new Category(1L, "Кино"); + + CategoryDto result = CategoryMapper.toCategoryDto(category); + + AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L); + AssertionsForClassTypes.assertThat(result.getName()).isEqualTo("Кино"); + } + + @Test + void shouldMapNewCategoryDtoToCategory() { + NewCategoryDto newCategoryDto = new NewCategoryDto("Кино"); + + Category result = CategoryMapper.toCategory(newCategoryDto); + + AssertionsForClassTypes.assertThat(result.getId()).isNull(); + AssertionsForClassTypes.assertThat(result.getName()).isEqualTo("Кино"); + } +} diff --git a/main/src/test/java/ru/practicum/CategoryPublicControllerTest.java b/main/src/test/java/ru/practicum/CategoryPublicControllerTest.java new file mode 100644 index 0000000..159f996 --- /dev/null +++ b/main/src/test/java/ru/practicum/CategoryPublicControllerTest.java @@ -0,0 +1,83 @@ +package ru.practicum; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.category.controller.CategoryPublicController; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.service.CategoryService; +import ru.practicum.error.ErrorHandler; + +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +public class CategoryPublicControllerTest { + + private final ObjectMapper mapper = new ObjectMapper(); + @Mock + private CategoryService categoryService; + @InjectMocks + private CategoryPublicController categoryController; + private MockMvc mvc; + private CategoryDto categoryDto; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + + mvc = MockMvcBuilders + .standaloneSetup(categoryController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + categoryDto = new CategoryDto(1L, "Кино"); + } + + @Test + void mustReturnAllCategories() throws Exception { + List categories = List.of( + new CategoryDto(1L, "Кино"), + new CategoryDto(2L, "Фестиваль") + ); + + when(categoryService.getCategories(anyInt(), anyInt())) + .thenReturn(categories); + + mvc.perform(get("/categories") + .param("from", "0") + .param("size", "10") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.size()").value(2)) + .andExpect(jsonPath("$[0].id", is(1))) + .andExpect(jsonPath("$[0].name", is("Кино"))) + .andExpect(jsonPath("$[1].id", is(2))) + .andExpect(jsonPath("$[1].name", is("Фестиваль"))); + } + + @Test + void mustReturnCategoryById() throws Exception { + when(categoryService.getCategory(1L)) + .thenReturn(categoryDto); + + mvc.perform(get("/categories/1") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.name", is("Кино"))); + } +} diff --git a/main/src/test/java/ru/practicum/CategoryServiceImplTest.java b/main/src/test/java/ru/practicum/CategoryServiceImplTest.java new file mode 100644 index 0000000..25a8e3c --- /dev/null +++ b/main/src/test/java/ru/practicum/CategoryServiceImplTest.java @@ -0,0 +1,116 @@ +package ru.practicum; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; +import ru.practicum.category.model.Category; +import ru.practicum.category.service.CategoryService; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.NotFoundException; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + + +@Transactional +@ActiveProfiles("test") +@SpringBootTest( + classes = MainServiceApp.class, + webEnvironment = SpringBootTest.WebEnvironment.NONE) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class CategoryServiceImplTest { + private final EntityManager em; + private final CategoryService categoryService; + + private Long categoryId; + + @BeforeEach + void setUp() { + Category category = new Category(); + category.setName("Кино"); + + em.persist(category); + em.flush(); + + categoryId = category.getId(); + } + + @Test + void mustReturnCategoryById() { + CategoryDto categoryDto = categoryService.getCategory(categoryId); + + assertThat(categoryDto.getId()).isEqualTo(categoryId); + assertThat(categoryDto.getName()).isEqualTo("Кино"); + } + + @Test + void shouldThrowNotFoundExceptionForInvalidId() { + Assertions.assertThrows(NotFoundException.class, () -> categoryService.getCategory(999999L)); + } + + @Test + void mustCreateCategory() { + NewCategoryDto newCategoryDto = new NewCategoryDto(); + newCategoryDto.setName("Фестиваль"); + + CategoryDto created = categoryService.createCategory(newCategoryDto); + + assertThat(created.getId()).isNotNull(); + assertThat(created.getName()).isEqualTo("Фестиваль"); + } + + @Test + void mustThrowConflictExceptionWhenCreatingDuplicateCategory() { + NewCategoryDto newCategoryDto = new NewCategoryDto(); + newCategoryDto.setName("Кино"); + + Assertions.assertThrows(ConflictException.class, () -> categoryService.createCategory(newCategoryDto)); + } + + @Test + void mustUpdateCategoryName() { + NewCategoryDto updateDto = new NewCategoryDto(); + updateDto.setName("Спектакль"); + + CategoryDto updated = categoryService.updateCategory(categoryId, updateDto); + + assertThat(updated.getId()).isEqualTo(categoryId); + assertThat(updated.getName()).isEqualTo("Спектакль"); + } + + @Test + void shouldThrowConflictExceptionWhenUpdatingToExistingName() { + Category another = new Category(); + another.setName("Спектакль"); + em.persist(another); + em.flush(); + + NewCategoryDto updateDto = new NewCategoryDto(); + updateDto.setName("Спектакль"); + + Assertions.assertThrows(ConflictException.class, () -> categoryService.updateCategory(categoryId, updateDto)); + } + + @Test + void mustDeleteCategory() { + categoryService.deleteCategory(categoryId); + + Assertions.assertThrows(NotFoundException.class, () -> categoryService.getCategory(categoryId)); + } + + @Test + void mustReturnPaginatedCategories() { + List categories = categoryService.getCategories(0, 10); + + assertThat(categories.get(0).getName()).isEqualTo("Кино"); + } +} From 587b2a0248602a2ed5690cbb5e00d58a1868211d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Sat, 16 Aug 2025 23:55:45 +0300 Subject: [PATCH 05/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20API=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F=D0=BC=D0=B8=20=D0=B8?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BA=D1=80=D1=8B=D1=82=D1=8C=20=D0=B5=D0=B3?= =?UTF-8?q?=D0=BE=20=D1=82=D0=B5=D1=81=D1=82=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ru/practicum/category/model/Category.java | 16 +++ .../user/controller/UserAdminController.java | 44 +++++++++ .../ru/practicum/user/dto/NewUserRequest.java | 24 +++++ .../java/ru/practicum/user/dto/UserDto.java | 16 +++ .../ru/practicum/user/dto/UserShortDto.java | 15 +++ .../practicum/user/dto/mapper/UserMapper.java | 33 +++++++ .../java/ru/practicum/user/model/User.java | 44 +++++++++ .../user/repository/UserRepository.java | 13 +++ .../practicum/user/service/UserService.java | 14 +++ .../user/service/UserServiceImpl.java | 61 ++++++++++++ main/src/main/resources/schema.sql | 8 ++ .../user/UserAdminControllerTest.java | 96 ++++++++++++++++++ .../java/ru/practicum/user/UserDtoTest.java | 56 +++++++++++ .../ru/practicum/user/UserMapperTest.java | 44 +++++++++ .../practicum/user/UserServiceImplTest.java | 99 +++++++++++++++++++ .../test/java/ru/practicum/user/UserTest.java | 28 ++++++ 16 files changed, 611 insertions(+) create mode 100644 main/src/main/java/ru/practicum/user/controller/UserAdminController.java create mode 100644 main/src/main/java/ru/practicum/user/dto/NewUserRequest.java create mode 100644 main/src/main/java/ru/practicum/user/dto/UserDto.java create mode 100644 main/src/main/java/ru/practicum/user/dto/UserShortDto.java create mode 100644 main/src/main/java/ru/practicum/user/dto/mapper/UserMapper.java create mode 100644 main/src/main/java/ru/practicum/user/model/User.java create mode 100644 main/src/main/java/ru/practicum/user/repository/UserRepository.java create mode 100644 main/src/main/java/ru/practicum/user/service/UserService.java create mode 100644 main/src/main/java/ru/practicum/user/service/UserServiceImpl.java create mode 100644 main/src/test/java/ru/practicum/user/UserAdminControllerTest.java create mode 100644 main/src/test/java/ru/practicum/user/UserDtoTest.java create mode 100644 main/src/test/java/ru/practicum/user/UserMapperTest.java create mode 100644 main/src/test/java/ru/practicum/user/UserServiceImplTest.java create mode 100644 main/src/test/java/ru/practicum/user/UserTest.java diff --git a/main/src/main/java/ru/practicum/category/model/Category.java b/main/src/main/java/ru/practicum/category/model/Category.java index 32e123d..9c669ba 100644 --- a/main/src/main/java/ru/practicum/category/model/Category.java +++ b/main/src/main/java/ru/practicum/category/model/Category.java @@ -5,6 +5,9 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import ru.practicum.user.model.User; + +import java.util.Objects; @NoArgsConstructor @AllArgsConstructor @@ -17,4 +20,17 @@ public class Category { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof User)) return false; + Category category = (Category) o; + return Objects.equals(id, category.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } } diff --git a/main/src/main/java/ru/practicum/user/controller/UserAdminController.java b/main/src/main/java/ru/practicum/user/controller/UserAdminController.java new file mode 100644 index 0000000..5262396 --- /dev/null +++ b/main/src/main/java/ru/practicum/user/controller/UserAdminController.java @@ -0,0 +1,44 @@ +package ru.practicum.user.controller; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.user.service.UserService; +import ru.practicum.user.dto.NewUserRequest; +import ru.practicum.user.dto.UserDto; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/admin/users") +public class UserAdminController { + private final UserService userService; + + @GetMapping + public List getUsers(@RequestParam(required = false) List ids, + @RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size) { + log.info("GET /admin/users"); + return userService.getUsers(ids, from, size); + } + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public UserDto createUser(@RequestBody @Valid NewUserRequest newUserRequest) { + log.info("POST /admin/users"); + return userService.createUser(newUserRequest); + } + + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping("/{userId}") + public void deleteUser(@PathVariable("userId") Long id) { + log.info("DELETE /admin/users/{}", id); + userService.deleteUser(id); + } +} diff --git a/main/src/main/java/ru/practicum/user/dto/NewUserRequest.java b/main/src/main/java/ru/practicum/user/dto/NewUserRequest.java new file mode 100644 index 0000000..aef7a3d --- /dev/null +++ b/main/src/main/java/ru/practicum/user/dto/NewUserRequest.java @@ -0,0 +1,24 @@ +package ru.practicum.user.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class NewUserRequest { + @Email(message = "Email должен быть в корректном формате") + @NotBlank(message = "Email не может быть пустым") + @Size(min = 6, max = 254, message = "Email должен быть в границах от {min} до {max} символов") + private String email; + + @NotBlank(message = "Имя пользователя не может быть пустым") + @Size(min = 2, max = 250, message = "Имя пользователя должно быть в границах от {min} до {max} символов") + private String name; +} diff --git a/main/src/main/java/ru/practicum/user/dto/UserDto.java b/main/src/main/java/ru/practicum/user/dto/UserDto.java new file mode 100644 index 0000000..66728db --- /dev/null +++ b/main/src/main/java/ru/practicum/user/dto/UserDto.java @@ -0,0 +1,16 @@ +package ru.practicum.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class UserDto { + private Long id; + private String email; + private String name; +} diff --git a/main/src/main/java/ru/practicum/user/dto/UserShortDto.java b/main/src/main/java/ru/practicum/user/dto/UserShortDto.java new file mode 100644 index 0000000..09df516 --- /dev/null +++ b/main/src/main/java/ru/practicum/user/dto/UserShortDto.java @@ -0,0 +1,15 @@ +package ru.practicum.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class UserShortDto { + private Long id; + private String name; +} diff --git a/main/src/main/java/ru/practicum/user/dto/mapper/UserMapper.java b/main/src/main/java/ru/practicum/user/dto/mapper/UserMapper.java new file mode 100644 index 0000000..34de561 --- /dev/null +++ b/main/src/main/java/ru/practicum/user/dto/mapper/UserMapper.java @@ -0,0 +1,33 @@ +package ru.practicum.user.dto.mapper; + +import lombok.experimental.UtilityClass; +import ru.practicum.user.model.User; +import ru.practicum.user.dto.NewUserRequest; +import ru.practicum.user.dto.UserDto; +import ru.practicum.user.dto.UserShortDto; + +@UtilityClass +public class UserMapper { + public static UserDto toUserDto(User user) { + return new UserDto( + user.getId(), + user.getEmail(), + user.getName() + ); + } + + public static User toUser(NewUserRequest newUserRequest) { + return new User( + null, + newUserRequest.getEmail(), + newUserRequest.getName() + ); + } + + public static UserShortDto userShortDto(User user) { + return new UserShortDto( + user.getId(), + user.getName() + ); + } +} diff --git a/main/src/main/java/ru/practicum/user/model/User.java b/main/src/main/java/ru/practicum/user/model/User.java new file mode 100644 index 0000000..45390cc --- /dev/null +++ b/main/src/main/java/ru/practicum/user/model/User.java @@ -0,0 +1,44 @@ +package ru.practicum.user.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Objects; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Table(name = "users") +@Entity +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Email + @Size(max = 254) + @Column(nullable = false, unique = true, length = 254) + private String email; + + @Column(nullable = false, length = 250) + private String name; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof User)) return false; + User user = (User) o; + return Objects.equals(email, user.email); + } + + @Override + public int hashCode() { + return Objects.hash(email); + } +} diff --git a/main/src/main/java/ru/practicum/user/repository/UserRepository.java b/main/src/main/java/ru/practicum/user/repository/UserRepository.java new file mode 100644 index 0000000..1d4c386 --- /dev/null +++ b/main/src/main/java/ru/practicum/user/repository/UserRepository.java @@ -0,0 +1,13 @@ +package ru.practicum.user.repository; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.user.model.User; + +import java.util.List; + +public interface UserRepository extends JpaRepository { + boolean existsByEmail(String email); + + List findByIdIn(List ids, Pageable pageable); +} diff --git a/main/src/main/java/ru/practicum/user/service/UserService.java b/main/src/main/java/ru/practicum/user/service/UserService.java new file mode 100644 index 0000000..93ef241 --- /dev/null +++ b/main/src/main/java/ru/practicum/user/service/UserService.java @@ -0,0 +1,14 @@ +package ru.practicum.user.service; + +import ru.practicum.user.dto.NewUserRequest; +import ru.practicum.user.dto.UserDto; + +import java.util.List; + +public interface UserService { + List getUsers(List ids, Integer from, Integer size); + + UserDto createUser(NewUserRequest newUserRequest); + + void deleteUser(Long id); +} diff --git a/main/src/main/java/ru/practicum/user/service/UserServiceImpl.java b/main/src/main/java/ru/practicum/user/service/UserServiceImpl.java new file mode 100644 index 0000000..dad03eb --- /dev/null +++ b/main/src/main/java/ru/practicum/user/service/UserServiceImpl.java @@ -0,0 +1,61 @@ +package ru.practicum.user.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.NotFoundException; +import ru.practicum.user.dto.NewUserRequest; +import ru.practicum.user.dto.UserDto; +import ru.practicum.user.dto.mapper.UserMapper; +import ru.practicum.user.repository.UserRepository; + +import java.util.List; +import java.util.stream.Collectors; + +@Transactional(readOnly = true) +@Slf4j +@RequiredArgsConstructor +@Service +public class UserServiceImpl implements UserService { + private final UserRepository userRepository; + + @Override + public List getUsers(List ids, Integer from, Integer size) { + PageRequest page = PageRequest.of(from / size, size); + + if (ids != null && !ids.isEmpty()) { + return userRepository.findByIdIn(ids, page) + .stream() + .map(UserMapper::toUserDto) + .collect(Collectors.toList()); + } else { + return userRepository.findAll(page) + .stream() + .map(UserMapper::toUserDto) + .collect(Collectors.toList()); + } + } + + @Transactional + @Override + public UserDto createUser(NewUserRequest newUserRequest) { + if (userRepository.existsByEmail(newUserRequest.getEmail())) { + log.warn("Пользователь с email - {} уже существует", newUserRequest.getEmail()); + throw new ConflictException("Пользователь с email - " + newUserRequest.getEmail() + " уже существует"); + } + return UserMapper.toUserDto(userRepository.save(UserMapper.toUser(newUserRequest))); + } + + @Transactional + @Override + public void deleteUser(Long id) { + if (!userRepository.existsById(id)) { + log.warn("Пользователь с id - {} не найден", id); + throw new NotFoundException("Пользователь с id - " + id + " не найден"); + } + userRepository.deleteById(id); + } +} diff --git a/main/src/main/resources/schema.sql b/main/src/main/resources/schema.sql index 7296e33..6238526 100644 --- a/main/src/main/resources/schema.sql +++ b/main/src/main/resources/schema.sql @@ -4,3 +4,11 @@ create table categories ( CONSTRAINT uq_category_name UNIQUE (name), CONSTRAINT pk_category_id PRIMARY KEY (id) ); + +create table users ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + email VARCHAR(254) NOT NULL, + name VARCHAR(250) NOT NULL, + CONSTRAINT uq_user_email UNIQUE (email), + CONSTRAINT pk_user_id PRIMARY KEY (id) +); diff --git a/main/src/test/java/ru/practicum/user/UserAdminControllerTest.java b/main/src/test/java/ru/practicum/user/UserAdminControllerTest.java new file mode 100644 index 0000000..244d23f --- /dev/null +++ b/main/src/test/java/ru/practicum/user/UserAdminControllerTest.java @@ -0,0 +1,96 @@ +package ru.practicum.user; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.error.ErrorHandler; +import ru.practicum.user.controller.UserAdminController; +import ru.practicum.user.dto.NewUserRequest; +import ru.practicum.user.dto.UserDto; +import ru.practicum.user.service.UserService; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.hamcrest.Matchers.is; + +@ExtendWith(MockitoExtension.class) +class UserAdminControllerTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Mock + private UserService userService; + + @InjectMocks + private UserAdminController userAdminController; + + private MockMvc mvc; + + private UserDto userDto; + private NewUserRequest newUserRequest; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + mvc = MockMvcBuilders + .standaloneSetup(userAdminController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + userDto = new UserDto(1L, "Max@mail.com", "Макс"); + newUserRequest = new NewUserRequest("Max@mail.com", "Макс"); + } + + @Test + void mustGetUsers() throws Exception { + when(userService.getUsers(null, 0, 10)) + .thenReturn(List.of(userDto)); + + mvc.perform(get("/admin/users") + .param("from", "0") + .param("size", "10") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id", is(1))) + .andExpect(jsonPath("$[0].email", is("Max@mail.com"))) + .andExpect(jsonPath("$[0].name", is("Макс"))); + } + + @Test + void mustCreateUser() throws Exception { + when(userService.createUser(any())) + .thenReturn(userDto); + + mvc.perform(post("/admin/users") + .content(mapper.writeValueAsString(newUserRequest)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.email", is("Max@mail.com"))) + .andExpect(jsonPath("$.name", is("Макс"))); + } + + @Test + void mustDeleteUser() throws Exception { + mvc.perform(delete("/admin/users/1")) + .andExpect(status().isNoContent()); + + verify(userService).deleteUser(1L); + } +} diff --git a/main/src/test/java/ru/practicum/user/UserDtoTest.java b/main/src/test/java/ru/practicum/user/UserDtoTest.java new file mode 100644 index 0000000..f07959b --- /dev/null +++ b/main/src/test/java/ru/practicum/user/UserDtoTest.java @@ -0,0 +1,56 @@ +package ru.practicum.user; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.MainServiceApp; +import ru.practicum.user.dto.NewUserRequest; +import ru.practicum.user.dto.UserDto; +import ru.practicum.user.dto.UserShortDto; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@ContextConfiguration(classes = MainServiceApp.class) +@JsonTest +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class UserDtoTest { + + private final JacksonTester jsonNewUserRequest; + private final JacksonTester jsonUserDto; + private final JacksonTester jsonUserShortDto; + + @Test + void shouldReturnNewUserRequest() throws Exception { + NewUserRequest dto = new NewUserRequest("Max@mail.com", "Макс"); + + JsonContent result = jsonNewUserRequest.write(dto); + + assertThat(result).extractingJsonPathStringValue("$.email").isEqualTo("Max@mail.com"); + assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Макс"); + } + + @Test + void shouldReturnUserDto() throws Exception { + UserDto dto = new UserDto(1L, "Max@mail.com", "Макс"); + + JsonContent result = jsonUserDto.write(dto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(result).extractingJsonPathStringValue("$.email").isEqualTo("Max@mail.com"); + assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Макс"); + } + + @Test + void shouldReturnUserShortDto() throws Exception { + UserShortDto dto = new UserShortDto(2L, "Макс"); + + JsonContent result = jsonUserShortDto.write(dto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(2); + assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Макс"); + } +} diff --git a/main/src/test/java/ru/practicum/user/UserMapperTest.java b/main/src/test/java/ru/practicum/user/UserMapperTest.java new file mode 100644 index 0000000..10454d4 --- /dev/null +++ b/main/src/test/java/ru/practicum/user/UserMapperTest.java @@ -0,0 +1,44 @@ +package ru.practicum.user; + +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.Test; +import ru.practicum.user.dto.NewUserRequest; +import ru.practicum.user.dto.UserDto; +import ru.practicum.user.dto.UserShortDto; +import ru.practicum.user.dto.mapper.UserMapper; +import ru.practicum.user.model.User; + +class UserMapperTest { + + @Test + void shouldMapUserToUserDto() { + User user = new User(1L, "Max@mail.com", "Макс"); + + UserDto result = UserMapper.toUserDto(user); + + AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L); + AssertionsForClassTypes.assertThat(result.getEmail()).isEqualTo("Max@mail.com"); + AssertionsForClassTypes.assertThat(result.getName()).isEqualTo("Макс"); + } + + @Test + void shouldMapNewUserRequestToUser() { + NewUserRequest newUserRequest = new NewUserRequest("Max@mail.com", "Макс"); + + User result = UserMapper.toUser(newUserRequest); + + AssertionsForClassTypes.assertThat(result.getId()).isNull(); + AssertionsForClassTypes.assertThat(result.getEmail()).isEqualTo("Max@mail.com"); + AssertionsForClassTypes.assertThat(result.getName()).isEqualTo("Макс"); + } + + @Test + void shouldMapUserToUserShortDto() { + User user = new User(2L, "Max@mail.com", "Макс"); + + UserShortDto result = UserMapper.userShortDto(user); + + AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(2L); + AssertionsForClassTypes.assertThat(result.getName()).isEqualTo("Макс"); + } +} \ No newline at end of file diff --git a/main/src/test/java/ru/practicum/user/UserServiceImplTest.java b/main/src/test/java/ru/practicum/user/UserServiceImplTest.java new file mode 100644 index 0000000..b06caba --- /dev/null +++ b/main/src/test/java/ru/practicum/user/UserServiceImplTest.java @@ -0,0 +1,99 @@ +package ru.practicum.user; + +import jakarta.persistence.EntityManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.MainServiceApp; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.NotFoundException; +import ru.practicum.user.dto.NewUserRequest; +import ru.practicum.user.dto.UserDto; +import ru.practicum.user.model.User; +import ru.practicum.user.service.UserService; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Transactional +@ActiveProfiles("test") +@SpringBootTest( + classes = MainServiceApp.class, + webEnvironment = SpringBootTest.WebEnvironment.NONE) +class UserServiceImplTest { + + @Autowired + private EntityManager em; + + @Autowired + private UserService userService; + + private Long userId; + + @BeforeEach + void setUp() { + User user = new User(); + user.setEmail("Max@mail.com"); + user.setName("Макс"); + + em.persist(user); + em.flush(); + + userId = user.getId(); + } + + @Test + void mustCreateUser() { + NewUserRequest newUser = new NewUserRequest("Petr@mail.com", "Петр"); + + UserDto created = userService.createUser(newUser); + + assertThat(created.getId()).isNotNull(); + assertThat(created.getEmail()).isEqualTo("Petr@mail.com"); + assertThat(created.getName()).isEqualTo("Петр"); + } + + @Test + void mustThrowConflictExceptionWhenCreatingDuplicateUser() { + NewUserRequest duplicate = new NewUserRequest("Max@mail.com", "Макс"); + + assertThrows(ConflictException.class, () -> userService.createUser(duplicate)); + } + + @Test + void mustReturnUsers() { + List users = userService.getUsers(null, 0, 10); + + assertThat(users.get(0).getId()).isEqualTo(userId); + assertThat(users.get(0).getEmail()).isEqualTo("Max@mail.com"); + } + + @Test + void mustDeleteUser() { + userService.deleteUser(userId); + + assertThrows(NotFoundException.class, () -> userService.deleteUser(userId)); + } + + @Test + void shouldThrowNotFoundExceptionWhenDeletingNonExistingUser() { + assertThrows(NotFoundException.class, () -> userService.deleteUser(10000L)); + } + + @Test + void mustReturnPaginatedUsers() { + User u2 = new User(); + u2.setEmail("second@mail.com"); + u2.setName("Евгений"); + em.persist(u2); + em.flush(); + + List users = userService.getUsers(null, 0, 1); + assertThat(users.get(0).getEmail()).isEqualTo("Max@mail.com"); + } +} diff --git a/main/src/test/java/ru/practicum/user/UserTest.java b/main/src/test/java/ru/practicum/user/UserTest.java new file mode 100644 index 0000000..c9c482f --- /dev/null +++ b/main/src/test/java/ru/practicum/user/UserTest.java @@ -0,0 +1,28 @@ +package ru.practicum.user; + +import org.junit.jupiter.api.Test; +import ru.practicum.user.model.User; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +class UserTest { + + @Test + void testEqualsAndHashCode() { + User user1 = new User(1L, "test@mail.com", "Макс"); + User user2 = new User(2L, "test@mail.com", "Петр"); + User user3 = new User(3L, "other@mail.com", "Макс"); + + assertThat(user1).isEqualTo(user1); + assertThat(user1.hashCode()).isEqualTo(user1.hashCode()); + + assertThat(user1).isEqualTo(user2); + assertThat(user1.hashCode()).isEqualTo(user2.hashCode()); + + assertThat(user1).isNotEqualTo(user3); + assertThat(user1.hashCode()).isNotEqualTo(user3.hashCode()); + + assertThat(user1).isNotEqualTo(null); + assertThat(user1).isNotEqualTo("string"); + } +} From 3bf1317cddb44318266652edfd0d08060e9c098b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Wed, 20 Aug 2025 08:29:20 +0300 Subject: [PATCH 06/15] =?UTF-8?q?=D0=92=D0=BD=D0=B5=D1=81=D1=82=D0=B8=20?= =?UTF-8?q?=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=BA=D0=B8=20=D0=B2=20=D0=BF=D1=80=D0=B5=D0=B4=D1=8B?= =?UTF-8?q?=D0=B4=D1=83=D1=89=D0=B8=D0=B9=20=D0=BA=D0=BE=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ru/practicum/category/model/Category.java | 3 +- .../event/dto/UpdateEventUserRequest.java | 44 +++++++++++++++++++ .../practicum/user/dto/mapper/UserMapper.java | 2 +- .../CategoryAdminControllerTest.java | 2 +- .../{ => category}/CategoryDtoTest.java | 3 +- .../{ => category}/CategoryMapperTest.java | 2 +- .../CategoryPublicControllerTest.java | 2 +- .../CategoryServiceImplTest.java | 3 +- .../ru/practicum/user/UserMapperTest.java | 2 +- .../practicum/repository/StatsRepository.java | 2 +- .../practicum/service/StatsServiceImpl.java | 2 +- 11 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 main/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java rename main/src/test/java/ru/practicum/{ => category}/CategoryAdminControllerTest.java (99%) rename main/src/test/java/ru/practicum/{ => category}/CategoryDtoTest.java (95%) rename main/src/test/java/ru/practicum/{ => category}/CategoryMapperTest.java (97%) rename main/src/test/java/ru/practicum/{ => category}/CategoryPublicControllerTest.java (98%) rename main/src/test/java/ru/practicum/{ => category}/CategoryServiceImplTest.java (98%) diff --git a/main/src/main/java/ru/practicum/category/model/Category.java b/main/src/main/java/ru/practicum/category/model/Category.java index 9c669ba..8e94341 100644 --- a/main/src/main/java/ru/practicum/category/model/Category.java +++ b/main/src/main/java/ru/practicum/category/model/Category.java @@ -5,7 +5,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import ru.practicum.user.model.User; import java.util.Objects; @@ -24,7 +23,7 @@ public class Category { @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof User)) return false; + if (!(o instanceof Category)) return false; Category category = (Category) o; return Objects.equals(id, category.id); } diff --git a/main/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java b/main/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java new file mode 100644 index 0000000..41151b4 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java @@ -0,0 +1,44 @@ +package ru.practicum.event.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.event.state.EventStateAction; +import ru.practicum.location.LocationDto; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class UpdateEventUserRequest { + @Size(min = 20, max = 2000, message = "Поле Аннотация должно быть в границах от {min} до {max} символов") + private String annotation; + + private Long category; + + @Size(min = 20, max = 7000, message = "Поле Описание должно быть в границах от {min} до {max} символов") + private String description; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime eventDate; + + private LocationDto location; + + private Boolean paid; + + @Min(value = 0, message = "Лимит участников не может быть отрицательным") + private Integer participantLimit; + + private Boolean requestModeration; + + private EventStateAction stateAction; + + @Size(min = 3, max = 120, message = "Поле Заголовок должно быть в границах от {min} до {max} символов") + private String title; +} diff --git a/main/src/main/java/ru/practicum/user/dto/mapper/UserMapper.java b/main/src/main/java/ru/practicum/user/dto/mapper/UserMapper.java index 34de561..0e180e1 100644 --- a/main/src/main/java/ru/practicum/user/dto/mapper/UserMapper.java +++ b/main/src/main/java/ru/practicum/user/dto/mapper/UserMapper.java @@ -24,7 +24,7 @@ public static User toUser(NewUserRequest newUserRequest) { ); } - public static UserShortDto userShortDto(User user) { + public static UserShortDto toUserShortDto(User user) { return new UserShortDto( user.getId(), user.getName() diff --git a/main/src/test/java/ru/practicum/CategoryAdminControllerTest.java b/main/src/test/java/ru/practicum/category/CategoryAdminControllerTest.java similarity index 99% rename from main/src/test/java/ru/practicum/CategoryAdminControllerTest.java rename to main/src/test/java/ru/practicum/category/CategoryAdminControllerTest.java index cced04e..ea28014 100644 --- a/main/src/test/java/ru/practicum/CategoryAdminControllerTest.java +++ b/main/src/test/java/ru/practicum/category/CategoryAdminControllerTest.java @@ -1,4 +1,4 @@ -package ru.practicum; +package ru.practicum.category; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; diff --git a/main/src/test/java/ru/practicum/CategoryDtoTest.java b/main/src/test/java/ru/practicum/category/CategoryDtoTest.java similarity index 95% rename from main/src/test/java/ru/practicum/CategoryDtoTest.java rename to main/src/test/java/ru/practicum/category/CategoryDtoTest.java index 54d08d5..8b9cda9 100644 --- a/main/src/test/java/ru/practicum/CategoryDtoTest.java +++ b/main/src/test/java/ru/practicum/category/CategoryDtoTest.java @@ -1,4 +1,4 @@ -package ru.practicum; +package ru.practicum.category; import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.Test; @@ -7,6 +7,7 @@ import org.springframework.boot.test.json.JacksonTester; import org.springframework.boot.test.json.JsonContent; import org.springframework.test.context.ContextConfiguration; +import ru.practicum.MainServiceApp; import ru.practicum.category.dto.CategoryDto; import ru.practicum.category.dto.NewCategoryDto; diff --git a/main/src/test/java/ru/practicum/CategoryMapperTest.java b/main/src/test/java/ru/practicum/category/CategoryMapperTest.java similarity index 97% rename from main/src/test/java/ru/practicum/CategoryMapperTest.java rename to main/src/test/java/ru/practicum/category/CategoryMapperTest.java index 2da4043..7c3deae 100644 --- a/main/src/test/java/ru/practicum/CategoryMapperTest.java +++ b/main/src/test/java/ru/practicum/category/CategoryMapperTest.java @@ -1,4 +1,4 @@ -package ru.practicum; +package ru.practicum.category; import org.assertj.core.api.AssertionsForClassTypes; import org.junit.jupiter.api.Test; diff --git a/main/src/test/java/ru/practicum/CategoryPublicControllerTest.java b/main/src/test/java/ru/practicum/category/CategoryPublicControllerTest.java similarity index 98% rename from main/src/test/java/ru/practicum/CategoryPublicControllerTest.java rename to main/src/test/java/ru/practicum/category/CategoryPublicControllerTest.java index 159f996..916b7d7 100644 --- a/main/src/test/java/ru/practicum/CategoryPublicControllerTest.java +++ b/main/src/test/java/ru/practicum/category/CategoryPublicControllerTest.java @@ -1,4 +1,4 @@ -package ru.practicum; +package ru.practicum.category; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; diff --git a/main/src/test/java/ru/practicum/CategoryServiceImplTest.java b/main/src/test/java/ru/practicum/category/CategoryServiceImplTest.java similarity index 98% rename from main/src/test/java/ru/practicum/CategoryServiceImplTest.java rename to main/src/test/java/ru/practicum/category/CategoryServiceImplTest.java index 25a8e3c..8aa6a88 100644 --- a/main/src/test/java/ru/practicum/CategoryServiceImplTest.java +++ b/main/src/test/java/ru/practicum/category/CategoryServiceImplTest.java @@ -1,4 +1,4 @@ -package ru.practicum; +package ru.practicum.category; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; @@ -9,6 +9,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; +import ru.practicum.MainServiceApp; import ru.practicum.category.dto.CategoryDto; import ru.practicum.category.dto.NewCategoryDto; import ru.practicum.category.model.Category; diff --git a/main/src/test/java/ru/practicum/user/UserMapperTest.java b/main/src/test/java/ru/practicum/user/UserMapperTest.java index 10454d4..c5fb454 100644 --- a/main/src/test/java/ru/practicum/user/UserMapperTest.java +++ b/main/src/test/java/ru/practicum/user/UserMapperTest.java @@ -36,7 +36,7 @@ void shouldMapNewUserRequestToUser() { void shouldMapUserToUserShortDto() { User user = new User(2L, "Max@mail.com", "Макс"); - UserShortDto result = UserMapper.userShortDto(user); + UserShortDto result = UserMapper.toUserShortDto(user); AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(2L); AssertionsForClassTypes.assertThat(result.getName()).isEqualTo("Макс"); diff --git a/stats/server/src/main/java/ru/practicum/repository/StatsRepository.java b/stats/server/src/main/java/ru/practicum/repository/StatsRepository.java index ebaa56e..04647de 100644 --- a/stats/server/src/main/java/ru/practicum/repository/StatsRepository.java +++ b/stats/server/src/main/java/ru/practicum/repository/StatsRepository.java @@ -2,8 +2,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import ru.practicum.view.StatsView; import ru.practicum.model.Stats; +import ru.practicum.view.StatsView; import java.time.LocalDateTime; import java.util.List; diff --git a/stats/server/src/main/java/ru/practicum/service/StatsServiceImpl.java b/stats/server/src/main/java/ru/practicum/service/StatsServiceImpl.java index 11fecb7..51eb20c 100644 --- a/stats/server/src/main/java/ru/practicum/service/StatsServiceImpl.java +++ b/stats/server/src/main/java/ru/practicum/service/StatsServiceImpl.java @@ -5,10 +5,10 @@ import org.springframework.stereotype.Service; import ru.practicum.StatsRequestDto; import ru.practicum.StatsResponseDto; -import ru.practicum.view.StatsView; import ru.practicum.exception.ValidationException; import ru.practicum.mapper.StatsMapper; import ru.practicum.repository.StatsRepository; +import ru.practicum.view.StatsView; import java.time.LocalDateTime; import java.util.List; From d2b62f76b178e1b5a5f717268ea47088cd352820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Wed, 20 Aug 2025 08:44:02 +0300 Subject: [PATCH 07/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D1=82=D1=8B=D0=B9?= =?UTF-8?q?=20API=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D1=8B=20=D1=81=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D1=8F?= =?UTF-8?q?=D0=BC=D0=B8=20=D0=B8=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D1=82?= =?UTF-8?q?=D1=8B=D0=B9=20API=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=81=D0=B0=D0=BC=D0=B8=20=D1=82=D0=B5=D0=BA=D1=83=D1=89=D0=B5?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8F=20=D0=BD=D0=B0=20=D1=83=D1=87=D0=B0?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=B5=20=D0=B2=20=D1=81=D0=BE=D0=B1=D1=8B?= =?UTF-8?q?=D1=82=D0=B8=D1=8F=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ru/practicum/error/ErrorHandler.java | 3 + .../controller/EventPrivateController.java | 68 ++++++ .../ru/practicum/event/dto/EventFullDto.java | 54 +++++ .../dto/EventRequestStatusUpdateRequest.java | 21 ++ .../dto/EventRequestStatusUpdateResult.java | 18 ++ .../ru/practicum/event/dto/EventShortDto.java | 36 +++ .../ru/practicum/event/dto/NewEventDto.java | 47 ++++ .../event/dto/mapper/EventMapper.java | 74 ++++++ .../java/ru/practicum/event/model/Event.java | 88 +++++++ .../event/repository/EventRepository.java | 15 ++ .../practicum/event/service/EventService.java | 22 ++ .../event/service/EventServiceImpl.java | 218 ++++++++++++++++++ .../ru/practicum/event/state/EventState.java | 7 + .../event/state/EventStateAction.java | 6 + .../java/ru/practicum/location/Location.java | 41 ++++ .../ru/practicum/location/LocationDto.java | 25 ++ .../ru/practicum/location/LocationMapper.java | 21 ++ .../location/LocationRepository.java | 6 + .../controller/RequestPrivateController.java | 39 ++++ .../request/dto/ParticipationRequestDto.java | 27 +++ .../request/dto/mapper/RequestMapper.java | 30 +++ .../ru/practicum/request/model/Request.java | 42 ++++ .../request/repository/RequestRepository.java | 21 ++ .../request/service/RequestService.java | 13 ++ .../request/service/RequestServiceImpl.java | 108 +++++++++ .../request/status/RequestStatus.java | 8 + main/src/main/resources/schema.sql | 46 +++- 27 files changed, 1102 insertions(+), 2 deletions(-) create mode 100644 main/src/main/java/ru/practicum/event/controller/EventPrivateController.java create mode 100644 main/src/main/java/ru/practicum/event/dto/EventFullDto.java create mode 100644 main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateRequest.java create mode 100644 main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateResult.java create mode 100644 main/src/main/java/ru/practicum/event/dto/EventShortDto.java create mode 100644 main/src/main/java/ru/practicum/event/dto/NewEventDto.java create mode 100644 main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java create mode 100644 main/src/main/java/ru/practicum/event/model/Event.java create mode 100644 main/src/main/java/ru/practicum/event/repository/EventRepository.java create mode 100644 main/src/main/java/ru/practicum/event/service/EventService.java create mode 100644 main/src/main/java/ru/practicum/event/service/EventServiceImpl.java create mode 100644 main/src/main/java/ru/practicum/event/state/EventState.java create mode 100644 main/src/main/java/ru/practicum/event/state/EventStateAction.java create mode 100644 main/src/main/java/ru/practicum/location/Location.java create mode 100644 main/src/main/java/ru/practicum/location/LocationDto.java create mode 100644 main/src/main/java/ru/practicum/location/LocationMapper.java create mode 100644 main/src/main/java/ru/practicum/location/LocationRepository.java create mode 100644 main/src/main/java/ru/practicum/request/controller/RequestPrivateController.java create mode 100644 main/src/main/java/ru/practicum/request/dto/ParticipationRequestDto.java create mode 100644 main/src/main/java/ru/practicum/request/dto/mapper/RequestMapper.java create mode 100644 main/src/main/java/ru/practicum/request/model/Request.java create mode 100644 main/src/main/java/ru/practicum/request/repository/RequestRepository.java create mode 100644 main/src/main/java/ru/practicum/request/service/RequestService.java create mode 100644 main/src/main/java/ru/practicum/request/service/RequestServiceImpl.java create mode 100644 main/src/main/java/ru/practicum/request/status/RequestStatus.java diff --git a/main/src/main/java/ru/practicum/error/ErrorHandler.java b/main/src/main/java/ru/practicum/error/ErrorHandler.java index aee4743..8e268be 100644 --- a/main/src/main/java/ru/practicum/error/ErrorHandler.java +++ b/main/src/main/java/ru/practicum/error/ErrorHandler.java @@ -1,6 +1,7 @@ package ru.practicum.error; import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -13,6 +14,7 @@ import java.util.Collections; import java.util.stream.Collectors; +@Slf4j @RestControllerAdvice public class ErrorHandler { @@ -74,6 +76,7 @@ public ApiError handleConstraintViolationException(ConstraintViolationException @ExceptionHandler(Throwable.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ApiError handleThrowable(Throwable e) { + log.error("Неожиданная ошибка: ", e); return new ApiError( Collections.emptyList(), "Произошла непредвиденная ошибка.", diff --git a/main/src/main/java/ru/practicum/event/controller/EventPrivateController.java b/main/src/main/java/ru/practicum/event/controller/EventPrivateController.java new file mode 100644 index 0000000..72b263f --- /dev/null +++ b/main/src/main/java/ru/practicum/event/controller/EventPrivateController.java @@ -0,0 +1,68 @@ +package ru.practicum.event.controller; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.event.dto.*; +import ru.practicum.event.service.EventService; +import ru.practicum.request.dto.ParticipationRequestDto; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/users/{userId}/events") +public class EventPrivateController { + private final EventService eventService; + + @GetMapping + public List getEventsByUserId(@PathVariable("userId") Long userId, + @RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size) { + log.info("GET /users/{}/events?from={}&size={}", userId, from, size); + return eventService.getEventsByUserId(userId, from, size); + } + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public EventFullDto createEvent(@PathVariable("userId") Long userId, + @Valid @RequestBody NewEventDto newEventDto) { + log.info("POST /users/{}/events", userId); + return eventService.createEvent(userId, newEventDto); + } + + @GetMapping("/{eventId}") + public EventFullDto getEventByUserIdAndEventId(@PathVariable("userId") Long userId, + @PathVariable("eventId") Long eventId) { + log.info("GET /users/{}/events/{}", userId, eventId); + return eventService.getEventByUserIdAndEventId(userId, eventId); + } + + @PatchMapping("/{eventId}") + public EventFullDto updateEventByUserIdAndEventId(@PathVariable("userId") Long userId, + @PathVariable("eventId") Long eventId, + @RequestBody UpdateEventUserRequest updateEventUserRequest) { + log.info("PATCH /users/{}/events/{}", userId, eventId); + return eventService.updateEventByUserIdAndEventId(userId, eventId, updateEventUserRequest); + } + + @GetMapping("/{eventId}/requests") + public List getEventRequestsByUserIdAndEventId(@PathVariable(value = "userId") Long userId, + @PathVariable(value = "eventId") Long eventId) { + log.info("GET /users/{}/events/{}/requests", userId, eventId); + return eventService.getEventRequestsByUserIdAndEventId(userId, eventId); + } + + @PatchMapping("/{eventId}/requests") + public EventRequestStatusUpdateResult changeStatus(@PathVariable final Long userId, + @PathVariable final Long eventId, + @RequestBody @Valid final EventRequestStatusUpdateRequest eventRequestStatusUpdateRequest) { + log.info("PATCH /users/{}/events/{}/requests", userId, eventId); + return eventService.changeStatus(userId, eventId, eventRequestStatusUpdateRequest); + } +} diff --git a/main/src/main/java/ru/practicum/event/dto/EventFullDto.java b/main/src/main/java/ru/practicum/event/dto/EventFullDto.java new file mode 100644 index 0000000..7b2dbc7 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/dto/EventFullDto.java @@ -0,0 +1,54 @@ +package ru.practicum.event.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.event.state.EventState; +import ru.practicum.location.LocationDto; +import ru.practicum.user.dto.UserShortDto; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class EventFullDto { + private Long id; + + private String annotation; + + private CategoryDto category; + + private Long confirmedRequests; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdOn; + + private String description; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime eventDate; + + private UserShortDto initiator; + + private LocationDto location; + + private Boolean paid; + + private Integer participantLimit; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime publishedOn; + + private Boolean requestModeration; + + private EventState state; + + private String title; + + private Long views; +} diff --git a/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateRequest.java b/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateRequest.java new file mode 100644 index 0000000..698e1c7 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateRequest.java @@ -0,0 +1,21 @@ +package ru.practicum.event.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.request.status.RequestStatus; + +import java.util.List; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class EventRequestStatusUpdateRequest { + @NotNull + List requestIds; + @NotNull + RequestStatus status; +} diff --git a/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateResult.java b/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateResult.java new file mode 100644 index 0000000..9168bd1 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateResult.java @@ -0,0 +1,18 @@ +package ru.practicum.event.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.request.dto.ParticipationRequestDto; + +import java.util.List; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class EventRequestStatusUpdateResult { + List confirmedRequests; + List rejectedRequests; +} diff --git a/main/src/main/java/ru/practicum/event/dto/EventShortDto.java b/main/src/main/java/ru/practicum/event/dto/EventShortDto.java new file mode 100644 index 0000000..1efd876 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/dto/EventShortDto.java @@ -0,0 +1,36 @@ +package ru.practicum.event.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.user.dto.UserShortDto; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class EventShortDto { + private Long id; + + private String annotation; + + private CategoryDto category; + + private Long confirmedRequests; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime eventDate; + + private UserShortDto initiator; + + private Boolean paid; + + private String title; + + private Long views; +} diff --git a/main/src/main/java/ru/practicum/event/dto/NewEventDto.java b/main/src/main/java/ru/practicum/event/dto/NewEventDto.java new file mode 100644 index 0000000..ef1c98f --- /dev/null +++ b/main/src/main/java/ru/practicum/event/dto/NewEventDto.java @@ -0,0 +1,47 @@ +package ru.practicum.event.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.location.LocationDto; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class NewEventDto { + @NotBlank(message = "Аннотация не может быть пустой") + @Size(min = 20, max = 2000, message = "Поле Аннотация должно быть в границах от {min} до {max} символов") + private String annotation; + + @NotNull(message = "Категория обязательна") + private Long category; + + @NotBlank(message = "Описание не может быть пустым") + @Size(min = 20, max = 7000, message = "Поле Описание должно быть в границах от {min} до {max} символов") + private String description; + + @NotNull(message = "Дата события обязательна") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime eventDate; + + @NotNull(message = "Локация обязательна") + private LocationDto location; + + private Boolean paid = false; + + private Integer participantLimit = 0; + + private Boolean requestModeration = true; + + @NotBlank(message = "Заголовок не может быть пустым") + @Size(min = 3, max = 120, message = "Поле Заголовок должно быть в границах от {min} до {max} символов") + private String title; +} diff --git a/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java b/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java new file mode 100644 index 0000000..f99979e --- /dev/null +++ b/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java @@ -0,0 +1,74 @@ +package ru.practicum.event.dto.mapper; + +import lombok.experimental.UtilityClass; +import ru.practicum.category.dto.mapper.CategoryMapper; +import ru.practicum.category.model.Category; +import ru.practicum.event.dto.EventShortDto; +import ru.practicum.event.model.Event; +import ru.practicum.event.dto.EventFullDto; +import ru.practicum.event.dto.NewEventDto; +import ru.practicum.event.state.EventState; +import ru.practicum.location.LocationMapper; +import ru.practicum.user.dto.mapper.UserMapper; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; + +@UtilityClass +public class EventMapper { + public static EventFullDto toEventFullDto(Event event) { + return new EventFullDto( + event.getId(), + event.getAnnotation(), + CategoryMapper.toCategoryDto(event.getCategory()), + event.getConfirmedRequests(), + event.getCreatedOn(), + event.getDescription(), + event.getEventDate(), + UserMapper.toUserShortDto(event.getInitiator()), + LocationMapper.toLocationDto(event.getLocation()), + event.getPaid(), + event.getParticipantLimit(), + event.getPublishedOn(), + event.getRequestModeration(), + event.getState(), + event.getTitle(), + event.getViews() + ); + } + + public static Event toEvent(NewEventDto newEventDto, Category category, User initiator) { + return new Event( + null, + newEventDto.getAnnotation(), + category, + 0L, + LocalDateTime.now(), + newEventDto.getDescription(), + newEventDto.getEventDate(), + initiator, + LocationMapper.toLocation(newEventDto.getLocation()), + newEventDto.getPaid(), + newEventDto.getParticipantLimit(), + null, + newEventDto.getRequestModeration(), + EventState.PENDING, + newEventDto.getTitle(), + 0L + ); + } + + public static EventShortDto toEventShortDto(Event event) { + return new EventShortDto( + event.getId(), + event.getAnnotation(), + CategoryMapper.toCategoryDto(event.getCategory()), + event.getConfirmedRequests(), + event.getEventDate(), + UserMapper.toUserShortDto(event.getInitiator()), + event.getPaid(), + event.getTitle(), + event.getViews() + ); + } +} diff --git a/main/src/main/java/ru/practicum/event/model/Event.java b/main/src/main/java/ru/practicum/event/model/Event.java new file mode 100644 index 0000000..e4d9b9e --- /dev/null +++ b/main/src/main/java/ru/practicum/event/model/Event.java @@ -0,0 +1,88 @@ +package ru.practicum.event.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.category.model.Category; +import ru.practicum.event.state.EventState; +import ru.practicum.location.Location; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; +import java.util.Objects; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Table(name = "events") +@Entity +public class Event { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 2000) + private String annotation; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "category_id") + private Category category; + + @Column(name = "confirmed_requests") + private Long confirmedRequests; + + @Column(name = "created_on", nullable = false) + private LocalDateTime createdOn; + + @Column(length = 7000, nullable = false) + private String description; + + @Column(name = "event_date", nullable = false) + private LocalDateTime eventDate; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "initiator_id") + private User initiator; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "location_id") + private Location location; + + @Column(nullable = false) + private Boolean paid = false; + + @Column(name = "participant_limit", nullable = false) + private Integer participantLimit = 0; + + @Column(name = "published_on") + private LocalDateTime publishedOn; + + @Column(name = "request_moderation", nullable = false) + private Boolean requestModeration = true; + + @Enumerated(EnumType.STRING) + @Column(length = 50, nullable = false) + private EventState state; + + @Column(nullable = false, length = 120) + private String title; + + private Long views; + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Event)) return false; + Event event = (Event) o; + return Objects.equals(id, event.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/main/src/main/java/ru/practicum/event/repository/EventRepository.java b/main/src/main/java/ru/practicum/event/repository/EventRepository.java new file mode 100644 index 0000000..40c65d7 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/repository/EventRepository.java @@ -0,0 +1,15 @@ +package ru.practicum.event.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.event.model.Event; + +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Optional; + +public interface EventRepository extends JpaRepository { + List findByInitiatorId(Long userId, Pageable pageable); + + Optional findByInitiatorIdAndId(Long userId, Long eventId); +} diff --git a/main/src/main/java/ru/practicum/event/service/EventService.java b/main/src/main/java/ru/practicum/event/service/EventService.java new file mode 100644 index 0000000..ab4551e --- /dev/null +++ b/main/src/main/java/ru/practicum/event/service/EventService.java @@ -0,0 +1,22 @@ +package ru.practicum.event.service; + +import ru.practicum.event.dto.*; +import ru.practicum.request.dto.ParticipationRequestDto; + +import java.util.List; + +public interface EventService { + EventFullDto createEvent(Long userId, NewEventDto newEventDto); + + List getEventsByUserId(Long userId, Integer from, Integer size); + + EventFullDto getEventByUserIdAndEventId(Long userId, Long eventId); + + EventFullDto updateEventByUserIdAndEventId(Long userId, Long eventId, UpdateEventUserRequest updateEventUserRequest); + + List getEventRequestsByUserIdAndEventId(Long userId, Long eventId); + + EventRequestStatusUpdateResult changeStatus(final Long userId, + final Long eventId, + final EventRequestStatusUpdateRequest eventRequestStatusUpdateRequest); +} diff --git a/main/src/main/java/ru/practicum/event/service/EventServiceImpl.java b/main/src/main/java/ru/practicum/event/service/EventServiceImpl.java new file mode 100644 index 0000000..a5d640f --- /dev/null +++ b/main/src/main/java/ru/practicum/event/service/EventServiceImpl.java @@ -0,0 +1,218 @@ +package ru.practicum.event.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.category.model.Category; +import ru.practicum.category.repository.CategoryRepository; +import ru.practicum.event.dto.*; +import ru.practicum.event.dto.mapper.EventMapper; +import ru.practicum.event.model.Event; +import ru.practicum.event.repository.EventRepository; +import ru.practicum.event.state.EventState; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.NotFoundException; +import ru.practicum.location.Location; +import ru.practicum.location.LocationRepository; +import ru.practicum.request.dto.ParticipationRequestDto; +import ru.practicum.request.dto.mapper.RequestMapper; +import ru.practicum.request.model.Request; +import ru.practicum.request.repository.RequestRepository; +import ru.practicum.request.status.RequestStatus; +import ru.practicum.user.model.User; +import ru.practicum.user.repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@Transactional(readOnly = true) +@Slf4j +@RequiredArgsConstructor +@Service +public class EventServiceImpl implements EventService { + private final EventRepository eventRepository; + private final UserRepository userRepository; + private final CategoryRepository categoryRepository; + private final RequestRepository requestRepository; + private final LocationRepository locationRepository; + + @Transactional + @Override + public EventFullDto createEvent(Long userId, NewEventDto newEventDto) { + User initiator = findUser(userId); + Category category = findCategory(newEventDto.getCategory()); + validateEventDate(newEventDto.getEventDate()); + + Event event = EventMapper.toEvent(newEventDto, category, initiator); + + if (event.getLocation() != null) { + Location savedLocation = locationRepository.save(event.getLocation()); + event.setLocation(savedLocation); + } + + return EventMapper.toEventFullDto(eventRepository.save(event)); + } + + @Override + public List getEventsByUserId(Long userId, Integer from, Integer size) { + findUser(userId); + + Pageable pageable = PageRequest.of(from / size, size, Sort.by("eventDate").ascending()); + + return eventRepository.findByInitiatorId(userId, pageable) + .stream() + .map(EventMapper::toEventShortDto) + .collect(Collectors.toList()); + } + + @Override + public EventFullDto getEventByUserIdAndEventId(Long userId, Long eventId) { + findUser(userId); + Event event = findEvent(userId, eventId); + + return EventMapper.toEventFullDto(event); + } + + @Transactional + @Override + public EventFullDto updateEventByUserIdAndEventId(Long userId, + Long eventId, + UpdateEventUserRequest updateEventUserRequest) { + findUser(userId); + Event event = findEvent(userId, eventId); + + if (!(event.getState().equals(EventState.PENDING) || event.getState().equals(EventState.CANCELED))) { + log.warn("Изменять можно только события в состоянии ожидания модерации или отмененные. Текущее состояние: {}", event.getState()); + throw new ConflictException("Изменять можно только события в состоянии ожидания модерации или отмененные"); + } + + if (updateEventUserRequest.getEventDate() != null) { + validateEventDate(updateEventUserRequest.getEventDate()); + event.setEventDate(updateEventUserRequest.getEventDate()); + } + + if (updateEventUserRequest.getAnnotation() != null) { + event.setAnnotation(updateEventUserRequest.getAnnotation()); + } + if (updateEventUserRequest.getCategory() != null) { + Category category = findCategory(updateEventUserRequest.getCategory()); + event.setCategory(category); + } + if (updateEventUserRequest.getDescription() != null) { + event.setDescription(updateEventUserRequest.getDescription()); + } + if (updateEventUserRequest.getPaid() != null) { + event.setPaid(updateEventUserRequest.getPaid()); + } + if (updateEventUserRequest.getParticipantLimit() != null) { + event.setParticipantLimit(updateEventUserRequest.getParticipantLimit()); + } + if (updateEventUserRequest.getTitle() != null) { + event.setTitle(updateEventUserRequest.getTitle()); + } + + if (updateEventUserRequest.getStateAction() != null) { + switch (updateEventUserRequest.getStateAction()) { + case SEND_TO_REVIEW -> event.setState(EventState.PENDING); + case CANCEL_REVIEW -> event.setState(EventState.CANCELED); + default -> throw new ConflictException("Недопустимое состояние события для обновления"); + } + } + + return EventMapper.toEventFullDto(eventRepository.save(event)); + } + + @Override + public List getEventRequestsByUserIdAndEventId(Long userId, Long eventId) { + findEvent(userId, eventId); + + List requests = requestRepository.findAllByEventId(eventId); + + return requests.stream() + .map(RequestMapper::toParticipationRequestDto) + .toList(); + } + + @Override + public EventRequestStatusUpdateResult changeStatus(Long userId, Long eventId, EventRequestStatusUpdateRequest eventRequestStatusUpdateRequest) { + Event event = findEvent(userId, eventId); + + if (event.getParticipantLimit() == 0 || Boolean.FALSE.equals(event.getRequestModeration())) { + List requests = requestRepository.findAllByIdIn(eventRequestStatusUpdateRequest.getRequestIds()); + List confirmed = requests.stream() + .map(RequestMapper::toParticipationRequestDto) + .toList(); + return new EventRequestStatusUpdateResult(confirmed, Collections.emptyList()); + } + + long confirmedCount = requestRepository.countByEventIdAndStatus(eventId, RequestStatus.CONFIRMED); + + List requestsToUpdate = requestRepository.findAllByIdIn(eventRequestStatusUpdateRequest.getRequestIds()); + + List confirmedRequests = new ArrayList<>(); + List rejectedRequests = new ArrayList<>(); + + for (Request r : requestsToUpdate) { + if (!r.getStatus().equals(RequestStatus.PENDING)) { + throw new ConflictException("Запрос должен иметь статус PENDING"); + } + + if (eventRequestStatusUpdateRequest.getStatus().equals(RequestStatus.CONFIRMED)) { + if (confirmedCount >= event.getParticipantLimit()) { + throw new ConflictException("Лимит участников достигнут"); + } + r.setStatus(RequestStatus.CONFIRMED); + confirmedCount++; + confirmedRequests.add(RequestMapper.toParticipationRequestDto(r)); + } else if (eventRequestStatusUpdateRequest.getStatus().equals(RequestStatus.REJECTED)) { + r.setStatus(RequestStatus.REJECTED); + rejectedRequests.add(RequestMapper.toParticipationRequestDto(r)); + } else { + throw new ConflictException("Неверный статус для обновления"); + } + + if (confirmedCount >= event.getParticipantLimit()) { + List pendingRequests = requestRepository.findAllByEventIdAndStatus(eventId, RequestStatus.PENDING); + for (Request pr : pendingRequests) { + pr.setStatus(RequestStatus.REJECTED); + rejectedRequests.add(RequestMapper.toParticipationRequestDto(pr)); + } + break; + } + } + requestRepository.saveAll(requestsToUpdate); + + return new EventRequestStatusUpdateResult(confirmedRequests, rejectedRequests); + } + + + private User findUser(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Пользователь по id: " + id + " не найден")); + } + + private Category findCategory(Long id) { + return categoryRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Категория по id: " + id + " не найдена")); + } + + private Event findEvent(Long userId, Long eventId) { + return eventRepository.findByInitiatorIdAndId(userId, eventId) + .orElseThrow(() -> new NotFoundException("Событие текущего пользователя не найдено")); + } + + private void validateEventDate(LocalDateTime date) { + LocalDateTime now = LocalDateTime.now(); + if (date.isBefore(now.plusHours(2))) { + log.warn("Дата события должна быть не менее чем через 2 часа от текущего времени"); + throw new ConflictException("Дата события должна быть не менее чем через 2 часа от текущего времени"); + } + } +} diff --git a/main/src/main/java/ru/practicum/event/state/EventState.java b/main/src/main/java/ru/practicum/event/state/EventState.java new file mode 100644 index 0000000..7849491 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/state/EventState.java @@ -0,0 +1,7 @@ +package ru.practicum.event.state; + +public enum EventState { + PENDING, + PUBLISHED, + CANCELED +} diff --git a/main/src/main/java/ru/practicum/event/state/EventStateAction.java b/main/src/main/java/ru/practicum/event/state/EventStateAction.java new file mode 100644 index 0000000..905addd --- /dev/null +++ b/main/src/main/java/ru/practicum/event/state/EventStateAction.java @@ -0,0 +1,6 @@ +package ru.practicum.event.state; + +public enum EventStateAction { + SEND_TO_REVIEW, + CANCEL_REVIEW +} diff --git a/main/src/main/java/ru/practicum/location/Location.java b/main/src/main/java/ru/practicum/location/Location.java new file mode 100644 index 0000000..989d822 --- /dev/null +++ b/main/src/main/java/ru/practicum/location/Location.java @@ -0,0 +1,41 @@ +package ru.practicum.location; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Objects; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Table(name = "locations") +@Entity +public class Location { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + Float lat; + + @Column(nullable = false) + Float lon; + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Location)) return false; + Location location = (Location) o; + return Objects.equals(id, location.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/main/src/main/java/ru/practicum/location/LocationDto.java b/main/src/main/java/ru/practicum/location/LocationDto.java new file mode 100644 index 0000000..838a582 --- /dev/null +++ b/main/src/main/java/ru/practicum/location/LocationDto.java @@ -0,0 +1,25 @@ +package ru.practicum.location; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class LocationDto { + @Min(-90) + @Max(90) + @NotNull + private Float lat; + + @Min(-180) + @Max(180) + @NotNull + private Float lon; +} diff --git a/main/src/main/java/ru/practicum/location/LocationMapper.java b/main/src/main/java/ru/practicum/location/LocationMapper.java new file mode 100644 index 0000000..22f7ef7 --- /dev/null +++ b/main/src/main/java/ru/practicum/location/LocationMapper.java @@ -0,0 +1,21 @@ +package ru.practicum.location; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class LocationMapper { + public static LocationDto toLocationDto(Location location) { + return new LocationDto( + location.lat, + location.lon + ); + } + + public static Location toLocation(LocationDto locationDto) { + return new Location( + null, + locationDto.getLat(), + locationDto.getLon() + ); + } +} diff --git a/main/src/main/java/ru/practicum/location/LocationRepository.java b/main/src/main/java/ru/practicum/location/LocationRepository.java new file mode 100644 index 0000000..03b0182 --- /dev/null +++ b/main/src/main/java/ru/practicum/location/LocationRepository.java @@ -0,0 +1,6 @@ +package ru.practicum.location; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LocationRepository extends JpaRepository { +} diff --git a/main/src/main/java/ru/practicum/request/controller/RequestPrivateController.java b/main/src/main/java/ru/practicum/request/controller/RequestPrivateController.java new file mode 100644 index 0000000..335a308 --- /dev/null +++ b/main/src/main/java/ru/practicum/request/controller/RequestPrivateController.java @@ -0,0 +1,39 @@ +package ru.practicum.request.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.request.service.RequestService; +import ru.practicum.request.dto.ParticipationRequestDto; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/users/{userId}/requests") +public class RequestPrivateController { + private final RequestService requestService; + + @GetMapping + public List getRequestsByUserId(@PathVariable(value = "userId") Long userId) { + log.info("GET /users/{}/requests", userId); + return requestService.getRequestsByUserId(userId); + } + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public ParticipationRequestDto createRequest(@PathVariable(value = "userId") Long userId, + @RequestParam(name = "eventId") Long eventId) { + log.info("Запрос на участие в событии с id= {}. POST /users/{}/requests", eventId, userId); + return requestService.createRequest(userId, eventId); + } + + @PatchMapping("/{requestId}/cancel") + public ParticipationRequestDto cancelRequest(@PathVariable(value = "userId") Long userId, + @PathVariable(value = "requestId") Long requestId) { + log.info("GET /users/{}/requests/{}/cancel", userId, requestId); + return requestService.cancelRequest(userId, requestId); + } +} diff --git a/main/src/main/java/ru/practicum/request/dto/ParticipationRequestDto.java b/main/src/main/java/ru/practicum/request/dto/ParticipationRequestDto.java new file mode 100644 index 0000000..11e6670 --- /dev/null +++ b/main/src/main/java/ru/practicum/request/dto/ParticipationRequestDto.java @@ -0,0 +1,27 @@ +package ru.practicum.request.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.request.status.RequestStatus; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class ParticipationRequestDto { + private Long id; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime created; + + private Long event; + + private Long requester; + + private RequestStatus status; +} diff --git a/main/src/main/java/ru/practicum/request/dto/mapper/RequestMapper.java b/main/src/main/java/ru/practicum/request/dto/mapper/RequestMapper.java new file mode 100644 index 0000000..c16c6f8 --- /dev/null +++ b/main/src/main/java/ru/practicum/request/dto/mapper/RequestMapper.java @@ -0,0 +1,30 @@ +package ru.practicum.request.dto.mapper; + +import lombok.experimental.UtilityClass; +import ru.practicum.event.model.Event; +import ru.practicum.request.dto.ParticipationRequestDto; +import ru.practicum.request.model.Request; +import ru.practicum.user.model.User; + +@UtilityClass +public class RequestMapper { + public static ParticipationRequestDto toParticipationRequestDto(Request request) { + return new ParticipationRequestDto( + request.getId(), + request.getCreated(), + request.getEvent().getId(), + request.getRequester().getId(), + request.getStatus() + ); + } + + public static Request toRequest(ParticipationRequestDto participationRequestDto, Event event, User requester) { + return new Request( + participationRequestDto.getId(), + participationRequestDto.getCreated(), + event, + requester, + participationRequestDto.getStatus() + ); + } +} diff --git a/main/src/main/java/ru/practicum/request/model/Request.java b/main/src/main/java/ru/practicum/request/model/Request.java new file mode 100644 index 0000000..4ca1119 --- /dev/null +++ b/main/src/main/java/ru/practicum/request/model/Request.java @@ -0,0 +1,42 @@ +package ru.practicum.request.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.event.model.Event; +import ru.practicum.request.status.RequestStatus; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Entity +@Table( + name = "requests", + uniqueConstraints = {@UniqueConstraint(columnNames = {"event_id", "requester_id"})} +) +public class Request { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "create_date", nullable = false) + private LocalDateTime created = LocalDateTime.now(); + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "event_id", nullable = false) + private Event event; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "requester_id", nullable = false) + private User requester; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private RequestStatus status = RequestStatus.PENDING; +} diff --git a/main/src/main/java/ru/practicum/request/repository/RequestRepository.java b/main/src/main/java/ru/practicum/request/repository/RequestRepository.java new file mode 100644 index 0000000..4c382dd --- /dev/null +++ b/main/src/main/java/ru/practicum/request/repository/RequestRepository.java @@ -0,0 +1,21 @@ +package ru.practicum.request.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.request.model.Request; +import ru.practicum.request.status.RequestStatus; + +import java.util.List; + +public interface RequestRepository extends JpaRepository { + Boolean existsByRequesterIdAndEventId(Long requesterId, Long eventId); + + Long countByEventIdAndStatus(Long eventId, RequestStatus status); + + List findAllByRequesterId(Long requesterId); + + List findAllByEventId(Long eventId); + + List findAllByIdIn(List ids); + + List findAllByEventIdAndStatus(Long eventId, RequestStatus status); +} diff --git a/main/src/main/java/ru/practicum/request/service/RequestService.java b/main/src/main/java/ru/practicum/request/service/RequestService.java new file mode 100644 index 0000000..f1dd929 --- /dev/null +++ b/main/src/main/java/ru/practicum/request/service/RequestService.java @@ -0,0 +1,13 @@ +package ru.practicum.request.service; + +import ru.practicum.request.dto.ParticipationRequestDto; + +import java.util.List; + +public interface RequestService { + ParticipationRequestDto createRequest(Long userId, Long eventId); + + List getRequestsByUserId(Long userId); + + ParticipationRequestDto cancelRequest(Long userId, Long requestId); +} diff --git a/main/src/main/java/ru/practicum/request/service/RequestServiceImpl.java b/main/src/main/java/ru/practicum/request/service/RequestServiceImpl.java new file mode 100644 index 0000000..0a4faf7 --- /dev/null +++ b/main/src/main/java/ru/practicum/request/service/RequestServiceImpl.java @@ -0,0 +1,108 @@ +package ru.practicum.request.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.event.model.Event; +import ru.practicum.event.repository.EventRepository; +import ru.practicum.event.state.EventState; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.NotFoundException; +import ru.practicum.request.dto.ParticipationRequestDto; +import ru.practicum.request.dto.mapper.RequestMapper; +import ru.practicum.request.model.Request; +import ru.practicum.request.repository.RequestRepository; +import ru.practicum.request.status.RequestStatus; +import ru.practicum.user.model.User; +import ru.practicum.user.repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Transactional(readOnly = true) +@Slf4j +@RequiredArgsConstructor +@Service +public class RequestServiceImpl implements RequestService { + private final RequestRepository requestRepository; + private final UserRepository userRepository; + private final EventRepository eventRepository; + + @Transactional + @Override + public ParticipationRequestDto createRequest(Long userId, Long eventId) { + User user = findUser(userId); + Event event = findEvent(eventId); + + if (event.getInitiator().getId().equals(userId)) { + throw new ConflictException("Инициатор события не может добавить заявку на участие в своём событии"); + } + + if (!EventState.PUBLISHED.equals(event.getState())) { + throw new ConflictException("Нельзя участвовать в неопубликованном событии"); + } + + boolean exists = requestRepository.existsByRequesterIdAndEventId(userId, eventId); + if (exists) { + throw new ConflictException("Нельзя добавить повторный запрос на участие в событии"); + } + + long confirmedRequests = requestRepository.countByEventIdAndStatus(eventId, RequestStatus.CONFIRMED); + if (event.getParticipantLimit() != null && event.getParticipantLimit() > 0 && + confirmedRequests >= event.getParticipantLimit()) { + throw new ConflictException("Достигнут лимит запросов на участие в событии"); + } + + Request request = new Request(); + request.setEvent(event); + request.setRequester(user); + request.setCreated(LocalDateTime.now()); + + if (Boolean.FALSE.equals(event.getRequestModeration()) || event.getParticipantLimit() == 0) { + request.setStatus(RequestStatus.CONFIRMED); + } else { + request.setStatus(RequestStatus.PENDING); + } + + return RequestMapper.toParticipationRequestDto(requestRepository.save(request)); + } + + @Override + public List getRequestsByUserId(Long userId) { + findUser(userId); + + List requests = requestRepository.findAllByRequesterId(userId); + + return requests.stream() + .map(RequestMapper::toParticipationRequestDto) + .toList(); + } + + @Transactional + @Override + public ParticipationRequestDto cancelRequest(Long userId, Long requestId) { + findUser(userId); + + Request request = requestRepository.findById(requestId) + .orElseThrow(() -> new NotFoundException("Запрос с id:" + requestId + " не найден")); + + if (!request.getRequester().getId().equals(userId)) { + throw new NotFoundException("Заявка с id:" + requestId + " недоступна для пользователя c id:" + userId); + } + + request.setStatus(RequestStatus.CANCELED); + + return RequestMapper.toParticipationRequestDto(requestRepository.save(request)); + } + + private User findUser(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Пользователь с id:" + id + " не найден")); + } + + private Event findEvent(Long id) { + return eventRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Событие с id:" + id + " не найдено")); + } +} diff --git a/main/src/main/java/ru/practicum/request/status/RequestStatus.java b/main/src/main/java/ru/practicum/request/status/RequestStatus.java new file mode 100644 index 0000000..680575b --- /dev/null +++ b/main/src/main/java/ru/practicum/request/status/RequestStatus.java @@ -0,0 +1,8 @@ +package ru.practicum.request.status; + +public enum RequestStatus { + PENDING, + CONFIRMED, + REJECTED, + CANCELED +} diff --git a/main/src/main/resources/schema.sql b/main/src/main/resources/schema.sql index 6238526..af55cb8 100644 --- a/main/src/main/resources/schema.sql +++ b/main/src/main/resources/schema.sql @@ -1,14 +1,56 @@ -create table categories ( +create table IF NOT EXISTS categories ( id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(50) NOT NULL, CONSTRAINT uq_category_name UNIQUE (name), CONSTRAINT pk_category_id PRIMARY KEY (id) ); -create table users ( +create table IF NOT EXISTS users ( id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, email VARCHAR(254) NOT NULL, name VARCHAR(250) NOT NULL, CONSTRAINT uq_user_email UNIQUE (email), CONSTRAINT pk_user_id PRIMARY KEY (id) ); + +create table IF NOT EXISTS locations ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + lat REAL, + lon REAL, + CONSTRAINT pk_location_id PRIMARY KEY (id) +); + +create table IF NOT EXISTS events ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + annotation VARCHAR(2000) NOT NULL, + category_id BIGINT NOT NULL, + confirmed_requests BIGINT, + created_on TIMESTAMP WITHOUT TIME ZONE NOT NULL, + description VARCHAR(7000) NOT NULL, + event_date TIMESTAMP WITHOUT TIME ZONE NOT NULL, + initiator_id BIGINT NOT NULL, + location_id BIGINT NOT NULL, + paid Boolean NOT NULL, + participant_limit INTEGER DEFAULT 0, + published_on TIMESTAMP WITHOUT TIME ZONE, + request_moderation BOOLEAN DEFAULT true, + state VARCHAR(50) NOT NULL, + title VARCHAR(120) NOT NULL, + views BIGINT, + CONSTRAINT fk_event_category FOREIGN KEY (category_id) REFERENCES categories (id), + CONSTRAINT fk_event_user FOREIGN KEY (initiator_id) REFERENCES users (id), + CONSTRAINT fk_event_location FOREIGN KEY (location_id) REFERENCES locations (id), + CONSTRAINT pk_event_id PRIMARY KEY (id) +); + +create table IF NOT EXISTS requests ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + create_date TIMESTAMP WITHOUT TIME ZONE NOT NULL, + event_id BIGINT NOT NULL, + requester_id BIGINT NOT NULL, + status VARCHAR(20) NOT NULL, + CONSTRAINT fk_request_event FOREIGN KEY (event_id) REFERENCES events (id), + CONSTRAINT fk_request_user FOREIGN KEY (requester_id) REFERENCES users (id), + CONSTRAINT uq_request UNIQUE (event_id, requester_id), + CONSTRAINT pk_request_id PRIMARY KEY (id) +); \ No newline at end of file From 2997c82a8ea332e3e1090e56c6fdcab14d1a6e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Wed, 20 Aug 2025 23:31:15 +0300 Subject: [PATCH 08/15] =?UTF-8?q?=D0=92=D0=BD=D0=B5=D1=81=D1=82=D0=B8=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B5=D0=B4=D1=8B=D0=B4=D1=83=D1=89=D0=B8=D0=B9?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ru/practicum/category/service/CategoryService.java | 1 - .../practicum/category/service/CategoryServiceImpl.java | 3 ++- main/src/main/java/ru/practicum/error/ErrorHandler.java | 2 +- .../java/ru/practicum/event/dto/mapper/EventMapper.java | 4 ++-- .../ru/practicum/event/repository/EventRepository.java | 3 +-- main/src/main/java/ru/practicum/location/Location.java | 9 +++------ .../request/controller/RequestPrivateController.java | 2 +- .../practicum/user/controller/UserAdminController.java | 2 +- .../java/ru/practicum/user/dto/mapper/UserMapper.java | 2 +- .../practicum/category/CategoryAdminControllerTest.java | 4 ++-- .../practicum/category/CategoryPublicControllerTest.java | 2 +- 11 files changed, 15 insertions(+), 19 deletions(-) diff --git a/main/src/main/java/ru/practicum/category/service/CategoryService.java b/main/src/main/java/ru/practicum/category/service/CategoryService.java index 486ead5..813ad36 100644 --- a/main/src/main/java/ru/practicum/category/service/CategoryService.java +++ b/main/src/main/java/ru/practicum/category/service/CategoryService.java @@ -1,6 +1,5 @@ package ru.practicum.category.service; -import org.springframework.web.bind.annotation.PathVariable; import ru.practicum.category.dto.CategoryDto; import ru.practicum.category.dto.NewCategoryDto; diff --git a/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java b/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java index 46b7001..f26d4a3 100644 --- a/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java +++ b/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java @@ -1,12 +1,12 @@ package ru.practicum.category.service; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import ru.practicum.category.dto.CategoryDto; import ru.practicum.category.dto.NewCategoryDto; import ru.practicum.category.dto.mapper.CategoryMapper; @@ -18,6 +18,7 @@ import java.util.List; import java.util.stream.Collectors; +@Transactional(readOnly = true) @Slf4j @RequiredArgsConstructor @Service diff --git a/main/src/main/java/ru/practicum/error/ErrorHandler.java b/main/src/main/java/ru/practicum/error/ErrorHandler.java index 8e268be..84dde0c 100644 --- a/main/src/main/java/ru/practicum/error/ErrorHandler.java +++ b/main/src/main/java/ru/practicum/error/ErrorHandler.java @@ -66,7 +66,7 @@ public ApiError handleConflictException(ConflictException e) { public ApiError handleConstraintViolationException(ConstraintViolationException e) { return new ApiError( Collections.emptyList(), - "Категория с таким названием уже существует.", + "В базе данных нарушено ограничение целостности.", "Integrity constraint has been violated.", "CONFLICT", LocalDateTime.now() diff --git a/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java b/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java index f99979e..5b8655b 100644 --- a/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java +++ b/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java @@ -3,10 +3,10 @@ import lombok.experimental.UtilityClass; import ru.practicum.category.dto.mapper.CategoryMapper; import ru.practicum.category.model.Category; -import ru.practicum.event.dto.EventShortDto; -import ru.practicum.event.model.Event; import ru.practicum.event.dto.EventFullDto; +import ru.practicum.event.dto.EventShortDto; import ru.practicum.event.dto.NewEventDto; +import ru.practicum.event.model.Event; import ru.practicum.event.state.EventState; import ru.practicum.location.LocationMapper; import ru.practicum.user.dto.mapper.UserMapper; diff --git a/main/src/main/java/ru/practicum/event/repository/EventRepository.java b/main/src/main/java/ru/practicum/event/repository/EventRepository.java index 40c65d7..cc25b6f 100644 --- a/main/src/main/java/ru/practicum/event/repository/EventRepository.java +++ b/main/src/main/java/ru/practicum/event/repository/EventRepository.java @@ -1,10 +1,9 @@ package ru.practicum.event.repository; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import ru.practicum.event.model.Event; -import org.springframework.data.domain.Pageable; - import java.util.List; import java.util.Optional; diff --git a/main/src/main/java/ru/practicum/location/Location.java b/main/src/main/java/ru/practicum/location/Location.java index 989d822..338d1fd 100644 --- a/main/src/main/java/ru/practicum/location/Location.java +++ b/main/src/main/java/ru/practicum/location/Location.java @@ -15,16 +15,13 @@ @Table(name = "locations") @Entity public class Location { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - @Column(nullable = false) Float lat; - @Column(nullable = false) Float lon; - + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; @Override public boolean equals(Object o) { diff --git a/main/src/main/java/ru/practicum/request/controller/RequestPrivateController.java b/main/src/main/java/ru/practicum/request/controller/RequestPrivateController.java index 335a308..cc3a2c2 100644 --- a/main/src/main/java/ru/practicum/request/controller/RequestPrivateController.java +++ b/main/src/main/java/ru/practicum/request/controller/RequestPrivateController.java @@ -4,8 +4,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; -import ru.practicum.request.service.RequestService; import ru.practicum.request.dto.ParticipationRequestDto; +import ru.practicum.request.service.RequestService; import java.util.List; diff --git a/main/src/main/java/ru/practicum/user/controller/UserAdminController.java b/main/src/main/java/ru/practicum/user/controller/UserAdminController.java index 5262396..9cce4cf 100644 --- a/main/src/main/java/ru/practicum/user/controller/UserAdminController.java +++ b/main/src/main/java/ru/practicum/user/controller/UserAdminController.java @@ -7,9 +7,9 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; -import ru.practicum.user.service.UserService; import ru.practicum.user.dto.NewUserRequest; import ru.practicum.user.dto.UserDto; +import ru.practicum.user.service.UserService; import java.util.List; diff --git a/main/src/main/java/ru/practicum/user/dto/mapper/UserMapper.java b/main/src/main/java/ru/practicum/user/dto/mapper/UserMapper.java index 0e180e1..e10d20f 100644 --- a/main/src/main/java/ru/practicum/user/dto/mapper/UserMapper.java +++ b/main/src/main/java/ru/practicum/user/dto/mapper/UserMapper.java @@ -1,10 +1,10 @@ package ru.practicum.user.dto.mapper; import lombok.experimental.UtilityClass; -import ru.practicum.user.model.User; import ru.practicum.user.dto.NewUserRequest; import ru.practicum.user.dto.UserDto; import ru.practicum.user.dto.UserShortDto; +import ru.practicum.user.model.User; @UtilityClass public class UserMapper { diff --git a/main/src/test/java/ru/practicum/category/CategoryAdminControllerTest.java b/main/src/test/java/ru/practicum/category/CategoryAdminControllerTest.java index ea28014..bc8f9e2 100644 --- a/main/src/test/java/ru/practicum/category/CategoryAdminControllerTest.java +++ b/main/src/test/java/ru/practicum/category/CategoryAdminControllerTest.java @@ -18,14 +18,14 @@ import java.nio.charset.StandardCharsets; +import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.hamcrest.Matchers.is; @ExtendWith(MockitoExtension.class) class CategoryAdminControllerTest { diff --git a/main/src/test/java/ru/practicum/category/CategoryPublicControllerTest.java b/main/src/test/java/ru/practicum/category/CategoryPublicControllerTest.java index 916b7d7..3b0ecbf 100644 --- a/main/src/test/java/ru/practicum/category/CategoryPublicControllerTest.java +++ b/main/src/test/java/ru/practicum/category/CategoryPublicControllerTest.java @@ -20,7 +20,7 @@ import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; From a7de7307a03bf3566e0eaefe3e656532a85e4b3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Wed, 20 Aug 2025 23:38:05 +0300 Subject: [PATCH 09/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20Admin=20=D0=B8=20Public=20API=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=D0=B1=D0=BE=D1=80=D0=BA=D0=B0=D0=BC=D0=B8=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9.=20=D0=9F=D0=BE=D0=BA?= =?UTF-8?q?=D1=80=D1=8B=D1=82=D1=8C=20=D1=82=D0=B5=D1=81=D1=82=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CompilationAdminController.java | 40 +++++ .../CompilationPublicController.java | 35 ++++ .../compilation/dto/CompilationDto.java | 20 +++ .../compilation/dto/NewCompilationDto.java | 24 +++ .../dto/UpdateCompilationRequest.java | 22 +++ .../dto/mapper/CompilationMapper.java | 34 ++++ .../compilation/model/Compilation.java | 37 +++++ .../repository/CompilationRepository.java | 10 ++ .../service/CompilationService.java | 19 +++ .../service/CompilationServiceImpl.java | 110 +++++++++++++ main/src/main/resources/schema.sql | 15 ++ .../CompilationAdminControllerTest.java | 103 ++++++++++++ .../compilation/CompilationDtoTest.java | 94 +++++++++++ .../compilation/CompilationMapperTest.java | 77 +++++++++ .../CompilationPublicControllerTest.java | 90 +++++++++++ .../CompilationServiceImplTest.java | 152 ++++++++++++++++++ .../user/UserAdminControllerTest.java | 4 +- 17 files changed, 884 insertions(+), 2 deletions(-) create mode 100644 main/src/main/java/ru/practicum/compilation/controller/CompilationAdminController.java create mode 100644 main/src/main/java/ru/practicum/compilation/controller/CompilationPublicController.java create mode 100644 main/src/main/java/ru/practicum/compilation/dto/CompilationDto.java create mode 100644 main/src/main/java/ru/practicum/compilation/dto/NewCompilationDto.java create mode 100644 main/src/main/java/ru/practicum/compilation/dto/UpdateCompilationRequest.java create mode 100644 main/src/main/java/ru/practicum/compilation/dto/mapper/CompilationMapper.java create mode 100644 main/src/main/java/ru/practicum/compilation/model/Compilation.java create mode 100644 main/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java create mode 100644 main/src/main/java/ru/practicum/compilation/service/CompilationService.java create mode 100644 main/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java create mode 100644 main/src/test/java/ru/practicum/compilation/CompilationAdminControllerTest.java create mode 100644 main/src/test/java/ru/practicum/compilation/CompilationDtoTest.java create mode 100644 main/src/test/java/ru/practicum/compilation/CompilationMapperTest.java create mode 100644 main/src/test/java/ru/practicum/compilation/CompilationPublicControllerTest.java create mode 100644 main/src/test/java/ru/practicum/compilation/CompilationServiceImplTest.java diff --git a/main/src/main/java/ru/practicum/compilation/controller/CompilationAdminController.java b/main/src/main/java/ru/practicum/compilation/controller/CompilationAdminController.java new file mode 100644 index 0000000..42e3c46 --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/controller/CompilationAdminController.java @@ -0,0 +1,40 @@ +package ru.practicum.compilation.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.dto.NewCompilationDto; +import ru.practicum.compilation.dto.UpdateCompilationRequest; +import ru.practicum.compilation.service.CompilationService; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/admin/compilations") +public class CompilationAdminController { + private final CompilationService compilationService; + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public CompilationDto createCompilation(@RequestBody @Valid NewCompilationDto newCompilationDto) { + log.info("POST /admin/compilations"); + return compilationService.createCompilation(newCompilationDto); + } + + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping("/{compId}") + public void deleteCompilation(@PathVariable("compId") Long id) { + log.info("DELETE /admin/compilations"); + compilationService.deleteCompilation(id); + } + + @PatchMapping("/{compId}") + public CompilationDto updateCompilation(@PathVariable("compId") Long id, + @RequestBody(required = false) @Valid UpdateCompilationRequest updateCompilationRequest) { + log.info("PATCH /admin/compilations/{}", id); + return compilationService.updateCompilation(id, updateCompilationRequest); + } +} diff --git a/main/src/main/java/ru/practicum/compilation/controller/CompilationPublicController.java b/main/src/main/java/ru/practicum/compilation/controller/CompilationPublicController.java new file mode 100644 index 0000000..d46b6b8 --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/controller/CompilationPublicController.java @@ -0,0 +1,35 @@ +package ru.practicum.compilation.controller; + +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.service.CompilationService; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/compilations") +public class CompilationPublicController { + private final CompilationService compilationService; + + @GetMapping + @ResponseStatus(HttpStatus.OK) + public List getCompilations(@RequestParam(required = false) Boolean pinned, + @RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size) { + log.info("GET /compilations"); + return compilationService.getCompilations(pinned, from, size); + } + + @GetMapping("/{compId}") + public CompilationDto getCompilationById(@PathVariable("compId") Long id) { + log.info("GET /compilations/{}", id); + return compilationService.getCompilationById(id); + } +} diff --git a/main/src/main/java/ru/practicum/compilation/dto/CompilationDto.java b/main/src/main/java/ru/practicum/compilation/dto/CompilationDto.java new file mode 100644 index 0000000..c4ab68d --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/dto/CompilationDto.java @@ -0,0 +1,20 @@ +package ru.practicum.compilation.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.event.dto.EventShortDto; + +import java.util.Set; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class CompilationDto { + private Long id; + private String title; + private Boolean pinned; + private Set events; +} diff --git a/main/src/main/java/ru/practicum/compilation/dto/NewCompilationDto.java b/main/src/main/java/ru/practicum/compilation/dto/NewCompilationDto.java new file mode 100644 index 0000000..7f6d1d1 --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/dto/NewCompilationDto.java @@ -0,0 +1,24 @@ +package ru.practicum.compilation.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Set; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class NewCompilationDto { + private Set events; + + private Boolean pinned; + + @NotBlank(message = "Заголовок не может быть пустым") + @Size(min = 1, max = 50, message = "Заголовок должен быть в границах от {min} до {max} символов") + private String title; +} diff --git a/main/src/main/java/ru/practicum/compilation/dto/UpdateCompilationRequest.java b/main/src/main/java/ru/practicum/compilation/dto/UpdateCompilationRequest.java new file mode 100644 index 0000000..61ffca8 --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/dto/UpdateCompilationRequest.java @@ -0,0 +1,22 @@ +package ru.practicum.compilation.dto; + +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Set; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class UpdateCompilationRequest { + private Set events; + + private Boolean pinned; + + @Size(min = 1, max = 50, message = "Заголовок должен быть в границах от {min} до {max} символов") + private String title; +} diff --git a/main/src/main/java/ru/practicum/compilation/dto/mapper/CompilationMapper.java b/main/src/main/java/ru/practicum/compilation/dto/mapper/CompilationMapper.java new file mode 100644 index 0000000..b39e8c7 --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/dto/mapper/CompilationMapper.java @@ -0,0 +1,34 @@ +package ru.practicum.compilation.dto.mapper; + +import lombok.experimental.UtilityClass; +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.dto.NewCompilationDto; +import ru.practicum.compilation.model.Compilation; +import ru.practicum.event.dto.mapper.EventMapper; + +import java.util.HashSet; +import java.util.stream.Collectors; + +@UtilityClass +public class CompilationMapper { + public static CompilationDto toCompilationDto(Compilation compilation) { + return new CompilationDto( + compilation.getId(), + compilation.getTitle(), + compilation.getPinned(), + compilation.getEvents() + .stream() + .map(EventMapper::toEventShortDto) + .collect(Collectors.toSet()) + ); + } + + public static Compilation toCompilation(NewCompilationDto newCompilationDto) { + return new Compilation( + null, + newCompilationDto.getTitle(), + newCompilationDto.getPinned() != null ? newCompilationDto.getPinned() : false, + new HashSet<>() + ); + } +} diff --git a/main/src/main/java/ru/practicum/compilation/model/Compilation.java b/main/src/main/java/ru/practicum/compilation/model/Compilation.java new file mode 100644 index 0000000..4c6aa7d --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/model/Compilation.java @@ -0,0 +1,37 @@ +package ru.practicum.compilation.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.event.model.Event; + +import java.util.HashSet; +import java.util.Set; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Entity +@Table(name = "compilations") +public class Compilation { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(length = 50, nullable = false) + private String title; + + @Column(nullable = false) + private Boolean pinned = false; + + @ManyToMany + @JoinTable( + name = "compilations_events", + joinColumns = @JoinColumn(name = "compilation_id"), + inverseJoinColumns = @JoinColumn(name = "event_id") + ) + private Set events = new HashSet<>(); +} diff --git a/main/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java b/main/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java new file mode 100644 index 0000000..d75872d --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java @@ -0,0 +1,10 @@ +package ru.practicum.compilation.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.compilation.model.Compilation; + +public interface CompilationRepository extends JpaRepository { + Page findAllByPinned(boolean pinned, Pageable pageable); +} diff --git a/main/src/main/java/ru/practicum/compilation/service/CompilationService.java b/main/src/main/java/ru/practicum/compilation/service/CompilationService.java new file mode 100644 index 0000000..641a966 --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/service/CompilationService.java @@ -0,0 +1,19 @@ +package ru.practicum.compilation.service; + +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.dto.NewCompilationDto; +import ru.practicum.compilation.dto.UpdateCompilationRequest; + +import java.util.List; + +public interface CompilationService { + CompilationDto createCompilation(NewCompilationDto newCompilationDto); + + void deleteCompilation(Long id); + + CompilationDto updateCompilation(Long id, UpdateCompilationRequest updateCompilationRequest); + + List getCompilations(Boolean pinned, Integer from, Integer size); + + CompilationDto getCompilationById(Long id); +} diff --git a/main/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java b/main/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java new file mode 100644 index 0000000..f82c1a9 --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java @@ -0,0 +1,110 @@ +package ru.practicum.compilation.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.dto.NewCompilationDto; +import ru.practicum.compilation.dto.UpdateCompilationRequest; +import ru.practicum.compilation.dto.mapper.CompilationMapper; +import ru.practicum.compilation.model.Compilation; +import ru.practicum.compilation.repository.CompilationRepository; +import ru.practicum.event.model.Event; +import ru.practicum.event.repository.EventRepository; +import ru.practicum.exception.NotFoundException; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Transactional(readOnly = true) +@Slf4j +@RequiredArgsConstructor +@Service +public class CompilationServiceImpl implements CompilationService { + private final CompilationRepository compilationRepository; + private final EventRepository eventRepository; + + @Transactional + @Override + public CompilationDto createCompilation(NewCompilationDto newCompilationDto) { + Compilation compilation = CompilationMapper.toCompilation(newCompilationDto); + + if (newCompilationDto.getEvents() != null && !newCompilationDto.getEvents().isEmpty()) { + Set events = new HashSet<>(eventRepository.findAllById(newCompilationDto.getEvents())); + compilation.setEvents(events); + } + + return CompilationMapper.toCompilationDto(compilationRepository.save(compilation)); + } + + @Transactional + @Override + public void deleteCompilation(Long id) { + if (!compilationRepository.existsById(id)) { + log.warn("Подборка с id - {} не найдена", id); + throw new NotFoundException("Подборка с id - " + id + " не найдена"); + } + compilationRepository.deleteById(id); + } + + @Transactional + @Override + public CompilationDto updateCompilation(Long id, UpdateCompilationRequest updateCompilationRequest) { + Compilation compilation = validateCompilation(id); + + if (updateCompilationRequest == null) { + return CompilationMapper.toCompilationDto(compilationRepository.findById(id).get()); + } + + if (updateCompilationRequest.getEvents() != null) { + Set events = new HashSet<>( + eventRepository.findAllById(updateCompilationRequest.getEvents()) + ); + compilation.setEvents(events); + } + + if (updateCompilationRequest.getPinned() != null) { + compilation.setPinned(updateCompilationRequest.getPinned()); + } + + if (updateCompilationRequest.getTitle() != null) { + compilation.setTitle(updateCompilationRequest.getTitle()); + } + + return CompilationMapper.toCompilationDto(compilationRepository.save(compilation)); + } + + @Override + public List getCompilations(Boolean pinned, Integer from, Integer size) { + Pageable pageable = PageRequest.of(from / size, size); + + Page compilations; + if (pinned != null) { + compilations = compilationRepository.findAllByPinned(pinned, pageable); + } else { + compilations = compilationRepository.findAll(pageable); + } + + return compilations.stream() + .map(CompilationMapper::toCompilationDto) + .collect(Collectors.toList()); + } + + @Override + public CompilationDto getCompilationById(Long id) { + Compilation compilation = validateCompilation(id); + + return CompilationMapper.toCompilationDto(compilation); + } + + private Compilation validateCompilation(Long id) { + return compilationRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Подборка с id:" + id + " не найдена")); + } +} diff --git a/main/src/main/resources/schema.sql b/main/src/main/resources/schema.sql index af55cb8..41a7974 100644 --- a/main/src/main/resources/schema.sql +++ b/main/src/main/resources/schema.sql @@ -53,4 +53,19 @@ create table IF NOT EXISTS requests ( CONSTRAINT fk_request_user FOREIGN KEY (requester_id) REFERENCES users (id), CONSTRAINT uq_request UNIQUE (event_id, requester_id), CONSTRAINT pk_request_id PRIMARY KEY (id) +); + +create table IF NOT EXISTS compilations ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + title VARCHAR(50) NOT NULL, + pinned BOOLEAN NOT NULL, + CONSTRAINT pk_compilation_id PRIMARY KEY (id) +); + +create table IF NOT EXISTS compilations_events( + compilation_id BIGINT NOT NULL, + event_id BIGINT NOT NULL, + CONSTRAINT fk_compilations_events_compilation FOREIGN KEY (compilation_id) REFERENCES compilations (id) ON update CASCADE on delete cascade, + CONSTRAINT fk_compilations_events_event FOREIGN KEY (event_id) REFERENCES events (id) ON update CASCADE on delete cascade, + CONSTRAINT pk_compilation_event PRIMARY KEY (compilation_id, event_id) ); \ No newline at end of file diff --git a/main/src/test/java/ru/practicum/compilation/CompilationAdminControllerTest.java b/main/src/test/java/ru/practicum/compilation/CompilationAdminControllerTest.java new file mode 100644 index 0000000..c7b9741 --- /dev/null +++ b/main/src/test/java/ru/practicum/compilation/CompilationAdminControllerTest.java @@ -0,0 +1,103 @@ +package ru.practicum.compilation; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.compilation.controller.CompilationAdminController; +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.dto.NewCompilationDto; +import ru.practicum.compilation.dto.UpdateCompilationRequest; +import ru.practicum.compilation.service.CompilationService; +import ru.practicum.error.ErrorHandler; + +import java.nio.charset.StandardCharsets; +import java.util.Set; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +public class CompilationAdminControllerTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Mock + private CompilationService compilationService; + + @InjectMocks + private CompilationAdminController compilationAdminController; + + private MockMvc mvc; + + private CompilationDto compilationDto; + private NewCompilationDto newCompilationDto; + private UpdateCompilationRequest updateCompilationRequest; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + mvc = MockMvcBuilders + .standaloneSetup(compilationAdminController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + compilationDto = new CompilationDto(1L, "Подборка сериалов", true, Set.of()); + newCompilationDto = new NewCompilationDto(Set.of(), true, "Подборка сериалов"); + updateCompilationRequest = new UpdateCompilationRequest(Set.of(), false, "Следующая подборка"); + } + + @Test + void mustCreateCompilation() throws Exception { + when(compilationService.createCompilation(any())) + .thenReturn(compilationDto); + + mvc.perform(post("/admin/compilations") + .content(mapper.writeValueAsString(newCompilationDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.title", is("Подборка сериалов"))) + .andExpect(jsonPath("$.pinned", is(true))); + } + + @Test + void mustDeleteCompilation() throws Exception { + mvc.perform(delete("/admin/compilations/1")) + .andExpect(status().isNoContent()); + + verify(compilationService).deleteCompilation(1L); + } + + @Test + void mustUpdateCompilation() throws Exception { + CompilationDto updated = new CompilationDto(1L, "Следующая подборка", false, Set.of()); + when(compilationService.updateCompilation(eq(1L), any(UpdateCompilationRequest.class))) + .thenReturn(updated); + + mvc.perform(patch("/admin/compilations/1") + .content(mapper.writeValueAsString(updateCompilationRequest)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.title", is("Следующая подборка"))) + .andExpect(jsonPath("$.pinned", is(false))); + } +} + diff --git a/main/src/test/java/ru/practicum/compilation/CompilationDtoTest.java b/main/src/test/java/ru/practicum/compilation/CompilationDtoTest.java new file mode 100644 index 0000000..636d5b1 --- /dev/null +++ b/main/src/test/java/ru/practicum/compilation/CompilationDtoTest.java @@ -0,0 +1,94 @@ +package ru.practicum.compilation; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.MainServiceApp; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.dto.NewCompilationDto; +import ru.practicum.compilation.dto.UpdateCompilationRequest; +import ru.practicum.event.dto.EventShortDto; +import ru.practicum.user.dto.UserShortDto; + +import java.time.LocalDateTime; +import java.util.Set; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@JsonTest +@ContextConfiguration(classes = MainServiceApp.class) +class CompilationDtoTest { + + @Autowired + private JacksonTester jsonCompilationDto; + + @Autowired + private JacksonTester jsonNewCompilationDto; + + @Autowired + private JacksonTester jsonUpdateCompilationRequest; + + @Test + void shouldReturnCompilationDto() throws Exception { + EventShortDto eventShortDto = new EventShortDto( + 10L, + "Интересный концерт", + new CategoryDto(1L, "Музыка"), + 50L, + LocalDateTime.of(2025, 1, 1, 19, 0), + new UserShortDto(5L, "Макс"), + true, + "Концерт в парке", + 1000L + ); + + CompilationDto compilationDto = new CompilationDto( + 1L, + "Необычные фотозоны", + true, + Set.of(eventShortDto) + ); + + JsonContent result = jsonCompilationDto.write(compilationDto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Необычные фотозоны"); + assertThat(result).extractingJsonPathBooleanValue("$.pinned").isEqualTo(true); + assertThat(result).extractingJsonPathArrayValue("$.events").hasSize(1); + assertThat(result).extractingJsonPathNumberValue("$.events[0].id").isEqualTo(10); + } + + @Test + void shouldReturnNewCompilationDto() throws Exception { + NewCompilationDto dto = new NewCompilationDto( + Set.of(100L, 200L), + false, + "Подборка сериалов" + ); + + JsonContent result = jsonNewCompilationDto.write(dto); + + assertThat(result).extractingJsonPathArrayValue("$.events").containsExactlyInAnyOrder(100, 200); + assertThat(result).extractingJsonPathBooleanValue("$.pinned").isEqualTo(false); + assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Подборка сериалов"); + } + + @Test + void shouldReturnUpdateCompilationRequest() throws Exception { + UpdateCompilationRequest dto = new UpdateCompilationRequest( + Set.of(300L), + true, + "Обновленная подборка" + ); + + JsonContent result = jsonUpdateCompilationRequest.write(dto); + + assertThat(result).extractingJsonPathArrayValue("$.events").containsExactly(300); + assertThat(result).extractingJsonPathBooleanValue("$.pinned").isEqualTo(true); + assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Обновленная подборка"); + } +} \ No newline at end of file diff --git a/main/src/test/java/ru/practicum/compilation/CompilationMapperTest.java b/main/src/test/java/ru/practicum/compilation/CompilationMapperTest.java new file mode 100644 index 0000000..4faea0c --- /dev/null +++ b/main/src/test/java/ru/practicum/compilation/CompilationMapperTest.java @@ -0,0 +1,77 @@ +package ru.practicum.compilation; + +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.Test; +import ru.practicum.category.model.Category; +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.dto.NewCompilationDto; +import ru.practicum.compilation.dto.mapper.CompilationMapper; +import ru.practicum.compilation.model.Compilation; +import ru.practicum.event.model.Event; +import ru.practicum.event.state.EventState; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; + +public class CompilationMapperTest { + @Test + void shouldMapCompilationToCompilationDto() { + Category category = new Category(1L, "Сериал"); + + User user = new User(); + user.setId(10L); + user.setName("Макс Иванов"); + user.setEmail("Max@example.com"); + + Event event = new Event(); + event.setId(100L); + event.setCategory(category); + event.setInitiator(user); + event.setTitle("Друзья"); + event.setAnnotation("Аннотация"); + event.setDescription("Описание"); + event.setCreatedOn(LocalDateTime.now()); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setState(EventState.PUBLISHED); + + Set events = new HashSet<>(); + events.add(event); + + Compilation compilation = new Compilation( + 1L, + "Подборка сериалов", + true, + events + ); + + CompilationDto result = CompilationMapper.toCompilationDto(compilation); + + AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L); + AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Подборка сериалов"); + AssertionsForClassTypes.assertThat(result.getPinned()).isTrue(); + } + + @Test + void shouldMapNewCompilationDtoToCompilationPinnedTrue() { + NewCompilationDto dto = new NewCompilationDto(Set.of(), true, "Подборка сериалов"); + + Compilation result = CompilationMapper.toCompilation(dto); + + AssertionsForClassTypes.assertThat(result.getId()).isNull(); + AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Подборка сериалов"); + AssertionsForClassTypes.assertThat(result.getPinned()).isTrue(); + } + + @Test + void shouldMapNewCompilationDtoToCompilationPinnedNull() { + NewCompilationDto dto = new NewCompilationDto(Set.of(), null, "Подборка сериалов"); + + Compilation result = CompilationMapper.toCompilation(dto); + + AssertionsForClassTypes.assertThat(result.getId()).isNull(); + AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Подборка сериалов"); + AssertionsForClassTypes.assertThat(result.getPinned()).isFalse(); + } +} diff --git a/main/src/test/java/ru/practicum/compilation/CompilationPublicControllerTest.java b/main/src/test/java/ru/practicum/compilation/CompilationPublicControllerTest.java new file mode 100644 index 0000000..2783235 --- /dev/null +++ b/main/src/test/java/ru/practicum/compilation/CompilationPublicControllerTest.java @@ -0,0 +1,90 @@ +package ru.practicum.compilation; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.compilation.controller.CompilationPublicController; +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.service.CompilationService; +import ru.practicum.error.ErrorHandler; + +import java.util.List; +import java.util.Set; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +public class CompilationPublicControllerTest { + private final ObjectMapper mapper = new ObjectMapper(); + + @Mock + private CompilationService compilationService; + + @InjectMocks + private CompilationPublicController compilationController; + + private MockMvc mvc; + + private CompilationDto compilationDto1; + private CompilationDto compilationDto2; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + + mvc = MockMvcBuilders + .standaloneSetup(compilationController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + compilationDto1 = new CompilationDto(1L, "Подборка сериалов", true, Set.of()); + compilationDto2 = new CompilationDto(2L, "Подборка фильмов", false, Set.of()); + } + + @Test + void mustReturnAllCompilations() throws Exception { + List compilations = List.of(compilationDto1, compilationDto2); + + when(compilationService.getCompilations(any(), anyInt(), anyInt())) + .thenReturn(compilations); + + mvc.perform(get("/compilations") + .param("from", "0") + .param("size", "10") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.size()").value(2)) + .andExpect(jsonPath("$[0].id", is(1))) + .andExpect(jsonPath("$[0].title", is("Подборка сериалов"))) + .andExpect(jsonPath("$[0].pinned", is(true))) + .andExpect(jsonPath("$[1].id", is(2))) + .andExpect(jsonPath("$[1].title", is("Подборка фильмов"))) + .andExpect(jsonPath("$[1].pinned", is(false))); + } + + @Test + void mustReturnCompilationById() throws Exception { + when(compilationService.getCompilationById(1L)) + .thenReturn(compilationDto1); + + mvc.perform(get("/compilations/1") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.title", is("Подборка сериалов"))) + .andExpect(jsonPath("$.pinned", is(true))); + } +} diff --git a/main/src/test/java/ru/practicum/compilation/CompilationServiceImplTest.java b/main/src/test/java/ru/practicum/compilation/CompilationServiceImplTest.java new file mode 100644 index 0000000..f696f64 --- /dev/null +++ b/main/src/test/java/ru/practicum/compilation/CompilationServiceImplTest.java @@ -0,0 +1,152 @@ +package ru.practicum.compilation; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.MainServiceApp; +import ru.practicum.category.model.Category; +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.dto.NewCompilationDto; +import ru.practicum.compilation.dto.UpdateCompilationRequest; +import ru.practicum.compilation.model.Compilation; +import ru.practicum.compilation.service.CompilationService; +import ru.practicum.event.model.Event; +import ru.practicum.event.state.EventState; +import ru.practicum.exception.NotFoundException; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Transactional +@ActiveProfiles("test") +@SpringBootTest(classes = MainServiceApp.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class CompilationServiceImplTest { + private final EntityManager em; + private final CompilationService compilationService; + + private Long compilationId; + private Long eventId; + + @BeforeEach + void setUp() { + Category category = new Category(); + category.setName("Сериалы"); + em.persist(category); + + User user = new User(); + user.setName("Макс Иванов"); + user.setEmail("Max@mail.com"); + em.persist(user); + + Event event = new Event(); + event.setTitle("Сериал Друзья"); + event.setAnnotation("Аннотация"); + event.setDescription("Описание"); + event.setCreatedOn(LocalDateTime.now()); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setState(EventState.PUBLISHED); + event.setCategory(category); + event.setInitiator(user); + + em.persist(event); + em.flush(); + + eventId = event.getId(); + + Compilation compilation = new Compilation(); + compilation.setTitle("Подборка сериалов"); + compilation.setPinned(true); + compilation.setEvents(Set.of(event)); + + em.persist(compilation); + em.flush(); + + compilationId = compilation.getId(); + } + + @Test + void mustReturnCompilationById() { + CompilationDto dto = compilationService.getCompilationById(compilationId); + + assertThat(dto.getId()).isEqualTo(compilationId); + assertThat(dto.getTitle()).isEqualTo("Подборка сериалов"); + assertThat(dto.getPinned()).isTrue(); + ; + } + + @Test + void shouldThrowNotFoundExceptionForInvalidId() { + assertThrows(NotFoundException.class, () -> compilationService.getCompilationById(999999L)); + } + + @Test + void mustCreateCompilation() { + NewCompilationDto newDto = new NewCompilationDto(Set.of(eventId), true, "Следующая подборка"); + + CompilationDto created = compilationService.createCompilation(newDto); + + assertThat(created.getId()).isNotNull(); + assertThat(created.getTitle()).isEqualTo("Следующая подборка"); + assertThat(created.getPinned()).isTrue(); + } + + @Test + void mustUpdateCompilation() { + UpdateCompilationRequest updateDto = new UpdateCompilationRequest(Set.of(eventId), false, "Обновленная подборка"); + + CompilationDto updated = compilationService.updateCompilation(compilationId, updateDto); + + assertThat(updated.getId()).isEqualTo(compilationId); + assertThat(updated.getTitle()).isEqualTo("Обновленная подборка"); + assertThat(updated.getPinned()).isFalse(); + } + + @Test + void mustUpdateCompilationWithNullRequest() { + CompilationDto updated = compilationService.updateCompilation(compilationId, null); + + assertThat(updated.getId()).isEqualTo(compilationId); + assertThat(updated.getTitle()).isEqualTo("Подборка сериалов"); + } + + @Test + void mustDeleteCompilation() { + compilationService.deleteCompilation(compilationId); + + assertThrows(NotFoundException.class, () -> compilationService.getCompilationById(compilationId)); + } + + @Test + void mustReturnPaginatedCompilations() { + List compilations = compilationService.getCompilations(null, 0, 10); + + assertThat(compilations).isNotEmpty(); + assertThat(compilations.get(0).getTitle()).isEqualTo("Подборка сериалов"); + } + + @Test + void mustReturnPinnedCompilationsOnly() { + List pinned = compilationService.getCompilations(true, 0, 10); + + assertThat(pinned).hasSize(1); + assertThat(pinned.get(0).getPinned()).isTrue(); + } + + @Test + void mustReturnEmptyListForUnpinnedFilter() { + List unpinned = compilationService.getCompilations(false, 0, 10); + + assertThat(unpinned).isEmpty(); + } +} diff --git a/main/src/test/java/ru/practicum/user/UserAdminControllerTest.java b/main/src/test/java/ru/practicum/user/UserAdminControllerTest.java index 244d23f..2198ef4 100644 --- a/main/src/test/java/ru/practicum/user/UserAdminControllerTest.java +++ b/main/src/test/java/ru/practicum/user/UserAdminControllerTest.java @@ -19,13 +19,13 @@ import java.nio.charset.StandardCharsets; import java.util.List; +import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.hamcrest.Matchers.is; @ExtendWith(MockitoExtension.class) class UserAdminControllerTest { From 27190725d6680e470b94e99f202c08229743efb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Fri, 22 Aug 2025 16:09:05 +0300 Subject: [PATCH 10/15] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=B2=20=D0=BF=D1=80=D0=B5=D0=B4=D1=8B=D0=B4=D1=83=D1=89?= =?UTF-8?q?=D0=B5=D0=BC=20=D0=BA=D0=BE=D0=B4=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../category/service/CategoryServiceImpl.java | 13 +++++++++---- .../java/ru/practicum/error/ErrorHandler.java | 17 ++++++++++++++++- .../controller/EventPrivateController.java | 2 +- .../ru/practicum/event/dto/EventFullDto.java | 2 +- .../dto/EventRequestStatusUpdateRequest.java | 4 ++-- .../event/dto/UpdateEventUserRequest.java | 2 +- .../practicum/event/dto/mapper/EventMapper.java | 2 +- .../java/ru/practicum/event/model/Event.java | 2 +- .../location/{ => dto}/LocationDto.java | 2 +- .../{ => dto/mapper}/LocationMapper.java | 8 +++++--- .../location/{ => model}/Location.java | 12 +++++++----- .../{ => repository}/LocationRepository.java | 3 ++- .../request/dto/mapper/RequestMapper.java | 12 ------------ 13 files changed, 47 insertions(+), 34 deletions(-) rename main/src/main/java/ru/practicum/location/{ => dto}/LocationDto.java (92%) rename main/src/main/java/ru/practicum/location/{ => dto/mapper}/LocationMapper.java (67%) rename main/src/main/java/ru/practicum/location/{ => model}/Location.java (89%) rename main/src/main/java/ru/practicum/location/{ => repository}/LocationRepository.java (62%) diff --git a/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java b/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java index f26d4a3..059062e 100644 --- a/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java +++ b/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java @@ -12,6 +12,7 @@ import ru.practicum.category.dto.mapper.CategoryMapper; import ru.practicum.category.model.Category; import ru.practicum.category.repository.CategoryRepository; +import ru.practicum.event.repository.EventRepository; import ru.practicum.exception.ConflictException; import ru.practicum.exception.NotFoundException; @@ -23,8 +24,8 @@ @RequiredArgsConstructor @Service public class CategoryServiceImpl implements CategoryService { - private final CategoryRepository categoryRepository; + private final EventRepository eventRepository; @Transactional @Override @@ -41,6 +42,7 @@ public CategoryDto createCategory(NewCategoryDto newCategoryDto) { @Override public void deleteCategory(Long id) { findCategoryById(id); + validateCategoryIsEmpty(id); categoryRepository.deleteById(id); } @@ -84,9 +86,12 @@ private Category findCategoryById(Long categoryId) { .orElseThrow(() -> new NotFoundException("Категория с данным id: " + categoryId + " не найдена")); } - // Продолжить реализацию после написания events private void validateCategoryIsEmpty(Long categoryId) { - log.warn("Категория с данным id: {} используется", categoryId); - throw new ConflictException("Категория используется"); + boolean isUsed = eventRepository.existsByCategoryId(categoryId); + + if (isUsed) { + log.warn("Категория с данным id: {} используется", categoryId); + throw new ConflictException("Категория используется"); + } } } diff --git a/main/src/main/java/ru/practicum/error/ErrorHandler.java b/main/src/main/java/ru/practicum/error/ErrorHandler.java index 84dde0c..e7980c8 100644 --- a/main/src/main/java/ru/practicum/error/ErrorHandler.java +++ b/main/src/main/java/ru/practicum/error/ErrorHandler.java @@ -4,11 +4,13 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import ru.practicum.exception.ConflictException; import ru.practicum.exception.NotFoundException; +import ru.practicum.exception.ValidationException; import java.time.LocalDateTime; import java.util.Collections; @@ -30,7 +32,8 @@ public ApiError handleNotFoundException(NotFoundException e) { ); } - @ExceptionHandler({MethodArgumentNotValidException.class}) + @ExceptionHandler({MethodArgumentNotValidException.class, + MissingServletRequestParameterException.class}) @ResponseStatus(HttpStatus.BAD_REQUEST) public ApiError handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { String message = e.getBindingResult().getFieldErrors().stream() @@ -49,6 +52,18 @@ public ApiError handleMethodArgumentNotValidException(MethodArgumentNotValidExce ); } + @ExceptionHandler(ValidationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError handleValidationException(ValidationException e) { + return new ApiError( + Collections.emptyList(), + e.getMessage(), + "Incorrectly made request.", + "BAD_REQUEST", + LocalDateTime.now() + ); + } + @ExceptionHandler({ConflictException.class}) @ResponseStatus(HttpStatus.CONFLICT) public ApiError handleConflictException(ConflictException e) { diff --git a/main/src/main/java/ru/practicum/event/controller/EventPrivateController.java b/main/src/main/java/ru/practicum/event/controller/EventPrivateController.java index 72b263f..08487b2 100644 --- a/main/src/main/java/ru/practicum/event/controller/EventPrivateController.java +++ b/main/src/main/java/ru/practicum/event/controller/EventPrivateController.java @@ -46,7 +46,7 @@ public EventFullDto getEventByUserIdAndEventId(@PathVariable("userId") Long user @PatchMapping("/{eventId}") public EventFullDto updateEventByUserIdAndEventId(@PathVariable("userId") Long userId, @PathVariable("eventId") Long eventId, - @RequestBody UpdateEventUserRequest updateEventUserRequest) { + @RequestBody @Valid UpdateEventUserRequest updateEventUserRequest) { log.info("PATCH /users/{}/events/{}", userId, eventId); return eventService.updateEventByUserIdAndEventId(userId, eventId, updateEventUserRequest); } diff --git a/main/src/main/java/ru/practicum/event/dto/EventFullDto.java b/main/src/main/java/ru/practicum/event/dto/EventFullDto.java index 7b2dbc7..32dda75 100644 --- a/main/src/main/java/ru/practicum/event/dto/EventFullDto.java +++ b/main/src/main/java/ru/practicum/event/dto/EventFullDto.java @@ -7,7 +7,7 @@ import lombok.Setter; import ru.practicum.category.dto.CategoryDto; import ru.practicum.event.state.EventState; -import ru.practicum.location.LocationDto; +import ru.practicum.location.dto.LocationDto; import ru.practicum.user.dto.UserShortDto; import java.time.LocalDateTime; diff --git a/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateRequest.java b/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateRequest.java index 698e1c7..b81139a 100644 --- a/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateRequest.java +++ b/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateRequest.java @@ -14,8 +14,8 @@ @Getter @Setter public class EventRequestStatusUpdateRequest { - @NotNull + @NotNull(message = "Идентификаторы запросов обязательны") List requestIds; - @NotNull + @NotNull(message = "Статус обязателен") RequestStatus status; } diff --git a/main/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java b/main/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java index 41151b4..3d25dd5 100644 --- a/main/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java +++ b/main/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java @@ -8,7 +8,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import ru.practicum.event.state.EventStateAction; -import ru.practicum.location.LocationDto; +import ru.practicum.location.dto.LocationDto; import java.time.LocalDateTime; diff --git a/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java b/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java index 5b8655b..19246cc 100644 --- a/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java +++ b/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java @@ -8,7 +8,7 @@ import ru.practicum.event.dto.NewEventDto; import ru.practicum.event.model.Event; import ru.practicum.event.state.EventState; -import ru.practicum.location.LocationMapper; +import ru.practicum.location.dto.mapper.LocationMapper; import ru.practicum.user.dto.mapper.UserMapper; import ru.practicum.user.model.User; diff --git a/main/src/main/java/ru/practicum/event/model/Event.java b/main/src/main/java/ru/practicum/event/model/Event.java index e4d9b9e..b854677 100644 --- a/main/src/main/java/ru/practicum/event/model/Event.java +++ b/main/src/main/java/ru/practicum/event/model/Event.java @@ -7,7 +7,7 @@ import lombok.Setter; import ru.practicum.category.model.Category; import ru.practicum.event.state.EventState; -import ru.practicum.location.Location; +import ru.practicum.location.model.Location; import ru.practicum.user.model.User; import java.time.LocalDateTime; diff --git a/main/src/main/java/ru/practicum/location/LocationDto.java b/main/src/main/java/ru/practicum/location/dto/LocationDto.java similarity index 92% rename from main/src/main/java/ru/practicum/location/LocationDto.java rename to main/src/main/java/ru/practicum/location/dto/LocationDto.java index 838a582..2547c6c 100644 --- a/main/src/main/java/ru/practicum/location/LocationDto.java +++ b/main/src/main/java/ru/practicum/location/dto/LocationDto.java @@ -1,4 +1,4 @@ -package ru.practicum.location; +package ru.practicum.location.dto; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; diff --git a/main/src/main/java/ru/practicum/location/LocationMapper.java b/main/src/main/java/ru/practicum/location/dto/mapper/LocationMapper.java similarity index 67% rename from main/src/main/java/ru/practicum/location/LocationMapper.java rename to main/src/main/java/ru/practicum/location/dto/mapper/LocationMapper.java index 22f7ef7..996437e 100644 --- a/main/src/main/java/ru/practicum/location/LocationMapper.java +++ b/main/src/main/java/ru/practicum/location/dto/mapper/LocationMapper.java @@ -1,13 +1,15 @@ -package ru.practicum.location; +package ru.practicum.location.dto.mapper; import lombok.experimental.UtilityClass; +import ru.practicum.location.dto.LocationDto; +import ru.practicum.location.model.Location; @UtilityClass public class LocationMapper { public static LocationDto toLocationDto(Location location) { return new LocationDto( - location.lat, - location.lon + location.getLat(), + location.getLon() ); } diff --git a/main/src/main/java/ru/practicum/location/Location.java b/main/src/main/java/ru/practicum/location/model/Location.java similarity index 89% rename from main/src/main/java/ru/practicum/location/Location.java rename to main/src/main/java/ru/practicum/location/model/Location.java index 338d1fd..0b039dc 100644 --- a/main/src/main/java/ru/practicum/location/Location.java +++ b/main/src/main/java/ru/practicum/location/model/Location.java @@ -1,4 +1,4 @@ -package ru.practicum.location; +package ru.practicum.location.model; import jakarta.persistence.*; import lombok.AllArgsConstructor; @@ -15,14 +15,16 @@ @Table(name = "locations") @Entity public class Location { - @Column(nullable = false) - Float lat; - @Column(nullable = false) - Float lon; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false) + private Float lat; + + @Column(nullable = false) + private Float lon; + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/main/src/main/java/ru/practicum/location/LocationRepository.java b/main/src/main/java/ru/practicum/location/repository/LocationRepository.java similarity index 62% rename from main/src/main/java/ru/practicum/location/LocationRepository.java rename to main/src/main/java/ru/practicum/location/repository/LocationRepository.java index 03b0182..c183685 100644 --- a/main/src/main/java/ru/practicum/location/LocationRepository.java +++ b/main/src/main/java/ru/practicum/location/repository/LocationRepository.java @@ -1,6 +1,7 @@ -package ru.practicum.location; +package ru.practicum.location.repository; import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.location.model.Location; public interface LocationRepository extends JpaRepository { } diff --git a/main/src/main/java/ru/practicum/request/dto/mapper/RequestMapper.java b/main/src/main/java/ru/practicum/request/dto/mapper/RequestMapper.java index c16c6f8..f3c04fc 100644 --- a/main/src/main/java/ru/practicum/request/dto/mapper/RequestMapper.java +++ b/main/src/main/java/ru/practicum/request/dto/mapper/RequestMapper.java @@ -1,10 +1,8 @@ package ru.practicum.request.dto.mapper; import lombok.experimental.UtilityClass; -import ru.practicum.event.model.Event; import ru.practicum.request.dto.ParticipationRequestDto; import ru.practicum.request.model.Request; -import ru.practicum.user.model.User; @UtilityClass public class RequestMapper { @@ -17,14 +15,4 @@ public static ParticipationRequestDto toParticipationRequestDto(Request request) request.getStatus() ); } - - public static Request toRequest(ParticipationRequestDto participationRequestDto, Event event, User requester) { - return new Request( - participationRequestDto.getId(), - participationRequestDto.getCreated(), - event, - requester, - participationRequestDto.getStatus() - ); - } } From f694008e7af5c95d3e1e260dfc9f568a930807ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Fri, 22 Aug 2025 16:25:46 +0300 Subject: [PATCH 11/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20Public=20=D0=B8=20Admin=20API=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D1=8F=D0=BC=D0=B8.=20=D0=9F?= =?UTF-8?q?=D0=BE=D0=BA=D1=80=D1=8B=D1=82=D1=8C=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/EventAdminController.java | 44 +++ .../controller/EventPublicController.java | 45 +++ .../ru/practicum/event/dto/NewEventDto.java | 10 +- .../event/dto/UpdateEventAdminRequest.java | 42 +++ .../event/repository/EventRepository.java | 8 +- .../practicum/event/service/EventService.java | 23 ++ .../event/service/EventServiceImpl.java | 178 +++++++++- .../specification/EventSpecifications.java | 81 +++++ .../event/state/EventStateActionSolution.java | 6 + .../event/EventAdminControllerTest.java | 126 +++++++ .../java/ru/practicum/event/EventDtoTest.java | 151 ++++++++ .../ru/practicum/event/EventMapperTest.java | 118 +++++++ .../event/EventPrivateControllerTest.java | 248 +++++++++++++ .../event/EventPublicControllerTest.java | 154 ++++++++ .../practicum/event/EventServiceImplTest.java | 332 ++++++++++++++++++ .../practicum/location/LocationDtoTest.java | 37 ++ .../location/LocationMapperTest.java | 31 ++ .../ru/practicum/request/RequestDtoTest.java | 41 +++ .../practicum/request/RequestMapperTest.java | 39 ++ .../request/RequestPrivateControllerTest.java | 104 ++++++ .../request/RequestServiceImplTest.java | 140 ++++++++ 21 files changed, 1948 insertions(+), 10 deletions(-) create mode 100644 main/src/main/java/ru/practicum/event/controller/EventAdminController.java create mode 100644 main/src/main/java/ru/practicum/event/controller/EventPublicController.java create mode 100644 main/src/main/java/ru/practicum/event/dto/UpdateEventAdminRequest.java create mode 100644 main/src/main/java/ru/practicum/event/specification/EventSpecifications.java create mode 100644 main/src/main/java/ru/practicum/event/state/EventStateActionSolution.java create mode 100644 main/src/test/java/ru/practicum/event/EventAdminControllerTest.java create mode 100644 main/src/test/java/ru/practicum/event/EventDtoTest.java create mode 100644 main/src/test/java/ru/practicum/event/EventMapperTest.java create mode 100644 main/src/test/java/ru/practicum/event/EventPrivateControllerTest.java create mode 100644 main/src/test/java/ru/practicum/event/EventPublicControllerTest.java create mode 100644 main/src/test/java/ru/practicum/event/EventServiceImplTest.java create mode 100644 main/src/test/java/ru/practicum/location/LocationDtoTest.java create mode 100644 main/src/test/java/ru/practicum/location/LocationMapperTest.java create mode 100644 main/src/test/java/ru/practicum/request/RequestDtoTest.java create mode 100644 main/src/test/java/ru/practicum/request/RequestMapperTest.java create mode 100644 main/src/test/java/ru/practicum/request/RequestPrivateControllerTest.java create mode 100644 main/src/test/java/ru/practicum/request/RequestServiceImplTest.java diff --git a/main/src/main/java/ru/practicum/event/controller/EventAdminController.java b/main/src/main/java/ru/practicum/event/controller/EventAdminController.java new file mode 100644 index 0000000..56ee807 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/controller/EventAdminController.java @@ -0,0 +1,44 @@ +package ru.practicum.event.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.*; +import ru.practicum.event.dto.EventFullDto; +import ru.practicum.event.dto.UpdateEventAdminRequest; +import ru.practicum.event.service.EventService; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/admin/events") +public class EventAdminController { + private final EventService eventService; + + @GetMapping + public List getAdminEvents( + @RequestParam(required = false) List users, + @RequestParam(required = false) List states, + @RequestParam(required = false) List categories, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime rangeStart, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime rangeEnd, + @RequestParam(defaultValue = "0") Integer from, + @RequestParam(defaultValue = "10") Integer size + ) { + log.info("GET /admin/events?users={}&states={}&categories={}&rangeStart={}&rangeEnd={}&from={}&size={}", + users, states, categories, rangeStart, rangeEnd, from, size); + + return eventService.getAdminEvents(users, states, categories, rangeStart, rangeEnd, from, size); + } + + @PatchMapping("/{eventId}") + public EventFullDto updateEventByAdmin(@PathVariable Long eventId, + @RequestBody @Valid UpdateEventAdminRequest updateEventAdminRequest) { + log.info("GET /admin/events/{}", eventId); + return eventService.updateEventByAdmin(eventId, updateEventAdminRequest); + } +} diff --git a/main/src/main/java/ru/practicum/event/controller/EventPublicController.java b/main/src/main/java/ru/practicum/event/controller/EventPublicController.java new file mode 100644 index 0000000..8c0718d --- /dev/null +++ b/main/src/main/java/ru/practicum/event/controller/EventPublicController.java @@ -0,0 +1,45 @@ +package ru.practicum.event.controller; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.*; +import ru.practicum.event.dto.EventFullDto; +import ru.practicum.event.dto.EventShortDto; +import ru.practicum.event.service.EventService; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/events") +public class EventPublicController { + private final EventService eventService; + + @GetMapping + public List getEvents( + @RequestParam(required = false) String text, + @RequestParam(required = false) List categories, + @RequestParam(required = false) Boolean paid, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime rangeStart, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime rangeEnd, + @RequestParam(defaultValue = "false") Boolean onlyAvailable, + @RequestParam(required = false) String sort, + @RequestParam(defaultValue = "0") Integer from, + @RequestParam(defaultValue = "10") Integer size, + HttpServletRequest request) { + log.info("GET /events?text={}&categories={}&paid={}&rangeStart={}&rangeEnd={}&onlyAvailable={}&sort={}&from={}&size={}", + text, categories, paid, rangeStart, rangeEnd, onlyAvailable, sort, from, size); + + return eventService.getEvents(text, categories, paid, rangeStart, rangeEnd, onlyAvailable, sort, from, size, request); + } + + @GetMapping("/{id}") + public EventFullDto getEventById(@PathVariable Long id, HttpServletRequest request) { + log.info("GET /events/{}", id); + return eventService.getEventById(id, request); + } +} diff --git a/main/src/main/java/ru/practicum/event/dto/NewEventDto.java b/main/src/main/java/ru/practicum/event/dto/NewEventDto.java index ef1c98f..ca11e49 100644 --- a/main/src/main/java/ru/practicum/event/dto/NewEventDto.java +++ b/main/src/main/java/ru/practicum/event/dto/NewEventDto.java @@ -1,6 +1,7 @@ package ru.practicum.event.dto; import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -8,7 +9,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import ru.practicum.location.LocationDto; +import ru.practicum.location.dto.LocationDto; import java.time.LocalDateTime; @@ -35,11 +36,12 @@ public class NewEventDto { @NotNull(message = "Локация обязательна") private LocationDto location; - private Boolean paid = false; + private Boolean paid; - private Integer participantLimit = 0; + @Min(value = 0, message = "Лимит участников не может быть отрицательным") + private Integer participantLimit; - private Boolean requestModeration = true; + private Boolean requestModeration; @NotBlank(message = "Заголовок не может быть пустым") @Size(min = 3, max = 120, message = "Поле Заголовок должно быть в границах от {min} до {max} символов") diff --git a/main/src/main/java/ru/practicum/event/dto/UpdateEventAdminRequest.java b/main/src/main/java/ru/practicum/event/dto/UpdateEventAdminRequest.java new file mode 100644 index 0000000..42308f5 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/dto/UpdateEventAdminRequest.java @@ -0,0 +1,42 @@ +package ru.practicum.event.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.event.state.EventStateActionSolution; +import ru.practicum.location.dto.LocationDto; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class UpdateEventAdminRequest { + @Size(min = 20, max = 2000, message = "Поле Аннотация должно быть в границах от {min} до {max} символов") + private String annotation; + + private Long category; + + @Size(min = 20, max = 7000, message = "Поле Описание должно быть в границах от {min} до {max} символов") + private String description; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime eventDate; + + private LocationDto location; + + private Boolean paid; + + private Integer participantLimit; + + private Boolean requestModeration; + + private EventStateActionSolution stateAction; + + @Size(min = 3, max = 120, message = "Поле Заголовок должно быть в границах от {min} до {max} символов") + private String title; +} diff --git a/main/src/main/java/ru/practicum/event/repository/EventRepository.java b/main/src/main/java/ru/practicum/event/repository/EventRepository.java index cc25b6f..faae129 100644 --- a/main/src/main/java/ru/practicum/event/repository/EventRepository.java +++ b/main/src/main/java/ru/practicum/event/repository/EventRepository.java @@ -2,13 +2,19 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import ru.practicum.event.model.Event; +import ru.practicum.event.state.EventState; import java.util.List; import java.util.Optional; -public interface EventRepository extends JpaRepository { +public interface EventRepository extends JpaRepository, JpaSpecificationExecutor { List findByInitiatorId(Long userId, Pageable pageable); Optional findByInitiatorIdAndId(Long userId, Long eventId); + + Optional findByIdAndState(Long id, EventState eventState); + + Boolean existsByCategoryId(Long categoryId); } diff --git a/main/src/main/java/ru/practicum/event/service/EventService.java b/main/src/main/java/ru/practicum/event/service/EventService.java index ab4551e..ece6bb7 100644 --- a/main/src/main/java/ru/practicum/event/service/EventService.java +++ b/main/src/main/java/ru/practicum/event/service/EventService.java @@ -1,8 +1,10 @@ package ru.practicum.event.service; +import jakarta.servlet.http.HttpServletRequest; import ru.practicum.event.dto.*; import ru.practicum.request.dto.ParticipationRequestDto; +import java.time.LocalDateTime; import java.util.List; public interface EventService { @@ -19,4 +21,25 @@ public interface EventService { EventRequestStatusUpdateResult changeStatus(final Long userId, final Long eventId, final EventRequestStatusUpdateRequest eventRequestStatusUpdateRequest); + + List getEvents(String text, List categories, Boolean paid, LocalDateTime rangeStart, + LocalDateTime rangeEnd, + Boolean onlyAvailable, + String sort, + Integer from, + Integer size, + HttpServletRequest request); + + EventFullDto getEventById(Long id, HttpServletRequest request); + + List getAdminEvents(List users, + List states, + List categories, + LocalDateTime rangeStart, + LocalDateTime rangeEnd, + Integer from, + Integer size); + + EventFullDto updateEventByAdmin(Long eventId, + UpdateEventAdminRequest updateEventAdminRequest); } diff --git a/main/src/main/java/ru/practicum/event/service/EventServiceImpl.java b/main/src/main/java/ru/practicum/event/service/EventServiceImpl.java index a5d640f..0b3ae30 100644 --- a/main/src/main/java/ru/practicum/event/service/EventServiceImpl.java +++ b/main/src/main/java/ru/practicum/event/service/EventServiceImpl.java @@ -1,23 +1,30 @@ package ru.practicum.event.service; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import ru.practicum.StatsClient; +import ru.practicum.StatsRequestDto; import ru.practicum.category.model.Category; import ru.practicum.category.repository.CategoryRepository; import ru.practicum.event.dto.*; import ru.practicum.event.dto.mapper.EventMapper; import ru.practicum.event.model.Event; import ru.practicum.event.repository.EventRepository; +import ru.practicum.event.specification.EventSpecifications; import ru.practicum.event.state.EventState; import ru.practicum.exception.ConflictException; import ru.practicum.exception.NotFoundException; -import ru.practicum.location.Location; -import ru.practicum.location.LocationRepository; +import ru.practicum.exception.ValidationException; +import ru.practicum.location.dto.mapper.LocationMapper; +import ru.practicum.location.model.Location; +import ru.practicum.location.repository.LocationRepository; import ru.practicum.request.dto.ParticipationRequestDto; import ru.practicum.request.dto.mapper.RequestMapper; import ru.practicum.request.model.Request; @@ -42,6 +49,8 @@ public class EventServiceImpl implements EventService { private final CategoryRepository categoryRepository; private final RequestRepository requestRepository; private final LocationRepository locationRepository; + private final StatsClient statsClient; + @Transactional @Override @@ -52,6 +61,15 @@ public EventFullDto createEvent(Long userId, NewEventDto newEventDto) { Event event = EventMapper.toEvent(newEventDto, category, initiator); + if (event.getPaid() == null) { + event.setPaid(false); + } + if (event.getParticipantLimit() == null) { + event.setParticipantLimit(0); + } + if (event.getRequestModeration() == null) { + event.setRequestModeration(true); + } if (event.getLocation() != null) { Location savedLocation = locationRepository.save(event.getLocation()); event.setLocation(savedLocation); @@ -140,6 +158,7 @@ public List getEventRequestsByUserIdAndEventId(Long use .toList(); } + @Transactional @Override public EventRequestStatusUpdateResult changeStatus(Long userId, Long eventId, EventRequestStatusUpdateRequest eventRequestStatusUpdateRequest) { Event event = findEvent(userId, eventId); @@ -192,15 +211,164 @@ public EventRequestStatusUpdateResult changeStatus(Long userId, Long eventId, Ev return new EventRequestStatusUpdateResult(confirmedRequests, rejectedRequests); } + @Override + public List getEvents(String text, List categories, Boolean paid, LocalDateTime rangeStart, + LocalDateTime rangeEnd, + Boolean onlyAvailable, + String sort, + Integer from, + Integer size, + HttpServletRequest request) { + if (rangeStart != null && rangeEnd != null && rangeEnd.isBefore(rangeStart)) { + log.warn("rangeEnd не может быть меньше rangeStart"); + throw new ValidationException("rangeEnd не может быть меньше rangeStart"); + } + + Specification spec = Specification.where(EventSpecifications.published()) + .and(EventSpecifications.textContains(text)) + .and(EventSpecifications.categoryIn(categories)) + .and(EventSpecifications.paidIs(paid)) + .and(EventSpecifications.dateBetween(rangeStart, rangeEnd)) + .and(EventSpecifications.onlyAvailable(onlyAvailable)); + + Pageable pageable = PageRequest.of(from / size, size, sortBy(sort)); + + List events = eventRepository.findAll(spec, pageable).getContent(); + + statsClient.createHit(new StatsRequestDto("ewm", request.getRequestURI(), request.getRemoteAddr(), LocalDateTime.now())); + + return events.stream() + .map(EventMapper::toEventShortDto) + .collect(Collectors.toList()); + } + + @Override + public EventFullDto getEventById(Long id, HttpServletRequest request) { + Event event = eventRepository.findByIdAndState(id, EventState.PUBLISHED) + .orElseThrow(() -> new NotFoundException("Опубликованное событие с id:" + id + " не найдено")); + + statsClient.createHit(new StatsRequestDto( + "ewm", + request.getRequestURI(), + request.getRemoteAddr(), + LocalDateTime.now() + )); + + event.setViews(event.getViews() + 1); + eventRepository.save(event); + + return EventMapper.toEventFullDto(event); + } + + @Override + public List getAdminEvents(List users, List states, + List categories, + LocalDateTime rangeStart, + LocalDateTime rangeEnd, + Integer from, + Integer size) { + + Specification spec = Specification.where(EventSpecifications.userIn(users)) + .and(EventSpecifications.stateIn(states)) + .and(EventSpecifications.categoryIn(categories)) + .and(EventSpecifications.dateBetween(rangeStart, rangeEnd)); + + Pageable pageable = PageRequest.of(from / size, size, Sort.by(Sort.Direction.ASC, "eventDate")); + + List events = eventRepository.findAll(spec, pageable).getContent(); + + events.forEach(e -> { + long confirmed = requestRepository.countByEventIdAndStatus(e.getId(), RequestStatus.CONFIRMED); + e.setConfirmedRequests(confirmed); + }); + + + return events.stream() + .map(e -> { + EventFullDto dto = EventMapper.toEventFullDto(e); + Long confirmed = requestRepository.countByEventIdAndStatus(e.getId(), RequestStatus.CONFIRMED); + dto.setConfirmedRequests(confirmed); + return dto; + }) + .collect(Collectors.toList()); + } + + @Transactional + @Override + public EventFullDto updateEventByAdmin(Long eventId, UpdateEventAdminRequest updateEventAdminRequest) { + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new NotFoundException("Событие с id:" + eventId + " не найдено")); + + if (updateEventAdminRequest.getAnnotation() != null) { + event.setAnnotation(updateEventAdminRequest.getAnnotation()); + } + if (updateEventAdminRequest.getDescription() != null) { + event.setDescription(updateEventAdminRequest.getDescription()); + } + if (updateEventAdminRequest.getTitle() != null) { + event.setTitle(updateEventAdminRequest.getTitle()); + } + if (updateEventAdminRequest.getCategory() != null) { + Category category = findCategory(updateEventAdminRequest.getCategory()); + event.setCategory(category); + } + if (updateEventAdminRequest.getLocation() != null) { + Location location = locationRepository.save(LocationMapper.toLocation(updateEventAdminRequest.getLocation())); + event.setLocation(location); + } + if (updateEventAdminRequest.getEventDate() != null) { + if (updateEventAdminRequest.getEventDate().isBefore(LocalDateTime.now().plusHours(1))) { + throw new ValidationException("Дата события должна быть не позднее, чем через 1 час после публикации"); + } + event.setEventDate(updateEventAdminRequest.getEventDate()); + } + if (updateEventAdminRequest.getPaid() != null) { + event.setPaid(updateEventAdminRequest.getPaid()); + } + if (updateEventAdminRequest.getParticipantLimit() != null) { + event.setParticipantLimit(updateEventAdminRequest.getParticipantLimit()); + } + if (updateEventAdminRequest.getRequestModeration() != null) { + event.setRequestModeration(updateEventAdminRequest.getRequestModeration()); + } + + if (updateEventAdminRequest.getStateAction() != null) { + switch (updateEventAdminRequest.getStateAction()) { + case PUBLISH_EVENT: + if (event.getState() != EventState.PENDING) { + throw new ConflictException("Невозможно опубликовать событие, так как оно находится в неправильном состоянии: " + event.getState()); + } + event.setState(EventState.PUBLISHED); + event.setPublishedOn(LocalDateTime.now()); + break; + case REJECT_EVENT: + if (event.getState() == EventState.PUBLISHED) { + throw new ConflictException("Невозможно отклонить событие, так как оно уже опубликовано"); + } + event.setState(EventState.CANCELED); + break; + } + } + + return EventMapper.toEventFullDto(eventRepository.save(event)); + } + + private Sort sortBy(String sort) { + if ("views".equalsIgnoreCase(sort)) { + return Sort.by(Sort.Direction.DESC, "views"); + } + return Sort.by(Sort.Direction.ASC, "eventDate"); + } + private User findUser(Long id) { return userRepository.findById(id) - .orElseThrow(() -> new NotFoundException("Пользователь по id: " + id + " не найден")); + .orElseThrow(() -> new NotFoundException("Пользователь с id: " + id + " не найден")); } private Category findCategory(Long id) { return categoryRepository.findById(id) - .orElseThrow(() -> new NotFoundException("Категория по id: " + id + " не найдена")); + .orElseThrow(() -> new NotFoundException("Категория с id: " + id + " не найдена")); } private Event findEvent(Long userId, Long eventId) { @@ -212,7 +380,7 @@ private void validateEventDate(LocalDateTime date) { LocalDateTime now = LocalDateTime.now(); if (date.isBefore(now.plusHours(2))) { log.warn("Дата события должна быть не менее чем через 2 часа от текущего времени"); - throw new ConflictException("Дата события должна быть не менее чем через 2 часа от текущего времени"); + throw new ValidationException("Дата события должна быть не менее чем через 2 часа от текущего времени"); } } } diff --git a/main/src/main/java/ru/practicum/event/specification/EventSpecifications.java b/main/src/main/java/ru/practicum/event/specification/EventSpecifications.java new file mode 100644 index 0000000..df7b205 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/specification/EventSpecifications.java @@ -0,0 +1,81 @@ +package ru.practicum.event.specification; + +import org.springframework.data.jpa.domain.Specification; +import ru.practicum.event.model.Event; +import ru.practicum.event.state.EventState; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +public class EventSpecifications { + public static Specification textContains(String text) { + return (root, query, cb) -> { + if (text == null || text.isBlank()) return null; + String pattern = "%" + text.toLowerCase() + "%"; + return cb.or( + cb.like(cb.lower(root.get("annotation")), pattern), + cb.like(cb.lower(root.get("description")), pattern) + ); + }; + } + + public static Specification categoryIn(List categories) { + return (root, query, cb) -> { + if (categories == null || categories.isEmpty()) return null; + return root.get("category").get("id").in(categories); + }; + } + + public static Specification paidIs(Boolean paid) { + return (root, query, cb) -> { + if (paid == null) return null; + return cb.equal(root.get("paid"), paid); + }; + } + + public static Specification dateBetween(LocalDateTime start, LocalDateTime end) { + LocalDateTime startDate = (start != null) ? start : LocalDateTime.now(); + LocalDateTime endDate = end; + + return (root, query, cb) -> { + if (startDate != null && endDate != null) { + return cb.between(root.get("eventDate"), startDate, endDate); + } else if (startDate != null) { + return cb.greaterThanOrEqualTo(root.get("eventDate"), startDate); + } else if (endDate != null) { + return cb.lessThanOrEqualTo(root.get("eventDate"), endDate); + } else { + return null; + } + }; + } + + public static Specification onlyAvailable(Boolean onlyAvailable) { + return (root, query, cb) -> { + if (onlyAvailable == null || !onlyAvailable) return null; + return cb.greaterThan(root.get("participantLimit"), root.get("confirmedRequests")); + }; + } + + public static Specification published() { + return (root, query, cb) -> cb.equal(root.get("state"), EventState.PUBLISHED); + } + + public static Specification userIn(List userIds) { + return (root, query, cb) -> { + if (userIds == null || userIds.isEmpty()) return null; + return root.get("initiator").get("id").in(userIds); + }; + } + + public static Specification stateIn(List states) { + return (root, query, cb) -> { + if (states == null || states.isEmpty()) return null; + List eventStates = states.stream() + .map(EventState::valueOf) + .collect(Collectors.toList()); + return root.get("state").in(eventStates); + }; + } +} diff --git a/main/src/main/java/ru/practicum/event/state/EventStateActionSolution.java b/main/src/main/java/ru/practicum/event/state/EventStateActionSolution.java new file mode 100644 index 0000000..8740cc3 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/state/EventStateActionSolution.java @@ -0,0 +1,6 @@ +package ru.practicum.event.state; + +public enum EventStateActionSolution { + PUBLISH_EVENT, + REJECT_EVENT +} diff --git a/main/src/test/java/ru/practicum/event/EventAdminControllerTest.java b/main/src/test/java/ru/practicum/event/EventAdminControllerTest.java new file mode 100644 index 0000000..3ce5b48 --- /dev/null +++ b/main/src/test/java/ru/practicum/event/EventAdminControllerTest.java @@ -0,0 +1,126 @@ +package ru.practicum.event; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.error.ErrorHandler; +import ru.practicum.event.controller.EventAdminController; +import ru.practicum.event.dto.EventFullDto; +import ru.practicum.event.dto.UpdateEventAdminRequest; +import ru.practicum.event.service.EventService; +import ru.practicum.location.dto.LocationDto; +import ru.practicum.user.dto.UserShortDto; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +class EventAdminControllerTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Mock + private EventService eventService; + + @InjectMocks + private EventAdminController eventAdminController; + + private MockMvc mvc; + + private EventFullDto eventFullDto; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + + mvc = MockMvcBuilders + .standaloneSetup(eventAdminController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + CategoryDto categoryDto = new CategoryDto(1L, "Категория"); + UserShortDto userShortDto = new UserShortDto(1L, "Макс Иванов"); + LocationDto locationDto = new LocationDto(50.01f, 31.25f); + + eventFullDto = new EventFullDto( + 1L, + "Аннотация", + categoryDto, + 5L, + LocalDateTime.now(), + "Описание", + LocalDateTime.now().plusDays(1), + userShortDto, + locationDto, + true, + 10, + LocalDateTime.now(), + true, + null, + "Заголовок", + 100L + ); + } + + @Test + void mustReturnAdminEvents() throws Exception { + List states = List.of("PUBLISHED"); + List users = List.of(1L, 2L); + List categories = List.of(1L, 2L); + + when(eventService.getAdminEvents( + eq(users), eq(states), eq(categories), + any(), any(), eq(0), eq(10) + )).thenReturn(List.of(eventFullDto)); + + mvc.perform(get("/admin/events") + .param("users", "1", "2") + .param("states", "PUBLISHED") + .param("categories", "1", "2") + .param("from", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(eventFullDto.getId())) + .andExpect(jsonPath("$[0].annotation").value(eventFullDto.getAnnotation())) + .andExpect(jsonPath("$[0].category.id").value(eventFullDto.getCategory().getId())) + .andExpect(jsonPath("$[0].category.name").value(eventFullDto.getCategory().getName())) + .andExpect(jsonPath("$[0].description").value(eventFullDto.getDescription())) + .andExpect(jsonPath("$[0].title").value(eventFullDto.getTitle())); + } + + @Test + void mustUpdateEventByAdmin() throws Exception { + UpdateEventAdminRequest updateRequest = new UpdateEventAdminRequest(); + updateRequest.setAnnotation("Обновленная аннотация"); + updateRequest.setDescription("Обновленное описание"); + updateRequest.setTitle("Обновленный заголовок"); + + when(eventService.updateEventByAdmin(eq(1L), any(UpdateEventAdminRequest.class))) + .thenReturn(eventFullDto); + + mvc.perform(patch("/admin/events/1") + .content(mapper.writeValueAsString(updateRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(eventFullDto.getId())) + .andExpect(jsonPath("$.annotation").value(eventFullDto.getAnnotation())) + .andExpect(jsonPath("$.description").value(eventFullDto.getDescription())) + .andExpect(jsonPath("$.title").value(eventFullDto.getTitle())); + } +} diff --git a/main/src/test/java/ru/practicum/event/EventDtoTest.java b/main/src/test/java/ru/practicum/event/EventDtoTest.java new file mode 100644 index 0000000..2cc2048 --- /dev/null +++ b/main/src/test/java/ru/practicum/event/EventDtoTest.java @@ -0,0 +1,151 @@ +package ru.practicum.event; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.MainServiceApp; +import ru.practicum.event.dto.*; +import ru.practicum.request.status.RequestStatus; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +@ContextConfiguration(classes = MainServiceApp.class) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class EventDtoTest { + + private final JacksonTester jsonEventFullDto; + private final JacksonTester jsonEventShortDto; + private final JacksonTester jsonNewEventDto; + private final JacksonTester jsonUpdateEventAdminRequest; + private final JacksonTester jsonUpdateEventUserRequest; + private final JacksonTester jsonEventRequestStatusUpdateRequest; + private final JacksonTester jsonEventRequestStatusUpdateResult; + + @Test + void shouldReturnEventFullDto() throws Exception { + EventFullDto dto = new EventFullDto( + 1L, "Аннотация", null, 5L, + LocalDateTime.of(2025, 5, 1, 12, 0, 0), "Описание", + LocalDateTime.of(2025, 5, 2, 12, 0, 0), null, null, + true, 10, LocalDateTime.of(2025, 5, 3, 12, 0, 0), + true, null, "Концерт в парке", 100L + ); + + JsonContent result = jsonEventFullDto.write(dto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(result).extractingJsonPathStringValue("$.annotation").isEqualTo("Аннотация"); + assertThat(result).extractingJsonPathStringValue("$.description").isEqualTo("Описание"); + assertThat(result).extractingJsonPathStringValue("$.createdOn").isEqualTo("2025-05-01 12:00:00"); + assertThat(result).extractingJsonPathStringValue("$.eventDate").isEqualTo("2025-05-02 12:00:00"); + assertThat(result).extractingJsonPathStringValue("$.publishedOn").isEqualTo("2025-05-03 12:00:00"); + assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Концерт в парке"); + assertThat(result).extractingJsonPathNumberValue("$.confirmedRequests").isEqualTo(5); + assertThat(result).extractingJsonPathNumberValue("$.participantLimit").isEqualTo(10); + assertThat(result).extractingJsonPathNumberValue("$.views").isEqualTo(100); + assertThat(result).extractingJsonPathBooleanValue("$.paid").isTrue(); + assertThat(result).extractingJsonPathBooleanValue("$.requestModeration").isTrue(); + } + + @Test + void shouldReturnEventShortDto() throws Exception { + EventShortDto dto = new EventShortDto(1L, "Аннотация", null, 5L, + LocalDateTime.of(2025, 1, 2, 12, 0, 0), null, true, "Концерт в парке", 100L); + + JsonContent result = jsonEventShortDto.write(dto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(result).extractingJsonPathStringValue("$.annotation").isEqualTo("Аннотация"); + assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Концерт в парке"); + assertThat(result).extractingJsonPathBooleanValue("$.paid").isTrue(); + assertThat(result).extractingJsonPathNumberValue("$.views").isEqualTo(100); + } + + @Test + void shouldReturnNewEventDto() throws Exception { + NewEventDto dto = new NewEventDto( + "Аннотация", + 1L, + "Описание", + LocalDateTime.of(2025, 5, 2, 12, 0, 0), + null, + true, + 10, + true, + "Концерт в парке" + ); + + JsonContent result = jsonNewEventDto.write(dto); + + assertThat(result).extractingJsonPathStringValue("$.annotation") + .isEqualTo("Аннотация"); + assertThat(result).extractingJsonPathStringValue("$.description") + .isEqualTo("Описание"); + assertThat(result).extractingJsonPathStringValue("$.title") + .isEqualTo("Концерт в парке"); + } + + @Test + void shouldReturnUpdateEventAdminRequest() throws Exception { + UpdateEventAdminRequest dto = new UpdateEventAdminRequest(); + dto.setAnnotation("Аннотация"); + dto.setDescription("Описание"); + dto.setTitle("Концерт в парке"); + + JsonContent result = jsonUpdateEventAdminRequest.write(dto); + + assertThat(result).extractingJsonPathStringValue("$.annotation") + .isEqualTo("Аннотация"); + assertThat(result).extractingJsonPathStringValue("$.description") + .isEqualTo("Описание"); + assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Концерт в парке"); + } + + @Test + void shouldReturnUpdateEventUserRequest() throws Exception { + UpdateEventUserRequest dto = new UpdateEventUserRequest(); + dto.setAnnotation("Аннотация"); + dto.setDescription("Описание"); + dto.setTitle("Концерт в парке"); + + JsonContent result = jsonUpdateEventUserRequest.write(dto); + + assertThat(result).extractingJsonPathStringValue("$.annotation") + .isEqualTo("Аннотация"); + assertThat(result).extractingJsonPathStringValue("$.description") + .isEqualTo("Описание"); + assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Концерт в парке"); + } + + @Test + void shouldReturnEventRequestStatusUpdateRequest() throws Exception { + EventRequestStatusUpdateRequest dto = new EventRequestStatusUpdateRequest( + List.of(1L, 2L), RequestStatus.CONFIRMED + ); + + JsonContent result = jsonEventRequestStatusUpdateRequest.write(dto); + + assertThat(result).extractingJsonPathArrayValue("$.requestIds").containsExactly(1, 2); + assertThat(result).extractingJsonPathStringValue("$.status").isEqualTo("CONFIRMED"); + } + + @Test + void shouldReturnEventRequestStatusUpdateResult() throws Exception { + EventRequestStatusUpdateResult dto = new EventRequestStatusUpdateResult( + List.of(), List.of() + ); + + JsonContent result = jsonEventRequestStatusUpdateResult.write(dto); + + assertThat(result).extractingJsonPathArrayValue("$.confirmedRequests").isEmpty(); + assertThat(result).extractingJsonPathArrayValue("$.rejectedRequests").isEmpty(); + } +} diff --git a/main/src/test/java/ru/practicum/event/EventMapperTest.java b/main/src/test/java/ru/practicum/event/EventMapperTest.java new file mode 100644 index 0000000..99676fa --- /dev/null +++ b/main/src/test/java/ru/practicum/event/EventMapperTest.java @@ -0,0 +1,118 @@ +package ru.practicum.event; + +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.Test; +import ru.practicum.category.model.Category; +import ru.practicum.event.dto.EventFullDto; +import ru.practicum.event.dto.EventShortDto; +import ru.practicum.event.dto.NewEventDto; +import ru.practicum.event.dto.mapper.EventMapper; +import ru.practicum.event.model.Event; +import ru.practicum.event.state.EventState; +import ru.practicum.location.dto.LocationDto; +import ru.practicum.location.model.Location; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; + +public class EventMapperTest { + @Test + void shouldMapEventToEventFullDto() { + Category category = new Category(1L, "Концерт"); + User initiator = new User(1L, "Макс Иванов", "Max@mail.ru"); + Location location = new Location(1L, 50.01f, 31.25f); + + Event event = new Event( + 1L, + "Аннотация", + category, + 5L, + LocalDateTime.of(2025, 5, 1, 12, 0), + "Описание", + LocalDateTime.of(2025, 5, 2, 12, 0), + initiator, + location, + true, + 10, + LocalDateTime.of(2025, 5, 3, 12, 0), + true, + EventState.PUBLISHED, + "Заголовок", + 100L + ); + + EventFullDto result = EventMapper.toEventFullDto(event); + + AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L); + AssertionsForClassTypes.assertThat(result.getAnnotation()).isEqualTo("Аннотация"); + AssertionsForClassTypes.assertThat(result.getDescription()).isEqualTo("Описание"); + AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Заголовок"); + AssertionsForClassTypes.assertThat(result.getPaid()).isTrue(); + AssertionsForClassTypes.assertThat(result.getParticipantLimit()).isEqualTo(10); + AssertionsForClassTypes.assertThat(result.getConfirmedRequests()).isEqualTo(5L); + AssertionsForClassTypes.assertThat(result.getViews()).isEqualTo(100L); + } + + @Test + void shouldMapEventToEventShortDto() { + Category category = new Category(1L, "Концерт"); + User initiator = new User(1L, "Макс Иванов", "Max@mail.ru"); + Location location = new Location(1L, 50.01f, 31.25f); + + Event event = new Event( + 1L, + "Аннотация", + category, + 5L, + LocalDateTime.of(2025, 5, 1, 12, 0), + "Описание", + LocalDateTime.of(2025, 5, 2, 12, 0), + initiator, + location, + true, + 10, + LocalDateTime.of(2025, 5, 3, 12, 0), + true, + EventState.PUBLISHED, + "Заголовок", + 100L + ); + + EventShortDto result = EventMapper.toEventShortDto(event); + + AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L); + AssertionsForClassTypes.assertThat(result.getAnnotation()).isEqualTo("Аннотация"); + AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Заголовок"); + AssertionsForClassTypes.assertThat(result.getPaid()).isTrue(); + AssertionsForClassTypes.assertThat(result.getConfirmedRequests()).isEqualTo(5L); + AssertionsForClassTypes.assertThat(result.getViews()).isEqualTo(100L); + } + + @Test + void shouldMapNewEventDtoToEvent() { + NewEventDto newEventDto = new NewEventDto( + "Аннотация", + 1L, + "Описание", + LocalDateTime.of(2025, 5, 2, 12, 0), + new LocationDto(50.01f, 31.25f), + true, + 10, + true, + "Заголовок" + ); + + Category category = new Category(1L, "Концерт"); + User initiator = new User(1L, "Макс Иванов", "Max@mail.ru"); + + Event result = EventMapper.toEvent(newEventDto, category, initiator); + + AssertionsForClassTypes.assertThat(result.getId()).isNull(); + AssertionsForClassTypes.assertThat(result.getAnnotation()).isEqualTo("Аннотация"); + AssertionsForClassTypes.assertThat(result.getDescription()).isEqualTo("Описание"); + AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Заголовок"); + AssertionsForClassTypes.assertThat(result.getPaid()).isTrue(); + AssertionsForClassTypes.assertThat(result.getParticipantLimit()).isEqualTo(10); + AssertionsForClassTypes.assertThat(result.getConfirmedRequests()).isEqualTo(0L); + } +} diff --git a/main/src/test/java/ru/practicum/event/EventPrivateControllerTest.java b/main/src/test/java/ru/practicum/event/EventPrivateControllerTest.java new file mode 100644 index 0000000..404d3f0 --- /dev/null +++ b/main/src/test/java/ru/practicum/event/EventPrivateControllerTest.java @@ -0,0 +1,248 @@ +package ru.practicum.event; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.error.ErrorHandler; +import ru.practicum.event.controller.EventPrivateController; +import ru.practicum.event.dto.*; +import ru.practicum.event.service.EventService; +import ru.practicum.location.dto.LocationDto; +import ru.practicum.request.dto.ParticipationRequestDto; +import ru.practicum.request.status.RequestStatus; +import ru.practicum.user.dto.UserShortDto; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@ExtendWith(MockitoExtension.class) +class EventPrivateControllerTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Mock + private EventService eventService; + + @InjectMocks + private EventPrivateController eventPrivateController; + + private MockMvc mvc; + + private EventFullDto eventFullDto; + private EventShortDto eventShortDto; + private NewEventDto newEventDto; + private UpdateEventUserRequest updateEventUserRequest; + private EventRequestStatusUpdateRequest updateRequest; + private EventRequestStatusUpdateResult updateResult; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + + mvc = MockMvcBuilders + .standaloneSetup(eventPrivateController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + CategoryDto categoryDto = new CategoryDto(1L, "Категория"); + UserShortDto userShortDto = new UserShortDto(1L, "Макс Иванов"); + LocationDto locationDto = new LocationDto(50.01f, 31.25f); + + eventFullDto = new EventFullDto(1L, + "Аннотация", + categoryDto, + 5L, + LocalDateTime.now(), + "Описание", + LocalDateTime.now().plusDays(1), + userShortDto, + locationDto, + true, + 10, + LocalDateTime.now(), + true, + null, + "Заголовок", + 100L + ); + + eventShortDto = new EventShortDto(1L, + "Аннотация", + null, + 5L, + LocalDateTime.now().plusDays(1), + null, + true, + "Заголовок", + 100L + ); + + newEventDto = new NewEventDto( + "Краткое описание, сжатый пересказ сути", + 1L, + "Наглядное представление о предмете", + LocalDateTime.now().plusDays(1), + locationDto, + true, + 10, + true, + "Заголовок" + ); + + updateEventUserRequest = new UpdateEventUserRequest( + "Обновленная аннотация", + 1L, + "Обновленное описание", + LocalDateTime.now().plusDays(2), + null, + true, + 15, + true, + null, + "Обновленный заголовок" + ); + + updateRequest = new EventRequestStatusUpdateRequest(); + updateRequest.setStatus(RequestStatus.CONFIRMED); + updateRequest.setRequestIds(List.of(1L, 2L)); + + updateResult = new EventRequestStatusUpdateResult( + List.of(new ParticipationRequestDto(1L, LocalDateTime.now(), 1L, 1L, RequestStatus.CONFIRMED)), + List.of() + ); + } + + @Test + void mustReturnEventsByUserId() throws Exception { + when(eventService.getEventsByUserId(eq(1L), anyInt(), anyInt())) + .thenReturn(List.of(eventShortDto)); + + mvc.perform(get("/users/1/events") + .param("from", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(eventShortDto.getId())) + .andExpect(jsonPath("$[0].annotation").value(eventShortDto.getAnnotation())) + .andExpect(jsonPath("$[0].category").doesNotExist()) + .andExpect(jsonPath("$[0].confirmedRequests").value(eventShortDto.getConfirmedRequests())) + .andExpect(jsonPath("$[0].eventDate").value(eventShortDto.getEventDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$[0].initiator").doesNotExist()) + .andExpect(jsonPath("$[0].paid").value(eventShortDto.getPaid())) + .andExpect(jsonPath("$[0].title").value(eventShortDto.getTitle())) + .andExpect(jsonPath("$[0].views").value(eventShortDto.getViews())); + } + + @Test + void mustCreateEvent() throws Exception { + when(eventService.createEvent(eq(1L), any(NewEventDto.class))) + .thenReturn(eventFullDto); + + mvc.perform(post("/users/1/events") + .content(mapper.writeValueAsString(newEventDto)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(eventFullDto.getId())) + .andExpect(jsonPath("$.annotation").value(eventFullDto.getAnnotation())) + .andExpect(jsonPath("$.category.id").value(eventFullDto.getCategory().getId())) + .andExpect(jsonPath("$.category.name").value(eventFullDto.getCategory().getName())) + .andExpect(jsonPath("$.confirmedRequests").value(eventFullDto.getConfirmedRequests())) + .andExpect(jsonPath("$.createdOn").value(eventFullDto.getCreatedOn().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$.description").value(eventFullDto.getDescription())) + .andExpect(jsonPath("$.eventDate").value(eventFullDto.getEventDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$.initiator.id").value(eventFullDto.getInitiator().getId())) + .andExpect(jsonPath("$.initiator.name").value(eventFullDto.getInitiator().getName())) + .andExpect(jsonPath("$.location.lat").value(eventFullDto.getLocation().getLat())) + .andExpect(jsonPath("$.location.lon").value(eventFullDto.getLocation().getLon())) + .andExpect(jsonPath("$.paid").value(eventFullDto.getPaid())) + .andExpect(jsonPath("$.participantLimit").value(eventFullDto.getParticipantLimit())) + .andExpect(jsonPath("$.publishedOn").value(eventFullDto.getPublishedOn().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$.requestModeration").value(eventFullDto.getRequestModeration())) + .andExpect(jsonPath("$.state").doesNotExist()) + .andExpect(jsonPath("$.title").value(eventFullDto.getTitle())) + .andExpect(jsonPath("$.views").value(eventFullDto.getViews())); + } + + @Test + void mustReturnEventByUserIdAndEventId() throws Exception { + when(eventService.getEventByUserIdAndEventId(1L, 1L)) + .thenReturn(eventFullDto); + + mvc.perform(get("/users/1/events/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(eventFullDto.getId())) + .andExpect(jsonPath("$.annotation").value(eventFullDto.getAnnotation())) + .andExpect(jsonPath("$.category.id").value(eventFullDto.getCategory().getId())) + .andExpect(jsonPath("$.category.name").value(eventFullDto.getCategory().getName())) + .andExpect(jsonPath("$.confirmedRequests").value(eventFullDto.getConfirmedRequests())) + .andExpect(jsonPath("$.createdOn").value(eventFullDto.getCreatedOn().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$.description").value(eventFullDto.getDescription())) + .andExpect(jsonPath("$.eventDate").value(eventFullDto.getEventDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$.initiator.id").value(eventFullDto.getInitiator().getId())) + .andExpect(jsonPath("$.initiator.name").value(eventFullDto.getInitiator().getName())) + .andExpect(jsonPath("$.location.lat").value(eventFullDto.getLocation().getLat())) + .andExpect(jsonPath("$.location.lon").value(eventFullDto.getLocation().getLon())) + .andExpect(jsonPath("$.paid").value(eventFullDto.getPaid())) + .andExpect(jsonPath("$.participantLimit").value(eventFullDto.getParticipantLimit())) + .andExpect(jsonPath("$.publishedOn").value(eventFullDto.getPublishedOn().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$.requestModeration").value(eventFullDto.getRequestModeration())) + .andExpect(jsonPath("$.state").doesNotExist()) + .andExpect(jsonPath("$.title").value(eventFullDto.getTitle())) + .andExpect(jsonPath("$.views").value(eventFullDto.getViews())); + } + + @Test + void mustUpdateEventByUserIdAndEventId() throws Exception { + when(eventService.updateEventByUserIdAndEventId(eq(1L), eq(1L), any(UpdateEventUserRequest.class))) + .thenReturn(eventFullDto); + + mvc.perform(patch("/users/1/events/1") + .content(mapper.writeValueAsString(updateEventUserRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(eventFullDto.getId())) + .andExpect(jsonPath("$.annotation").value(eventFullDto.getAnnotation())) + .andExpect(jsonPath("$.description").value(eventFullDto.getDescription())) + .andExpect(jsonPath("$.title").value(eventFullDto.getTitle())); + } + + @Test + void mustReturnEventRequests() throws Exception { + when(eventService.getEventRequestsByUserIdAndEventId(1L, 1L)) + .thenReturn(List.of()); + + mvc.perform(get("/users/1/events/1/requests")) + .andExpect(status().isOk()) + .andExpect(content().json("[]")); + } + + @Test + void mustChangeStatus() throws Exception { + when(eventService.changeStatus(eq(1L), eq(1L), any(EventRequestStatusUpdateRequest.class))) + .thenReturn(updateResult); + + mvc.perform(patch("/users/1/events/1/requests") + .content(mapper.writeValueAsString(updateRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.confirmedRequests[0].id").value(updateResult.getConfirmedRequests().get(0).getId())) + .andExpect(jsonPath("$.confirmedRequests[0].event").value(updateResult.getConfirmedRequests().get(0).getEvent())) + .andExpect(jsonPath("$.confirmedRequests[0].requester").value(updateResult.getConfirmedRequests().get(0).getRequester())) + .andExpect(jsonPath("$.confirmedRequests[0].status").value(updateResult.getConfirmedRequests().get(0).getStatus().toString())) + .andExpect(jsonPath("$.rejectedRequests").isEmpty()); + } +} diff --git a/main/src/test/java/ru/practicum/event/EventPublicControllerTest.java b/main/src/test/java/ru/practicum/event/EventPublicControllerTest.java new file mode 100644 index 0000000..775c92a --- /dev/null +++ b/main/src/test/java/ru/practicum/event/EventPublicControllerTest.java @@ -0,0 +1,154 @@ +package ru.practicum.event; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.error.ErrorHandler; +import ru.practicum.event.controller.EventPublicController; +import ru.practicum.event.dto.EventFullDto; +import ru.practicum.event.dto.EventShortDto; +import ru.practicum.event.service.EventService; +import ru.practicum.location.dto.LocationDto; +import ru.practicum.user.dto.UserShortDto; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +class EventPublicControllerTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Mock + private EventService eventService; + + @InjectMocks + private EventPublicController eventPublicController; + + private MockMvc mvc; + + private EventFullDto eventFullDto; + private EventShortDto eventShortDto; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + + mvc = MockMvcBuilders + .standaloneSetup(eventPublicController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + CategoryDto categoryDto = new CategoryDto(1L, "Категория"); + UserShortDto userShortDto = new UserShortDto(1L, "Макс Иванов"); + LocationDto locationDto = new LocationDto(50.01f, 31.25f); + + eventFullDto = new EventFullDto( + 1L, + "Аннотация", + categoryDto, + 10L, + LocalDateTime.now(), + "Описание", + LocalDateTime.now().plusDays(1), + userShortDto, + locationDto, + true, + 100, + LocalDateTime.now(), + true, + null, + "Заголовок", + 500L + ); + + eventShortDto = new EventShortDto( + 1L, + "Аннотация", + categoryDto, + 10L, + LocalDateTime.now().plusDays(1), + userShortDto, + true, + "Заголовок", + 500L + ); + } + + @Test + void mustReturnPublicEvents() throws Exception { + when(eventService.getEvents( + anyString(), anyList(), anyBoolean(), + any(), any(), + anyBoolean(), anyString(), + anyInt(), anyInt(), + any(HttpServletRequest.class) + )).thenReturn(List.of(eventShortDto)); + + mvc.perform(get("/events") + .param("text", "текст для поиска") + .param("categories", "1") + .param("paid", "true") + .param("rangeStart", "2025-05-22 12:00:00") + .param("rangeEnd", "2025-05-23 12:00:00") + .param("onlyAvailable", "true") + .param("sort", "EVENT_DATE") + .param("from", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(eventShortDto.getId())) + .andExpect(jsonPath("$[0].annotation").value(eventShortDto.getAnnotation())) + .andExpect(jsonPath("$[0].category.id").value(eventShortDto.getCategory().getId())) + .andExpect(jsonPath("$[0].category.name").value(eventShortDto.getCategory().getName())) + .andExpect(jsonPath("$[0].confirmedRequests").value(eventShortDto.getConfirmedRequests())) + .andExpect(jsonPath("$[0].eventDate").value(eventShortDto.getEventDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$[0].initiator.id").value(eventShortDto.getInitiator().getId())) + .andExpect(jsonPath("$[0].initiator.name").value(eventShortDto.getInitiator().getName())) + .andExpect(jsonPath("$[0].paid").value(eventShortDto.getPaid())) + .andExpect(jsonPath("$[0].title").value(eventShortDto.getTitle())) + .andExpect(jsonPath("$[0].views").value(eventShortDto.getViews())); + } + + @Test + void mustReturnPublicEventById() throws Exception { + when(eventService.getEventById(eq(1L), any(HttpServletRequest.class))) + .thenReturn(eventFullDto); + + mvc.perform(get("/events/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(eventFullDto.getId())) + .andExpect(jsonPath("$.annotation").value(eventFullDto.getAnnotation())) + .andExpect(jsonPath("$.category.id").value(eventFullDto.getCategory().getId())) + .andExpect(jsonPath("$.category.name").value(eventFullDto.getCategory().getName())) + .andExpect(jsonPath("$.confirmedRequests").value(eventFullDto.getConfirmedRequests())) + .andExpect(jsonPath("$.createdOn").value(eventFullDto.getCreatedOn().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$.description").value(eventFullDto.getDescription())) + .andExpect(jsonPath("$.eventDate").value(eventFullDto.getEventDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$.initiator.id").value(eventFullDto.getInitiator().getId())) + .andExpect(jsonPath("$.initiator.name").value(eventFullDto.getInitiator().getName())) + .andExpect(jsonPath("$.location.lat").value(eventFullDto.getLocation().getLat())) + .andExpect(jsonPath("$.location.lon").value(eventFullDto.getLocation().getLon())) + .andExpect(jsonPath("$.paid").value(eventFullDto.getPaid())) + .andExpect(jsonPath("$.participantLimit").value(eventFullDto.getParticipantLimit())) + .andExpect(jsonPath("$.publishedOn").value(eventFullDto.getPublishedOn().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$.requestModeration").value(eventFullDto.getRequestModeration())) + .andExpect(jsonPath("$.state").doesNotExist()) + .andExpect(jsonPath("$.title").value(eventFullDto.getTitle())) + .andExpect(jsonPath("$.views").value(eventFullDto.getViews())); + } +} diff --git a/main/src/test/java/ru/practicum/event/EventServiceImplTest.java b/main/src/test/java/ru/practicum/event/EventServiceImplTest.java new file mode 100644 index 0000000..3bcafb3 --- /dev/null +++ b/main/src/test/java/ru/practicum/event/EventServiceImplTest.java @@ -0,0 +1,332 @@ +package ru.practicum.event; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.MainServiceApp; +import ru.practicum.category.model.Category; +import ru.practicum.event.dto.*; +import ru.practicum.event.model.Event; +import ru.practicum.event.service.EventService; +import ru.practicum.event.state.EventState; +import ru.practicum.event.state.EventStateActionSolution; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.ValidationException; +import ru.practicum.location.dto.LocationDto; +import ru.practicum.location.model.Location; +import ru.practicum.request.model.Request; +import ru.practicum.request.status.RequestStatus; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + + +@Transactional +@ActiveProfiles("test") +@SpringBootTest( + classes = MainServiceApp.class, + webEnvironment = SpringBootTest.WebEnvironment.NONE) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class EventServiceImplTest { + + private final EntityManager em; + private final EventService eventService; + private Long userId; + private Long categoryId; + private Long locationId; + + @BeforeEach + void setUp() { + User user = new User(); + user.setName("Макс Иванов"); + user.setEmail("Max@mail.com"); + em.persist(user); + + userId = user.getId(); + + Category category = new Category(); + category.setName("Кино"); + em.persist(category); + categoryId = category.getId(); + + Location location = new Location(); + location.setLat(50.01f); + location.setLon(31.25f); + em.persist(location); + locationId = location.getId(); + + em.flush(); + } + + @Test + void mustCreateEvent() { + NewEventDto dto = new NewEventDto( + "Аннотация", + categoryId, + "Описание", + LocalDateTime.now().plusDays(1), + new LocationDto(50.01f, 31.25f), + true, + 10, + true, + "Заголовок" + ); + + EventFullDto created = eventService.createEvent(userId, dto); + + assertThat(created.getId()).isNotNull(); + assertThat(created.getAnnotation()).isEqualTo(dto.getAnnotation()); + assertThat(created.getTitle()).isEqualTo(dto.getTitle()); + assertThat(created.getCategory().getId()).isEqualTo(categoryId); + } + + @Test + void mustGetEventByUserIdAndEventId() { + Event event = new Event(); + event.setAnnotation("Аннотация"); + event.setCategory(em.find(Category.class, categoryId)); + event.setCreatedOn(LocalDateTime.now()); + event.setDescription("Описание"); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setInitiator(em.find(User.class, userId)); + event.setLocation(em.find(Location.class, locationId)); + event.setPaid(true); + event.setParticipantLimit(10); + event.setRequestModeration(true); + event.setState(EventState.PENDING); + event.setTitle("Заголовок"); + + em.persist(event); + em.flush(); + + EventFullDto found = eventService.getEventByUserIdAndEventId(userId, event.getId()); + + assertThat(found.getId()).isEqualTo(event.getId()); + assertThat(found.getAnnotation()).isEqualTo(event.getAnnotation()); + assertThat(found.getTitle()).isEqualTo(event.getTitle()); + } + + @Test + void mustUpdateEventByUser() { + Event event = new Event(); + event.setAnnotation("Аннотация"); + event.setCategory(em.find(Category.class, categoryId)); + event.setCreatedOn(LocalDateTime.now()); + event.setDescription("Описание"); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setInitiator(em.find(User.class, userId)); + event.setLocation(em.find(Location.class, locationId)); + event.setPaid(true); + event.setParticipantLimit(10); + event.setRequestModeration(true); + event.setState(EventState.PENDING); + event.setTitle("Заголовок"); + + em.persist(event); + em.flush(); + + UpdateEventUserRequest updateDto = new UpdateEventUserRequest(); + updateDto.setAnnotation("Обновленная аннотация"); + updateDto.setTitle("Обновленный заголовок"); + + EventFullDto updated = eventService.updateEventByUserIdAndEventId(userId, event.getId(), updateDto); + + assertThat(updated.getAnnotation()).isEqualTo("Обновленная аннотация"); + assertThat(updated.getTitle()).isEqualTo("Обновленный заголовок"); + } + + @Test + void shouldThrowValidationExceptionForInvalidEventDate() { + NewEventDto dto = new NewEventDto( + "Аннотация", + categoryId, + "Описание", + LocalDateTime.now().plusMinutes(30), + new LocationDto(50.01f, 31.25f), + true, + 10, + true, + "Заголовок" + ); + + Assertions.assertThrows(ValidationException.class, () -> eventService.createEvent(userId, dto)); + } + + @Test + void mustReturnEventsByUserId() { + Event event = new Event(); + event.setAnnotation("Аннотация"); + event.setCategory(em.find(Category.class, categoryId)); + event.setCreatedOn(LocalDateTime.now()); + event.setDescription("Описание"); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setInitiator(em.find(User.class, userId)); + event.setLocation(em.find(Location.class, locationId)); + event.setPaid(true); + event.setParticipantLimit(10); + event.setRequestModeration(true); + event.setState(EventState.PENDING); + event.setTitle("Заголовок"); + + em.persist(event); + em.flush(); + + List events = eventService.getEventsByUserId(userId, 0, 10); + + assertThat(events).isNotEmpty(); + assertThat(events.get(0).getId()).isEqualTo(event.getId()); + } + + @Test + void mustReturnAdminEvents() { + Event event = new Event(); + event.setAnnotation("Аннотация"); + event.setCategory(em.find(Category.class, categoryId)); + event.setCreatedOn(LocalDateTime.now()); + event.setDescription("Описание"); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setInitiator(em.find(User.class, userId)); + event.setLocation(em.find(Location.class, locationId)); + event.setPaid(true); + event.setParticipantLimit(10); + event.setRequestModeration(true); + event.setState(EventState.PENDING); + event.setTitle("Заголовок"); + + em.persist(event); + em.flush(); + + List events = eventService.getAdminEvents( + List.of(userId), + List.of("PENDING"), + List.of(categoryId), + null, + null, + 0, + 10 + ); + + assertThat(events).hasSize(1); + EventFullDto dto = events.get(0); + assertThat(dto.getId()).isEqualTo(event.getId()); + assertThat(dto.getTitle()).isEqualTo("Заголовок"); + assertThat(dto.getCategory().getId()).isEqualTo(categoryId); + } + + @Test + void mustUpdateEventByAdmin() { + Event event = new Event(); + event.setAnnotation("Аннотация"); + event.setCategory(em.find(Category.class, categoryId)); + event.setCreatedOn(LocalDateTime.now()); + event.setDescription("Описание"); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setInitiator(em.find(User.class, userId)); + event.setLocation(em.find(Location.class, locationId)); + event.setPaid(true); + event.setParticipantLimit(10); + event.setRequestModeration(true); + event.setState(EventState.PENDING); + event.setTitle("Заголовок"); + + em.persist(event); + em.flush(); + + UpdateEventAdminRequest updateRequest = new UpdateEventAdminRequest(); + updateRequest.setTitle("Обновленный заголовок"); + updateRequest.setAnnotation("Обновленная аннотация"); + updateRequest.setStateAction(EventStateActionSolution.PUBLISH_EVENT); + + EventFullDto updated = eventService.updateEventByAdmin(event.getId(), updateRequest); + + assertThat(updated.getTitle()).isEqualTo("Обновленный заголовок"); + assertThat(updated.getAnnotation()).isEqualTo("Обновленная аннотация"); + assertThat(updated.getState()).isEqualTo(EventState.PUBLISHED); + } + + @Test + void shouldThrowConflictExceptionWhenPublishingWrongState() { + Event event = new Event(); + event.setAnnotation("Аннотация"); + event.setCategory(em.find(Category.class, categoryId)); + event.setCreatedOn(LocalDateTime.now()); + event.setDescription("Описание"); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setInitiator(em.find(User.class, userId)); + event.setLocation(em.find(Location.class, locationId)); + event.setPaid(true); + event.setParticipantLimit(10); + event.setRequestModeration(true); + event.setState(EventState.PUBLISHED); + event.setTitle("Заголовок"); + + em.persist(event); + em.flush(); + + UpdateEventAdminRequest updateRequest = new UpdateEventAdminRequest(); + updateRequest.setStateAction(EventStateActionSolution.PUBLISH_EVENT); + + Assertions.assertThrows(ConflictException.class, + () -> eventService.updateEventByAdmin(event.getId(), updateRequest)); + } + + @Test + void mustChangeRequestStatus() { + Event event = new Event(); + event.setAnnotation("Аннотация"); + event.setCategory(em.find(Category.class, categoryId)); + event.setCreatedOn(LocalDateTime.now()); + event.setDescription("Описание"); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setInitiator(em.find(User.class, userId)); + event.setLocation(em.find(Location.class, locationId)); + event.setPaid(true); + event.setParticipantLimit(1); + event.setRequestModeration(true); + event.setState(EventState.PENDING); + event.setTitle("Заголовок"); + + em.persist(event); + User requester1 = new User(); + requester1.setName("Макс Иванов"); + requester1.setEmail("Max@mail.ru"); + em.persist(requester1); + + User requester2 = new User(); + requester2.setName("Ирина Дубинина"); + requester2.setEmail("Iren@mail.ru"); + em.persist(requester2); + + Request request1 = new Request(); + request1.setEvent(event); + request1.setRequester(requester1); + request1.setStatus(RequestStatus.PENDING); + + Request request2 = new Request(); + request2.setEvent(event); + request2.setRequester(requester2); + request2.setStatus(RequestStatus.PENDING); + + em.persist(request1); + em.persist(request2); + em.flush(); + + EventRequestStatusUpdateRequest statusRequest = new EventRequestStatusUpdateRequest(); + statusRequest.setRequestIds(List.of(request1.getId(), request2.getId())); + statusRequest.setStatus(RequestStatus.CONFIRMED); + + EventRequestStatusUpdateResult result = eventService.changeStatus(event.getInitiator().getId(), event.getId(), statusRequest); + + assertThat(result.getConfirmedRequests()).hasSize(1); + assertThat(result.getRejectedRequests()).hasSize(1); + } +} \ No newline at end of file diff --git a/main/src/test/java/ru/practicum/location/LocationDtoTest.java b/main/src/test/java/ru/practicum/location/LocationDtoTest.java new file mode 100644 index 0000000..26b411f --- /dev/null +++ b/main/src/test/java/ru/practicum/location/LocationDtoTest.java @@ -0,0 +1,37 @@ +package ru.practicum.location; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import ru.practicum.location.dto.LocationDto; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@JsonTest +class LocationDtoTest { + + @Autowired + private JacksonTester jsonLocationDto; + + @Test + void shouldSerializeLocationDto() throws Exception { + LocationDto dto = new LocationDto(50.01f, 31.25f); + + JsonContent result = jsonLocationDto.write(dto); + + assertThat(result).extractingJsonPathNumberValue("$.lat").isEqualTo(50.01); + assertThat(result).extractingJsonPathNumberValue("$.lon").isEqualTo(31.25); + } + + @Test + void shouldDeserializeLocationDto() throws Exception { + String json = "{\"lat\": 50.01, \"lon\": 31.25}"; + + LocationDto dto = jsonLocationDto.parseObject(json); + + assertThat(dto.getLat()).isEqualTo(50.01f); + assertThat(dto.getLon()).isEqualTo(31.25f); + } +} diff --git a/main/src/test/java/ru/practicum/location/LocationMapperTest.java b/main/src/test/java/ru/practicum/location/LocationMapperTest.java new file mode 100644 index 0000000..230dccd --- /dev/null +++ b/main/src/test/java/ru/practicum/location/LocationMapperTest.java @@ -0,0 +1,31 @@ +package ru.practicum.location; + +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.Test; +import ru.practicum.location.dto.LocationDto; +import ru.practicum.location.dto.mapper.LocationMapper; +import ru.practicum.location.model.Location; + +class LocationMapperTest { + + @Test + void shouldLocationToLocationDto() { + Location location = new Location(1L, 50.01f, 31.25f); + + LocationDto result = LocationMapper.toLocationDto(location); + + AssertionsForClassTypes.assertThat(result.getLat()).isEqualTo(50.01f); + AssertionsForClassTypes.assertThat(result.getLon()).isEqualTo(31.25f); + } + + @Test + void shouldLocationDtoToLocation() { + LocationDto dto = new LocationDto(50.01f, 31.25f); + + Location result = LocationMapper.toLocation(dto); + + AssertionsForClassTypes.assertThat(result.getId()).isNull(); + AssertionsForClassTypes.assertThat(result.getLat()).isEqualTo(50.01f); + AssertionsForClassTypes.assertThat(result.getLon()).isEqualTo(31.25f); + } +} diff --git a/main/src/test/java/ru/practicum/request/RequestDtoTest.java b/main/src/test/java/ru/practicum/request/RequestDtoTest.java new file mode 100644 index 0000000..4315154 --- /dev/null +++ b/main/src/test/java/ru/practicum/request/RequestDtoTest.java @@ -0,0 +1,41 @@ +package ru.practicum.request; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import ru.practicum.request.dto.ParticipationRequestDto; +import ru.practicum.request.status.RequestStatus; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@JsonTest +class RequestDtoTest { + + @Autowired + private JacksonTester jsonParticipationRequestDto; + + @Test + void shouldReturnParticipationRequestDto() throws Exception { + LocalDateTime created = LocalDateTime.of(2025, 1, 1, 12, 30, 45); + ParticipationRequestDto dto = new ParticipationRequestDto( + 10L, + created, + 1L, + 5L, + RequestStatus.CONFIRMED + ); + + JsonContent result = jsonParticipationRequestDto.write(dto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(10); + assertThat(result).extractingJsonPathStringValue("$.created") + .isEqualTo("2025-01-01 12:30:45"); + assertThat(result).extractingJsonPathNumberValue("$.event").isEqualTo(1); + assertThat(result).extractingJsonPathNumberValue("$.requester").isEqualTo(5); + assertThat(result).extractingJsonPathStringValue("$.status").isEqualTo("CONFIRMED"); + } +} diff --git a/main/src/test/java/ru/practicum/request/RequestMapperTest.java b/main/src/test/java/ru/practicum/request/RequestMapperTest.java new file mode 100644 index 0000000..610372f --- /dev/null +++ b/main/src/test/java/ru/practicum/request/RequestMapperTest.java @@ -0,0 +1,39 @@ +package ru.practicum.request; + +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.Test; +import ru.practicum.event.model.Event; +import ru.practicum.request.dto.ParticipationRequestDto; +import ru.practicum.request.dto.mapper.RequestMapper; +import ru.practicum.request.model.Request; +import ru.practicum.request.status.RequestStatus; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; + +public class RequestMapperTest { + @Test + void shouldRequestToParticipationRequestDto() { + LocalDateTime created = LocalDateTime.of(2025, 1, 1, 12, 0, 0); + + User requester = new User(2L, "Max@mail.com", "Макс Иванов"); + Event event = new Event(); + event.setId(3L); + + Request request = new Request( + 1L, + created, + event, + requester, + RequestStatus.PENDING + ); + + ParticipationRequestDto result = RequestMapper.toParticipationRequestDto(request); + + AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L); + AssertionsForClassTypes.assertThat(result.getCreated()).isEqualTo(created); + AssertionsForClassTypes.assertThat(result.getEvent()).isEqualTo(3L); + AssertionsForClassTypes.assertThat(result.getRequester()).isEqualTo(2L); + AssertionsForClassTypes.assertThat(result.getStatus()).isEqualTo(RequestStatus.PENDING); + } +} diff --git a/main/src/test/java/ru/practicum/request/RequestPrivateControllerTest.java b/main/src/test/java/ru/practicum/request/RequestPrivateControllerTest.java new file mode 100644 index 0000000..1967711 --- /dev/null +++ b/main/src/test/java/ru/practicum/request/RequestPrivateControllerTest.java @@ -0,0 +1,104 @@ +package ru.practicum.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.error.ErrorHandler; +import ru.practicum.request.controller.RequestPrivateController; +import ru.practicum.request.dto.ParticipationRequestDto; +import ru.practicum.request.service.RequestService; +import ru.practicum.request.status.RequestStatus; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +public class RequestPrivateControllerTest { + private final ObjectMapper mapper = new ObjectMapper(); + + @Mock + private RequestService requestService; + + @InjectMocks + private RequestPrivateController requestPrivateController; + + private MockMvc mvc; + private ParticipationRequestDto requestDto; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + mvc = MockMvcBuilders + .standaloneSetup(requestPrivateController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + requestDto = new ParticipationRequestDto( + 1L, + LocalDateTime.of(2025, 5, 1, 12, 0, 0), + 1L, + 5L, + RequestStatus.PENDING + ); + } + + @Test + void mustGetRequestsByUserId() throws Exception { + when(requestService.getRequestsByUserId(anyLong())) + .thenReturn(List.of(requestDto)); + + mvc.perform(get("/users/5/requests") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id", is(1))) + .andExpect(jsonPath("$[0].event", is(1))) + .andExpect(jsonPath("$[0].requester", is(5))) + .andExpect(jsonPath("$[0].status", is("PENDING"))) + .andExpect(jsonPath("$[0].created", is("2025-05-01 12:00:00"))); + } + + @Test + void mustCreateRequest() throws Exception { + when(requestService.createRequest(anyLong(), anyLong())) + .thenReturn(requestDto); + + mvc.perform(post("/users/5/requests") + .param("eventId", "100") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.event", is(1))) + .andExpect(jsonPath("$.requester", is(5))) + .andExpect(jsonPath("$.status", is("PENDING"))) + .andExpect(jsonPath("$.created", is("2025-05-01 12:00:00"))); + } + + @Test + void mustCancelRequest() throws Exception { + when(requestService.cancelRequest(anyLong(), anyLong())) + .thenReturn(requestDto); + + mvc.perform(patch("/users/5/requests/1/cancel") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.event", is(1))) + .andExpect(jsonPath("$.requester", is(5))) + .andExpect(jsonPath("$.status", is("PENDING"))) + .andExpect(jsonPath("$.created", is("2025-05-01 12:00:00"))); + } +} diff --git a/main/src/test/java/ru/practicum/request/RequestServiceImplTest.java b/main/src/test/java/ru/practicum/request/RequestServiceImplTest.java new file mode 100644 index 0000000..9b30016 --- /dev/null +++ b/main/src/test/java/ru/practicum/request/RequestServiceImplTest.java @@ -0,0 +1,140 @@ +package ru.practicum.request; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.MainServiceApp; +import ru.practicum.category.model.Category; +import ru.practicum.event.model.Event; +import ru.practicum.event.state.EventState; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.NotFoundException; +import ru.practicum.location.model.Location; +import ru.practicum.request.dto.ParticipationRequestDto; +import ru.practicum.request.service.RequestService; +import ru.practicum.request.status.RequestStatus; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Transactional +@ActiveProfiles("test") +@SpringBootTest(classes = MainServiceApp.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class RequestServiceImplTest { + private final EntityManager em; + private final RequestService requestService; + + private User user; + private User initiator; + private Category category; + private Location location; + private Event event; + + @BeforeEach + void setUp() { + category = new Category(); + category.setName("Концерт"); + em.persist(category); + + location = new Location(); + location.setLat(50.01f); + location.setLon(31.25f); + em.persist(location); + + user = new User(); + user.setName("Макс Иванов"); + user.setEmail("Max@mail.com"); + em.persist(user); + + initiator = new User(); + initiator.setName("Ирина Дубова"); + initiator.setEmail("Iren@mail.com"); + em.persist(initiator); + + event = new Event(); + event.setTitle("Концерт в парке"); + event.setAnnotation("Аннотация"); + event.setDescription("Описание"); + event.setCategory(category); + event.setLocation(location); + event.setInitiator(initiator); + event.setCreatedOn(LocalDateTime.now()); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setState(EventState.PUBLISHED); + event.setParticipantLimit(1); + em.persist(event); + + em.flush(); + } + + @Test + void mustCreateRequest() { + ParticipationRequestDto requestDto = requestService.createRequest(user.getId(), event.getId()); + + assertThat(requestDto.getId()).isNotNull(); + assertThat(requestDto.getEvent()).isEqualTo(event.getId()); + assertThat(requestDto.getRequester()).isEqualTo(user.getId()); + assertThat(requestDto.getStatus()).isEqualTo(RequestStatus.PENDING); + } + + @Test + void shouldThrowConflictWhenUserIsInitiator() { + assertThrows(ConflictException.class, + () -> requestService.createRequest(initiator.getId(), event.getId())); + } + + @Test + void shouldThrowConflictWhenEventNotPublished() { + event.setState(EventState.PENDING); + em.flush(); + + assertThrows(ConflictException.class, + () -> requestService.createRequest(user.getId(), event.getId())); + } + + @Test + void shouldThrowConflictWhenDuplicateRequest() { + requestService.createRequest(user.getId(), event.getId()); + + assertThrows(ConflictException.class, + () -> requestService.createRequest(user.getId(), event.getId())); + } + + @Test + void mustReturnRequestsByUserId() { + ParticipationRequestDto requestDto = requestService.createRequest(user.getId(), event.getId()); + + List requests = requestService.getRequestsByUserId(user.getId()); + + assertThat(requests).hasSize(1); + assertThat(requests.get(0).getId()).isEqualTo(requestDto.getId()); + } + + @Test + void mustCancelRequest() { + ParticipationRequestDto requestDto = requestService.createRequest(user.getId(), event.getId()); + + ParticipationRequestDto canceled = requestService.cancelRequest(user.getId(), requestDto.getId()); + + assertThat(canceled.getStatus()).isEqualTo(RequestStatus.CANCELED); + } + + @Test + void shouldThrowNotFoundWhenCancelingOtherUserRequest() { + ParticipationRequestDto requestDto = requestService.createRequest(user.getId(), event.getId()); + + assertThrows(NotFoundException.class, + () -> requestService.cancelRequest(initiator.getId(), requestDto.getId())); + } +} + From 2202fce3388439bfdfab1aed2f796724437cc361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Fri, 22 Aug 2025 16:58:45 +0300 Subject: [PATCH 12/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20spring-boot-maven-plugin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/pom.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/main/pom.xml b/main/pom.xml index c4c902b..95f9b62 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -74,4 +74,13 @@ runtime + + + + + org.springframework.boot + spring-boot-maven-plugin + + + \ No newline at end of file From 835f39c1c1e49f7e746d8d5449442974a48faf4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Fri, 22 Aug 2025 18:12:54 +0300 Subject: [PATCH 13/15] =?UTF-8?q?=D0=A1=D0=BA=D0=BE=D1=80=D1=80=D0=B5?= =?UTF-8?q?=D0=BA=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B8=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2?= =?UTF-8?q?=20application.yml=20=D0=B4=D0=BB=D1=8F=20server=20=D0=B8=20mai?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/src/main/resources/application.yml | 7 +++++-- stats/server/src/main/resources/application.yml | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/main/src/main/resources/application.yml b/main/src/main/resources/application.yml index eca3458..d39638a 100644 --- a/main/src/main/resources/application.yml +++ b/main/src/main/resources/application.yml @@ -10,7 +10,8 @@ spring: on-profile: dev datasource: driver-class-name: org.postgresql.Driver - url: jdbc:postgresql://localhost:6542/maindb +# url: jdbc:postgresql://localhost:6542/maindb + url: jdbc:postgresql://main-db:6542/maindb username: dbuser2 password: 1234567 jpa: @@ -24,8 +25,10 @@ spring: server: port: 8080 +#client: +# url: http://localhost:9090 client: - url: http://localhost:9090 + url: http://stats-server:9090 --- diff --git a/stats/server/src/main/resources/application.yml b/stats/server/src/main/resources/application.yml index aeb5141..e493d28 100644 --- a/stats/server/src/main/resources/application.yml +++ b/stats/server/src/main/resources/application.yml @@ -1,7 +1,8 @@ spring: datasource: driver-class-name: org.postgresql.Driver - url: jdbc:postgresql://localhost:6541/statsdb +# url: jdbc:postgresql://localhost:6541/statsdb + url: jdbc:postgresql://stats-db:6541/statsdb username: dbuser password: 12345 jpa: From 17abe17ee6318feb204429b602b86dcda0d8b52a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Fri, 22 Aug 2025 18:39:56 +0300 Subject: [PATCH 14/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=87?= =?UTF-8?q?=D0=B8=D0=BA=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA=20=D0=B2=20?= =?UTF-8?q?server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ru/practicum/error/ApiError.java | 19 ++++++ .../java/ru/practicum/error/ErrorHandler.java | 63 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 stats/server/src/main/java/ru/practicum/error/ApiError.java create mode 100644 stats/server/src/main/java/ru/practicum/error/ErrorHandler.java diff --git a/stats/server/src/main/java/ru/practicum/error/ApiError.java b/stats/server/src/main/java/ru/practicum/error/ApiError.java new file mode 100644 index 0000000..327ff47 --- /dev/null +++ b/stats/server/src/main/java/ru/practicum/error/ApiError.java @@ -0,0 +1,19 @@ +package ru.practicum.error; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +@AllArgsConstructor +@Getter +public class ApiError { + private final List errors; + private final String message; + private final String reason; + private final String status; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private final LocalDateTime timestamp; +} diff --git a/stats/server/src/main/java/ru/practicum/error/ErrorHandler.java b/stats/server/src/main/java/ru/practicum/error/ErrorHandler.java new file mode 100644 index 0000000..d277ca0 --- /dev/null +++ b/stats/server/src/main/java/ru/practicum/error/ErrorHandler.java @@ -0,0 +1,63 @@ +package ru.practicum.error; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import ru.practicum.exception.ValidationException; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.stream.Collectors; + +@Slf4j +@RestControllerAdvice +public class ErrorHandler { + @ExceptionHandler({MethodArgumentNotValidException.class, + MissingServletRequestParameterException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + String message = e.getBindingResult().getFieldErrors().stream() + .map(fe -> String.format("Field: %s. Error: %s. Value: %s", + fe.getField(), + fe.getDefaultMessage(), + fe.getRejectedValue() != null ? fe.getRejectedValue().toString() : "null")) + .collect(Collectors.joining("; ")); + + return new ApiError( + Collections.emptyList(), + message, + "Incorrectly made request.", + "BAD_REQUEST", + LocalDateTime.now() + ); + } + + @ExceptionHandler(ValidationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError handleValidationException(ValidationException e) { + return new ApiError( + Collections.emptyList(), + e.getMessage(), + "Incorrectly made request.", + "BAD_REQUEST", + LocalDateTime.now() + ); + } + + @ExceptionHandler(Throwable.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiError handleThrowable(Throwable e) { + log.error("Неожиданная ошибка: ", e); + return new ApiError( + Collections.emptyList(), + "Произошла непредвиденная ошибка.", + "Internal server error.", + "INTERNAL_SERVER_ERROR", + LocalDateTime.now() + ); + } +} From be9b633284bb869efcd13825b27d055f0191bd4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Mon, 25 Aug 2025 01:12:54 +0300 Subject: [PATCH 15/15] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=87=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 1 + .../ru/practicum/category/model/Category.java | 3 +- .../compilation/model/Compilation.java | 13 +++++ .../event/dto/mapper/EventMapper.java | 7 ++- .../java/ru/practicum/event/model/Event.java | 6 +-- .../event/service/EventServiceImpl.java | 52 +++++++++++++++++-- .../specification/EventSpecifications.java | 11 ++-- .../ru/practicum/location/model/Location.java | 3 +- .../ru/practicum/request/model/Request.java | 13 +++++ .../java/ru/practicum/user/model/User.java | 7 +-- main/src/main/resources/schema.sql | 1 - .../ru/practicum/event/EventMapperTest.java | 8 +-- stats/client/pom.xml | 6 +++ .../main/java/ru/practicum/StatsClient.java | 44 ++++++++++++---- .../ru/practicum/StatsViewRequestDto.java | 21 ++++++++ .../practicum/service/StatsServiceImpl.java | 3 ++ 16 files changed, 149 insertions(+), 50 deletions(-) create mode 100644 stats/dto/src/main/java/ru/practicum/StatsViewRequestDto.java diff --git a/docker-compose.yml b/docker-compose.yml index d3e93f6..b101d78 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,7 @@ services: - "8080:8080" depends_on: - main-db + - stats-server environment: - SPRING_DATASOURCE_URL=jdbc:postgresql://main-db:5432/maindb - SPRING_DATASOURCE_USERNAME=dbuser2 diff --git a/main/src/main/java/ru/practicum/category/model/Category.java b/main/src/main/java/ru/practicum/category/model/Category.java index 8e94341..9c4c844 100644 --- a/main/src/main/java/ru/practicum/category/model/Category.java +++ b/main/src/main/java/ru/practicum/category/model/Category.java @@ -23,8 +23,7 @@ public class Category { @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof Category)) return false; - Category category = (Category) o; + if (!(o instanceof Category category)) return false; return Objects.equals(id, category.id); } diff --git a/main/src/main/java/ru/practicum/compilation/model/Compilation.java b/main/src/main/java/ru/practicum/compilation/model/Compilation.java index 4c6aa7d..feac846 100644 --- a/main/src/main/java/ru/practicum/compilation/model/Compilation.java +++ b/main/src/main/java/ru/practicum/compilation/model/Compilation.java @@ -8,6 +8,7 @@ import ru.practicum.event.model.Event; import java.util.HashSet; +import java.util.Objects; import java.util.Set; @NoArgsConstructor @@ -34,4 +35,16 @@ public class Compilation { inverseJoinColumns = @JoinColumn(name = "event_id") ) private Set events = new HashSet<>(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Compilation compilation)) return false; + return Objects.equals(id, compilation.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } } diff --git a/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java b/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java index 19246cc..a482770 100644 --- a/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java +++ b/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java @@ -33,7 +33,7 @@ public static EventFullDto toEventFullDto(Event event) { event.getRequestModeration(), event.getState(), event.getTitle(), - event.getViews() + null ); } @@ -53,8 +53,7 @@ public static Event toEvent(NewEventDto newEventDto, Category category, User ini null, newEventDto.getRequestModeration(), EventState.PENDING, - newEventDto.getTitle(), - 0L + newEventDto.getTitle() ); } @@ -68,7 +67,7 @@ public static EventShortDto toEventShortDto(Event event) { UserMapper.toUserShortDto(event.getInitiator()), event.getPaid(), event.getTitle(), - event.getViews() + null ); } } diff --git a/main/src/main/java/ru/practicum/event/model/Event.java b/main/src/main/java/ru/practicum/event/model/Event.java index b854677..9507b2c 100644 --- a/main/src/main/java/ru/practicum/event/model/Event.java +++ b/main/src/main/java/ru/practicum/event/model/Event.java @@ -70,14 +70,10 @@ public class Event { @Column(nullable = false, length = 120) private String title; - private Long views; - - @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof Event)) return false; - Event event = (Event) o; + if (!(o instanceof Event event)) return false; return Objects.equals(id, event.id); } diff --git a/main/src/main/java/ru/practicum/event/service/EventServiceImpl.java b/main/src/main/java/ru/practicum/event/service/EventServiceImpl.java index 0b3ae30..fde6046 100644 --- a/main/src/main/java/ru/practicum/event/service/EventServiceImpl.java +++ b/main/src/main/java/ru/practicum/event/service/EventServiceImpl.java @@ -11,6 +11,8 @@ import org.springframework.transaction.annotation.Transactional; import ru.practicum.StatsClient; import ru.practicum.StatsRequestDto; +import ru.practicum.StatsResponseDto; +import ru.practicum.StatsViewRequestDto; import ru.practicum.category.model.Category; import ru.practicum.category.repository.CategoryRepository; import ru.practicum.event.dto.*; @@ -37,6 +39,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; @Transactional(readOnly = true) @@ -235,10 +239,35 @@ public List getEvents(String text, List categories, Boolean List events = eventRepository.findAll(spec, pageable).getContent(); - statsClient.createHit(new StatsRequestDto("ewm", request.getRequestURI(), request.getRemoteAddr(), LocalDateTime.now())); + statsClient.createHit(new StatsRequestDto( + "ewm", + request.getRequestURI(), + request.getRemoteAddr(), + LocalDateTime.now() + )); + + Map eventUriAndIdMap = events.stream() + .map(Event::getId) + .collect(Collectors.toMap(id -> "/events/" + id, Function.identity())); + + List stats = statsClient.getStats( + StatsViewRequestDto.builder() + .uris(new ArrayList<>(eventUriAndIdMap.keySet())) + .unique(true) + .build() + ); + + Map views = stats.stream() + .collect(Collectors.toMap(StatsResponseDto::getUri, + StatsResponseDto::getHits + )); return events.stream() - .map(EventMapper::toEventShortDto) + .map(event -> { + EventShortDto eventShortDto = EventMapper.toEventShortDto(event); + eventShortDto.setViews(views.getOrDefault("/events/" + event.getId(), 0L)); + return eventShortDto; + }) .collect(Collectors.toList()); } @@ -254,10 +283,23 @@ public EventFullDto getEventById(Long id, HttpServletRequest request) { LocalDateTime.now() )); - event.setViews(event.getViews() + 1); - eventRepository.save(event); + Long views = statsClient.getStats( + StatsViewRequestDto.builder() + .uris(List.of("/events/" + id)) + .start(event.getPublishedOn()) + .end(LocalDateTime.now()) + .unique(true) + .build() - return EventMapper.toEventFullDto(event); + ).stream() + .findAny() + .map(StatsResponseDto::getHits) + .orElse(0L); + + EventFullDto eventFullDto = EventMapper.toEventFullDto(event); + eventFullDto.setViews(views); + + return eventFullDto; } @Override diff --git a/main/src/main/java/ru/practicum/event/specification/EventSpecifications.java b/main/src/main/java/ru/practicum/event/specification/EventSpecifications.java index df7b205..0b9b04e 100644 --- a/main/src/main/java/ru/practicum/event/specification/EventSpecifications.java +++ b/main/src/main/java/ru/practicum/event/specification/EventSpecifications.java @@ -36,17 +36,12 @@ public static Specification paidIs(Boolean paid) { public static Specification dateBetween(LocalDateTime start, LocalDateTime end) { LocalDateTime startDate = (start != null) ? start : LocalDateTime.now(); - LocalDateTime endDate = end; return (root, query, cb) -> { - if (startDate != null && endDate != null) { - return cb.between(root.get("eventDate"), startDate, endDate); - } else if (startDate != null) { - return cb.greaterThanOrEqualTo(root.get("eventDate"), startDate); - } else if (endDate != null) { - return cb.lessThanOrEqualTo(root.get("eventDate"), endDate); + if (end != null) { + return cb.between(root.get("eventDate"), startDate, end); } else { - return null; + return cb.greaterThanOrEqualTo(root.get("eventDate"), startDate); } }; } diff --git a/main/src/main/java/ru/practicum/location/model/Location.java b/main/src/main/java/ru/practicum/location/model/Location.java index 0b039dc..779ea3d 100644 --- a/main/src/main/java/ru/practicum/location/model/Location.java +++ b/main/src/main/java/ru/practicum/location/model/Location.java @@ -28,8 +28,7 @@ public class Location { @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof Location)) return false; - Location location = (Location) o; + if (!(o instanceof Location location)) return false; return Objects.equals(id, location.id); } diff --git a/main/src/main/java/ru/practicum/request/model/Request.java b/main/src/main/java/ru/practicum/request/model/Request.java index 4ca1119..67bdc0d 100644 --- a/main/src/main/java/ru/practicum/request/model/Request.java +++ b/main/src/main/java/ru/practicum/request/model/Request.java @@ -10,6 +10,7 @@ import ru.practicum.user.model.User; import java.time.LocalDateTime; +import java.util.Objects; @NoArgsConstructor @AllArgsConstructor @@ -39,4 +40,16 @@ public class Request { @Enumerated(EnumType.STRING) @Column(nullable = false) private RequestStatus status = RequestStatus.PENDING; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Request request)) return false; + return Objects.equals(id, request.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } } diff --git a/main/src/main/java/ru/practicum/user/model/User.java b/main/src/main/java/ru/practicum/user/model/User.java index 45390cc..9b43fa3 100644 --- a/main/src/main/java/ru/practicum/user/model/User.java +++ b/main/src/main/java/ru/practicum/user/model/User.java @@ -1,8 +1,6 @@ package ru.practicum.user.model; import jakarta.persistence.*; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -21,8 +19,6 @@ public class User { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Email - @Size(max = 254) @Column(nullable = false, unique = true, length = 254) private String email; @@ -32,8 +28,7 @@ public class User { @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof User)) return false; - User user = (User) o; + if (!(o instanceof User user)) return false; return Objects.equals(email, user.email); } diff --git a/main/src/main/resources/schema.sql b/main/src/main/resources/schema.sql index 41a7974..d34c148 100644 --- a/main/src/main/resources/schema.sql +++ b/main/src/main/resources/schema.sql @@ -36,7 +36,6 @@ create table IF NOT EXISTS events ( request_moderation BOOLEAN DEFAULT true, state VARCHAR(50) NOT NULL, title VARCHAR(120) NOT NULL, - views BIGINT, CONSTRAINT fk_event_category FOREIGN KEY (category_id) REFERENCES categories (id), CONSTRAINT fk_event_user FOREIGN KEY (initiator_id) REFERENCES users (id), CONSTRAINT fk_event_location FOREIGN KEY (location_id) REFERENCES locations (id), diff --git a/main/src/test/java/ru/practicum/event/EventMapperTest.java b/main/src/test/java/ru/practicum/event/EventMapperTest.java index 99676fa..428630d 100644 --- a/main/src/test/java/ru/practicum/event/EventMapperTest.java +++ b/main/src/test/java/ru/practicum/event/EventMapperTest.java @@ -37,8 +37,7 @@ void shouldMapEventToEventFullDto() { LocalDateTime.of(2025, 5, 3, 12, 0), true, EventState.PUBLISHED, - "Заголовок", - 100L + "Заголовок" ); EventFullDto result = EventMapper.toEventFullDto(event); @@ -50,7 +49,6 @@ void shouldMapEventToEventFullDto() { AssertionsForClassTypes.assertThat(result.getPaid()).isTrue(); AssertionsForClassTypes.assertThat(result.getParticipantLimit()).isEqualTo(10); AssertionsForClassTypes.assertThat(result.getConfirmedRequests()).isEqualTo(5L); - AssertionsForClassTypes.assertThat(result.getViews()).isEqualTo(100L); } @Test @@ -74,8 +72,7 @@ void shouldMapEventToEventShortDto() { LocalDateTime.of(2025, 5, 3, 12, 0), true, EventState.PUBLISHED, - "Заголовок", - 100L + "Заголовок" ); EventShortDto result = EventMapper.toEventShortDto(event); @@ -85,7 +82,6 @@ void shouldMapEventToEventShortDto() { AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Заголовок"); AssertionsForClassTypes.assertThat(result.getPaid()).isTrue(); AssertionsForClassTypes.assertThat(result.getConfirmedRequests()).isEqualTo(5L); - AssertionsForClassTypes.assertThat(result.getViews()).isEqualTo(100L); } @Test diff --git a/stats/client/pom.xml b/stats/client/pom.xml index 3ef2b52..b947d24 100644 --- a/stats/client/pom.xml +++ b/stats/client/pom.xml @@ -35,5 +35,11 @@ org.springframework.boot spring-boot-starter-actuator + + + org.projectlombok + lombok + true + \ No newline at end of file diff --git a/stats/client/src/main/java/ru/practicum/StatsClient.java b/stats/client/src/main/java/ru/practicum/StatsClient.java index 96c7c34..4f6b564 100644 --- a/stats/client/src/main/java/ru/practicum/StatsClient.java +++ b/stats/client/src/main/java/ru/practicum/StatsClient.java @@ -3,17 +3,23 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.stereotype.Service; import org.springframework.web.util.DefaultUriBuilderFactory; -import java.util.HashMap; +import java.time.format.DateTimeFormatter; +import java.util.Collections; import java.util.List; import java.util.Map; @Service public class StatsClient extends BaseClient { + private String uri; + @Autowired public StatsClient(@Value("${client.url}") String serviceUrl, RestTemplateBuilder builder) { super( @@ -22,21 +28,37 @@ public StatsClient(@Value("${client.url}") String serviceUrl, RestTemplateBuilde .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) .build() ); + uri = serviceUrl; } public ResponseEntity createHit(StatsRequestDto statsRequestDto) { return post("/hit", statsRequestDto); } - public ResponseEntity getStats(String start, String end, List uris, Boolean unique) { - Map parameters = new HashMap<>(); - parameters.put("start", start); - parameters.put("end", end); - if (uris != null && !uris.isEmpty()) { - parameters.put("uris", String.join(",", uris)); - } - parameters.put("unique", unique != null ? unique : false); + public List getStats(StatsViewRequestDto statsViewRequestDto) { + String urisCsv = String.join(",", statsViewRequestDto.getUris()); + + final String url = uri + "/stats?start={start}&end={end}&uris={uris}&unique={unique}"; - return get("/stats", parameters); + try { + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + Map.of( + "start", statsViewRequestDto.getStart().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), + "end", statsViewRequestDto.getEnd().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), + "uris", urisCsv, + "unique", statsViewRequestDto.getUnique() + ) + ); + + List stats = response.getBody(); + return stats != null ? stats : Collections.emptyList(); + } catch (Exception e) { + return Collections.emptyList(); + } } -} +} \ No newline at end of file diff --git a/stats/dto/src/main/java/ru/practicum/StatsViewRequestDto.java b/stats/dto/src/main/java/ru/practicum/StatsViewRequestDto.java new file mode 100644 index 0000000..be2757c --- /dev/null +++ b/stats/dto/src/main/java/ru/practicum/StatsViewRequestDto.java @@ -0,0 +1,21 @@ +package ru.practicum; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.List; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class StatsViewRequestDto { + private List uris; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime start; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime end; + private Boolean unique; +} diff --git a/stats/server/src/main/java/ru/practicum/service/StatsServiceImpl.java b/stats/server/src/main/java/ru/practicum/service/StatsServiceImpl.java index 51eb20c..e3a940a 100644 --- a/stats/server/src/main/java/ru/practicum/service/StatsServiceImpl.java +++ b/stats/server/src/main/java/ru/practicum/service/StatsServiceImpl.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import ru.practicum.StatsRequestDto; import ru.practicum.StatsResponseDto; import ru.practicum.exception.ValidationException; @@ -19,11 +20,13 @@ public class StatsServiceImpl implements StatsService { private final StatsRepository statsRepository; + @Transactional @Override public StatsRequestDto createHit(StatsRequestDto statsRequestDto) { return StatsMapper.toStatsDto(statsRepository.save(StatsMapper.toStats(statsRequestDto))); } + @Transactional(readOnly = true) @Override public List getStats(LocalDateTime start, LocalDateTime end,