diff --git a/README.md b/README.md new file mode 100644 index 0000000..3671fb5 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Комментарии — API (техническое задание) + +Краткое описание: набор REST-эндпоинтов для управления комментариями. Три уровня доступа: +- Admin — администраторские операции (удаление комментария по его идентификатору). +- Private — операции пользователя над собственными комментариями (создать, изменить, удалить, получить все свои комментарии). +- Public — публичный доступ (просмотр комментариев конкретного события). + +--- + +## AdminCommentController +Цель: операции управления всеми комментариями. + +- DELETE `/admin/comments/{commentId}` + - Описание: удалить комментарий (жёсткое удаление) по его идентификатору. + - Ограничения: без модерации комментариев. + - Права: только админ. + - Ответ: `204 No Content` при успехе. + +--- + +## PrivateCommentController +Цель: пользовательские операции над собственными комментариями. + +- POST `/users/{userId}/comments/{eventId}` + - Описание: создать новый комментарий от имени пользователя `userId`. + - Ограничения: длина комментария 5000 символов, проверка существования идентификаторов события и пользователя. + - Права: только аутентифицированный пользователь. + - Ответ: `201 Created` с созданным объектом. + +- PATCH `/users/{userId}/comments/{eventId}/{commentId}` + - Описание: обновить собственный комментарий. + - Ограничения: длина комментария 5000 символов, проверка существования идентификаторов комментария, события и пользователя. + - Права: только автор комментария. + - Ответ: обновлённый объект комментария. + +- DELETE `/users/{userId}/comments/{commentId}` + - Описание: удалить собственный комментарий (жёсткое удаление). + - Ограничения: проверка существования идентификаторов комментария и пользователя. + - Права: только автор комментария. + - Ответ: `204 No Content`. + +- GET `/users/{userId}/comments` + - Описание: получить список собственных комментариев. + - Ограничения: организовать пагинацию (по умолчанию по 10 комментариев на странице). + - Права: только аутентифицированный пользователь. + - Ответ: список комментариев. + +--- + +## PublicCommentController +Цель: публичный просмотр комментариев. + +- GET `/comments/event/{eventId}` + - Описание: получить публичный список комментариев к событию. + - Ограничения: организовать пагинацию (по умолчанию по 10 комментариев на странице). + - Доступ: публичный (без авторизации). + - Ответ: список публичных комментариев. \ No newline at end of file diff --git a/main-service/src/main/java/ru/practicum/category/controller/AdminCategoryController.java b/main-service/src/main/java/ru/practicum/category/controller/AdminCategoryController.java index 859fbf6..14afeec 100644 --- a/main-service/src/main/java/ru/practicum/category/controller/AdminCategoryController.java +++ b/main-service/src/main/java/ru/practicum/category/controller/AdminCategoryController.java @@ -5,12 +5,14 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.category.dto.CategoryDto; import ru.practicum.category.dto.NewCategoryDto; import ru.practicum.category.service.CategoryService; @Slf4j +@Validated @RestController @RequestMapping("/admin/categories") @RequiredArgsConstructor @@ -20,21 +22,21 @@ public class AdminCategoryController { @PostMapping @ResponseStatus(HttpStatus.CREATED) public CategoryDto createCategory(@RequestBody @Valid NewCategoryDto newCategoryDto) { - log.debug("Controller: createCategory data {}", newCategoryDto); + log.debug("Controller: createCategory data={}", newCategoryDto); return categoryService.createCategory(newCategoryDto); } @DeleteMapping("/{categoryId}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteCategory(@PathVariable @Positive Long categoryId) { - log.debug("Controller: deleteCategory id={}", categoryId); + log.debug("Controller: deleteCategory categoryId={}", categoryId); categoryService.deleteCategory(categoryId); } @PatchMapping("/{categoryId}") public CategoryDto updateCategory(@PathVariable @Positive Long categoryId, @RequestBody @Valid NewCategoryDto newCategoryDto) { - log.debug("Controller: updateCategory id={}, data={}", categoryId, newCategoryDto); + log.debug("Controller: updateCategory categoryId={}, data={}", categoryId, newCategoryDto); return categoryService.updateCategory(categoryId, newCategoryDto); } } diff --git a/main-service/src/main/java/ru/practicum/category/controller/PublicCategoryController.java b/main-service/src/main/java/ru/practicum/category/controller/PublicCategoryController.java index 367393b..e7398e5 100644 --- a/main-service/src/main/java/ru/practicum/category/controller/PublicCategoryController.java +++ b/main-service/src/main/java/ru/practicum/category/controller/PublicCategoryController.java @@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.category.dto.CategoryDto; import ru.practicum.category.service.CategoryService; @@ -13,6 +14,7 @@ import java.util.List; @Slf4j +@Validated @RestController @RequestMapping("/categories") @RequiredArgsConstructor @@ -23,15 +25,14 @@ public class PublicCategoryController { public List getCategories( @RequestParam(defaultValue = "0") @PositiveOrZero Integer from, @RequestParam(defaultValue = "10") @Positive Integer size) { - int page = from / size; - Pageable pageable = PageRequest.of(page, size); - log.debug("Controller: getCategories with from={}, size={}", from, size); + Pageable pageable = PageRequest.of(from / size, size); + log.debug("Controller: getCategories from={}, size={}", from, size); return categoryService.getCategories(pageable); } @GetMapping("/{categoryId}") public CategoryDto getCategoryById(@PathVariable @Positive Long categoryId) { - log.debug("Controller: getCategoryById with id={}", categoryId); + log.debug("Controller: getCategoryById categoryId={}", categoryId); return categoryService.getCategoryById(categoryId); } } diff --git a/main-service/src/main/java/ru/practicum/category/repository/CategoryRepository.java b/main-service/src/main/java/ru/practicum/category/repository/CategoryRepository.java index 96920b0..b6e486b 100644 --- a/main-service/src/main/java/ru/practicum/category/repository/CategoryRepository.java +++ b/main-service/src/main/java/ru/practicum/category/repository/CategoryRepository.java @@ -1,9 +1,12 @@ package ru.practicum.category.repository; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import ru.practicum.category.model.Category; +import java.util.List; import java.util.Optional; public interface CategoryRepository extends JpaRepository { @@ -11,4 +14,7 @@ public interface CategoryRepository extends JpaRepository { Optional findByName(@Param("name") String name); Optional findByNameAndIdNot(@Param("name") String name, @Param("id") Long id); + + @Query("SELECT c FROM Category c") + List findAllList(Pageable pageable); } diff --git a/main-service/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java b/main-service/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java index 8f12c67..63bbf6a 100644 --- a/main-service/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java +++ b/main-service/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java @@ -2,7 +2,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -69,7 +68,7 @@ public CategoryDto updateCategory(Long categoryId, NewCategoryDto newCategoryDto @Override public List getCategories(Pageable pageable) { - Page categoriesPage = categoryRepository.findAll(pageable); + List categoriesPage = categoryRepository.findAllList(pageable); if (categoriesPage.isEmpty()) { return List.of(); diff --git a/main-service/src/main/java/ru/practicum/comment/controller/AdminCommentController.java b/main-service/src/main/java/ru/practicum/comment/controller/AdminCommentController.java new file mode 100644 index 0000000..ff6ad09 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/comment/controller/AdminCommentController.java @@ -0,0 +1,25 @@ +package ru.practicum.comment.controller; + +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.comment.service.CommentService; + +@Slf4j +@Validated +@RestController +@RequestMapping("/admin/comments") +@RequiredArgsConstructor +public class AdminCommentController { + private final CommentService commentService; + + @DeleteMapping("/{commentId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteCommentAdmin(@PathVariable @Positive Long commentId) { + log.debug("Controller: deleteCommentAdmin commentId={}", commentId); + commentService.deleteCommentAdmin(commentId); + } +} diff --git a/main-service/src/main/java/ru/practicum/comment/controller/PrivateCommentController.java b/main-service/src/main/java/ru/practicum/comment/controller/PrivateCommentController.java new file mode 100644 index 0000000..643bfeb --- /dev/null +++ b/main-service/src/main/java/ru/practicum/comment/controller/PrivateCommentController.java @@ -0,0 +1,65 @@ +package ru.practicum.comment.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.data.domain.PageRequest; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.comment.dto.CommentDto; +import ru.practicum.comment.dto.NewCommentDto; +import ru.practicum.comment.dto.UpdateCommentDto; +import ru.practicum.comment.service.CommentService; + +import java.util.List; + +@Slf4j +@Validated +@RestController +@RequestMapping("/users/{userId}/comments") +@RequiredArgsConstructor +public class PrivateCommentController { + private final CommentService commentService; + + @PostMapping("/{eventId}") + @ResponseStatus(HttpStatus.CREATED) + public CommentDto createComment(@PathVariable @Positive Long userId, + @PathVariable @Positive Long eventId, + @RequestBody @Valid NewCommentDto newCommentDto + ) { + log.debug("Controller: createComment userId={}, eventId={}, data={}", userId, eventId, newCommentDto); + return commentService.createComment(userId, eventId, newCommentDto); + } + + @PatchMapping("/{eventId}/{commentId}") + public CommentDto updateComment(@PathVariable @Positive Long userId, + @PathVariable @Positive Long eventId, + @PathVariable @Positive Long commentId, + @RequestBody @Valid UpdateCommentDto updateCommentDto + ) { + log.debug("Controller: updateComment userId={}, eventId={}, commentId={}, data={}", userId, eventId, commentId, + updateCommentDto); + return commentService.updateComment(userId, eventId, commentId, updateCommentDto); + } + + @DeleteMapping("/{commentId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteComment(@PathVariable @Positive Long userId, + @PathVariable @Positive Long commentId + ) { + log.debug("Controller: deleteComment userId={}, commentId={}", userId, commentId); + commentService.deleteComment(userId, commentId); + } + + @GetMapping + public List getCommentsForUser(@PathVariable @Positive Long userId, + @RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size + ) { + log.debug("Controller: getCommentsForUser userId={}, from={}, size={}", userId, from, size); + return commentService.getCommentsForUser(userId, PageRequest.of(from / size, size)); + } +} diff --git a/main-service/src/main/java/ru/practicum/comment/controller/PublicCommentController.java b/main-service/src/main/java/ru/practicum/comment/controller/PublicCommentController.java new file mode 100644 index 0000000..d57cde1 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/comment/controller/PublicCommentController.java @@ -0,0 +1,31 @@ +package ru.practicum.comment.controller; + +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.comment.dto.CommentDto; +import ru.practicum.comment.service.CommentService; + +import java.util.List; + +@Slf4j +@Validated +@RestController +@RequestMapping("/comments") +@RequiredArgsConstructor +public class PublicCommentController { + private final CommentService commentService; + + @GetMapping("/event/{eventId}") + public List getCommentsForEvent(@PathVariable @Positive Long eventId, + @RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size + ) { + log.debug("Controller: getCommentsForEvent eventId={}, from={}, size={}", eventId, from, size); + return commentService.getCommentsForEvent(eventId, PageRequest.of(from / size, size)); + } +} diff --git a/main-service/src/main/java/ru/practicum/comment/dto/CommentDto.java b/main-service/src/main/java/ru/practicum/comment/dto/CommentDto.java new file mode 100644 index 0000000..c03e2a8 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/comment/dto/CommentDto.java @@ -0,0 +1,21 @@ +package ru.practicum.comment.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDateTime; + +import static ru.practicum.constants.DateTimeConstants.DATE_TIME_PATTERN; + +public record CommentDto( + Long id, + + String text, + + Long userId, + + Long eventId, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DATE_TIME_PATTERN) + LocalDateTime createdDate +) { +} diff --git a/main-service/src/main/java/ru/practicum/comment/dto/NewCommentDto.java b/main-service/src/main/java/ru/practicum/comment/dto/NewCommentDto.java new file mode 100644 index 0000000..377b748 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/comment/dto/NewCommentDto.java @@ -0,0 +1,11 @@ +package ru.practicum.comment.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record NewCommentDto( + @NotBlank(message = "Комментарий не может быть пустым") + @Size(min = 1, max = 5000, message = "Комментарий должен содержать от {min} до {max} символов") + String text +) { +} diff --git a/main-service/src/main/java/ru/practicum/comment/dto/UpdateCommentDto.java b/main-service/src/main/java/ru/practicum/comment/dto/UpdateCommentDto.java new file mode 100644 index 0000000..902b906 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/comment/dto/UpdateCommentDto.java @@ -0,0 +1,11 @@ +package ru.practicum.comment.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record UpdateCommentDto( + @NotBlank(message = "Комментарий не может быть пустым") + @Size(min = 1, max = 5000, message = "Комментарий должен содержать от {min} до {max} символов") + String text +) { +} diff --git a/main-service/src/main/java/ru/practicum/comment/mapper/CommentMapper.java b/main-service/src/main/java/ru/practicum/comment/mapper/CommentMapper.java new file mode 100644 index 0000000..1e760e5 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/comment/mapper/CommentMapper.java @@ -0,0 +1,26 @@ +package ru.practicum.comment.mapper; + +import org.mapstruct.*; +import ru.practicum.comment.dto.CommentDto; +import ru.practicum.comment.dto.NewCommentDto; +import ru.practicum.comment.dto.UpdateCommentDto; +import ru.practicum.comment.model.Comment; +import ru.practicum.event.model.Event; +import ru.practicum.user.model.User; + +@Mapper(componentModel = "spring") +public interface CommentMapper { + @Mapping(target = "id", ignore = true) + @Mapping(target = "createdDate", ignore = true) + Comment toComment(NewCommentDto newCommentDto, User user, Event event); + + @Mapping(target = "userId", source = "user.id") + @Mapping(target = "eventId", source = "event.id") + CommentDto toDto(Comment comment); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "user", ignore = true) + @Mapping(target = "event", ignore = true) + @Mapping(target = "createdDate", ignore = true) + void updateCommentFromDto(UpdateCommentDto dto, @MappingTarget Comment comment); +} diff --git a/main-service/src/main/java/ru/practicum/comment/model/Comment.java b/main-service/src/main/java/ru/practicum/comment/model/Comment.java new file mode 100644 index 0000000..af8a6a0 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/comment/model/Comment.java @@ -0,0 +1,38 @@ +package ru.practicum.comment.model; + +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import ru.practicum.event.model.Event; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "comments") +@Getter +@Setter +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @EqualsAndHashCode.Include + private Long id; + + @Column(nullable = false, length = 5000) + private String text; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "event_id", nullable = false) + private Event event; + + @Column(name = "created_date", nullable = false) + @CreationTimestamp + private LocalDateTime createdDate; +} diff --git a/main-service/src/main/java/ru/practicum/comment/repository/CommentRepository.java b/main-service/src/main/java/ru/practicum/comment/repository/CommentRepository.java new file mode 100644 index 0000000..79f62b9 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/comment/repository/CommentRepository.java @@ -0,0 +1,47 @@ +package ru.practicum.comment.repository; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import ru.practicum.comment.model.Comment; + +import java.util.List; +import java.util.Optional; + +public interface CommentRepository extends JpaRepository { + + @Query(""" + SELECT c FROM Comment c + LEFT JOIN FETCH c.user + LEFT JOIN FETCH c.event + WHERE c.id = :id AND c.user.id = :userId AND c.event.id = :eventId""" + ) + Optional findByIdAndUserIdAndEventId(@Param("id") Long id, @Param("userId") Long userId, @Param("eventId") Long eventId); + + @Query(""" + SELECT c FROM Comment c + LEFT JOIN FETCH c.user + LEFT JOIN FETCH c.event + WHERE c.id = :id AND c.user.id = :userId""" + ) + Optional findByIdAndUserId(@Param("id") Long id, @Param("userId") Long userId); + + @Query(""" + SELECT c FROM Comment c + LEFT JOIN FETCH c.user + LEFT JOIN FETCH c.event + WHERE c.event.id = :eventId + ORDER BY c.createdDate DESC""" + ) + List findByEventId(@Param("eventId") Long eventId, Pageable pageable); + + @Query(""" + SELECT c FROM Comment c + LEFT JOIN FETCH c.user + LEFT JOIN FETCH c.event + WHERE c.user.id = :userId + ORDER BY c.createdDate DESC""" + ) + List findByUserId(@Param("userId") Long userId, Pageable pageable); +} diff --git a/main-service/src/main/java/ru/practicum/comment/service/CommentService.java b/main-service/src/main/java/ru/practicum/comment/service/CommentService.java new file mode 100644 index 0000000..ab0a783 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/comment/service/CommentService.java @@ -0,0 +1,22 @@ +package ru.practicum.comment.service; + +import org.springframework.data.domain.Pageable; +import ru.practicum.comment.dto.CommentDto; +import ru.practicum.comment.dto.NewCommentDto; +import ru.practicum.comment.dto.UpdateCommentDto; + +import java.util.List; + +public interface CommentService { + CommentDto createComment(Long userId, Long eventId, NewCommentDto newCommentDto); + + CommentDto updateComment(Long userId, Long eventId, Long commentId, UpdateCommentDto updateCommentDto); + + void deleteComment(Long userId, Long commentId); + + void deleteCommentAdmin(Long commentId); + + List getCommentsForEvent(Long eventId, Pageable pageable); + + List getCommentsForUser(Long userId, Pageable pageable); +} diff --git a/main-service/src/main/java/ru/practicum/comment/service/CommentServiceImpl.java b/main-service/src/main/java/ru/practicum/comment/service/CommentServiceImpl.java new file mode 100644 index 0000000..1e89a45 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/comment/service/CommentServiceImpl.java @@ -0,0 +1,106 @@ +package ru.practicum.comment.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.comment.dto.CommentDto; +import ru.practicum.comment.dto.NewCommentDto; +import ru.practicum.comment.dto.UpdateCommentDto; +import ru.practicum.comment.mapper.CommentMapper; +import ru.practicum.comment.model.Comment; +import ru.practicum.comment.repository.CommentRepository; +import ru.practicum.event.model.Event; +import ru.practicum.event.model.EventState; +import ru.practicum.event.service.EventService; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.NotFoundException; +import ru.practicum.user.model.User; +import ru.practicum.user.service.UserService; + +import java.util.List; + +@Slf4j +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class CommentServiceImpl implements CommentService { + private final CommentRepository commentRepository; + private final EventService eventService; + private final UserService userService; + private final CommentMapper commentMapper; + + @Override + @Transactional + public CommentDto createComment(Long userId, Long eventId, NewCommentDto newCommentDto) { + User user = userService.getUserByIdOrThrow(userId); + Event event = eventService.getEventOrThrow(eventId); + + if (EventState.PUBLISHED != event.getState()) + throw new ConflictException("Нельзя комментировать неопубликованное событие"); + + Comment comment = commentMapper.toComment(newCommentDto, user, event); + + log.info("Создан новый комментарий: authorId={}, eventId={}, newCommentDto={}", userId, eventId, newCommentDto); + return commentMapper.toDto(commentRepository.save(comment)); + } + + @Override + @Transactional + public CommentDto updateComment(Long userId, Long eventId, Long commentId, UpdateCommentDto updateCommentDto) { + Comment comment = commentRepository.findByIdAndUserIdAndEventId(commentId, userId, eventId) + .orElseThrow(() -> new NotFoundException("Комментарий с id=" + commentId + + " от пользователя с id=" + userId + + " к событию с id=" + eventId + " не найден")); + + commentMapper.updateCommentFromDto(updateCommentDto, comment); + + log.info("Комментарий обновлен: commentId={}, userId={}, eventId={}, updateCommentDto={}", + commentId, userId, eventId, updateCommentDto); + return commentMapper.toDto(commentRepository.save(comment)); + } + + @Override + @Transactional + public void deleteComment(Long userId, Long commentId) { + commentRepository.findByIdAndUserId(commentId, userId) + .orElseThrow(() -> new NotFoundException("Комментарий с id=" + commentId + + " от пользователя с id=" + userId + " не найден")); + + log.info("Комментарий удален: commentId={}, userId={}", commentId, userId); + commentRepository.deleteById(commentId); + } + + @Override + @Transactional + public void deleteCommentAdmin(Long commentId) { + commentRepository.findById(commentId) + .orElseThrow(() -> new NotFoundException("Комментарий с id=" + commentId + " не найден")); + + log.info("Комментарий удален администратором: commentId={}", commentId); + commentRepository.deleteById(commentId); + } + + @Override + public List getCommentsForEvent(Long eventId, Pageable pageable) { + eventService.getEventOrThrow(eventId); + + log.info("Получен список комментариев для события: eventId={}", eventId); + return commentRepository.findByEventId(eventId, pageable) + .stream() + .map(commentMapper::toDto) + .toList(); + } + + @Override + public List getCommentsForUser(Long userId, Pageable pageable) { + userService.getUserByIdOrThrow(userId); + + log.info("Получен список комментариев для пользователя: userId={}", userId); + return commentRepository.findByUserId(userId, pageable) + .stream() + .map(commentMapper::toDto) + .toList(); + } +} diff --git a/main-service/src/main/java/ru/practicum/compilation/controller/AdminCompilationController.java b/main-service/src/main/java/ru/practicum/compilation/controller/AdminCompilationController.java index f4c8f98..2e83f81 100644 --- a/main-service/src/main/java/ru/practicum/compilation/controller/AdminCompilationController.java +++ b/main-service/src/main/java/ru/practicum/compilation/controller/AdminCompilationController.java @@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.compilation.dto.CompilationDto; import ru.practicum.compilation.dto.NewCompilationDto; @@ -12,6 +13,7 @@ import ru.practicum.compilation.service.CompilationService; @Slf4j +@Validated @RestController @RequestMapping("/admin/compilations") @RequiredArgsConstructor @@ -21,7 +23,7 @@ public class AdminCompilationController { @PostMapping @ResponseStatus(HttpStatus.CREATED) public CompilationDto createCompilation(@RequestBody @Valid NewCompilationDto request) { - log.debug("Controller: createCompilation data {}", request); + log.debug("Controller: createCompilation data={}", request); return compilationService.createCompilation(request); } @@ -35,7 +37,7 @@ public void deleteCompilation(@PathVariable @Positive Long compId) { @PatchMapping("/{compId}") public CompilationDto updateCompilation(@PathVariable @Positive Long compId, @RequestBody @Valid UpdateCompilationRequest request) { - log.debug("Controller: updateCompilation compId={}, data {}", compId, request); + log.debug("Controller: updateCompilation compId={}, data={}", compId, request); return compilationService.updateCompilation(compId, request); } } diff --git a/main-service/src/main/java/ru/practicum/compilation/controller/PublicCompilationController.java b/main-service/src/main/java/ru/practicum/compilation/controller/PublicCompilationController.java index 5ee2a3c..e30d0c4 100644 --- a/main-service/src/main/java/ru/practicum/compilation/controller/PublicCompilationController.java +++ b/main-service/src/main/java/ru/practicum/compilation/controller/PublicCompilationController.java @@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.compilation.dto.CompilationDto; import ru.practicum.compilation.service.CompilationService; @@ -13,6 +14,7 @@ import java.util.List; @Slf4j +@Validated @RestController @RequestMapping("/compilations") @RequiredArgsConstructor diff --git a/main-service/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java b/main-service/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java index 349ca99..da7c31e 100644 --- a/main-service/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java +++ b/main-service/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java @@ -1,16 +1,15 @@ 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 org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import ru.practicum.compilation.model.Compilation; +import java.util.List; import java.util.Optional; public interface CompilationRepository extends JpaRepository { - Page findByPinned(Boolean pinned, Pageable pageable); @Query(""" SELECT c FROM Compilation c @@ -21,4 +20,18 @@ public interface CompilationRepository extends JpaRepository Optional findByIdWithEvents(@Param("id") Long id); boolean existsByTitle(String title); + + @Query(""" + SELECT c.id FROM Compilation c + WHERE (:pinned IS NULL OR c.pinned = :pinned) + ORDER BY c.id ASC""") + List findIdsByPinned(@Param("pinned") Boolean pinned, Pageable pageable); + + @Query(""" + SELECT DISTINCT c FROM Compilation c + LEFT JOIN FETCH c.events e + LEFT JOIN FETCH e.category + LEFT JOIN FETCH e.initiator + WHERE c.id IN :ids""") + List findAllByIdInWithEvents(@Param("ids") List ids); } diff --git a/main-service/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java b/main-service/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java index ac5ae0a..0ce3c5f 100644 --- a/main-service/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java +++ b/main-service/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java @@ -2,7 +2,6 @@ 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.data.domain.Sort; @@ -19,9 +18,9 @@ import ru.practicum.exception.ConflictException; import ru.practicum.exception.NotFoundException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; @Slf4j @Service @@ -100,22 +99,27 @@ public List getCompilations(Boolean pinned, Pageable pageable) { Sort.by("id").ascending() ); - Page compilationsPage; - if (pinned != null) - compilationsPage = compilationRepository.findByPinned(pinned, sortedPageable); - else - compilationsPage = compilationRepository.findAll(sortedPageable); + List ids = compilationRepository.findIdsByPinned(pinned, sortedPageable); - List compilations = compilationsPage.getContent(); - - if (!compilations.isEmpty()) { - log.info("Получен список подборок pinned={}, pageable={}", pinned, pageable); - return compilations.stream() - .map(compilationMapper::toDto) - .toList(); + if (ids == null || ids.isEmpty()) { + log.info("Список подборок пуст pinned={}, pageable={}", pinned, pageable); + return List.of(); } - log.info("Список подборок пуст"); - return List.of(); + + List compilationsWithEvents = compilationRepository.findAllByIdInWithEvents(ids); + + Map byId = compilationsWithEvents.stream() + .collect(Collectors.toMap(Compilation::getId, Function.identity())); + + List ordered = ids.stream() + .map(byId::get) + .filter(Objects::nonNull) + .toList(); + + log.info("Получен список подборок pinned={}, pageable={}", pinned, pageable); + return ordered.stream() + .map(compilationMapper::toDto) + .toList(); } @Override diff --git a/main-service/src/main/java/ru/practicum/event/controller/AdminEventController.java b/main-service/src/main/java/ru/practicum/event/controller/AdminEventController.java index 4a3d982..d0a03ba 100644 --- a/main-service/src/main/java/ru/practicum/event/controller/AdminEventController.java +++ b/main-service/src/main/java/ru/practicum/event/controller/AdminEventController.java @@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.PageRequest; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.event.dto.EventFullDto; import ru.practicum.event.dto.SearchEventAdminRequest; @@ -14,6 +15,7 @@ import java.util.List; @Slf4j +@Validated @RestController @RequiredArgsConstructor @RequestMapping("/admin/events") @@ -25,7 +27,7 @@ public List getEventsAdmin(@ModelAttribute @Valid SearchEventAdmin int size = (request.size() != null && request.size() > 0) ? request.size() : 10; int from = request.from() != null ? request.from() : 0; PageRequest pageRequest = PageRequest.of(from / size, size); - log.debug("Controller: getEventAdmin filters {}", request); + log.debug("Controller: getEventAdmin filters={}", request); return eventService.getEventsAdmin(request, pageRequest); } @@ -33,7 +35,7 @@ public List getEventsAdmin(@ModelAttribute @Valid SearchEventAdmin public EventFullDto updateEventAdmin(@PathVariable @Positive Long eventId, @RequestBody @Valid UpdateEventAdminRequest request ) { - log.debug("Controller: updateEventAdmin id={}, data {}", eventId, request); + log.debug("Controller: updateEventAdmin eventId={}, data={}", eventId, request); return eventService.updateEventAdmin(eventId, request); } } diff --git a/main-service/src/main/java/ru/practicum/event/controller/PrivateEventController.java b/main-service/src/main/java/ru/practicum/event/controller/PrivateEventController.java index 7c3d58e..2f50603 100644 --- a/main-service/src/main/java/ru/practicum/event/controller/PrivateEventController.java +++ b/main-service/src/main/java/ru/practicum/event/controller/PrivateEventController.java @@ -9,6 +9,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.event.dto.EventFullDto; import ru.practicum.event.dto.EventShortDto; @@ -23,6 +24,7 @@ import java.util.List; @Slf4j +@Validated @RestController @RequestMapping("/users/{userId}/events") @RequiredArgsConstructor @@ -36,7 +38,7 @@ public List getEvents(@PathVariable("userId") @Positive Long user @RequestParam(defaultValue = "10") @Positive Integer size ) { Pageable pageable = PageRequest.of(from / size, size); - log.debug("Controller: getEvents with id={} with pageable {}", userId, pageable); + log.debug("Controller: getEvents userId={}, pageable={}", userId, pageable); return eventService.getEvents(userId, pageable); } @@ -45,7 +47,7 @@ public List getEvents(@PathVariable("userId") @Positive Long user public EventFullDto createEvent(@PathVariable("userId") @Positive Long userId, @RequestBody @Valid NewEventDto newEventDto ) { - log.debug("Controller: createEvent with id={} with data {}", userId, newEventDto); + log.debug("Controller: createEvent userId={}, data={}", userId, newEventDto); return eventService.createEvent(userId, newEventDto); } @@ -54,7 +56,7 @@ public EventFullDto getEvent(@PathVariable("userId") @Positive Long userId, @PathVariable("eventId") @Positive Long eventId, HttpServletRequest request ) { - log.debug("Controller: getEvent with id={} and eventId={}", userId, eventId); + log.debug("Controller: getEvent userId={}, eventId={}", userId, eventId); return eventService.getEvent(userId, eventId, request.getRemoteAddr()); } @@ -63,7 +65,7 @@ public EventFullDto updateEvent(@PathVariable("userId") @Positive Long userId, @PathVariable("eventId") @Positive Long eventId, @RequestBody @Valid UpdateEventUserRequest request ) { - log.debug("Controller: updateEvent with id={} and eventId={} with data {}", userId, eventId, request); + log.debug("Controller: updateEvent userId={}, eventId={}, data={}", userId, eventId, request); return eventService.updateEvent(userId, eventId, request); } @@ -71,7 +73,7 @@ public EventFullDto updateEvent(@PathVariable("userId") @Positive Long userId, public List getRequestsByEvent(@PathVariable("userId") @Positive Long userId, @PathVariable("eventId") @Positive Long eventId ) { - log.debug("Controller: getRequestsByEvent with id={} and eventId={}", userId, eventId); + log.debug("Controller: getRequestsByEvent userId={}, eventId={}", userId, eventId); return requestService.getRequestsByEvent(userId, eventId); } @@ -80,7 +82,7 @@ public EventRequestStatusUpdateResult updateRequestStatus(@PathVariable("userId" @PathVariable("eventId") @Positive Long eventId, @RequestBody EventRequestStatusUpdateRequest request ) { - log.debug("Controller: updateRequestStatus with id={} and eventId={} with data {}", userId, eventId, request); + log.debug("Controller: updateRequestStatus userId={}, eventId={}, data={}", userId, eventId, request); return requestService.updateRequestStatus(userId, eventId, request); } } diff --git a/main-service/src/main/java/ru/practicum/event/controller/PublicEventController.java b/main-service/src/main/java/ru/practicum/event/controller/PublicEventController.java index ef94b27..394b3c3 100644 --- a/main-service/src/main/java/ru/practicum/event/controller/PublicEventController.java +++ b/main-service/src/main/java/ru/practicum/event/controller/PublicEventController.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.PageRequest; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.event.dto.EventFullDto; import ru.practicum.event.dto.EventShortDto; @@ -15,6 +16,7 @@ import java.util.List; @Slf4j +@Validated @RestController @RequestMapping("/events") @RequiredArgsConstructor @@ -28,13 +30,14 @@ public List getEventsPublic(@ModelAttribute @Valid SearchEventPub int size = (request.size() != null && request.size() > 0) ? request.size() : 10; int from = request.from() != null ? request.from() : 0; PageRequest pageRequest = PageRequest.of(from / size, size); - log.debug("Controller: getEventsPublic with filters {}", request); + log.debug("Controller: getEventsPublic filters={}", request); return eventService.getEventsPublic(request, pageRequest, httpRequest.getRemoteAddr()); } @GetMapping("/{id}") - public EventFullDto getEventByIdPublic(@PathVariable @Positive Long id, HttpServletRequest request) { - log.debug("Controller: getEventByIdPublic with id={}", id); - return eventService.getEventByIdPublic(id, request.getRemoteAddr()); + public EventFullDto getEventByIdPublic(@PathVariable("id") @Positive Long eventId, + HttpServletRequest httpRequest) { + log.debug("Controller: getEventByIdPublic eventId={}", eventId); + return eventService.getEventByIdPublic(eventId, httpRequest.getRemoteAddr()); } } diff --git a/main-service/src/main/java/ru/practicum/event/repository/EventRepository.java b/main-service/src/main/java/ru/practicum/event/repository/EventRepository.java index 7e4d094..44ae52b 100644 --- a/main-service/src/main/java/ru/practicum/event/repository/EventRepository.java +++ b/main-service/src/main/java/ru/practicum/event/repository/EventRepository.java @@ -1,6 +1,5 @@ package ru.practicum.event.repository; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -13,7 +12,7 @@ public interface EventRepository extends JpaRepository, JpaSpecificationExecutor { - Page findAllByInitiatorIdOrderByCreatedOnDesc(Long initiatorId, Pageable pageable); + List findAllByInitiatorIdOrderByCreatedOnDesc(Long initiatorId, Pageable pageable); Event findFirstByOrderByCreatedOnAsc(); diff --git a/main-service/src/main/java/ru/practicum/event/service/EventServiceImpl.java b/main-service/src/main/java/ru/practicum/event/service/EventServiceImpl.java index 8608b9a..34765e0 100644 --- a/main-service/src/main/java/ru/practicum/event/service/EventServiceImpl.java +++ b/main-service/src/main/java/ru/practicum/event/service/EventServiceImpl.java @@ -66,17 +66,17 @@ public EventFullDto createEvent(Long userId, NewEventDto newEventDto) { @Override public List getEvents(Long userId, Pageable pageable) { userService.getUserById(userId); - Page eventsPage = eventRepository.findAllByInitiatorIdOrderByCreatedOnDesc(userId, pageable); + List events = eventRepository.findAllByInitiatorIdOrderByCreatedOnDesc(userId, pageable); - if (eventsPage.isEmpty()) return List.of(); + if (events.isEmpty()) return List.of(); - List eventIds = eventsPage.stream() + List eventIds = events.stream() .map(Event::getId) .toList(); EventStatistics stats = getEventStatistics(eventIds); - return eventsPage.stream() + return events.stream() .map(event -> eventMapper.toEventShortDto( event, stats.confirmedRequests().getOrDefault(event.getId(), 0), diff --git a/main-service/src/main/java/ru/practicum/request/controller/PrivateRequestController.java b/main-service/src/main/java/ru/practicum/request/controller/PrivateRequestController.java index 23ae88d..d56480b 100644 --- a/main-service/src/main/java/ru/practicum/request/controller/PrivateRequestController.java +++ b/main-service/src/main/java/ru/practicum/request/controller/PrivateRequestController.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.request.dto.ParticipationRequestDto; import ru.practicum.request.service.RequestService; @@ -11,6 +12,7 @@ import java.util.List; @Slf4j +@Validated @RestController @RequestMapping("/users/{userId}/requests") @RequiredArgsConstructor diff --git a/main-service/src/main/java/ru/practicum/user/controller/AdminUserController.java b/main-service/src/main/java/ru/practicum/user/controller/AdminUserController.java index b55727b..cf2c200 100644 --- a/main-service/src/main/java/ru/practicum/user/controller/AdminUserController.java +++ b/main-service/src/main/java/ru/practicum/user/controller/AdminUserController.java @@ -8,6 +8,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.user.dto.NewUserRequest; import ru.practicum.user.dto.UserDto; @@ -16,6 +17,7 @@ import java.util.List; @Slf4j +@Validated @RestController @RequestMapping("/admin/users") @RequiredArgsConstructor @@ -25,7 +27,7 @@ public class AdminUserController { @PostMapping @ResponseStatus(HttpStatus.CREATED) public UserDto createUser(@RequestBody @Valid NewUserRequest userRequest) { - log.debug("Controller: createUser {}", userRequest); + log.debug("Controller: createUser data={}", userRequest); return userService.createUser(userRequest); } @@ -42,7 +44,7 @@ public List getUsers(@RequestParam(required = false) List ids, @DeleteMapping("/{userId}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteUser(@PathVariable @Positive Long userId) { - log.debug("Controller: deleteUser id={}", userId); + log.debug("Controller: deleteUser userId={}", userId); userService.deleteUser(userId); } } diff --git a/main-service/src/main/java/ru/practicum/user/repository/UserRepository.java b/main-service/src/main/java/ru/practicum/user/repository/UserRepository.java index e3d8095..39d8c66 100644 --- a/main-service/src/main/java/ru/practicum/user/repository/UserRepository.java +++ b/main-service/src/main/java/ru/practicum/user/repository/UserRepository.java @@ -1,8 +1,8 @@ package ru.practicum.user.repository; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import ru.practicum.user.model.User; import java.util.List; @@ -10,5 +10,8 @@ public interface UserRepository extends JpaRepository { boolean existsByEmail(String email); - Page findByIdIn(List ids, Pageable pageable); + @Query("SELECT u FROM User u") + List findAllList(Pageable pageable); + + List findByIdIn(List ids, Pageable pageable); } diff --git a/main-service/src/main/java/ru/practicum/user/service/UserServiceImpl.java b/main-service/src/main/java/ru/practicum/user/service/UserServiceImpl.java index f9666c0..7bab55b 100644 --- a/main-service/src/main/java/ru/practicum/user/service/UserServiceImpl.java +++ b/main-service/src/main/java/ru/practicum/user/service/UserServiceImpl.java @@ -2,7 +2,6 @@ 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.data.domain.Sort; @@ -48,8 +47,8 @@ public List getUsers(List ids, Pageable pageable) { pageable.getPageSize(), Sort.by(ASC, "id") ); - Page users = (ids == null || ids.isEmpty()) - ? userRepository.findAll(sortedPageable) + List users = (ids == null || ids.isEmpty()) + ? userRepository.findAllList(sortedPageable) : userRepository.findByIdIn(ids, sortedPageable); return users.stream() .map(userMapper::toDto) diff --git a/main-service/src/main/resources/schema.sql b/main-service/src/main/resources/schema.sql index fab07f9..59365c2 100644 --- a/main-service/src/main/resources/schema.sql +++ b/main-service/src/main/resources/schema.sql @@ -45,4 +45,15 @@ create TABLE IF NOT EXISTS compilation_events ( compilation_id BIGINT NOT NULL REFERENCES compilations (id), event_id BIGINT NOT NULL REFERENCES events (id), CONSTRAINT pk_compilation_events PRIMARY KEY (compilation_id, event_id) -); \ No newline at end of file +); + +create TABLE IF NOT EXISTS comments ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + text TEXT NOT NULL, + user_id BIGINT NOT NULL REFERENCES users (id), + event_id BIGINT NOT NULL REFERENCES events (id), + created_date TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_comments_event_id ON comments(event_id); +CREATE INDEX IF NOT EXISTS idx_comments_user_id ON comments(user_id); \ No newline at end of file diff --git a/postman/feature.json b/postman/feature.json new file mode 100644 index 0000000..3fc3b7f --- /dev/null +++ b/postman/feature.json @@ -0,0 +1,829 @@ +{ + "info": { + "_postman_id": "0b4cd248-a26a-4053-b877-6c6779f465ca", + "name": "Feature", + "description": "Тестирование комментариев", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "45621385", + "_collection_link": "https://belkinivan.postman.co/workspace/Belkin-Ivan's-Workspace~70934887-56e4-4bfd-8ac6-47a16ac3b83d/collection/45621385-0b4cd248-a26a-4053-b877-6c6779f465ca?action=share&source=collection_link&creator=45621385" + }, + "item": [ + { + "name": "Admin", + "item": [ + { + "name": "Удаление комментария администратором", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Код ответа должен быть 204\", function () {", + " pm.response.to.have.status(204);", + "});", + "", + "pm.test(\"Тело ответа должно быть пустым\", function () {", + " pm.response.to.have.body('');", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const prepareAdminTest = async () => {", + " const api = new API(pm);", + " const rnd = new RandomUtils();", + " ", + " try {", + " const user = await api.addUser(rnd.getUser());", + " const category = await api.addCategory(rnd.getCategory());", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));", + " event = await api.publishEvent(event.id);", + " ", + " const commentData = {", + " text: \"Комментарий для тестирования удаления администратором\"", + " };", + " ", + " const comment = await api.post(", + " `/users/${user.id}/comments/${event.id}`, ", + " commentData, ", + " pm.collectionVariables.get(\"baseUrl\")", + " );", + " ", + " pm.collectionVariables.set(\"adminCommentId\", comment.id);", + " console.log(\"Создан комментарий для удаления администратором:\", comment.id);", + " ", + " } catch(err) {", + " console.error(\"Ошибка при подготовке комментария:\", err);", + " }", + "};", + "", + "prepareAdminTest();" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{baseUrl}}/admin/comments/{{adminCommentId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "comments", + "{{adminCommentId}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Private", + "item": [ + { + "name": "Создание комментария", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const prepareCommentTest = async () => {", + " const api = new API(pm);", + " const rnd = new RandomUtils();", + " ", + " try {", + " // Создаем пользователя и событие", + " const user = await api.addUser(rnd.getUser());", + " const category = await api.addCategory(rnd.getCategory());", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));", + " event = await api.publishEvent(event.id);", + " ", + " // Сохраняем ID для использования в тестах", + " pm.collectionVariables.set(\"userId\", user.id);", + " pm.collectionVariables.set(\"eventId\", event.id);", + " ", + " // Подготавливаем данные комментария", + " const commentData = {", + " text: \"Комментарий к событию.\"", + " };", + " ", + " pm.request.body.update({", + " mode: 'raw',", + " raw: JSON.stringify(commentData),", + " options: { raw: { language: 'json' } }", + " });", + " ", + " console.log(\"Подготовлены данные для создания комментария:\", commentData);", + " ", + " } catch(err) {", + " console.error(\"Ошибка при подготовке данных:\", err);", + " }", + "};", + "", + "prepareCommentTest();" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// Проверка успешного создания комментария", + "pm.test(\"Статус код должен быть 201\", function () {", + " pm.response.to.have.status(201);", + "});", + "", + "pm.test(\"Ответ должен содержать поля комментария\", function () {", + " const response = pm.response.json();", + " pm.expect(response).to.have.property('id');", + " pm.expect(response).to.have.property('text');", + " pm.expect(response).to.have.property('userId');", + " pm.expect(response).to.have.property('eventId');", + " pm.expect(response).to.have.property('createdDate');", + " ", + " // Сохраняем ID комментария для дальнейших тестов", + " pm.collectionVariables.set(\"commentId\", response.id);", + "});", + "", + "pm.test(\"Текст комментария должен совпадать с отправленным\", function () {", + " const response = pm.response.json();", + " const requestBody = JSON.parse(pm.request.body.raw);", + " pm.expect(response.text).to.equal(requestBody.text);", + "});", + "", + "pm.test(\"ID события должны совпадать с отправленным\", function () {", + " const response = pm.response.json();", + " const eventId = pm.collectionVariables.get(\"eventId\");", + " pm.expect(response.eventId).to.equal(parseInt(eventId));", + "});", + "", + "pm.test(\"ID пользователя должны совпадать с отправленным\", function () {", + " const response = pm.response.json();", + " const userId = pm.collectionVariables.get(\"userId\");", + " pm.expect(response.userId).to.equal(parseInt(userId));", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"text\": \"Новый комментарий\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/{{userId}}/comments/{{eventId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "{{userId}}", + "comments", + "{{eventId}}" + ] + } + }, + "response": [] + }, + { + "name": "Создание слишком длинного комментария", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "// Создаем комментарий длиннее 5000 символов", + "const longText = 'a'.repeat(5001);", + "const invalidComment = {", + " text: longText", + "};", + "", + "pm.request.body.update({", + " mode: 'raw',", + " raw: JSON.stringify(invalidComment),", + " options: { raw: { language: 'json' } }", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Статус код должен быть 400\", function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"text\": \"a\".repeat(5001)}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/{{userId}}/comments/{{eventId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "{{userId}}", + "comments", + "{{eventId}}" + ] + } + }, + "response": [] + }, + { + "name": "Создание пустого комментария", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const emptyComment = {", + " text: \"\"", + "};", + "", + "pm.request.body.update({", + " mode: 'raw',", + " raw: JSON.stringify(emptyComment),", + " options: { raw: { language: 'json' } }", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Статус код должен быть 400\", function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"text\": \"\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/{{userId}}/comments/{{eventId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "{{userId}}", + "comments", + "{{eventId}}" + ] + } + }, + "response": [] + }, + { + "name": "Получение комментариев пользователя", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Статус код должен быть 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"В ответе должен быть список комментариев пользователя\", function () {", + " const response = pm.response.json();", + " const userId = pm.collectionVariables.get(\"userId\");", + " ", + " pm.expect(response).to.be.an('array');", + " ", + " response.forEach(comment => {", + " pm.expect(comment.userId).to.equal(parseInt(userId));", + " });", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/users/{{userId}}/comments?from=0&size=10", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "{{userId}}", + "comments" + ], + "query": [ + { + "key": "from", + "value": "0" + }, + { + "key": "size", + "value": "10" + } + ] + } + }, + "response": [] + }, + { + "name": "Обновление комментария", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const updateData = {", + " text: \"Обновленный текст комментария\"", + "};", + "", + "pm.request.body.update({", + " mode: 'raw',", + " raw: JSON.stringify(updateData),", + " options: { raw: { language: 'json' } }", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Статус код должен быть 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Текст комментария должен быть обновленный\", function () {", + " const response = pm.response.json();", + " const requestBody = JSON.parse(pm.request.body.raw);", + " pm.expect(response.text).to.equal(requestBody.text);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"text\": \"Обновленный текст комментария\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/users/{{userId}}/comments/{{eventId}}/{{commentId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "{{userId}}", + "comments", + "{{eventId}}", + "{{commentId}}" + ] + } + }, + "response": [] + }, + { + "name": "Удаление комментария пользователем", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Статус код должен быть 204\", function () {", + " pm.response.to.have.status(204);", + "});", + "", + "pm.test(\"Тело ответа должно быть пустым\", function () {", + " pm.response.to.have.body('');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{baseUrl}}/users/{{userId}}/comments/{{commentId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "{{userId}}", + "comments", + "{{commentId}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Public", + "item": [ + { + "name": "Получение комментариев события", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Статус код должен быть 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"В ответе должен быть список комментариев\", function () {", + " const response = pm.response.json();", + " pm.expect(response).to.be.an('array');", + " pm.expect(response.length).to.be.greaterThan(0);", + "});", + "", + "pm.test(\"Комментарии должны относиться к определенному событию\", function () {", + " const response = pm.response.json();", + " const eventId = pm.collectionVariables.get(\"eventId\");", + " ", + " response.forEach(comment => {", + " pm.expect(comment.eventId).to.equal(parseInt(eventId));", + " });", + "});", + "", + "pm.test(\"Комментарии должны быть отсортированы по дате создания\", function () {", + " const response = pm.response.json();", + " if (response.length > 1) {", + " const firstDate = new Date(response[0].createdDate);", + " const secondDate = new Date(response[1].createdDate);", + " pm.expect(firstDate.getTime()).to.be.greaterThan(secondDate.getTime());", + " }", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const prepareCommentTest = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + " \r", + " try {\r", + " // Создаем пользователя и событие\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let event = await api.addEvent(user.id, rnd.getEvent(category.id));\r", + " event = await api.publishEvent(event.id);\r", + " const comment = await api.addComment(user.id, event.id, rnd.getComment()); \r", + " // Сохраняем ID для использования в тестах\r", + " pm.collectionVariables.set(\"userId\", user.id);\r", + " pm.collectionVariables.set(\"eventId\", event.id);\r", + " \r", + " console.log(\"Подготовлены данные для создания комментария:\", comment);\r", + " \r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке данных:\", err);\r", + " }\r", + "};\r", + "\r", + "prepareCommentTest();" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/comments/event/{{eventId}}?from=0&size=10", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "comments", + "event", + "{{eventId}}" + ], + "query": [ + { + "key": "from", + "value": "0" + }, + { + "key": "size", + "value": "10" + } + ] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "requests": {}, + "exec": [ + "// Класс для работы с API комментариев", + "API = class {", + " constructor(postman, verbose = false, baseUrl = \"http://localhost:8080\") {", + " this.baseUrl = baseUrl;", + " this.pm = postman;", + " this._verbose = verbose;", + " }", + "", + " async addUser(user, verbose=null) {", + " return this.post(\"/admin/users\", user, \"Ошибка при добавлении нового пользователя: \", verbose);", + " }", + "", + " async addCategory(category, verbose=null) {", + " return this.post(\"/admin/categories\", category, \"Ошибка при добавлении новой категории: \", verbose);", + " }", + "", + " async addComment(userId, eventId, comment, verbose=null) {", + " return this.post(\"/users/\" + userId + \"/comments/\" + eventId, comment, \"Ошибка при добавлении нового комментария: \", verbose);", + " }", + "", + " async addEvent(userId, event, verbose=null) {", + " return this.post(\"/users/\" + userId + \"/events\", event, \"Ошибка при добавлении нового события: \", verbose);", + " }", + "", + " async publishEvent(eventId, verbose=null) {", + " return this.patch('/admin/events/' + eventId, {stateAction: \"PUBLISH_EVENT\"}, \"Ошибка при публикации события\", verbose);", + " }", + "", + " async post(path, body, errorText = \"Ошибка при выполнении post-запроса: \", verbose=null) {", + " return this.sendRequest(\"POST\", path, body, errorText, verbose);", + " }", + "", + " async patch(path, body = null, errorText = \"Ошибка при выполнении patch-запроса: \", verbose=null) {", + " return this.sendRequest(\"PATCH\", path, body, errorText);", + " }", + "", + " async get(path, body = null, errorText = \"Ошибка при выполнении get-запроса: \", verbose=null) {", + " return this.sendRequest(\"GET\", path, body, errorText, verbose);", + " }", + "", + " async sendRequest(method, path, body=null, errorText = \"Ошибка при выполнении запроса: \", verbose=null) {", + " return new Promise((resolve, reject) => {", + " verbose = verbose == null ? this._verbose : verbose;", + " ", + " const request = {", + " url: this.baseUrl + path,", + " method: method,", + " body: body == null ? \"\" : JSON.stringify(body),", + " header: { \"Content-Type\": \"application/json\" },", + " };", + "", + " if(verbose) {", + " console.log(\"Отправляю запрос: \", request);", + " }", + "", + " try {", + " this.pm.sendRequest(request, (error, response) => {", + " if(error || (response.code >= 400 && response.code <= 599)) {", + " let err = error ? error : JSON.stringify(response.json());", + " console.error(\"При выполнении запроса к серверу возникла ошибка.\\n\", err,", + " \"\\nДля отладки проблемы повторите такой же запрос к вашей программе \" + ", + " \"на локальном компьютере. Данные запроса:\\n\", JSON.stringify(request));", + "", + " reject(new Error(errorText + err));", + " }", + "", + " if(verbose) {", + " console.log(\"Результат обработки запроса: код состояния - \", response.code, \", тело: \", response.json());", + " }", + " ", + " if (response.stream.length === 0){", + " reject(new Error('Отправлено пустое тело ответа'))", + " }else{", + " resolve(response.json());", + " }", + " });", + " } catch(err) {", + " if(verbose) {", + " console.error(errorText, err);", + " }", + " return Promise.reject(err);", + " }", + " });", + " }", + "};", + "", + "RandomUtils = class {", + " constructor() {}", + "", + " getUser() {", + " return {", + " name: pm.variables.replaceIn('{{$randomFullName}}'),", + " email: pm.variables.replaceIn('{{$randomEmail}}')", + " };", + " }", + "", + " getCategory() {", + " return {", + " name: pm.variables.replaceIn('{{$randomWord}}') + Math.floor(Math.random() * 100).toString()", + " };", + " }", + "", + " getComment() {", + " return {", + " text: pm.variables.replaceIn('{{$randomWord}}') + Math.floor(Math.random() * 100).toString()", + " };", + " }", + "", + " getEvent(categoryId) {", + " return {", + " annotation: \"Событие для проверки комментариев\",", + " category: categoryId,", + " description: \"Описание события для проверки комментариев\",", + " eventDate: this.getFutureDateTime(),", + " location: {", + " lat: 55.5555,", + " lon: 44.4444,", + " },", + " paid: false,", + " participantLimit: 0,", + " requestModeration: false,", + " title: \"Событие для проверки комментариев\",", + " }", + " }", + " ", + " getFutureDateTime(hourShift = 5, minuteShift=0, yearShift=0) {", + " let moment = require('moment');", + "", + " let m = moment();", + " m.add(hourShift, 'hour');", + " m.add(minuteShift, 'minute');", + " m.add(yearShift, 'year');", + "", + " return m.format('YYYY-MM-DD HH:mm:ss');", + " }", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "requests": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:8080" + }, + { + "key": "userId", + "value": "" + }, + { + "key": "eventId", + "value": "" + }, + { + "key": "commentId", + "value": "" + }, + { + "key": "adminCommentId", + "value": "" + }, + { + "key": "userEmail", + "value": "" + } + ] +} \ No newline at end of file