diff --git a/README.md b/README.md index d36813d..745597d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # java-explore-with-me -Итоговый проект учебного курса. май 2025. гр. 53. +Итоговый проект учебного курса. июль 2025. + +Pull request: +https://github.com/andrej1307/java-explore-with-me/pull/3 diff --git a/ewm-service/schema.png b/ewm-service/schema.png deleted file mode 100644 index fdde26e..0000000 Binary files a/ewm-service/schema.png and /dev/null differ diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java index f2d57af..3b93dd4 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java @@ -7,16 +7,15 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import ru.practicum.evmsevice.client.StatsClient; -import ru.practicum.evmsevice.dto.CategoryDto; -import ru.practicum.evmsevice.dto.CompilationDto; -import ru.practicum.evmsevice.dto.EventFullDto; -import ru.practicum.evmsevice.dto.EventShortDto; +import ru.practicum.evmsevice.dto.*; +import ru.practicum.evmsevice.enums.CommentState; import ru.practicum.evmsevice.enums.EventState; import ru.practicum.evmsevice.exception.NotFoundException; import ru.practicum.evmsevice.mapper.CategoryMapper; import ru.practicum.evmsevice.mapper.EventMapper; import ru.practicum.evmsevice.model.Event; import ru.practicum.evmsevice.service.CategoryService; +import ru.practicum.evmsevice.service.CommentService; import ru.practicum.evmsevice.service.CompilationService; import ru.practicum.evmsevice.service.EventService; @@ -31,6 +30,7 @@ public class PublicController { private final EventService eventService; private final CompilationService compilationService; private final CategoryService categoryService; + private final CommentService commentService; @Value("${spring.application.name}") private String appName; @@ -108,4 +108,28 @@ public CategoryDto findCategoryById(@PathVariable("catId") int catId) { log.info("Пользователь запрашивает категорию id={}.", catId); return CategoryMapper.toDto(categoryService.getCategoryById(catId)); } + + @GetMapping("/events/{eventId}/comments") + @ResponseStatus(HttpStatus.OK) + public List getCommentsByEventId(@PathVariable Integer eventId, + @RequestParam(name = "text", required = false) String text, + @RequestParam(name = "authorIds", required = false) List authorIds, + @RequestParam(name = "rangeStart", required = false) String rangeStart, + @RequestParam(name = "rangeEnd", required = false) String rangeEnd, + @RequestParam(name = "state", defaultValue = "APPROVED") String state, + @RequestParam(name = "sort", defaultValue = "new") String sort, + @RequestParam(name = "from", defaultValue = "0") Integer from, + @RequestParam(name = "size", defaultValue = "10") Integer size) { + CommentState commentState = CommentState.from(state).orElse(CommentState.APPROVED); + log.info("Поиск коментариев к событию id={}.", eventId); + return commentService.getCommentsByEventId(eventId, text, authorIds, rangeStart, rangeEnd, commentState, sort, from, size); + } + + @GetMapping("/comments/{commentId}") + @ResponseStatus(HttpStatus.OK) + public CommentDto getComment(@PathVariable Integer commentId) { + log.info("Поиск коментария id={}.", commentId); + return commentService.getCommentById(commentId); + } + } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserCommentController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserCommentController.java new file mode 100644 index 0000000..3560b10 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserCommentController.java @@ -0,0 +1,72 @@ +package ru.practicum.evmsevice.controller; + +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.evmsevice.dto.CommentDto; +import ru.practicum.evmsevice.dto.CommentModerationDto; +import ru.practicum.evmsevice.dto.CommentsGroupDto; +import ru.practicum.evmsevice.dto.NewCommentDto; +import ru.practicum.evmsevice.service.CommentService; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping("/users") +public class UserCommentController { + private final CommentService commentService; + + @PostMapping("/{userId}/events/{eventId}/comments") + @ResponseStatus(HttpStatus.CREATED) + public CommentDto addNewComment(@PathVariable Integer userId, + @PathVariable Integer eventId, + @RequestBody @Validated NewCommentDto commentDto) { + log.info("Пользователь id={} добавляет комментарий к событию id={}. {}", + userId, eventId, commentDto.toString()); + return commentService.addComment(userId, eventId, commentDto); + } + + @PatchMapping("/{userId}/comments/{commentId}") + @ResponseStatus(HttpStatus.OK) + public CommentDto updateComment(@PathVariable Integer userId, + @PathVariable Integer commentId, + @RequestBody @Validated NewCommentDto commentDto) { + log.info("Пользователь id={} редактирует комментарий id={}. {}", + userId, commentId, commentDto.toString()); + return commentService.updateComment(userId, commentId, commentDto); + } + + @PatchMapping("/{userId}/events/{eventId}/comments") + @ResponseStatus(HttpStatus.OK) + public CommentsGroupDto addNewComment(@PathVariable Integer userId, + @PathVariable Integer eventId, + @RequestBody CommentModerationDto cmModDto) { + log.info("Пользователь id={} модерирует комментарии к событию id={}. {}", + userId, eventId, cmModDto.toString()); + return commentService.moderationComments(userId, eventId, cmModDto); + } + + + @GetMapping("/{userId}/comments") + @ResponseStatus(HttpStatus.OK) + public List getCommentsByUserId(@PathVariable Integer userId, + @RequestParam(name = "from", defaultValue = "0") Integer from, + @RequestParam(name = "size", defaultValue = "10") Integer size) { + log.info("Поиск всех комментариев пользователя id={}.", userId); + return commentService.getCommentsByUserId(userId, from, size); + } + + + @DeleteMapping("/{userId}/comments/{commentId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteComment(@PathVariable Integer commentId, + @PathVariable Integer userId) { + + log.info("Пользователь id={} удаляет комментарий id={}.", userId, commentId); + commentService.deleteComment(userId, commentId); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java index 29f8748..d83245a 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java @@ -8,6 +8,7 @@ import ru.practicum.evmsevice.dto.*; import ru.practicum.evmsevice.mapper.RequestMapper; import ru.practicum.evmsevice.model.Request; +import ru.practicum.evmsevice.service.CommentService; import ru.practicum.evmsevice.service.EventService; import ru.practicum.evmsevice.service.RequestService; @@ -20,6 +21,7 @@ public class UserController { private final EventService eventService; private final RequestService requestService; + private final CommentService commentService; @PostMapping("/{id}/events") @ResponseStatus(HttpStatus.CREATED) @@ -103,4 +105,6 @@ public RequestDto canceledRequestById(@PathVariable Integer userId, log.info("Пользователь id={} отменяет запрос id={}.", userId, requestId); return RequestMapper.toRequestDto(requestService.canceledRequest(userId, requestId)); } + + } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java new file mode 100644 index 0000000..12d11f0 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java @@ -0,0 +1,26 @@ +package ru.practicum.evmsevice.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.evmsevice.enums.CommentState; + +import java.time.LocalDateTime; + +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class CommentDto { + private Integer id; + private UserDto author; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdOn; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime editedOn; + private Integer eventId; + private String text; + private CommentState state; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentModerationDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentModerationDto.java new file mode 100644 index 0000000..32b12ff --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentModerationDto.java @@ -0,0 +1,19 @@ +package ru.practicum.evmsevice.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import ru.practicum.evmsevice.enums.CommentState; + +import java.util.ArrayList; +import java.util.List; + +@Setter +@Getter +@NoArgsConstructor +@ToString +public class CommentModerationDto { + private List commentIds = new ArrayList<>(); + private CommentState state; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentsGroupDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentsGroupDto.java new file mode 100644 index 0000000..0f76dfe --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentsGroupDto.java @@ -0,0 +1,19 @@ +package ru.practicum.evmsevice.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.ArrayList; +import java.util.List; + +@Setter +@Getter +@NoArgsConstructor +@ToString + +public class CommentsGroupDto { + private List approvedComments = new ArrayList<>(); + private List rejectedComments = new ArrayList<>(); +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewCommentDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewCommentDto.java new file mode 100644 index 0000000..5b99ef5 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewCommentDto.java @@ -0,0 +1,16 @@ +package ru.practicum.evmsevice.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.*; + +@Setter +@Getter +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class NewCommentDto { + @NotBlank(message = "Текст комментария должен быть задан.") + @Size(min = 3, max = 2000, message = "Длина комментария должна быть от 3 до 2000 символов.") + private String text; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/enums/CommentState.java b/ewm-service/src/main/java/ru/practicum/evmsevice/enums/CommentState.java new file mode 100644 index 0000000..222f5b8 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/enums/CommentState.java @@ -0,0 +1,18 @@ +package ru.practicum.evmsevice.enums; + +import java.util.Optional; + +public enum CommentState { + PENDING, + APPROVED, + REJECTED; + + public static Optional from(String state) { + for (CommentState commentState : CommentState.values()) { + if (commentState.name().equalsIgnoreCase(state)) { + return Optional.of(commentState); + } + } + return Optional.empty(); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CommentMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CommentMapper.java new file mode 100644 index 0000000..96e94b3 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CommentMapper.java @@ -0,0 +1,31 @@ +package ru.practicum.evmsevice.mapper; + +import ru.practicum.evmsevice.dto.CommentDto; +import ru.practicum.evmsevice.dto.NewCommentDto; +import ru.practicum.evmsevice.model.Comment; + +import java.time.LocalDateTime; + +public class CommentMapper { + private CommentMapper() { + } + + public static Comment getComment(NewCommentDto newCommemtDto) { + Comment comment = new Comment(); + comment.setCreatedOn(LocalDateTime.now()); + comment.setText(newCommemtDto.getText()); + return comment; + } + + public static CommentDto toDto(Comment comment) { + CommentDto commentDto = new CommentDto(); + commentDto.setId(comment.getId()); + commentDto.setEventId(comment.getEventId()); + commentDto.setText(comment.getText()); + commentDto.setCreatedOn(comment.getCreatedOn()); + commentDto.setEditedOn(comment.getEditedOn()); + commentDto.setAuthor(UserMapper.toUserDto(comment.getAuthor())); + commentDto.setState(comment.getState()); + return commentDto; + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java new file mode 100644 index 0000000..1f5fed2 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java @@ -0,0 +1,34 @@ +package ru.practicum.evmsevice.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.evmsevice.enums.CommentState; + +import java.time.LocalDateTime; + +@Entity +@Setter +@Getter +@Table(name = "comments", schema = "public") +@NoArgsConstructor +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + @ManyToOne + @JoinColumn(name = "author_id", nullable = false) + private User author; + @Column(name = "event_id", nullable = false) + private Integer eventId; + @Column(name = "text", nullable = false) + private String text; + @Column(name = "created_on", nullable = false) + private LocalDateTime createdOn; + @Column(name = "edited_on") + private LocalDateTime editedOn; + @Column(name = "state") + @Enumerated(EnumType.STRING) + private CommentState state; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentRepository.java new file mode 100644 index 0000000..1c95ece --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentRepository.java @@ -0,0 +1,14 @@ +package ru.practicum.evmsevice.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import ru.practicum.evmsevice.model.Comment; + +import java.util.List; + +public interface CommentRepository extends JpaRepository, + JpaSpecificationExecutor { + List findAllByEventId(Integer eventId); + + List findAllByAuthor_Id(Integer authorId); +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentSpecification.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentSpecification.java new file mode 100644 index 0000000..18e55ea --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentSpecification.java @@ -0,0 +1,41 @@ +package ru.practicum.evmsevice.repository; + +import org.springframework.data.jpa.domain.Specification; +import ru.practicum.evmsevice.enums.CommentState; +import ru.practicum.evmsevice.model.Comment; + +import java.time.LocalDateTime; +import java.util.List; + +public class CommentSpecification { + public static Specification commentEventIdEqual(Integer eventId) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.in(root.get("eventId")).value(eventId)); + } + + public static Specification commentStateEqual(CommentState state) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.in(root.get("state")).value(state)); + } + + public static Specification commentContains(String text) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.like(root.get("text"), "%" + text + "%")); + } + + public static Specification commentAuthorIdIn(List userIds) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.in(root.join("author").get("id")).value(userIds)); + } + + public static Specification commentCreatedAfter(LocalDateTime startDateTime) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.greaterThanOrEqualTo(root.get("createdOn"), startDateTime)); + } + + public static Specification commentCreatedBefore(LocalDateTime endDateTime) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.lessThan(root.get("createdOn"), endDateTime)); + } + +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java new file mode 100644 index 0000000..0a0028a --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java @@ -0,0 +1,33 @@ +package ru.practicum.evmsevice.service; + +import ru.practicum.evmsevice.dto.CommentDto; +import ru.practicum.evmsevice.dto.CommentModerationDto; +import ru.practicum.evmsevice.dto.CommentsGroupDto; +import ru.practicum.evmsevice.dto.NewCommentDto; +import ru.practicum.evmsevice.enums.CommentState; + +import java.util.List; + +public interface CommentService { + CommentDto addComment(Integer userId, Integer eventId, NewCommentDto commentDto); + + CommentDto updateComment(Integer userId, Integer commentId, NewCommentDto commentDto); + + CommentsGroupDto moderationComments(Integer userId, Integer eventId, CommentModerationDto commentModerationDto); + + CommentDto getCommentById(Integer commentId); + + List getCommentsByEventId(Integer eventId, + String text, + List authorIds, + String rangeStart, + String rangeEnd, + CommentState state, + String sort, + Integer from, + Integer size); + + List getCommentsByUserId(Integer userId, Integer from, Integer size); + + void deleteComment(Integer userId, Integer commentId); +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java new file mode 100644 index 0000000..1c893a8 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java @@ -0,0 +1,234 @@ +package ru.practicum.evmsevice.service; + +import lombok.RequiredArgsConstructor; +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.evmsevice.dto.CommentDto; +import ru.practicum.evmsevice.dto.CommentModerationDto; +import ru.practicum.evmsevice.dto.CommentsGroupDto; +import ru.practicum.evmsevice.dto.NewCommentDto; +import ru.practicum.evmsevice.enums.CommentState; +import ru.practicum.evmsevice.enums.EventState; +import ru.practicum.evmsevice.exception.BadRequestException; +import ru.practicum.evmsevice.exception.DataConflictException; +import ru.practicum.evmsevice.exception.NotFoundException; +import ru.practicum.evmsevice.exception.ValidationException; +import ru.practicum.evmsevice.mapper.CommentMapper; +import ru.practicum.evmsevice.model.Comment; +import ru.practicum.evmsevice.model.Event; +import ru.practicum.evmsevice.model.User; +import ru.practicum.evmsevice.repository.CommentRepository; +import ru.practicum.evmsevice.repository.CommentSpecification; +import ru.practicum.evmsevice.repository.EventRepository; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class CommentServiceImpl implements CommentService { + private static final DateTimeFormatter DATA_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final Integer COMMENT_PERIOD_HOURS = 72; + private final UserService userService; + private final EventRepository eventRepository; + private final CommentRepository commentRepository; + + /** + * Создаем новый комментарий + * + * @param userId - идентификатор пользователя + * @param eventId - идентификатор события + * @param commentDto - входящий объект комметнария + * @return - сохраненный объект комментария + */ + @Override + @Transactional + public CommentDto addComment(Integer userId, Integer eventId, NewCommentDto commentDto) { + User user = userService.getUserById(userId); + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new NotFoundException("Не найдено событие id=" + eventId)); + if (!event.getState().equals(EventState.PUBLISHED)) { + throw new ValidationException("Комментировать возможно только опубликованные события."); + } + if (event.getEventDate().plusHours(COMMENT_PERIOD_HOURS).isBefore(LocalDateTime.now())) { + throw new ValidationException("Комментировать событие возможно только в течении " + + COMMENT_PERIOD_HOURS + " часов от начала события."); + } + Comment comment = CommentMapper.getComment(commentDto); + comment.setAuthor(user); + comment.setEventId(eventId); + comment.setState(CommentState.PENDING); + comment.setCreatedOn(LocalDateTime.now()); + Comment savedComment = commentRepository.save(comment); + return CommentMapper.toDto(savedComment); + } + + @Override + @Transactional + public CommentDto updateComment(Integer userId, Integer commentId, NewCommentDto commentDto) { + User user = userService.getUserById(userId); + Comment comment = commentRepository.findById(commentId) + .orElseThrow(() -> new NotFoundException("Не найден комментарий id=" + commentId)); + if (!comment.getAuthor().getId().equals(user.getId())) { + throw new ValidationException("Редактировать комментарий может только его автор."); + } + comment.setText(commentDto.getText()); + comment.setEditedOn(LocalDateTime.now()); + Comment savedComment = commentRepository.save(comment); + return CommentMapper.toDto(savedComment); + } + + /** + * Модерация комментариев к событию + * + * @param userId - идентификатор инициатора события + * @param eventId - идентификатор события + * @param commentModerationDto - объект идентификаторов коментариев для модерации + * @return - список комментариев с измененным статусом + */ + @Override + @Transactional + public CommentsGroupDto moderationComments(Integer userId, Integer eventId, CommentModerationDto commentModerationDto) { + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new NotFoundException("Не найдено событие id=" + eventId)); + if (!event.getInitiator().getId().equals(userId)) { + throw new DataConflictException(String.format( + "Польдователь id=%d не является инициатором события id=%d.", + userId, eventId)); + } + List comments = commentRepository.findAllById(commentModerationDto.getCommentIds()); + CommentsGroupDto cgDto = new CommentsGroupDto(); + CommentState commentState = commentModerationDto.getState(); + for (Comment comment : comments) { + if (!comment.getEventId().equals(eventId)) { + throw new DataConflictException(String.format( + "Комментарий id=%d не относится к событию id=%d.", + comment.getId(), eventId)); + } + // меняем состояние только у комментариев ожидающих модерацмм + if (comment.getState().equals(CommentState.PENDING)) { + comment.setState(commentState); + } + Comment savedComment = commentRepository.save(comment); + CommentDto commentDto = CommentMapper.toDto(savedComment); + if (commentDto.getState().equals(CommentState.APPROVED)) { + cgDto.getApprovedComments().add(commentDto); + } + if (commentDto.getState().equals(CommentState.REJECTED)) { + cgDto.getRejectedComments().add(commentDto); + } + } + return cgDto; + } + + @Override + public CommentDto getCommentById(Integer commentId) { + Comment comment = commentRepository.findById(commentId) + .orElseThrow(() -> new NotFoundException("Не найден комментарий id=" + commentId)); + return CommentMapper.toDto(comment); + } + + @Override + public List getCommentsByEventId(Integer eventId, + String text, + List authorIds, + String rangeStart, + String rangeEnd, + CommentState state, + String sort, + Integer from, + Integer size) { + LocalDateTime startDateTime = null; + LocalDateTime endDateTime = null; + + if (rangeStart != null && !rangeStart.isEmpty()) { + try { + startDateTime = LocalDateTime.parse(rangeStart, DATA_TIME_FORMATTER); + } catch (DateTimeParseException e) { + throw new ValidationException("Некорректный формат времени. " + e.getMessage()); + } + } + if (rangeEnd != null && !rangeEnd.isEmpty()) { + try { + endDateTime = LocalDateTime.parse(rangeEnd, DATA_TIME_FORMATTER); + } catch (DateTimeParseException e) { + throw new ValidationException("Некорректный формат времени. " + e.getMessage()); + } + } + if (startDateTime != null && endDateTime != null) { + if (startDateTime.isAfter(endDateTime)) { + throw new BadRequestException( + "Parameter: rangeStart, rangeEnd. " + + "Error: Введен некорректный интервал времени." + + ". Value: " + startDateTime.format(DATA_TIME_FORMATTER) + + ", " + endDateTime.format(DATA_TIME_FORMATTER) + ); + } + } + + Specification spec = Specification.where(null); + // Задаем спецификации для поиска комментариев к событию + spec = spec.and(CommentSpecification.commentEventIdEqual(eventId)); + // ...поиск Комментариев по тексту + if (text != null) { + spec = spec.and(CommentSpecification.commentContains(text)); + } + // ... поиск по списку идентификаторов авторов комментариев + if (authorIds != null) { + spec = spec.and(CommentSpecification.commentAuthorIdIn(authorIds)); + } + // поиск по времени создания комментария + if (startDateTime != null) { + spec = spec.and(CommentSpecification.commentCreatedAfter(startDateTime)); + } + if (endDateTime != null) { + spec = spec.and(CommentSpecification.commentCreatedBefore(endDateTime)); + } + spec = spec.and(CommentSpecification.commentStateEqual(state)); + List comments = new ArrayList<>(); + if (sort != null) { + if (sort.equalsIgnoreCase("OLD")) { + comments = commentRepository.findAll(spec, Sort.by("createdOn")); + } else { + comments = commentRepository.findAll(spec, Sort.by("createdOn").descending()); + } + } else { + comments = commentRepository.findAll(spec, Sort.by("createdOn").descending()); + } + + return comments.stream() + .map(CommentMapper::toDto) + .skip(from) + .limit(size) + .toList(); + } + + @Override + public List getCommentsByUserId(Integer userId, Integer from, Integer size) { + return commentRepository.findAllByAuthor_Id(userId).stream() + .map(CommentMapper::toDto) + .skip(from) + .limit(size) + .toList(); + } + + @Override + @Transactional + public void deleteComment(Integer userId, Integer commentId) { + Comment comment = commentRepository.findById(commentId) + .orElseThrow(() -> new NotFoundException("Не найден комментарий id=" + commentId)); + if (!comment.getAuthor().getId().equals(userId)) { + throw new DataConflictException(String.format( + "Пользователь id=%d не является автором комментария id=%d.", + userId, commentId + )); + } + commentRepository.delete(comment); + } +} diff --git a/ewm-service/src/main/resources/schema.sql b/ewm-service/src/main/resources/schema.sql index 8c871d8..5d03831 100644 --- a/ewm-service/src/main/resources/schema.sql +++ b/ewm-service/src/main/resources/schema.sql @@ -59,7 +59,6 @@ CREATE TABLE IF NOT EXISTS compilations CONSTRAINT UQ_COMPILATION_TITLE UNIQUE (title) ); - CREATE TABLE IF NOT EXISTS eventlinks ( event_id INTEGER NOT NULL, @@ -68,3 +67,16 @@ CREATE TABLE IF NOT EXISTS eventlinks CONSTRAINT fk_links_to_events FOREIGN KEY (event_id) REFERENCES events (id), CONSTRAINT fk_links_to_compilations FOREIGN KEY (compilation_id) REFERENCES compilations (id) ); + +CREATE TABLE IF NOT EXISTS comments +( + id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, + author_id INTEGER NOT NULL, + event_id INTEGER NOT NULL, + text VARCHAR(2000) NOT NULL, + state VARCHAR(20) NOT NULL, + created_on TIMESTAMP WITHOUT TIME ZONE, + edited_on TIMESTAMP WITHOUT TIME ZONE, + CONSTRAINT pk_comment PRIMARY KEY (id), + CONSTRAINT fk_event_for_comment FOREIGN KEY (event_id) REFERENCES events (id) +); \ No newline at end of file diff --git a/Postman/ewm-main-service.json b/postman/ewm-main-service.json similarity index 100% rename from Postman/ewm-main-service.json rename to postman/ewm-main-service.json diff --git a/Postman/ewm-stat-service.json b/postman/ewm-stat-service.json similarity index 100% rename from Postman/ewm-stat-service.json rename to postman/ewm-stat-service.json diff --git a/postman/feature.json b/postman/feature.json new file mode 100644 index 0000000..a1eca19 --- /dev/null +++ b/postman/feature.json @@ -0,0 +1,1523 @@ +{ + "info": { + "_postman_id": "a7bb2177-ee2a-4f24-a50e-e58ca6eefa20", + "name": "Test Explore With Me - Feature comments", + "description": "Коллекция тестов API работы с комменариями событий.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "39468895" + }, + "item": [ + { + "name": "Добавление нового комментария к событию", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор, автор комментария\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " // устанавливаем в пути запроса идентификатор события\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " // создаем объект автора комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set('uid', commentUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " // Генерируем случайный текст комментария\r", + " let comment;\r", + " try {\r", + " comment = rnd.getComment();\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " // формируем тело запроса\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(comment),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код 201 и данные в формате json\", function () {\r", + " pm.response.to.be.success; // код ответа должен быть 'успешно' \r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "http://localhost:8080/users/:userId/events/:eventId/comments", + "host": [ + "http://localhost:8080" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "comments" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + } + }, + "response": [] + }, + { + "name": "Добавление пустого комментария", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор, автор комментария\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " // устанавливаем в пути запроса идентификатор события\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " // создаем объект автора комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set('uid', commentUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " // Генерируем случайный текст комментария\r", + "\r", + " // формируем тело запроса\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({text: \"\"}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; // код ответа должен быть 400\r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "http://localhost:8080/users/:userId/events/:eventId/comments", + "host": [ + "http://localhost:8080" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "comments" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + } + }, + "response": [] + }, + { + "name": "Добавление комментария к неопубликованному событию", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор, автор комментария\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " // устанавливаем в пути запроса идентификатор события\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " // создаем объект автора комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set('uid', commentUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " // Генерируем случайный текст комментария\r", + " let comment;\r", + " try {\r", + " comment = rnd.getComment();\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " // формируем тело запроса\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(comment),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код 403 и данные в формате json\", function () {\r", + " pm.response.to.be.forbidden; // код ответа должен быть 403' \r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "http://localhost:8080/users/:userId/events/:eventId/comments", + "host": [ + "http://localhost:8080" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "comments" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + } + }, + "response": [] + }, + { + "name": "Добавление комментария к несуществующему событию", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор, автор комментария\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " // устанавливаем в пути запроса идентификатор события\r", + " pm.collectionVariables.set(\"eid\", 10000)\r", + " // создаем объект автора комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set('uid', commentUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " // Генерируем случайный текст комментария\r", + " let comment;\r", + " try {\r", + " comment = rnd.getComment();\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " // формируем тело запроса\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(comment),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код 404 и данные в формате json\", function () {\r", + " pm.response.to.be.notFound; // код ответа должен быть \r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "http://localhost:8080/users/:userId/events/:eventId/comments", + "host": [ + "http://localhost:8080" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "comments" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + } + }, + "response": [] + }, + { + "name": "Исправление комментария", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор, автор комментария\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " // создаем объект автора комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set('uid', commentUser.id);\r", + " // создаем новый коментарий\r", + " comment = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", + " pm.collectionVariables.set('cmid', comment.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " // формируем тело запроса\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({text: \"Исправленный текст комментария.\"}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код 200 и данные в формате json\", function () {\r", + " pm.response.to.be.success; // код ответа должен быть 200\r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "http://localhost:8080/users/:userId/comments/:commentId", + "host": [ + "http://localhost:8080" + ], + "path": [ + "users", + ":userId", + "comments", + ":commentId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "commentId", + "value": "{{cmid}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Модерация комментария", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор события, автор комментария\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " // устанавливаем идентификаторы в пути запроса\r", + " pm.collectionVariables.set('uid', user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " // создаем объект автора комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " // создаем новый коментарий\r", + " comment = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", + " // формируем тело запроса\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({commentIds: [comment.id],\r", + " state: \"APPROVED\"}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; // код ответа должен быть 200 \r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "http://localhost:8080/users/:userId/events/:eventId/comments", + "host": [ + "http://localhost:8080" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "comments" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + } + }, + "response": [] + }, + { + "name": "Поиск комментариев пользователя", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор события, автор комментария, комментарии\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " // создаем объект автора комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " // устанавливаем идентификатор автора в пути запроса\r", + " pm.collectionVariables.set('uid', commentUser.id);\r", + " // создаем новые коментарии\r", + " comment1 = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", + " comment2 = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", + " // одобряем добавленные комментарии\r", + " modComment = await api.approvedComment(user.id, event.id, comment1.id);\r", + " modComment = await api.approvedComment(user.id, event.id, comment2.id); \r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "const body = pm.response.json();\r", + "pm.test(\"Тело ответа должно быть массивом и колличество элементов больше 1\", function () {\r", + " pm.response.to.be.ok; // код ответа должен быть 200 \r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + " pm.expect(body).is.an('array'); // проверяем, что тело ответа является массивом \r", + " pm.expect(body.length).to.be.gte(1); // длина массива должна быть больше 1\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://localhost:8080/users/:userId/comments", + "host": [ + "http://localhost:8080" + ], + "path": [ + "users", + ":userId", + "comments" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + } + ] + } + }, + "response": [] + }, + { + "name": "Поиск комментариев к заданному событию", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор события, автор комментария, комментарии\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " // устанавливаем идентификатор события в пути запроса\r", + " pm.collectionVariables.set('eid', event.id);\r", + " comment1 = await api.addComment(user.id, event.id, rnd.getComment());\r", + " modComment = await api.approvedComment(user.id, event.id, comment1.id);\r", + " // создаем объект автора Дополнительного комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " // устанавливаем идентификатор автора в пути запроса\r", + " pm.collectionVariables.set('uid', commentUser.id);\r", + " comment2 = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", + " modComment = await api.approvedComment(user.id, event.id, comment2.id); \r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "const body = pm.response.json();\r", + "pm.test(\"Тело ответа должно быть массивом и колличество элементов больше 1\", function () {\r", + " pm.response.to.be.ok; // код ответа должен быть 200 \r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + " pm.expect(body).is.an('array'); // проверяем, что тело ответа является массивом \r", + " pm.expect(body.length).to.be.gte(1); // длина массива должна быть больше 1\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://localhost:8080/events/:eventId/comments", + "host": [ + "http://localhost:8080" + ], + "path": [ + "events", + ":eventId", + "comments" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Поиск комментария по идентификатору", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор события, автор комментария, комментарии\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " comment = await api.addComment(user.id, event.id, rnd.getComment());\r", + " modComment = await api.approvedComment(user.id, event.id, comment.id);\r", + " // устанавливаем идентификатор события в пути запроса\r", + " pm.collectionVariables.set('cmid', comment.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "const body = pm.response.json();\r", + "pm.test(\"Тело ответа должно быть в формате json\", function () {\r", + " pm.response.to.be.ok; // код ответа должен быть 200 \r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Комментарий должны содержать поля: id, author, eventId, text, state\", function () {\r", + " pm.expect(target).to.have.property('id');\r", + " pm.expect(target).to.have.property('author');\r", + " pm.expect(target).to.have.property('eventId');\r", + " pm.expect(target).to.have.property('text');\r", + " pm.expect(target).to.have.property('state');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).equal(pm.collectionVariables.get(\"cmid\"));\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://localhost:8080/comments/:commentId", + "host": [ + "http://localhost:8080" + ], + "path": [ + "comments", + ":commentId" + ], + "variable": [ + { + "key": "commentId", + "value": "{{cmid}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Поиск комментария к событию по фильтру", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор события, автор комментария, комментарии\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " // устанавливаем идентификатор события в пути запроса\r", + " pm.collectionVariables.set('eid', event.id);\r", + " comment1 = await api.addComment(user.id, event.id, rnd.getComment());\r", + " modComment = await api.approvedComment(user.id, event.id, comment1.id);\r", + " // создаем объект автора Дополнительного комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " comment2 = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", + " modComment = await api.approvedComment(user.id, event.id, comment2.id); \r", + " pm.request.removeQueryParams(['text', 'authorIds', 'state']);\r", + " pm.request.addQueryParams([`text=` + comment1.text, 'authorIds=' + comment1.author.id]);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "const body = pm.response.json();\r", + "pm.test(\"Тело ответа должно быть в формате json\", function () {\r", + " pm.response.to.be.ok; // код ответа должен быть 200 \r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Комментарий должны содержать поля: id, author, eventId, text, state\", function () {\r", + " pm.expect(target[0]).to.have.property('id');\r", + " pm.expect(target[0]).to.have.property('author');\r", + " pm.expect(target[0]).to.have.property('eventId');\r", + " pm.expect(target[0]).to.have.property('text');\r", + " pm.expect(target[0]).to.have.property('state');\r", + "});\r", + "\r", + "pm.test(\"Должен быть найден только один комментарий по заданному фильтру\", function () {\r", + " pm.expect(target.length).to.eql(1);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://localhost:8080/events/:eventId/comments", + "host": [ + "http://localhost:8080" + ], + "path": [ + "events", + ":eventId", + "comments" + ], + "variable": [ + { + "key": "eventId", + "value": "{{eid}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Удаление комментария", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " // создаем объект автора комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set('uid', commentUser.id);\r", + " // создаем новый коментарий\r", + " const comment = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", + " pm.collectionVariables.set(\"cmid\", comment.id)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 204\", function () {\r", + " pm.response.to.have.status(204);\r", + "});\r", + "\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://localhost:8080/users/:userId/comments/:commentId", + "host": [ + "http://localhost:8080" + ], + "path": [ + "users", + ":userId", + "comments", + ":commentId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}" + }, + { + "key": "commentId", + "value": "{{cmid}}" + } + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "API = class {\r", + " constructor(postman, verbose = false, baseUrl = \"http://localhost:8080\") {\r", + " this.baseUrl = baseUrl;\r", + " this.pm = postman;\r", + " this._verbose = verbose;\r", + " }\r", + "\r", + " async addUser(user, verbose=null) {\r", + " return this.post(\"/admin/users\", user, \"Ошибка при добавлении нового пользователя: \", verbose);\r", + " }\r", + "\r", + " async addCategory(category, verbose=null) {\r", + " return this.post(\"/admin/categories\", category, \"Ошибка при добавлении новой категории: \", verbose);\r", + " }\r", + "\r", + " async addEvent(userId, event, verbose=null) {\r", + " return this.post(\"/users/\" + userId + \"/events\", event, \"Ошибка при добавлении нового события: \", verbose);\r", + " }\r", + "\r", + " async addComment(userId, eventId, comment, verbose=null) {\r", + " return this.post(\"/users/\" + userId + \"/events/\" + eventId + \"/comments\", comment, \"Ошибка при добавлении нового комментария: \", verbose);\r", + " }\r", + "\r", + " async addCompilation(compilation, verbose=null) {\r", + " return this.post(\"/admin/compilations\", compilation, \"Ошибка при добавлении новой подборки: \", verbose);\r", + " }\r", + "\r", + " async publishParticipationRequest(eventId, userId, verbose=null) {\r", + " return this.post('/users/' + userId + '/requests?eventId=' + eventId, null, \"Ошибка при добавлении нового запроса на участие в событии\", verbose);\r", + " }\r", + "\r", + " async publishEvent(eventId, verbose=null) {\r", + " return this.patch('/admin/events/' + eventId, {stateAction: \"PUBLISH_EVENT\"}, \"Ошибка при публикации события\", verbose);\r", + " }\r", + "\r", + " async approvedComment(userId, eventId, commentId, verbose=null) {\r", + " return this.patch(\"/users/\" + userId + \"/events/\" + eventId + \"/comments\", {commentIds: [commentId], state: \"APPROVED\"}, \"Ошибка при модерации комментария.\", verbose);\r", + " }\r", + " \r", + " async rejectEvent(eventId, verbose=null) {\r", + " return this.patch('/admin/events/' + eventId, {stateAction: \"REJECT_EVENT\"}, \"Ошибка при отмене события\", verbose);\r", + " }\r", + "\r", + " async acceptParticipationRequest(eventId, userId, reqId, verbose=null) {\r", + " return this.patch('/users/' + userId + '/events/' + eventId + '/requests', {requestIds:[reqId], status: \"CONFIRMED\"}, \"Ошибка при принятии заявки на участие в событии\", verbose);\r", + " }\r", + "\r", + " async findCategory(catId, verbose=null) {\r", + " return this.get('/categories/' + catId, null, \"Ошибка при поиске категории по id\", verbose);\r", + " }\r", + "\r", + " async findComment(commentId, verbose=null) {\r", + " return this.get('/comments/' + commentId, null, \"Ошибка при поиске комментария по id\", verbose);\r", + " }\r", + "\r", + " async findCompilation(compId, verbose=null) {\r", + " return this.get('/compilations/' + compId, null, \"Ошибка при поиске подборки по id\", verbose);\r", + " }\r", + "\r", + " async findEvent(eventId, verbose=null) {\r", + " return this.get('/events/' + eventId, null, \"Ошибка при поиске события по id\", verbose);\r", + " }\r", + "\r", + " async findUser(userId, verbose=null) {\r", + " return this.get('/admin/users?ids=' + userId, null, \"Ошибка при поиске пользователя по id\", verbose);\r", + " }\r", + "\r", + " async post(path, body, errorText = \"Ошибка при выполнении post-запроса: \", verbose=null) {\r", + " return this.sendRequest(\"POST\", path, body, errorText, verbose);\r", + " }\r", + "\r", + " async patch(path, body = null, errorText = \"Ошибка при выполнении patch-запроса: \", verbose=null) {\r", + " return this.sendRequest(\"PATCH\", path, body, errorText, verbose);\r", + " }\r", + "\r", + " async get(path, body = null, errorText = \"Ошибка при выполнении get-запроса: \", verbose=null) {\r", + " return this.sendRequest(\"GET\", path, body, errorText, verbose);\r", + " }\r", + " async sendRequest(method, path, body=null, errorText = \"Ошибка при выполнении запроса: \", verbose=null) {\r", + " return new Promise((resolve, reject) => {\r", + " verbose = verbose == null ? this._verbose : verbose;\r", + " const request = {\r", + " url: this.baseUrl + path,\r", + " method: method,\r", + " body: body == null ? \"\" : JSON.stringify(body),\r", + " header: { \"Content-Type\": \"application/json\" },\r", + " };\r", + " if(verbose) {\r", + " console.log(\"Отправляю запрос: \", request);\r", + " }\r", + "\r", + " try {\r", + " this.pm.sendRequest(request, (error, response) => {\r", + " if(error || (response.code >= 400 && response.code <= 599)) {\r", + " let err = error ? error : JSON.stringify(response.json());\r", + " console.error(\"При выполнении запроса к серверу возникла ошика.\\n\", err,\r", + " \"\\nДля отладки проблемы повторите такой же запрос к вашей программе \" + \r", + " \"на локальном компьютере. Данные запроса:\\n\", JSON.stringify(request));\r", + "\r", + " reject(new Error(errorText + err));\r", + " }\r", + " if(verbose) {\r", + " console.log(\"Результат обработки запроса: код состояния - \", response.code, \", тело: \", response.json());\r", + " }\r", + " if (response.stream.length === 0){\r", + " reject(new Error('Отправлено пустое тело ответа'))\r", + " }else{\r", + " resolve(response.json());\r", + " }\r", + " });\r", + " \r", + " } catch(err) {\r", + " if(verbose) {\r", + " console.error(errorText, err);\r", + " }\r", + " return Promise.reject(err);\r", + " }\r", + " });\r", + " }\r", + "};\r", + "\r", + "RandomUtils = class {\r", + " constructor() {}\r", + "\r", + " getUser() {\r", + " return {\r", + " name: pm.variables.replaceIn('{{$randomFullName}}'),\r", + " email: pm.variables.replaceIn('{{$randomEmail}}')\r", + " };\r", + " }\r", + "\r", + " getCategory() {\r", + " return {\r", + " name: pm.variables.replaceIn('{{$randomWord}}') + Math.floor(Math.random() * 10000 * Math.random()).toString()\r", + " };\r", + " }\r", + "\r", + " getComment() {\r", + " return {\r", + " text: pm.variables.replaceIn('{{$randomWord}}') + Math.floor(Math.random() * 10000 * Math.random()).toString()\r", + " };\r", + " }\r", + "\r", + " getEvent(categoryId) {\r", + " return {\r", + " annotation: pm.variables.replaceIn('{{$randomLoremParagraph}}'),\r", + " category: categoryId,\r", + " description: pm.variables.replaceIn('{{$randomLoremParagraphs}}'),\r", + " eventDate: this.getFutureDateTime(),\r", + " location: {\r", + " lat: parseFloat(pm.variables.replaceIn('{{$randomLatitude}}')),\r", + " lon: parseFloat(pm.variables.replaceIn('{{$randomLongitude}}')),\r", + " },\r", + " paid: pm.variables.replaceIn('{{$randomBoolean}}'),\r", + " participantLimit: pm.variables.replaceIn('{{$randomInt}}'),\r", + " requestModeration: pm.variables.replaceIn('{{$randomBoolean}}'),\r", + " title: pm.variables.replaceIn('{{$randomLoremSentence}}'),\r", + " }\r", + " }\r", + "\r", + " getCompilation(...eventIds) {\r", + " return {\r", + " title: pm.variables.replaceIn('{{$randomLoremSentence}}').slice(0, 50),\r", + " pinned: pm.variables.replaceIn('{{$randomBoolean}}'),\r", + " events: eventIds\r", + " };\r", + " }\r", + "\r", + "\r", + " getFutureDateTime(hourShift = 5, minuteShift=0, yearShift=0) {\r", + " let moment = require('moment');\r", + "\r", + " let m = moment();\r", + " m.add(hourShift, 'hour');\r", + " m.add(minuteShift, 'minute');\r", + " m.add(yearShift, 'year');\r", + "\r", + " return m.format('YYYY-MM-DD HH:mm:ss');\r", + " }\r", + "\r", + " getWord(length = 1) {\r", + " let result = '';\r", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\r", + " const charactersLength = characters.length;\r", + " let counter = 0;\r", + " while (counter < length) {\r", + " result += characters.charAt(Math.floor(Math.random() * charactersLength));\r", + " counter += 1;\r", + " }\r", + " return result;\r", + " }\r", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "id", + "value": "1" + }, + { + "key": "baseUrl", + "value": "", + "type": "default" + }, + { + "key": "uid", + "value": "" + }, + { + "key": "eid", + "value": "" + }, + { + "key": "cmid", + "value": "" + } + ] +} \ No newline at end of file