From 81d9e03777d1e155fafdd676f5e63223038c2467 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Sun, 5 Apr 2026 21:30:59 +0300 Subject: [PATCH 01/28] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=20?= =?UTF-8?q?=D0=B2=D0=B5=D1=89=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lombok.config | 23 +- pom.xml | 522 ++++++++++-------- .../shareit/booking/BookingController.java | 2 +- .../shareit/booking/dto/BookingMapper.java | 2 +- .../booking/dto/IncomingBookingDto.java | 4 +- .../booking/service/BookingServiceImpl.java | 29 +- .../shareit/item/ItemController.java | 15 +- .../shareit/item/dto/CommentDto.java | 4 +- .../shareit/item/dto/CommentMapper.java | 5 +- ...temDtoFullVersion.java => ItemBigDto.java} | 2 +- .../practicum/shareit/item/dto/ItemDto.java | 4 +- .../shareit/item/dto/ItemLowDto.java | 7 + .../shareit/item/dto/ItemMapper.java | 19 +- .../practicum/shareit/item/model/Comment.java | 2 +- .../shareit/item/service/ItemService.java | 6 +- .../shareit/item/service/ItemServiceImpl.java | 77 ++- .../request/ItemRequestController.java | 36 +- .../request/dto/ItemRequestBigDto.java | 33 ++ .../shareit/request/dto/ItemRequestDto.java | 24 + .../request/dto/ItemRequestMapper.java | 36 ++ .../shareit/request/model/ItemRequest.java | 4 +- .../repository/ItemRequestRepository.java | 4 + .../request/service/ItemRequestService.java | 20 + .../service/ItemRequestServiceImpl.java | 85 +++ .../shareit/user/UserController.java | 2 +- .../practicum/shareit/user/dto/UserDto.java | 2 +- .../shareit/user/service/UserServiceImpl.java | 12 +- .../practicum/shareit/util/UtilService.java | 48 ++ .../util/config/ShateItConfiguration.java | 38 ++ .../shareit/util/error/ErrorHandler.java | 6 +- .../util/stub/MethodNotImplemented.java | 8 - ...MethodNotImplementedBeanPostProcessor.java | 54 -- .../shareit/util/{ => validate}/Marker.java | 2 +- src/main/resources/application.properties | 3 + 34 files changed, 723 insertions(+), 417 deletions(-) rename src/main/java/ru/practicum/shareit/item/dto/{ItemDtoFullVersion.java => ItemBigDto.java} (94%) create mode 100644 src/main/java/ru/practicum/shareit/item/dto/ItemLowDto.java create mode 100644 src/main/java/ru/practicum/shareit/request/dto/ItemRequestBigDto.java create mode 100644 src/main/java/ru/practicum/shareit/util/UtilService.java create mode 100644 src/main/java/ru/practicum/shareit/util/config/ShateItConfiguration.java delete mode 100644 src/main/java/ru/practicum/shareit/util/stub/MethodNotImplemented.java delete mode 100644 src/main/java/ru/practicum/shareit/util/stub/MethodNotImplementedBeanPostProcessor.java rename src/main/java/ru/practicum/shareit/util/{ => validate}/Marker.java (55%) diff --git a/lombok.config b/lombok.config index fa7bee0..5f96b2e 100644 --- a/lombok.config +++ b/lombok.config @@ -1,10 +1,29 @@ +# Не искать настройки за пределами проекта config.stopBubbling = true -lombok.anyconstructor.addconstructorproperties = false + +# Хранить имена параметров конструктора +lombok.anyconstructor.addconstructorproperties = true + +# Добавить @Generated над сгенерированнвм классом lombok.addLombokGeneratedAnnotation = true + +# Добавлять @SuppressWarnings в методы lombok.addSuppressWarnings = false + +# Все поля по умолчанию приватные lombok.fieldDefaults.defaultPrivate = true + +# Экуалс и хэшкод учитывают родителя lombok.equalsAndHashCode.callSuper = call + +# Экуалс и хэшкод не используют геттеры lombok.equalsAndHashCode.doNotUseGetters = true + +# Тустринг содержит родителя с его полями lombok.toString.callSuper = call + +# Тустринг не использует геттеры lombok.toString.doNotUseGetters = true -lombok.noArgsConstructor.extraPrivate = true + +# Добавлять дополнительный приватный конструктор без параметров +#lombok.noArgsConstructor.extraPrivate = true diff --git a/pom.xml b/pom.xml index e05de59..0218e57 100644 --- a/pom.xml +++ b/pom.xml @@ -1,254 +1,286 @@ - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.3.2 - - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + - ru.practicum - shareit - 0.0.1-SNAPSHOT + ru.practicum + shareit + 0.0.1-SNAPSHOT - ShareIt + ShareIt - - 21 - + + 21 + 5.1.0 + - - - org.projectlombok - lombok - true - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-configuration-processor - true - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.boot - spring-boot-starter-validation - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.postgresql - postgresql - runtime - - - com.h2database - h2 - test - - - org.zalando - logbook-spring-boot-starter - 3.9.0 - - + + + org.projectlombok + lombok + provided + true + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.querydsl + querydsl-jpa + jakarta + ${querydsl.version} + + + org.postgresql + postgresql + runtime + + + com.h2database + h2 + test + + + org.zalando + logbook-spring-boot-starter + 3.9.0 + + - - - - src/main/resources - true - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - test - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.1.2 - - checkstyle.xml - true - true - true - - - - - check - - compile - - - - - com.puppycrawl.tools - checkstyle - 11.1.0 - compile - - - - - com.github.spotbugs - spotbugs-maven-plugin - 4.8.5.0 - - Max - High - - - - - check - - - - - - org.jacoco - jacoco-maven-plugin - 0.8.12 - - file - - - - jacoco-initialize - - prepare-agent - - - - jacoco-check - - check - - - - - BUNDLE - - - INSTRUCTION - COVEREDRATIO - 0.01 - - - LINE - COVEREDRATIO - 0.9 - - - BRANCH - COVEREDRATIO - 0.6 - - - COMPLEXITY - COVEREDRATIO - 0.6 - - - METHOD - COVEREDRATIO - 0.7 - - - CLASS - MISSEDCOUNT - 1 - - - - - - - - jacoco-report - test - - report - - - - - - - - - - check - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - com.github.spotbugs - spotbugs-maven-plugin - - - - - - - com.github.spotbugs - spotbugs-maven-plugin - - - - - - coverage - - - - org.jacoco - jacoco-maven-plugin - - - - - + + + + src/main/resources + true + + + + + com.mysema.maven + apt-maven-plugin + 1.1.3 + + + + process + + + target/generated-sources/java + com.querydsl.apt.jpa.JPAAnnotationProcessor + + + + + + com.querydsl + querydsl-apt + jakarta + ${querydsl.version} + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + test + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.1.2 + + checkstyle.xml + true + true + true + + + + + check + + compile + + + + + com.puppycrawl.tools + checkstyle + 11.1.0 + compile + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.8.5.0 + + Max + High + + + + + check + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + file + + + + jacoco-initialize + + prepare-agent + + + + jacoco-check + + check + + + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + 0.01 + + + LINE + COVEREDRATIO + 0.9 + + + BRANCH + COVEREDRATIO + 0.6 + + + COMPLEXITY + COVEREDRATIO + 0.6 + + + METHOD + COVEREDRATIO + 0.7 + + + CLASS + MISSEDCOUNT + 1 + + + + + + + + jacoco-report + test + + report + + + + + + + + + + check + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + com.github.spotbugs + spotbugs-maven-plugin + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + + + + + + coverage + + + + org.jacoco + jacoco-maven-plugin + + + + + diff --git a/src/main/java/ru/practicum/shareit/booking/BookingController.java b/src/main/java/ru/practicum/shareit/booking/BookingController.java index 84bbaa7..dfa97a2 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -7,7 +7,7 @@ import ru.practicum.shareit.booking.dto.BookingDto; import ru.practicum.shareit.booking.dto.IncomingBookingDto; import ru.practicum.shareit.booking.service.BookingService; -import ru.practicum.shareit.util.Marker; +import ru.practicum.shareit.util.validate.Marker; import java.util.List; diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java b/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java index 9520b4a..67dea89 100644 --- a/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java @@ -18,7 +18,7 @@ public static BookingDto toBookingDto(@NonNull Booking booking) { booking.getId(), booking.getStart().atZone(ZoneId.systemDefault()).toLocalDateTime(), booking.getEnd().atZone(ZoneId.systemDefault()).toLocalDateTime(), - ItemMapper.toItemDto(booking.getItem()), + ItemMapper.toDto(booking.getItem()), UserMapper.toUserDto(booking.getBooker()), booking.getStatus() ); diff --git a/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java b/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java index 4c95ab3..c4ab40e 100644 --- a/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java +++ b/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java @@ -2,12 +2,14 @@ import jakarta.validation.constraints.*; import lombok.Data; -import ru.practicum.shareit.util.Marker; +import lombok.NoArgsConstructor; +import ru.practicum.shareit.util.validate.Marker; import java.time.LocalDateTime; @Data @NotNull +@NoArgsConstructor public class IncomingBookingDto { @Positive(message = "id вещи - обязательный параметр, должен быть положительным числом") diff --git a/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java b/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java index 2cbfcbf..eaa2a7b 100644 --- a/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java @@ -13,9 +13,8 @@ import ru.practicum.shareit.booking.model.StateOfBooking; import ru.practicum.shareit.booking.repository.BookingRepository; import ru.practicum.shareit.item.model.Item; -import ru.practicum.shareit.item.repository.ItemRepository; import ru.practicum.shareit.user.model.User; -import ru.practicum.shareit.user.repository.UserRepository; +import ru.practicum.shareit.util.UtilService; import ru.practicum.shareit.util.exception.BookingStatusException; import ru.practicum.shareit.util.exception.ForbiddenException; import ru.practicum.shareit.util.exception.NotFoundException; @@ -30,8 +29,7 @@ public class BookingServiceImpl implements BookingService { BookingRepository bookingRepository; - ItemRepository itemRepository; - UserRepository userRepository; + UtilService utilService; @Override public BookingDto addNewBooking(Long userId, @NonNull IncomingBookingDto bookingDto) { @@ -41,14 +39,9 @@ public BookingDto addNewBooking(Long userId, @NonNull IncomingBookingDto booking throw new ValidationException("Дата начала бронирования должна быть раньше даты окончания"); } - User booker = userRepository.findById(userId).orElseThrow( - () -> new ForbiddenException("Пользователь с id=" + userId + " не найден") - ); - + User booker = utilService.getUser(userId); long itemId = bookingDto.getItemId(); - Item item = itemRepository.findById(itemId).orElseThrow( - () -> new NotFoundException("Вещь с id=" + itemId + " не найдена") - ); + Item item = utilService.getItem(itemId); if (item.getOwner().getId().equals(userId)) { throw new ValidationException("Нельзя забронировать свою-же вещь"); } @@ -63,7 +56,7 @@ public BookingDto addNewBooking(Long userId, @NonNull IncomingBookingDto booking @Override public BookingDto approveBooking(long userId, long bookingId, boolean approved) { - checkUser(userId); + utilService.checkUser(userId); Booking booking = getBooking(bookingId); if (userId != booking.getItem().getOwner().getId()) { throw new ForbiddenException("Только владелец может менять статус вещи"); @@ -90,7 +83,7 @@ public BookingDto approveBooking(long userId, long bookingId, boolean approved) @Override public BookingDto getBooking(Long userId, long bookingId) { - checkUser(userId); + utilService.checkUser(userId); Booking booking = getBooking(bookingId); if (!booking.getBooker().getId().equals(userId) && !booking.getItem().getOwner().getId().equals(userId)) { throw new ForbiddenException("Может быть выполнено либо автором бронирования, либо владельцем вещи"); @@ -100,7 +93,7 @@ public BookingDto getBooking(Long userId, long bookingId) { @Override public List getAllBookingsByBooker(long userId, String state) { - checkUser(userId); + utilService.checkUser(userId); StateOfBooking stateOfBooking = StateOfBooking.of(state); switch (stateOfBooking) { case ALL -> { @@ -151,7 +144,7 @@ public List getAllBookingsByBooker(long userId, String state) { @Override public List getAllBookingsByOwner(long userId, String state) { - checkUser(userId); + utilService.checkUser(userId); StateOfBooking stateOfBooking = StateOfBooking.of(state); switch (stateOfBooking) { case ALL -> { @@ -200,12 +193,6 @@ public List getAllBookingsByOwner(long userId, String state) { } } - private void checkUser(Long userId) { - if (!userRepository.existsById(userId)) { - throw new ForbiddenException("Пользователь с id=" + userId + " не найден."); - } - } - @NonNull private Booking getBooking(Long bookingId) { return bookingRepository.findById(bookingId).orElseThrow( diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/src/main/java/ru/practicum/shareit/item/ItemController.java index be70df4..93c10ba 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -6,9 +6,9 @@ import org.springframework.web.bind.annotation.*; import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; -import ru.practicum.shareit.item.dto.ItemDtoFullVersion; +import ru.practicum.shareit.item.dto.ItemBigDto; import ru.practicum.shareit.item.service.ItemService; -import ru.practicum.shareit.util.Marker; +import ru.practicum.shareit.util.validate.Marker; import java.util.List; @@ -24,7 +24,7 @@ public class ItemController { final ItemService itemService; @GetMapping("/{itemId}") - public ItemDtoFullVersion getItem( + public ItemBigDto getItem( @RequestHeader("X-Sharer-User-Id") long userId, @PathVariable long itemId ) { @@ -32,7 +32,7 @@ public ItemDtoFullVersion getItem( } @GetMapping - public List getAllItemsOfUser(@RequestHeader("X-Sharer-User-Id") long userId) { + public List getAllItemsOfUser(@RequestHeader("X-Sharer-User-Id") long userId) { return itemService.getAllItemsOfUser(userId); } @@ -44,6 +44,13 @@ public List search( return itemService.search(userId, text); } + /*Реализуйте ещё одну полезную опцию в вашем приложении, + чтобы пользователи смогли отвечать на запросы друг друга. + Для этого при создании вещи должна быть возможность указать + id запроса, в ответ на который создаётся нужная вещь. + Добавьте поле requestId в тело запроса POST /items. + Обратите внимание: должна сохраниться возможность добавить + вещь и без указания requestId*/ @PostMapping @Validated(Marker.OnCreate.class) public ItemDto addNewItem( diff --git a/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java b/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java index f52f008..09646eb 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java +++ b/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java @@ -1,8 +1,8 @@ package ru.practicum.shareit.item.dto; import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.Builder; import lombok.Data; @@ -22,6 +22,6 @@ public class CommentDto { @JsonProperty("created") LocalDateTime createdDate; - @NotBlank + @Size(max = 500) String text; } diff --git a/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java b/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java index a6650e0..b6af998 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java +++ b/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java @@ -24,7 +24,8 @@ public static Comment toComment(@NonNull CommentDto dto, @NonNull Item item, @N .setId(dto.getId()) .setItem(item) .setAuthor(author) - .setCreatedDate(dto.getCreatedDate() == null ? LocalDateTime.now() : dto.getCreatedDate()) - .setText(dto.getText() == null ? null : dto.getText().trim()); + .setCreatedDate(dto.getCreatedDate() == null ? + LocalDateTime.now() : dto.getCreatedDate()) + .setText(dto.getText()); } } diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemDtoFullVersion.java b/src/main/java/ru/practicum/shareit/item/dto/ItemBigDto.java similarity index 94% rename from src/main/java/ru/practicum/shareit/item/dto/ItemDtoFullVersion.java rename to src/main/java/ru/practicum/shareit/item/dto/ItemBigDto.java index caba5a3..41055e2 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDtoFullVersion.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemBigDto.java @@ -15,7 +15,7 @@ @NotNull @Builder @FieldDefaults(makeFinal = true) -public class ItemDtoFullVersion { +public class ItemBigDto { Long id; String name; diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java index 7db7208..a272c75 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -1,11 +1,10 @@ package ru.practicum.shareit.item.dto; -import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.*; import lombok.Builder; import lombok.Data; import lombok.experimental.FieldDefaults; -import ru.practicum.shareit.util.Marker; +import ru.practicum.shareit.util.validate.Marker; /** * TODO Sprint add-controllers. @@ -29,6 +28,5 @@ public class ItemDto { @NotNull(groups = Marker.OnCreate.class) Boolean available; - @JsonProperty("request") Long requestId; } diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemLowDto.java b/src/main/java/ru/practicum/shareit/item/dto/ItemLowDto.java new file mode 100644 index 0000000..73b8d9d --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemLowDto.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.item.dto; + +public record ItemLowDto( + Long id, + String name, + Long ownerId +) {} diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java b/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java index cfb2ee2..88b6af8 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java @@ -2,6 +2,7 @@ import org.springframework.lang.NonNull; import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.request.model.ItemRequest; import ru.practicum.shareit.user.model.User; import java.time.LocalDateTime; @@ -10,7 +11,7 @@ public class ItemMapper { @NonNull - public static ItemDto toItemDto(@NonNull Item item) { + public static ItemDto toDto(@NonNull Item item) { return ItemDto.builder() .id(item.getId()) .name(item.getName()) @@ -20,13 +21,13 @@ public static ItemDto toItemDto(@NonNull Item item) { .build(); } - public static ItemDtoFullVersion toFullItemDto( + public static ItemBigDto toBigDto( @NonNull Item item, LocalDateTime lastBookingDate, LocalDateTime nextBookingDate, List comments ) { - return ItemDtoFullVersion.builder() + return ItemBigDto.builder() .id(item.getId()) .name(item.getName()) .description(item.getDescription()) @@ -38,14 +39,22 @@ public static ItemDtoFullVersion toFullItemDto( .build(); } + public static ItemLowDto toLowDto(Item item) { + return new ItemLowDto( + item.getId(), + item.getName(), + item.getOwner().getId() + ); + } + @NonNull - public static Item toItem(@NonNull ItemDto itemDto, User owner) { + public static Item toEntity(@NonNull ItemDto itemDto, User owner, ItemRequest request) { return new Item() .setId(itemDto.getId()) .setName(itemDto.getName()) .setDescription(itemDto.getDescription()) .setAvailable(itemDto.getAvailable()) .setOwner(owner) - .setRequest(null); + .setRequest(request); } } diff --git a/src/main/java/ru/practicum/shareit/item/model/Comment.java b/src/main/java/ru/practicum/shareit/item/model/Comment.java index 6c965e9..19177d7 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Comment.java +++ b/src/main/java/ru/practicum/shareit/item/model/Comment.java @@ -34,7 +34,7 @@ public class Comment { LocalDateTime createdDate; - @Column(name = "comment_text") + @Column(name = "comment_text", length = 500) String text; //region equals and hashCode diff --git a/src/main/java/ru/practicum/shareit/item/service/ItemService.java b/src/main/java/ru/practicum/shareit/item/service/ItemService.java index 81e1582..fb6396b 100644 --- a/src/main/java/ru/practicum/shareit/item/service/ItemService.java +++ b/src/main/java/ru/practicum/shareit/item/service/ItemService.java @@ -4,7 +4,7 @@ import org.springframework.transaction.annotation.Transactional; import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; -import ru.practicum.shareit.item.dto.ItemDtoFullVersion; +import ru.practicum.shareit.item.dto.ItemBigDto; import java.util.List; @@ -12,10 +12,10 @@ public interface ItemService { /// Получить вещь по id - ItemDtoFullVersion getItem(long userId, long itemId); + ItemBigDto getItem(long userId, long itemId); /// Получить все вещи - List getAllItemsOfUser(long userId); + List getAllItemsOfUser(long userId); /// Поиск вещей по подстроке List search(long userId, String search); diff --git a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java index c91badc..a4a8063 100644 --- a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java @@ -11,8 +11,9 @@ import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.item.repository.CommentRepository; import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.request.model.ItemRequest; import ru.practicum.shareit.user.model.User; -import ru.practicum.shareit.user.repository.UserRepository; +import ru.practicum.shareit.util.UtilService; import ru.practicum.shareit.util.exception.ForbiddenException; import ru.practicum.shareit.util.exception.NotFoundException; import ru.practicum.shareit.util.exception.ParameterNotValidException; @@ -31,23 +32,23 @@ public class ItemServiceImpl implements ItemService { ItemRepository itemRepository; - UserRepository userRepository; BookingRepository bookingRepository; CommentRepository commentRepository; + UtilService utilService; @Override - public ItemDtoFullVersion getItem(long userId, long itemId) { - Item item = getItemWithCheckPresent(itemId); + public ItemBigDto getItem(long userId, long itemId) { + Item item = utilService.getItem(itemId); if (item.getOwner().getId() != userId) { - return ItemMapper.toFullItemDto( + return ItemMapper.toBigDto( item, null, null, getCommentDtos(itemId) ); } - return ItemMapper.toFullItemDto( + return ItemMapper.toBigDto( item, bookingRepository.findLastBookingDate(itemId), bookingRepository.findNextBookingDate(itemId), @@ -56,8 +57,8 @@ public ItemDtoFullVersion getItem(long userId, long itemId) { } @Override - public List getAllItemsOfUser(long userId) { - checkUser(userId); + public List getAllItemsOfUser(long userId) { + utilService.checkUser(userId); Map items = itemRepository.findAllByOwner_Id(userId) .stream() .collect(Collectors.toMap(Item::getId, Function.identity())); @@ -65,9 +66,9 @@ public List getAllItemsOfUser(long userId) { Map comments = commentRepository.findAllByItemIdIn(items.keySet()).stream() .collect(Collectors.toMap(comment -> comment.getItem().getId(), Function.identity())); - List result = new ArrayList<>(); + List result = new ArrayList<>(); for (Item item : items.values()) { - result.add(ItemMapper.toFullItemDto( + result.add(ItemMapper.toBigDto( item, getLastBookingDate(bookings), getNextBookingDate(bookings), @@ -88,41 +89,47 @@ public List search(long userId, String text) { return Collections.emptyList(); } return itemRepository.search(text).stream() - .map(ItemMapper::toItemDto) + .map(ItemMapper::toDto) .toList(); } @Override - public ItemDto addNewItem(Long userId, ItemDto itemDto) { - User user = getUserWithCheckPresent(userId); - return ItemMapper.toItemDto(itemRepository.save(ItemMapper.toItem(itemDto, user))); + public ItemDto addNewItem(Long userId, @NonNull ItemDto itemDto) { + User user = utilService.getUser(userId); + Long itemRequestId = itemDto.getRequestId(); + if (itemDto.getRequestId() == null) { + return ItemMapper.toDto(itemRepository.save(ItemMapper.toEntity(itemDto, user, null))); + } else { + ItemRequest request = utilService.getItemRequest(itemRequestId); + return ItemMapper.toDto(itemRepository.save(ItemMapper.toEntity(itemDto, user, request))); + } } @Override public ItemDto updateItem(long userId, long itemId, @NonNull ItemDto itemDto) { - checkUser(userId); - Item oldItem = getItemWithCheckPresentAndOwner(itemId, userId); + utilService.checkUser(userId); + Item oldItem = getItemWithOwner(itemId, userId); Item item = new Item() .setId(itemId) .setName(itemDto.getName() == null ? oldItem.getName() : itemDto.getName()) .setDescription(itemDto.getDescription() == null ? oldItem.getDescription() : itemDto.getDescription()) .setAvailable(itemDto.getAvailable() == null ? oldItem.isAvailable() : itemDto.getAvailable()) - .setOwner(getUserWithCheckPresent(userId)) - .setRequest(null); - return ItemMapper.toItemDto(itemRepository.save(item)); + .setOwner(utilService.getUser(userId)) + .setRequest(itemDto.getRequestId() == null ? null : utilService.getItemRequest(itemDto.getRequestId())); + return ItemMapper.toDto(itemRepository.save(item)); } @Override public void deleteItem(long userId, long itemId) { - checkUser(userId); - getItemWithCheckPresentAndOwner(itemId, userId); + utilService.checkUser(userId); + getItemWithOwner(itemId, userId); itemRepository.deleteById(itemId); } @Override public CommentDto addComment(long userId, long itemId, @NonNull CommentDto commentDto) { - User user = getUserWithCheckPresent(userId); - Item item = getItemWithCheckPresent(itemId); + User user = utilService.getUser(userId); + Item item = utilService.getItem(itemId); if (item.getOwner().getId().equals(userId)) { throw new ForbiddenException("Нельзя оставить комментарий к своей вещи"); } @@ -140,29 +147,9 @@ public CommentDto addComment(long userId, long itemId, @NonNull CommentDto comme ); } - private void checkUser(long userId) { - if (!userRepository.existsById(userId)) { - throw new NotFoundException("Пользователь с id=" + userId + " не найден."); - } - } - - @NonNull - private User getUserWithCheckPresent(long userId) { - return userRepository.findById(userId).orElseThrow(() -> - new NotFoundException("Пользователь с id=" + userId + " не найден.") - ); - } - - @NonNull - private Item getItemWithCheckPresent(long itemId) { - return itemRepository.findById(itemId).orElseThrow(() -> - new NotFoundException("Вещь с id=" + itemId + " не найдена.") - ); - } - @NonNull - private Item getItemWithCheckPresentAndOwner(long itemId, long userId) { - Item item = getItemWithCheckPresent(itemId); + private Item getItemWithOwner(long itemId, long userId) { + Item item = utilService.getItem(itemId); if (item.getOwner().getId() == userId) { return item; } diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequestController.java b/src/main/java/ru/practicum/shareit/request/ItemRequestController.java index 064e2e9..90d4d9d 100644 --- a/src/main/java/ru/practicum/shareit/request/ItemRequestController.java +++ b/src/main/java/ru/practicum/shareit/request/ItemRequestController.java @@ -1,12 +1,44 @@ package ru.practicum.shareit.request; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestBigDto; +import ru.practicum.shareit.request.service.ItemRequestService; + +import java.util.List; /** * TODO Sprint add-item-requests. */ @RestController @RequestMapping(path = "/requests") +@RequiredArgsConstructor public class ItemRequestController { + + final ItemRequestService requestService; + + @PostMapping + public ItemRequestDto addNewRequest( + @RequestHeader("X-Sharer-User-Id") long userId, + @RequestBody @Valid ItemRequestDto requestDto + ) { + return requestService.addNewRequest(userId, requestDto); + } + + @GetMapping + public List getRequestListByUser(@RequestHeader("X-Sharer-User-Id") long requestorId) { + return requestService.getRequestListByUser(requestorId); + } + + @GetMapping("/all") + public List getAllRequests(@RequestHeader("X-Sharer-User-Id") long userId) { + return requestService.getAllRequests(userId); + } + + @GetMapping("/{requestId}") + public ItemRequestBigDto getRequestById(@PathVariable long requestId) { + return requestService.getRequestById(requestId); + } } diff --git a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestBigDto.java b/src/main/java/ru/practicum/shareit/request/dto/ItemRequestBigDto.java new file mode 100644 index 0000000..f2546d4 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/request/dto/ItemRequestBigDto.java @@ -0,0 +1,33 @@ +package ru.practicum.shareit.request.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.item.dto.ItemLowDto; +import ru.practicum.shareit.user.dto.UserDto; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@NotNull +@Builder +@FieldDefaults(makeFinal = true) +public class ItemRequestBigDto { + + Long id; + + @Size(min = 1, max = 500) + String description; + + UserDto requestor; + + @JsonProperty("items") + List itemDtos; + + @JsonProperty("created") + LocalDateTime createdDate; +} diff --git a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java b/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java index 7b3ed54..d565eaf 100644 --- a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java +++ b/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java @@ -1,7 +1,31 @@ package ru.practicum.shareit.request.dto; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.user.dto.UserDto; + +import java.time.LocalDateTime; + /** * TODO Sprint add-item-requests. */ +@Data +@NotNull +@Builder +@FieldDefaults(makeFinal = true) public class ItemRequestDto { + + Long id; + + @Size(min = 1, max = 500) + String description; + + UserDto requestor; + + @JsonProperty("created") + LocalDateTime createdDate; } diff --git a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestMapper.java b/src/main/java/ru/practicum/shareit/request/dto/ItemRequestMapper.java index 6d9d394..76d5c40 100644 --- a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestMapper.java +++ b/src/main/java/ru/practicum/shareit/request/dto/ItemRequestMapper.java @@ -1,4 +1,40 @@ package ru.practicum.shareit.request.dto; +import ru.practicum.shareit.item.dto.ItemMapper; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.user.dto.UserMapper; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.List; + public class ItemRequestMapper { + + public static ItemRequestDto toDto(ItemRequest itemRequest) { + return ItemRequestDto.builder() + .id(itemRequest.getId()) + .description(itemRequest.getDescription()) + .requestor(UserMapper.toUserDto(itemRequest.getRequestor())) + .createdDate(itemRequest.getCreatedDate()) + .build(); + } + + public static ItemRequestBigDto toBigDto(ItemRequest itemRequest, List items) { + return ItemRequestBigDto.builder() + .id(itemRequest.getId()) + .description(itemRequest.getDescription()) + .requestor(UserMapper.toUserDto(itemRequest.getRequestor())) + .itemDtos(items.stream().map(ItemMapper::toLowDto).toList()) + .createdDate(itemRequest.getCreatedDate()) + .build(); + } + + public static ItemRequest toEntity(ItemRequestDto requestDto, User requestor) { + return new ItemRequest() + .setDescription(requestDto.getDescription()) + .setRequestor(requestor) + .setCreatedDate(requestDto.getCreatedDate() == null ? + LocalDateTime.now() : requestDto.getCreatedDate()); + } } diff --git a/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java index 2bb0df7..dbc891a 100644 --- a/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java +++ b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java @@ -30,6 +30,6 @@ public class ItemRequest { @JoinColumn(name = "requestor_id", nullable = false) User requestor; - @Column(nullable = false) - LocalDateTime created; + @Column(name = "created", nullable = false) + LocalDateTime createdDate; } diff --git a/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java b/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java index 6d136f3..79e35a4 100644 --- a/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java +++ b/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java @@ -3,5 +3,9 @@ import org.springframework.data.jpa.repository.JpaRepository; import ru.practicum.shareit.request.model.ItemRequest; +import java.util.List; + public interface ItemRequestRepository extends JpaRepository { + + List findAllByRequestor_Id(Long requestorId); } diff --git a/src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java b/src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java index 4eae728..9959772 100644 --- a/src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java +++ b/src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java @@ -1,4 +1,24 @@ package ru.practicum.shareit.request.service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestBigDto; + +import java.util.List; + +@Transactional(readOnly = true) public interface ItemRequestService { + + /// Добавить новый запрос вещи + @Transactional + ItemRequestDto addNewRequest(long userId, ItemRequestDto requestDto); + + /// Получить список своих запросов вместе с данными об ответах на них + List getRequestListByUser(long userId); + + /// Получить список запросов, созданных другими пользователями + List getAllRequests(long userId); + + /// Получить данные об одном конкретном запросе вместе с данными об ответах на него + ItemRequestBigDto getRequestById(long requestId); } diff --git a/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java b/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java index 95db20a..2384bf7 100644 --- a/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java @@ -1,4 +1,89 @@ package ru.practicum.shareit.request.service; +import com.querydsl.jpa.JPQLQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.model.QItem; +import ru.practicum.shareit.request.dto.ItemRequestBigDto; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestMapper; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.request.model.QItemRequest; +import ru.practicum.shareit.request.repository.ItemRequestRepository; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.util.UtilService; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor public class ItemRequestServiceImpl implements ItemRequestService { + + final ItemRequestRepository itemRequestRepository; + final JPQLQueryFactory queryFactory; + final UtilService utilService; + + @Override + public ItemRequestDto addNewRequest(long userId, ItemRequestDto requestDto) { + User user = utilService.getUser(userId); + ItemRequest itemRequest = ItemRequestMapper.toEntity(requestDto, user); + itemRequest = itemRequestRepository.save(itemRequest); + return ItemRequestMapper.toDto(itemRequest); + } + + @Override + public List getRequestListByUser(long userId) { + utilService.checkUser(userId); + + List itemRequests = itemRequestRepository.findAllByRequestor_Id(userId); + if (itemRequests.isEmpty()) { + return Collections.emptyList(); + } + + List requestIds = itemRequests.stream() + .map(ItemRequest::getId) + .toList(); + + QItem qItem = QItem.item; + Map> itemsByRequestId = queryFactory + .selectFrom(qItem) + .where(qItem.request.id.in(requestIds)) + .fetch() + .stream() + .collect(Collectors.groupingBy(item -> item.getRequest().getId())); + + return itemRequests.stream() + .map(itemRequest -> ItemRequestMapper.toBigDto( + itemRequest, + itemsByRequestId.getOrDefault(itemRequest.getId(), Collections.emptyList()) + )) + .sorted(Comparator.comparing(ItemRequestBigDto::getCreatedDate).reversed()) + .toList(); + } + + @Override + public List getAllRequests(long userId) { + utilService.checkUser(userId); + QItemRequest qItemRequest = QItemRequest.itemRequest; + return queryFactory.selectFrom(qItemRequest) + .where(qItemRequest.requestor.id.notIn(userId)) + .orderBy(qItemRequest.createdDate.desc()) + .fetch() + .stream() + .map(ItemRequestMapper::toDto) + .toList(); + } + + @Override + public ItemRequestBigDto getRequestById(long requestId) { + QItem qItem = QItem.item; + List items = queryFactory.selectFrom(qItem).where(qItem.request.id.eq(requestId)).fetch(); + ItemRequest request = utilService.getItemRequest(requestId); + return ItemRequestMapper.toBigDto(request, items); + } } diff --git a/src/main/java/ru/practicum/shareit/user/UserController.java b/src/main/java/ru/practicum/shareit/user/UserController.java index 8dec375..d22d176 100644 --- a/src/main/java/ru/practicum/shareit/user/UserController.java +++ b/src/main/java/ru/practicum/shareit/user/UserController.java @@ -6,7 +6,7 @@ import org.springframework.web.bind.annotation.*; import ru.practicum.shareit.user.dto.UserDto; import ru.practicum.shareit.user.service.UserService; -import ru.practicum.shareit.util.Marker; +import ru.practicum.shareit.util.validate.Marker; import java.util.List; diff --git a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java b/src/main/java/ru/practicum/shareit/user/dto/UserDto.java index d538b0b..8d94d28 100644 --- a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java +++ b/src/main/java/ru/practicum/shareit/user/dto/UserDto.java @@ -4,7 +4,7 @@ import lombok.Builder; import lombok.Data; import lombok.experimental.FieldDefaults; -import ru.practicum.shareit.util.Marker; +import ru.practicum.shareit.util.validate.Marker; @Data @NotNull diff --git a/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java b/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java index 6bfc640..10e6dad 100644 --- a/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java @@ -6,6 +6,7 @@ import ru.practicum.shareit.user.dto.UserDto; import ru.practicum.shareit.user.dto.UserMapper; import ru.practicum.shareit.user.repository.UserRepository; +import ru.practicum.shareit.util.UtilService; import ru.practicum.shareit.util.exception.DuplicatedDataException; import ru.practicum.shareit.util.exception.NotFoundException; @@ -16,10 +17,11 @@ class UserServiceImpl implements UserService { final UserRepository userRepository; + final UtilService utilService; @Override public UserDto getUser(long userId) { - return UserMapper.toUserDto(getUserWithCheckPresent(userId)); + return UserMapper.toUserDto(utilService.getUser(userId)); } @Override @@ -37,7 +39,7 @@ public UserDto saveUser(UserDto userDto) { @Override public UserDto updateUser(long userId, UserDto userDto) { - User user = getUserWithCheckPresent(userId); + User user = utilService.getUser(userId); if (userDto.getName() != null) { user.setName(userDto.getName()); } @@ -63,10 +65,4 @@ private void checkEmail(String email) { throw new DuplicatedDataException("Пользователь с такой почтой " + email + " уже есть."); } } - - private User getUserWithCheckPresent(long userId) { - return userRepository.findById(userId).orElseThrow(() -> - new NotFoundException("Пользователь с id=" + userId + " не найден.") - ); - } } diff --git a/src/main/java/ru/practicum/shareit/util/UtilService.java b/src/main/java/ru/practicum/shareit/util/UtilService.java new file mode 100644 index 0000000..fd1287c --- /dev/null +++ b/src/main/java/ru/practicum/shareit/util/UtilService.java @@ -0,0 +1,48 @@ +package ru.practicum.shareit.util; + +import lombok.RequiredArgsConstructor; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.request.repository.ItemRequestRepository; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; +import ru.practicum.shareit.util.exception.ForbiddenException; +import ru.practicum.shareit.util.exception.NotFoundException; + +@Service +@RequiredArgsConstructor +public class UtilService { + final UserRepository userRepository; + final ItemRepository itemRepository; + final ItemRequestRepository itemRequestRepository; + + public void checkUser(Long userId) { + if (!userRepository.existsById(userId)) { + throw new ForbiddenException("Пользователь с id=" + userId + " не найден."); + } + } + + @NonNull + public User getUser(long userId) { + return userRepository.findById(userId).orElseThrow(() -> + new NotFoundException("Пользователь с id=" + userId + " не найден.") + ); + } + + @NonNull + public Item getItem(long itemId) { + return itemRepository.findById(itemId).orElseThrow(() -> + new NotFoundException("Вещь с id=" + itemId + " не найдена.") + ); + } + + @NonNull + public ItemRequest getItemRequest(long requestId) { + return itemRequestRepository.findById(requestId).orElseThrow( + () -> new NotFoundException("Запрос с id=" + requestId + " на добавление вещи не найден.") + ); + } +} diff --git a/src/main/java/ru/practicum/shareit/util/config/ShateItConfiguration.java b/src/main/java/ru/practicum/shareit/util/config/ShateItConfiguration.java new file mode 100644 index 0000000..cab8ec4 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/util/config/ShateItConfiguration.java @@ -0,0 +1,38 @@ +package ru.practicum.shareit.util.config; + +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; + +import java.time.format.DateTimeFormatter; + +@SpringBootConfiguration +public class ShateItConfiguration { + + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory queryFactory() { + return new JPAQueryFactory(entityManager); + } + + @Bean + public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { + String pattern = "yyyy-MM-dd'T'HH:mm:ss"; + return builder -> { + builder.simpleDateFormat(pattern); + builder.serializers(new LocalDateTimeSerializer( + DateTimeFormatter.ofPattern(pattern) + )); + builder.deserializers(new LocalDateTimeDeserializer( + DateTimeFormatter.ofPattern(pattern) + )); + }; + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java b/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java index 6b9f82b..19b0c1a 100644 --- a/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java +++ b/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java @@ -86,10 +86,10 @@ public ErrorResponse handleMissingRequestHeaderException(final MissingRequestHea return new ErrorResponse("Отсутствует заголовок в запросе: " + e.getHeaderName()); } - @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseStatus(HttpStatus.I_AM_A_TEAPOT) @ExceptionHandler(HttpMessageNotReadableException.class) - public ErrorResponse handleEmptyBody() { - return new ErrorResponse("Тело запроса отсутствует или некорректно"); + public ErrorResponse handleEmptyBody(final HttpMessageNotReadableException e) { + return new ErrorResponse("Тело запроса отсутствует или некорректно " + e.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) diff --git a/src/main/java/ru/practicum/shareit/util/stub/MethodNotImplemented.java b/src/main/java/ru/practicum/shareit/util/stub/MethodNotImplemented.java deleted file mode 100644 index 7f210ac..0000000 --- a/src/main/java/ru/practicum/shareit/util/stub/MethodNotImplemented.java +++ /dev/null @@ -1,8 +0,0 @@ -package ru.practicum.shareit.util.stub; - -import java.lang.annotation.*; - -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface MethodNotImplemented { -} diff --git a/src/main/java/ru/practicum/shareit/util/stub/MethodNotImplementedBeanPostProcessor.java b/src/main/java/ru/practicum/shareit/util/stub/MethodNotImplementedBeanPostProcessor.java deleted file mode 100644 index 064d30c..0000000 --- a/src/main/java/ru/practicum/shareit/util/stub/MethodNotImplementedBeanPostProcessor.java +++ /dev/null @@ -1,54 +0,0 @@ -package ru.practicum.shareit.util.stub; - -import lombok.NonNull; -import org.springframework.aop.framework.ProxyFactory; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.stereotype.Component; -import ru.practicum.shareit.util.exception.MethodNotImplementedException; - -import java.lang.reflect.Method; - -@Component -public class MethodNotImplementedBeanPostProcessor implements BeanPostProcessor { - - @Override - public Object postProcessAfterInitialization(Object bean, @NonNull String beanName) - throws BeansException { - - Class targetClass = bean.getClass(); - - boolean hasAnnotatedMethod = false; - for (Method method : targetClass.getMethods()) { - if (method.isAnnotationPresent(MethodNotImplemented.class)) { - hasAnnotatedMethod = true; - break; - } - } - - if (!hasAnnotatedMethod) { - return bean; - } - - ProxyFactory proxyFactory = new ProxyFactory(bean); - - proxyFactory.addAdvice((org.aopalliance.intercept.MethodInterceptor) invocation -> { - Method method = invocation.getMethod(); - Method implMethod = targetClass.getMethod( - method.getName(), - method.getParameterTypes() - ); - - if (implMethod.isAnnotationPresent(MethodNotImplemented.class)) { - throw new MethodNotImplementedException( - targetClass.getSimpleName(), - method.getName() - ); - } - - return invocation.proceed(); - }); - - return proxyFactory.getProxy(); - } -} diff --git a/src/main/java/ru/practicum/shareit/util/Marker.java b/src/main/java/ru/practicum/shareit/util/validate/Marker.java similarity index 55% rename from src/main/java/ru/practicum/shareit/util/Marker.java rename to src/main/java/ru/practicum/shareit/util/validate/Marker.java index 6d03882..112f99b 100644 --- a/src/main/java/ru/practicum/shareit/util/Marker.java +++ b/src/main/java/ru/practicum/shareit/util/validate/Marker.java @@ -1,4 +1,4 @@ -package ru.practicum.shareit.util; +package ru.practicum.shareit.util.validate; public interface Marker { interface OnCreate {} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 46af283..cbed0d5 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,4 @@ +spring.main.banner-mode=off spring.main.log-startup-info=false spring.jackson.serialization.indent_output=true spring.sql.init.mode=always @@ -14,6 +15,8 @@ hibernate.default-schema=public hibernate.show-sql=true hibernate.format-sql=true +logging.level.root=error +logging.level.org.springframework.boot.web=info logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.orm.jdbc.bind=trace logging.level.org.zalando.logbook=TRACE From 170b162ffce4e4b1b44ee9201442eb9ac427b4d3 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Tue, 7 Apr 2026 10:21:38 +0300 Subject: [PATCH 02/28] =?UTF-8?q?=D0=92=D1=8B=D0=B4=D0=B5=D0=BB=D1=8F?= =?UTF-8?q?=D0=B5=D0=BC=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8E=20=D0=B2=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 34 +++ compose.yaml | 16 -- docker-compose.yml | 39 +++ gateway/Dockerfile | 5 + gateway/checkstyle.xml | 257 ++++++++++++++++++ gateway/pom.xml | 65 +++++ .../ru/practicum/shareit/ShareItGateway.java | 11 + .../shareit/booking/BookingClient.java | 47 ++++ .../shareit/booking/BookingController.java | 57 ++++ .../booking/dto/BookItemRequestDto.java | 20 ++ .../shareit/booking/dto/BookingState.java | 27 ++ .../practicum/shareit/client/BaseClient.java | 136 +++++++++ .../src/main/resources/application.properties | 7 + gateway/suppressions.xml | 7 + pom.xml | 112 +------- server/Dockerfile | 5 + server/checkstyle.xml | 257 ++++++++++++++++++ server/pom.xml | 120 ++++++++ .../ru/practicum/shareit/ShareItServer.java | 4 +- .../shareit/booking/BookingController.java | 10 +- .../shareit/booking/dto/BookingDto.java | 5 - .../shareit/booking/dto/BookingMapper.java | 0 .../booking/dto/IncomingBookingDto.java | 14 + .../shareit/booking/model/Booking.java | 0 .../shareit/booking/model/BookingStatus.java | 0 .../shareit/booking/model/StateOfBooking.java | 0 .../booking/repository/BookingRepository.java | 0 .../booking/service/BookingService.java | 0 .../booking/service/BookingServiceImpl.java | 6 +- .../shareit/item/ItemController.java | 21 +- .../shareit/item/dto/CommentDto.java | 4 - .../shareit/item/dto/CommentMapper.java | 0 .../shareit/item/dto/ItemBigDto.java | 2 - .../practicum/shareit/item/dto/ItemDto.java | 22 ++ .../shareit/item/dto/ItemLowDto.java | 0 .../shareit/item/dto/ItemMapper.java | 0 .../practicum/shareit/item/model/Comment.java | 0 .../ru/practicum/shareit/item/model/Item.java | 3 - .../item/repository/CommentRepository.java | 0 .../item/repository/ItemRepository.java | 0 .../shareit/item/service/ItemService.java | 0 .../shareit/item/service/ItemServiceImpl.java | 0 .../request/ItemRequestController.java | 6 +- .../request/dto/ItemRequestBigDto.java | 4 - .../shareit/request/dto/ItemRequestDto.java | 4 - .../request/dto/ItemRequestMapper.java | 0 .../shareit/request/model/ItemRequest.java | 3 - .../repository/ItemRequestRepository.java | 0 .../request/service/ItemRequestService.java | 0 .../service/ItemRequestServiceImpl.java | 0 .../shareit/user/UserController.java | 10 +- .../practicum/shareit/user/dto/UserDto.java | 17 ++ .../shareit/user/dto/UserMapper.java | 0 .../ru/practicum/shareit/user/model/User.java | 0 .../user/repository/UserRepository.java | 0 .../shareit/user/service/UserService.java | 0 .../shareit/user/service/UserServiceImpl.java | 0 .../practicum/shareit/util/UtilService.java | 0 .../util/config/ShateItConfiguration.java | 0 .../shareit/util/error/ErrorHandler.java | 1 - .../shareit/util/error/ErrorResponse.java | 0 .../exception/BookingStatusException.java | 0 .../exception/ConditionsNotMetException.java | 0 .../exception/DuplicatedDataException.java | 0 .../util/exception/ForbiddenException.java | 0 .../MethodNotImplementedException.java | 0 .../util/exception/NotFoundException.java | 0 .../exception/ParameterNotValidException.java | 0 .../util/exception/ValidationException.java | 7 + .../hibernate/HibernateEqualsAndHashCode.java | 0 .../src/main/resources/application.properties | 22 ++ {src => server/src}/main/resources/schema.sql | 0 server/suppressions.xml | 7 + .../booking/dto/IncomingBookingDto.java | 25 -- .../practicum/shareit/item/dto/ItemDto.java | 32 --- .../practicum/shareit/user/dto/UserDto.java | 25 -- .../FutureOrPresentWithTolerance.java | 26 -- ...FutureOrPresentWithToleranceValidator.java | 96 ------- .../util/datetime/TimeRoundingStrategy.java | 9 - .../shareit/util/validate/Marker.java | 5 - .../resources/application-test.properties | 12 - src/main/resources/application.properties | 22 -- .../ru/practicum/shareit/ShareItTests.java | 13 - 83 files changed, 1200 insertions(+), 459 deletions(-) create mode 100644 .gitignore delete mode 100644 compose.yaml create mode 100644 docker-compose.yml create mode 100644 gateway/Dockerfile create mode 100644 gateway/checkstyle.xml create mode 100644 gateway/pom.xml create mode 100644 gateway/src/main/java/ru/practicum/shareit/ShareItGateway.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/booking/BookingClient.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/booking/dto/BookItemRequestDto.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingState.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java create mode 100644 gateway/src/main/resources/application.properties create mode 100644 gateway/suppressions.xml create mode 100644 server/Dockerfile create mode 100644 server/checkstyle.xml create mode 100644 server/pom.xml rename src/main/java/ru/practicum/shareit/ShareItApp.java => server/src/main/java/ru/practicum/shareit/ShareItServer.java (73%) rename {src => server/src}/main/java/ru/practicum/shareit/booking/BookingController.java (85%) rename {src => server/src}/main/java/ru/practicum/shareit/booking/dto/BookingDto.java (79%) rename {src => server/src}/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java (100%) create mode 100644 server/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java rename {src => server/src}/main/java/ru/practicum/shareit/booking/model/Booking.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/booking/model/BookingStatus.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/booking/model/StateOfBooking.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/booking/service/BookingService.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java (95%) rename {src => server/src}/main/java/ru/practicum/shareit/item/ItemController.java (63%) rename {src => server/src}/main/java/ru/practicum/shareit/item/dto/CommentDto.java (73%) rename {src => server/src}/main/java/ru/practicum/shareit/item/dto/CommentMapper.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/dto/ItemBigDto.java (91%) create mode 100644 server/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java rename {src => server/src}/main/java/ru/practicum/shareit/item/dto/ItemLowDto.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/dto/ItemMapper.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/model/Comment.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/model/Item.java (97%) rename {src => server/src}/main/java/ru/practicum/shareit/item/repository/CommentRepository.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/repository/ItemRepository.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/service/ItemService.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/request/ItemRequestController.java (90%) rename {src => server/src}/main/java/ru/practicum/shareit/request/dto/ItemRequestBigDto.java (82%) rename {src => server/src}/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java (79%) rename {src => server/src}/main/java/ru/practicum/shareit/request/dto/ItemRequestMapper.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/request/model/ItemRequest.java (94%) rename {src => server/src}/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/request/service/ItemRequestService.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/user/UserController.java (79%) create mode 100644 server/src/main/java/ru/practicum/shareit/user/dto/UserDto.java rename {src => server/src}/main/java/ru/practicum/shareit/user/dto/UserMapper.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/user/model/User.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/user/repository/UserRepository.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/user/service/UserService.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/util/UtilService.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/util/config/ShateItConfiguration.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/util/error/ErrorHandler.java (98%) rename {src => server/src}/main/java/ru/practicum/shareit/util/error/ErrorResponse.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/util/exception/BookingStatusException.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/util/exception/ConditionsNotMetException.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/util/exception/DuplicatedDataException.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/util/exception/ForbiddenException.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/util/exception/MethodNotImplementedException.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/util/exception/NotFoundException.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/util/exception/ParameterNotValidException.java (100%) create mode 100644 server/src/main/java/ru/practicum/shareit/util/exception/ValidationException.java rename {src => server/src}/main/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCode.java (100%) create mode 100644 server/src/main/resources/application.properties rename {src => server/src}/main/resources/schema.sql (100%) create mode 100644 server/suppressions.xml delete mode 100644 src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java delete mode 100644 src/main/java/ru/practicum/shareit/item/dto/ItemDto.java delete mode 100644 src/main/java/ru/practicum/shareit/user/dto/UserDto.java delete mode 100644 src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithTolerance.java delete mode 100644 src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithToleranceValidator.java delete mode 100644 src/main/java/ru/practicum/shareit/util/datetime/TimeRoundingStrategy.java delete mode 100644 src/main/java/ru/practicum/shareit/util/validate/Marker.java delete mode 100644 src/main/resources/application-test.properties delete mode 100644 src/main/resources/application.properties delete mode 100644 src/test/java/ru/practicum/shareit/ShareItTests.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53a38f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ +/.stels/ diff --git a/compose.yaml b/compose.yaml deleted file mode 100644 index ae1e654..0000000 --- a/compose.yaml +++ /dev/null @@ -1,16 +0,0 @@ -services: - db: - image: postgres:16.1 - ports: - - "5432:5432" - volumes: - - ./volumes/postgres:/var/lib/postgresql/data/ - environment: - - POSTGRES_DB=shareit - - POSTGRES_USER=dbuser - - POSTGRES_PASSWORD=12345 - healthcheck: - test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER - timeout: 5s - interval: 5s - retries: 10 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1b3607c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +services: + gateway: + build: gateway + image: shareit-gateway + container_name: shareit-gateway + ports: + - "8080:8080" + depends_on: + - server + environment: + - SHAREIT_SERVER_URL=http://server:9090 + + server: + build: server + image: shareit-server + container_name: shareit-server + ports: + - "9090:9090" + depends_on: + - db + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/shareit + - SPRING_DATASOURCE_USERNAME=dbuser + - SPRING_DATASOURCE_PASSWORD=12345 + + db: + image: postgres:16.1 + container_name: postgres + ports: + - "6541:5432" + environment: + - POSTGRES_DB=shareit + - POSTGRES_USER=dbuser + - POSTGRES_PASSWORD=12345 + healthcheck: + test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER + timeout: 5s + interval: 5s + retries: 10 \ No newline at end of file diff --git a/gateway/Dockerfile b/gateway/Dockerfile new file mode 100644 index 0000000..0ff1817 --- /dev/null +++ b/gateway/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:21-jre-jammy +VOLUME /tmp +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file diff --git a/gateway/checkstyle.xml b/gateway/checkstyle.xml new file mode 100644 index 0000000..c28a0d3 --- /dev/null +++ b/gateway/checkstyle.xml @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gateway/pom.xml b/gateway/pom.xml new file mode 100644 index 0000000..8f94c64 --- /dev/null +++ b/gateway/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + + ru.practicum + shareit + 0.0.1-SNAPSHOT + + + shareit-gateway + + ShareIt Gateway + + + + org.projectlombok + lombok + provided + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-actuator + + + org.hibernate.validator + hibernate-validator + + + org.apache.httpcomponents.client5 + httpclient5 + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/ShareItGateway.java b/gateway/src/main/java/ru/practicum/shareit/ShareItGateway.java new file mode 100644 index 0000000..7b8ca0a --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/ShareItGateway.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ShareItGateway { + public static void main(String[] args) { + SpringApplication.run(ShareItGateway.class, args); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/BookingClient.java b/gateway/src/main/java/ru/practicum/shareit/booking/BookingClient.java new file mode 100644 index 0000000..438b060 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/BookingClient.java @@ -0,0 +1,47 @@ +package ru.practicum.shareit.booking; + +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.util.DefaultUriBuilderFactory; + +import ru.practicum.shareit.booking.dto.BookItemRequestDto; +import ru.practicum.shareit.booking.dto.BookingState; +import ru.practicum.shareit.client.BaseClient; + +@Service +public class BookingClient extends BaseClient { + private static final String API_PREFIX = "/bookings"; + + @Autowired + public BookingClient(@Value("${shareit-server.url}") String serverUrl, RestTemplateBuilder builder) { + super( + builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + API_PREFIX)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build() + ); + } + + public ResponseEntity getBookings(long userId, BookingState state, Integer from, Integer size) { + Map parameters = Map.of( + "state", state.name(), + "from", from, + "size", size + ); + return get("?state={state}&from={from}&size={size}", userId, parameters); + } + + public ResponseEntity bookItem(long userId, BookItemRequestDto requestDto) { + return post("", userId, requestDto); + } + + public ResponseEntity getBooking(long userId, Long bookingId) { + return get("/" + bookingId, userId); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java b/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java new file mode 100644 index 0000000..bceb75f --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -0,0 +1,57 @@ +package ru.practicum.shareit.booking; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import ru.practicum.shareit.booking.dto.BookItemRequestDto; +import ru.practicum.shareit.booking.dto.BookingState; + +@Controller +@RequestMapping(path = "/bookings") +@RequiredArgsConstructor +@Slf4j +@Validated +public class BookingController { + private final BookingClient bookingClient; + + @GetMapping + public ResponseEntity getBookings( + @RequestHeader("X-Sharer-User-Id") long userId, + @RequestParam(name = "state", defaultValue = "all") String stateParam, + @PositiveOrZero @RequestParam(name = "from", defaultValue = "0") Integer from, + @Positive @RequestParam(name = "size", defaultValue = "10") Integer size) { + BookingState state = BookingState.from(stateParam) + .orElseThrow(() -> new IllegalArgumentException("Unknown state: " + stateParam)); + log.info("Get booking with state {}, userId={}, from={}, size={}", stateParam, userId, from, size); + return bookingClient.getBookings(userId, state, from, size); + } + + @PostMapping + public ResponseEntity bookItem( + @RequestHeader("X-Sharer-User-Id") long userId, + @RequestBody @Valid BookItemRequestDto requestDto) { + log.info("Creating booking {}, userId={}", requestDto, userId); + return bookingClient.bookItem(userId, requestDto); + } + + @GetMapping("/{bookingId}") + public ResponseEntity getBooking( + @RequestHeader("X-Sharer-User-Id") long userId, + @PathVariable Long bookingId) { + log.info("Get booking {}, userId={}", bookingId, userId); + return bookingClient.getBooking(userId, bookingId); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookItemRequestDto.java b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookItemRequestDto.java new file mode 100644 index 0000000..738f5c0 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookItemRequestDto.java @@ -0,0 +1,20 @@ +package ru.practicum.shareit.booking.dto; + +import java.time.LocalDateTime; + +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.FutureOrPresent; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class BookItemRequestDto { + private long itemId; + @FutureOrPresent + private LocalDateTime start; + @Future + private LocalDateTime end; +} diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingState.java b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingState.java new file mode 100644 index 0000000..7bfa164 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingState.java @@ -0,0 +1,27 @@ +package ru.practicum.shareit.booking.dto; + +import java.util.Optional; + +public enum BookingState { + // Все + ALL, + // Текущие + CURRENT, + // Будущие + FUTURE, + // Завершенные + PAST, + // Отклоненные + REJECTED, + // Ожидающие подтверждения + WAITING; + + public static Optional from(String stringState) { + for (BookingState state : values()) { + if (state.name().equalsIgnoreCase(stringState)) { + return Optional.of(state); + } + } + return Optional.empty(); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java b/gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java new file mode 100644 index 0000000..5dc82ad --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java @@ -0,0 +1,136 @@ +package ru.practicum.shareit.client; + +import jakarta.annotation.Nonnull; +import org.springframework.http.*; +import org.springframework.lang.Nullable; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; + +import java.util.List; +import java.util.Map; + +public class BaseClient { + protected final RestTemplate rest; + + public BaseClient(RestTemplate rest) { + this.rest = rest; + } + + protected ResponseEntity get(String path) { + return get(path, null, null); + } + + protected ResponseEntity get(String path, long userId) { + return get(path, userId, null); + } + + protected ResponseEntity get(String path, + Long userId, + @Nullable Map parameters) { + return makeAndSendRequest(HttpMethod.GET, path, userId, parameters, null); + } + + protected ResponseEntity post(String path, T body) { + return post(path, null, null, body); + } + + protected ResponseEntity post(String path, long userId, T body) { + return post(path, userId, null, body); + } + + protected ResponseEntity post(String path, + Long userId, + @Nullable Map parameters, + T body) { + return makeAndSendRequest(HttpMethod.POST, path, userId, parameters, body); + } + + protected ResponseEntity put(String path, long userId, T body) { + return put(path, userId, null, body); + } + + protected ResponseEntity put(String path, + long userId, + @Nullable Map parameters, + T body) { + return makeAndSendRequest(HttpMethod.PUT, path, userId, parameters, body); + } + + protected ResponseEntity patch(String path, T body) { + return patch(path, null, null, body); + } + + protected ResponseEntity patch(String path, long userId) { + return patch(path, userId, null, null); + } + + protected ResponseEntity patch(String path, long userId, T body) { + return patch(path, userId, null, body); + } + + protected ResponseEntity patch(String path, + Long userId, + @Nullable Map parameters, + T body) { + return makeAndSendRequest(HttpMethod.PATCH, path, userId, parameters, body); + } + + protected ResponseEntity delete(String path) { + return delete(path, null, null); + } + + protected ResponseEntity delete(String path, long userId) { + return delete(path, userId, null); + } + + protected ResponseEntity delete(String path, + Long userId, + @Nullable Map parameters) { + return makeAndSendRequest(HttpMethod.DELETE, path, userId, parameters, null); + } + + private ResponseEntity makeAndSendRequest(HttpMethod method, + String path, + Long userId, + @Nullable Map parameters, + @Nullable T body) { + HttpEntity requestEntity = new HttpEntity<>(body, defaultHeaders(userId)); + + ResponseEntity shareitServerResponse; + try { + if (parameters != null) { + shareitServerResponse = rest.exchange(path, method, requestEntity, Object.class, parameters); + } else { + shareitServerResponse = rest.exchange(path, method, requestEntity, Object.class); + } + } catch (HttpStatusCodeException e) { + return ResponseEntity.status(e.getStatusCode()).body(e.getResponseBodyAsByteArray()); + } + return prepareGatewayResponse(shareitServerResponse); + } + + @Nonnull + private HttpHeaders defaultHeaders(Long userId) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(List.of(MediaType.APPLICATION_JSON)); + if (userId != null) { + headers.set("X-Sharer-User-Id", String.valueOf(userId)); + } + return headers; + } + + private static ResponseEntity prepareGatewayResponse(@Nonnull ResponseEntity response) { + if (response.getStatusCode().is2xxSuccessful()) { + return response; + } + + ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.status(response.getStatusCode()); + + if (response.hasBody()) { + return responseBuilder.body(response.getBody()); + } + + return responseBuilder.build(); + } +} diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties new file mode 100644 index 0000000..2ee0851 --- /dev/null +++ b/gateway/src/main/resources/application.properties @@ -0,0 +1,7 @@ +logging.level.org.springframework.web.client.RestTemplate=DEBUG +#logging.level.org.apache.http=DEBUG +#logging.level.httpclient.wire=DEBUG + +server.port=8080 + +shareit-server.url=http://localhost:9090 \ No newline at end of file diff --git a/gateway/suppressions.xml b/gateway/suppressions.xml new file mode 100644 index 0000000..b2c6822 --- /dev/null +++ b/gateway/suppressions.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0218e57..224f9ac 100644 --- a/pom.xml +++ b/pom.xml @@ -2,6 +2,7 @@ 4.0.0 + org.springframework.boot spring-boot-starter-parent @@ -12,114 +13,26 @@ ru.practicum shareit 0.0.1-SNAPSHOT + pom ShareIt + + gateway + server + + 21 - 5.1.0 - - org.projectlombok - lombok - provided - true - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-configuration-processor - true - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.boot - spring-boot-starter-validation - - - org.springframework.boot - spring-boot-starter-data-jpa - - - com.querydsl - querydsl-jpa - jakarta - ${querydsl.version} - - - org.postgresql - postgresql - runtime - - - com.h2database - h2 - test - - - org.zalando - logbook-spring-boot-starter - 3.9.0 - - - src/main/resources - true - - - com.mysema.maven - apt-maven-plugin - 1.1.3 - - - - process - - - target/generated-sources/java - com.querydsl.apt.jpa.JPAAnnotationProcessor - - - - - - com.querydsl - querydsl-apt - jakarta - ${querydsl.version} - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - @@ -270,17 +183,6 @@ - - coverage - - - - org.jacoco - jacoco-maven-plugin - - - - diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..0ff1817 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:21-jre-jammy +VOLUME /tmp +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file diff --git a/server/checkstyle.xml b/server/checkstyle.xml new file mode 100644 index 0000000..c28a0d3 --- /dev/null +++ b/server/checkstyle.xml @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/pom.xml b/server/pom.xml new file mode 100644 index 0000000..3cb7ce7 --- /dev/null +++ b/server/pom.xml @@ -0,0 +1,120 @@ + + + 4.0.0 + + + ru.practicum + shareit + 0.0.1-SNAPSHOT + + + shareit-server + + ShareIt Server + + + 5.1.0 + + + + + org.projectlombok + lombok + provided + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.postgresql + postgresql + runtime + + + com.h2database + h2 + runtime + + + org.springframework.boot + spring-boot-configuration-processor + true + + + com.querydsl + querydsl-jpa + jakarta + ${querydsl.version} + + + org.zalando + logbook-spring-boot-starter + 3.9.0 + + + + + + + com.mysema.maven + apt-maven-plugin + 1.1.3 + + + + process + + + target/generated-sources/java + com.querydsl.apt.jpa.JPAAnnotationProcessor + + + + + + com.querydsl + querydsl-apt + jakarta + ${querydsl.version} + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + coverage + + + + org.jacoco + jacoco-maven-plugin + + + + + + + diff --git a/src/main/java/ru/practicum/shareit/ShareItApp.java b/server/src/main/java/ru/practicum/shareit/ShareItServer.java similarity index 73% rename from src/main/java/ru/practicum/shareit/ShareItApp.java rename to server/src/main/java/ru/practicum/shareit/ShareItServer.java index 83b60e0..8d611fc 100644 --- a/src/main/java/ru/practicum/shareit/ShareItApp.java +++ b/server/src/main/java/ru/practicum/shareit/ShareItServer.java @@ -4,8 +4,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class ShareItApp { +public class ShareItServer { public static void main(String[] args) { - SpringApplication.run(ShareItApp.class, args); + SpringApplication.run(ShareItServer.class, args); } } diff --git a/src/main/java/ru/practicum/shareit/booking/BookingController.java b/server/src/main/java/ru/practicum/shareit/booking/BookingController.java similarity index 85% rename from src/main/java/ru/practicum/shareit/booking/BookingController.java rename to server/src/main/java/ru/practicum/shareit/booking/BookingController.java index dfa97a2..c59ad12 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/server/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -1,21 +1,14 @@ package ru.practicum.shareit.booking; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.shareit.booking.dto.BookingDto; import ru.practicum.shareit.booking.dto.IncomingBookingDto; import ru.practicum.shareit.booking.service.BookingService; -import ru.practicum.shareit.util.validate.Marker; import java.util.List; -/** - * TODO Sprint add-bookings. - */ @RestController -@Validated @RequestMapping(path = "/bookings") @RequiredArgsConstructor public class BookingController { @@ -46,10 +39,9 @@ public List getAllBookingsByOwner( } @PostMapping - @Validated(Marker.OnCreate.class) public BookingDto addNewBooking( @RequestHeader("X-Sharer-User-Id") Long userId, - @Valid @RequestBody IncomingBookingDto bookingDto + @RequestBody IncomingBookingDto bookingDto ) { return bookingService.addNewBooking(userId, bookingDto); } diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java similarity index 79% rename from src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java rename to server/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java index fb00380..2b8231f 100644 --- a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java +++ b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java @@ -1,16 +1,11 @@ package ru.practicum.shareit.booking.dto; -import jakarta.validation.constraints.NotNull; import ru.practicum.shareit.booking.model.BookingStatus; import ru.practicum.shareit.item.dto.ItemDto; import ru.practicum.shareit.user.dto.UserDto; import java.time.LocalDateTime; -/** - * TODO Sprint add-bookings. - */ -@NotNull public record BookingDto( Long id, LocalDateTime start, diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java rename to server/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java diff --git a/server/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java b/server/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java new file mode 100644 index 0000000..4e4dc66 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java @@ -0,0 +1,14 @@ +package ru.practicum.shareit.booking.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +public class IncomingBookingDto { + long itemId; + LocalDateTime start; + LocalDateTime end; +} diff --git a/src/main/java/ru/practicum/shareit/booking/model/Booking.java b/server/src/main/java/ru/practicum/shareit/booking/model/Booking.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/model/Booking.java rename to server/src/main/java/ru/practicum/shareit/booking/model/Booking.java diff --git a/src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java b/server/src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java rename to server/src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java diff --git a/src/main/java/ru/practicum/shareit/booking/model/StateOfBooking.java b/server/src/main/java/ru/practicum/shareit/booking/model/StateOfBooking.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/model/StateOfBooking.java rename to server/src/main/java/ru/practicum/shareit/booking/model/StateOfBooking.java diff --git a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java b/server/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java rename to server/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java diff --git a/src/main/java/ru/practicum/shareit/booking/service/BookingService.java b/server/src/main/java/ru/practicum/shareit/booking/service/BookingService.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/service/BookingService.java rename to server/src/main/java/ru/practicum/shareit/booking/service/BookingService.java diff --git a/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java b/server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java similarity index 95% rename from src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java rename to server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java index eaa2a7b..bc40ca1 100644 --- a/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java +++ b/server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java @@ -1,6 +1,5 @@ package ru.practicum.shareit.booking.service; -import jakarta.validation.ValidationException; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; import org.springframework.lang.NonNull; @@ -15,10 +14,7 @@ import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.user.model.User; import ru.practicum.shareit.util.UtilService; -import ru.practicum.shareit.util.exception.BookingStatusException; -import ru.practicum.shareit.util.exception.ForbiddenException; -import ru.practicum.shareit.util.exception.NotFoundException; -import ru.practicum.shareit.util.exception.ParameterNotValidException; +import ru.practicum.shareit.util.exception.*; import java.time.LocalDateTime; import java.util.List; diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/server/src/main/java/ru/practicum/shareit/item/ItemController.java similarity index 63% rename from src/main/java/ru/practicum/shareit/item/ItemController.java rename to server/src/main/java/ru/practicum/shareit/item/ItemController.java index 93c10ba..429a6d4 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/server/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -1,22 +1,15 @@ package ru.practicum.shareit.item; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; import ru.practicum.shareit.item.dto.ItemBigDto; import ru.practicum.shareit.item.service.ItemService; -import ru.practicum.shareit.util.validate.Marker; import java.util.List; -/** - * TODO Sprint add-controllers. - */ @RestController -@Validated @RequestMapping("/items") @RequiredArgsConstructor public class ItemController { @@ -44,18 +37,10 @@ public List search( return itemService.search(userId, text); } - /*Реализуйте ещё одну полезную опцию в вашем приложении, - чтобы пользователи смогли отвечать на запросы друг друга. - Для этого при создании вещи должна быть возможность указать - id запроса, в ответ на который создаётся нужная вещь. - Добавьте поле requestId в тело запроса POST /items. - Обратите внимание: должна сохраниться возможность добавить - вещь и без указания requestId*/ @PostMapping - @Validated(Marker.OnCreate.class) public ItemDto addNewItem( @RequestHeader("X-Sharer-User-Id") long userId, - @Valid @RequestBody ItemDto item + @RequestBody ItemDto item ) { return itemService.addNewItem(userId, item); } @@ -64,7 +49,7 @@ public ItemDto addNewItem( public CommentDto addComment( @RequestHeader("X-Sharer-User-Id") long userId, @PathVariable long itemId, - @Valid @RequestBody CommentDto commentDto + @RequestBody CommentDto commentDto ) { return itemService.addComment(userId, itemId, commentDto); } @@ -73,7 +58,7 @@ public CommentDto addComment( public ItemDto updateItem( @RequestHeader("X-Sharer-User-Id") long userId, @PathVariable long itemId, - @Valid @RequestBody ItemDto itemDto + @RequestBody ItemDto itemDto ) { return itemService.updateItem(userId, itemId, itemDto); } diff --git a/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java similarity index 73% rename from src/main/java/ru/practicum/shareit/item/dto/CommentDto.java rename to server/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java index 09646eb..1b15e9a 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java +++ b/server/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java @@ -1,8 +1,6 @@ package ru.practicum.shareit.item.dto; import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; import lombok.Builder; import lombok.Data; @@ -10,7 +8,6 @@ @Data @Builder -@NotNull public class CommentDto { Long id; @@ -22,6 +19,5 @@ public class CommentDto { @JsonProperty("created") LocalDateTime createdDate; - @Size(max = 500) String text; } diff --git a/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java b/server/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java rename to server/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemBigDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemBigDto.java similarity index 91% rename from src/main/java/ru/practicum/shareit/item/dto/ItemBigDto.java rename to server/src/main/java/ru/practicum/shareit/item/dto/ItemBigDto.java index 41055e2..6689850 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemBigDto.java +++ b/server/src/main/java/ru/practicum/shareit/item/dto/ItemBigDto.java @@ -1,7 +1,6 @@ package ru.practicum.shareit.item.dto; import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.validation.constraints.NotNull; import lombok.*; import lombok.experimental.FieldDefaults; @@ -12,7 +11,6 @@ * TODO Sprint add-controllers. */ @Data -@NotNull @Builder @FieldDefaults(makeFinal = true) public class ItemBigDto { diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java new file mode 100644 index 0000000..5d4c19b --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -0,0 +1,22 @@ +package ru.practicum.shareit.item.dto; + + +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +@Data +@Builder +@FieldDefaults(makeFinal = true) +public class ItemDto { + + Long id; + + String name; + + String description; + + Boolean available; + + Long requestId; +} diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemLowDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemLowDto.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/dto/ItemLowDto.java rename to server/src/main/java/ru/practicum/shareit/item/dto/ItemLowDto.java diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java rename to server/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java diff --git a/src/main/java/ru/practicum/shareit/item/model/Comment.java b/server/src/main/java/ru/practicum/shareit/item/model/Comment.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/model/Comment.java rename to server/src/main/java/ru/practicum/shareit/item/model/Comment.java diff --git a/src/main/java/ru/practicum/shareit/item/model/Item.java b/server/src/main/java/ru/practicum/shareit/item/model/Item.java similarity index 97% rename from src/main/java/ru/practicum/shareit/item/model/Item.java rename to server/src/main/java/ru/practicum/shareit/item/model/Item.java index f17ad6b..e099864 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/server/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -12,9 +12,6 @@ import static ru.practicum.shareit.util.hibernate.HibernateEqualsAndHashCode.*; -/** - * TODO Sprint add-controllers. - */ @Data @Entity @NoArgsConstructor diff --git a/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java b/server/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java rename to server/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java diff --git a/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java b/server/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java rename to server/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java diff --git a/src/main/java/ru/practicum/shareit/item/service/ItemService.java b/server/src/main/java/ru/practicum/shareit/item/service/ItemService.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/service/ItemService.java rename to server/src/main/java/ru/practicum/shareit/item/service/ItemService.java diff --git a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java b/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java rename to server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequestController.java b/server/src/main/java/ru/practicum/shareit/request/ItemRequestController.java similarity index 90% rename from src/main/java/ru/practicum/shareit/request/ItemRequestController.java rename to server/src/main/java/ru/practicum/shareit/request/ItemRequestController.java index 90d4d9d..7520470 100644 --- a/src/main/java/ru/practicum/shareit/request/ItemRequestController.java +++ b/server/src/main/java/ru/practicum/shareit/request/ItemRequestController.java @@ -1,6 +1,5 @@ package ru.practicum.shareit.request; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import ru.practicum.shareit.request.dto.ItemRequestDto; @@ -9,9 +8,6 @@ import java.util.List; -/** - * TODO Sprint add-item-requests. - */ @RestController @RequestMapping(path = "/requests") @RequiredArgsConstructor @@ -22,7 +18,7 @@ public class ItemRequestController { @PostMapping public ItemRequestDto addNewRequest( @RequestHeader("X-Sharer-User-Id") long userId, - @RequestBody @Valid ItemRequestDto requestDto + @RequestBody ItemRequestDto requestDto ) { return requestService.addNewRequest(userId, requestDto); } diff --git a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestBigDto.java b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestBigDto.java similarity index 82% rename from src/main/java/ru/practicum/shareit/request/dto/ItemRequestBigDto.java rename to server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestBigDto.java index f2546d4..5093566 100644 --- a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestBigDto.java +++ b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestBigDto.java @@ -1,8 +1,6 @@ package ru.practicum.shareit.request.dto; import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; import lombok.Builder; import lombok.Data; import lombok.experimental.FieldDefaults; @@ -13,14 +11,12 @@ import java.util.List; @Data -@NotNull @Builder @FieldDefaults(makeFinal = true) public class ItemRequestBigDto { Long id; - @Size(min = 1, max = 500) String description; UserDto requestor; diff --git a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java similarity index 79% rename from src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java rename to server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java index d565eaf..06880f3 100644 --- a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java +++ b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java @@ -1,8 +1,6 @@ package ru.practicum.shareit.request.dto; import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; import lombok.Builder; import lombok.Data; import lombok.experimental.FieldDefaults; @@ -14,14 +12,12 @@ * TODO Sprint add-item-requests. */ @Data -@NotNull @Builder @FieldDefaults(makeFinal = true) public class ItemRequestDto { Long id; - @Size(min = 1, max = 500) String description; UserDto requestor; diff --git a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestMapper.java b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestMapper.java similarity index 100% rename from src/main/java/ru/practicum/shareit/request/dto/ItemRequestMapper.java rename to server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestMapper.java diff --git a/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java b/server/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java similarity index 94% rename from src/main/java/ru/practicum/shareit/request/model/ItemRequest.java rename to server/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java index dbc891a..26e97b4 100644 --- a/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java +++ b/server/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java @@ -9,9 +9,6 @@ import java.time.LocalDateTime; -/** - * TODO Sprint add-item-requests. - */ @Data @Entity @Table(name = "requests") diff --git a/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java b/server/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java similarity index 100% rename from src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java rename to server/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java diff --git a/src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java similarity index 100% rename from src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java rename to server/src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java diff --git a/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java similarity index 100% rename from src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java rename to server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java diff --git a/src/main/java/ru/practicum/shareit/user/UserController.java b/server/src/main/java/ru/practicum/shareit/user/UserController.java similarity index 79% rename from src/main/java/ru/practicum/shareit/user/UserController.java rename to server/src/main/java/ru/practicum/shareit/user/UserController.java index d22d176..02e9274 100644 --- a/src/main/java/ru/practicum/shareit/user/UserController.java +++ b/server/src/main/java/ru/practicum/shareit/user/UserController.java @@ -1,18 +1,13 @@ package ru.practicum.shareit.user; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.shareit.user.dto.UserDto; import ru.practicum.shareit.user.service.UserService; -import ru.practicum.shareit.util.validate.Marker; import java.util.List; -/** - * TODO Sprint add-controllers. - */ @RestController @Validated @RequestMapping(path = "/users") @@ -32,15 +27,14 @@ public List getAllUsers() { } @PostMapping - @Validated(Marker.OnCreate.class) - public UserDto addNewUser(@Valid @RequestBody UserDto userDto) { + public UserDto addNewUser(@RequestBody UserDto userDto) { return userService.saveUser(userDto); } @PatchMapping("/{userId}") public UserDto updateUser( @PathVariable long userId, - @Valid @RequestBody UserDto userDto + @RequestBody UserDto userDto ) { return userService.updateUser(userId, userDto); } diff --git a/server/src/main/java/ru/practicum/shareit/user/dto/UserDto.java b/server/src/main/java/ru/practicum/shareit/user/dto/UserDto.java new file mode 100644 index 0000000..f378fa0 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/dto/UserDto.java @@ -0,0 +1,17 @@ +package ru.practicum.shareit.user.dto; + +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +@Data +@Builder +@FieldDefaults(makeFinal = true) +public class UserDto { + + Long id; + + String name; + + String email; +} diff --git a/src/main/java/ru/practicum/shareit/user/dto/UserMapper.java b/server/src/main/java/ru/practicum/shareit/user/dto/UserMapper.java similarity index 100% rename from src/main/java/ru/practicum/shareit/user/dto/UserMapper.java rename to server/src/main/java/ru/practicum/shareit/user/dto/UserMapper.java diff --git a/src/main/java/ru/practicum/shareit/user/model/User.java b/server/src/main/java/ru/practicum/shareit/user/model/User.java similarity index 100% rename from src/main/java/ru/practicum/shareit/user/model/User.java rename to server/src/main/java/ru/practicum/shareit/user/model/User.java diff --git a/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java b/server/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java similarity index 100% rename from src/main/java/ru/practicum/shareit/user/repository/UserRepository.java rename to server/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java diff --git a/src/main/java/ru/practicum/shareit/user/service/UserService.java b/server/src/main/java/ru/practicum/shareit/user/service/UserService.java similarity index 100% rename from src/main/java/ru/practicum/shareit/user/service/UserService.java rename to server/src/main/java/ru/practicum/shareit/user/service/UserService.java diff --git a/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java b/server/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java similarity index 100% rename from src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java rename to server/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java diff --git a/src/main/java/ru/practicum/shareit/util/UtilService.java b/server/src/main/java/ru/practicum/shareit/util/UtilService.java similarity index 100% rename from src/main/java/ru/practicum/shareit/util/UtilService.java rename to server/src/main/java/ru/practicum/shareit/util/UtilService.java diff --git a/src/main/java/ru/practicum/shareit/util/config/ShateItConfiguration.java b/server/src/main/java/ru/practicum/shareit/util/config/ShateItConfiguration.java similarity index 100% rename from src/main/java/ru/practicum/shareit/util/config/ShateItConfiguration.java rename to server/src/main/java/ru/practicum/shareit/util/config/ShateItConfiguration.java diff --git a/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java b/server/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java similarity index 98% rename from src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java rename to server/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java index 19b0c1a..3c2fbae 100644 --- a/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java +++ b/server/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java @@ -1,6 +1,5 @@ package ru.practicum.shareit.util.error; -import jakarta.validation.ValidationException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; diff --git a/src/main/java/ru/practicum/shareit/util/error/ErrorResponse.java b/server/src/main/java/ru/practicum/shareit/util/error/ErrorResponse.java similarity index 100% rename from src/main/java/ru/practicum/shareit/util/error/ErrorResponse.java rename to server/src/main/java/ru/practicum/shareit/util/error/ErrorResponse.java diff --git a/src/main/java/ru/practicum/shareit/util/exception/BookingStatusException.java b/server/src/main/java/ru/practicum/shareit/util/exception/BookingStatusException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/util/exception/BookingStatusException.java rename to server/src/main/java/ru/practicum/shareit/util/exception/BookingStatusException.java diff --git a/src/main/java/ru/practicum/shareit/util/exception/ConditionsNotMetException.java b/server/src/main/java/ru/practicum/shareit/util/exception/ConditionsNotMetException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/util/exception/ConditionsNotMetException.java rename to server/src/main/java/ru/practicum/shareit/util/exception/ConditionsNotMetException.java diff --git a/src/main/java/ru/practicum/shareit/util/exception/DuplicatedDataException.java b/server/src/main/java/ru/practicum/shareit/util/exception/DuplicatedDataException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/util/exception/DuplicatedDataException.java rename to server/src/main/java/ru/practicum/shareit/util/exception/DuplicatedDataException.java diff --git a/src/main/java/ru/practicum/shareit/util/exception/ForbiddenException.java b/server/src/main/java/ru/practicum/shareit/util/exception/ForbiddenException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/util/exception/ForbiddenException.java rename to server/src/main/java/ru/practicum/shareit/util/exception/ForbiddenException.java diff --git a/src/main/java/ru/practicum/shareit/util/exception/MethodNotImplementedException.java b/server/src/main/java/ru/practicum/shareit/util/exception/MethodNotImplementedException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/util/exception/MethodNotImplementedException.java rename to server/src/main/java/ru/practicum/shareit/util/exception/MethodNotImplementedException.java diff --git a/src/main/java/ru/practicum/shareit/util/exception/NotFoundException.java b/server/src/main/java/ru/practicum/shareit/util/exception/NotFoundException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/util/exception/NotFoundException.java rename to server/src/main/java/ru/practicum/shareit/util/exception/NotFoundException.java diff --git a/src/main/java/ru/practicum/shareit/util/exception/ParameterNotValidException.java b/server/src/main/java/ru/practicum/shareit/util/exception/ParameterNotValidException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/util/exception/ParameterNotValidException.java rename to server/src/main/java/ru/practicum/shareit/util/exception/ParameterNotValidException.java diff --git a/server/src/main/java/ru/practicum/shareit/util/exception/ValidationException.java b/server/src/main/java/ru/practicum/shareit/util/exception/ValidationException.java new file mode 100644 index 0000000..74e5d38 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/util/exception/ValidationException.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.util.exception; + +public class ValidationException extends RuntimeException { + public ValidationException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCode.java b/server/src/main/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCode.java similarity index 100% rename from src/main/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCode.java rename to server/src/main/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCode.java diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties new file mode 100644 index 0000000..8a85b26 --- /dev/null +++ b/server/src/main/resources/application.properties @@ -0,0 +1,22 @@ +server.port=9090 + +spring.data.jdbc.dialect=POSTGRESQL +spring.sql.init.mode=ALWAYS +hibernate.jdbc.time-zone=UTC +hibernate.default-schema=public +spring.jpa.hibernate.ddl-auto=none +spring.jpa.properties.hibernate.format_sql=true + +#--- +spring.datasource.driver-class-name=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://localhost:6541/shareit +spring.datasource.username=dbuser +spring.datasource.password=12345 +#spring.datasource.username=shareit +#spring.datasource.password=shareit +#--- +#spring.config.activate.on-profile=test +#spring.datasource.driverClassName=org.h2.Driver +#spring.datasource.url=jdbc:h2:mem:shareit +#spring.datasource.username=shareit +#spring.datasource.password=shareit \ No newline at end of file diff --git a/src/main/resources/schema.sql b/server/src/main/resources/schema.sql similarity index 100% rename from src/main/resources/schema.sql rename to server/src/main/resources/schema.sql diff --git a/server/suppressions.xml b/server/suppressions.xml new file mode 100644 index 0000000..b2c6822 --- /dev/null +++ b/server/suppressions.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java b/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java deleted file mode 100644 index c4ab40e..0000000 --- a/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java +++ /dev/null @@ -1,25 +0,0 @@ -package ru.practicum.shareit.booking.dto; - -import jakarta.validation.constraints.*; -import lombok.Data; -import lombok.NoArgsConstructor; -import ru.practicum.shareit.util.validate.Marker; - -import java.time.LocalDateTime; - -@Data -@NotNull -@NoArgsConstructor -public class IncomingBookingDto { - - @Positive(message = "id вещи - обязательный параметр, должен быть положительным числом") - long itemId; - - @FutureOrPresent(message = "Дата начала не может быть в прошлом") - @NotNull(groups = Marker.OnCreate.class, message = "Дата начала бронирования - обязательна") - LocalDateTime start; - - @Future(message = "Дата окончания должна быть в будущем") - @NotNull(groups = Marker.OnCreate.class, message = "Дата окончания бронирования - обязательна") - LocalDateTime end; -} diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java deleted file mode 100644 index a272c75..0000000 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java +++ /dev/null @@ -1,32 +0,0 @@ -package ru.practicum.shareit.item.dto; - -import jakarta.validation.constraints.*; -import lombok.Builder; -import lombok.Data; -import lombok.experimental.FieldDefaults; -import ru.practicum.shareit.util.validate.Marker; - -/** - * TODO Sprint add-controllers. - */ -@Data -@NotNull -@Builder -@FieldDefaults(makeFinal = true) -public class ItemDto { - - Long id; - - @NotBlank(groups = Marker.OnCreate.class) - @Size(min = 1, max = 255) - String name; - - @NotBlank(groups = Marker.OnCreate.class) - @Size(min = 1, max = 255) - String description; - - @NotNull(groups = Marker.OnCreate.class) - Boolean available; - - Long requestId; -} diff --git a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java b/src/main/java/ru/practicum/shareit/user/dto/UserDto.java deleted file mode 100644 index 8d94d28..0000000 --- a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java +++ /dev/null @@ -1,25 +0,0 @@ -package ru.practicum.shareit.user.dto; - -import jakarta.validation.constraints.*; -import lombok.Builder; -import lombok.Data; -import lombok.experimental.FieldDefaults; -import ru.practicum.shareit.util.validate.Marker; - -@Data -@NotNull -@Builder -@FieldDefaults(makeFinal = true) -public class UserDto { - - Long id; - - @NotBlank(groups = Marker.OnCreate.class) - @Size(min = 1, max = 255) - String name; - - @NotBlank(groups = Marker.OnCreate.class) - @Size(min = 1, max = 512) - @Email - String email; -} diff --git a/src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithTolerance.java b/src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithTolerance.java deleted file mode 100644 index 79857d6..0000000 --- a/src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithTolerance.java +++ /dev/null @@ -1,26 +0,0 @@ -package ru.practicum.shareit.util.datetime; - -import jakarta.validation.Constraint; -import jakarta.validation.Payload; - -import java.lang.annotation.*; - -/// Generated by GPT -@Documented -@Constraint(validatedBy = FutureOrPresentWithToleranceValidator.class) -@Target({ ElementType.FIELD, ElementType.PARAMETER }) -@Retention(RetentionPolicy.RUNTIME) -public @interface FutureOrPresentWithTolerance { - - String message() default "must be in the present or future (with tolerance)"; - - Class[] groups() default {}; - - Class[] payload() default {}; - - long toleranceMillis() default 0; - - String zone() default ""; - - TimeRoundingStrategy rounding() default TimeRoundingStrategy.NONE; -} diff --git a/src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithToleranceValidator.java b/src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithToleranceValidator.java deleted file mode 100644 index e4147a6..0000000 --- a/src/main/java/ru/practicum/shareit/util/datetime/FutureOrPresentWithToleranceValidator.java +++ /dev/null @@ -1,96 +0,0 @@ -package ru.practicum.shareit.util.datetime; - -import jakarta.validation.ConstraintValidator; -import jakarta.validation.ConstraintValidatorContext; - -import java.time.*; - -/// Generated by GPT -public class FutureOrPresentWithToleranceValidator - implements ConstraintValidator { - - long toleranceMillis; - String zoneIdStr; - TimeRoundingStrategy roundingStrategy; - - @Override - public void initialize(FutureOrPresentWithTolerance constraintAnnotation) { - this.toleranceMillis = constraintAnnotation.toleranceMillis(); - this.zoneIdStr = constraintAnnotation.zone(); - this.roundingStrategy = constraintAnnotation.rounding(); - } - - @Override - public boolean isValid(Object value, ConstraintValidatorContext context) { - if (value == null) { - return true; - } - - Clock clock = context.getClockProvider().getClock(); - - ZoneId zone = resolveZone(clock); - - Instant now = applyRounding(clock.instant()); - Instant valueInstant = toInstant(value, zone); - - if (valueInstant == null) { - return false; - } - - valueInstant = applyRounding(valueInstant); - - return valueInstant.toEpochMilli() >= (now.toEpochMilli() - toleranceMillis); - } - - private ZoneId resolveZone(Clock clock) { - if (zoneIdStr == null || zoneIdStr.isBlank()) { - return clock.getZone(); - } - return ZoneId.of(zoneIdStr); - } - - private Instant toInstant(Object value, ZoneId zone) { - - if (value instanceof Instant) { - return (Instant) value; - } - - if (value instanceof OffsetDateTime) { - return ((OffsetDateTime) value).toInstant(); - } - - if (value instanceof ZonedDateTime) { - return ((ZonedDateTime) value).toInstant(); - } - - if (value instanceof LocalDateTime) { - return ((LocalDateTime) value).atZone(zone).toInstant(); - } - - if (value instanceof LocalDate) { - return ((LocalDate) value).atStartOfDay(zone).toInstant(); - } - - return null; - } - - private Instant applyRounding(Instant instant) { - - switch (roundingStrategy) { - - case TRUNCATE_TO_SECONDS: - return instant.truncatedTo(java.time.temporal.ChronoUnit.SECONDS); - - case TRUNCATE_TO_MINUTES: - return instant.truncatedTo(java.time.temporal.ChronoUnit.MINUTES); - - case ROUND_TO_SECONDS: - long millis = instant.toEpochMilli(); - long rounded = Math.round(millis / 1000.0) * 1000; - return Instant.ofEpochMilli(rounded); - - default: - return instant; - } - } -} diff --git a/src/main/java/ru/practicum/shareit/util/datetime/TimeRoundingStrategy.java b/src/main/java/ru/practicum/shareit/util/datetime/TimeRoundingStrategy.java deleted file mode 100644 index 6ef8f41..0000000 --- a/src/main/java/ru/practicum/shareit/util/datetime/TimeRoundingStrategy.java +++ /dev/null @@ -1,9 +0,0 @@ -package ru.practicum.shareit.util.datetime; - -public enum TimeRoundingStrategy { - - NONE, // без округления - TRUNCATE_TO_SECONDS, // обрезать до секунд - TRUNCATE_TO_MINUTES, // обрезать до минут - ROUND_TO_SECONDS // математическое округление -} diff --git a/src/main/java/ru/practicum/shareit/util/validate/Marker.java b/src/main/java/ru/practicum/shareit/util/validate/Marker.java deleted file mode 100644 index 112f99b..0000000 --- a/src/main/java/ru/practicum/shareit/util/validate/Marker.java +++ /dev/null @@ -1,5 +0,0 @@ -package ru.practicum.shareit.util.validate; - -public interface Marker { - interface OnCreate {} -} diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties deleted file mode 100644 index 7095f54..0000000 --- a/src/main/resources/application-test.properties +++ /dev/null @@ -1,12 +0,0 @@ -spring.jpa.hibernate.ddl-auto=none -spring.jpa.properties.hibernate.format_sql=true -spring.sql.init.mode=always -logging.level.org.springframework.orm.jpa=INFO -logging.level.org.springframework.transaction=INFO -logging.level.org.springframework.transaction.interceptor=TRACE -logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG - -spring.datasource.driver-class-name=org.h2.Driver -spring.datasource.url=jdbc:h2:file:./db/filmorate -spring.datasource.username=password -spring.datasource.password=sa diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index cbed0d5..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,22 +0,0 @@ -spring.main.banner-mode=off -spring.main.log-startup-info=false -spring.jackson.serialization.indent_output=true -spring.sql.init.mode=always -spring.jpa.hibernate.ddl-auto=validate -spring.jpa.properties.hibernate.format_sql=true - -spring.datasource.driver-class-name=org.postgresql.Driver -spring.datasource.url=jdbc:postgresql://localhost:5432/shareit -spring.datasource.username=dbuser -spring.datasource.password=12345 - -hibernate.jdbc.time-zone=UTC -hibernate.default-schema=public -hibernate.show-sql=true -hibernate.format-sql=true - -logging.level.root=error -logging.level.org.springframework.boot.web=info -logging.level.org.hibernate.SQL=DEBUG -logging.level.org.hibernate.orm.jdbc.bind=trace -logging.level.org.zalando.logbook=TRACE diff --git a/src/test/java/ru/practicum/shareit/ShareItTests.java b/src/test/java/ru/practicum/shareit/ShareItTests.java deleted file mode 100644 index 4d79052..0000000 --- a/src/test/java/ru/practicum/shareit/ShareItTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.practicum.shareit; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ShareItTests { - - @Test - void contextLoads() { - } - -} From 97dd6d110f87da4613bd185bed88baabbe979eaf Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Fri, 10 Apr 2026 21:10:05 +0300 Subject: [PATCH 03/28] =?UTF-8?q?=D0=A2=D0=B5=D1=81=D1=82=D1=8B=20Postman?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B9=D0=B4=D0=B5=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 26 +++-- .../shareit/booking/BookingClient.java | 76 ++++++++------- .../shareit/booking/BookingController.java | 55 +++++++---- .../booking/dto/BookItemRequestDto.java | 20 ---- .../shareit/booking/dto/GBookingDto.java | 22 +++++ .../practicum/shareit/client/BaseClient.java | 10 +- .../shareit/home/HomeController.java | 43 +++++++++ .../ru/practicum/shareit/item/ItemClient.java | 67 +++++++++++++ .../shareit/item/ItemController.java | 96 +++++++++++++++++++ .../shareit/item/dto/GCommentDto.java | 13 +++ .../practicum/shareit/item/dto/GItemDto.java | 21 ++++ .../shareit/request/ItemRequestClient.java | 54 +++++++++++ .../request/ItemRequestController.java | 54 +++++++++++ .../shareit/request/dto/GItemRequestDto.java | 9 ++ .../ru/practicum/shareit/user/UserClient.java | 53 ++++++++++ .../shareit/user/UserController.java | 63 ++++++++++++ .../practicum/shareit/user/dto/GUserDto.java | 17 ++++ .../practicum/shareit/util/ErrorResponse.java | 4 + .../ru/practicum/shareit/util/Marker.java | 5 + .../util/ShareItGatewayExceptionHandler.java | 49 ++++++++++ .../config/ShateItGatewayConfiguration.java | 27 ++++++ gateway/src/main/resources/banner.txt | 5 + gateway/src/test/java/ShareitGatewayTest.java | 13 +++ .../shareit/user/service/UserServiceImpl.java | 2 +- .../src/main/resources/application.properties | 27 +++--- server/src/main/resources/schema.sql | 10 +- .../src/test/java/user/UserServiceTest.java | 35 +++++++ 27 files changed, 771 insertions(+), 105 deletions(-) delete mode 100644 gateway/src/main/java/ru/practicum/shareit/booking/dto/BookItemRequestDto.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/booking/dto/GBookingDto.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/home/HomeController.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/item/ItemClient.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/item/ItemController.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/item/dto/GCommentDto.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/item/dto/GItemDto.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/request/ItemRequestClient.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/request/ItemRequestController.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/request/dto/GItemRequestDto.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/user/UserClient.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/user/UserController.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/user/dto/GUserDto.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/util/ErrorResponse.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/util/Marker.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/util/ShareItGatewayExceptionHandler.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/util/config/ShateItGatewayConfiguration.java create mode 100644 gateway/src/main/resources/banner.txt create mode 100644 gateway/src/test/java/ShareitGatewayTest.java create mode 100644 server/src/test/java/user/UserServiceTest.java diff --git a/docker-compose.yml b/docker-compose.yml index 1b3607c..d864fe1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,8 @@ services: gateway: - build: gateway + build: + context: ./gateway + dockerfile: Dockerfile image: shareit-gateway container_name: shareit-gateway ports: @@ -11,7 +13,9 @@ services: - SHAREIT_SERVER_URL=http://server:9090 server: - build: server + build: + context: ./server + dockerfile: Dockerfile image: shareit-server container_name: shareit-server ports: @@ -20,20 +24,24 @@ services: - db environment: - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/shareit - - SPRING_DATASOURCE_USERNAME=dbuser - - SPRING_DATASOURCE_PASSWORD=12345 + - SPRING_DATASOURCE_USERNAME=shareit + - SPRING_DATASOURCE_PASSWORD=shareit db: image: postgres:16.1 container_name: postgres - ports: - - "6541:5432" +# ports: +# - "6541:5432" environment: - POSTGRES_DB=shareit - - POSTGRES_USER=dbuser - - POSTGRES_PASSWORD=12345 + - POSTGRES_USER=shareit + - POSTGRES_PASSWORD=shareit healthcheck: test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER timeout: 5s interval: 5s - retries: 10 \ No newline at end of file + retries: 10 + + # sudo docker compose down --rmi all --volumes --remove-orphans + # sudo docker compose build --no-cache gateway server + # sudo docker compose up -d --force-recreate diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/BookingClient.java b/gateway/src/main/java/ru/practicum/shareit/booking/BookingClient.java index 438b060..f47e7fc 100644 --- a/gateway/src/main/java/ru/practicum/shareit/booking/BookingClient.java +++ b/gateway/src/main/java/ru/practicum/shareit/booking/BookingClient.java @@ -1,7 +1,5 @@ package ru.practicum.shareit.booking; -import java.util.Map; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.client.RestTemplateBuilder; @@ -9,39 +7,53 @@ import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.stereotype.Service; import org.springframework.web.util.DefaultUriBuilderFactory; - -import ru.practicum.shareit.booking.dto.BookItemRequestDto; +import ru.practicum.shareit.booking.dto.GBookingDto; import ru.practicum.shareit.booking.dto.BookingState; import ru.practicum.shareit.client.BaseClient; +import java.util.Map; + @Service public class BookingClient extends BaseClient { - private static final String API_PREFIX = "/bookings"; - - @Autowired - public BookingClient(@Value("${shareit-server.url}") String serverUrl, RestTemplateBuilder builder) { - super( - builder - .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + API_PREFIX)) - .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) - .build() - ); - } - - public ResponseEntity getBookings(long userId, BookingState state, Integer from, Integer size) { - Map parameters = Map.of( - "state", state.name(), - "from", from, - "size", size - ); - return get("?state={state}&from={from}&size={size}", userId, parameters); - } - - public ResponseEntity bookItem(long userId, BookItemRequestDto requestDto) { - return post("", userId, requestDto); - } - - public ResponseEntity getBooking(long userId, Long bookingId) { - return get("/" + bookingId, userId); - } + private static final String API_PREFIX = "/bookings"; + + @Autowired + public BookingClient(@Value("${shareit-server.url}") String serverUrl, RestTemplateBuilder builder) { + super( + builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + API_PREFIX)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build() + ); + } + + public ResponseEntity getBookings(long userId, BookingState state, Integer from, Integer size) { + Map parameters = Map.of( + "state", state.name(), + "from", from, + "size", size + ); + return get("?state={state}&from={from}&size={size}", userId, parameters); + } + + public ResponseEntity bookItem(long userId, GBookingDto requestDto) { + return post("", userId, requestDto); + } + + public ResponseEntity getBooking(long userId, Long bookingId) { + return get("/" + bookingId, userId); + } + + public ResponseEntity getBookingsByOwner(long userId, BookingState state, Integer from, Integer size) { + Map parameters = Map.of( + "state", state.name(), + "from", from, + "size", size + ); + return get("/owner?state={state}&from={from}&size={size}", userId, parameters); + } + + public ResponseEntity approveBooking(long userId, long bookingId, boolean approved) { + return patch("/" + bookingId + "?approved=" + approved, userId); + } } diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java b/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java index bceb75f..4f31c29 100644 --- a/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -1,23 +1,17 @@ package ru.practicum.shareit.booking; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; - import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.PositiveOrZero; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import ru.practicum.shareit.booking.dto.BookItemRequestDto; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.booking.dto.GBookingDto; import ru.practicum.shareit.booking.dto.BookingState; +import ru.practicum.shareit.util.Marker; @Controller @RequestMapping(path = "/bookings") @@ -28,7 +22,7 @@ public class BookingController { private final BookingClient bookingClient; @GetMapping - public ResponseEntity getBookings( + public ResponseEntity getBookingsByBooker( @RequestHeader("X-Sharer-User-Id") long userId, @RequestParam(name = "state", defaultValue = "all") String stateParam, @PositiveOrZero @RequestParam(name = "from", defaultValue = "0") Integer from, @@ -39,19 +33,38 @@ public ResponseEntity getBookings( return bookingClient.getBookings(userId, state, from, size); } - @PostMapping - public ResponseEntity bookItem( + @GetMapping("/owner") + public ResponseEntity getAllBookingsByOwner( @RequestHeader("X-Sharer-User-Id") long userId, - @RequestBody @Valid BookItemRequestDto requestDto) { - log.info("Creating booking {}, userId={}", requestDto, userId); - return bookingClient.bookItem(userId, requestDto); + @RequestParam(name = "state", defaultValue = "all") String stateParam, + @PositiveOrZero @RequestParam(name = "from", defaultValue = "0") Integer from, + @Positive @RequestParam(name = "size", defaultValue = "10") Integer size) { + BookingState state = BookingState.from(stateParam) + .orElseThrow(() -> new IllegalArgumentException("Unknown state: " + stateParam)); + log.info("Get booking by Owner with state {}, userId={}, from={}, size={}", stateParam, userId, from, size); + return bookingClient.getBookingsByOwner(userId, state, from, size); } @GetMapping("/{bookingId}") - public ResponseEntity getBooking( - @RequestHeader("X-Sharer-User-Id") long userId, - @PathVariable Long bookingId) { + public ResponseEntity getBookingById(@RequestHeader("X-Sharer-User-Id") long userId, + @PathVariable Long bookingId) { log.info("Get booking {}, userId={}", bookingId, userId); return bookingClient.getBooking(userId, bookingId); } + + @PostMapping + @Validated(Marker.OnCreate.class) + public ResponseEntity bookItem(@RequestHeader("X-Sharer-User-Id") long userId, + @RequestBody @Valid GBookingDto requestDto) { + log.info("Creating booking {}, userId={}", requestDto.toString(), userId); + return bookingClient.bookItem(userId, requestDto); + } + + @PatchMapping("/{bookingId}") + public ResponseEntity approveBooking(@RequestHeader("X-Sharer-User-Id") long userId, + @PathVariable long bookingId, + @RequestParam boolean approved) { + log.info("Approve booking {}, userId={}", bookingId, userId); + return bookingClient.approveBooking(userId, bookingId, approved); + } } diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookItemRequestDto.java b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookItemRequestDto.java deleted file mode 100644 index 738f5c0..0000000 --- a/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookItemRequestDto.java +++ /dev/null @@ -1,20 +0,0 @@ -package ru.practicum.shareit.booking.dto; - -import java.time.LocalDateTime; - -import jakarta.validation.constraints.Future; -import jakarta.validation.constraints.FutureOrPresent; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -@AllArgsConstructor -public class BookItemRequestDto { - private long itemId; - @FutureOrPresent - private LocalDateTime start; - @Future - private LocalDateTime end; -} diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/dto/GBookingDto.java b/gateway/src/main/java/ru/practicum/shareit/booking/dto/GBookingDto.java new file mode 100644 index 0000000..545ffde --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/dto/GBookingDto.java @@ -0,0 +1,22 @@ +package ru.practicum.shareit.booking.dto; + +import java.time.LocalDateTime; + +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotNull; +import ru.practicum.shareit.util.Marker; + +public record GBookingDto( + + @NotNull(groups = Marker.OnCreate.class) + long itemId, + + @FutureOrPresent + @NotNull(groups = Marker.OnCreate.class) + LocalDateTime start, + + @Future + @NotNull(groups = Marker.OnCreate.class) + LocalDateTime end +) {} diff --git a/gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java b/gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java index 5dc82ad..a336826 100644 --- a/gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java +++ b/gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java @@ -34,12 +34,12 @@ protected ResponseEntity post(String path, T body) { return post(path, null, null, body); } - protected ResponseEntity post(String path, long userId, T body) { + protected ResponseEntity post(String path, @Nullable Long userId, T body) { return post(path, userId, null, body); } protected ResponseEntity post(String path, - Long userId, + @Nullable Long userId, @Nullable Map parameters, T body) { return makeAndSendRequest(HttpMethod.POST, path, userId, parameters, body); @@ -64,12 +64,12 @@ protected ResponseEntity patch(String path, long userId) { return patch(path, userId, null, null); } - protected ResponseEntity patch(String path, long userId, T body) { + protected ResponseEntity patch(String path, @Nullable Long userId, T body) { return patch(path, userId, null, body); } protected ResponseEntity patch(String path, - Long userId, + @Nullable Long userId, @Nullable Map parameters, T body) { return makeAndSendRequest(HttpMethod.PATCH, path, userId, parameters, body); @@ -91,7 +91,7 @@ protected ResponseEntity delete(String path, private ResponseEntity makeAndSendRequest(HttpMethod method, String path, - Long userId, + @Nullable Long userId, @Nullable Map parameters, @Nullable T body) { HttpEntity requestEntity = new HttpEntity<>(body, defaultHeaders(userId)); diff --git a/gateway/src/main/java/ru/practicum/shareit/home/HomeController.java b/gateway/src/main/java/ru/practicum/shareit/home/HomeController.java new file mode 100644 index 0000000..41df18e --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/home/HomeController.java @@ -0,0 +1,43 @@ +package ru.practicum.shareit.home; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(path = "/") +public class HomeController { + @GetMapping + public String home() { + return """ +


+
+					___________________¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶
+					_________________¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶
+					__¶_¶__________¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶______¶__¶__¶
+					__¶_¶__¶______¶¶¶¶¶¶____¶¶¶¶¶¶____¶¶¶¶¶¶______¶_¶_¶
+					¶_¶_¶_¶______¶¶¶¶¶¶______¶¶¶¶______¶¶¶¶¶¶¶____¶¶¶¶¶
+					_¶¶¶¶¶______¶¶¶¶¶¶¶___O_¶¶¶¶¶__O__¶¶¶¶¶¶¶¶_¶¶¶¶¶¶¶
+					_¶¶¶¶¶¶¶¶__¶¶¶¶¶¶¶¶¶____¶¶¶¶¶¶____¶¶¶¶¶¶¶¶¶¶___¶¶¶
+					____¶¶____¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶____¶¶
+					____¶_____¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶__¶¶___¶
+					_____¶¶¶¶¶¶¶¶__¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶__¶¶¶¶¶¶
+					__________¶¶¶__¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶__¶¶¶
+					___________¶¶¶_____¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶_____¶¶
+					____________¶¶________¶¶¶¶¶¶¶¶¶¶_______¶¶
+					_____________¶¶¶_______________________¶
+					_______________¶¶____¶¶¶¶¶¶¶¶¶¶¶______¶
+					________________¶¶¶_____¶¶¶¶¶¶¶¶¶¶¶__¶
+					___________________¶¶¶_____¶¶¶¶¶¶¶¶¶¶
+					______________________¶¶¶¶¶__¶¶¶¶¶¶¶¶¶
+					_____________________________¶¶¶¶¶¶¶¶¶¶
+					______________________________¶¶¶¶¶¶¶¶¶
+						      _                    _ _  \s
+						  ___| |__   __ _ _ __ ___(_) |_\s
+						 / __| '_ \\ / _` | '__/ _ \\ | __|
+						 \\__ \\ | | | (_| | | |  __/ | |_\s
+						 |___/_| |_|\\__,_|_|  \\___|_|\\__|
+				
+ """; + } +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/item/ItemClient.java b/gateway/src/main/java/ru/practicum/shareit/item/ItemClient.java new file mode 100644 index 0000000..69adf6c --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/ItemClient.java @@ -0,0 +1,67 @@ +package ru.practicum.shareit.item; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.util.DefaultUriBuilderFactory; +import ru.practicum.shareit.client.BaseClient; +import ru.practicum.shareit.item.dto.GCommentDto; +import ru.practicum.shareit.item.dto.GItemDto; + +import java.util.Map; + +@Service +public class ItemClient extends BaseClient { + + private static final String API_PREFIX = "/items"; + + @Autowired + public ItemClient(@Value("${shareit-server.url}") String serverUrl, RestTemplateBuilder builder) { + super( + builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + API_PREFIX)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build() + ); + } + + public ResponseEntity getItemById(long userId, long itemId) { + return get("/" + itemId, userId); + } + + public ResponseEntity addNewItem(long userId, GItemDto item) { + return post("", userId, item); + } + + public ResponseEntity getAllItemsOfUser(long userId, Integer from, Integer size) { + Map parameters = Map.of( + "from", from, + "size", size + ); + return get("?from={from}&size={size}", userId, parameters); + } + + public ResponseEntity search(long userId, String text, Integer from, Integer size) { + Map parameters = Map.of( + "text", text, + "from", from, + "size", size + ); + return get("/search?text={text}&from={from}&size={size}", userId, parameters); + } + + public ResponseEntity updateItem(long userId, long itemId, GItemDto itemDto) { + return patch("/" + itemId, userId, itemDto); + } + + public ResponseEntity deleteItem(long userId, long itemId) { + return delete("/" + itemId, userId); + } + + public ResponseEntity addComment(long userId, long itemId, GCommentDto commentDto) { + return post("/" + itemId + "/comment", userId, commentDto); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/item/ItemController.java b/gateway/src/main/java/ru/practicum/shareit/item/ItemController.java new file mode 100644 index 0000000..ec9d364 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -0,0 +1,96 @@ +package ru.practicum.shareit.item; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.item.dto.GCommentDto; +import ru.practicum.shareit.item.dto.GItemDto; +import ru.practicum.shareit.util.Marker; + +@Slf4j +@Validated +@Controller +@RequiredArgsConstructor +@RequestMapping(path = "/items") +public class ItemController { + + final ItemClient itemClient; + + @GetMapping("/{itemId}") + public ResponseEntity getItemById(@RequestHeader("X-Sharer-User-Id") long userId, + @PathVariable long itemId + ) { + log.info("Get item by id={} for user with id={}", itemId, userId); + return itemClient.getItemById(userId, itemId); + } + + @GetMapping + public ResponseEntity getAllItemsOfUser( + @RequestHeader("X-Sharer-User-Id") long userId, + @PositiveOrZero @RequestParam(name = "from", defaultValue = "0") Integer from, + @Positive @RequestParam(name = "size", defaultValue = "10") Integer size + ) { + log.info("Get all items for user with id={}", userId); + return itemClient.getAllItemsOfUser(userId, from, size); + } + + @GetMapping("/search") + public ResponseEntity search( + @RequestHeader("X-Sharer-User-Id") long userId, + @RequestParam String text, + @PositiveOrZero @RequestParam(name = "from", defaultValue = "0") Integer from, + @Positive @RequestParam(name = "size", defaultValue = "10") Integer size + ) { + return itemClient.search(userId, text, from, size); + } + + @PostMapping + @Validated(Marker.OnCreate.class) + public ResponseEntity addNewItem( + @RequestHeader("X-Sharer-User-Id") long userId, + @Valid @RequestBody GItemDto item + ) { + log.info("Add new item for user with id={}", userId); + return itemClient.addNewItem(userId, item); + } + + @PostMapping("/{itemId}/comment") + public ResponseEntity addComment( + @RequestHeader("X-Sharer-User-Id") long userId, + @PathVariable long itemId, + @RequestBody GCommentDto commentDto + ) { + log.info("Add new comment for item with id={} for user with id={}", itemId, userId); + return itemClient.addComment(userId, itemId, commentDto); + } + + @PatchMapping("/{itemId}") + public ResponseEntity updateItem( + @RequestHeader("X-Sharer-User-Id") long userId, + @PathVariable long itemId, + @RequestBody GItemDto itemDto + ) { + log.info("Update item with id={} for user with id={}", itemId, userId); + return itemClient.updateItem(userId, itemId, itemDto); + } + + @DeleteMapping("/{itemId}") + public ResponseEntity deleteItem( + @RequestHeader("X-Sharer-User-Id") long userId, + @PathVariable(name = "itemId") long itemId + ) { + log.info("Delete item with id={} for user with id={}", itemId, userId); + ResponseEntity response = itemClient.deleteItem(userId, itemId); + if (response.getStatusCode().is2xxSuccessful()) { + return new ResponseEntity<>("", HttpStatus.OK); + } + return response; + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/item/dto/GCommentDto.java b/gateway/src/main/java/ru/practicum/shareit/item/dto/GCommentDto.java new file mode 100644 index 0000000..5fb34bd --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/dto/GCommentDto.java @@ -0,0 +1,13 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.constraints.NotNull; +import ru.practicum.shareit.util.Marker; + +public record GCommentDto( + + @NotNull(groups = Marker.OnCreate.class) + Long itemId, + + @NotNull + String text +) {} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/item/dto/GItemDto.java b/gateway/src/main/java/ru/practicum/shareit/item/dto/GItemDto.java new file mode 100644 index 0000000..c4521a0 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/dto/GItemDto.java @@ -0,0 +1,21 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import ru.practicum.shareit.util.Marker; + +public record GItemDto( + + @NotNull(groups = Marker.OnCreate.class) + @Size(min = 1, max = 255) + String name, + + @NotNull(groups = Marker.OnCreate.class) + @Size(max = 500) + String description, + + @NotNull(groups = Marker.OnCreate.class) + Boolean available, + + Long requestId +) {} diff --git a/gateway/src/main/java/ru/practicum/shareit/request/ItemRequestClient.java b/gateway/src/main/java/ru/practicum/shareit/request/ItemRequestClient.java new file mode 100644 index 0000000..ba62411 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/request/ItemRequestClient.java @@ -0,0 +1,54 @@ +package ru.practicum.shareit.request; + +import jakarta.annotation.Nonnull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.util.DefaultUriBuilderFactory; +import ru.practicum.shareit.client.BaseClient; +import ru.practicum.shareit.request.dto.GItemRequestDto; + +import java.util.Map; + +@Service +public class ItemRequestClient extends BaseClient { + + private static final String API_PREFIX = "/requests"; + + @Autowired + public ItemRequestClient(@Value("${shareit-server.url}") String serverUrl, @Nonnull RestTemplateBuilder builder) { + super( + builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + API_PREFIX)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build() + ); + } + + public ResponseEntity addNewRequest(long userId, GItemRequestDto requestDto) { + return post("", userId, requestDto); + } + + public ResponseEntity getRequestListByRequestor(long requestorId, Integer from, Integer size) { + Map parameters = Map.of( + "from", from, + "size", size + ); + return get("?from={from}&size={size}", requestorId, parameters); + } + + public ResponseEntity getRequestsListForUser(long userId, Integer from, Integer size) { + Map parameters = Map.of( + "from", from, + "size", size + ); + return get("/all?from={from}&size={size}", userId, parameters); + } + + public ResponseEntity getRequestById(long requestId) { + return get("/" + requestId); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/request/ItemRequestController.java b/gateway/src/main/java/ru/practicum/shareit/request/ItemRequestController.java new file mode 100644 index 0000000..009cf1d --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/request/ItemRequestController.java @@ -0,0 +1,54 @@ +package ru.practicum.shareit.request; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.request.dto.GItemRequestDto; + +@Slf4j +@RestController +@RequestMapping(path = "/requests") +@RequiredArgsConstructor +public class ItemRequestController { + + final ItemRequestClient itemRequestClient; + + @PostMapping + public ResponseEntity addNewRequest( + @RequestHeader("X-Sharer-User-Id") long userId, + @RequestBody @Valid GItemRequestDto requestDto + ) { + log.info("Add new request for user with id={}", userId); + return itemRequestClient.addNewRequest(userId, requestDto); + } + + @GetMapping + public ResponseEntity getRequestListByRequestor( + @RequestHeader("X-Sharer-User-Id") long requestorId, + @PositiveOrZero @RequestParam(name = "from", defaultValue = "0") Integer from, + @Positive @RequestParam(name = "size", defaultValue = "10") Integer size + ) { + log.info("Get all requests by requestor with id={}", requestorId); + return itemRequestClient.getRequestListByRequestor(requestorId, from, size); + } + + @GetMapping("/all") + public ResponseEntity getRequestListForUser( + @RequestHeader("X-Sharer-User-Id") long userId, + @PositiveOrZero @RequestParam(name = "from", defaultValue = "0") Integer from, + @Positive @RequestParam(name = "size", defaultValue = "10") Integer size + ) { + log.info("Get all requests for user with id={}", userId); + return itemRequestClient.getRequestsListForUser(userId, from, size); + } + + @GetMapping("/{requestId}") + public ResponseEntity getRequestById(@PathVariable long requestId) { + log.info("Get request with id={}", requestId); + return itemRequestClient.getRequestById(requestId); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/request/dto/GItemRequestDto.java b/gateway/src/main/java/ru/practicum/shareit/request/dto/GItemRequestDto.java new file mode 100644 index 0000000..8bf30ab --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/request/dto/GItemRequestDto.java @@ -0,0 +1,9 @@ +package ru.practicum.shareit.request.dto; + +import jakarta.validation.constraints.NotNull; + +public record GItemRequestDto( + + @NotNull + String description +) {} diff --git a/gateway/src/main/java/ru/practicum/shareit/user/UserClient.java b/gateway/src/main/java/ru/practicum/shareit/user/UserClient.java new file mode 100644 index 0000000..88cbd29 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/user/UserClient.java @@ -0,0 +1,53 @@ +package ru.practicum.shareit.user; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.util.DefaultUriBuilderFactory; +import ru.practicum.shareit.client.BaseClient; +import ru.practicum.shareit.user.dto.GUserDto; + +import java.util.Map; + +@Service +public class UserClient extends BaseClient { + + private static final String API_PREFIX = "/users"; + + @Autowired + public UserClient(@Value("${shareit-server.url}") String serverUrl, RestTemplateBuilder builder) { + super( + builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + API_PREFIX)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build() + ); + } + + public ResponseEntity getUser(long userId) { + return get("/" + userId); + } + + public ResponseEntity getAllUsers(Integer from, Integer size) { + Map parameters = Map.of( + "from", from, + "size", size + ); + return get("?from={from}&size={size}", null, parameters); + } + + public ResponseEntity saveUser(GUserDto userDto) { + return post("", null, userDto); + } + + public ResponseEntity updateUser(long userId, GUserDto userDto) { + return patch("/" + userId, null, userDto); + } + + public ResponseEntity deleteUser(long userId) { + return delete("/" + userId); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/user/UserController.java b/gateway/src/main/java/ru/practicum/shareit/user/UserController.java new file mode 100644 index 0000000..f170867 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/user/UserController.java @@ -0,0 +1,63 @@ +package ru.practicum.shareit.user; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.user.dto.GUserDto; +import ru.practicum.shareit.util.Marker; + +@Slf4j +@Validated +@Controller +@RequiredArgsConstructor +@RequestMapping(path = "/users") +public class UserController { + + final UserClient userClient; + + @GetMapping("/{userId}") + public ResponseEntity getUserById(@PathVariable long userId) { + log.info("Get user by id={}", userId); + return userClient.getUser(userId); + } + + @GetMapping + public ResponseEntity getAllUsers( + @PositiveOrZero @RequestParam(name = "from", defaultValue = "0") Integer from, + @Positive @RequestParam(name = "size", defaultValue = "10") Integer size) { + log.info("Get all users"); + return userClient.getAllUsers(from, size); + } + + @PostMapping + @Validated(Marker.OnCreate.class) + public ResponseEntity addNewUser(@RequestBody @Valid GUserDto userDto) { + log.info("Add new user"); + return userClient.saveUser(userDto); + } + + @PatchMapping("/{userId}") + public ResponseEntity updateUser(@PathVariable long userId, + @RequestBody @Valid GUserDto userDto + ) { + log.info("Update user with id={}", userId); + return userClient.updateUser(userId, userDto); + } + + @DeleteMapping("/{userId}") + public ResponseEntity deleteUser(@PathVariable long userId) { + log.info("Delete user with id={}", userId); + ResponseEntity response = userClient.deleteUser(userId); + if (response.getStatusCode().is2xxSuccessful()) { + return new ResponseEntity<>("", HttpStatus.OK); + } + return response; + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/user/dto/GUserDto.java b/gateway/src/main/java/ru/practicum/shareit/user/dto/GUserDto.java new file mode 100644 index 0000000..94c6683 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/user/dto/GUserDto.java @@ -0,0 +1,17 @@ +package ru.practicum.shareit.user.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import ru.practicum.shareit.util.Marker; + +public record GUserDto( + + @NotNull(groups = Marker.OnCreate.class) + @Size(min = 1, max = 255) + String name, + + @Email @NotNull(groups = Marker.OnCreate.class) + @Size(min = 1, max = 255) + String email +) {} diff --git a/gateway/src/main/java/ru/practicum/shareit/util/ErrorResponse.java b/gateway/src/main/java/ru/practicum/shareit/util/ErrorResponse.java new file mode 100644 index 0000000..383f103 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/util/ErrorResponse.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.util; + +public record ErrorResponse(String error) { +} diff --git a/gateway/src/main/java/ru/practicum/shareit/util/Marker.java b/gateway/src/main/java/ru/practicum/shareit/util/Marker.java new file mode 100644 index 0000000..d9e48e5 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/util/Marker.java @@ -0,0 +1,5 @@ +package ru.practicum.shareit.util; + +public interface Marker { + interface OnCreate {} +} diff --git a/gateway/src/main/java/ru/practicum/shareit/util/ShareItGatewayExceptionHandler.java b/gateway/src/main/java/ru/practicum/shareit/util/ShareItGatewayExceptionHandler.java new file mode 100644 index 0000000..4760561 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/util/ShareItGatewayExceptionHandler.java @@ -0,0 +1,49 @@ +package ru.practicum.shareit.util; + +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.Objects; +import java.util.stream.Collectors; + +@Slf4j +@RestControllerAdvice +public class ShareItGatewayExceptionHandler { + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + protected ErrorResponse handleConstraintViolation(final ConstraintViolationException ex) { + String errorMessage = ex.getConstraintViolations().stream() + .map(violation -> { + String field = violation.getPropertyPath().toString(); + + if (field.contains(".")) { + field = field.substring(field.lastIndexOf('.') + 1); + } + + return "Поле " + field + ": " + violation.getMessage(); + }) + .collect(Collectors.joining("; ")); + + ErrorResponse response = new ErrorResponse(errorMessage); + log.error(errorMessage); + + return response; + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + protected ErrorResponse handleMethodArgumentNotValid(final MethodArgumentNotValidException ex) { + ErrorResponse response = new ErrorResponse( + "Поле " + Objects.requireNonNull(ex.getFieldError()).getField() + " " + + ex.getFieldError().getDefaultMessage() + ); + log.error(response.error()); + return response; + } +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/util/config/ShateItGatewayConfiguration.java b/gateway/src/main/java/ru/practicum/shareit/util/config/ShateItGatewayConfiguration.java new file mode 100644 index 0000000..74b6282 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/util/config/ShateItGatewayConfiguration.java @@ -0,0 +1,27 @@ +package ru.practicum.shareit.util.config; + +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; + +import java.time.format.DateTimeFormatter; + +@SpringBootConfiguration +public class ShateItGatewayConfiguration { + + @Bean + public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { + String pattern = "yyyy-MM-dd'T'HH:mm:ss"; + return builder -> { + builder.simpleDateFormat(pattern); + builder.serializers(new LocalDateTimeSerializer( + DateTimeFormatter.ofPattern(pattern) + )); + builder.deserializers(new LocalDateTimeDeserializer( + DateTimeFormatter.ofPattern(pattern) + )); + }; + } +} \ No newline at end of file diff --git a/gateway/src/main/resources/banner.txt b/gateway/src/main/resources/banner.txt new file mode 100644 index 0000000..f664a37 --- /dev/null +++ b/gateway/src/main/resources/banner.txt @@ -0,0 +1,5 @@ + _ _ _ + ___| |__ __ _ _ __ ___(_) |_ + / __| '_ \ / _` | '__/ _ \ | __| + \__ \ | | | (_| | | | __/ | |_ + |___/_| |_|\__,_|_| \___|_|\__| diff --git a/gateway/src/test/java/ShareitGatewayTest.java b/gateway/src/test/java/ShareitGatewayTest.java new file mode 100644 index 0000000..c91be13 --- /dev/null +++ b/gateway/src/test/java/ShareitGatewayTest.java @@ -0,0 +1,13 @@ +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.shareit.ShareItGateway; + +@SpringBootTest +@ContextConfiguration(classes = ShareItGateway.class) +public class ShareitGatewayTest { + + @Test + public void startingTest() { + } +} diff --git a/server/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java b/server/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java index 10e6dad..b48e52c 100644 --- a/server/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java +++ b/server/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java @@ -14,7 +14,7 @@ @Service @RequiredArgsConstructor -class UserServiceImpl implements UserService { +public class UserServiceImpl implements UserService { final UserRepository userRepository; final UtilService utilService; diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 8a85b26..51a49c8 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -1,22 +1,25 @@ server.port=9090 +spring.main.banner-mode=OFF spring.data.jdbc.dialect=POSTGRESQL spring.sql.init.mode=ALWAYS + hibernate.jdbc.time-zone=UTC hibernate.default-schema=public -spring.jpa.hibernate.ddl-auto=none -spring.jpa.properties.hibernate.format_sql=true +hibernate.show-sql=true +hibernate.format-sql=true +spring.jpa.hibernate.ddl-auto=validate +spring.jpa.properties.hibernate.format_sql=true +spring.jackson.serialization.indent_output=true #--- spring.datasource.driver-class-name=org.postgresql.Driver -spring.datasource.url=jdbc:postgresql://localhost:6541/shareit -spring.datasource.username=dbuser -spring.datasource.password=12345 -#spring.datasource.username=shareit -#spring.datasource.password=shareit +spring.datasource.url=jdbc:postgresql://localhost:5432/shareit +spring.datasource.username=shareit +spring.datasource.password=shareit #--- -#spring.config.activate.on-profile=test -#spring.datasource.driverClassName=org.h2.Driver -#spring.datasource.url=jdbc:h2:mem:shareit -#spring.datasource.username=shareit -#spring.datasource.password=shareit \ No newline at end of file +logging.level.root=error +logging.level.org.springframework.boot.web=info +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.orm.jdbc.bind=trace +logging.level.org.zalando.logbook=TRACE diff --git a/server/src/main/resources/schema.sql b/server/src/main/resources/schema.sql index 2763290..0c949b2 100644 --- a/server/src/main/resources/schema.sql +++ b/server/src/main/resources/schema.sql @@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS public.requests ( id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, description varchar(255) NULL, requestor_id int8 NOT NULL, - created timestamp without time zone NOT NULL, + created timestamp(3) without time zone NOT NULL, CONSTRAINT pk_requests PRIMARY KEY (id), CONSTRAINT fk_requests_users_id FOREIGN KEY (requestor_id) REFERENCES public.users(id) ON DELETE CASCADE ); @@ -31,8 +31,8 @@ CREATE INDEX IF NOT EXISTS idx_items_description ON items(description); CREATE TABLE IF NOT EXISTS public.bookings ( id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, - start_date timestamp without time zone NOT NULL NOT NULL, - end_date timestamp without time zone NOT NULL, + start_date timestamp(3) without time zone NOT NULL NOT NULL, + end_date timestamp(3) without time zone NOT NULL, item_id bigint NOT NULL, booker_id bigint NOT NULL, status varchar(255) check (status in ('WAITING','APPROVED','REJECTED','CANCELED')), @@ -46,9 +46,9 @@ CREATE TABLE IF NOT EXISTS public.comments ( id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, item_id bigint NOT NULL, author_id bigint NOT NULL, - created_date timestamp without time zone NOT NULL, + created_date timestamp(3) without time zone NOT NULL, comment_text VARCHAR(255) NOT NULL, CONSTRAINT pk_comments PRIMARY KEY (id), CONSTRAINT fk_comments_items_id FOREIGN KEY (item_id) REFERENCES public.items(id) ON DELETE CASCADE, - CONSTRAINT fk_comments_users_id FOREIGN KEY (item_id) REFERENCES public.users(id) ON DELETE CASCADE + CONSTRAINT fk_comments_users_id FOREIGN KEY (author_id) REFERENCES public.users(id) ON DELETE CASCADE ); diff --git a/server/src/test/java/user/UserServiceTest.java b/server/src/test/java/user/UserServiceTest.java new file mode 100644 index 0000000..f6320cf --- /dev/null +++ b/server/src/test/java/user/UserServiceTest.java @@ -0,0 +1,35 @@ +package user; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.shareit.ShareItServer; +import ru.practicum.shareit.user.service.UserServiceImpl; +import ru.practicum.shareit.util.UtilService; + +@Transactional +@Import({UserServiceImpl.class, UtilService.class}) +@ContextConfiguration(classes = {ShareItServer.class}) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@DataJpaTest(properties = { + "spring.datasource.driver-class-name=org.h2.Driver", + "spring.datasource.url=jdbc:h2:mem:shareit" +}) +public class UserServiceTest { + + @PersistenceContext + private EntityManager em; + + @Autowired + private UserServiceImpl userService; + + @Test + public void startingTest() { + } +} From 6b87c7334a62d2f162f10e399ba992e37d802790 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Mon, 13 Apr 2026 02:11:49 +0300 Subject: [PATCH 04/28] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=D0=B0=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=88=D0=BB=D1=8B=D1=85=20=D0=B8=20=D0=B1=D1=83=D0=B4=D1=83?= =?UTF-8?q?=D1=89=D0=B8=D1=85=20=D0=B4=D0=B0=D1=82=20=D0=B1=D1=80=D0=BE?= =?UTF-8?q?=D0=BD=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ItemServiceImpl.getLastBookingDate, ItemServiceImpl.getNextBookingDate --- docker-compose.yml | 3 + .../shareit/booking/BookingClient.java | 2 +- .../shareit/booking/BookingController.java | 2 +- .../shareit/booking/dto/GBookingDto.java | 14 +- .../src/main/resources/application.properties | 7 - gateway/src/main/resources/application.yaml | 10 + pom.xml | 4 +- server/pom.xml | 13 +- .../shareit/booking/dto/BookingDto.java | 3 +- .../shareit/booking/dto/BookingMapper.java | 6 +- .../booking/dto/IncomingBookingDto.java | 2 - .../shareit/booking/model/Booking.java | 8 +- .../booking/service/BookingServiceImpl.java | 1 + .../shareit/item/dto/ItemBigDto.java | 3 - .../practicum/shareit/item/dto/ItemDto.java | 5 - .../shareit/item/service/ItemServiceImpl.java | 10 +- .../user/repository/UserRepository.java | 1 + .../util/config/ShateItConfiguration.java | 1 + .../src/main/resources/application.properties | 25 -- server/src/main/resources/application.yaml | 50 ++++ .../java/ru/practicum/shareit/TestUtils.java | 105 +++++++ .../repository/BookingRepositoryTest.java | 168 +++++++++++ .../shareit/item/ItemIntegationTest.java | 276 ++++++++++++++++++ .../shareit/item/dto/ItemBigDtoJsonTest.java | 116 ++++++++ .../shareit/item/service/ItemServiceTest.java | 34 +++ .../dto/ItemRequestBigDtoJsonTest.java | 93 ++++++ .../request/dto/ItemRequestDtoJsonTest.java | 61 ++++ .../shareit/user/UserControllerTest.java | 206 +++++++++++++ .../shareit/user/dto/UserDtoJsonTest.java | 37 +++ .../user/repository/UserRepositoryTest.java | 41 +++ .../shareit/user/service/UserServiceTest.java | 194 ++++++++++++ .../shareit/util/UtilServiceTest.java | 61 ++++ .../src/test/java/user/UserServiceTest.java | 35 --- 33 files changed, 1491 insertions(+), 106 deletions(-) delete mode 100644 gateway/src/main/resources/application.properties create mode 100644 gateway/src/main/resources/application.yaml delete mode 100644 server/src/main/resources/application.properties create mode 100644 server/src/main/resources/application.yaml create mode 100644 server/src/test/java/ru/practicum/shareit/TestUtils.java create mode 100644 server/src/test/java/ru/practicum/shareit/booking/repository/BookingRepositoryTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/item/dto/ItemBigDtoJsonTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/item/service/ItemServiceTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/request/dto/ItemRequestBigDtoJsonTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/request/dto/ItemRequestDtoJsonTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/user/UserControllerTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/user/dto/UserDtoJsonTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/user/repository/UserRepositoryTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/user/service/UserServiceTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/util/UtilServiceTest.java delete mode 100644 server/src/test/java/user/UserServiceTest.java diff --git a/docker-compose.yml b/docker-compose.yml index d864fe1..a456459 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ services: depends_on: - server environment: + - TZ=Europe/Moscow - SHAREIT_SERVER_URL=http://server:9090 server: @@ -23,6 +24,7 @@ services: depends_on: - db environment: + - TZ=Europe/Moscow - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/shareit - SPRING_DATASOURCE_USERNAME=shareit - SPRING_DATASOURCE_PASSWORD=shareit @@ -33,6 +35,7 @@ services: # ports: # - "6541:5432" environment: + - TZ=Europe/Moscow - POSTGRES_DB=shareit - POSTGRES_USER=shareit - POSTGRES_PASSWORD=shareit diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/BookingClient.java b/gateway/src/main/java/ru/practicum/shareit/booking/BookingClient.java index f47e7fc..44b16fb 100644 --- a/gateway/src/main/java/ru/practicum/shareit/booking/BookingClient.java +++ b/gateway/src/main/java/ru/practicum/shareit/booking/BookingClient.java @@ -7,8 +7,8 @@ import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.stereotype.Service; import org.springframework.web.util.DefaultUriBuilderFactory; -import ru.practicum.shareit.booking.dto.GBookingDto; import ru.practicum.shareit.booking.dto.BookingState; +import ru.practicum.shareit.booking.dto.GBookingDto; import ru.practicum.shareit.client.BaseClient; import java.util.Map; diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java b/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java index 4f31c29..e3b6fea 100644 --- a/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -9,8 +9,8 @@ import org.springframework.stereotype.Controller; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import ru.practicum.shareit.booking.dto.GBookingDto; import ru.practicum.shareit.booking.dto.BookingState; +import ru.practicum.shareit.booking.dto.GBookingDto; import ru.practicum.shareit.util.Marker; @Controller diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/dto/GBookingDto.java b/gateway/src/main/java/ru/practicum/shareit/booking/dto/GBookingDto.java index 545ffde..f607999 100644 --- a/gateway/src/main/java/ru/practicum/shareit/booking/dto/GBookingDto.java +++ b/gateway/src/main/java/ru/practicum/shareit/booking/dto/GBookingDto.java @@ -5,18 +5,22 @@ import jakarta.validation.constraints.Future; import jakarta.validation.constraints.FutureOrPresent; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; import ru.practicum.shareit.util.Marker; -public record GBookingDto( +@Data +@AllArgsConstructor +public class GBookingDto { @NotNull(groups = Marker.OnCreate.class) - long itemId, + long itemId; @FutureOrPresent @NotNull(groups = Marker.OnCreate.class) - LocalDateTime start, + LocalDateTime start; @Future @NotNull(groups = Marker.OnCreate.class) - LocalDateTime end -) {} + LocalDateTime end; +} diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties deleted file mode 100644 index 2ee0851..0000000 --- a/gateway/src/main/resources/application.properties +++ /dev/null @@ -1,7 +0,0 @@ -logging.level.org.springframework.web.client.RestTemplate=DEBUG -#logging.level.org.apache.http=DEBUG -#logging.level.httpclient.wire=DEBUG - -server.port=8080 - -shareit-server.url=http://localhost:9090 \ No newline at end of file diff --git a/gateway/src/main/resources/application.yaml b/gateway/src/main/resources/application.yaml new file mode 100644 index 0000000..eacf1f1 --- /dev/null +++ b/gateway/src/main/resources/application.yaml @@ -0,0 +1,10 @@ +server.port: 8080 + +shareit-server.url: http://localhost:9090 + +logging: + level: +# httpclient.wire: DEBUG + org: + springframework.web.client.RestTemplate: DEBUG +# apache.http: DEBUG diff --git a/pom.xml b/pom.xml index 224f9ac..1e26e32 100644 --- a/pom.xml +++ b/pom.xml @@ -6,8 +6,8 @@ org.springframework.boot spring-boot-starter-parent - 3.3.2 - + 3.5.9 + ru.practicum diff --git a/server/pom.xml b/server/pom.xml index 3cb7ce7..59e10d4 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -42,6 +42,11 @@ org.springframework.boot spring-boot-starter-actuator + + org.springframework.boot + spring-boot-configuration-processor + true + org.postgresql postgresql @@ -52,21 +57,17 @@ h2 runtime - - org.springframework.boot - spring-boot-configuration-processor - true - com.querydsl querydsl-jpa jakarta ${querydsl.version} + provided org.zalando logbook-spring-boot-starter - 3.9.0 + 4.0.3 diff --git a/server/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java index 2b8231f..e364b2d 100644 --- a/server/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java +++ b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java @@ -13,4 +13,5 @@ public record BookingDto( ItemDto item, UserDto booker, BookingStatus status -) {} +) { +} diff --git a/server/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java index 67dea89..16596dd 100644 --- a/server/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java +++ b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java @@ -8,16 +8,14 @@ import ru.practicum.shareit.user.dto.UserMapper; import ru.practicum.shareit.user.model.User; -import java.time.ZoneId; - public class BookingMapper { @NonNull public static BookingDto toBookingDto(@NonNull Booking booking) { return new BookingDto( booking.getId(), - booking.getStart().atZone(ZoneId.systemDefault()).toLocalDateTime(), - booking.getEnd().atZone(ZoneId.systemDefault()).toLocalDateTime(), + booking.getStart(), + booking.getEnd(), ItemMapper.toDto(booking.getItem()), UserMapper.toUserDto(booking.getBooker()), booking.getStatus() diff --git a/server/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java b/server/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java index 4e4dc66..c224045 100644 --- a/server/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java +++ b/server/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java @@ -1,12 +1,10 @@ package ru.practicum.shareit.booking.dto; import lombok.Data; -import lombok.NoArgsConstructor; import java.time.LocalDateTime; @Data -@NoArgsConstructor public class IncomingBookingDto { long itemId; LocalDateTime start; diff --git a/server/src/main/java/ru/practicum/shareit/booking/model/Booking.java b/server/src/main/java/ru/practicum/shareit/booking/model/Booking.java index b0d322f..2e59cf5 100644 --- a/server/src/main/java/ru/practicum/shareit/booking/model/Booking.java +++ b/server/src/main/java/ru/practicum/shareit/booking/model/Booking.java @@ -1,7 +1,9 @@ package ru.practicum.shareit.booking.model; + import jakarta.persistence.*; -import lombok.*; +import lombok.Data; +import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import lombok.experimental.ExtensionMethod; import ru.practicum.shareit.item.model.Item; @@ -13,10 +15,6 @@ import static ru.practicum.shareit.util.hibernate.HibernateEqualsAndHashCode.*; -/** - * TODO Sprint add-bookings. - */ - @Data @Entity @NoArgsConstructor diff --git a/server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java b/server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java index bc40ca1..d0feaac 100644 --- a/server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java +++ b/server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java @@ -31,6 +31,7 @@ public class BookingServiceImpl implements BookingService { public BookingDto addNewBooking(Long userId, @NonNull IncomingBookingDto bookingDto) { LocalDateTime start = bookingDto.getStart(); LocalDateTime end = bookingDto.getEnd(); + if (start.isAfter(end) || start.isEqual(end)) { throw new ValidationException("Дата начала бронирования должна быть раньше даты окончания"); } diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/ItemBigDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemBigDto.java index 6689850..1bbbd20 100644 --- a/server/src/main/java/ru/practicum/shareit/item/dto/ItemBigDto.java +++ b/server/src/main/java/ru/practicum/shareit/item/dto/ItemBigDto.java @@ -7,9 +7,6 @@ import java.time.LocalDateTime; import java.util.List; -/** - * TODO Sprint add-controllers. - */ @Data @Builder @FieldDefaults(makeFinal = true) diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java index 5d4c19b..ae17b78 100644 --- a/server/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java +++ b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -9,14 +9,9 @@ @Builder @FieldDefaults(makeFinal = true) public class ItemDto { - Long id; - String name; - String description; - Boolean available; - Long requestId; } diff --git a/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java b/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java index a4a8063..9e23e5b 100644 --- a/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java +++ b/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java @@ -70,8 +70,8 @@ public List getAllItemsOfUser(long userId) { for (Item item : items.values()) { result.add(ItemMapper.toBigDto( item, - getLastBookingDate(bookings), - getNextBookingDate(bookings), + getLastBookingDate(bookings, item.getId()), + getNextBookingDate(bookings, item.getId()), comments.entrySet().stream() .filter(entry -> entry.getKey().equals(item.getId())) .map(Map.Entry::getValue) @@ -156,18 +156,20 @@ private Item getItemWithOwner(long itemId, long userId) { throw new NotFoundException("Пользователь с id=" + userId + " не является владельцем вещи с id=" + itemId); } - private LocalDateTime getLastBookingDate(@NonNull List bookings) { + private LocalDateTime getLastBookingDate(@NonNull List bookings, Long itemId) { LocalDateTime now = LocalDateTime.now(); return bookings.stream() + .filter(booking -> booking.getItem().getId().equals(itemId)) .map(Booking::getStart) .filter(start -> !start.isAfter(now)) .max(LocalDateTime::compareTo) .orElse(null); } - private LocalDateTime getNextBookingDate(@NonNull List bookings) { + private LocalDateTime getNextBookingDate(@NonNull List bookings, Long itemId) { LocalDateTime now = LocalDateTime.now(); return bookings.stream() + .filter(booking -> booking.getItem().getId().equals(itemId)) .map(Booking::getStart) .filter(start -> start.isAfter(now)) .min(LocalDateTime::compareTo) diff --git a/server/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java b/server/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java index cedef42..fb20957 100644 --- a/server/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java +++ b/server/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java @@ -8,6 +8,7 @@ @Repository public interface UserRepository extends JpaRepository { + boolean existsByEmail(String email); @Modifying diff --git a/server/src/main/java/ru/practicum/shareit/util/config/ShateItConfiguration.java b/server/src/main/java/ru/practicum/shareit/util/config/ShateItConfiguration.java index cab8ec4..0337307 100644 --- a/server/src/main/java/ru/practicum/shareit/util/config/ShateItConfiguration.java +++ b/server/src/main/java/ru/practicum/shareit/util/config/ShateItConfiguration.java @@ -35,4 +35,5 @@ public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { )); }; } + } \ No newline at end of file diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties deleted file mode 100644 index 51a49c8..0000000 --- a/server/src/main/resources/application.properties +++ /dev/null @@ -1,25 +0,0 @@ -server.port=9090 - -spring.main.banner-mode=OFF -spring.data.jdbc.dialect=POSTGRESQL -spring.sql.init.mode=ALWAYS - -hibernate.jdbc.time-zone=UTC -hibernate.default-schema=public -hibernate.show-sql=true -hibernate.format-sql=true - -spring.jpa.hibernate.ddl-auto=validate -spring.jpa.properties.hibernate.format_sql=true -spring.jackson.serialization.indent_output=true -#--- -spring.datasource.driver-class-name=org.postgresql.Driver -spring.datasource.url=jdbc:postgresql://localhost:5432/shareit -spring.datasource.username=shareit -spring.datasource.password=shareit -#--- -logging.level.root=error -logging.level.org.springframework.boot.web=info -logging.level.org.hibernate.SQL=DEBUG -logging.level.org.hibernate.orm.jdbc.bind=trace -logging.level.org.zalando.logbook=TRACE diff --git a/server/src/main/resources/application.yaml b/server/src/main/resources/application.yaml new file mode 100644 index 0000000..82242f3 --- /dev/null +++ b/server/src/main/resources/application.yaml @@ -0,0 +1,50 @@ +server: + port: 9090 + +spring: + main: + banner-mode: OFF + sql: + init: + mode: ALWAYS + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + format_sql: true + jackson: + serialization: + INDENT_OUTPUT: true + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://localhost:5432/shareit + username: shareit + password: shareit + +hibernate: +# jdbc: +# time-zone: UTC + default-schema: public + show-sql: true + format-sql: true + +# logging: +# level: +# root: error +# org: +# springframework: boot.web=info +# hibernate: +# sql: DEBUG +# orm: +# jdbc: +# bind: trace +# zalando: +# logbook: TRACE + +logging: + level: + root: info + org: + zalando: + logbook: trace diff --git a/server/src/test/java/ru/practicum/shareit/TestUtils.java b/server/src/test/java/ru/practicum/shareit/TestUtils.java new file mode 100644 index 0000000..09c74af --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/TestUtils.java @@ -0,0 +1,105 @@ +package ru.practicum.shareit; + +import org.hamcrest.Matcher; +import ru.practicum.shareit.item.model.Comment; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasProperty; + +public class TestUtils { + + public final LocalDateTime pastDate = LocalDateTime.parse("2023-01-01T00:00:01"); + public final LocalDateTime futureDate = LocalDateTime.parse("2028-01-01T00:00:01"); + + public String generateRandomText(int length, String addAfterStr, Character... addSynbols) { + java.util.List symbols = new ArrayList<>(List.of( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z' + )); + symbols.addAll(Arrays.asList(addSynbols)); + Random random = new Random(); + StringBuilder sb = new StringBuilder(length); + + for (int i = 0; i < length; i++) { + int index = random.nextInt(symbols.size()); + sb.append(symbols.get(index)); + } + + return sb.append(addAfterStr).toString(); + } + + public User makeNewUser(int id) { + return new User() + .setId((long) id) + .setName(generateRandomText(10, "", ' ')) + .setEmail(generateRandomText(10, "@mail.ru")); + } + + public User makeNewUser(long id) { + return new User() + .setId(id) + .setName(generateRandomText(10, "", ' ')) + .setEmail(generateRandomText(10, "@mail.ru")); + } + + public User makeNewUser() { + return new User() + .setId(null) + .setName(generateRandomText(10, "", ' ')) + .setEmail(generateRandomText(10, "@mail.ru")); + } + + public User getCopyOfUser(User user) { + return new User() + .setId(user.getId()) + .setName(user.getName()) + .setEmail(user.getEmail()); + } + + public Matcher matchesUserDto(User user) { + return allOf( + hasProperty("id", notNullValue()), + hasProperty("name", equalTo(user.getName())), + hasProperty("email", equalTo(user.getEmail())) + ); + } + + public Item makeNewItem(int id, User owner, ItemRequest request) { + return new Item() + .setId((long) id) + .setName(generateRandomText(25, "", ' ', ' ')) + .setDescription(generateRandomText(50, "", ' ', ' ', ' ', ' ')) + .setAvailable(true) + .setOwner(owner) + .setRequest(request); + } + + public ItemRequest makeNewItemRequest(int id, User requestor) { + return new ItemRequest() + .setId(id) + .setDescription(generateRandomText(50, "", ' ', ' ', ' ', ' ')) + .setCreatedDate(LocalDateTime.now().minusHours(1)) + .setRequestor(requestor); + } + + public Comment makeNewComment(int id, Item item, User author, LocalDateTime date) { + return new Comment() + .setId((long) id) + .setText(generateRandomText(50, "", ' ', ' ', ' ', ' ')) + .setItem(item) + .setAuthor(author) + .setCreatedDate(date); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/booking/repository/BookingRepositoryTest.java b/server/src/test/java/ru/practicum/shareit/booking/repository/BookingRepositoryTest.java new file mode 100644 index 0000000..6f99fdb --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/repository/BookingRepositoryTest.java @@ -0,0 +1,168 @@ +package ru.practicum.shareit.booking.repository; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.hamcrest.Matcher; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import ru.practicum.shareit.TestUtils; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.CommentRepository; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.request.repository.ItemRequestRepository; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +@Transactional +@DataJpaTest(properties = { + "spring.datasource.driver-class-name=org.h2.Driver", + "spring.datasource.url=jdbc:h2:mem:shareit" +}) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class BookingRepositoryTest { + + final TestUtils testUtils = new TestUtils(); + + final UserRepository userRepository; + final ItemRequestRepository requestRepository; + final ItemRepository itemRepository; + final BookingRepository bookingRepository; + final CommentRepository commentRepository; + + private User addNewUser() { + return userRepository.save(testUtils.makeNewUser()); + } + + private ItemRequest addNewItemRequest(User user) { + return requestRepository.save( + new ItemRequest() + .setRequestor(user) + .setDescription(testUtils.generateRandomText(50, "", ' ', ' ')) + .setCreatedDate(LocalDateTime.now()) + ); + } + + private Item addNewItemWithRequest(User owner, User requestor) { + return itemRepository.save( + new Item() + .setName(testUtils.generateRandomText(10, "", ' ')) + .setDescription(testUtils.generateRandomText(50, "", ' ', ' ')) + .setAvailable(true) + .setOwner(owner) + .setRequest(addNewItemRequest(requestor)) + ); + } + + private Booking addNewFullBooking(User requestor, User owner, User booker) { + LocalDateTime now = LocalDateTime.now(); + return bookingRepository.save( + new Booking() + .setBooker(booker) + .setItem(addNewItemWithRequest(owner, requestor)) + .setStart(now) + .setEnd(now.plusHours(1)) + .setStatus(BookingStatus.APPROVED) + ); + } + + private Booking addNewBooking(Item item, User booker, BookingStatus status) { + LocalDateTime now = LocalDateTime.now(); + return bookingRepository.save( + new Booking() + .setBooker(booker) + .setItem(item) + .setStart(now) + .setEnd(now.plusHours(1)) + .setStatus(status) + ); + } + + private Matcher matchesBooking(Booking booking) { + return allOf( + hasProperty("id", notNullValue()), + hasProperty("booker", equalTo(booking.getBooker())), + hasProperty("item", equalTo(booking.getItem())), + hasProperty("start", equalTo(booking.getStart())), + hasProperty("end", equalTo(booking.getEnd())), + hasProperty("status", equalTo(booking.getStatus())) + ); + } + + @Test + void findAllByOwner() { + User requestor = addNewUser(); + User owner = addNewUser(); + User booker = addNewUser(); + + Booking booking1 = addNewFullBooking(requestor, owner, requestor); + Item item = booking1.getItem(); + Booking booking2 = addNewBooking(item, booker, BookingStatus.WAITING); + + List bookingList = bookingRepository.findAllByOwner(owner.getId()); + + assertThat(bookingList, hasSize(2)); + assertThat(bookingList, contains(matchesBooking(booking2), matchesBooking(booking1))); + } + + @Test + void findCurrentByOwner() { + } + + @Test + void findPastByOwner() { + } + + @Test + void findFutureByOwner() { + } + + @Test + void findAllByOwnerAndStatusOrderByStartDesc() { + } + + @Test + void findCurrentByBooker() { + } + + @Test + void findPastByBooker() { + } + + @Test + void findFutureByBooker() { + } + + @Test + void findLastBookingDate() { + } + + @Test + void findNextBookingDate() { + } + + @Test + void findAllByBookerIdOrderByStartDesc() { + } + + @Test + void findAllByBooker_IdAndStatusOrderByStartDesc() { + } + + @Test + void findAllBookingByItem_Id() { + } + + @Test + void findAllBookingByItemIdIn() { + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java new file mode 100644 index 0000000..8baa824 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java @@ -0,0 +1,276 @@ +package ru.practicum.shareit.item; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.shareit.ShareItServer; +import ru.practicum.shareit.TestUtils; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.item.model.Comment; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.CommentRepository; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.item.service.ItemService; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.request.repository.ItemRequestRepository; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; +import ru.practicum.shareit.util.UtilService; +import ru.practicum.shareit.util.error.ErrorHandler; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ContextConfiguration(classes = ShareItServer.class) +public class ItemIntegationTest { + + final TestUtils testUtils = new TestUtils(); + + @Autowired + ObjectMapper mapper; + + @Autowired + private MockMvc mvc; + + @Autowired + ItemController itemController; + + @Autowired + ItemService itemService; + + @Autowired + UtilService utilService; + + @MockitoBean + UserRepository userRepository; + + @MockitoBean + ItemRepository itemRepository; + + @MockitoBean + ItemRequestRepository itemRequestRepository; + + @MockitoBean + BookingRepository bookingRepository; + + @MockitoBean + CommentRepository commentRepository; + + @BeforeEach + void setUp() { + mvc = MockMvcBuilders + .standaloneSetup(itemController) + .setControllerAdvice(new ErrorHandler()) + .setMessageConverters(new MappingJackson2HttpMessageConverter(mapper)) + .build(); + } + + @Nested + class GetItem { + + @Test + @SneakyThrows + void whenUserIdIsEqualsOwnerId_htenReturnsItemWithBookingDates() { + User owner = testUtils.makeNewUser(1); + Item item = testUtils.makeNewItem(100, owner, null); + + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + when(bookingRepository.findLastBookingDate(item.getId())).thenReturn(testUtils.pastDate); + when(bookingRepository.findNextBookingDate(item.getId())).thenReturn(testUtils.futureDate); + + mvc.perform(get("/items/{itemid}", item.getId()) + .header("X-Sharer-User-Id", owner.getId())) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()) + // region .andExpect item + .andExpect(jsonPath("$.id", is(item.getId()), Long.class)) + .andExpect(jsonPath("$.name", is(item.getName()), String.class)) + .andExpect(jsonPath("$.description", is(item.getDescription()), String.class)) + .andExpect(jsonPath("$.available", is(item.isAvailable()), Boolean.class)) + .andExpect(jsonPath("$.request", nullValue())) + .andExpect(jsonPath("$.lastBooking", is(testUtils.pastDate.toString()), String.class)) + .andExpect(jsonPath("$.nextBooking", is(testUtils.futureDate.toString()), String.class)) + .andExpect(jsonPath("$.comments", empty())); + // endregion .andExpect item + } + + @Test + @SneakyThrows + void whenUserIdIsNotEqualsOwnerId_htenReturnsItemWithNullableBookingDates() { + User owner = testUtils.makeNewUser(1); + Item item = testUtils.makeNewItem(100, owner, null); + + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + + mvc.perform(get("/items/{itemid}", item.getId()) + .header("X-Sharer-User-Id", item.getId())) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()) + // region .andExpect item + .andExpect(jsonPath("$.id", is(item.getId()), Long.class)) + .andExpect(jsonPath("$.name", is(item.getName()), String.class)) + .andExpect(jsonPath("$.description", is(item.getDescription()), String.class)) + .andExpect(jsonPath("$.available", is(item.isAvailable()), Boolean.class)) + .andExpect(jsonPath("$.request", nullValue())) + .andExpect(jsonPath("$.lastBooking", nullValue())) + .andExpect(jsonPath("$.nextBooking", nullValue())) + .andExpect(jsonPath("$.comments", empty())); + // endregion .andExpect item + + verify(bookingRepository, never()).findLastBookingDate(item.getId()); + verify(bookingRepository, never()).findNextBookingDate(item.getId()); + } + } + + + @Nested + class GetAllItemsOfUser { + + @Test + @SneakyThrows + void getAllItemsOfUser() { + User owner = testUtils.makeNewUser(1); + User booker = testUtils.makeNewUser(2); + + ItemRequest request1 = testUtils.makeNewItemRequest(50, testUtils.makeNewUser()); + ItemRequest request2 = testUtils.makeNewItemRequest(51, testUtils.makeNewUser()); + + Item item1 = testUtils.makeNewItem( + 100, + owner, + request1 + ); + Item item2 = testUtils.makeNewItem( + 101, + owner, + request2 + ); + + Booking booking1 = new Booking() + .setId((long) 70) + .setItem(item1) + .setBooker(booker) + .setStart(testUtils.pastDate) + .setEnd(testUtils.pastDate.plusHours(10)) + .setStatus(BookingStatus.APPROVED); + + Booking booking2 = new Booking() + .setId((long) 70) + .setItem(item2) + .setBooker(booker) + .setStart(testUtils.futureDate) + .setEnd(testUtils.futureDate.plusHours(10)) + .setStatus(BookingStatus.WAITING); + + Comment comment = testUtils.makeNewComment( + 90, + item1, + booker, + testUtils.pastDate.plusHours(11) + ); + + when(userRepository.existsById(owner.getId())).thenReturn(true); + when(itemRepository.findAllByOwner_Id(owner.getId())).thenReturn(List.of(item1, item2)); + when(bookingRepository.findAllBookingByItemIdIn(Set.of(item1.getId(), item2.getId()))) + .thenReturn(List.of(booking1, booking2)); + when(commentRepository.findAllByItemIdIn(Set.of(item1.getId(), item2.getId()))) + .thenReturn(List.of(comment)); + + mvc.perform(get("/items").header("X-Sharer-User-Id", owner.getId())) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andExpect(jsonPath("$", hasSize(2))) + // region .andExpect item 1 + .andExpect(jsonPath("$[0].id", is(item1.getId()), Long.class)) + .andExpect(jsonPath("$[0].name", is(item1.getName()))) + .andExpect(jsonPath("$[0].description", is(item1.getDescription()))) + .andExpect(jsonPath("$[0].available", is(item1.isAvailable()))) + .andExpect(jsonPath("$[0].request", is(request1.getId()), Long.class)) + .andExpect(jsonPath("$[0].lastBooking", is(booking1.getStart().toString()))) + .andExpect(jsonPath("$[0].nextBooking", nullValue())) + .andExpect(jsonPath("$[0].comments[0].id", is(comment.getId()), Long.class)) + .andExpect(jsonPath("$[0].comments[0].text", is(comment.getText()))) + .andExpect(jsonPath("$[0].comments[0].itemId", is(comment.getItem().getId()), Long.class)) + .andExpect(jsonPath("$[0].comments[0].created", is(comment.getCreatedDate().toString()))) + .andExpect(jsonPath("$[0].comments[0].authorName", is(comment.getAuthor().getName()))) + // endregion .andExpect item 1 + // region .andExpect item 2 + .andExpect(jsonPath("$[1].id", is(item2.getId()), Long.class)) + .andExpect(jsonPath("$[1].name", is(item2.getName()))) + .andExpect(jsonPath("$[1].description", is(item2.getDescription()))) + .andExpect(jsonPath("$[1].available", is(item2.isAvailable()))) + .andExpect(jsonPath("$[1].request", is(request2.getId()), Long.class)) + .andExpect(jsonPath("$[1].lastBooking", nullValue())) + .andExpect(jsonPath("$[1].nextBooking", is(booking2.getStart().toString()))) + .andExpect(jsonPath("$[1].comments", empty())); + // endregion .andExpect item 2 + } + } + + @Nested + class Search { + + @Test + @SneakyThrows + void search() { + } + } + + @Nested + class AddNewItem { + + @Test + @SneakyThrows + void addNewItem() { + } + } + + @Nested + class UpdateItem { + + @Test + @SneakyThrows + void updateItem() { + } + } + + @Nested + class DeleteItem { + + @Test + @SneakyThrows + void deleteItem() { + } + } + + @Nested + class AddComment { + + @Test + @SneakyThrows + void addComment() { + } + } +} diff --git a/server/src/test/java/ru/practicum/shareit/item/dto/ItemBigDtoJsonTest.java b/server/src/test/java/ru/practicum/shareit/item/dto/ItemBigDtoJsonTest.java new file mode 100644 index 0000000..46e5f96 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/dto/ItemBigDtoJsonTest.java @@ -0,0 +1,116 @@ +package ru.practicum.shareit.item.dto; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.shareit.ShareItServer; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +@ContextConfiguration(classes = ShareItServer.class) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class ItemBigDtoJsonTest { + + final JacksonTester itemBigDtoJson; + + @Test + @SneakyThrows + void testItemBigDto() { + ItemBigDto itemBigDto = ItemBigDto.builder() + .id(1L) + .name("this is item name") + .description("this is description") + .available(true) + .requestId(123L) // request + .lastBookingDate(LocalDateTime.parse("2027-01-01T00:00:01")) // lastBooking + .nextBookingDate(LocalDateTime.parse("2027-01-02T01:02:01")) // nextBooking + .comments(List.of( + CommentDto.builder() + .id(6L) + .itemId(1L) + .authorName("this is author name 1") + .createdDate(LocalDateTime.parse("2027-01-01T03:20:01")) // created + .text("this is text 1") + .build(), + CommentDto.builder() + .id(7L) + .itemId(1L) + .authorName("this is author name 2") + .createdDate(LocalDateTime.parse("2027-03-01T07:21:01")) // created + .text("this is text 2") + .build() + )) + .build(); + + JsonContent result = itemBigDtoJson.write(itemBigDto); + + // region itemBigDto + assertThat(result) + .extractingJsonPathNumberValue("$.id") + .isEqualTo(1); + assertThat(result) + .extractingJsonPathStringValue("$.name") + .isEqualTo("this is item name"); + assertThat(result) + .extractingJsonPathStringValue("$.description") + .isEqualTo("this is description"); + assertThat(result) + .extractingJsonPathBooleanValue("$.available") + .isEqualTo(true); + assertThat(result) + .extractingJsonPathNumberValue("$.request") + .isEqualTo(123); + assertThat(result) + .extractingJsonPathStringValue("$.lastBooking") + .isEqualTo("2027-01-01T00:00:01"); + assertThat(result) + .extractingJsonPathStringValue("$.nextBooking") + .isEqualTo("2027-01-02T01:02:01"); + // endregion itemBigDto + + // region comments[0] + assertThat(result) + .extractingJsonPathNumberValue("$.comments[0].id") + .isEqualTo(6); + assertThat(result) + .extractingJsonPathNumberValue("$.comments[0].itemId") + .isEqualTo(1); + assertThat(result) + .extractingJsonPathStringValue("$.comments[0].authorName") + .isEqualTo("this is author name 1"); + assertThat(result) + .extractingJsonPathStringValue("$.comments[0].created") + .isEqualTo("2027-01-01T03:20:01"); + assertThat(result) + .extractingJsonPathStringValue("$.comments[0].text") + .isEqualTo("this is text 1"); + // endregion comments[0] + + // region comments[1] + assertThat(result) + .extractingJsonPathNumberValue("$.comments[1].id") + .isEqualTo(7); + assertThat(result) + .extractingJsonPathNumberValue("$.comments[1].itemId") + .isEqualTo(1); + assertThat(result) + .extractingJsonPathStringValue("$.comments[1].authorName") + .isEqualTo("this is author name 2"); + assertThat(result) + .extractingJsonPathStringValue("$.comments[1].created") + .isEqualTo("2027-03-01T07:21:01"); + assertThat(result) + .extractingJsonPathStringValue("$.comments[1].text") + .isEqualTo("this is text 2"); + // endregion comments[1] + } +} diff --git a/server/src/test/java/ru/practicum/shareit/item/service/ItemServiceTest.java b/server/src/test/java/ru/practicum/shareit/item/service/ItemServiceTest.java new file mode 100644 index 0000000..7e5cc14 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/service/ItemServiceTest.java @@ -0,0 +1,34 @@ +package ru.practicum.shareit.item.service; + +import org.junit.jupiter.api.Test; + +class ItemServiceTest { + + @Test + void getItem() { + } + + @Test + void getAllItemsOfUser() { + } + + @Test + void search() { + } + + @Test + void addNewItem() { + } + + @Test + void updateItem() { + } + + @Test + void deleteItem() { + } + + @Test + void addComment() { + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/request/dto/ItemRequestBigDtoJsonTest.java b/server/src/test/java/ru/practicum/shareit/request/dto/ItemRequestBigDtoJsonTest.java new file mode 100644 index 0000000..f5a64b2 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/dto/ItemRequestBigDtoJsonTest.java @@ -0,0 +1,93 @@ +package ru.practicum.shareit.request.dto; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.shareit.ShareItServer; +import ru.practicum.shareit.item.dto.ItemLowDto; +import ru.practicum.shareit.user.dto.UserDto; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +@ContextConfiguration(classes = ShareItServer.class) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class ItemRequestBigDtoJsonTest { + + final JacksonTester requestBigDtoJson; + + @Test + @SneakyThrows + void testItemRequestBigDto() { + ItemRequestBigDto requestBigDto = ItemRequestBigDto.builder() + .id(1L) + .description("this is description") + .createdDate(LocalDateTime.parse("2027-01-01T00:00:01")) + .requestor(UserDto.builder() + .id(5L) + .name("John") + .email("john.doe@mail.com") + .build() + ) + .itemDtos(List.of( + new ItemLowDto(7L, "first item dto", 14L), + new ItemLowDto(8L, "second item dto", 15L) + )) + .build(); + + JsonContent result = requestBigDtoJson.write(requestBigDto); + + // region ItemRequestBigDto + assertThat(result) + .extractingJsonPathNumberValue("$.id") + .isEqualTo(1); + assertThat(result) + .extractingJsonPathStringValue("$.description") + .isEqualTo("this is description"); + assertThat(result) + .extractingJsonPathStringValue("$.created") + .isEqualTo("2027-01-01T00:00:01"); + assertThat(result) + .extractingJsonPathNumberValue("$.requestor.id") + .isEqualTo(5); + assertThat(result) + .extractingJsonPathStringValue("$.requestor.name") + .isEqualTo("John"); + assertThat(result) + .extractingJsonPathStringValue("$.requestor.email") + .isEqualTo("john.doe@mail.com"); + // endregion ItemRequestBigDto + + // region items[0] + assertThat(result) + .extractingJsonPathNumberValue("$.items[0].id") + .isEqualTo(7); + assertThat(result) + .extractingJsonPathStringValue("$.items[0].name") + .isEqualTo("first item dto"); + assertThat(result) + .extractingJsonPathNumberValue("$.items[0].ownerId") + .isEqualTo(14); + // endregion items[0] + + // region items[1] + assertThat(result) + .extractingJsonPathNumberValue("$.items[1].id") + .isEqualTo(8); + assertThat(result) + .extractingJsonPathStringValue("$.items[1].name") + .isEqualTo("second item dto"); + assertThat(result) + .extractingJsonPathNumberValue("$.items[1].ownerId") + .isEqualTo(15); + // endregion items[1] + } +} diff --git a/server/src/test/java/ru/practicum/shareit/request/dto/ItemRequestDtoJsonTest.java b/server/src/test/java/ru/practicum/shareit/request/dto/ItemRequestDtoJsonTest.java new file mode 100644 index 0000000..6115bea --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/dto/ItemRequestDtoJsonTest.java @@ -0,0 +1,61 @@ +package ru.practicum.shareit.request.dto; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.shareit.ShareItServer; +import ru.practicum.shareit.user.dto.UserDto; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +@ContextConfiguration(classes = ShareItServer.class) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class ItemRequestDtoJsonTest { + + final JacksonTester requestDtoJson; + + @Test + @SneakyThrows + void testItemRequestDto() { + ItemRequestDto requestDto = ItemRequestDto.builder() + .id(1L) + .description("this is description") + .createdDate(LocalDateTime.parse("2027-01-01T00:00:01")) + .requestor(UserDto.builder() + .id(5L) + .name("John") + .email("john.doe@mail.com") + .build() + ) + .build(); + + JsonContent result = requestDtoJson.write(requestDto); + + assertThat(result) + .extractingJsonPathNumberValue("$.id") + .isEqualTo(1); + assertThat(result) + .extractingJsonPathStringValue("$.description") + .isEqualTo("this is description"); + assertThat(result) + .extractingJsonPathStringValue("$.created") + .isEqualTo("2027-01-01T00:00:01"); + assertThat(result) + .extractingJsonPathNumberValue("$.requestor.id") + .isEqualTo(5); + assertThat(result) + .extractingJsonPathStringValue("$.requestor.name") + .isEqualTo("John"); + assertThat(result) + .extractingJsonPathStringValue("$.requestor.email") + .isEqualTo("john.doe@mail.com"); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/user/UserControllerTest.java b/server/src/test/java/ru/practicum/shareit/user/UserControllerTest.java new file mode 100644 index 0000000..da83bd3 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/UserControllerTest.java @@ -0,0 +1,206 @@ +package ru.practicum.shareit.user; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.service.UserService; +import ru.practicum.shareit.util.error.ErrorHandler; +import ru.practicum.shareit.util.exception.NotFoundException; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +class UserControllerTest { + + final ObjectMapper mapper = new ObjectMapper(); + + @InjectMocks + UserController userController; + + @Mock + UserService userService; + + MockMvc mvc; + + UserDto userDto; + + @BeforeEach + void setUp() { + mvc = MockMvcBuilders + .standaloneSetup(userController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + userDto = UserDto.builder() + .id(1L) + .name("John") + .email("john.doe@mail.com") + .build(); + } + + @Nested + class GetUser { + + @Test + @SneakyThrows + void returnResponseWithStatusOkAndBodyOfActualUserDto() { + long userId = 1L; + when(userService.getUser(userId)).thenReturn(userDto); + + mvc.perform(get("/users/{userId}", userId) + .content(mapper.writeValueAsString(userDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(userDto.getId()), Long.class)) + .andExpect(jsonPath("$.name", is(userDto.getName()))) + .andExpect(jsonPath("$.email", is(userDto.getEmail()))); + + verify(userService).getUser(userId); + } + + @Test + @SneakyThrows + void returnResponseWithStatus404AndBodyOfNotFoundException() { + long userId = anyLong(); + when(userService.getUser(userId)).thenThrow(NotFoundException.class); + + mvc.perform(get("/users/{userId}", userId) + .content(mapper.writeValueAsString(userDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().is(404)); + + verify(userService).getUser(userId); + } + } + + @Nested + class GetAllUsers { + + @Test + @SneakyThrows + void returnResponseWithStatusOkAndBodyOfListWithActualUserDto() { + when(userService.getAllUsers()).thenReturn(List.of(userDto)); + + mvc.perform(get("/users") + .content(mapper.writeValueAsString(userDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.[0].id", is(userDto.getId()), Long.class)) + .andExpect(jsonPath("$.[0].name", is(userDto.getName()), String.class)) + .andExpect(jsonPath("$.[0].email", is(userDto.getEmail()), String.class)); + + verify(userService).getAllUsers(); + } + + @Test + @SneakyThrows + void returnResponseWithStatusOkAndBodyOfEmptyList() { + when(userService.getAllUsers()).thenReturn(Collections.emptyList()); + + mvc.perform(get("/users") + .content(mapper.writeValueAsString(userDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", is(Collections.emptyList()))); + + verify(userService).getAllUsers(); + } + } + + @Nested + class AddNewUser { + + @Test + @SneakyThrows + void returnResponseWithStatusOkAndBodyOfActualUserDto() { + when(userService.saveUser(any())) + .thenReturn(userDto); + + String result = mvc.perform(post("/users") + .content(mapper.writeValueAsString(userDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + verify(userService).saveUser(any()); + assertEquals(result, mapper.writeValueAsString(userDto)); + } + } + + @Nested + class UpdateUser { + + @Test + @SneakyThrows + void test() { + UserDto newUserDto = UserDto.builder().name("NewUser").build(); + UserDto resultUserDto = UserDto.builder() + .id(userDto.getId()) + .name(newUserDto.getName()) + .email(userDto.getEmail()) + .build(); + when(userService.updateUser(userDto.getId(), newUserDto)).thenReturn(resultUserDto); + + mvc.perform(patch("/users/{userId}", 1L) + .content(mapper.writeValueAsString(newUserDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(resultUserDto.getId()), Long.class)) + .andExpect(jsonPath("$.name", is(resultUserDto.getName()), String.class)) + .andExpect(jsonPath("$.email", is(resultUserDto.getEmail()), String.class)); + + verify(userService).updateUser(userDto.getId(), newUserDto); + } + } + + @Nested + class DeleteUser { + + @Test + @SneakyThrows + void test() { + mvc.perform(delete("/users/{userId}", 1L)) + .andExpect(status().isOk()); + + verify(userService).deleteUser(1L); + } + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/user/dto/UserDtoJsonTest.java b/server/src/test/java/ru/practicum/shareit/user/dto/UserDtoJsonTest.java new file mode 100644 index 0000000..6701a4c --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/dto/UserDtoJsonTest.java @@ -0,0 +1,37 @@ +package ru.practicum.shareit.user.dto; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.shareit.ShareItServer; + +import static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +@ContextConfiguration(classes = ShareItServer.class) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class UserDtoJsonTest { + + final JacksonTester json; + + @Test + @SneakyThrows + void testUserDto() { + UserDto userDto = UserDto.builder() + .id(1L) + .name("John") + .email("john.doe@mail.com") + .build(); + + JsonContent result = json.write(userDto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("John"); + assertThat(result).extractingJsonPathStringValue("$.email").isEqualTo("john.doe@mail.com"); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/user/repository/UserRepositoryTest.java b/server/src/test/java/ru/practicum/shareit/user/repository/UserRepositoryTest.java new file mode 100644 index 0000000..f470309 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/repository/UserRepositoryTest.java @@ -0,0 +1,41 @@ +package ru.practicum.shareit.user.repository; + +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import ru.practicum.shareit.user.model.User; + +import static org.junit.jupiter.api.Assertions.*; + +@Transactional +@DataJpaTest(properties = { + "spring.datasource.driver-class-name=org.h2.Driver", + "spring.datasource.url=jdbc:h2:mem:shareit" +}) +class UserRepositoryTest { + + @Autowired + UserRepository userRepository; + + private User addNewUser() { + return userRepository.save(new User() + .setName("username") + .setEmail("email@mail.ru") + ); + } + + @Test + void existsByEmail() { + addNewUser(); + assertTrue(userRepository.existsByEmail("email@mail.ru")); + assertFalse(userRepository.existsByEmail("username@mail.ru")); + } + + @Test + void deleteUserById() { + User user = addNewUser(); + int count = userRepository.deleteUserById(user.getId()); + assertEquals(1, count); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/user/service/UserServiceTest.java b/server/src/test/java/ru/practicum/shareit/user/service/UserServiceTest.java new file mode 100644 index 0000000..7af513c --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/service/UserServiceTest.java @@ -0,0 +1,194 @@ +package ru.practicum.shareit.user.service; + +import lombok.SneakyThrows; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import ru.practicum.shareit.TestUtils; + +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; +import ru.practicum.shareit.util.UtilService; +import ru.practicum.shareit.util.exception.NotFoundException; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + + +@ExtendWith(MockitoExtension.class) +public class UserServiceTest { + + @Mock + UserRepository userRepository; + + @Mock + UtilService utilService; + + @InjectMocks + UserServiceImpl userService; + + final TestUtils testUtils = new TestUtils(); + + @Nested + class SaveUser { + + @Test + void returnActualUserDto() { + User user = testUtils.makeNewUser(1); + UserDto userDto = UserDto.builder() + .id(user.getId()) + .name(user.getName()) + .email(user.getEmail()) + .build(); + when(userRepository.save(user)).thenReturn(user); + + UserDto resultDto = userService.saveUser(userDto); + assertThat(resultDto, testUtils.matchesUserDto(user)); + } + } + + @Nested + class GetUser { + + @Test + void returnActualUserDto() { + User user = testUtils.makeNewUser(1); + when(utilService.getUser(user.getId())).thenReturn(user); + + UserDto resultDto = userService.getUser(user.getId()); + + assertThat(resultDto, testUtils.matchesUserDto(user)); + } + } + + @Nested + class GetAllUsers { + + @Test + public void returnListOfActualUserDtos() { + List users = List.of( + testUtils.makeNewUser(1), + testUtils.makeNewUser(2), + testUtils.makeNewUser(3) + ); + when(userRepository.findAll()).thenReturn(users); + + List userDtos = userService.getAllUsers(); + + assertThat(userDtos, contains( + testUtils.matchesUserDto(users.get(0)), + testUtils.matchesUserDto(users.get(1)), + testUtils.matchesUserDto(users.get(2)) + )); + } + } + + @Nested + class UpdateUser { + + @Test + void whenUserWasRenamed_returnActualUserDto() { + User oldUser = testUtils.makeNewUser(1); + UserDto requestDto = UserDto.builder() + .name("new name") + .build(); + User newUser = new User() + .setId(oldUser.getId()) + .setName(requestDto.getName()) + .setEmail(oldUser.getEmail()); + + when(utilService.getUser(oldUser.getId())).thenReturn(testUtils.getCopyOfUser(oldUser)); + when(userRepository.save(newUser)).thenReturn(newUser); + + UserDto resultDto = userService.updateUser(oldUser.getId(), requestDto); + + assertNotEquals(resultDto.getName(), oldUser.getName()); + assertThat(resultDto, testUtils.matchesUserDto(newUser)); + verify(userRepository).save(newUser); + } + + @Test + void whenUserEmailWasChanged_returnActualUserDto() { + User oldUser = testUtils.makeNewUser(1); + UserDto requestDto = UserDto.builder() + .email(testUtils.generateRandomText(10, "@mail.com")) + .build(); + User newUser = new User() + .setId(oldUser.getId()) + .setName(oldUser.getName()) + .setEmail(requestDto.getEmail()); + + when(utilService.getUser(oldUser.getId())).thenReturn(testUtils.getCopyOfUser(oldUser)); + when(userRepository.save(newUser)).thenReturn(newUser); + + UserDto resultDto = userService.updateUser(oldUser.getId(), requestDto); + + assertNotEquals(resultDto.getEmail(), oldUser.getEmail()); + assertThat(resultDto, testUtils.matchesUserDto(newUser)); + verify(userRepository).save(newUser); + } + + @Test + @SneakyThrows + void whenUserEmailIsConflicted_returnDuplicatedDataException() { + User oldUser = testUtils.makeNewUser(1); + + Method checkEmail = userService.getClass().getDeclaredMethod("checkEmail", String.class); + checkEmail.setAccessible(true); + when(userRepository.existsByEmail(oldUser.getEmail())).thenReturn(true); + + assertThrows(InvocationTargetException.class, + () -> checkEmail.invoke(userService, oldUser.getEmail())); + } + } + + @Nested + class DeleteUser { + + @Test + void whenUserWasDeleted_returnVoid() { + User user = testUtils.makeNewUser(1); + when(userRepository.existsById(user.getId())).thenReturn(true); + when(userRepository.deleteUserById(user.getId())).thenReturn(1); + + userService.deleteUser(user.getId()); + verify(userRepository).existsById(user.getId()); + verify(userRepository).deleteUserById(user.getId()); + } + + @Test + void whenUserWasNotDeleted_returnRuntimeException() { + User user = testUtils.makeNewUser(1); + when(userRepository.existsById(user.getId())).thenReturn(true); + when(userRepository.deleteUserById(user.getId())).thenReturn(0); + + assertThrows(RuntimeException.class, () -> userService.deleteUser(user.getId())); + + verify(userRepository).existsById(user.getId()); + verify(userRepository).deleteUserById(user.getId()); + } + + @Test + void whenUserIsNotFound_returnNotFoundException() { + User user = testUtils.makeNewUser(1); + when(userRepository.existsById(user.getId())).thenReturn(false); + + assertThrows(NotFoundException.class, + () -> userService.deleteUser(user.getId())); + + verify(userRepository).existsById(user.getId()); + verify(userRepository, never()).deleteUserById(user.getId()); + } + } +} diff --git a/server/src/test/java/ru/practicum/shareit/util/UtilServiceTest.java b/server/src/test/java/ru/practicum/shareit/util/UtilServiceTest.java new file mode 100644 index 0000000..07bcbb9 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/util/UtilServiceTest.java @@ -0,0 +1,61 @@ +package ru.practicum.shareit.util; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.request.repository.ItemRequestRepository; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.repository.UserRepository; +import ru.practicum.shareit.util.error.ErrorHandler; + +@ExtendWith(MockitoExtension.class) +class UtilServiceTest { + + @InjectMocks + private UtilService utilService; + + @Mock + private UserRepository userRepository; + + @Mock + private ItemRepository itemRepository; + + @Mock + private ItemRequestRepository itemRequestRepository; + + private UserDto userDto; + + @Autowired + ErrorHandler errorHandler; + + @BeforeEach + void setUp() { + userDto = UserDto.builder() + .id(1L) + .name("John") + .email("john.doe@mail.com") + .build(); + } + + @Test + void checkUser() { + } + + @Test + void getUser() { + } + + @Test + void getItem() { + } + + @Test + void getItemRequest() { + } +} \ No newline at end of file diff --git a/server/src/test/java/user/UserServiceTest.java b/server/src/test/java/user/UserServiceTest.java deleted file mode 100644 index f6320cf..0000000 --- a/server/src/test/java/user/UserServiceTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package user; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import jakarta.transaction.Transactional; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; -import org.springframework.test.context.ContextConfiguration; -import ru.practicum.shareit.ShareItServer; -import ru.practicum.shareit.user.service.UserServiceImpl; -import ru.practicum.shareit.util.UtilService; - -@Transactional -@Import({UserServiceImpl.class, UtilService.class}) -@ContextConfiguration(classes = {ShareItServer.class}) -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -@DataJpaTest(properties = { - "spring.datasource.driver-class-name=org.h2.Driver", - "spring.datasource.url=jdbc:h2:mem:shareit" -}) -public class UserServiceTest { - - @PersistenceContext - private EntityManager em; - - @Autowired - private UserServiceImpl userService; - - @Test - public void startingTest() { - } -} From b5200d5fe62fe0eaa0ef07038bbec3f7f0580f4b Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Mon, 13 Apr 2026 11:13:57 +0300 Subject: [PATCH 05/28] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=D0=B0=20=D0=B2=D0=B5=D1=89?= =?UTF-8?q?=D0=B8=20=D0=BF=D0=BE=20=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8E=20=D0=B8=20=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ItemServiceImpl.search --- .../shareit/item/service/ItemServiceImpl.java | 4 +- .../java/ru/practicum/shareit/TestUtils.java | 30 +++++++ .../shareit/item/ItemIntegationTest.java | 89 ++++++++++++------- .../shareit/item/service/ItemServiceTest.java | 34 ------- 4 files changed, 92 insertions(+), 65 deletions(-) delete mode 100644 server/src/test/java/ru/practicum/shareit/item/service/ItemServiceTest.java diff --git a/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java b/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java index 9e23e5b..a40755c 100644 --- a/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java +++ b/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java @@ -85,10 +85,12 @@ public List getAllItemsOfUser(long userId) { @Override public List search(long userId, String text) { - if (text == null || text.isEmpty()) { + utilService.checkUser(userId); + if (text.isBlank()) { return Collections.emptyList(); } return itemRepository.search(text).stream() + .filter(item -> item.getOwner().getId() != userId) .map(ItemMapper::toDto) .toList(); } diff --git a/server/src/test/java/ru/practicum/shareit/TestUtils.java b/server/src/test/java/ru/practicum/shareit/TestUtils.java index 09c74af..d1f93e3 100644 --- a/server/src/test/java/ru/practicum/shareit/TestUtils.java +++ b/server/src/test/java/ru/practicum/shareit/TestUtils.java @@ -1,6 +1,8 @@ package ru.practicum.shareit; import org.hamcrest.Matcher; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingStatus; import ru.practicum.shareit.item.model.Comment; import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.request.model.ItemRequest; @@ -86,6 +88,10 @@ public Item makeNewItem(int id, User owner, ItemRequest request) { .setRequest(request); } + public Item makeNewFastItem(int id) { + return makeNewItem(id, makeNewUser(id + 1), makeNewItemRequest(id + 10, makeNewUser(id + 2))); + } + public ItemRequest makeNewItemRequest(int id, User requestor) { return new ItemRequest() .setId(id) @@ -102,4 +108,28 @@ public Comment makeNewComment(int id, Item item, User author, LocalDateTime date .setAuthor(author) .setCreatedDate(date); } + + public Booking makeNewAnyFullFastBooking(int id, LocalDateTime date, BookingStatus status) { + User owner = makeNewUser(id + 10); + User booker = makeNewUser(id + 11); + Item item = makeNewItem(id + 20, owner, makeNewItemRequest(id + 30, booker)); + return new Booking() + .setId((long) id) + .setItem(item) + .setBooker(booker) + .setStart(date) + .setEnd(date.plusHours(10)) + .setStatus(status); + } + + public Booking makeNewAnyFullBooking(int id, User booker, User owner, LocalDateTime date, BookingStatus status) { + Item item = makeNewItem(id + 20, owner, makeNewItemRequest(id + 30, booker)); + return new Booking() + .setId((long) id) + .setItem(item) + .setBooker(booker) + .setStart(date) + .setEnd(date.plusHours(10)) + .setStatus(status); + } } diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java index 8baa824..22281fa 100644 --- a/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java +++ b/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java @@ -150,43 +150,33 @@ class GetAllItemsOfUser { @Test @SneakyThrows - void getAllItemsOfUser() { - User owner = testUtils.makeNewUser(1); - User booker = testUtils.makeNewUser(2); + void returnResponseWithStatusOkAndActualItems() { + Booking booking1 = testUtils.makeNewAnyFullFastBooking( + 10, + testUtils.pastDate, + BookingStatus.APPROVED + ); - ItemRequest request1 = testUtils.makeNewItemRequest(50, testUtils.makeNewUser()); - ItemRequest request2 = testUtils.makeNewItemRequest(51, testUtils.makeNewUser()); + User booker = booking1.getBooker(); + User owner = booking1.getItem().getOwner(); - Item item1 = testUtils.makeNewItem( - 100, - owner, - request1 - ); - Item item2 = testUtils.makeNewItem( - 101, + Booking booking2 = testUtils.makeNewAnyFullBooking( + 11, + booker, owner, - request2 + testUtils.futureDate, + BookingStatus.WAITING ); - Booking booking1 = new Booking() - .setId((long) 70) - .setItem(item1) - .setBooker(booker) - .setStart(testUtils.pastDate) - .setEnd(testUtils.pastDate.plusHours(10)) - .setStatus(BookingStatus.APPROVED); - - Booking booking2 = new Booking() - .setId((long) 70) - .setItem(item2) - .setBooker(booker) - .setStart(testUtils.futureDate) - .setEnd(testUtils.futureDate.plusHours(10)) - .setStatus(BookingStatus.WAITING); + Item item1 = booking1.getItem(); + Item item2 = booking2.getItem(); + + ItemRequest request1 = item1.getRequest(); + ItemRequest request2 = item2.getRequest(); Comment comment = testUtils.makeNewComment( 90, - item1, + booking1.getItem(), booker, testUtils.pastDate.plusHours(11) ); @@ -234,7 +224,46 @@ class Search { @Test @SneakyThrows - void search() { + void whenTextIsValid_returnsListWithActualItems() { + Item item1 = testUtils.makeNewFastItem(10); + Item item2 = testUtils.makeNewFastItem(11); + Item item3 = testUtils.makeNewFastItem(12); + String text = item1.getName(); + item2.setDescription(text + item2.getName()); + item3.setName("hghgh" + text); + Long seacherId = item3.getOwner().getId(); + + when(userRepository.existsById(seacherId)).thenReturn(true); + when(itemRepository.search(item1.getName())).thenReturn(List.of(item1, item2)); + + mvc.perform(get("/items/search?text={text}", item1.getName()) + .header("X-Sharer-User-Id", seacherId)) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andExpect(jsonPath("$", hasSize(2))); + } + + @Test + @SneakyThrows + void whenTextIsEmpty_returnsEmptyList() { + when(userRepository.existsById(1L)).thenReturn(true); + + mvc.perform(get("/items/search?text={text}", "") + .header("X-Sharer-User-Id", 1)) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andExpect(jsonPath("$", hasSize(0))); + } + + @Test + @SneakyThrows + void whenUserIsNotFound_returnsResponseStatusForbidden() { + when(userRepository.existsById(1L)).thenReturn(false); + + mvc.perform(get("/items/search?text={text}", "") + .header("X-Sharer-User-Id", 1)) + .andExpect(status().isForbidden()) + .andDo(MockMvcResultHandlers.print()); } } diff --git a/server/src/test/java/ru/practicum/shareit/item/service/ItemServiceTest.java b/server/src/test/java/ru/practicum/shareit/item/service/ItemServiceTest.java deleted file mode 100644 index 7e5cc14..0000000 --- a/server/src/test/java/ru/practicum/shareit/item/service/ItemServiceTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package ru.practicum.shareit.item.service; - -import org.junit.jupiter.api.Test; - -class ItemServiceTest { - - @Test - void getItem() { - } - - @Test - void getAllItemsOfUser() { - } - - @Test - void search() { - } - - @Test - void addNewItem() { - } - - @Test - void updateItem() { - } - - @Test - void deleteItem() { - } - - @Test - void addComment() { - } -} \ No newline at end of file From b5f889d2abd70062ee6fd99154656231a2dbfd9c Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Mon, 13 Apr 2026 14:07:26 +0300 Subject: [PATCH 06/28] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D0=B1=D0=B0=D0=B3=20=D0=BF=D0=BE=D0=B8?= =?UTF-8?q?=D1=81=D0=BA=D0=B0=20=D0=B1=D1=80=D0=BE=D0=BD=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D0=B9=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ItemServiceImpl.addComment --- .../shareit/item/service/ItemServiceImpl.java | 14 +- .../java/ru/practicum/shareit/TestUtils.java | 10 + .../shareit/item/ItemIntegationTest.java | 234 +++++++++++++++++- 3 files changed, 246 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java b/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java index a40755c..d493700 100644 --- a/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java +++ b/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java @@ -130,21 +130,23 @@ public void deleteItem(long userId, long itemId) { @Override public CommentDto addComment(long userId, long itemId, @NonNull CommentDto commentDto) { - User user = utilService.getUser(userId); + User booker = utilService.getUser(userId); Item item = utilService.getItem(itemId); if (item.getOwner().getId().equals(userId)) { throw new ForbiddenException("Нельзя оставить комментарий к своей вещи"); } - if (bookingRepository.findAllBookingByItem_Id(item.getId()).stream() - .noneMatch(booking -> booking.getBooker().getId().equals(userId) && - booking.getEnd().isBefore(LocalDateTime.now())) - ) { + + List bookings = bookingRepository.findAllBookingByItem_Id(item.getId()).stream() + .filter(booking -> booking.getBooker().getId() == userId) + .toList(); + + if (bookings.stream().noneMatch(booking -> booking.getEnd().isBefore(LocalDateTime.now()))) { throw new ParameterNotValidException( "userId", "Комментарии доступны только тем, кто уже по пользовался вещью" ); } return CommentMapper.toDto(commentRepository.save( - CommentMapper.toComment(commentDto, item, user) + CommentMapper.toComment(commentDto, item, booker) ) ); } diff --git a/server/src/test/java/ru/practicum/shareit/TestUtils.java b/server/src/test/java/ru/practicum/shareit/TestUtils.java index d1f93e3..03221de 100644 --- a/server/src/test/java/ru/practicum/shareit/TestUtils.java +++ b/server/src/test/java/ru/practicum/shareit/TestUtils.java @@ -132,4 +132,14 @@ public Booking makeNewAnyFullBooking(int id, User booker, User owner, LocalDateT .setEnd(date.plusHours(10)) .setStatus(status); } + + public Item getCopyOfItem(Item item) { + return new Item() + .setId(item.getId()) + .setName(item.getName()) + .setDescription(item.getDescription()) + .setAvailable(item.isAvailable()) + .setOwner(item.getOwner()) + .setRequest(item.getRequest()); + } } diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java index 22281fa..4bd87c3 100644 --- a/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java +++ b/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java @@ -8,6 +8,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -19,6 +20,10 @@ import ru.practicum.shareit.booking.model.Booking; import ru.practicum.shareit.booking.model.BookingStatus; import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.dto.CommentMapper; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemMapper; import ru.practicum.shareit.item.model.Comment; import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.item.repository.CommentRepository; @@ -31,13 +36,17 @@ import ru.practicum.shareit.util.UtilService; import ru.practicum.shareit.util.error.ErrorHandler; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; import static org.hamcrest.Matchers.*; import static org.mockito.Mockito.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -237,7 +246,7 @@ void whenTextIsValid_returnsListWithActualItems() { when(itemRepository.search(item1.getName())).thenReturn(List.of(item1, item2)); mvc.perform(get("/items/search?text={text}", item1.getName()) - .header("X-Sharer-User-Id", seacherId)) + .header("X-Sharer-User-Id", seacherId)) .andExpect(status().isOk()) .andDo(MockMvcResultHandlers.print()) .andExpect(jsonPath("$", hasSize(2))); @@ -272,7 +281,57 @@ class AddNewItem { @Test @SneakyThrows - void addNewItem() { + void whenRequestIsNotNull_returnItemWithRequestId() { + Item item = testUtils.makeNewFastItem(10); + + when(userRepository.findById(item.getOwner().getId())).thenReturn(Optional.of(item.getOwner())); + when(itemRequestRepository.findById(item.getRequest().getId())) + .thenReturn(Optional.ofNullable(item.getRequest())); + when(itemRepository.save(item)).thenReturn(item); + + // region mvc test + mvc.perform(post("/items") + .header("X-Sharer-User-Id", item.getOwner().getId()) + .content(mapper.writeValueAsString(ItemMapper.toDto(item))) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andExpect(jsonPath("$.id", is(item.getId()), Long.class)) + .andExpect(jsonPath("$.name", is(item.getName()), String.class)) + .andExpect(jsonPath("$.description", is(item.getDescription()), String.class)) + .andExpect(jsonPath("$.available", is(item.isAvailable()), Boolean.class)) + .andExpect(jsonPath("$.requestId", is(item.getRequest().getId()), Long.class)); + // endregion mvc test + } + + @Test + @SneakyThrows + void whenRequestIsNull_returnItemWithNullableRequestId() { + Item item = testUtils.makeNewFastItem(10); + item.setRequest(null); + + when(userRepository.findById(item.getOwner().getId())).thenReturn(Optional.of(item.getOwner())); + when(itemRepository.save(item)).thenReturn(item); + + // region mvc test + mvc.perform(post("/items") + .header("X-Sharer-User-Id", item.getOwner().getId()) + .content(mapper.writeValueAsString(ItemMapper.toDto(item))) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andExpect(jsonPath("$.id", is(item.getId()), Long.class)) + .andExpect(jsonPath("$.name", is(item.getName()), String.class)) + .andExpect(jsonPath("$.description", is(item.getDescription()), String.class)) + .andExpect(jsonPath("$.available", is(item.isAvailable()), Boolean.class)) + .andExpect(jsonPath("$.requestId", nullValue())); + // endregion mvc test + + verify(itemRequestRepository, never()).findById(anyLong()); } } @@ -281,7 +340,59 @@ class UpdateItem { @Test @SneakyThrows - void updateItem() { + void whenUserIsOwnerOfItem_thenReturnsUpdatableItem() { + Item item = testUtils.makeNewFastItem(10); + ItemDto itemDto = ItemDto.builder().name("this is new name").build(); + Item newItem = testUtils.getCopyOfItem(item).setName(itemDto.getName()); + + when(userRepository.existsById(item.getOwner().getId())).thenReturn(true); + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + when(userRepository.findById(item.getOwner().getId())).thenReturn(Optional.of(item.getOwner())); + when(itemRequestRepository.findById(item.getRequest().getId())) + .thenReturn(Optional.ofNullable(item.getRequest())); + when(itemRepository.save(newItem)).thenReturn(newItem); + + // region mvc test + mvc.perform(patch("/items/{itemId}", item.getId()) + .header("X-Sharer-User-Id", item.getOwner().getId()) + .content(mapper.writeValueAsString(itemDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andExpect(jsonPath("$.id", is(newItem.getId()), Long.class)) + .andExpect(jsonPath("$.name", is(newItem.getName()), String.class)) + .andExpect(jsonPath("$.description", is(newItem.getDescription()), String.class)) + .andExpect(jsonPath("$.available", is(newItem.isAvailable()), Boolean.class)) + .andExpect(jsonPath("$.requestId", is(newItem.getRequest().getId()), Long.class)); + // endregion mvc test + } + + @Test + @SneakyThrows + void whenUserIsNotOwnerOfItem_thenReturnsResponseWithStatuNotFound() { + Item item = testUtils.makeNewFastItem(10); + ItemDto itemDto = ItemDto.builder().name("this is new name").build(); + Item newItem = testUtils.getCopyOfItem(item).setName(itemDto.getName()); + + when(userRepository.existsById(item.getOwner().getId() + 1)).thenReturn(true); + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + + // region mvc test + mvc.perform(patch("/items/{itemId}", item.getId()) + .header("X-Sharer-User-Id", item.getOwner().getId() + 1) + .content(mapper.writeValueAsString(itemDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andDo(MockMvcResultHandlers.print()); + // endregion mvc test + + verify(userRepository, never()).findById(item.getOwner().getId() + 1); + verify(itemRequestRepository, never()).findById(item.getRequest().getId()); + verify(itemRepository, never()).save(newItem); } } @@ -290,7 +401,37 @@ class DeleteItem { @Test @SneakyThrows - void deleteItem() { + void wnenUserIsOwnerOfItem_thenReturnsResponseWithStatusOk() { + Item item = testUtils.makeNewFastItem(10); + when(userRepository.existsById(item.getOwner().getId())).thenReturn(true); + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + doNothing().when(itemRepository).deleteById(item.getId()); + + // region mvc test + mvc.perform(delete("/items/{itemId}", item.getId()) + .header("X-Sharer-User-Id", item.getOwner().getId()) + ) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()); + // endregion mvc test + } + + @Test + @SneakyThrows + void wnenUserIsNotOwnerOfItem_thenReturnsResponseWithStatuNotFound() { + Item item = testUtils.makeNewFastItem(10); + when(userRepository.existsById(item.getOwner().getId() + 1)).thenReturn(true); + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + + // region mvc test + mvc.perform(delete("/items/{itemId}", item.getId()) + .header("X-Sharer-User-Id", item.getOwner().getId() + 1) + ) + .andExpect(status().isNotFound()) + .andDo(MockMvcResultHandlers.print()); + // endregion mvc test + + verify(itemRepository, never()).deleteById(item.getId()); } } @@ -299,7 +440,88 @@ class AddComment { @Test @SneakyThrows - void addComment() { + void whenUserIsNotOwnerOfItem_thenReturnsResponseWithStatusOkAndBodyOfActualComment() { + Booking booking = testUtils.makeNewAnyFullFastBooking(10, testUtils.pastDate, BookingStatus.APPROVED); + Item item = booking.getItem(); + User booker = booking.getBooker(); + String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")); + Comment comment = testUtils.makeNewComment(100, item, booker, LocalDateTime.parse(date)); + CommentDto commentDto = CommentMapper.toDto(comment); + + when(userRepository.findById(booker.getId())).thenReturn(Optional.of(booker)); + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + when(bookingRepository.findAllBookingByItem_Id(item.getId())) + .thenReturn(Collections.singletonList(booking)); + when(commentRepository.save(comment)).thenReturn(comment); + + // region mvc test + mvc.perform(post("/items/{itemId}/comment", item.getId()) + .header("X-Sharer-User-Id", booker.getId()) + .content(mapper.writeValueAsString(commentDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andExpect(jsonPath("$.id", is(comment.getId()), Long.class)) + .andExpect(jsonPath("$.itemId", is(item.getId()), Long.class)) + .andExpect(jsonPath("$.authorName", is(booker.getName()))) + .andExpect(jsonPath("$.created", is(comment.getCreatedDate().toString()))) + .andExpect(jsonPath("$.text", is(comment.getText()))); + // endregion mvc test + + verify(commentRepository).save(any()); + } + + @Test + @SneakyThrows + void whenUserIsOwnerOfItem_thenReturnsResponseWithStatusForbidden() { + User owner = testUtils.makeNewUser(10); + Item item = testUtils.makeNewItem(50, owner, null); + CommentDto commentDto = CommentDto.builder().text("this is comment").build(); + + when(userRepository.findById(owner.getId())).thenReturn(Optional.of(owner)); + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + + // region mvc test + mvc.perform(post("/items/{itemId}/comment", item.getId()) + .header("X-Sharer-User-Id", owner.getId()) + .content(mapper.writeValueAsString(commentDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + // endregion mvc test + + verify(bookingRepository, never()).findAllBookingByItem_Id(item.getId()); + verify(commentRepository, never()).save(any()); + } + + @Test + @SneakyThrows + void whenBookingIsNotCompleted_thenReturnsResponseWithStatusBadRequest() { + Booking booking = testUtils.makeNewAnyFullFastBooking(10, testUtils.pastDate, BookingStatus.APPROVED); + booking.setEnd(testUtils.futureDate); + Item item = booking.getItem(); + User booker = booking.getBooker(); + CommentDto commentDto = CommentDto.builder().text("this is comment").build(); + + when(userRepository.findById(booker.getId())).thenReturn(Optional.of(booker)); + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + when(bookingRepository.findAllBookingByItem_Id(item.getId())) + .thenReturn(Collections.singletonList(booking)); + + // region mvc test + mvc.perform(post("/items/{itemId}/comment", item.getId()) + .header("X-Sharer-User-Id", booker.getId()) + .content(mapper.writeValueAsString(commentDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + // endregion mvc test + + verify(commentRepository, never()).save(any()); } } } From 06ed27720150b1fdc7ced8b0913f0b276056b058 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Mon, 13 Apr 2026 14:44:42 +0300 Subject: [PATCH 07/28] =?UTF-8?q?=D0=9D=D0=B5=D0=B1=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D1=88=D0=B0=D1=8F=20=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BF=D0=BE=D0=B8=D1=81?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B1=D1=80=D0=BE=D0=BD=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B9=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ItemServiceImpl.addComment --- .../shareit/item/service/ItemServiceImpl.java | 3 +- .../shareit/item/ItemIntegationTest.java | 38 ++++++++++++++++--- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java b/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java index d493700..d300ea6 100644 --- a/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java +++ b/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java @@ -140,7 +140,8 @@ public CommentDto addComment(long userId, long itemId, @NonNull CommentDto comme .filter(booking -> booking.getBooker().getId() == userId) .toList(); - if (bookings.stream().noneMatch(booking -> booking.getEnd().isBefore(LocalDateTime.now()))) { + if (bookings.isEmpty() || bookings.stream() + .noneMatch(booking -> booking.getEnd().isBefore(LocalDateTime.now()))) { throw new ParameterNotValidException( "userId", "Комментарии доступны только тем, кто уже по пользовался вещью" ); diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java index 4bd87c3..2d0772e 100644 --- a/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java +++ b/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java @@ -243,7 +243,7 @@ void whenTextIsValid_returnsListWithActualItems() { Long seacherId = item3.getOwner().getId(); when(userRepository.existsById(seacherId)).thenReturn(true); - when(itemRepository.search(item1.getName())).thenReturn(List.of(item1, item2)); + when(itemRepository.search(item1.getName())).thenReturn(List.of(item1, item2, item3)); mvc.perform(get("/items/search?text={text}", item1.getName()) .header("X-Sharer-User-Id", seacherId)) @@ -441,9 +441,11 @@ class AddComment { @Test @SneakyThrows void whenUserIsNotOwnerOfItem_thenReturnsResponseWithStatusOkAndBodyOfActualComment() { - Booking booking = testUtils.makeNewAnyFullFastBooking(10, testUtils.pastDate, BookingStatus.APPROVED); - Item item = booking.getItem(); - User booker = booking.getBooker(); + Booking booking1 = testUtils.makeNewAnyFullFastBooking(10, testUtils.pastDate, BookingStatus.APPROVED); + Booking booking2 = testUtils.makeNewAnyFullFastBooking(11, testUtils.pastDate, BookingStatus.APPROVED); + booking2.setItem(booking1.getItem()); + Item item = booking1.getItem(); + User booker = booking1.getBooker(); String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")); Comment comment = testUtils.makeNewComment(100, item, booker, LocalDateTime.parse(date)); CommentDto commentDto = CommentMapper.toDto(comment); @@ -451,7 +453,7 @@ void whenUserIsNotOwnerOfItem_thenReturnsResponseWithStatusOkAndBodyOfActualComm when(userRepository.findById(booker.getId())).thenReturn(Optional.of(booker)); when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); when(bookingRepository.findAllBookingByItem_Id(item.getId())) - .thenReturn(Collections.singletonList(booking)); + .thenReturn(List.of(booking1, booking2)); when(commentRepository.save(comment)).thenReturn(comment); // region mvc test @@ -523,5 +525,31 @@ void whenBookingIsNotCompleted_thenReturnsResponseWithStatusBadRequest() { verify(commentRepository, never()).save(any()); } + + @Test + @SneakyThrows + void whenBookingIsLost_thenReturnsResponseWithStatusBadRequest() { + User booker = testUtils.makeNewUser(10); + User owner = testUtils.makeNewUser(11); + Item item = testUtils.makeNewItem(50, owner, null); + CommentDto commentDto = CommentDto.builder().text("this is comment").build(); + + when(userRepository.findById(booker.getId())).thenReturn(Optional.of(booker)); + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + when(bookingRepository.findAllBookingByItem_Id(item.getId())) + .thenReturn(Collections.emptyList()); + + // region mvc test + mvc.perform(post("/items/{itemId}/comment", item.getId()) + .header("X-Sharer-User-Id", booker.getId()) + .content(mapper.writeValueAsString(commentDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + // endregion mvc test + + verify(commentRepository, never()).save(any()); + } } } From b4a4588facfb4512f09593dfc245f3dd514bc391 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Mon, 13 Apr 2026 22:33:35 +0300 Subject: [PATCH 08/28] =?UTF-8?q?QueryDsl=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D1=81=D0=B5=D0=BD=20=D0=B2=20=D0=BE=D1=82=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D1=80=D0=B5=D0=BF=D0=BE=D0=B7?= =?UTF-8?q?=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/pom.xml | 2 +- .../shareit/request/dto/ItemRequestDto.java | 3 - .../request/dto/ItemRequestMapper.java | 8 +- .../shareit/request/model/ItemRequest.java | 21 ++- .../repository/QueryDslRepository.java | 47 +++++ .../service/ItemRequestServiceImpl.java | 31 +--- .../exception/ParameterNotValidException.java | 4 +- .../java/ru/practicum/shareit/TestUtils.java | 2 +- .../shareit/item/ItemIntegationTest.java | 14 +- .../request/ItemRequestIntegrationTest.java | 164 ++++++++++++++++++ .../util/config/QuerydslTestConfig.java | 15 ++ 11 files changed, 267 insertions(+), 44 deletions(-) create mode 100644 server/src/main/java/ru/practicum/shareit/request/repository/QueryDslRepository.java create mode 100644 server/src/test/java/ru/practicum/shareit/request/ItemRequestIntegrationTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/util/config/QuerydslTestConfig.java diff --git a/server/pom.xml b/server/pom.xml index 59e10d4..4bb3f80 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -62,7 +62,7 @@ querydsl-jpa jakarta ${querydsl.version} - provided + compile org.zalando diff --git a/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java index 06880f3..e8063b0 100644 --- a/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java +++ b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java @@ -8,9 +8,6 @@ import java.time.LocalDateTime; -/** - * TODO Sprint add-item-requests. - */ @Data @Builder @FieldDefaults(makeFinal = true) diff --git a/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestMapper.java b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestMapper.java index 76d5c40..5138d01 100644 --- a/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestMapper.java +++ b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestMapper.java @@ -1,5 +1,6 @@ package ru.practicum.shareit.request.dto; +import org.springframework.lang.NonNull; import ru.practicum.shareit.item.dto.ItemMapper; import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.request.model.ItemRequest; @@ -11,7 +12,7 @@ public class ItemRequestMapper { - public static ItemRequestDto toDto(ItemRequest itemRequest) { + public static ItemRequestDto toDto(@NonNull ItemRequest itemRequest) { return ItemRequestDto.builder() .id(itemRequest.getId()) .description(itemRequest.getDescription()) @@ -20,7 +21,7 @@ public static ItemRequestDto toDto(ItemRequest itemRequest) { .build(); } - public static ItemRequestBigDto toBigDto(ItemRequest itemRequest, List items) { + public static ItemRequestBigDto toBigDto(@NonNull ItemRequest itemRequest, @NonNull List items) { return ItemRequestBigDto.builder() .id(itemRequest.getId()) .description(itemRequest.getDescription()) @@ -30,8 +31,9 @@ public static ItemRequestBigDto toBigDto(ItemRequest itemRequest, List ite .build(); } - public static ItemRequest toEntity(ItemRequestDto requestDto, User requestor) { + public static ItemRequest toEntity(@NonNull ItemRequestDto requestDto, User requestor) { return new ItemRequest() + .setId(requestDto.getId()) .setDescription(requestDto.getDescription()) .setRequestor(requestor) .setCreatedDate(requestDto.getCreatedDate() == null ? diff --git a/server/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java b/server/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java index 26e97b4..201d3a8 100644 --- a/server/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java +++ b/server/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java @@ -8,6 +8,9 @@ import ru.practicum.shareit.util.hibernate.HibernateEqualsAndHashCode; import java.time.LocalDateTime; +import java.util.Objects; + +import static ru.practicum.shareit.util.hibernate.HibernateEqualsAndHashCode.*; @Data @Entity @@ -19,7 +22,7 @@ public class ItemRequest { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - long id; + Long id; String description; @@ -29,4 +32,20 @@ public class ItemRequest { @Column(name = "created", nullable = false) LocalDateTime createdDate; + + //region equals and hashCode + @Override + public final boolean equals(Object object) { + return this == object + || object != null + && persistentClass(this) == object.persistentClass() + && object instanceof ItemRequest request + && Objects.equals(getId(), request.getId()); + } + + @Override + public final int hashCode() { + return persistentClass(this).hashCode(); + } + //endregion } diff --git a/server/src/main/java/ru/practicum/shareit/request/repository/QueryDslRepository.java b/server/src/main/java/ru/practicum/shareit/request/repository/QueryDslRepository.java new file mode 100644 index 0000000..2cc7e58 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/repository/QueryDslRepository.java @@ -0,0 +1,47 @@ +package ru.practicum.shareit.request.repository; + +import com.querydsl.jpa.JPQLQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.model.QItem; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestMapper; +import ru.practicum.shareit.request.model.QItemRequest; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Repository +@RequiredArgsConstructor +public class QueryDslRepository { + + final JPQLQueryFactory queryFactory; + + final QItem qItem = QItem.item; + final QItemRequest qItemRequest = QItemRequest.itemRequest; + + public Map> getMapOfItemsByRequestId(List requestIds) { + return queryFactory + .selectFrom(qItem) + .where(qItem.request.id.in(requestIds)) + .fetch() + .stream() + .collect(Collectors.groupingBy(item -> item.getRequest().getId())); + } + + public List getAllRequestsWitoutByUser(Long userId) { + return queryFactory.selectFrom(qItemRequest) + .where(qItemRequest.requestor.id.notIn(userId)) + .orderBy(qItemRequest.createdDate.desc()) + .fetch() + .stream() + .map(ItemRequestMapper::toDto) + .toList(); + } + + public List getAllItemsByRequest(Long requestId) { + return queryFactory.selectFrom(qItem).where(qItem.request.id.eq(requestId)).fetch(); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java index 2384bf7..0f778b2 100644 --- a/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java +++ b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java @@ -1,15 +1,13 @@ package ru.practicum.shareit.request.service; -import com.querydsl.jpa.JPQLQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import ru.practicum.shareit.item.model.Item; -import ru.practicum.shareit.item.model.QItem; import ru.practicum.shareit.request.dto.ItemRequestBigDto; import ru.practicum.shareit.request.dto.ItemRequestDto; import ru.practicum.shareit.request.dto.ItemRequestMapper; import ru.practicum.shareit.request.model.ItemRequest; -import ru.practicum.shareit.request.model.QItemRequest; +import ru.practicum.shareit.request.repository.QueryDslRepository; import ru.practicum.shareit.request.repository.ItemRequestRepository; import ru.practicum.shareit.user.model.User; import ru.practicum.shareit.util.UtilService; @@ -18,22 +16,21 @@ import java.util.Comparator; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class ItemRequestServiceImpl implements ItemRequestService { final ItemRequestRepository itemRequestRepository; - final JPQLQueryFactory queryFactory; + final QueryDslRepository queryDslRepository; final UtilService utilService; @Override public ItemRequestDto addNewRequest(long userId, ItemRequestDto requestDto) { User user = utilService.getUser(userId); ItemRequest itemRequest = ItemRequestMapper.toEntity(requestDto, user); - itemRequest = itemRequestRepository.save(itemRequest); - return ItemRequestMapper.toDto(itemRequest); + ItemRequest resultRequest = itemRequestRepository.save(itemRequest); + return ItemRequestMapper.toDto(resultRequest); } @Override @@ -49,13 +46,7 @@ public List getRequestListByUser(long userId) { .map(ItemRequest::getId) .toList(); - QItem qItem = QItem.item; - Map> itemsByRequestId = queryFactory - .selectFrom(qItem) - .where(qItem.request.id.in(requestIds)) - .fetch() - .stream() - .collect(Collectors.groupingBy(item -> item.getRequest().getId())); + Map> itemsByRequestId = queryDslRepository.getMapOfItemsByRequestId(requestIds); return itemRequests.stream() .map(itemRequest -> ItemRequestMapper.toBigDto( @@ -69,20 +60,12 @@ public List getRequestListByUser(long userId) { @Override public List getAllRequests(long userId) { utilService.checkUser(userId); - QItemRequest qItemRequest = QItemRequest.itemRequest; - return queryFactory.selectFrom(qItemRequest) - .where(qItemRequest.requestor.id.notIn(userId)) - .orderBy(qItemRequest.createdDate.desc()) - .fetch() - .stream() - .map(ItemRequestMapper::toDto) - .toList(); + return queryDslRepository.getAllRequestsWitoutByUser(userId); } @Override public ItemRequestBigDto getRequestById(long requestId) { - QItem qItem = QItem.item; - List items = queryFactory.selectFrom(qItem).where(qItem.request.id.eq(requestId)).fetch(); + List items = queryDslRepository.getAllItemsByRequest(requestId); ItemRequest request = utilService.getItemRequest(requestId); return ItemRequestMapper.toBigDto(request, items); } diff --git a/server/src/main/java/ru/practicum/shareit/util/exception/ParameterNotValidException.java b/server/src/main/java/ru/practicum/shareit/util/exception/ParameterNotValidException.java index 9c3dc73..109e986 100644 --- a/server/src/main/java/ru/practicum/shareit/util/exception/ParameterNotValidException.java +++ b/server/src/main/java/ru/practicum/shareit/util/exception/ParameterNotValidException.java @@ -4,8 +4,8 @@ @Getter public class ParameterNotValidException extends IllegalArgumentException { - String parameter; - String reason; + final String parameter; + final String reason; public ParameterNotValidException(String parameter, String reason) { super(reason); diff --git a/server/src/test/java/ru/practicum/shareit/TestUtils.java b/server/src/test/java/ru/practicum/shareit/TestUtils.java index 03221de..50f3ee7 100644 --- a/server/src/test/java/ru/practicum/shareit/TestUtils.java +++ b/server/src/test/java/ru/practicum/shareit/TestUtils.java @@ -94,7 +94,7 @@ public Item makeNewFastItem(int id) { public ItemRequest makeNewItemRequest(int id, User requestor) { return new ItemRequest() - .setId(id) + .setId((long) id) .setDescription(generateRandomText(50, "", ' ', ' ', ' ', ' ')) .setCreatedDate(LocalDateTime.now().minusHours(1)) .setRequestor(requestor); diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java index 2d0772e..a279c22 100644 --- a/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java +++ b/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java @@ -28,12 +28,10 @@ import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.item.repository.CommentRepository; import ru.practicum.shareit.item.repository.ItemRepository; -import ru.practicum.shareit.item.service.ItemService; import ru.practicum.shareit.request.model.ItemRequest; import ru.practicum.shareit.request.repository.ItemRequestRepository; import ru.practicum.shareit.user.model.User; import ru.practicum.shareit.user.repository.UserRepository; -import ru.practicum.shareit.util.UtilService; import ru.practicum.shareit.util.error.ErrorHandler; import java.nio.charset.StandardCharsets; @@ -66,12 +64,6 @@ public class ItemIntegationTest { @Autowired ItemController itemController; - @Autowired - ItemService itemService; - - @Autowired - UtilService utilService; - @MockitoBean UserRepository userRepository; @@ -153,7 +145,6 @@ void whenUserIdIsNotEqualsOwnerId_htenReturnsItemWithNullableBookingDates() { } } - @Nested class GetAllItemsOfUser { @@ -245,6 +236,7 @@ void whenTextIsValid_returnsListWithActualItems() { when(userRepository.existsById(seacherId)).thenReturn(true); when(itemRepository.search(item1.getName())).thenReturn(List.of(item1, item2, item3)); + // region mvc test mvc.perform(get("/items/search?text={text}", item1.getName()) .header("X-Sharer-User-Id", seacherId)) .andExpect(status().isOk()) @@ -257,11 +249,13 @@ void whenTextIsValid_returnsListWithActualItems() { void whenTextIsEmpty_returnsEmptyList() { when(userRepository.existsById(1L)).thenReturn(true); + // region mvc test mvc.perform(get("/items/search?text={text}", "") .header("X-Sharer-User-Id", 1)) .andExpect(status().isOk()) .andDo(MockMvcResultHandlers.print()) .andExpect(jsonPath("$", hasSize(0))); + // endregion mvc test } @Test @@ -269,10 +263,12 @@ void whenTextIsEmpty_returnsEmptyList() { void whenUserIsNotFound_returnsResponseStatusForbidden() { when(userRepository.existsById(1L)).thenReturn(false); + // region mvc test mvc.perform(get("/items/search?text={text}", "") .header("X-Sharer-User-Id", 1)) .andExpect(status().isForbidden()) .andDo(MockMvcResultHandlers.print()); + // endregion mvc test } } diff --git a/server/src/test/java/ru/practicum/shareit/request/ItemRequestIntegrationTest.java b/server/src/test/java/ru/practicum/shareit/request/ItemRequestIntegrationTest.java new file mode 100644 index 0000000..f79ca16 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/ItemRequestIntegrationTest.java @@ -0,0 +1,164 @@ +package ru.practicum.shareit.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.shareit.ShareItServer; +import ru.practicum.shareit.TestUtils; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.item.repository.CommentRepository; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestMapper; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.request.repository.ItemRequestRepository; +import ru.practicum.shareit.request.repository.QueryDslRepository; +import ru.practicum.shareit.request.service.ItemRequestService; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; +import ru.practicum.shareit.util.UtilService; +import ru.practicum.shareit.util.error.ErrorHandler; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ContextConfiguration(classes = ShareItServer.class) +class ItemRequestIntegrationTest { + + final TestUtils testUtils = new TestUtils(); + + @Autowired + ObjectMapper mapper; + + @Autowired + private MockMvc mvc; + + @Autowired + ItemRequestController itemRequestController; + + @Autowired + ItemRequestService itemRequestService; + + @Autowired + UtilService utilService; + + // region repositories + + @MockitoBean + UserRepository userRepository; + + @MockitoBean + ItemRepository itemRepository; + + @MockitoBean + ItemRequestRepository itemRequestRepository; + + @MockitoBean + BookingRepository bookingRepository; + + @MockitoBean + CommentRepository commentRepository; + + @MockitoBean + QueryDslRepository queryDslRepository; + + // endregion repositories + + @BeforeEach + void setUp() { + mvc = MockMvcBuilders + .standaloneSetup(itemRequestController) + .setControllerAdvice(new ErrorHandler()) + .setMessageConverters(new MappingJackson2HttpMessageConverter(mapper)) + .build(); + } + + @Nested + class AddNewRequest { + + @Test + @SneakyThrows + void addNewRequest() { + User requestor = testUtils.makeNewUser(1); + ItemRequest itemRequest = testUtils.makeNewItemRequest(1, requestor); + ItemRequestDto requestDto = ItemRequestDto.builder() + .description(itemRequest.getDescription()) + .createdDate(itemRequest.getCreatedDate()) + .build(); + + when(userRepository.findById(requestor.getId())).thenReturn(Optional.of(requestor)); + when(itemRequestRepository.save(ItemRequestMapper.toEntity(requestDto, requestor))).thenReturn(itemRequest); + + // region mvc test + String result = mvc.perform(post("/requests") + .header("X-Sharer-User-Id", requestor.getId()) + .content(mapper.writeValueAsString(requestDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse() + .getContentAsString(); + // endregion mvc test + + assertEquals(result, mapper.writeValueAsString(ItemRequestMapper.toDto(itemRequest))); + } + } + + @Nested + class GetRequestListByUser { + + @Test + void getRequestListByUser() { + User requestor = testUtils.makeNewUser(1); + + List itemRequests = List.of( + testUtils.makeNewItemRequest(1, requestor), + testUtils.makeNewItemRequest(2, testUtils.makeNewUser(2)), + testUtils.makeNewItemRequest(3, requestor) + ); + + when(userRepository.existsById(requestor.getId())).thenReturn(true); + when(itemRequestRepository.findAllByRequestor_Id(requestor.getId())).thenReturn(itemRequests); + + } + } + + @Nested + class GetAllRequests { + + @Test + void getAllRequests() { + } + } + + @Nested + class GetRequestById { + + @Test + void getRequestById() { + } + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/util/config/QuerydslTestConfig.java b/server/src/test/java/ru/practicum/shareit/util/config/QuerydslTestConfig.java new file mode 100644 index 0000000..de2f169 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/util/config/QuerydslTestConfig.java @@ -0,0 +1,15 @@ +package ru.practicum.shareit.util.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class QuerydslTestConfig { + + @Bean + public JPAQueryFactory queryFactory(EntityManager em) { + return new JPAQueryFactory(em); + } +} From 3120077a289e78f9687145bf9f5a9ee8e86b0db8 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Tue, 14 Apr 2026 17:44:52 +0300 Subject: [PATCH 09/28] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D0=B1=D0=B0=D0=B3=20=D0=BE=D1=82=D1=81?= =?UTF-8?q?=D1=83=D1=82=D1=81=D1=82=D0=B2=D0=B8=D1=8F=20=D1=81=D0=BE=D1=85?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=B4=D1=82=D0=B2=D0=B5=D1=80=D0=B6=D0=B4=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B8=20=D0=B1=D1=80=D0=BE=D0=BD=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BookingServiceImpl.approveBooking --- server/pom.xml | 15 ++ .../booking/dto/IncomingBookingDto.java | 2 + .../booking/service/BookingServiceImpl.java | 15 +- .../repository/QueryDslRepository.java | 10 +- .../service/ItemRequestServiceImpl.java | 4 +- .../java/ru/practicum/shareit/TestUtils.java | 76 +++++- .../booking/BookingControllerTest.java | 197 +++++++++++++++ .../repository/BookingRepositoryTest.java | 7 +- .../service/BookingServiceImplTest.java | 236 ++++++++++++++++++ ...tegationTest.java => ItemContextTest.java} | 6 +- ...nTest.java => ItemRequestContextTest.java} | 2 +- .../repository/QueryDslRepositoryTest.java | 150 +++++++++++ .../user/repository/UserRepositoryTest.java | 7 +- .../shareit/user/service/UserServiceTest.java | 1 - 14 files changed, 693 insertions(+), 35 deletions(-) create mode 100644 server/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/booking/service/BookingServiceImplTest.java rename server/src/test/java/ru/practicum/shareit/item/{ItemIntegationTest.java => ItemContextTest.java} (99%) rename server/src/test/java/ru/practicum/shareit/request/{ItemRequestIntegrationTest.java => ItemRequestContextTest.java} (99%) create mode 100644 server/src/test/java/ru/practicum/shareit/request/repository/QueryDslRepositoryTest.java diff --git a/server/pom.xml b/server/pom.xml index 4bb3f80..7b7483b 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -112,6 +112,21 @@ org.jacoco jacoco-maven-plugin + + + report + prepare-package + + report + + + + + **/Q* + + + + diff --git a/server/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java b/server/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java index c224045..039af0d 100644 --- a/server/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java +++ b/server/src/main/java/ru/practicum/shareit/booking/dto/IncomingBookingDto.java @@ -1,10 +1,12 @@ package ru.practicum.shareit.booking.dto; +import lombok.AllArgsConstructor; import lombok.Data; import java.time.LocalDateTime; @Data +@AllArgsConstructor public class IncomingBookingDto { long itemId; LocalDateTime start; diff --git a/server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java b/server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java index d0feaac..f06db9b 100644 --- a/server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java +++ b/server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java @@ -28,16 +28,16 @@ public class BookingServiceImpl implements BookingService { UtilService utilService; @Override - public BookingDto addNewBooking(Long userId, @NonNull IncomingBookingDto bookingDto) { - LocalDateTime start = bookingDto.getStart(); - LocalDateTime end = bookingDto.getEnd(); + public BookingDto addNewBooking(Long userId, @NonNull IncomingBookingDto incomingBookingDto) { + LocalDateTime start = incomingBookingDto.getStart(); + LocalDateTime end = incomingBookingDto.getEnd(); if (start.isAfter(end) || start.isEqual(end)) { throw new ValidationException("Дата начала бронирования должна быть раньше даты окончания"); } User booker = utilService.getUser(userId); - long itemId = bookingDto.getItemId(); + long itemId = incomingBookingDto.getItemId(); Item item = utilService.getItem(itemId); if (item.getOwner().getId().equals(userId)) { throw new ValidationException("Нельзя забронировать свою-же вещь"); @@ -46,9 +46,8 @@ public BookingDto addNewBooking(Long userId, @NonNull IncomingBookingDto booking throw new ValidationException("Вещь с id=" + item.getId() + " не доступна для аренды"); } - return BookingMapper.toBookingDto( - bookingRepository.save(BookingMapper.fromIncomingDto(bookingDto, item, booker)) - ); + Booking bookingToSave = BookingMapper.fromIncomingDto(incomingBookingDto, item, booker); + return BookingMapper.toBookingDto(bookingRepository.save(bookingToSave)); } @Override @@ -75,7 +74,7 @@ public BookingDto approveBooking(long userId, long bookingId, boolean approved) case CANCELED -> throw new BookingStatusException("Бронирование уже было отменено создателем"); } - return BookingMapper.toBookingDto(booking); + return BookingMapper.toBookingDto(bookingRepository.save(booking)); } @Override diff --git a/server/src/main/java/ru/practicum/shareit/request/repository/QueryDslRepository.java b/server/src/main/java/ru/practicum/shareit/request/repository/QueryDslRepository.java index 2cc7e58..ecf2075 100644 --- a/server/src/main/java/ru/practicum/shareit/request/repository/QueryDslRepository.java +++ b/server/src/main/java/ru/practicum/shareit/request/repository/QueryDslRepository.java @@ -5,8 +5,7 @@ import org.springframework.stereotype.Repository; import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.item.model.QItem; -import ru.practicum.shareit.request.dto.ItemRequestDto; -import ru.practicum.shareit.request.dto.ItemRequestMapper; +import ru.practicum.shareit.request.model.ItemRequest; import ru.practicum.shareit.request.model.QItemRequest; import java.util.List; @@ -31,14 +30,11 @@ public Map> getMapOfItemsByRequestId(List requestIds) { .collect(Collectors.groupingBy(item -> item.getRequest().getId())); } - public List getAllRequestsWitoutByUser(Long userId) { + public List getAllRequestsWitoutByUser(Long userId) { return queryFactory.selectFrom(qItemRequest) .where(qItemRequest.requestor.id.notIn(userId)) .orderBy(qItemRequest.createdDate.desc()) - .fetch() - .stream() - .map(ItemRequestMapper::toDto) - .toList(); + .fetch(); } public List getAllItemsByRequest(Long requestId) { diff --git a/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java index 0f778b2..a940c70 100644 --- a/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java +++ b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java @@ -60,7 +60,9 @@ public List getRequestListByUser(long userId) { @Override public List getAllRequests(long userId) { utilService.checkUser(userId); - return queryDslRepository.getAllRequestsWitoutByUser(userId); + return queryDslRepository.getAllRequestsWitoutByUser(userId).stream() + .map(ItemRequestMapper::toDto) + .toList(); } @Override diff --git a/server/src/test/java/ru/practicum/shareit/TestUtils.java b/server/src/test/java/ru/practicum/shareit/TestUtils.java index 50f3ee7..481c772 100644 --- a/server/src/test/java/ru/practicum/shareit/TestUtils.java +++ b/server/src/test/java/ru/practicum/shareit/TestUtils.java @@ -88,6 +88,26 @@ public Item makeNewItem(int id, User owner, ItemRequest request) { .setRequest(request); } + public Item makeNewItem(long id, User owner, ItemRequest request) { + return new Item() + .setId(id) + .setName(generateRandomText(25, "", ' ', ' ')) + .setDescription(generateRandomText(50, "", ' ', ' ', ' ', ' ')) + .setAvailable(true) + .setOwner(owner) + .setRequest(request); + } + + public Item makeNewItem(User owner, ItemRequest request) { + return new Item() + .setId(null) + .setName(generateRandomText(25, "", ' ', ' ')) + .setDescription(generateRandomText(50, "", ' ', ' ', ' ', ' ')) + .setAvailable(true) + .setOwner(owner) + .setRequest(request); + } + public Item makeNewFastItem(int id) { return makeNewItem(id, makeNewUser(id + 1), makeNewItemRequest(id + 10, makeNewUser(id + 2))); } @@ -100,6 +120,22 @@ public ItemRequest makeNewItemRequest(int id, User requestor) { .setRequestor(requestor); } + public ItemRequest makeNewItemRequest(int id, User requestor, LocalDateTime createdDate) { + return new ItemRequest() + .setId(null) + .setDescription(generateRandomText(50, "", ' ', ' ', ' ', ' ')) + .setCreatedDate(createdDate) + .setRequestor(requestor); + } + + public ItemRequest makeNewItemRequest(User requestor, LocalDateTime createdDate) { + return new ItemRequest() + .setId(null) + .setDescription(generateRandomText(50, "", ' ', ' ', ' ', ' ')) + .setCreatedDate(createdDate) + .setRequestor(requestor); + } + public Comment makeNewComment(int id, Item item, User author, LocalDateTime date) { return new Comment() .setId((long) id) @@ -112,7 +148,20 @@ public Comment makeNewComment(int id, Item item, User author, LocalDateTime date public Booking makeNewAnyFullFastBooking(int id, LocalDateTime date, BookingStatus status) { User owner = makeNewUser(id + 10); User booker = makeNewUser(id + 11); - Item item = makeNewItem(id + 20, owner, makeNewItemRequest(id + 30, booker)); + Item item = makeNewItem(id + 20, owner, makeNewItemRequest(id + 30, booker, date.plusHours(11))); + return new Booking() + .setId((long) id) + .setItem(item) + .setBooker(booker) + .setStart(date) + .setEnd(date.plusHours(10)) + .setStatus(status); + } + + public Booking makeNewAnyFullFastBooking(int id, LocalDateTime date, BookingStatus status, ItemRequest request) { + User owner = makeNewUser(id + 10); + User booker = makeNewUser(id + 11); + Item item = makeNewItem(id + 20, owner, request); return new Booking() .setId((long) id) .setItem(item) @@ -122,6 +171,19 @@ public Booking makeNewAnyFullFastBooking(int id, LocalDateTime date, BookingStat .setStatus(status); } + public Booking makeNewAnyFullFastBooking(long id, LocalDateTime date, BookingStatus status, ItemRequest request) { + User owner = makeNewUser(id + 10); + User booker = makeNewUser(id + 11); + Item item = makeNewItem(id + 20, owner, request); + return new Booking() + .setId(id) + .setItem(item) + .setBooker(booker) + .setStart(date) + .setEnd(date.plusHours(10)) + .setStatus(status); + } + public Booking makeNewAnyFullBooking(int id, User booker, User owner, LocalDateTime date, BookingStatus status) { Item item = makeNewItem(id + 20, owner, makeNewItemRequest(id + 30, booker)); return new Booking() @@ -133,7 +195,7 @@ public Booking makeNewAnyFullBooking(int id, User booker, User owner, LocalDateT .setStatus(status); } - public Item getCopyOfItem(Item item) { + public Item makeCopyOfItem(Item item) { return new Item() .setId(item.getId()) .setName(item.getName()) @@ -142,4 +204,14 @@ public Item getCopyOfItem(Item item) { .setOwner(item.getOwner()) .setRequest(item.getRequest()); } + + public Booking makeCopyOfBooking(Booking booking) { + return new Booking() + .setId(booking.getId()) + .setItem(booking.getItem()) + .setBooker(booking.getBooker()) + .setStart(booking.getStart()) + .setEnd(booking.getEnd()) + .setStatus(booking.getStatus()); + } } diff --git a/server/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java b/server/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java new file mode 100644 index 0000000..d11d837 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java @@ -0,0 +1,197 @@ +package ru.practicum.shareit.booking; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.shareit.TestUtils; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingMapper; +import ru.practicum.shareit.booking.dto.IncomingBookingDto; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.booking.model.StateOfBooking; +import ru.practicum.shareit.booking.service.BookingService; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.util.UtilService; +import ru.practicum.shareit.util.error.ErrorHandler; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +@WebMvcTest(controllers = BookingController.class) +public class BookingControllerTest { + + @Autowired + ObjectMapper mapper; + + @Autowired + MockMvc mvc; + + @Autowired + BookingController bookingController; + + @MockitoBean + BookingService bookingService; + + @MockitoBean + UtilService utilService; + + final TestUtils testUtils = new TestUtils(); + + @BeforeEach + void setUp() { + mvc = MockMvcBuilders + .standaloneSetup(bookingController) + .setControllerAdvice(new ErrorHandler()) + .setMessageConverters(new MappingJackson2HttpMessageConverter(mapper)) + .build(); + } + + @Test + @SneakyThrows + void addNewBooking_whenBookingIsValid() { + Booking booking = testUtils.makeNewAnyFullFastBooking(1, testUtils.pastDate, BookingStatus.APPROVED); + BookingDto bookingDto = BookingMapper.toBookingDto(booking); + IncomingBookingDto incomingBookingDto = new IncomingBookingDto( + booking.getItem().getId(), booking.getStart(), booking.getEnd()); + + when(bookingService.addNewBooking(booking.getBooker().getId(), incomingBookingDto)).thenReturn(bookingDto); + + String result = mvc.perform(post("/bookings") + .header("X-Sharer-User-Id", booking.getBooker().getId()) + .content(mapper.writeValueAsString(incomingBookingDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(result, is(mapper.writeValueAsString(bookingDto))); + } + + @Test + @SneakyThrows + void getBooking_whenBookingIsValid() { + Booking booking = testUtils.makeNewAnyFullFastBooking(1, testUtils.pastDate, BookingStatus.APPROVED); + BookingDto bookingDto = BookingMapper.toBookingDto(booking); + + when(bookingService.getBooking(booking.getBooker().getId(), booking.getId())).thenReturn(bookingDto); + + String result = mvc.perform(get("/bookings/{bookingId}", booking.getId()) + .header("X-Sharer-User-Id", booking.getBooker().getId())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(result, is(mapper.writeValueAsString(bookingDto))); + } + + @Test + @SneakyThrows + void getAllBookingsByBooker_whenBookingIsValid() { + Booking booking1 = testUtils.makeNewAnyFullFastBooking(1, testUtils.pastDate, BookingStatus.APPROVED); + Booking booking2 = testUtils.makeNewAnyFullFastBooking(2, testUtils.pastDate, BookingStatus.APPROVED); + booking2.setBooker(booking1.getBooker()); + List bookingDtos = List.of( + BookingMapper.toBookingDto(booking1), + BookingMapper.toBookingDto(booking2) + ); + StateOfBooking state = StateOfBooking.ALL; + + when(bookingService.getAllBookingsByBooker(booking1.getBooker().getId(), String.valueOf(state))) + .thenReturn(bookingDtos); + + String result = mvc.perform(get("/bookings?state={state}", state) + .header("X-Sharer-User-Id", booking1.getBooker().getId())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(result, is(mapper.writeValueAsString(bookingDtos))); + } + + @Test + @SneakyThrows + void getAllBookingsByOwner_whenBookingIsValid() { + User owner = testUtils.makeNewUser(1); + Booking booking1 = testUtils.makeNewAnyFullBooking( + 10, + testUtils.makeNewUser(2), + owner, + testUtils.pastDate, + BookingStatus.APPROVED + ); + Booking booking2 = testUtils.makeNewAnyFullBooking( + 11, + testUtils.makeNewUser(3), + owner, + testUtils.futureDate, + BookingStatus.APPROVED + ); + List bookingDtos = List.of( + BookingMapper.toBookingDto(booking1), + BookingMapper.toBookingDto(booking2) + ); + StateOfBooking state = StateOfBooking.ALL; + + when(bookingService.getAllBookingsByOwner(owner.getId(), String.valueOf(state))) + .thenReturn(bookingDtos); + + String result = mvc.perform(get("/bookings/owner?state={state}", state) + .header("X-Sharer-User-Id", owner.getId())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(result, is(mapper.writeValueAsString(bookingDtos))); + } + + @Test + @SneakyThrows + void approveBooking_whenBookingIsValid() { + Booking booking = testUtils.makeNewAnyFullFastBooking(1, testUtils.pastDate, BookingStatus.APPROVED); + BookingDto bookingDto = BookingMapper.toBookingDto(booking); + + + when(bookingService.approveBooking(booking.getItem().getOwner().getId(), booking.getId(), true)) + .thenReturn(bookingDto); + + String result = mvc.perform( + patch("/bookings/{bookingId}?approved={approved}", booking.getId(), true) + .header("X-Sharer-User-Id", booking.getItem().getOwner().getId())) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(result, is(mapper.writeValueAsString(bookingDto))); + } + +} diff --git a/server/src/test/java/ru/practicum/shareit/booking/repository/BookingRepositoryTest.java b/server/src/test/java/ru/practicum/shareit/booking/repository/BookingRepositoryTest.java index 6f99fdb..41931cd 100644 --- a/server/src/test/java/ru/practicum/shareit/booking/repository/BookingRepositoryTest.java +++ b/server/src/test/java/ru/practicum/shareit/booking/repository/BookingRepositoryTest.java @@ -1,6 +1,5 @@ package ru.practicum.shareit.booking.repository; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.hamcrest.Matcher; import org.junit.jupiter.api.Test; @@ -23,11 +22,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; -@Transactional -@DataJpaTest(properties = { - "spring.datasource.driver-class-name=org.h2.Driver", - "spring.datasource.url=jdbc:h2:mem:shareit" -}) +@DataJpaTest @RequiredArgsConstructor(onConstructor_ = @Autowired) class BookingRepositoryTest { diff --git a/server/src/test/java/ru/practicum/shareit/booking/service/BookingServiceImplTest.java b/server/src/test/java/ru/practicum/shareit/booking/service/BookingServiceImplTest.java new file mode 100644 index 0000000..7be6fab --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/service/BookingServiceImplTest.java @@ -0,0 +1,236 @@ +package ru.practicum.shareit.booking.service; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import ru.practicum.shareit.TestUtils; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingMapper; +import ru.practicum.shareit.booking.dto.IncomingBookingDto; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.user.service.UserService; +import ru.practicum.shareit.util.UtilService; +import ru.practicum.shareit.util.exception.ValidationException; + +import java.util.Optional; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class BookingServiceImplTest { + + @Mock + UserService userService; + + @Mock + UtilService utilService; + + @Mock + ItemRepository itemRepository; + + @Mock + BookingRepository bookingRepository; + + @InjectMocks + BookingServiceImpl bookingService; + + final TestUtils testUtils = new TestUtils(); + + @Nested + class AddNewBooking { + + @Test + void whenAllIsOk_thenReturnActualBookingDto() { + // region setup + Booking booking = testUtils.makeNewAnyFullFastBooking( + 1, + testUtils.futureDate, + BookingStatus.WAITING, + null + ); + BookingDto bookingDto = BookingMapper.toBookingDto(booking); + IncomingBookingDto incomingBookingDto = new IncomingBookingDto( + booking.getItem().getId(), booking.getStart(), booking.getEnd()); + Booking savedBooking = testUtils.makeCopyOfBooking(booking).setId(null); + // endregion setup + + when(utilService.getUser(booking.getBooker().getId())).thenReturn(booking.getBooker()); + when(utilService.getItem(booking.getItem().getId())).thenReturn(booking.getItem()); + when(bookingRepository.save(savedBooking)).thenReturn(booking); + + BookingDto resultDto = bookingService.addNewBooking(booking.getBooker().getId(), incomingBookingDto); + + assertThat(resultDto, equalTo(bookingDto)); + verify(bookingRepository).save(savedBooking); + } + + @Test + void whenStartDateIsAfterEndDate_thenReturnValidationException() { + // region setup + Booking booking = testUtils.makeNewAnyFullFastBooking( + 1, + testUtils.futureDate, + BookingStatus.WAITING, + null + ); + + // Breaking it here + booking.setStart(booking.getEnd().plusYears(1)); + + IncomingBookingDto incomingBookingDto = new IncomingBookingDto( + booking.getItem().getId(), booking.getStart(), booking.getEnd()); + // endregion setup + + assertThrows(ValidationException.class, () -> + bookingService.addNewBooking(booking.getBooker().getId(), incomingBookingDto) + ); + + verify(utilService, never()).getItem(anyLong()); + verify(utilService, never()).getUser(anyLong()); + verify(bookingRepository, never()).save(any(Booking.class)); + } + + @Test + void whenStartDateIsEqualEndDate_thenReturnValidationException() { + // region setup + Booking booking = testUtils.makeNewAnyFullFastBooking( + 1, + testUtils.futureDate, + BookingStatus.WAITING, + null + ); + + // Breaking it here + booking.setStart(booking.getEnd()); + + IncomingBookingDto incomingBookingDto = new IncomingBookingDto( + booking.getItem().getId(), booking.getStart(), booking.getEnd()); + // endregion setup + + assertThrows(ValidationException.class, () -> + bookingService.addNewBooking(booking.getBooker().getId(), incomingBookingDto) + ); + + verify(utilService, never()).getItem(anyLong()); + verify(utilService, never()).getUser(anyLong()); + verify(bookingRepository, never()).save(any(Booking.class)); + } + + @Test + void whenBookerIsEqualOwner_thenReturnValidationException() { + // region setup + Booking booking = testUtils.makeNewAnyFullFastBooking( + 1, + testUtils.futureDate, + BookingStatus.WAITING, + null + ); + + // Breaking it here + booking.setBooker(booking.getItem().getOwner()); + + IncomingBookingDto incomingBookingDto = new IncomingBookingDto( + booking.getItem().getId(), booking.getStart(), booking.getEnd()); + // endregion setup + + when(utilService.getUser(booking.getBooker().getId())).thenReturn(booking.getBooker()); + when(utilService.getItem(booking.getItem().getId())).thenReturn(booking.getItem()); + + assertThrows(ValidationException.class, () -> + bookingService.addNewBooking(booking.getBooker().getId(), incomingBookingDto) + ); + + verify(bookingRepository, never()).save(any(Booking.class)); + } + + @Test + void whenItemIsNotAvaialble_thenReturnValidationException() { + // region setup + Booking booking = testUtils.makeNewAnyFullFastBooking( + 1, + testUtils.futureDate, + BookingStatus.WAITING, + null + ); + + // Breaking it here + booking.setItem(booking.getItem().setAvailable(false)); + + IncomingBookingDto incomingBookingDto = new IncomingBookingDto( + booking.getItem().getId(), booking.getStart(), booking.getEnd()); + // endregion setup + + when(utilService.getUser(booking.getBooker().getId())).thenReturn(booking.getBooker()); + when(utilService.getItem(booking.getItem().getId())).thenReturn(booking.getItem()); + + assertThrows(ValidationException.class, () -> + bookingService.addNewBooking(booking.getBooker().getId(), incomingBookingDto) + ); + + verify(bookingRepository, never()).save(any(Booking.class)); + } + } + + @Nested + class ApproveBooking { + + @Test + void whenAllIsOk_thenReturnActualBookingDto() { + // region setup + long bookingId = 1L; + Booking approovedBooking = testUtils.makeNewAnyFullFastBooking( + bookingId, + testUtils.futureDate, + BookingStatus.APPROVED, + null + ); + Long ownerId = approovedBooking.getBooker().getId(); + approovedBooking.setItem(approovedBooking.getItem().setOwner(approovedBooking.getBooker())); + Booking inBaseBooking = testUtils.makeCopyOfBooking(approovedBooking).setStatus(BookingStatus.WAITING); + BookingDto bookingDto = BookingMapper.toBookingDto(approovedBooking); + // endregion setup + + doNothing().when(utilService).checkUser(ownerId); + when(bookingRepository.findById(bookingId)).thenReturn(Optional.of(inBaseBooking)); + when(bookingRepository.save(inBaseBooking)).thenReturn(approovedBooking); + + BookingDto resultDto = bookingService + .approveBooking(ownerId, bookingId, true); + + assertThat(resultDto, equalTo(bookingDto)); + } + } + + @Nested + class GetBooking { + + @Test + void testGetBooking() { + } + } + + @Nested + class GetAllBookingsByBooker { + + @Test + void testGetAllBookingsByBooker() { + } + } + + @Nested + class GetAllBookingsByOwner { + + @Test + void testGetAllBookingsByOwner() { + } + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemContextTest.java similarity index 99% rename from server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java rename to server/src/test/java/ru/practicum/shareit/item/ItemContextTest.java index a279c22..eda413e 100644 --- a/server/src/test/java/ru/practicum/shareit/item/ItemIntegationTest.java +++ b/server/src/test/java/ru/practicum/shareit/item/ItemContextTest.java @@ -51,7 +51,7 @@ @SpringBootTest @AutoConfigureMockMvc @ContextConfiguration(classes = ShareItServer.class) -public class ItemIntegationTest { +public class ItemContextTest { final TestUtils testUtils = new TestUtils(); @@ -339,7 +339,7 @@ class UpdateItem { void whenUserIsOwnerOfItem_thenReturnsUpdatableItem() { Item item = testUtils.makeNewFastItem(10); ItemDto itemDto = ItemDto.builder().name("this is new name").build(); - Item newItem = testUtils.getCopyOfItem(item).setName(itemDto.getName()); + Item newItem = testUtils.makeCopyOfItem(item).setName(itemDto.getName()); when(userRepository.existsById(item.getOwner().getId())).thenReturn(true); when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); @@ -370,7 +370,7 @@ void whenUserIsOwnerOfItem_thenReturnsUpdatableItem() { void whenUserIsNotOwnerOfItem_thenReturnsResponseWithStatuNotFound() { Item item = testUtils.makeNewFastItem(10); ItemDto itemDto = ItemDto.builder().name("this is new name").build(); - Item newItem = testUtils.getCopyOfItem(item).setName(itemDto.getName()); + Item newItem = testUtils.makeCopyOfItem(item).setName(itemDto.getName()); when(userRepository.existsById(item.getOwner().getId() + 1)).thenReturn(true); when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); diff --git a/server/src/test/java/ru/practicum/shareit/request/ItemRequestIntegrationTest.java b/server/src/test/java/ru/practicum/shareit/request/ItemRequestContextTest.java similarity index 99% rename from server/src/test/java/ru/practicum/shareit/request/ItemRequestIntegrationTest.java rename to server/src/test/java/ru/practicum/shareit/request/ItemRequestContextTest.java index f79ca16..3347bc4 100644 --- a/server/src/test/java/ru/practicum/shareit/request/ItemRequestIntegrationTest.java +++ b/server/src/test/java/ru/practicum/shareit/request/ItemRequestContextTest.java @@ -43,7 +43,7 @@ @SpringBootTest @AutoConfigureMockMvc @ContextConfiguration(classes = ShareItServer.class) -class ItemRequestIntegrationTest { +class ItemRequestContextTest { final TestUtils testUtils = new TestUtils(); diff --git a/server/src/test/java/ru/practicum/shareit/request/repository/QueryDslRepositoryTest.java b/server/src/test/java/ru/practicum/shareit/request/repository/QueryDslRepositoryTest.java new file mode 100644 index 0000000..c5564d1 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/repository/QueryDslRepositoryTest.java @@ -0,0 +1,150 @@ +package ru.practicum.shareit.request.repository; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import ru.practicum.shareit.TestUtils; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.util.config.QuerydslTestConfig; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@DataJpaTest +@Import({QuerydslTestConfig.class, QueryDslRepository.class}) +public class QueryDslRepositoryTest { + + @PersistenceContext + EntityManager entityManager; + + @Autowired + QueryDslRepository queryDslRepository; + + final TestUtils testUtils = new TestUtils(); + + private User addNewUser() { + User user = testUtils.makeNewUser(); + entityManager.persist(user); + return user; + } + + private ItemRequest addNewItemRequest(User requestor) { + ItemRequest itemRequest = testUtils.makeNewItemRequest(requestor, LocalDateTime.now()); + entityManager.persist(itemRequest); + return itemRequest; + } + + private Item addNewItem(User owner, ItemRequest request) { + Item item = testUtils.makeNewItem(owner, request); + entityManager.persist(item); + return item; + } + + @Test + void getMapOfItemsByRequestId() { + List users = List.of( + addNewUser(), + addNewUser(), + addNewUser(), + addNewUser(), + addNewUser(), + addNewUser() + ); + + List itemRequests = List.of( + addNewItemRequest(users.get(0)), + addNewItemRequest(users.get(1)), + addNewItemRequest(users.get(2)) + ); + + List items = List.of( + addNewItem(users.get(0), itemRequests.get(2)), + addNewItem(users.get(1), itemRequests.get(0)), + addNewItem(users.get(2), itemRequests.get(1)), + addNewItem(users.get(3), itemRequests.get(0)), + addNewItem(users.get(4), itemRequests.get(1)), + addNewItem(users.get(5), itemRequests.get(2)) + ); + + Map> localResult = new HashMap<>(); + for (Item item : items) { + localResult.computeIfAbsent( + item.getRequest().getId(), k -> new ArrayList<>()) + .add(item); + } + + List itemIds = localResult.keySet().stream().toList(); + + Map> repositoryResult = queryDslRepository.getMapOfItemsByRequestId(itemIds); + + assertThat(localResult, is(repositoryResult)); + } + + @Test + void getAllRequestsWitoutByUser() { + List users = List.of( + addNewUser(), + addNewUser(), + addNewUser() + ); + + List itemRequests = List.of( + addNewItemRequest(users.get(0)), + addNewItemRequest(users.get(1)), + addNewItemRequest(users.get(2)) + ); + + List result = queryDslRepository.getAllRequestsWitoutByUser(users.getFirst().getId()); + + assertThat(result, is(List.of(itemRequests.get(2), itemRequests.get(1)))); + } + + @Test + void getAllItemsByRequest() { + List users = List.of( + addNewUser(), + addNewUser(), + addNewUser() + ); + + List itemRequests = List.of( + addNewItemRequest(users.get(0)), + addNewItemRequest(users.get(1)) + ); + + List items = List.of( + addNewItem(users.get(0), itemRequests.getLast()), + addNewItem(users.get(1), itemRequests.getFirst()), + addNewItem(users.get(2), itemRequests.getLast()), + addNewItem(users.get(0), itemRequests.getFirst()), + addNewItem(users.get(1), itemRequests.getLast()), + addNewItem(users.get(2), itemRequests.getLast()) + ); + + List first = items.stream() + .filter(item -> item.getRequest().equals(itemRequests.getFirst())) + .toList(); + + List last = items.stream() + .filter(item -> item.getRequest().equals(itemRequests.getLast())) + .toList(); + + List resultFirst = queryDslRepository.getAllItemsByRequest(itemRequests.getFirst().getId()); + List resultLast = queryDslRepository.getAllItemsByRequest(itemRequests.getLast().getId()); + + assertThat(first, is(resultFirst)); + assertThat(last, is(resultLast)); + } +} + diff --git a/server/src/test/java/ru/practicum/shareit/user/repository/UserRepositoryTest.java b/server/src/test/java/ru/practicum/shareit/user/repository/UserRepositoryTest.java index f470309..9f404a5 100644 --- a/server/src/test/java/ru/practicum/shareit/user/repository/UserRepositoryTest.java +++ b/server/src/test/java/ru/practicum/shareit/user/repository/UserRepositoryTest.java @@ -1,6 +1,5 @@ package ru.practicum.shareit.user.repository; -import jakarta.transaction.Transactional; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; @@ -8,11 +7,7 @@ import static org.junit.jupiter.api.Assertions.*; -@Transactional -@DataJpaTest(properties = { - "spring.datasource.driver-class-name=org.h2.Driver", - "spring.datasource.url=jdbc:h2:mem:shareit" -}) +@DataJpaTest class UserRepositoryTest { @Autowired diff --git a/server/src/test/java/ru/practicum/shareit/user/service/UserServiceTest.java b/server/src/test/java/ru/practicum/shareit/user/service/UserServiceTest.java index 7af513c..7c44122 100644 --- a/server/src/test/java/ru/practicum/shareit/user/service/UserServiceTest.java +++ b/server/src/test/java/ru/practicum/shareit/user/service/UserServiceTest.java @@ -25,7 +25,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; - @ExtendWith(MockitoExtension.class) public class UserServiceTest { From 4003fd858f4d5ac0238083d276b196bae93ddd11 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Wed, 15 Apr 2026 14:00:15 +0300 Subject: [PATCH 10/28] =?UTF-8?q?=D0=9D=D0=B5=D0=B1=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D1=88=D0=BE=D0=B5=20=D1=83=D0=BF=D1=80=D0=BE=D1=89=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20StateOfBooking=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88?= =?UTF-8?q?=D0=B5=20=D0=BD=D0=B5=20=D0=B2=D0=BE=D0=B7=D0=B2=D1=80=D0=B0?= =?UTF-8?q?=D1=89=D0=B0=D0=B5=D1=82=20null?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StateOfBooking, BookingServiceImpl.getAllBookingsByBooker, BookingServiceImpl.getAllBookingsByOwner --- .../shareit/booking/model/StateOfBooking.java | 1 + .../booking/service/BookingServiceImpl.java | 16 +- .../service/BookingServiceImplTest.java | 777 +++++++++++++++++- 3 files changed, 766 insertions(+), 28 deletions(-) diff --git a/server/src/main/java/ru/practicum/shareit/booking/model/StateOfBooking.java b/server/src/main/java/ru/practicum/shareit/booking/model/StateOfBooking.java index 0190228..4929b0f 100644 --- a/server/src/main/java/ru/practicum/shareit/booking/model/StateOfBooking.java +++ b/server/src/main/java/ru/practicum/shareit/booking/model/StateOfBooking.java @@ -13,6 +13,7 @@ public enum StateOfBooking { @Getter private String invalidValue; + @NonNull public static StateOfBooking of(String text) { if (text == null) { return ALL; diff --git a/server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java b/server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java index f06db9b..db31c01 100644 --- a/server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java +++ b/server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java @@ -128,13 +128,9 @@ public List getAllBookingsByBooker(long userId, String state) { .map(BookingMapper::toBookingDto) .toList(); } - case null, default -> { - assert stateOfBooking != null; - throw new ParameterNotValidException( + default -> throw new ParameterNotValidException( "state = " + stateOfBooking.getInvalidValue(), - "допустимые значения: " + StateOfBooking.getValidValues() - ); - } + "допустимые значения: " + StateOfBooking.getValidValues()); } } @@ -179,13 +175,9 @@ public List getAllBookingsByOwner(long userId, String state) { .map(BookingMapper::toBookingDto) .toList(); } - case null, default -> { - assert stateOfBooking != null; - throw new ParameterNotValidException( + default -> throw new ParameterNotValidException( "state = " + stateOfBooking.getInvalidValue(), - "допустимые значения: " + StateOfBooking.getValidValues() - ); - } + "допустимые значения: " + StateOfBooking.getValidValues()); } } diff --git a/server/src/test/java/ru/practicum/shareit/booking/service/BookingServiceImplTest.java b/server/src/test/java/ru/practicum/shareit/booking/service/BookingServiceImplTest.java index 7be6fab..e8ac151 100644 --- a/server/src/test/java/ru/practicum/shareit/booking/service/BookingServiceImplTest.java +++ b/server/src/test/java/ru/practicum/shareit/booking/service/BookingServiceImplTest.java @@ -13,12 +13,12 @@ import ru.practicum.shareit.booking.model.Booking; import ru.practicum.shareit.booking.model.BookingStatus; import ru.practicum.shareit.booking.repository.BookingRepository; -import ru.practicum.shareit.item.repository.ItemRepository; -import ru.practicum.shareit.user.service.UserService; +import ru.practicum.shareit.user.model.User; import ru.practicum.shareit.util.UtilService; -import ru.practicum.shareit.util.exception.ValidationException; +import ru.practicum.shareit.util.exception.*; -import java.util.Optional; +import java.time.LocalDateTime; +import java.util.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -28,15 +28,9 @@ @ExtendWith(MockitoExtension.class) class BookingServiceImplTest { - @Mock - UserService userService; - @Mock UtilService utilService; - @Mock - ItemRepository itemRepository; - @Mock BookingRepository bookingRepository; @@ -184,7 +178,7 @@ void whenItemIsNotAvaialble_thenReturnValidationException() { class ApproveBooking { @Test - void whenAllIsOk_thenReturnActualBookingDto() { + void whenApproveIsTrue_andUserIsOwner_thenReturnApprovedBookingDto() { // region setup long bookingId = 1L; Booking approovedBooking = testUtils.makeNewAnyFullFastBooking( @@ -193,8 +187,7 @@ void whenAllIsOk_thenReturnActualBookingDto() { BookingStatus.APPROVED, null ); - Long ownerId = approovedBooking.getBooker().getId(); - approovedBooking.setItem(approovedBooking.getItem().setOwner(approovedBooking.getBooker())); + Long ownerId = approovedBooking.getItem().getOwner().getId(); Booking inBaseBooking = testUtils.makeCopyOfBooking(approovedBooking).setStatus(BookingStatus.WAITING); BookingDto bookingDto = BookingMapper.toBookingDto(approovedBooking); // endregion setup @@ -208,13 +201,243 @@ void whenAllIsOk_thenReturnActualBookingDto() { assertThat(resultDto, equalTo(bookingDto)); } + + @Test + void whenApproveIsTrue_andUserIsOwner_andBookingAlredyApprouved_thenReturnApprovedBookingDto() { + // region setup + long bookingId = 1L; + Booking approovedBooking = testUtils.makeNewAnyFullFastBooking( + bookingId, + testUtils.futureDate, + BookingStatus.APPROVED, + null + ); + Long ownerId = approovedBooking.getItem().getOwner().getId(); + Booking inBaseBooking = testUtils.makeCopyOfBooking(approovedBooking).setStatus(BookingStatus.APPROVED); + BookingDto bookingDto = BookingMapper.toBookingDto(approovedBooking); + // endregion setup + + doNothing().when(utilService).checkUser(ownerId); + when(bookingRepository.findById(bookingId)).thenReturn(Optional.of(inBaseBooking)); + when(bookingRepository.save(inBaseBooking)).thenReturn(approovedBooking); + + BookingDto resultDto = bookingService + .approveBooking(ownerId, bookingId, true); + + assertThat(resultDto, equalTo(bookingDto)); + } + + @Test + void whenApproveIsFalse_andUserIsOwner_thenReturnRejectedBookingDto() { + // region setup + long bookingId = 1L; + Booking approovedBooking = testUtils.makeNewAnyFullFastBooking( + bookingId, + testUtils.futureDate, + BookingStatus.REJECTED, + null + ); + Long ownerId = approovedBooking.getItem().getOwner().getId(); + Booking inBaseBooking = testUtils.makeCopyOfBooking(approovedBooking).setStatus(BookingStatus.WAITING); + BookingDto bookingDto = BookingMapper.toBookingDto(approovedBooking); + // endregion setup + + doNothing().when(utilService).checkUser(ownerId); + when(bookingRepository.findById(bookingId)).thenReturn(Optional.of(inBaseBooking)); + when(bookingRepository.save(inBaseBooking)).thenReturn(approovedBooking); + + BookingDto resultDto = bookingService + .approveBooking(ownerId, bookingId, false); + + assertThat(resultDto, equalTo(bookingDto)); + } + + @Test + void whenApproveIsTrue_butBookingWasRejected_thenStatusWillChanged_andReturnApprovedBookingDto() { + // region setup + long bookingId = 1L; + Booking approovedBooking = testUtils.makeNewAnyFullFastBooking( + bookingId, + testUtils.futureDate, + BookingStatus.APPROVED, + null + ); + Long ownerId = approovedBooking.getItem().getOwner().getId(); + Booking inBaseBooking = testUtils.makeCopyOfBooking(approovedBooking).setStatus(BookingStatus.REJECTED); + BookingDto bookingDto = BookingMapper.toBookingDto(approovedBooking); + // endregion setup + + doNothing().when(utilService).checkUser(ownerId); + when(bookingRepository.findById(bookingId)).thenReturn(Optional.of(inBaseBooking)); + when(bookingRepository.save(inBaseBooking)).thenReturn(approovedBooking); + + BookingDto resultDto = bookingService + .approveBooking(ownerId, bookingId, true); + + assertThat(resultDto, equalTo(bookingDto)); + } + + @Test + void whenApproveIsFalse_andBookingWasRejected_thenStatusWillChanged_andReturnRejectedBookingDto() { + // region setup + long bookingId = 1L; + Booking approovedBooking = testUtils.makeNewAnyFullFastBooking( + bookingId, + testUtils.futureDate, + BookingStatus.REJECTED, + null + ); + Long ownerId = approovedBooking.getItem().getOwner().getId(); + Booking inBaseBooking = testUtils.makeCopyOfBooking(approovedBooking).setStatus(BookingStatus.REJECTED); + BookingDto bookingDto = BookingMapper.toBookingDto(approovedBooking); + // endregion setup + + doNothing().when(utilService).checkUser(ownerId); + when(bookingRepository.findById(bookingId)).thenReturn(Optional.of(inBaseBooking)); + when(bookingRepository.save(inBaseBooking)).thenReturn(approovedBooking); + + BookingDto resultDto = bookingService + .approveBooking(ownerId, bookingId, false); + + assertThat(resultDto, equalTo(bookingDto)); + } + + @Test + void whenApproveIsTrue_butBookingIsCanceled_thenReturnBookingStatusException() { + // region setup + long bookingId = 1L; + Booking approovedBooking = testUtils.makeNewAnyFullFastBooking( + bookingId, + testUtils.futureDate, + BookingStatus.APPROVED, + null + ); + Long ownerId = approovedBooking.getItem().getOwner().getId(); + Booking inBaseBooking = testUtils.makeCopyOfBooking(approovedBooking).setStatus(BookingStatus.CANCELED); + // endregion setup + + doNothing().when(utilService).checkUser(ownerId); + when(bookingRepository.findById(bookingId)).thenReturn(Optional.of(inBaseBooking)); + + assertThrows(BookingStatusException.class, () -> bookingService + .approveBooking(ownerId, bookingId, true)); + + verify(bookingRepository, never()).save(any(Booking.class)); + } + + @Test + void whenUserIsNotAnOwnerAndNotABooker_thenReturnForbiddenException() { + // region setup + long bookingId = 1L; + Booking approovedBooking = testUtils.makeNewAnyFullFastBooking( + bookingId, + testUtils.futureDate, + BookingStatus.APPROVED, + null + ); + Long ownerId = approovedBooking.getItem().getOwner().getId(); + approovedBooking.setItem(approovedBooking.getItem().setOwner(testUtils.makeNewUser(500))); + approovedBooking.setBooker(testUtils.makeNewUser(501)); + Booking inBaseBooking = testUtils.makeCopyOfBooking(approovedBooking).setStatus(BookingStatus.WAITING); + // endregion setup + + doNothing().when(utilService).checkUser(ownerId); + when(bookingRepository.findById(bookingId)).thenReturn(Optional.of(inBaseBooking)); + + assertThrows(ForbiddenException.class, () -> bookingService + .approveBooking(ownerId, bookingId, true)); + + verify(bookingRepository, never()).save(any(Booking.class)); + } + + @Test + void whenBookingIsLost_thenReturnNotFoundException() { + // region setup + long bookingId = 1L; + Booking approovedBooking = testUtils.makeNewAnyFullFastBooking( + bookingId, + testUtils.futureDate, + BookingStatus.APPROVED, + null + ); + Long ownerId = approovedBooking.getItem().getOwner().getId(); + // endregion setup + + doNothing().when(utilService).checkUser(ownerId); + when(bookingRepository.findById(bookingId)).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> bookingService + .approveBooking(ownerId, bookingId, true)); + + verify(bookingRepository, never()).save(any(Booking.class)); + } + } @Nested class GetBooking { @Test - void testGetBooking() { + void whenUserIsOwner_thenReturnActualBookingDto() { + // region setup + long bookingId = 1L; + Booking booking = testUtils.makeNewAnyFullFastBooking( + bookingId, + testUtils.futureDate, + BookingStatus.APPROVED, + null + ); + Long ownerId = booking.getItem().getOwner().getId(); + BookingDto bookingDto = BookingMapper.toBookingDto(booking); + // endregion setup + + doNothing().when(utilService).checkUser(ownerId); + when(bookingRepository.findById(bookingId)).thenReturn(Optional.of(booking)); + + BookingDto resultDto = bookingService.getBooking(ownerId, bookingId); + + assertThat(resultDto, equalTo(bookingDto)); + } + + @Test + void whenUserIsBooker_thenReturnActualBookingDto() { + // region setup + long bookingId = 1L; + Booking booking = testUtils.makeNewAnyFullFastBooking( + bookingId, + testUtils.futureDate, + BookingStatus.APPROVED, + null + ); + Long bookerId = booking.getBooker().getId(); + BookingDto bookingDto = BookingMapper.toBookingDto(booking); + // endregion setup + + doNothing().when(utilService).checkUser(bookerId); + when(bookingRepository.findById(bookingId)).thenReturn(Optional.of(booking)); + + BookingDto resultDto = bookingService.getBooking(bookerId, bookingId); + + assertThat(resultDto, equalTo(bookingDto)); + } + + @Test + void whenUserIsNotBooker_andUserIsNotOwner_thenReturnForbiddenException() { + // region setup + long bookingId = 1L; + Booking booking = testUtils.makeNewAnyFullFastBooking( + bookingId, + testUtils.futureDate, + BookingStatus.APPROVED, + null + ); + Long userId = 500L; + // endregion setup + + doNothing().when(utilService).checkUser(userId); + when(bookingRepository.findById(bookingId)).thenReturn(Optional.of(booking)); + + assertThrows(ForbiddenException.class, () -> bookingService.getBooking(userId, bookingId)); } } @@ -222,7 +445,268 @@ void testGetBooking() { class GetAllBookingsByBooker { @Test - void testGetAllBookingsByBooker() { + void whenUserIsBooker_andStateIsAll_thenReturnListOfAllBookingDtosOfBooker() { + // region setup + User booker = testUtils.makeNewUser(1); + long bookerId = booker.getId(); + LocalDateTime now = LocalDateTime.now(); + List bookings = List.of( + // future + testUtils.makeNewAnyFullFastBooking( + 53, testUtils.futureDate, BookingStatus.WAITING, null), + // current + testUtils.makeNewAnyFullFastBooking( + 52, now.minusHours(5), BookingStatus.APPROVED, null), + // rejected + testUtils.makeNewAnyFullFastBooking( + 51, testUtils.pastDate.plusDays(30), BookingStatus.REJECTED, null), + // past + testUtils.makeNewAnyFullFastBooking( + 50, testUtils.pastDate, BookingStatus.APPROVED) + ); + bookings.forEach(booking -> booking.setBooker(booker)); + List bookingDtos = bookings.stream() + .map(BookingMapper::toBookingDto) + .toList(); + // endregion setup + + doNothing().when(utilService).checkUser(bookerId); + when(bookingRepository.findAllByBookerIdOrderByStartDesc(bookerId)).thenReturn(bookings); + + List resultDtoList = bookingService.getAllBookingsByBooker(bookerId, "all"); + + assertThat(resultDtoList, equalTo(bookingDtos)); + } + + @Test + void whenUserIsBooker_andStateIsNull_thenReturnListOfAllBookingDtosOfBooker() { + // region setup + User booker = testUtils.makeNewUser(1); + long bookerId = booker.getId(); + LocalDateTime now = LocalDateTime.now(); + List bookings = List.of( + // future + testUtils.makeNewAnyFullFastBooking( + 53, testUtils.futureDate, BookingStatus.WAITING, null), + // current + testUtils.makeNewAnyFullFastBooking( + 52, now.minusHours(5), BookingStatus.APPROVED, null), + // rejected + testUtils.makeNewAnyFullFastBooking( + 51, testUtils.pastDate.plusDays(30), BookingStatus.REJECTED, null), + // past + testUtils.makeNewAnyFullFastBooking( + 50, testUtils.pastDate, BookingStatus.APPROVED) + ); + bookings.forEach(booking -> booking.setBooker(booker)); + List bookingDtos = bookings.stream() + .map(BookingMapper::toBookingDto) + .toList(); + // endregion setup + + doNothing().when(utilService).checkUser(bookerId); + when(bookingRepository.findAllByBookerIdOrderByStartDesc(bookerId)).thenReturn(bookings); + + List resultDtoList = bookingService.getAllBookingsByBooker(bookerId, null); + + assertThat(resultDtoList, equalTo(bookingDtos)); + } + + @Test + void whenUserIsBooker_andStateIsCurrent_thenReturnListOfCurrentBookingDtosOfBooker() { + // region setup + User booker = testUtils.makeNewUser(1); + long bookerId = booker.getId(); + LocalDateTime now = LocalDateTime.now(); + List bookings = List.of( + // future + testUtils.makeNewAnyFullFastBooking( + 53, testUtils.futureDate, BookingStatus.WAITING, null), + // current + testUtils.makeNewAnyFullFastBooking( + 52, now.minusHours(5), BookingStatus.APPROVED, null), + // rejected + testUtils.makeNewAnyFullFastBooking( + 51, testUtils.pastDate.plusDays(30), BookingStatus.REJECTED, null), + // past + testUtils.makeNewAnyFullFastBooking( + 50, testUtils.pastDate, BookingStatus.APPROVED) + ); + bookings.forEach(booking -> booking.setBooker(booker)); + bookings = bookings.stream() + .filter(booking -> booking.getStart().isBefore(now) && booking.getEnd().isAfter(now)) + .toList(); + List bookingDtos = bookings.stream() + .map(BookingMapper::toBookingDto) + .toList(); + // endregion setup + + doNothing().when(utilService).checkUser(bookerId); + when(bookingRepository.findCurrentByBooker(bookerId)).thenReturn(bookings); + + List resultDtoList = bookingService.getAllBookingsByBooker(bookerId, "CURRENT"); + + assertThat(resultDtoList, equalTo(bookingDtos)); + } + + @Test + void whenUserIsBooker_andStateIsPast_thenReturnListOfPastBookingDtosOfBooker() { + // region setup + User booker = testUtils.makeNewUser(1); + long bookerId = booker.getId(); + LocalDateTime now = LocalDateTime.now(); + List bookings = List.of( + // future + testUtils.makeNewAnyFullFastBooking( + 53, testUtils.futureDate, BookingStatus.WAITING, null), + // current + testUtils.makeNewAnyFullFastBooking( + 52, now.minusHours(5), BookingStatus.APPROVED, null), + // rejected + testUtils.makeNewAnyFullFastBooking( + 51, testUtils.pastDate.plusDays(30), BookingStatus.REJECTED, null), + // past + testUtils.makeNewAnyFullFastBooking( + 50, testUtils.pastDate, BookingStatus.APPROVED) + ); + bookings.forEach(booking -> booking.setBooker(booker)); + bookings = bookings.stream() + .filter(booking -> booking.getEnd().isBefore(now)) + .toList(); + List bookingDtos = bookings.stream() + .map(BookingMapper::toBookingDto) + .toList(); + // endregion setup + + doNothing().when(utilService).checkUser(bookerId); + when(bookingRepository.findPastByBooker(bookerId)).thenReturn(bookings); + + List resultDtoList = bookingService.getAllBookingsByBooker(bookerId, "past"); + + assertThat(resultDtoList, equalTo(bookingDtos)); + } + + @Test + void whenUserIsBooker_andStateIsFuture_thenReturnListOfFutureBookingDtosOfBooker() { + // region setup + User booker = testUtils.makeNewUser(1); + long bookerId = booker.getId(); + LocalDateTime now = LocalDateTime.now(); + List bookings = List.of( + // future + testUtils.makeNewAnyFullFastBooking( + 53, testUtils.futureDate, BookingStatus.WAITING, null), + // current + testUtils.makeNewAnyFullFastBooking( + 52, now.minusHours(5), BookingStatus.APPROVED, null), + // rejected + testUtils.makeNewAnyFullFastBooking( + 51, testUtils.pastDate.plusDays(30), BookingStatus.REJECTED, null), + // past + testUtils.makeNewAnyFullFastBooking( + 50, testUtils.pastDate, BookingStatus.APPROVED) + ); + bookings.forEach(booking -> booking.setBooker(booker)); + bookings = bookings.stream() + .filter(booking -> booking.getStart().isAfter(now)) + .toList(); + List bookingDtos = bookings.stream() + .map(BookingMapper::toBookingDto) + .toList(); + // endregion setup + + doNothing().when(utilService).checkUser(bookerId); + when(bookingRepository.findFutureByBooker(bookerId)).thenReturn(bookings); + + List resultDtoList = bookingService.getAllBookingsByBooker(bookerId, "Future"); + + assertThat(resultDtoList, equalTo(bookingDtos)); + } + + @Test + void whenUserIsBooker_andStatusIsWaiting_thenReturnListOfWaitingBookingDtosOfBooker() { + // region setup + User booker = testUtils.makeNewUser(1); + long bookerId = booker.getId(); + LocalDateTime now = LocalDateTime.now(); + List bookings = List.of( + // future + testUtils.makeNewAnyFullFastBooking( + 53, testUtils.futureDate, BookingStatus.WAITING, null), + // current + testUtils.makeNewAnyFullFastBooking( + 52, now.minusHours(5), BookingStatus.APPROVED, null), + // rejected + testUtils.makeNewAnyFullFastBooking( + 51, testUtils.pastDate.plusDays(30), BookingStatus.REJECTED, null), + // past + testUtils.makeNewAnyFullFastBooking( + 50, testUtils.pastDate, BookingStatus.APPROVED) + ); + bookings.forEach(booking -> booking.setBooker(booker)); + bookings = bookings.stream() + .filter(booking -> booking.getStatus().equals(BookingStatus.WAITING)) + .toList(); + List bookingDtos = bookings.stream() + .map(BookingMapper::toBookingDto) + .toList(); + // endregion setup + + doNothing().when(utilService).checkUser(bookerId); + when(bookingRepository.findAllByBooker_IdAndStatusOrderByStartDesc(bookerId, BookingStatus.WAITING)) + .thenReturn(bookings); + + List resultDtoList = bookingService.getAllBookingsByBooker(bookerId, "waItIng"); + + assertThat(resultDtoList, equalTo(bookingDtos)); + } + + @Test + void whenUserIsBooker_andStatusIsRejected_thenReturnListOfRejectedBookingDtosOfBooker() { + // region setup + User booker = testUtils.makeNewUser(1); + long bookerId = booker.getId(); + LocalDateTime now = LocalDateTime.now(); + List bookings = List.of( + // future + testUtils.makeNewAnyFullFastBooking( + 53, testUtils.futureDate, BookingStatus.WAITING, null), + // current + testUtils.makeNewAnyFullFastBooking( + 52, now.minusHours(5), BookingStatus.APPROVED, null), + // rejected + testUtils.makeNewAnyFullFastBooking( + 51, testUtils.pastDate.plusDays(30), BookingStatus.REJECTED, null), + // past + testUtils.makeNewAnyFullFastBooking( + 50, testUtils.pastDate, BookingStatus.APPROVED) + ); + bookings.forEach(booking -> booking.setBooker(booker)); + bookings = bookings.stream() + .filter(booking -> booking.getStatus().equals(BookingStatus.REJECTED)) + .toList(); + List bookingDtos = bookings.stream() + .map(BookingMapper::toBookingDto) + .toList(); + // endregion setup + + doNothing().when(utilService).checkUser(bookerId); + when(bookingRepository.findAllByBooker_IdAndStatusOrderByStartDesc(bookerId, BookingStatus.REJECTED)) + .thenReturn(bookings); + + List resultDtoList = bookingService.getAllBookingsByBooker(bookerId, "rejected"); + + assertThat(resultDtoList, equalTo(bookingDtos)); + } + + @Test + void whenUserIsBooker_andStatusIsUndefined_thenReturnParameterNotValidException() { + long bookerId = 1L; + + doNothing().when(utilService).checkUser(bookerId); + + assertThrows(ParameterNotValidException.class, () -> + bookingService.getAllBookingsByBooker(bookerId, "XXX")); } } @@ -230,7 +714,268 @@ void testGetAllBookingsByBooker() { class GetAllBookingsByOwner { @Test - void testGetAllBookingsByOwner() { + void whenUserIsBooker_andStateIsAll_thenReturnListOfAllBookingDtosOfBooker() { + // region setup + User owner = testUtils.makeNewUser(1); + long ownerId = owner.getId(); + LocalDateTime now = LocalDateTime.now(); + List bookings = List.of( + // future + testUtils.makeNewAnyFullFastBooking( + 53, testUtils.futureDate, BookingStatus.WAITING, null), + // current + testUtils.makeNewAnyFullFastBooking( + 52, now.minusHours(5), BookingStatus.APPROVED, null), + // rejected + testUtils.makeNewAnyFullFastBooking( + 51, testUtils.pastDate.plusDays(30), BookingStatus.REJECTED, null), + // past + testUtils.makeNewAnyFullFastBooking( + 50, testUtils.pastDate, BookingStatus.APPROVED) + ); + bookings.forEach(booking -> booking.getItem().setOwner(owner)); + List bookingDtos = bookings.stream() + .map(BookingMapper::toBookingDto) + .toList(); + // endregion setup + + doNothing().when(utilService).checkUser(ownerId); + when(bookingRepository.findAllByOwner(ownerId)).thenReturn(bookings); + + List resultDtoList = bookingService.getAllBookingsByOwner(ownerId, "all"); + + assertThat(resultDtoList, equalTo(bookingDtos)); + } + + @Test + void whenUserIsBooker_andStateIsNull_thenReturnListOfAllBookingDtosOfBooker() { + // region setup + User owner = testUtils.makeNewUser(1); + long ownerId = owner.getId(); + LocalDateTime now = LocalDateTime.now(); + List bookings = List.of( + // future + testUtils.makeNewAnyFullFastBooking( + 53, testUtils.futureDate, BookingStatus.WAITING, null), + // current + testUtils.makeNewAnyFullFastBooking( + 52, now.minusHours(5), BookingStatus.APPROVED, null), + // rejected + testUtils.makeNewAnyFullFastBooking( + 51, testUtils.pastDate.plusDays(30), BookingStatus.REJECTED, null), + // past + testUtils.makeNewAnyFullFastBooking( + 50, testUtils.pastDate, BookingStatus.APPROVED) + ); + bookings.forEach(booking -> booking.getItem().setOwner(owner)); + List bookingDtos = bookings.stream() + .map(BookingMapper::toBookingDto) + .toList(); + // endregion setup + + doNothing().when(utilService).checkUser(ownerId); + when(bookingRepository.findAllByOwner(ownerId)).thenReturn(bookings); + + List resultDtoList = bookingService.getAllBookingsByOwner(ownerId, null); + + assertThat(resultDtoList, equalTo(bookingDtos)); + } + + @Test + void whenUserIsBooker_andStateIsCurrent_thenReturnListOfCurrentBookingDtosOfBooker() { + // region setup + User owner = testUtils.makeNewUser(1); + long ownerId = owner.getId(); + LocalDateTime now = LocalDateTime.now(); + List bookings = List.of( + // future + testUtils.makeNewAnyFullFastBooking( + 53, testUtils.futureDate, BookingStatus.WAITING, null), + // current + testUtils.makeNewAnyFullFastBooking( + 52, now.minusHours(5), BookingStatus.APPROVED, null), + // rejected + testUtils.makeNewAnyFullFastBooking( + 51, testUtils.pastDate.plusDays(30), BookingStatus.REJECTED, null), + // past + testUtils.makeNewAnyFullFastBooking( + 50, testUtils.pastDate, BookingStatus.APPROVED) + ); + bookings.forEach(booking -> booking.getItem().setOwner(owner)); + bookings = bookings.stream() + .filter(booking -> booking.getStart().isBefore(now) && booking.getEnd().isAfter(now)) + .toList(); + List bookingDtos = bookings.stream() + .map(BookingMapper::toBookingDto) + .toList(); + // endregion setup + + doNothing().when(utilService).checkUser(ownerId); + when(bookingRepository.findCurrentByOwner(ownerId)).thenReturn(bookings); + + List resultDtoList = bookingService.getAllBookingsByOwner(ownerId, "CURRENT"); + + assertThat(resultDtoList, equalTo(bookingDtos)); + } + + @Test + void whenUserIsBooker_andStateIsPast_thenReturnListOfPastBookingDtosOfBooker() { + // region setup + User owner = testUtils.makeNewUser(1); + long ownerId = owner.getId(); + LocalDateTime now = LocalDateTime.now(); + List bookings = List.of( + // future + testUtils.makeNewAnyFullFastBooking( + 53, testUtils.futureDate, BookingStatus.WAITING, null), + // current + testUtils.makeNewAnyFullFastBooking( + 52, now.minusHours(5), BookingStatus.APPROVED, null), + // rejected + testUtils.makeNewAnyFullFastBooking( + 51, testUtils.pastDate.plusDays(30), BookingStatus.REJECTED, null), + // past + testUtils.makeNewAnyFullFastBooking( + 50, testUtils.pastDate, BookingStatus.APPROVED) + ); + bookings.forEach(booking -> booking.getItem().setOwner(owner)); + bookings = bookings.stream() + .filter(booking -> booking.getEnd().isBefore(now)) + .toList(); + List bookingDtos = bookings.stream() + .map(BookingMapper::toBookingDto) + .toList(); + // endregion setup + + doNothing().when(utilService).checkUser(ownerId); + when(bookingRepository.findPastByOwner(ownerId)).thenReturn(bookings); + + List resultDtoList = bookingService.getAllBookingsByOwner(ownerId, "past"); + + assertThat(resultDtoList, equalTo(bookingDtos)); + } + + @Test + void whenUserIsBooker_andStateIsFuture_thenReturnListOfFutureBookingDtosOfBooker() { + // region setup + User owner = testUtils.makeNewUser(1); + long ownerId = owner.getId(); + LocalDateTime now = LocalDateTime.now(); + List bookings = List.of( + // future + testUtils.makeNewAnyFullFastBooking( + 53, testUtils.futureDate, BookingStatus.WAITING, null), + // current + testUtils.makeNewAnyFullFastBooking( + 52, now.minusHours(5), BookingStatus.APPROVED, null), + // rejected + testUtils.makeNewAnyFullFastBooking( + 51, testUtils.pastDate.plusDays(30), BookingStatus.REJECTED, null), + // past + testUtils.makeNewAnyFullFastBooking( + 50, testUtils.pastDate, BookingStatus.APPROVED) + ); + bookings.forEach(booking -> booking.getItem().setOwner(owner)); + bookings = bookings.stream() + .filter(booking -> booking.getStart().isAfter(now)) + .toList(); + List bookingDtos = bookings.stream() + .map(BookingMapper::toBookingDto) + .toList(); + // endregion setup + + doNothing().when(utilService).checkUser(ownerId); + when(bookingRepository.findFutureByOwner(ownerId)).thenReturn(bookings); + + List resultDtoList = bookingService.getAllBookingsByOwner(ownerId, "Future"); + + assertThat(resultDtoList, equalTo(bookingDtos)); + } + + @Test + void whenUserIsBooker_andStatusIsWaiting_thenReturnListOfWaitingBookingDtosOfBooker() { + // region setup + User owner = testUtils.makeNewUser(1); + long ownerId = owner.getId(); + LocalDateTime now = LocalDateTime.now(); + List bookings = List.of( + // future + testUtils.makeNewAnyFullFastBooking( + 53, testUtils.futureDate, BookingStatus.WAITING, null), + // current + testUtils.makeNewAnyFullFastBooking( + 52, now.minusHours(5), BookingStatus.APPROVED, null), + // rejected + testUtils.makeNewAnyFullFastBooking( + 51, testUtils.pastDate.plusDays(30), BookingStatus.REJECTED, null), + // past + testUtils.makeNewAnyFullFastBooking( + 50, testUtils.pastDate, BookingStatus.APPROVED) + ); + bookings.forEach(booking -> booking.getItem().setOwner(owner)); + bookings = bookings.stream() + .filter(booking -> booking.getStatus().equals(BookingStatus.WAITING)) + .toList(); + List bookingDtos = bookings.stream() + .map(BookingMapper::toBookingDto) + .toList(); + // endregion setup + + doNothing().when(utilService).checkUser(ownerId); + when(bookingRepository.findAllByOwnerAndStatusOrderByStartDesc(ownerId, BookingStatus.WAITING)) + .thenReturn(bookings); + + List resultDtoList = bookingService.getAllBookingsByOwner(ownerId, "waItIng"); + + assertThat(resultDtoList, equalTo(bookingDtos)); + } + + @Test + void whenUserIsBooker_andStatusIsRejected_thenReturnListOfRejectedBookingDtosOfBooker() { + // region setup + User owner = testUtils.makeNewUser(1); + long ownerId = owner.getId(); + LocalDateTime now = LocalDateTime.now(); + List bookings = List.of( + // future + testUtils.makeNewAnyFullFastBooking( + 53, testUtils.futureDate, BookingStatus.WAITING, null), + // current + testUtils.makeNewAnyFullFastBooking( + 52, now.minusHours(5), BookingStatus.APPROVED, null), + // rejected + testUtils.makeNewAnyFullFastBooking( + 51, testUtils.pastDate.plusDays(30), BookingStatus.REJECTED, null), + // past + testUtils.makeNewAnyFullFastBooking( + 50, testUtils.pastDate, BookingStatus.APPROVED) + ); + bookings.forEach(booking -> booking.getItem().setOwner(owner)); + bookings = bookings.stream() + .filter(booking -> booking.getStatus().equals(BookingStatus.REJECTED)) + .toList(); + List bookingDtos = bookings.stream() + .map(BookingMapper::toBookingDto) + .toList(); + // endregion setup + + doNothing().when(utilService).checkUser(ownerId); + when(bookingRepository.findAllByOwnerAndStatusOrderByStartDesc(ownerId, BookingStatus.REJECTED)) + .thenReturn(bookings); + + List resultDtoList = bookingService.getAllBookingsByOwner(ownerId, "rejected"); + + assertThat(resultDtoList, equalTo(bookingDtos)); + } + + @Test + void whenUserIsBooker_andStatusIsUndefined_thenReturnParameterNotValidException() { + long ownerId = 1L; + + doNothing().when(utilService).checkUser(ownerId); + + assertThrows(ParameterNotValidException.class, () -> + bookingService.getAllBookingsByOwner(ownerId, "XXX")); } } } \ No newline at end of file From e2a38c1991bdf44c444d8802e68240320b0152c3 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Wed, 15 Apr 2026 22:44:07 +0300 Subject: [PATCH 11/28] =?UTF-8?q?=D0=92=D1=81=D0=B5=20=D0=BC=D0=B0=D0=BF?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D1=8B=20=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shareit/booking/dto/BookingMapper.java | 2 + .../shareit/item/dto/CommentMapper.java | 2 + .../shareit/item/dto/ItemMapper.java | 2 + .../request/dto/ItemRequestMapper.java | 2 + .../shareit/user/dto/UserMapper.java | 2 + .../shareit/util/error/ErrorHandler.java | 6 - .../exception/ConditionsNotMetException.java | 7 - .../java/ru/practicum/shareit/TestUtils.java | 10 +- .../shareit/booking/BookingContextTest.java | 125 +++++++++++++ .../booking/BookingControllerTest.java | 2 - .../shareit/item/ItemContextTest.java | 18 +- .../request/ItemRequestContextTest.java | 164 ------------------ .../request/ItemRequestControllerTest.java | 164 ++++++++++++++++++ .../service/ItemRequestServiceImplTest.java | 164 ++++++++++++++++++ .../shareit/user/UserContextTest.java | 116 +++++++++++++ .../shareit/util/UtilServiceTest.java | 39 ++--- 16 files changed, 619 insertions(+), 206 deletions(-) delete mode 100644 server/src/main/java/ru/practicum/shareit/util/exception/ConditionsNotMetException.java create mode 100644 server/src/test/java/ru/practicum/shareit/booking/BookingContextTest.java delete mode 100644 server/src/test/java/ru/practicum/shareit/request/ItemRequestContextTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/request/ItemRequestControllerTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/request/service/ItemRequestServiceImplTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/user/UserContextTest.java diff --git a/server/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java index 16596dd..5da0c57 100644 --- a/server/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java +++ b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingMapper.java @@ -1,6 +1,7 @@ package ru.practicum.shareit.booking.dto; import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; import ru.practicum.shareit.booking.model.Booking; import ru.practicum.shareit.booking.model.BookingStatus; import ru.practicum.shareit.item.dto.ItemMapper; @@ -8,6 +9,7 @@ import ru.practicum.shareit.user.dto.UserMapper; import ru.practicum.shareit.user.model.User; +@Component public class BookingMapper { @NonNull diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java b/server/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java index b6af998..1c31e4a 100644 --- a/server/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java +++ b/server/src/main/java/ru/practicum/shareit/item/dto/CommentMapper.java @@ -1,12 +1,14 @@ package ru.practicum.shareit.item.dto; import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; import ru.practicum.shareit.item.model.Comment; import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.user.model.User; import java.time.LocalDateTime; +@Component public class CommentMapper { public static CommentDto toDto(@NonNull Comment comment) { diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java index 88b6af8..e87409f 100644 --- a/server/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java +++ b/server/src/main/java/ru/practicum/shareit/item/dto/ItemMapper.java @@ -1,6 +1,7 @@ package ru.practicum.shareit.item.dto; import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.request.model.ItemRequest; import ru.practicum.shareit.user.model.User; @@ -8,6 +9,7 @@ import java.time.LocalDateTime; import java.util.List; +@Component public class ItemMapper { @NonNull diff --git a/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestMapper.java b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestMapper.java index 5138d01..8c4b980 100644 --- a/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestMapper.java +++ b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestMapper.java @@ -1,6 +1,7 @@ package ru.practicum.shareit.request.dto; import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; import ru.practicum.shareit.item.dto.ItemMapper; import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.request.model.ItemRequest; @@ -10,6 +11,7 @@ import java.time.LocalDateTime; import java.util.List; +@Component public class ItemRequestMapper { public static ItemRequestDto toDto(@NonNull ItemRequest itemRequest) { diff --git a/server/src/main/java/ru/practicum/shareit/user/dto/UserMapper.java b/server/src/main/java/ru/practicum/shareit/user/dto/UserMapper.java index 3a47070..24b6aae 100644 --- a/server/src/main/java/ru/practicum/shareit/user/dto/UserMapper.java +++ b/server/src/main/java/ru/practicum/shareit/user/dto/UserMapper.java @@ -1,7 +1,9 @@ package ru.practicum.shareit.user.dto; +import org.springframework.stereotype.Component; import ru.practicum.shareit.user.model.User; +@Component public class UserMapper { public static UserDto toUserDto(User user) { diff --git a/server/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java b/server/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java index 3c2fbae..b564167 100644 --- a/server/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java +++ b/server/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java @@ -23,12 +23,6 @@ @Slf4j public class ErrorHandler { - @ResponseStatus(HttpStatus.NOT_IMPLEMENTED) - @ExceptionHandler - public ErrorResponse handleMethodNotImplemented(final MethodNotImplementedException e) { - return new ErrorResponse(e.getMessage()); - } - @ResponseStatus(HttpStatus.NOT_FOUND) @ExceptionHandler public ErrorResponse handleNotFound(final NotFoundException e) { diff --git a/server/src/main/java/ru/practicum/shareit/util/exception/ConditionsNotMetException.java b/server/src/main/java/ru/practicum/shareit/util/exception/ConditionsNotMetException.java deleted file mode 100644 index a49d237..0000000 --- a/server/src/main/java/ru/practicum/shareit/util/exception/ConditionsNotMetException.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.util.exception; - -public class ConditionsNotMetException extends RuntimeException { - public ConditionsNotMetException(String message) { - super(message); - } -} diff --git a/server/src/test/java/ru/practicum/shareit/TestUtils.java b/server/src/test/java/ru/practicum/shareit/TestUtils.java index 481c772..a2c1402 100644 --- a/server/src/test/java/ru/practicum/shareit/TestUtils.java +++ b/server/src/test/java/ru/practicum/shareit/TestUtils.java @@ -109,7 +109,7 @@ public Item makeNewItem(User owner, ItemRequest request) { } public Item makeNewFastItem(int id) { - return makeNewItem(id, makeNewUser(id + 1), makeNewItemRequest(id + 10, makeNewUser(id + 2))); + return makeNewItem(id, makeNewUser(id + 10), makeNewItemRequest(id + 20, makeNewUser(id + 11))); } public ItemRequest makeNewItemRequest(int id, User requestor) { @@ -214,4 +214,12 @@ public Booking makeCopyOfBooking(Booking booking) { .setEnd(booking.getEnd()) .setStatus(booking.getStatus()); } + + public ItemRequest makeCopyOfRequest(ItemRequest request) { + return new ItemRequest() + .setId(request.getId()) + .setDescription(request.getDescription()) + .setCreatedDate(request.getCreatedDate()) + .setRequestor(request.getRequestor()); + } } diff --git a/server/src/test/java/ru/practicum/shareit/booking/BookingContextTest.java b/server/src/test/java/ru/practicum/shareit/booking/BookingContextTest.java new file mode 100644 index 0000000..f2de323 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingContextTest.java @@ -0,0 +1,125 @@ +package ru.practicum.shareit.booking; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.EntityManagerFactory; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.shareit.TestUtils; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.item.repository.CommentRepository; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.request.repository.ItemRequestRepository; +import ru.practicum.shareit.request.repository.QueryDslRepository; +import ru.practicum.shareit.user.repository.UserRepository; +import ru.practicum.shareit.util.error.ErrorHandler; +import ru.practicum.shareit.util.error.ErrorResponse; + +import java.util.Optional; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.never; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@EnableAutoConfiguration(exclude = { + DataSourceAutoConfiguration.class, + HibernateJpaAutoConfiguration.class +}) +public class BookingContextTest { + + @Autowired + ObjectMapper mapper; + + @Autowired + private MockMvc mvc; + + @Autowired + BookingController controller; + + @MockitoBean + UserRepository userRepository; + + @MockitoBean + ItemRepository itemRepository; + + @MockitoBean + ItemRequestRepository itemRequestRepository; + + @MockitoBean + BookingRepository bookingRepository; + + @MockitoBean + CommentRepository commentRepository; + + @MockitoBean + QueryDslRepository queryDslRepository; + + @MockitoBean + EntityManagerFactory entityManagerFactory; + + final TestUtils testUtils = new TestUtils(); + + @BeforeEach + void setUp() { + mvc = MockMvcBuilders + .standaloneSetup(controller) + .setControllerAdvice(new ErrorHandler()) + .setMessageConverters(new MappingJackson2HttpMessageConverter(mapper)) + .build(); + } + + @Test + @SneakyThrows + void approveBooking_whenBookingWasCanseled_thenReturnErrorResponse() { + // region setup + long bookingId = 1L; + Booking approovedBooking = testUtils.makeNewAnyFullFastBooking( + bookingId, + testUtils.futureDate, + BookingStatus.APPROVED, + null + ); + Long ownerId = approovedBooking.getItem().getOwner().getId(); + Booking inBaseBooking = testUtils.makeCopyOfBooking(approovedBooking).setStatus(BookingStatus.CANCELED); + // endregion setup + + when(userRepository.existsById(ownerId)).thenReturn(true); + when(bookingRepository.findById(bookingId)).thenReturn(Optional.of(inBaseBooking)); + + String result = mvc.perform( + patch("/bookings/{bookingId}?approved={approved}", bookingId, true) + .header("X-Sharer-User-Id", ownerId)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isBadRequest()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(result, is( + mapper.writeValueAsString(new ErrorResponse("Бронирование уже было отменено создателем")) + )); + + verify(bookingRepository, never()).save(any(Booking.class)); + } + + +} diff --git a/server/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java b/server/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java index d11d837..29df280 100644 --- a/server/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -34,7 +33,6 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@Slf4j @WebMvcTest(controllers = BookingController.class) public class BookingControllerTest { diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemContextTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemContextTest.java index eda413e..b149fc9 100644 --- a/server/src/test/java/ru/practicum/shareit/item/ItemContextTest.java +++ b/server/src/test/java/ru/practicum/shareit/item/ItemContextTest.java @@ -1,21 +1,23 @@ package ru.practicum.shareit.item; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.EntityManagerFactory; import lombok.SneakyThrows; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import ru.practicum.shareit.ShareItServer; import ru.practicum.shareit.TestUtils; import ru.practicum.shareit.booking.model.Booking; import ru.practicum.shareit.booking.model.BookingStatus; @@ -30,6 +32,7 @@ import ru.practicum.shareit.item.repository.ItemRepository; import ru.practicum.shareit.request.model.ItemRequest; import ru.practicum.shareit.request.repository.ItemRequestRepository; +import ru.practicum.shareit.request.repository.QueryDslRepository; import ru.practicum.shareit.user.model.User; import ru.practicum.shareit.user.repository.UserRepository; import ru.practicum.shareit.util.error.ErrorHandler; @@ -50,7 +53,10 @@ @SpringBootTest @AutoConfigureMockMvc -@ContextConfiguration(classes = ShareItServer.class) +@EnableAutoConfiguration(exclude = { + DataSourceAutoConfiguration.class, + HibernateJpaAutoConfiguration.class +}) public class ItemContextTest { final TestUtils testUtils = new TestUtils(); @@ -79,6 +85,12 @@ public class ItemContextTest { @MockitoBean CommentRepository commentRepository; + @MockitoBean + QueryDslRepository queryDslRepository; + + @MockitoBean + EntityManagerFactory entityManagerFactory; + @BeforeEach void setUp() { mvc = MockMvcBuilders diff --git a/server/src/test/java/ru/practicum/shareit/request/ItemRequestContextTest.java b/server/src/test/java/ru/practicum/shareit/request/ItemRequestContextTest.java deleted file mode 100644 index 3347bc4..0000000 --- a/server/src/test/java/ru/practicum/shareit/request/ItemRequestContextTest.java +++ /dev/null @@ -1,164 +0,0 @@ -package ru.practicum.shareit.request; - -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.SneakyThrows; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.result.MockMvcResultHandlers; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import ru.practicum.shareit.ShareItServer; -import ru.practicum.shareit.TestUtils; -import ru.practicum.shareit.booking.repository.BookingRepository; -import ru.practicum.shareit.item.repository.CommentRepository; -import ru.practicum.shareit.item.repository.ItemRepository; -import ru.practicum.shareit.request.dto.ItemRequestDto; -import ru.practicum.shareit.request.dto.ItemRequestMapper; -import ru.practicum.shareit.request.model.ItemRequest; -import ru.practicum.shareit.request.repository.ItemRequestRepository; -import ru.practicum.shareit.request.repository.QueryDslRepository; -import ru.practicum.shareit.request.service.ItemRequestService; -import ru.practicum.shareit.user.model.User; -import ru.practicum.shareit.user.repository.UserRepository; -import ru.practicum.shareit.util.UtilService; -import ru.practicum.shareit.util.error.ErrorHandler; - -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@SpringBootTest -@AutoConfigureMockMvc -@ContextConfiguration(classes = ShareItServer.class) -class ItemRequestContextTest { - - final TestUtils testUtils = new TestUtils(); - - @Autowired - ObjectMapper mapper; - - @Autowired - private MockMvc mvc; - - @Autowired - ItemRequestController itemRequestController; - - @Autowired - ItemRequestService itemRequestService; - - @Autowired - UtilService utilService; - - // region repositories - - @MockitoBean - UserRepository userRepository; - - @MockitoBean - ItemRepository itemRepository; - - @MockitoBean - ItemRequestRepository itemRequestRepository; - - @MockitoBean - BookingRepository bookingRepository; - - @MockitoBean - CommentRepository commentRepository; - - @MockitoBean - QueryDslRepository queryDslRepository; - - // endregion repositories - - @BeforeEach - void setUp() { - mvc = MockMvcBuilders - .standaloneSetup(itemRequestController) - .setControllerAdvice(new ErrorHandler()) - .setMessageConverters(new MappingJackson2HttpMessageConverter(mapper)) - .build(); - } - - @Nested - class AddNewRequest { - - @Test - @SneakyThrows - void addNewRequest() { - User requestor = testUtils.makeNewUser(1); - ItemRequest itemRequest = testUtils.makeNewItemRequest(1, requestor); - ItemRequestDto requestDto = ItemRequestDto.builder() - .description(itemRequest.getDescription()) - .createdDate(itemRequest.getCreatedDate()) - .build(); - - when(userRepository.findById(requestor.getId())).thenReturn(Optional.of(requestor)); - when(itemRequestRepository.save(ItemRequestMapper.toEntity(requestDto, requestor))).thenReturn(itemRequest); - - // region mvc test - String result = mvc.perform(post("/requests") - .header("X-Sharer-User-Id", requestor.getId()) - .content(mapper.writeValueAsString(requestDto)) - .characterEncoding(StandardCharsets.UTF_8) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(MockMvcResultHandlers.print()) - .andReturn() - .getResponse() - .getContentAsString(); - // endregion mvc test - - assertEquals(result, mapper.writeValueAsString(ItemRequestMapper.toDto(itemRequest))); - } - } - - @Nested - class GetRequestListByUser { - - @Test - void getRequestListByUser() { - User requestor = testUtils.makeNewUser(1); - - List itemRequests = List.of( - testUtils.makeNewItemRequest(1, requestor), - testUtils.makeNewItemRequest(2, testUtils.makeNewUser(2)), - testUtils.makeNewItemRequest(3, requestor) - ); - - when(userRepository.existsById(requestor.getId())).thenReturn(true); - when(itemRequestRepository.findAllByRequestor_Id(requestor.getId())).thenReturn(itemRequests); - - } - } - - @Nested - class GetAllRequests { - - @Test - void getAllRequests() { - } - } - - @Nested - class GetRequestById { - - @Test - void getRequestById() { - } - } -} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/request/ItemRequestControllerTest.java b/server/src/test/java/ru/practicum/shareit/request/ItemRequestControllerTest.java new file mode 100644 index 0000000..0fd6cf1 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/ItemRequestControllerTest.java @@ -0,0 +1,164 @@ +package ru.practicum.shareit.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.shareit.TestUtils; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.request.dto.ItemRequestBigDto; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestMapper; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.request.service.ItemRequestService; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.util.UtilService; +import ru.practicum.shareit.util.error.ErrorHandler; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(ItemRequestController.class) +class ItemRequestControllerTest { + + @Autowired + ObjectMapper mapper; + + @Autowired + MockMvc mvc; + + @Autowired + ItemRequestController controller; + + @MockitoBean + ItemRequestService requestService; + + @MockitoBean + UtilService utilService; + + final TestUtils testUtils = new TestUtils(); + + @BeforeEach + void setUp() { + mvc = MockMvcBuilders + .standaloneSetup(controller) + .setControllerAdvice(new ErrorHandler()) + .setMessageConverters(new MappingJackson2HttpMessageConverter(mapper)) + .build(); + } + + @Test + @SneakyThrows + void addNewRequest() { + User requestor = testUtils.makeNewUser(1); + ItemRequest itemRequest = testUtils.makeNewItemRequest(50, requestor); + ItemRequestDto requestDto = ItemRequestDto.builder() + .description(itemRequest.getDescription()) + .createdDate(itemRequest.getCreatedDate()) + .build(); + ItemRequestDto resultRequestDto = ItemRequestMapper.toDto(itemRequest); + + when(requestService.addNewRequest(requestor.getId(), requestDto)).thenReturn(resultRequestDto); + + String result = mvc.perform(post("/requests") + .header("X-Sharer-User-Id", requestor.getId()) + .content(mapper.writeValueAsString(requestDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertEquals(result, mapper.writeValueAsString(ItemRequestMapper.toDto(itemRequest))); + } + + @Test + @SneakyThrows + void getRequestListByUser() { + User requestor = testUtils.makeNewUser(5); + Item item1 = testUtils.makeNewFastItem(40); + Item item2 = testUtils.makeNewFastItem(41); + Item item3 = testUtils.makeNewFastItem(42); + List bigDtos = List.of( + ItemRequestMapper + .toBigDto(testUtils.makeNewItemRequest(70, requestor), List.of(item1, item3)), + ItemRequestMapper + .toBigDto(testUtils.makeNewItemRequest(71, requestor), List.of(item1, item2)) + ); + + when(requestService.getRequestListByUser(requestor.getId())).thenReturn(bigDtos); + + String result = mvc.perform(get("/requests") + .header("X-Sharer-User-Id", requestor.getId())) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertEquals(result, mapper.writeValueAsString(bigDtos)); + } + + @Test + @SneakyThrows + void getAllRequests() { + User user = testUtils.makeNewUser(1); + List requests = List.of( + testUtils.makeNewItemRequest(10, testUtils.makeNewUser(2)), + testUtils.makeNewItemRequest(11, testUtils.makeNewUser(3)) + ); + List requestDtos = requests.stream() + .map(ItemRequestMapper::toDto) + .toList(); + + when(requestService.getAllRequests(user.getId())).thenReturn(requestDtos); + + String result = mvc.perform(get("/requests/all") + .header("X-Sharer-User-Id", user.getId())) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertEquals(result, mapper.writeValueAsString(requestDtos)); + } + + @Test + @SneakyThrows + void getRequestById() { + ItemRequest itemRequest = testUtils.makeNewItemRequest(50, testUtils.makeNewUser(1)); + Item item = testUtils.makeNewFastItem(30); + ItemRequestBigDto bigDto = ItemRequestMapper.toBigDto(itemRequest, List.of(item)); + + when(requestService.getRequestById(itemRequest.getId())).thenReturn(bigDto); + + String result = mvc.perform(get("/requests/{requestId}", itemRequest.getId()) + .header("X-Sharer-User-Id", anyLong())) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertEquals(result, mapper.writeValueAsString(bigDto)); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/request/service/ItemRequestServiceImplTest.java b/server/src/test/java/ru/practicum/shareit/request/service/ItemRequestServiceImplTest.java new file mode 100644 index 0000000..43f167d --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/service/ItemRequestServiceImplTest.java @@ -0,0 +1,164 @@ +package ru.practicum.shareit.request.service; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import ru.practicum.shareit.TestUtils; +import ru.practicum.shareit.item.dto.ItemLowDto; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.request.dto.ItemRequestBigDto; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestMapper; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.request.repository.ItemRequestRepository; +import ru.practicum.shareit.request.repository.QueryDslRepository; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.util.UtilService; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ItemRequestServiceImplTest { + + @Mock + UtilService utilService; + + @Mock + ItemRequestRepository itemRequestRepository; + + @Mock + QueryDslRepository queryDslRepository; + + @InjectMocks + ItemRequestServiceImpl requestService; + + final TestUtils testUtils = new TestUtils(); + + @Nested + class AddNewRequest { + + @Test + void addNewRequest() { + // region setup + ItemRequest request = testUtils.makeNewItemRequest(1, testUtils.makeNewUser(10)); + User requestor = request.getRequestor(); + ItemRequestDto dtoIn = ItemRequestDto.builder() + .description(request.getDescription()) + .createdDate(request.getCreatedDate()) + .build(); + ItemRequestDto dtoOut = ItemRequestMapper.toDto(request); + // endregion setup + + when(utilService.getUser(requestor.getId())).thenReturn(requestor); + when(itemRequestRepository.save(testUtils.makeCopyOfRequest(request).setId(null))).thenReturn(request); + + ItemRequestDto resultDto = requestService.addNewRequest(requestor.getId(), dtoIn); + + assertThat(resultDto, equalTo(dtoOut)); + } + } + + @Nested + class GetRequestListByUser { + + @Test + void getRequestListByUser() { + // region setup + User requestor = testUtils.makeNewUser(5); + List items = List.of( + testUtils.makeNewFastItem(40), + testUtils.makeNewFastItem(41), + testUtils.makeNewFastItem(42) + ); + + List itemRequests = List.of( + testUtils.makeNewItemRequest(70, requestor), + testUtils.makeNewItemRequest(71, requestor) + ); + List requestIds = itemRequests.stream().map(ItemRequest::getId).toList(); + List bigDtos = List.of( + ItemRequestMapper + .toBigDto(itemRequests.getLast(), List.of(items.get(0), items.get(2))), + ItemRequestMapper + .toBigDto(itemRequests.getFirst(), List.of(items.get(0), items.get(1))) + ); + Map> bigMap = new HashMap<>(); + for (ItemRequestBigDto bigDto : bigDtos) { + List itemIds = bigDto.getItemDtos().stream().map(ItemLowDto::id).toList(); + bigMap.put( + bigDto.getId(), + items.stream().filter(item -> itemIds.contains(item.getId())) + .toList() + ); + } + // endregion setup + + doNothing().when(utilService).checkUser(requestor.getId()); + when(itemRequestRepository.findAllByRequestor_Id(requestor.getId())).thenReturn(itemRequests); + when(queryDslRepository.getMapOfItemsByRequestId(requestIds)).thenReturn(bigMap); + + List resultDtos = requestService.getRequestListByUser(requestor.getId()); + + assertThat(resultDtos, equalTo(bigDtos)); + } + } + + @Nested + class GetAllRequests { + + @Test + void getAllRequests() { + // region setup + Long userId = 1L; + List requests = List.of( + testUtils.makeNewItemRequest(10, testUtils.makeNewUser()), + testUtils.makeNewItemRequest(11, testUtils.makeNewUser()), + testUtils.makeNewItemRequest(12, testUtils.makeNewUser()) + ); + List dtos = requests.stream().map(ItemRequestMapper::toDto).toList(); + // endregion setup + + doNothing().when(utilService).checkUser(userId); + when(queryDslRepository.getAllRequestsWitoutByUser(userId)).thenReturn(requests); + + List resultDtos = requestService.getAllRequests(userId); + + assertThat(resultDtos, equalTo(dtos)); + } + } + + @Nested + class GetRequestById { + + @Test + void getRequestById() { + // region setup + ItemRequest request = testUtils.makeNewItemRequest(1, testUtils.makeNewUser(10)); + User requestor = request.getRequestor(); + List items = List.of( + testUtils.makeNewFastItem(40), + testUtils.makeNewFastItem(41), + testUtils.makeNewFastItem(42) + ); + ItemRequestBigDto bigDto = ItemRequestMapper.toBigDto(request, items); + // endregion setup + + when(queryDslRepository.getAllItemsByRequest(request.getId())).thenReturn(items); + when(utilService.getItemRequest(request.getId())).thenReturn(request); + + ItemRequestBigDto resultDto = requestService.getRequestById(request.getId()); + + assertThat(resultDto, equalTo(bigDto)); + } + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/user/UserContextTest.java b/server/src/test/java/ru/practicum/shareit/user/UserContextTest.java new file mode 100644 index 0000000..5b792e0 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/UserContextTest.java @@ -0,0 +1,116 @@ +package ru.practicum.shareit.user; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.EntityManagerFactory; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.shareit.TestUtils; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.item.repository.CommentRepository; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.request.repository.ItemRequestRepository; +import ru.practicum.shareit.request.repository.QueryDslRepository; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.dto.UserMapper; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; +import ru.practicum.shareit.util.error.ErrorHandler; +import ru.practicum.shareit.util.error.ErrorResponse; + +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.never; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@EnableAutoConfiguration(exclude = { + DataSourceAutoConfiguration.class, + HibernateJpaAutoConfiguration.class +}) +public class UserContextTest { + + final TestUtils testUtils = new TestUtils(); + + @Autowired + ObjectMapper mapper; + + @Autowired + private MockMvc mvc; + + @Autowired + UserController controller; + + // region mocks + @MockitoBean + UserRepository userRepository; + + @MockitoBean + ItemRepository itemRepository; + + @MockitoBean + ItemRequestRepository itemRequestRepository; + + @MockitoBean + BookingRepository bookingRepository; + + @MockitoBean + CommentRepository commentRepository; + + @MockitoBean + QueryDslRepository queryDslRepository; + + @MockitoBean + EntityManagerFactory entityManagerFactory; + // endregion mocks + + @BeforeEach + void setUp() { + mvc = MockMvcBuilders + .standaloneSetup(controller) + .setControllerAdvice(new ErrorHandler()) + .setMessageConverters(new MappingJackson2HttpMessageConverter(mapper)) + .build(); + } + + @Test + @SneakyThrows + void saveUser_whenThisUserAlredyExists_thenReturnErrorResponse() { + User user = testUtils.makeNewUser(1); + UserDto userDto = UserMapper.toUserDto(user); + + when(userRepository.existsByEmail(user.getEmail())).thenReturn(true); + + String result = mvc.perform(post("/users") + .content(mapper.writeValueAsString(userDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isConflict()) + .andReturn() + .getResponse() + .getContentAsString(); + + verify(userRepository, never()).save(any()); + assertEquals(result, + mapper.writeValueAsString(new ErrorResponse( + "Пользователь с такой почтой " + user.getEmail() + " уже есть.")) + ); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/util/UtilServiceTest.java b/server/src/test/java/ru/practicum/shareit/util/UtilServiceTest.java index 07bcbb9..8ae900f 100644 --- a/server/src/test/java/ru/practicum/shareit/util/UtilServiceTest.java +++ b/server/src/test/java/ru/practicum/shareit/util/UtilServiceTest.java @@ -1,25 +1,25 @@ package ru.practicum.shareit.util; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.beans.factory.annotation.Autowired; +import ru.practicum.shareit.TestUtils; import ru.practicum.shareit.item.repository.ItemRepository; import ru.practicum.shareit.request.repository.ItemRequestRepository; -import ru.practicum.shareit.user.dto.UserDto; import ru.practicum.shareit.user.repository.UserRepository; -import ru.practicum.shareit.util.error.ErrorHandler; +import ru.practicum.shareit.util.exception.NotFoundException; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class UtilServiceTest { - @InjectMocks - private UtilService utilService; - @Mock private UserRepository userRepository; @@ -29,33 +29,26 @@ class UtilServiceTest { @Mock private ItemRequestRepository itemRequestRepository; - private UserDto userDto; - - @Autowired - ErrorHandler errorHandler; - - @BeforeEach - void setUp() { - userDto = UserDto.builder() - .id(1L) - .name("John") - .email("john.doe@mail.com") - .build(); - } + @InjectMocks + private UtilService utilService; - @Test - void checkUser() { - } + final TestUtils testUtils = new TestUtils(); @Test void getUser() { + when(userRepository.findById(anyLong())).thenReturn(Optional.empty()); + assertThrows(NotFoundException.class, () -> utilService.getUser(anyLong())); } @Test void getItem() { + when(itemRepository.findById(anyLong())).thenReturn(Optional.empty()); + assertThrows(NotFoundException.class, () -> utilService.getItem(anyLong())); } @Test void getItemRequest() { + when(itemRequestRepository.findById(anyLong())).thenReturn(Optional.empty()); + assertThrows(NotFoundException.class, () -> utilService.getItemRequest(anyLong())); } } \ No newline at end of file From 00814e22aa975a5443d7f4a9f7a7a23b13f1e908 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Thu, 16 Apr 2026 00:28:49 +0300 Subject: [PATCH 12/28] =?UTF-8?q?=D0=9E=D1=87=D0=B8=D1=81=D1=82=D0=BA?= =?UTF-8?q?=D0=B0=20ErrorHandler=20=D0=BE=D1=82=20=D0=BB=D0=B8=D1=88=D0=BD?= =?UTF-8?q?=D0=B5=D0=B3=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 9 ++-- .../shareit/util/error/ErrorHandler.java | 49 ----------------- .../shareit/booking/BookingContextTest.java | 43 ++++++++++++++- .../shareit/item/ItemContextTest.java | 54 +++++++++++++++++-- .../shareit/user/UserContextTest.java | 17 ++++++ 5 files changed, 113 insertions(+), 59 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index a456459..7f43e6c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: depends_on: - server environment: - - TZ=Europe/Moscow +# - TZ=Europe/Moscow - SHAREIT_SERVER_URL=http://server:9090 server: @@ -24,7 +24,7 @@ services: depends_on: - db environment: - - TZ=Europe/Moscow +# - TZ=Europe/Moscow - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/shareit - SPRING_DATASOURCE_USERNAME=shareit - SPRING_DATASOURCE_PASSWORD=shareit @@ -35,7 +35,7 @@ services: # ports: # - "6541:5432" environment: - - TZ=Europe/Moscow +# - TZ=Europe/Moscow - POSTGRES_DB=shareit - POSTGRES_USER=shareit - POSTGRES_PASSWORD=shareit @@ -45,6 +45,3 @@ services: interval: 5s retries: 10 - # sudo docker compose down --rmi all --volumes --remove-orphans - # sudo docker compose build --no-cache gateway server - # sudo docker compose up -d --force-recreate diff --git a/server/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java b/server/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java index b564167..a8565d2 100644 --- a/server/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java +++ b/server/src/main/java/ru/practicum/shareit/util/error/ErrorHandler.java @@ -2,21 +2,11 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.validation.FieldError; -import org.springframework.validation.method.MethodValidationException; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.MissingRequestHeaderException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import ru.practicum.shareit.util.exception.*; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; @RestControllerAdvice @@ -55,51 +45,12 @@ public ErrorResponse handleDuplicatedData(final DuplicatedDataException e) { return new ErrorResponse(e.getMessage()); } - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler - public ErrorResponse handleMethodValidationException(final MethodValidationException e) { - return new ErrorResponse(e.getAllErrors().getFirst().getDefaultMessage()); - } - - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public ErrorResponse handleMethodArgumentTypeMismatchException(final MethodArgumentTypeMismatchException e) { - return new ErrorResponse("Не верно указан параметр запроса: " + e.getPropertyName() + "=" + e.getValue()); - } - @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler public ErrorResponse handleValidationException(final ValidationException e) { return new ErrorResponse(e.getMessage()); } - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(MissingRequestHeaderException.class) - public ErrorResponse handleMissingRequestHeaderException(final MissingRequestHeaderException e) { - return new ErrorResponse("Отсутствует заголовок в запросе: " + e.getHeaderName()); - } - - @ResponseStatus(HttpStatus.I_AM_A_TEAPOT) - @ExceptionHandler(HttpMessageNotReadableException.class) - public ErrorResponse handleEmptyBody(final HttpMessageNotReadableException e) { - return new ErrorResponse("Тело запроса отсутствует или некорректно " + e.getMessage()); - } - - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleMethodArgumentException(final MethodArgumentNotValidException e) { - Map errors = new HashMap<>(); - - e.getBindingResult().getAllErrors().forEach((error) -> { - String fieldName = ((FieldError) error).getField(); - String errorMessage = error.getDefaultMessage(); - errors.put(fieldName, errorMessage); - }); - - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .contentType(MediaType.APPLICATION_JSON) - .body(errors); - } - @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler public ErrorResponse handleAnyThrowable(final Throwable e) { diff --git a/server/src/test/java/ru/practicum/shareit/booking/BookingContextTest.java b/server/src/test/java/ru/practicum/shareit/booking/BookingContextTest.java index f2de323..2b9b9a3 100644 --- a/server/src/test/java/ru/practicum/shareit/booking/BookingContextTest.java +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingContextTest.java @@ -11,12 +11,14 @@ import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import ru.practicum.shareit.TestUtils; +import ru.practicum.shareit.booking.dto.IncomingBookingDto; import ru.practicum.shareit.booking.model.Booking; import ru.practicum.shareit.booking.model.BookingStatus; import ru.practicum.shareit.booking.repository.BookingRepository; @@ -28,14 +30,15 @@ import ru.practicum.shareit.util.error.ErrorHandler; import ru.practicum.shareit.util.error.ErrorResponse; +import java.nio.charset.StandardCharsets; import java.util.Optional; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -import static org.mockito.Mockito.never; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @@ -55,6 +58,7 @@ public class BookingContextTest { @Autowired BookingController controller; + // region mocks @MockitoBean UserRepository userRepository; @@ -75,6 +79,7 @@ public class BookingContextTest { @MockitoBean EntityManagerFactory entityManagerFactory; + // endregion mocks final TestUtils testUtils = new TestUtils(); @@ -121,5 +126,41 @@ void approveBooking_whenBookingWasCanseled_thenReturnErrorResponse() { verify(bookingRepository, never()).save(any(Booking.class)); } + @Test + @SneakyThrows + void addNewBooking_whenItenIsNotApprouved_thenReturnErrorResponse() { + Booking booking = testUtils.makeNewAnyFullFastBooking( + 1, + testUtils.futureDate, + BookingStatus.WAITING, + null + ); + booking.getItem().setAvailable(false); + IncomingBookingDto incomingBookingDto = new IncomingBookingDto( + booking.getItem().getId(), booking.getStart(), booking.getEnd()); + when(userRepository.findById(booking.getBooker().getId())).thenReturn(Optional.of(booking.getBooker())); + when(itemRepository.findById(booking.getItem().getId())).thenReturn(Optional.of(booking.getItem())); + + String result = mvc.perform( + post("/bookings") + .header("X-Sharer-User-Id", booking.getBooker().getId()) + .content(mapper.writeValueAsString(incomingBookingDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + ) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isBadRequest()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(result, is( + mapper.writeValueAsString( + new ErrorResponse("Вещь с id=" + booking.getItem().getId() + " не доступна для аренды")) + )); + + verify(bookingRepository, never()).save(any(Booking.class)); + } } diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemContextTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemContextTest.java index b149fc9..4d5c1a4 100644 --- a/server/src/test/java/ru/practicum/shareit/item/ItemContextTest.java +++ b/server/src/test/java/ru/practicum/shareit/item/ItemContextTest.java @@ -70,6 +70,7 @@ public class ItemContextTest { @Autowired ItemController itemController; + // region mocks @MockitoBean UserRepository userRepository; @@ -90,6 +91,7 @@ public class ItemContextTest { @MockitoBean EntityManagerFactory entityManagerFactory; + // endregion mocks @BeforeEach void setUp() { @@ -348,10 +350,56 @@ class UpdateItem { @Test @SneakyThrows - void whenUserIsOwnerOfItem_thenReturnsUpdatableItem() { + void updateAllfields_whenUserIsOwnerOfItem_thenReturnsUpdatableItem() { + // region setup Item item = testUtils.makeNewFastItem(10); - ItemDto itemDto = ItemDto.builder().name("this is new name").build(); - Item newItem = testUtils.makeCopyOfItem(item).setName(itemDto.getName()); + ItemDto itemDto = ItemDto.builder() + .name("this is new name") + .description("this is new description") + .available(false) + .requestId(100L) + .build(); + ItemRequest newRequest = testUtils.makeNewItemRequest(100, testUtils.makeNewUser(200)); + Item newItem = testUtils.makeCopyOfItem(item) + .setName(itemDto.getName()) + .setDescription(itemDto.getDescription()) + .setAvailable(itemDto.getAvailable()) + .setRequest(newRequest); + // endregion setup + + when(userRepository.existsById(item.getOwner().getId())).thenReturn(true); + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + when(userRepository.findById(item.getOwner().getId())).thenReturn(Optional.of(item.getOwner())); + when(itemRequestRepository.findById(item.getRequest().getId())) + .thenReturn(Optional.ofNullable(item.getRequest())); + when(itemRequestRepository.findById(newItem.getRequest().getId())).thenReturn(Optional.of(newRequest)); + when(itemRepository.save(newItem)).thenReturn(newItem); + + // region mvc test + mvc.perform(patch("/items/{itemId}", item.getId()) + .header("X-Sharer-User-Id", item.getOwner().getId()) + .content(mapper.writeValueAsString(itemDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andExpect(jsonPath("$.id", is(newItem.getId()), Long.class)) + .andExpect(jsonPath("$.name", is(newItem.getName()), String.class)) + .andExpect(jsonPath("$.description", is(newItem.getDescription()), String.class)) + .andExpect(jsonPath("$.available", is(newItem.isAvailable()), Boolean.class)) + .andExpect(jsonPath("$.requestId", is(newItem.getRequest().getId()), Long.class)); + // endregion mvc test + } + + @Test + @SneakyThrows + void updateNullfields_whenUserIsOwnerOfItem_thenReturnsUpdatableItem() { + // region setup + Item item = testUtils.makeNewFastItem(10); + ItemDto itemDto = ItemDto.builder().build(); + Item newItem = testUtils.makeCopyOfItem(item); + // endregion setup when(userRepository.existsById(item.getOwner().getId())).thenReturn(true); when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); diff --git a/server/src/test/java/ru/practicum/shareit/user/UserContextTest.java b/server/src/test/java/ru/practicum/shareit/user/UserContextTest.java index 5b792e0..99d716d 100644 --- a/server/src/test/java/ru/practicum/shareit/user/UserContextTest.java +++ b/server/src/test/java/ru/practicum/shareit/user/UserContextTest.java @@ -30,6 +30,7 @@ import ru.practicum.shareit.util.error.ErrorResponse; import java.nio.charset.StandardCharsets; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; @@ -113,4 +114,20 @@ void saveUser_whenThisUserAlredyExists_thenReturnErrorResponse() { "Пользователь с такой почтой " + user.getEmail() + " уже есть.")) ); } + + @Test + @SneakyThrows + void saveUser_whenRequestBosyIsBroken_thenReturnError500() { + String result = mvc.perform(post("/users") + .content(mapper.writeValueAsString(Optional.empty())) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is5xxServerError()) + .andReturn() + .getResponse() + .getContentAsString(); + + verify(userRepository, never()).save(any()); + } } From 38731da58514c9e6b016b8030f3c202e759a3393 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Thu, 16 Apr 2026 00:37:08 +0300 Subject: [PATCH 13/28] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20checkstyle.xml=20=D0=B8=D0=B7=20=D0=B8=D0=BD?= =?UTF-8?q?=D0=B4=D0=BA=D0=B5=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- checkstyle.xml | 257 ------------------------------------------------- 1 file changed, 257 deletions(-) delete mode 100644 checkstyle.xml diff --git a/checkstyle.xml b/checkstyle.xml deleted file mode 100644 index c28a0d3..0000000 --- a/checkstyle.xml +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 9de2643f9bc2bdcd0071b8128007c95995a7819b Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Thu, 16 Apr 2026 00:42:07 +0300 Subject: [PATCH 14/28] =?UTF-8?q?=D0=97=D0=B0=D0=BC=D0=B5=D0=BD=D0=B0=20ch?= =?UTF-8?q?eckstyle.xml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- checkstyle.xml | 257 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 checkstyle.xml diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..05d5086 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 0b8778f26e78650e3609b25a4275cda47e018bd3 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Thu, 16 Apr 2026 00:59:57 +0300 Subject: [PATCH 15/28] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BB=D0=B8=D1=88=D0=BD=D0=B8=D1=85=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20checkstyle.xml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gateway/checkstyle.xml | 257 --------------------------------------- gateway/suppressions.xml | 7 -- server/checkstyle.xml | 257 --------------------------------------- server/suppressions.xml | 7 -- 4 files changed, 528 deletions(-) delete mode 100644 gateway/checkstyle.xml delete mode 100644 gateway/suppressions.xml delete mode 100644 server/checkstyle.xml delete mode 100644 server/suppressions.xml diff --git a/gateway/checkstyle.xml b/gateway/checkstyle.xml deleted file mode 100644 index c28a0d3..0000000 --- a/gateway/checkstyle.xml +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/gateway/suppressions.xml b/gateway/suppressions.xml deleted file mode 100644 index b2c6822..0000000 --- a/gateway/suppressions.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/server/checkstyle.xml b/server/checkstyle.xml deleted file mode 100644 index c28a0d3..0000000 --- a/server/checkstyle.xml +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/server/suppressions.xml b/server/suppressions.xml deleted file mode 100644 index b2c6822..0000000 --- a/server/suppressions.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file From d7b1dbd0306c89e18dca56c2f19d6efb9d368995 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Thu, 16 Apr 2026 01:07:06 +0300 Subject: [PATCH 16/28] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B5=D1=89=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/api-tests.yml | 8 - SchemaDatabase.png | Bin 74333 -> 0 bytes checkstyle.xml | 257 -------------------------------- suppressions.xml | 7 - 4 files changed, 272 deletions(-) delete mode 100644 .github/workflows/api-tests.yml delete mode 100644 SchemaDatabase.png delete mode 100644 checkstyle.xml delete mode 100644 suppressions.xml diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml deleted file mode 100644 index 51c3d8b..0000000 --- a/.github/workflows/api-tests.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: ShareIt API Tests - -on: - pull_request: - -jobs: - build: - uses: yandex-praktikum/java-shareit/.github/workflows/api-tests.yml@ci \ No newline at end of file diff --git a/SchemaDatabase.png b/SchemaDatabase.png deleted file mode 100644 index 1f547e72495f6810f2554d0c5afaf59d604b3754..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 74333 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 diff --git a/checkstyle.xml b/checkstyle.xml deleted file mode 100644 index 05d5086..0000000 --- a/checkstyle.xml +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/suppressions.xml b/suppressions.xml deleted file mode 100644 index b2c6822..0000000 --- a/suppressions.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file From d29946e1475b48bec227a1b9e5254db6c35e41cf Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Thu, 16 Apr 2026 12:28:16 +0300 Subject: [PATCH 17/28] =?UTF-8?q?=D0=9F=D0=BE=D0=BF=D1=8B=D1=82=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=BE=D0=B1=D1=80=D0=B0=D1=82?= =?UTF-8?q?=D0=BD=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/api-tests.yml | 196 +++ .github/workflows/build.sh | 8 + .github/workflows/dashboard-template.hbs | 1672 ++++++++++++++++++++++ .github/workflows/run.sh | 10 + .github/workflows/wait-for-it.sh | 183 +++ checkstyle.xml | 257 ++++ suppressions.xml | 7 + 7 files changed, 2333 insertions(+) create mode 100644 .github/workflows/api-tests.yml create mode 100644 .github/workflows/build.sh create mode 100644 .github/workflows/dashboard-template.hbs create mode 100644 .github/workflows/run.sh create mode 100644 .github/workflows/wait-for-it.sh create mode 100644 checkstyle.xml create mode 100644 suppressions.xml diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml new file mode 100644 index 0000000..c90846c --- /dev/null +++ b/.github/workflows/api-tests.yml @@ -0,0 +1,196 @@ +name: ShareIt API Tests + +on: + pull_request: + workflow_call: + +jobs: + check-repo: + runs-on: ubuntu-latest + + steps: + - name: Check repo not fork and public + run: | + REPO=$(gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "/repos/${GITHUB_REPOSITORY}") + FORK=$(jq '.fork' <<< "$REPO") + PRIVATE=$(jq '.private' <<< "$REPO") + echo "FORK='$FORK', PRIVATE='$PRIVATE', GITHUB_REPOSITORY_OWNER=${GITHUB_REPOSITORY_OWNER} " + if [[ "$FORK" == "true" ]] + then + echo "Use the repository automatically created by Yandex Practicum (works in fork repositories are not accepted)" + echo "Используйте только репозиторий созданный Yandex Practicum, работы в форк репозитории не принимаются" + exit -1 + fi + if [[ "$GITHUB_REPOSITORY_OWNER" == "yandex-praktikum" ]] + then + echo "Use the repository automatically created by Yandex Practicum (works in fork repositories are not accepted)" + echo "Используйте только репозиторий созданный Yandex Practicum, работы в форк репозитории не принимаются" + exit -2 + fi + if [[ "$PRIVATE" == "true" && "$GITHUB_REPOSITORY_OWNER" != "praktikum-java" ]] + then + echo "Share your repository, make it public" + echo "Откройте доступ к вашему репозиторию, сделайте его публичным" + exit -3 + fi + env: + GH_TOKEN: ${{ github.token }} + + - name: Check branch name + run: | + if [[ "$GITHUB_BASE_REF" != "main" && "$GITHUB_REPOSITORY_OWNER" != "praktikum-java" ]] + then + echo "Set the pull request to merge branch 'main' (instead of '$GITHUB_BASE_REF')" + echo "Задайте в Pull request ветку слияния 'main' (вместо '$GITHUB_BASE_REF')" + exit -2 + fi + echo "Github target '$GITHUB_BASE_REF' - OK" + + if [[ "$GITHUB_HEAD_REF" == "add-controllers" ]] + then + echo "Sprint 14: add-controllers - OK" + exit + fi + if [[ "$GITHUB_HEAD_REF" == "add-bookings" ]] + then + echo "Sprint 15: add-bookings - OK" + PULL=$(gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/${GITHUB_REPOSITORY}/pulls?head=${GITHUB_REPOSITORY_OWNER}:add-controllers || true) + OPEN=$(jq '. | length' <<< "$PULL") + if [[ "$OPEN" != "0" && "$GITHUB_REPOSITORY_OWNER" != "praktikum-java" ]] + then + PULL_URL=$(jq '.[0].html_url' <<< "$PULL") + echo "Merge the add-controllers branch pull request: ${PULL_URL}" + echo "Объедините pull request ветки add-controllers: ${PULL_URL}" + exit -3 + fi + echo "Sprint 14: add-controllers - Merged" + exit + fi + if [[ "$GITHUB_HEAD_REF" == "add-item-requests-and-gateway" ]] + then + echo "Sprint 16: add-item-requests-and-gateway - OK" + PULL=$(gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/${GITHUB_REPOSITORY}/pulls?head=${GITHUB_REPOSITORY_OWNER}:add-bookings || true) + OPEN=$(jq '. | length' <<< "$PULL") + if [[ "$OPEN" != "0" && "$GITHUB_REPOSITORY_OWNER" != "praktikum-java" ]] + then + PULL_URL=$(jq '.[0].html_url' <<< "$PULL") + echo "Merge the add-bookings branch pull request: ${PULL_URL}" + echo "Объедините pull request ветки add-bookings: ${PULL_URL}" + exit -4 + fi + echo "Sprint 15: add-bookings - Merged" + exit + fi + echo "Correct branch name '$GITHUB_HEAD_REF' according to the spec, allowed: add-controllers, add-bookings, add-item-requests-and-gateway" + echo "Исправьте пожалуйста имя ветки '$GITHUB_HEAD_REF' согласно заданию, разрешены: add-controllers, add-bookings, add-item-requests-and-gateway" + exit -1 + env: + GH_TOKEN: ${{ github.token }} + + - name: Check files + run: | + PULL=$(gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/${GITHUB_REPOSITORY}/pulls/${PULL_NUMBER}/files?per_page=100 || true) + FILENAMES=$(jq '.[] | .filename' <<< "$PULL") + if [[ "$FILENAMES" =~ "api-tests.yml" ]] + then + echo "The pull request contains the api-tests.yml file and cannot be modified. Remove it from PR" + echo "Pull request содержит файл api-tests.yml, его изменять нельзя. Удалите его из PR" + exit -1 + fi + if [[ "$FILENAMES" =~ "checkstyle.xml" ]] + then + echo "The pull request contains the checkstyle.xml file and cannot be modified. Remove it from PR" + echo "Pull request содержит файл checkstyle.xml, его изменять нельзя. Удалите его из PR" + exit -2 + fi + if [[ "$FILENAMES" =~ ".class" ]] || [[ "$FILENAMES" =~ ".jar" ]] || [[ "$FILENAMES" =~ "mvn" ]] || [[ "$FILENAMES" =~ ".DS_Store" ]] \ + || [[ "$FILENAMES" =~ ".idea" ]] || [[ "$FILENAMES" =~ ".iws" ]] || [[ "$FILENAMES" =~ ".iml" ]] || [[ "$FILENAMES" =~ ".ipr" ]] \ + || [[ "$FILENAMES" =~ ".db" ]] || [[ "$FILENAMES" =~ ".log" ]] || [[ "$FILENAMES" =~ "target/" ]] + then + echo "The pull request contains the binary files. Remove them (*.class, *.jar, *.DS_Store ...) from PR" + echo "Pull request содержит двоичные файлы. Удалите их (*.class, *.jar, *.DS_Store ...) из PR" + exit -3 + fi + echo "PR files - OK" + exit + env: + PULL_NUMBER: ${{ github.event.number }} + GH_TOKEN: ${{ github.token }} + + build-shareit: + + needs: check-repo + runs-on: ubuntu-latest + + steps: + - name: Checkout target repo + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + + - name: Checkout tests + uses: actions/checkout@v4 + with: + repository: 'yandex-praktikum/java-shareit' + ref: ${{ github.event.pull_request.head.ref }} + path: tests + + - name: Check and Build application + run: | + chmod a+x ./tests/.github/workflows/build.sh + ./tests/.github/workflows/build.sh + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: '16.x' + + - name: Install newman + run: | + npm install -g newman + npm install -g newman-reporter-htmlextra + + - name: Run Application + run: | + chmod a+x ./tests/.github/workflows/run.sh + ./tests/.github/workflows/run.sh + + - name: Run POSTMAN tests + run: > + newman run ./tests/postman/sprint.json + --delay-request 50 -r cli,htmlextra + --verbose --color on --reporter-htmlextra-darkTheme + --reporter-htmlextra-export reports/shareIt.html + --reporter-htmlextra-title "Отчет по тестам" + --reporter-htmlextra-logs true + --reporter-htmlextra-template ./tests/.github/workflows/dashboard-template.hbs + + - name: Compose logs + if: ${{ failure() }} + run: | + if test -f "docker-compose.yml"; then + docker compose -f docker-compose.yml ps + docker compose -f docker-compose.yml stop + docker compose -f docker-compose.yml logs -f gateway >> ./gateway.log || true + docker compose -f docker-compose.yml logs -f server >> ./server.log || true + fi + + + - name: Upload log artifact + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: shareIt_log + path: ./*.log + + - name: Archive artifacts + uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: postman_tests_report + path: reports diff --git a/.github/workflows/build.sh b/.github/workflows/build.sh new file mode 100644 index 0000000..158d8ec --- /dev/null +++ b/.github/workflows/build.sh @@ -0,0 +1,8 @@ +cp -rf ./tests/checkstyle.xml ./checkstyle.xml +cp -rf ./tests/suppressions.xml ./suppressions.xml +cp -rf ./tests/gateway/Dockerfile ./gateway/Dockerfile +cp -rf ./tests/server/Dockerfile ./server/Dockerfile +cp -rf ./tests/docker-compose.yml ./docker-compose.yml +mvn enforcer:enforce -Denforcer.rules=requireProfileIdsExist -P check --no-transfer-progress && +mvn verify -P check,coverage --no-transfer-progress && +docker compose -f docker-compose.yml build \ No newline at end of file diff --git a/.github/workflows/dashboard-template.hbs b/.github/workflows/dashboard-template.hbs new file mode 100644 index 0000000..56aa163 --- /dev/null +++ b/.github/workflows/dashboard-template.hbs @@ -0,0 +1,1672 @@ + + + + + {{browserTitle}} + + + + + + + + + +
+
+ + + +
+ {{#with summary}} +
+
+ + {{/with}} +
+
+
+
+

{{title}}

+
{{timestamp}}
+{{#with summary}} +
+
+
+
+
+ +
+
Итого итераций
+

{{stats.iterations.total}}

+
+
+
+
+
+
+
+ +
+
Всего проверенных утверждений
+

{{totalTests stats.assertions.total skippedTests.length}}

+
+
+
+
+
+
+
+ +
+
Всего проваленных тестов
+

{{failures.length}}

+
+
+
+
+
+
+
+ +
+
Всего пропущено тестов
+

{{#gt skippedTests.length 0}}{{skippedTests.length}}{{else}}0{{/gt}}

+
+
+
+
+
+
+
+
+
+
+
+
File Information
+ Коллекция: {{collection.name}}
+ {{/with}} + {{#if folders}} Указанные папки: {{folders}}
{{/if}} + {{#with summary}} + {{#if environment.name}} Окружение: {{environment.name}}
{{/if}} +
+
+
+
+ {{#if @root.showGlobalData}} + {{#if globals.values.members.length}} +
+
+
+
+
+ +
+
+
+ +
+ + + + {{#each globals.values.members}} + {{#isNotIn key @root.skipGlobalVars}} + + + + + {{/isNotIn}} + {{/each}} + +
Название переменнойЗначение переменной
{{key}}{{value}}
+
+
+
+
+
+
+
+ {{/if}} + {{/if}} + {{#if @root.showEnvironmentData}} + {{#if environment.values.members.length}} +
+
+
+
+
+ +
+
+
+ +
+ + + + {{#each environment.values.members}} + {{#isNotIn key @root.skipEnvironmentVars}} + + + + + {{/isNotIn}} + {{/each}} + +
Название переменнойЗначение переменной
{{key}}{{value}}
+
+
+
+
+
+
+
+ {{/if}} + {{/if}} + {{#if collection.description}} +
+
+
+
+
Описание Коллекции
+
+ {{collection.description}} +
+
+
+
+
+ {{/if}} +
+
+
+
+
Временные рамки и данные
+ Общая длительность выполнения: {{duration}}
+ Всего данных получено: {{responseTotal}}
+ Среднее время отклика: {{responseAverage}}
+
+
+
+
+ {{/with}} +
+
+
+ + + + + + + + + + {{#with summary.stats}} + + + + + + + + + + + + + + + + {{/with}} + {{#with summary}} + + + + + + + + + + + {{/with}} + +
Элемент сводных данныхВсегоПровалено
Requests{{requests.total}}{{requests.failed}}
Prerequest Scripts{{prerequestScripts.total}}{{prerequestScripts.failed}}
Test Scripts{{testScripts.total}}{{testScripts.failed}}
Assertions{{totalTests stats.assertions.total skippedTests.length}}{{stats.assertions.failed}}
Skipped Tests{{#gt skippedTests.length 0}}{{skippedTests.length}}{{else}}0{{/gt}}-
+
+
+
+
+
+
+
+
+
+
+ + + {{#if summary.failures.length}} +
+ +
+
+
+ + {{#with summary}} +
+

Showing {{failures.length}} {{#gt failures.length 1}}Failures{{else}}Failure{{/gt}}

+
+ {{/with}} + {{#each summary.failures}} +
+
+
+ +
+
+
Failed Test: {{error.test}}
+
+
Assertion Error Message
+
+
{{error.message}}
+
+
+
+
+
+
+ {{/each}} + {{else}} +
+

There are no failed tests



+
+ {{/if}} +
+ +
+ + + {{#if summary.skippedTests.length}} +
+ +
+
+
+ + {{#with summary}} +
+

Showing {{skippedTests.length}} Skipped {{#gt skippedTests.length 1}}Tests{{else}}Test{{/gt}}

+
+ {{/with}} + {{#each summary.skippedTests}} +
+
+
+ +
+
+
Request Name: {{item.name}}
+
+
+
+
+
+ {{/each}} + {{else}} +
+

There are no skipped tests



+
+ {{/if}} +
+
+ + + +
+ {{#if summary.failures.length}} + + {{/if}} + + +
+ +
+ {{#with summary}} +
{{stats.iterations.total}} {{#gt stats.iterations.total 1}}Iterations{{else}}Iteration{{/gt}} available to view
+ {{#gt stats.iterations.total 18}}{{/gt}} + {{/with}} + +
+
+
+{{#each aggregations}} + {{#isNotIn parent.name @root.skipFolders}} + {{#if parent.name }} + +
+ {{> aggregations}} +
+ {{else}} + {{> aggregations}} + {{/if}} + {{/isNotIn}} +{{/each}} +
+
+
+
+
+
+ +{{#*inline "aggregations"}} +{{#isNotIn parent.name @root.skipFolders}} +{{#if @root.showFolderDescription}} +{{#if parent.description.content}} + +
+
+
+
+
+
Описание папки
+
+ {{parent.description.content}} +
+
+
+
+
+
+{{/if}} +{{/if}} +{{#each executions}} +{{#isNotIn item.name @root.skipRequests}} +
+
+
+
+
+ + {{#if cumulativeTests.skipped}} + {{cumulativeTests.skipped}} Пропущено {{#gt cumulativeTests.skipped 1}}Тестов{{else}}Тест{{/gt}} + {{/if}} +
+
+
+ {{#with request}} + {{#if description.content}} +
+
+
+
+
+
Описание запроса
+
+ {{description.content}} +
+
+
+
+
+
+ {{/if}} + {{/with}} +
+
+
+
+
+
Информация о запросе
+ HTTP-метод запроса: {{request.method}}
+ URL запроса: {{request.url}}
+
+
+
+
+
Информация об ответе
+ Код статуса ответа: {{response.code}} - {{response.status}}
+ Среднее время на запрос: {{mean.time}}
+ Средний размер одного запроса: {{mean.size}}
+
+
Процент прохождения тестов
+
+ {{#if assertions.length}} +
+
+
{{#gte cumulativeTests.passed 1}}{{percent cumulativeTests.passed cumulativeTests.failed}} %{{else}}0 %{{/gte}}
+
+
+ {{else}} +
+
+
Для данного запроса нет тестов
+
+
+ {{/if}} +
+
+
+
+
+
+ {{#with request}} + {{#unless @root.omitHeaders}} + {{#unless @root.skipSensitiveData}} +
+
+
+
+
+
Заголовки запроса
+ {{#if headers}} +
+ + + + {{#each headers.members}} + {{#isNotIn key @root.skipHeaders}} + + + + + {{/isNotIn}} + {{/each}} + +
Название заголовкаЗначение заголовка
{{key}}{{value}}
+
+ {{/if}} +
+
+
+
+
+ {{/unless}} + {{/unless}} + {{/with}} + {{#unless @root.skipSensitiveData}} + {{#unless @root.omitRequestBodies}} + {{#isNotIn item.name @root.hideRequestBody}} + {{#with request}} + {{#if body.raw}} +
+
+
+
+
+
Тело запроса
+
+
{{body.raw}}
+
+ +
+
+
+
+
+ {{/if}} + {{/with}} + {{/isNotIn}} + {{/unless}} + {{/unless}} + {{#unless @root.skipSensitiveData}} + {{#unless @root.omitRequestBodies}} + {{#isNotIn item.name @root.hideRequestBody}} + {{#with request}} + {{#if body.formdata.members}} +
+
+
+
+
+
Тело Запроса
+
+
{{formdata body.formdata.members}}
+
+ +
+
+
+
+
+ {{/if}} + {{/with}} + {{/isNotIn}} + {{/unless}} + {{/unless}} + {{#unless @root.skipSensitiveData}} + {{#unless @root.omitRequestBodies}} + {{#isNotIn item.name @root.hideRequestBody}} + {{#with request}} + {{#if body.urlencoded.members}} +
+
+
+
+
+
Тело Запроса
+
+
{{formdata body.urlencoded.members}}
+
+ +
+
+
+
+
+ {{/if}} + {{/with}} + {{/isNotIn}} + {{/unless}} + {{/unless}} + {{#unless @root.skipSensitiveData}} + {{#unless @root.omitRequestBodies}} + {{#isNotIn item.name @root.hideRequestBody}} + {{#with request}} + {{#if body.graphql}} +
+
+
+
+
+
Тело Запроса
+
+
{{body.graphql.query}}
+
+ + {{#if body.graphql.variables }} +
Graphql Variables
+
+
{{body.graphql.variables}}
+
+ {{/if}} +
+
+
+
+
+ {{/if}} + {{/with}} + {{/isNotIn}} + {{/unless}} + {{/unless}} + {{#unless @root.omitHeaders}} + {{#unless @root.skipSensitiveData}} +
+
+
+
+
+
Заголовки Ответа
+ {{#if response.header}} +
+ + + + {{#each response.header}} + {{#isNotIn key @root.skipHeaders}} + + + + + {{/isNotIn}} + {{/each}} + +
Название заголовкаЗначение заголовка
{{key}}{{value}}
+
+ {{/if}} +
+
+
+
+
+ {{/unless}} + {{/unless}} + {{#unless @root.skipSensitiveData}} + {{#unless @root.omitResponseBodies}} + {{#isNotIn item.name @root.hideResponseBody}} +
+
+
+
+
+
Тело Ответа
+ {{#if response.body}} +
+
{{response.body}}
+
+ + {{else}} +
У ответа на этот запрос нет тела
+ {{/if}} +
+
+
+
+
+ {{/isNotIn}} + {{/unless}} + {{/unless}} + {{#if consoleLogs.length}} +
+
+
+
+
+
Логи консоли
+
+
+ + + + {{#each consoleLogs}} + + + + {{/each}} + +
Залогированные сообщения
{{#each messages}}{{this}}{{/each}}
+
+
+
+
+
+
+
+ {{/if}} +
+
+
+
Информация о тесте
+ {{#if assertions.length}} +
+ + + + {{#each assertions}} + + + + + + + {{/each}} + + + + + + + + + +
НазваниеПройденоПроваленоПропущено
{{this.name}}{{this.passed}}{{this.failed}}{{this.skipped}}
Всего{{cumulativeTests.passed}}{{cumulativeTests.failed}}{{cumulativeTests.skipped}}
+
+
+
+
+
+
+
{{#lte cumulativeTests.failed 1}}Проваленный тест{{else}}Проваленные тесты{{/lte}}
+
+ + + + {{#each assertions}} + {{#isTheSame testFailure.test this.name}} + + + + + {{/isTheSame}} + {{/each}} + +
Название тестаОшибка проверки
{{testFailure.test}}
{{testFailure.message}}
+
+
+
+
+
+
+ {{else}} +
No Tests for this request
+ {{/if}} +
+
+
+
+
+
+
+
+
+{{/isNotIn}} +{{/each}} +{{/isNotIn}} +{{/inline}} + + + + + + +{{#eq noSyntaxHighlighting false}} + + +{{/eq}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/workflows/run.sh b/.github/workflows/run.sh new file mode 100644 index 0000000..b02c7e2 --- /dev/null +++ b/.github/workflows/run.sh @@ -0,0 +1,10 @@ +chmod a+x ./tests/.github/workflows/wait-for-it.sh +docker compose -f docker-compose.yml up --detach && +echo "Docker is up" && +./tests/.github/workflows/wait-for-it.sh -t 60 localhost:9090 && +echo "Server is up" && +./tests/.github/workflows/wait-for-it.sh -t 60 localhost:8080 && +echo "Gateway is up" +result=$? +docker compose -f docker-compose.yml logs +exit $result diff --git a/.github/workflows/wait-for-it.sh b/.github/workflows/wait-for-it.sh new file mode 100644 index 0000000..859de3f --- /dev/null +++ b/.github/workflows/wait-for-it.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else +# (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + (curl --fail --silent $WAITFORIT_HOST:$WAITFORIT_PORT/actuator/health | grep UP) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi \ No newline at end of file diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..05d5086 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/suppressions.xml b/suppressions.xml new file mode 100644 index 0000000..b2c6822 --- /dev/null +++ b/suppressions.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file From 565f1638491f098e0c13c16742e3d6e87c4f67a8 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Thu, 16 Apr 2026 12:46:40 +0300 Subject: [PATCH 18/28] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B5=D1=89=D0=B5=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - .run/ShareItGateway.run.xml | 15 + .run/ShareItServer.run.xml | 15 + postman/sprint.json | 4052 +++++++++++++++++++++++++++++++++++ 4 files changed, 4082 insertions(+), 1 deletion(-) create mode 100644 .run/ShareItGateway.run.xml create mode 100644 .run/ShareItServer.run.xml create mode 100644 postman/sprint.json diff --git a/.gitignore b/.gitignore index 53a38f5..549e00a 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,3 @@ build/ ### VS Code ### .vscode/ -/.stels/ diff --git a/.run/ShareItGateway.run.xml b/.run/ShareItGateway.run.xml new file mode 100644 index 0000000..32c8129 --- /dev/null +++ b/.run/ShareItGateway.run.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/.run/ShareItServer.run.xml b/.run/ShareItServer.run.xml new file mode 100644 index 0000000..a8ed9e5 --- /dev/null +++ b/.run/ShareItServer.run.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/postman/sprint.json b/postman/sprint.json new file mode 100644 index 0000000..6e87b6b --- /dev/null +++ b/postman/sprint.json @@ -0,0 +1,4052 @@ +{ + "info": { + "_postman_id": "7fc26681-0775-4307-885f-fd53560f0317", + "name": "Sprint 16 ShareIt (add-item-requests-and-gateway)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "23073145", + "_collection_link": "https://universal-shadow-295426.postman.co/workspace/My-Workspace~4200f6aa-0504-44b1-8a1d-707d0dcbd5ce/collection/13708500-7fc26681-0775-4307-885f-fd53560f0317?action=share&source=collection_link&creator=23073145" + }, + "item": [ + { + "name": "users", + "item": [ + { + "name": "Create user", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = rnd.getUser();\r", + " pm.collectionVariables.set(\"userName\", user.name);\r", + " pm.collectionVariables.set(\"userEmail\", user.email);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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(\"Status code is 200 or 201\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([200,201]);\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{userName}}\",\n \"email\": \"{{userEmail}}\"\n}" + }, + "url": { + "raw": "localhost:8080/users", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users" + ] + } + }, + "response": [] + }, + { + "name": "Create user without email", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user2 = rnd.getUser();\r", + " pm.collectionVariables.set(\"userName\", user2.name);\r", + " pm.collectionVariables.set(\"userEmail\", user2.email);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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(\"Status code is 400\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{userName}}\"\n}" + }, + "url": { + "raw": "localhost:8080/users", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users" + ] + } + }, + "response": [] + }, + { + "name": "Create 2 users with same email", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user1 = rnd.getUser();\r", + " us = await api.addUser(user1);\r", + " user2 = rnd.getUser();\r", + " user2.email = user1.email;\r", + " pm.collectionVariables.set(\"userName\", user2.name);\r", + " pm.collectionVariables.set(\"userEmail\", user2.email);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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(\"Status code is 409\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([409, 500]);\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{userName}}\",\n \"email\": \"{{userEmail}}\"\n}" + }, + "url": { + "raw": "localhost:8080/users", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users" + ] + } + }, + "response": [] + }, + { + "name": "Create user with invalid email", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = rnd.getUser();\r", + " pm.collectionVariables.set(\"userName\", user.name);\r", + " pm.collectionVariables.set(\"userEmail\", user.email);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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(\"Status code is 400\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{userName}}\",\n \"email\": \"user.com\"\n}" + }, + "url": { + "raw": "localhost:8080/users", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users" + ] + } + }, + "response": [] + }, + { + "name": "User update", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = rnd.getUser();\r", + " us = await api.addUser(user);\r", + " pm.collectionVariables.set(\"userId\", us.id);\r", + " us = rnd.getUser()\r", + " pm.collectionVariables.set(\"userName\", us.name);\r", + " pm.collectionVariables.set(\"userEmail\", us.email);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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(\"Status code is 200\", function () {\r", + " pm.response.to.be.ok;\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "pm.test(\"Test user 'id' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('id');\r", + " var id = pm.collectionVariables.get(\"userId\");\r", + " pm.expect(jsonData.id, '\"id\" must be ' + id).to.eql(Number(id));\r", + "});\r", + "pm.test(\"Test user 'email' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('email');\r", + " var email = pm.collectionVariables.get(\"userEmail\");\r", + " pm.expect(jsonData.email, '\"email\" must be ' + email).to.eql(email);\r", + "});\r", + "pm.test(\"Test user 'name' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('name');\r", + " var name = pm.collectionVariables.get(\"userName\");\r", + " pm.expect(jsonData.name, '\"name\" must be ' + name).to.eql(name);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{userName}}\",\n \"email\": \"{{userEmail}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/users/{{userId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "{{userId}}" + ] + } + }, + "response": [] + }, + { + "name": "User update name", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = rnd.getUser();\r", + " us = await api.addUser(user);\r", + " pm.collectionVariables.set(\"userId\", us.id);\r", + " us = rnd.getUser()\r", + " pm.collectionVariables.set(\"userName\", us.name);\r", + " pm.collectionVariables.set(\"userEmail\", us.email);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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(\"Status code is 200\", function () {\r", + " pm.response.to.be.ok;\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "pm.test(\"Test user 'id' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('id');\r", + " var id = pm.collectionVariables.get(\"userId\");\r", + " pm.expect(jsonData.id, '\"id\" must be ' + id).to.eql(Number(id));\r", + "});\r", + "pm.test(\"Test user 'name' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('name');\r", + " var name = pm.collectionVariables.get(\"userName\");\r", + " pm.expect(jsonData.name, '\"name\" must be ' + name).to.eql(name);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{userName}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/users/{{userId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "{{userId}}" + ] + } + }, + "response": [] + }, + { + "name": "User update email", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = rnd.getUser();\r", + " us = await api.addUser(user);\r", + " pm.collectionVariables.set(\"userId\", us.id);\r", + " us = rnd.getUser()\r", + " pm.collectionVariables.set(\"userName\", us.name);\r", + " pm.collectionVariables.set(\"userEmail\", us.email);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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(\"Status code is 200\", function () {\r", + " pm.response.to.be.ok;\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "pm.test(\"Test user 'id' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('id');\r", + " var id = pm.collectionVariables.get(\"userId\");\r", + " pm.expect(jsonData.id, '\"id\" must be ' + id).to.eql(Number(id));\r", + "});\r", + "pm.test(\"Test user 'email' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('email');\r", + " var email = pm.collectionVariables.get(\"userEmail\");\r", + " pm.expect(jsonData.email, '\"email\" must be ' + email).to.eql(email);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{userEmail}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/users/{{userId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "{{userId}}" + ] + } + }, + "response": [] + }, + { + "name": "User update with existing email", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = rnd.getUser();\r", + " us = await api.addUser(user);\r", + " user2 = rnd.getUser();\r", + " us2 = await api.addUser(user2)\r", + " pm.collectionVariables.set(\"userId\", us2.id);\r", + " usa = rnd.getUser()\r", + " pm.collectionVariables.set(\"userName\", usa.name);\r", + " pm.collectionVariables.set(\"userEmail\", user.email);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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(\"Status code is 409\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([409, 500]);\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{userName}}\",\n \"email\": \"{{userEmail}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/users/{{userId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "{{userId}}" + ] + } + }, + "response": [] + }, + { + "name": "Get user", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = rnd.getUser();\r", + " us = await api.addUser(user);\r", + " pm.collectionVariables.set(\"userId\", us.id);\r", + " pm.collectionVariables.set(\"userName\", us.name);\r", + " pm.collectionVariables.set(\"userEmail\", us.email);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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(\"Status code is 200\", function () {\r", + " pm.response.to.be.ok;\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "pm.test(\"Test user 'id' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('id');\r", + " var id = pm.collectionVariables.get(\"userId\");\r", + " pm.expect(jsonData.id, '\"id\" must be ' + id).to.eql(Number(id));\r", + "});\r", + "pm.test(\"Test user 'email' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('email');\r", + " var email = pm.collectionVariables.get(\"userEmail\");\r", + " pm.expect(jsonData.email, '\"email\" must be ' + email).to.eql(email);\r", + "});\r", + "pm.test(\"Test user 'name' field\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('name');\r", + " var name = pm.collectionVariables.get(\"userName\");\r", + " pm.expect(jsonData.name, '\"name\" must be ' + name).to.eql(name);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{baseUrl}}/users/{{userId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "{{userId}}" + ] + } + }, + "response": [] + }, + { + "name": "User delete", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([200,204]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = rnd.getUser();\r", + " us = await api.addUser(user);\r", + " pm.collectionVariables.set(\"userId\", us.id);\r", + " pm.collectionVariables.set(\"userName\", us.name);\r", + " pm.collectionVariables.set(\"userEmail\", us.email);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/users/{{userId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "{{userId}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Item", + "item": [ + { + "name": "Create Item", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " item = rnd.getItem();\r", + " pm.collectionVariables.set(\"item\", item);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " pm.collectionVariables.set(\"itemAvailable\", item.available);\r", + " pm.collectionVariables.set(\"itemDescription\", item.description);\r", + " user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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(\"Status code is 200 or 201\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([200,201]);\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "var item = pm.collectionVariables.get(\"item\");\r", + "\r", + "pm.test(\"Response data equal to request\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('id');\r", + " pm.expect(jsonData).to.have.property('name');\r", + " pm.expect(jsonData).to.have.property('description');\r", + " pm.expect(jsonData).to.have.property('available');\r", + " pm.expect(jsonData.name, `\"name\" must be ${item.name}`).to.eql(item.name);\r", + " pm.expect(jsonData.description, `\"description\" must be ${item.description}`).to.eql(item.description);\r", + " pm.expect(jsonData.available.toString(), `\"available\" must be ${item.available}`).to.eql(item.available.toString());\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{itemName}}\",\n \"description\": \"{{itemDescription}}\",\n \"available\": {{itemAvailable}}\n}" + }, + "url": { + "raw": "localhost:8080/items", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "items" + ] + } + }, + "response": [] + }, + { + "name": "Create Item on request", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user1 = await api.addUser(rnd.getUser());\r", + " var request = await api.addRequest(rnd.getRequest(), user1.id);\r", + " pm.collectionVariables.set(\"requestId\", request.id);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + "\r", + " item = rnd.getItem();\r", + " pm.collectionVariables.set(\"item\", item);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " pm.collectionVariables.set(\"itemAvailable\", item.available);\r", + " pm.collectionVariables.set(\"itemDescription\", item.description);\r", + "\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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(\"Status code is 200 or 201\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([200,201]);\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "var item = pm.collectionVariables.get(\"item\");\r", + "\r", + "pm.test(\"Response data equal to request\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('id');\r", + " pm.expect(jsonData).to.have.property('name');\r", + " pm.expect(jsonData).to.have.property('description');\r", + " pm.expect(jsonData).to.have.property('available');\r", + " pm.expect(jsonData.name, `\"name\" must be ${item.name}`).to.eql(item.name);\r", + " pm.expect(jsonData.description, `\"description\" must be ${item.description}`).to.eql(item.description);\r", + " pm.expect(jsonData.available.toString(), `\"available\" must be ${item.available}`).to.eql(item.available.toString());\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{itemName}}\",\n \"description\": \"{{itemDescription}}\",\n \"available\": {{itemAvailable}},\n \"requestId\": {{requestId}}\n}" + }, + "url": { + "raw": "localhost:8080/items", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "items" + ] + } + }, + "response": [] + }, + { + "name": "Create Item without name on request", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user1 = await api.addUser(rnd.getUser());\r", + " var request = await api.addRequest(rnd.getRequest(), user1.id);\r", + " pm.collectionVariables.set(\"requestId\", request.id);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + "\r", + " item = rnd.getItem();\r", + " pm.collectionVariables.set(\"item\", item);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " pm.collectionVariables.set(\"itemAvailable\", item.available);\r", + " pm.collectionVariables.set(\"itemDescription\", item.description);\r", + "\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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(\"Status code is 400\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "pm.test(\"Response data have error\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('error');\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"description\": \"{{itemDescription}}\",\n \"available\": {{itemAvailable}},\n \"requestId\": {{requestId}}\n}" + }, + "url": { + "raw": "localhost:8080/items", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "items" + ] + } + }, + "response": [] + }, + { + "name": "Create Item without description on request", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user1 = await api.addUser(rnd.getUser());\r", + " var request = await api.addRequest(rnd.getRequest(), user1.id);\r", + " pm.collectionVariables.set(\"requestId\", request.id);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + "\r", + " item = rnd.getItem();\r", + " pm.collectionVariables.set(\"item\", item);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " pm.collectionVariables.set(\"itemAvailable\", item.available);\r", + " pm.collectionVariables.set(\"itemDescription\", item.description);\r", + "\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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(\"Status code is 400\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "pm.test(\"Response data have error\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('error');\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{itemName}}\",\n \"available\": {{itemAvailable}},\n \"requestId\": {{requestId}}\n}" + }, + "url": { + "raw": "localhost:8080/items", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "items" + ] + } + }, + "response": [] + }, + { + "name": "Create Item without available on request", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user1 = await api.addUser(rnd.getUser());\r", + " var request = await api.addRequest(rnd.getRequest(), user1.id);\r", + " pm.collectionVariables.set(\"requestId\", request.id);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + "\r", + " item = rnd.getItem();\r", + " pm.collectionVariables.set(\"item\", item);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " pm.collectionVariables.set(\"itemAvailable\", item.available);\r", + " pm.collectionVariables.set(\"itemDescription\", item.description);\r", + "\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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(\"Status code is 400\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "pm.test(\"Response data have error\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('error');\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"{{itemName}}\",\n \"description\": \"{{itemDescription}}\",\n \"requestId\": {{requestId}}\n}" + }, + "url": { + "raw": "localhost:8080/items", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "items" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "requests", + "item": [ + { + "name": "Create request", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " request1 = rnd.getRequest();\r", + " pm.collectionVariables.set(\"requestDescription\", request1.description);\r", + " user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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(\"Status code is 200 or 201\", function () {\r", + " pm.expect(pm.response.code).to.be.oneOf([200,201]);\r", + "});\r", + "pm.test(\"Response have body\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "pm.test(\"Response data equal to request\", function () {\r", + " var description = pm.collectionVariables.get(\"requestDescription\");\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('id');\r", + " pm.expect(jsonData).to.have.property('description');\r", + " pm.expect(jsonData).to.have.property('created');\r", + " pm.expect(jsonData.description, `\"description\" must be ${description}`).to.eql(description);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}" + } + ], + "body": { + "mode": "raw", + "raw": "{ \n \"description\": \"{{requestDescription}}\"\n}" + }, + "url": { + "raw": "localhost:8080/requests", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "requests" + ] + } + }, + "response": [] + }, + { + "name": "Get user requests", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user.id);\r", + " request1 = await api.addRequest(rnd.getRequest(), user.id);\r", + " request2 = await api.addRequest(rnd.getRequest(), user.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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(\"Status code is 200\", function () {\r", + " pm.response.to.be.ok;\r", + "});\r", + "pm.test(\"User requests amount\", function () {\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData.length, 'List length must be 2').to.eql(2);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}" + } + ], + "url": { + "raw": "localhost:8080/requests", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "requests" + ] + } + }, + "response": [] + }, + { + "name": "Get user request by id", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user1 = await api.addUser(rnd.getUser());\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " req = await api.addRequest(rnd.getRequest(), user1.id);\r", + " pm.collectionVariables.set(\"requestId\", req.id);\r", + " item = await api.addItem(rnd.getItemForRequest(req.id), user2.id);\r", + " pm.collectionVariables.set(\"itemName\", item.name);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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(\"Status code is 200\", function () {\r", + " pm.response.to.be.ok;\r", + "});\r", + "pm.test(\"User requests amount\", function () {\r", + " var name = pm.collectionVariables.get(\"itemName\");\r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.have.property('id');\r", + " pm.expect(jsonData).to.have.property('description');\r", + " pm.expect(jsonData).to.have.property('created');\r", + " pm.expect(jsonData).to.have.property('items');\r", + " pm.expect(jsonData.items[0].name, `\"item name\" must be ${name}`).to.eql(name);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}" + } + ], + "url": { + "raw": "localhost:8080/requests/{{requestId}}", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "requests", + "{{requestId}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "bookings", + "item": [ + { + "name": "Booking unavailable item", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = false\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"itemId\": {{itemId}},\n \"start\": \"{{start}}\",\n \"end\": \"{{end}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + }, + { + "name": "Booking create failed by wrong userId", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 500\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([500, 404, 403]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id + 1);\r", + " it = rnd.getItem()\r", + " it.available = false\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"itemId\": {{itemId}},\n \"start\": \"{{start}}\",\n \"end\": \"{{end}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + }, + { + "name": "Booking create failed by not found itemId", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 404\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([404]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = false\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id + 1);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"itemId\": {{itemId}},\n \"start\": \"{{start}}\",\n \"end\": \"{{end}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + }, + { + "name": "Booking create failed by end in past", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().subtract(3, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().subtract(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = false\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"itemId\": {{itemId}},\n \"start\": \"{{start}}\",\n \"end\": \"{{end}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + }, + { + "name": "Booking create failed by start equal end", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = false\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"itemId\": {{itemId}},\n \"start\": \"{{start}}\",\n \"end\": \"{{end}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + }, + { + "name": "Booking create failed by start equal null", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "var moment = require('moment');\r", + "\r", + "var start = moment().add(2, 'd');\r", + "pm.environment.set('start_null', start.format('YYYY-MM-DDTHH:mm:ss'));\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"itemId\": {{itemId}},\n \"end\": \"{{end}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + }, + { + "name": "Booking create failed by end equal null", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "var moment = require('moment');\r", + "\r", + "var start = moment().add(2, 'd');\r", + "pm.environment.set('end_null', start.format('YYYY-MM-DDTHH:mm:ss'));\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"itemId\": {{itemId}},\n \"start\": \"{{start}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + }, + { + "name": "Booking create failed by start in past", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().subtract(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = false\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"itemId\": {{itemId}},\n \"start\": \"{{start}}\",\n \"end\": \"{{end}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + }, + { + "name": "Booking available item", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200 or 201\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([200,201]);", + "});", + "pm.test(\"Has booking create response\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "var item = pm.collectionVariables.get('item')", + "var user1 = pm.collectionVariables.get('user1')", + "var user2 = pm.collectionVariables.get('user2')", + "var start = pm.collectionVariables.get('start')", + "var end = pm.collectionVariables.get('end')", + "pm.test(\"Test booking 'start' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('start');", + " pm.expect(jsonData.start, '\"start\" must be \"' + pm.collectionVariables.get('start') + '\"').to.eql(pm.collectionVariables.get('start'));", + "});", + "pm.test(\"Test booking 'end' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('end');", + " pm.expect(jsonData.end, '\"end\" must be \"' + pm.collectionVariables.get('end') + '\"').to.eql(pm.collectionVariables.get('end'));", + "});", + "pm.test(\"Test booking 'status' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('status');", + " pm.expect(jsonData.status, '\"status\" must be \"WAITING\"').to.eql('WAITING');", + "});", + "pm.test(\"Test booking 'booker.id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('booker');", + " pm.expect(jsonData.booker).to.have.property('id');", + " pm.expect(jsonData.booker.id, '\"booker.id\" must be ${user2.id}').to.eql(user2.id);", + "});", + "pm.test(\"Test booking 'item.id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('id');", + " pm.expect(jsonData.item.id, '\"item.id\" must be ${item.id}').to.eql(item.id);", + "});", + "pm.test(\"Test booking 'item.name' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('name');", + " pm.expect(jsonData.item.name, '\"item.name\" must be ${item.name}').to.eql(item.name);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user1\", user1);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user2\", user2);\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"item\", item);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"itemId\": {{itemId}},\n \"start\": \"{{start}}\",\n \"end\": \"{{end}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + }, + { + "name": "Booking approve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([200]);", + "});", + "pm.test(\"Has booking patch response\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "var item = pm.collectionVariables.get('item')", + "var book = pm.collectionVariables.get('booking')", + "var user1 = pm.collectionVariables.get('user1')", + "var user2 = pm.collectionVariables.get('user2')", + "pm.test(\"Test booking 'id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('id');", + " pm.expect(jsonData.id, '\"id\" must be ${book.id}').to.eql(book.id);", + "});", + "pm.test(\"Test booking 'start' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('start');", + " pm.expect(jsonData.start, '\"start\" must be \"' + pm.collectionVariables.get('start') + '\"').to.eql(pm.collectionVariables.get('start'));", + "});", + "pm.test(\"Test booking 'end' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('end');", + " pm.expect(jsonData.end, '\"end\" must be \"' + pm.collectionVariables.get('end') + '\"').to.eql(pm.collectionVariables.get('end'));", + "});", + "pm.test(\"Test booking 'status' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('status');", + " pm.expect(jsonData.status, '\"status\" must be \"APPROVED\"').to.eql('APPROVED');", + "});", + "pm.test(\"Test booking 'booker.id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('booker');", + " pm.expect(jsonData.booker).to.have.property('id');", + " pm.expect(jsonData.booker.id, '\"booker.id\" must be ${user2.id}').to.eql(user2.id);", + "});", + "pm.test(\"Test booking 'item.id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('id');", + " pm.expect(jsonData.item.id, '\"item.id\" must be ${item.id}').to.eql(item.id);", + "});", + "pm.test(\"Test booking 'item.name' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('name');", + " pm.expect(jsonData.item.name, '\"item.name\" must be ${item.name}').to.eql(item.name);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user1\", user1);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user2\", user2);\r", + " pm.collectionVariables.set(\"userId\", user1.id);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"item\", item);\r", + " book = await api.addBooking(rnd.getBooking(item.id, start, end), user2.id)\r", + " pm.collectionVariables.set(\"bookingId\", book.id);\r", + " pm.collectionVariables.set(\"booking\", book);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/bookings/{{bookingId}}?approved=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings", + "{{bookingId}}" + ], + "query": [ + { + "key": "approved", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "Booking approve by wrong user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 403\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 403, 500]);", + "});", + "pm.test(\"Has booking patch response\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user1\", user1);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user2\", user2);\r", + " pm.collectionVariables.set(\"userId\", user1.id + 2);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"item\", item);\r", + " book = await api.addBooking(rnd.getBooking(item.id, start, end), user2.id)\r", + " pm.collectionVariables.set(\"bookingId\", book.id);\r", + " pm.collectionVariables.set(\"booking\", book);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/bookings/{{bookingId}}?approved=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings", + "{{bookingId}}" + ], + "query": [ + { + "key": "approved", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "Get booking by booker", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.be.ok;", + "});", + "pm.test(\"Has booking create response\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "var item = pm.collectionVariables.get('item')", + "var user1 = pm.collectionVariables.get('user1')", + "var user2 = pm.collectionVariables.get('user2')", + "var start = pm.collectionVariables.get('start')", + "var end = pm.collectionVariables.get('end')", + "pm.test(\"Test booking 'start' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('start');", + " pm.expect(jsonData.start, '\"start\" must be \"' + pm.collectionVariables.get('start') + '\"').to.eql(pm.collectionVariables.get('start'));", + "});", + "pm.test(\"Test booking 'end' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('end');", + " pm.expect(jsonData.end, '\"end\" must be \"' + pm.collectionVariables.get('end') + '\"').to.eql(pm.collectionVariables.get('end'));", + "});", + "pm.test(\"Test booking 'status' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('status');", + " pm.expect(jsonData.status, '\"status\" must be \"WAITING\"').to.eql('WAITING');", + "});", + "pm.test(\"Test booking 'booker.id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('booker');", + " pm.expect(jsonData.booker).to.have.property('id');", + " pm.expect(jsonData.booker.id, '\"booker.id\" must be ${user2.id}').to.eql(user2.id);", + "});", + "pm.test(\"Test booking 'item.id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('id');", + " pm.expect(jsonData.item.id, '\"item.id\" must be ${item.id}').to.eql(item.id);", + "});", + "pm.test(\"Test booking 'item.name' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('name');", + " pm.expect(jsonData.item.name, '\"item.name\" must be ${item.name}').to.eql(item.name);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user1\", user1);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user2\", user2);\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"item\", item);\r", + " book = await api.addBooking(rnd.getBooking(item.id, start, end), user2.id)\r", + " pm.collectionVariables.set(\"bookingId\", book.id);\r", + " pm.collectionVariables.set(\"booking\", book);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/bookings/{{bookingId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings", + "{{bookingId}}" + ] + } + }, + "response": [] + }, + { + "name": "Get booking by owner", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.be.ok;", + "});", + "pm.test(\"Has booking create response\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "var item = pm.collectionVariables.get('item')", + "var user1 = pm.collectionVariables.get('user1')", + "var user2 = pm.collectionVariables.get('user2')", + "var start = pm.collectionVariables.get('start')", + "var end = pm.collectionVariables.get('end')", + "pm.test(\"Test booking 'start' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('start');", + " pm.expect(jsonData.start, '\"start\" must be \"' + pm.collectionVariables.get('start') + '\"').to.eql(pm.collectionVariables.get('start'));", + "});", + "pm.test(\"Test booking 'end' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('end');", + " pm.expect(jsonData.end, '\"end\" must be \"' + pm.collectionVariables.get('end') + '\"').to.eql(pm.collectionVariables.get('end'));", + "});", + "pm.test(\"Test booking 'status' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('status');", + " pm.expect(jsonData.status, '\"status\" must be \"WAITING\"').to.eql('WAITING');", + "});", + "pm.test(\"Test booking 'booker.id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('booker');", + " pm.expect(jsonData.booker).to.have.property('id');", + " pm.expect(jsonData.booker.id, '\"booker.id\" must be ${user2.id}').to.eql(user2.id);", + "});", + "pm.test(\"Test booking 'item.id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('id');", + " pm.expect(jsonData.item.id, '\"item.id\" must be ${item.id}').to.eql(item.id);", + "});", + "pm.test(\"Test booking 'item.name' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('name');", + " pm.expect(jsonData.item.name, '\"item.name\" must be ${item.name}').to.eql(item.name);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user1\", user1);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user2\", user2);\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"item\", item);\r", + " book = await api.addBooking(rnd.getBooking(item.id, start, end), user2.id)\r", + " pm.collectionVariables.set(\"bookingId\", book.id);\r", + " pm.collectionVariables.set(\"booking\", book);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/bookings/{{bookingId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings", + "{{bookingId}}" + ] + } + }, + "response": [] + }, + { + "name": "Get all bookings from wrong user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 500\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([500,404,403]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"userId\", user2.id + 1);\r", + "\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/bookings/owner", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings", + "owner" + ] + } + }, + "response": [] + }, + { + "name": "Get all user bookings", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.be.ok;", + "});", + "pm.test(\"Has booking patch response\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "var item = pm.collectionVariables.get('item')", + "var book = pm.collectionVariables.get('booking')", + "var user1 = pm.collectionVariables.get('user1')", + "var user2 = pm.collectionVariables.get('user2')", + "var jsonData = pm.response.json()[0];", + "", + "pm.test(\"Test booking 'id' field\", function () {", + " pm.expect(jsonData).to.have.property('id');", + " pm.expect(jsonData.id, '\"id\" must be ${book.id}').to.eql(book.id);", + "});", + "pm.test(\"Test booking 'start' field\", function () {", + " pm.expect(jsonData).to.have.property('start');", + " pm.expect(jsonData.start, '\"start\" must be \"' + pm.collectionVariables.get('start') + '\"').to.eql(pm.collectionVariables.get('start'));", + "});", + "pm.test(\"Test booking 'end' field\", function () {", + " pm.expect(jsonData).to.have.property('end');", + " pm.expect(jsonData.end, '\"end\" must be \"' + pm.collectionVariables.get('end') + '\"').to.eql(pm.collectionVariables.get('end'));", + "});", + "pm.test(\"Test booking 'status' field\", function () {", + " pm.expect(jsonData).to.have.property('status');", + " pm.expect(jsonData.status, '\"status\" must be \"WAITING\"').to.eql('WAITING');", + "});", + "pm.test(\"Test booking 'booker.id' field\", function () {", + " pm.expect(jsonData).to.have.property('booker');", + " pm.expect(jsonData.booker).to.have.property('id');", + " pm.expect(jsonData.booker.id, '\"booker.id\" must be ${user2.id}').to.eql(user2.id);", + "});", + "pm.test(\"Test booking 'item.id' field\", function () {", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('id');", + " pm.expect(jsonData.item.id, '\"item.id\" must be ${item.id}').to.eql(item.id);", + "});", + "pm.test(\"Test booking 'item.name' field\", function () {", + " pm.expect(jsonData).to.have.property('item');", + " pm.expect(jsonData.item).to.have.property('name');", + " pm.expect(jsonData.item.name, '\"item.name\" must be ${item.name}').to.eql(item.name);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'days').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user1\", user1);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user2\", user2);\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"item\", item);\r", + " book = await api.addBooking(rnd.getBooking(item.id, start, end), user2.id)\r", + " pm.collectionVariables.set(\"bookingId\", book.id);\r", + " pm.collectionVariables.set(\"booking\", book);\r", + "\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/bookings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "bookings" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "comments", + "item": [ + { + "name": "Comment past booking", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200 or 201\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([200,201]);", + "});", + "pm.test(\"Response have body\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "var item = pm.collectionVariables.get(\"item\");", + "var user1 = pm.collectionVariables.get(\"user1\");", + "var user2 = pm.collectionVariables.get(\"user2\");", + "var text = pm.collectionVariables.get(\"commentText\")", + "pm.test(\"Response data equal to request\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('id');", + " pm.expect(jsonData).to.have.property('text');", + " pm.expect(jsonData).to.have.property('authorName');", + " pm.expect(jsonData).to.have.property('created');", + " pm.expect(jsonData.text, `\"text\" must be ` + text).to.eql(text);", + " pm.expect(jsonData.authorName, `\"authorName\" must be ${user2.name}`).to.eql(user2.name);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'seconds').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'seconds').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user1\", user1);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user2\", user2);\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"item\", item);\r", + " book = await api.addBooking(rnd.getBooking(item.id, start, end), user2.id)\r", + " await api.approveBooking(book.id, user1.id)\r", + " pm.collectionVariables.set(\"bookingId\", book.id);\r", + " pm.collectionVariables.set(\"booking\", book);\r", + " pm.collectionVariables.set(\"commentText\", rnd.getWord(50));\r", + " setTimeout(function(){}, 3000);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + }, + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"text\": \"{{commentText}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/items/{{itemId}}/comment", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items", + "{{itemId}}", + "comment" + ] + } + }, + "response": [] + }, + { + "name": "Comment approved booking", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 500])", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'seconds').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'seconds').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user1\", user1);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user2\", user2);\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"item\", item);\r", + " book = await api.addBooking(rnd.getBooking(item.id, start, end), user2.id)\r", + " await api.approveBooking(book.id, user1.id)\r", + " pm.collectionVariables.set(\"bookingId\", book.id);\r", + " pm.collectionVariables.set(\"booking\", book);\r", + " pm.collectionVariables.set(\"commentText\", rnd.getWord(50));\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\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": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + }, + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"text\": \"{{commentText}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/items/{{itemId}}/comment", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items", + "{{itemId}}", + "comment" + ] + } + }, + "response": [] + }, + { + "name": "Get item with comments", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.be.ok;", + "});", + "pm.test(\"Has item create response\", function () {", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "pm.test(\"Test item 'id' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('id');", + "});", + "pm.test(\"Test item 'name' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('name');", + "});", + "pm.test(\"Test item 'description' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('description');", + "});", + "pm.test(\"Test item 'available' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('available');", + "});", + "pm.test(\"Test item 'lastBooking' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('lastBooking');", + " pm.expect(jsonData.lastBooking, '\"lastBooking\" must be \"null\"').null;", + "});", + "pm.test(\"Test item 'nextBooking' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('nextBooking');", + " pm.expect(jsonData.nextBooking, '\"nextBooking\" must be \"null\"').null;", + "});", + "pm.test(\"Test item 'comments' field\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('comments');", + " pm.expect(jsonData.comments.length, 'length of \"comments\" must be \"1\"').to.eql(1);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " var moment = require('moment');\r", + " const currentDate = moment();\r", + " var start = currentDate.clone().add(1, 'seconds').format('YYYY-MM-DDTHH:mm:ss');\r", + " var end = currentDate.clone().add(2, 'seconds').format('YYYY-MM-DDTHH:mm:ss');\r", + " pm.collectionVariables.set('start', start);\r", + " pm.collectionVariables.set('end', end);\r", + " \r", + " user1 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user1\", user1);\r", + " user2 = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set(\"user2\", user2);\r", + " pm.collectionVariables.set(\"userId\", user2.id);\r", + " it = rnd.getItem()\r", + " it.available = true\r", + " item = await api.addItem(it, user1.id)\r", + " pm.collectionVariables.set(\"itemId\", item.id);\r", + " pm.collectionVariables.set(\"item\", item);\r", + " book = await api.addBooking(rnd.getBooking(item.id, start, end), user2.id)\r", + " await api.approveBooking(book.id, user1.id)\r", + " pm.collectionVariables.set(\"bookingId\", book.id);\r", + " pm.collectionVariables.set(\"booking\", book);\r", + " pm.collectionVariables.set(\"commentText\", rnd.getWord(50));\r", + " pausecomp(3000);\r", + " await api.getBooking(book.id, user1.id)\r", + " comment = await api.addComment({\"text\": rnd.getWord(50)}, item.id, user2.id)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "function pausecomp(millis)\r", + " {\r", + " var date = new Date();\r", + " var curDate = null;\r", + " do { curDate = new Date(); }\r", + " while(curDate-date < millis);\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": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "X-Sharer-User-Id", + "value": "{{userId}}", + "type": "text" + }, + { + "key": "Accept", + "value": "*/*", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/items/{{itemId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "items", + "{{itemId}}" + ] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "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, id=0, verbose=null) {\r", + " return this.post(\"/users\", user, id, \"Ошибка при добавлении нового пользователя: \", verbose);\r", + " }\r", + "\r", + " async addItem(item, id=0, verbose=null) {\r", + " return this.post(\"/items\", item, id, \"Ошибка при добавлении новой вещи: \", verbose);\r", + " }\r", + "\r", + " async addBooking(booking, id=0, verbose=null) {\r", + " return this.post(\"/bookings\", booking, id, \"Ошибка при добавлении нового бронирования: \", verbose);\r", + " }\r", + "\r", + " async addComment(comment, itemId, id=0, verbose=null) {\r", + " return this.post(\"/items/\" + itemId + \"/comment\", comment, id, \"Ошибка при добавлении нового комментария: \", verbose);\r", + " }\r", + "\r", + " async getBooking(bookingId, id=0, verbose=null) {\r", + " return this.get(\"/bookings/\"+bookingId, {}, id, \"Ошибка при получении информации о бронировании: \", verbose);\r", + " }\r", + "\r", + " async approveBooking(bookingId, id=0, verbose=null) {\r", + " return this.patch(\"/bookings/\"+bookingId+\"?approved=true\", {}, id, \"Ошибка при подтверждении бронирования: \", verbose);\r", + " }\r", + "\r", + " async addRequest(request, id=0, verbose=null) {\r", + " return this.post(\"/requests\", request, id, \"Ошибка при добавлении нового запроса: \", verbose);\r", + " }\r", + " \r", + " async post(path, body, id=0, errorText = \"Ошибка при выполнении post-запроса: \", verbose=null) {\r", + " return this.sendRequest(\"POST\", path, body, id, errorText, verbose);\r", + " }\r", + "\r", + " async patch(path, body = null, id=0, errorText = \"Ошибка при выполнении patch-запроса: \", verbose=null) {\r", + " return this.sendRequest(\"PATCH\", path, body, id, errorText, verbose);\r", + " }\r", + "\r", + " async get(path, body = null, id=0, errorText = \"Ошибка при выполнении get-запроса: \", verbose=null) {\r", + " return this.sendRequest(\"GET\", path, body, id, errorText, verbose);\r", + " }\r", + "\r", + " async put(path, body = null, id=0, errorText = \"Ошибка при выполнении put-запроса: \", verbose=null) {\r", + " return this.sendRequest(\"PUT\", path, body, id, errorText, verbose);\r", + " }\r", + "\r", + " async delete(path, body = null, id=0, errorText = \"Ошибка при выполнении delte-запроса: \", verbose=null) {\r", + " return this.sendRequest(\"DELETE\", path, body, id, errorText, verbose);\r", + " }\r", + "\r", + " async sendRequest(method, path, body=null, id=0, errorText = \"Ошибка при выполнении запроса: \", verbose=null) {\r", + " return new Promise((resolve, reject) => {\r", + " verbose = verbose == null ? this._verbose : verbose;\r", + " var req = {};\r", + " if (id == 0){\r", + " req = {\r", + " url: this.baseUrl + path,\r", + " method: method,\r", + " body: body == null ? \"\" : JSON.stringify(body),\r", + " header: { \"Content-Type\": \"application/json\"},\r", + " };\r", + " }else{\r", + " req = {\r", + " url: this.baseUrl + path,\r", + " method: method,\r", + " body: body == null ? \"\" : JSON.stringify(body),\r", + " header: [{\r", + " \"key\": \"X-Sharer-User-Id\",\r", + " \"value\": id,\r", + " \"type\": \"text\",\r", + " },\r", + " {\r", + " \"key\": \"Content-Type\",\r", + " \"name\": \"Content-Type\",\r", + " \"value\": \"application/json\",\r", + " \"type\": \"text\"\r", + " }]\r", + " };\r", + " }\r", + " if(verbose) {\r", + " console.log(\"Отправляю запрос: \", req);\r", + " }\r", + "\r", + " try {\r", + " this.pm.sendRequest(req, (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", + " resolve(null);\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", + " getRequest() {\r", + " return {\r", + " description: this.getWord(50)\r", + " };\r", + " }\r", + "\r", + " getBooking(id, startBook, endBook) {\r", + " return {\r", + " itemId: id, \r", + " start: startBook,\r", + " end: endBook \r", + " };\r", + " }\r", + "\r", + " getItem() {\r", + " return {\r", + " name: this.getWord(10),\r", + " description: this.getWord(50),\r", + " available: pm.variables.replaceIn('{{$randomBoolean}}')\t\r", + " };\r", + " }\r", + "\r", + " getItemForRequest(id) {\r", + " return {\r", + " name: this.getWord(10),\r", + " description: this.getWord(50),\r", + " available: pm.variables.replaceIn('{{$randomBoolean}}'),\r", + " requestId: id\r", + " };\r", + " }\r", + "\r", + " getFilm(director=null) {\r", + " let date = new Date(new Date(1960, 0, 1).getTime() + Math.random() * (new Date(2010, 0, 1).getTime() - new Date(1960, 0, 1).getTime()));\r", + " var toReturn = {\r", + " name: this.getWord(15),\r", + " description: this.getWord(50),\r", + " releaseDate: date.toISOString().slice(0,10),\r", + " duration: Math.floor(Math.random() * (180 - 60 + 1) + 60),\r", + " mpa: { id: Math.floor(Math.random() * (5 - 1 + 1) + 1)},\r", + " genres: [{ id: Math.floor(Math.random() * (6 - 1 + 1) + 1)}]\r", + " };\r", + " if (director!==null)\r", + " toReturn.directors = [{ id: director.id}];\r", + " return toReturn;\r", + " }\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", + "\r", + " getName(length = 1) {\r", + " let result = '';\r", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\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", + "\r", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "userName", + "value": "" + }, + { + "key": "userEmail", + "value": "" + }, + { + "key": "userId", + "value": "1" + }, + { + "key": "itemName", + "value": "" + }, + { + "key": "itemAvailable", + "value": "" + }, + { + "key": "itemDescription", + "value": "" + }, + { + "key": "item", + "value": "" + }, + { + "key": "requestId", + "value": "" + }, + { + "key": "requestDescription", + "value": "" + }, + { + "key": "baseUrl", + "value": "localhost:8080" + }, + { + "key": "start", + "value": "" + }, + { + "key": "end", + "value": "" + }, + { + "key": "itemId", + "value": "" + }, + { + "key": "user1", + "value": "" + }, + { + "key": "user2", + "value": "" + }, + { + "key": "bookingId", + "value": "1" + }, + { + "key": "booking", + "value": "" + }, + { + "key": "commentText", + "value": "" + } + ] +} From f27c159bcb983607235cbc24e8f3094d47c210a1 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Thu, 16 Apr 2026 13:08:12 +0300 Subject: [PATCH 19/28] =?UTF-8?q?=D0=95=D1=89=D0=B5=20=D0=BE=D0=B4=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=D0=BF=D1=8B=D1=82=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/api-tests.yml | 192 +-- .github/workflows/build.sh | 8 - .github/workflows/dashboard-template.hbs | 1672 ---------------------- .github/workflows/run.sh | 10 - .github/workflows/wait-for-it.sh | 183 --- README.md | 3 - checkstyle.xml | 2 +- 7 files changed, 3 insertions(+), 2067 deletions(-) delete mode 100644 .github/workflows/build.sh delete mode 100644 .github/workflows/dashboard-template.hbs delete mode 100644 .github/workflows/run.sh delete mode 100644 .github/workflows/wait-for-it.sh diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index c90846c..51c3d8b 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -2,195 +2,7 @@ name: ShareIt API Tests on: pull_request: - workflow_call: jobs: - check-repo: - runs-on: ubuntu-latest - - steps: - - name: Check repo not fork and public - run: | - REPO=$(gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "/repos/${GITHUB_REPOSITORY}") - FORK=$(jq '.fork' <<< "$REPO") - PRIVATE=$(jq '.private' <<< "$REPO") - echo "FORK='$FORK', PRIVATE='$PRIVATE', GITHUB_REPOSITORY_OWNER=${GITHUB_REPOSITORY_OWNER} " - if [[ "$FORK" == "true" ]] - then - echo "Use the repository automatically created by Yandex Practicum (works in fork repositories are not accepted)" - echo "Используйте только репозиторий созданный Yandex Practicum, работы в форк репозитории не принимаются" - exit -1 - fi - if [[ "$GITHUB_REPOSITORY_OWNER" == "yandex-praktikum" ]] - then - echo "Use the repository automatically created by Yandex Practicum (works in fork repositories are not accepted)" - echo "Используйте только репозиторий созданный Yandex Practicum, работы в форк репозитории не принимаются" - exit -2 - fi - if [[ "$PRIVATE" == "true" && "$GITHUB_REPOSITORY_OWNER" != "praktikum-java" ]] - then - echo "Share your repository, make it public" - echo "Откройте доступ к вашему репозиторию, сделайте его публичным" - exit -3 - fi - env: - GH_TOKEN: ${{ github.token }} - - - name: Check branch name - run: | - if [[ "$GITHUB_BASE_REF" != "main" && "$GITHUB_REPOSITORY_OWNER" != "praktikum-java" ]] - then - echo "Set the pull request to merge branch 'main' (instead of '$GITHUB_BASE_REF')" - echo "Задайте в Pull request ветку слияния 'main' (вместо '$GITHUB_BASE_REF')" - exit -2 - fi - echo "Github target '$GITHUB_BASE_REF' - OK" - - if [[ "$GITHUB_HEAD_REF" == "add-controllers" ]] - then - echo "Sprint 14: add-controllers - OK" - exit - fi - if [[ "$GITHUB_HEAD_REF" == "add-bookings" ]] - then - echo "Sprint 15: add-bookings - OK" - PULL=$(gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/${GITHUB_REPOSITORY}/pulls?head=${GITHUB_REPOSITORY_OWNER}:add-controllers || true) - OPEN=$(jq '. | length' <<< "$PULL") - if [[ "$OPEN" != "0" && "$GITHUB_REPOSITORY_OWNER" != "praktikum-java" ]] - then - PULL_URL=$(jq '.[0].html_url' <<< "$PULL") - echo "Merge the add-controllers branch pull request: ${PULL_URL}" - echo "Объедините pull request ветки add-controllers: ${PULL_URL}" - exit -3 - fi - echo "Sprint 14: add-controllers - Merged" - exit - fi - if [[ "$GITHUB_HEAD_REF" == "add-item-requests-and-gateway" ]] - then - echo "Sprint 16: add-item-requests-and-gateway - OK" - PULL=$(gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/${GITHUB_REPOSITORY}/pulls?head=${GITHUB_REPOSITORY_OWNER}:add-bookings || true) - OPEN=$(jq '. | length' <<< "$PULL") - if [[ "$OPEN" != "0" && "$GITHUB_REPOSITORY_OWNER" != "praktikum-java" ]] - then - PULL_URL=$(jq '.[0].html_url' <<< "$PULL") - echo "Merge the add-bookings branch pull request: ${PULL_URL}" - echo "Объедините pull request ветки add-bookings: ${PULL_URL}" - exit -4 - fi - echo "Sprint 15: add-bookings - Merged" - exit - fi - echo "Correct branch name '$GITHUB_HEAD_REF' according to the spec, allowed: add-controllers, add-bookings, add-item-requests-and-gateway" - echo "Исправьте пожалуйста имя ветки '$GITHUB_HEAD_REF' согласно заданию, разрешены: add-controllers, add-bookings, add-item-requests-and-gateway" - exit -1 - env: - GH_TOKEN: ${{ github.token }} - - - name: Check files - run: | - PULL=$(gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/${GITHUB_REPOSITORY}/pulls/${PULL_NUMBER}/files?per_page=100 || true) - FILENAMES=$(jq '.[] | .filename' <<< "$PULL") - if [[ "$FILENAMES" =~ "api-tests.yml" ]] - then - echo "The pull request contains the api-tests.yml file and cannot be modified. Remove it from PR" - echo "Pull request содержит файл api-tests.yml, его изменять нельзя. Удалите его из PR" - exit -1 - fi - if [[ "$FILENAMES" =~ "checkstyle.xml" ]] - then - echo "The pull request contains the checkstyle.xml file and cannot be modified. Remove it from PR" - echo "Pull request содержит файл checkstyle.xml, его изменять нельзя. Удалите его из PR" - exit -2 - fi - if [[ "$FILENAMES" =~ ".class" ]] || [[ "$FILENAMES" =~ ".jar" ]] || [[ "$FILENAMES" =~ "mvn" ]] || [[ "$FILENAMES" =~ ".DS_Store" ]] \ - || [[ "$FILENAMES" =~ ".idea" ]] || [[ "$FILENAMES" =~ ".iws" ]] || [[ "$FILENAMES" =~ ".iml" ]] || [[ "$FILENAMES" =~ ".ipr" ]] \ - || [[ "$FILENAMES" =~ ".db" ]] || [[ "$FILENAMES" =~ ".log" ]] || [[ "$FILENAMES" =~ "target/" ]] - then - echo "The pull request contains the binary files. Remove them (*.class, *.jar, *.DS_Store ...) from PR" - echo "Pull request содержит двоичные файлы. Удалите их (*.class, *.jar, *.DS_Store ...) из PR" - exit -3 - fi - echo "PR files - OK" - exit - env: - PULL_NUMBER: ${{ github.event.number }} - GH_TOKEN: ${{ github.token }} - - build-shareit: - - needs: check-repo - runs-on: ubuntu-latest - - steps: - - name: Checkout target repo - uses: actions/checkout@v4 - - - name: Set up JDK 21 - uses: actions/setup-java@v4 - with: - java-version: '21' - distribution: 'temurin' - cache: maven - - - name: Checkout tests - uses: actions/checkout@v4 - with: - repository: 'yandex-praktikum/java-shareit' - ref: ${{ github.event.pull_request.head.ref }} - path: tests - - - name: Check and Build application - run: | - chmod a+x ./tests/.github/workflows/build.sh - ./tests/.github/workflows/build.sh - - - name: Install Node - uses: actions/setup-node@v4 - with: - node-version: '16.x' - - - name: Install newman - run: | - npm install -g newman - npm install -g newman-reporter-htmlextra - - - name: Run Application - run: | - chmod a+x ./tests/.github/workflows/run.sh - ./tests/.github/workflows/run.sh - - - name: Run POSTMAN tests - run: > - newman run ./tests/postman/sprint.json - --delay-request 50 -r cli,htmlextra - --verbose --color on --reporter-htmlextra-darkTheme - --reporter-htmlextra-export reports/shareIt.html - --reporter-htmlextra-title "Отчет по тестам" - --reporter-htmlextra-logs true - --reporter-htmlextra-template ./tests/.github/workflows/dashboard-template.hbs - - - name: Compose logs - if: ${{ failure() }} - run: | - if test -f "docker-compose.yml"; then - docker compose -f docker-compose.yml ps - docker compose -f docker-compose.yml stop - docker compose -f docker-compose.yml logs -f gateway >> ./gateway.log || true - docker compose -f docker-compose.yml logs -f server >> ./server.log || true - fi - - - - name: Upload log artifact - if: ${{ failure() }} - uses: actions/upload-artifact@v4 - with: - name: shareIt_log - path: ./*.log - - - name: Archive artifacts - uses: actions/upload-artifact@v4 - if: ${{ failure() }} - with: - name: postman_tests_report - path: reports + build: + uses: yandex-praktikum/java-shareit/.github/workflows/api-tests.yml@ci \ No newline at end of file diff --git a/.github/workflows/build.sh b/.github/workflows/build.sh deleted file mode 100644 index 158d8ec..0000000 --- a/.github/workflows/build.sh +++ /dev/null @@ -1,8 +0,0 @@ -cp -rf ./tests/checkstyle.xml ./checkstyle.xml -cp -rf ./tests/suppressions.xml ./suppressions.xml -cp -rf ./tests/gateway/Dockerfile ./gateway/Dockerfile -cp -rf ./tests/server/Dockerfile ./server/Dockerfile -cp -rf ./tests/docker-compose.yml ./docker-compose.yml -mvn enforcer:enforce -Denforcer.rules=requireProfileIdsExist -P check --no-transfer-progress && -mvn verify -P check,coverage --no-transfer-progress && -docker compose -f docker-compose.yml build \ No newline at end of file diff --git a/.github/workflows/dashboard-template.hbs b/.github/workflows/dashboard-template.hbs deleted file mode 100644 index 56aa163..0000000 --- a/.github/workflows/dashboard-template.hbs +++ /dev/null @@ -1,1672 +0,0 @@ - - - - - {{browserTitle}} - - - - - - - - - -
-
- - - -
- {{#with summary}} -
-
- - {{/with}} -
-
-
-
-

{{title}}

-
{{timestamp}}
-{{#with summary}} -
-
-
-
-
- -
-
Итого итераций
-

{{stats.iterations.total}}

-
-
-
-
-
-
-
- -
-
Всего проверенных утверждений
-

{{totalTests stats.assertions.total skippedTests.length}}

-
-
-
-
-
-
-
- -
-
Всего проваленных тестов
-

{{failures.length}}

-
-
-
-
-
-
-
- -
-
Всего пропущено тестов
-

{{#gt skippedTests.length 0}}{{skippedTests.length}}{{else}}0{{/gt}}

-
-
-
-
-
-
-
-
-
-
-
-
File Information
- Коллекция: {{collection.name}}
- {{/with}} - {{#if folders}} Указанные папки: {{folders}}
{{/if}} - {{#with summary}} - {{#if environment.name}} Окружение: {{environment.name}}
{{/if}} -
-
-
-
- {{#if @root.showGlobalData}} - {{#if globals.values.members.length}} -
-
-
-
-
- -
-
-
- -
- - - - {{#each globals.values.members}} - {{#isNotIn key @root.skipGlobalVars}} - - - - - {{/isNotIn}} - {{/each}} - -
Название переменнойЗначение переменной
{{key}}{{value}}
-
-
-
-
-
-
-
- {{/if}} - {{/if}} - {{#if @root.showEnvironmentData}} - {{#if environment.values.members.length}} -
-
-
-
-
- -
-
-
- -
- - - - {{#each environment.values.members}} - {{#isNotIn key @root.skipEnvironmentVars}} - - - - - {{/isNotIn}} - {{/each}} - -
Название переменнойЗначение переменной
{{key}}{{value}}
-
-
-
-
-
-
-
- {{/if}} - {{/if}} - {{#if collection.description}} -
-
-
-
-
Описание Коллекции
-
- {{collection.description}} -
-
-
-
-
- {{/if}} -
-
-
-
-
Временные рамки и данные
- Общая длительность выполнения: {{duration}}
- Всего данных получено: {{responseTotal}}
- Среднее время отклика: {{responseAverage}}
-
-
-
-
- {{/with}} -
-
-
- - - - - - - - - - {{#with summary.stats}} - - - - - - - - - - - - - - - - {{/with}} - {{#with summary}} - - - - - - - - - - - {{/with}} - -
Элемент сводных данныхВсегоПровалено
Requests{{requests.total}}{{requests.failed}}
Prerequest Scripts{{prerequestScripts.total}}{{prerequestScripts.failed}}
Test Scripts{{testScripts.total}}{{testScripts.failed}}
Assertions{{totalTests stats.assertions.total skippedTests.length}}{{stats.assertions.failed}}
Skipped Tests{{#gt skippedTests.length 0}}{{skippedTests.length}}{{else}}0{{/gt}}-
-
-
-
-
-
-
-
-
-
-
- - - {{#if summary.failures.length}} -
- -
-
-
- - {{#with summary}} -
-

Showing {{failures.length}} {{#gt failures.length 1}}Failures{{else}}Failure{{/gt}}

-
- {{/with}} - {{#each summary.failures}} -
-
-
- -
-
-
Failed Test: {{error.test}}
-
-
Assertion Error Message
-
-
{{error.message}}
-
-
-
-
-
-
- {{/each}} - {{else}} -
-

There are no failed tests



-
- {{/if}} -
- -
- - - {{#if summary.skippedTests.length}} -
- -
-
-
- - {{#with summary}} -
-

Showing {{skippedTests.length}} Skipped {{#gt skippedTests.length 1}}Tests{{else}}Test{{/gt}}

-
- {{/with}} - {{#each summary.skippedTests}} -
-
-
- -
-
-
Request Name: {{item.name}}
-
-
-
-
-
- {{/each}} - {{else}} -
-

There are no skipped tests



-
- {{/if}} -
-
- - - -
- {{#if summary.failures.length}} - - {{/if}} - - -
- -
- {{#with summary}} -
{{stats.iterations.total}} {{#gt stats.iterations.total 1}}Iterations{{else}}Iteration{{/gt}} available to view
- {{#gt stats.iterations.total 18}}{{/gt}} - {{/with}} - -
-
-
-{{#each aggregations}} - {{#isNotIn parent.name @root.skipFolders}} - {{#if parent.name }} - -
- {{> aggregations}} -
- {{else}} - {{> aggregations}} - {{/if}} - {{/isNotIn}} -{{/each}} -
-
-
-
-
-
- -{{#*inline "aggregations"}} -{{#isNotIn parent.name @root.skipFolders}} -{{#if @root.showFolderDescription}} -{{#if parent.description.content}} - -
-
-
-
-
-
Описание папки
-
- {{parent.description.content}} -
-
-
-
-
-
-{{/if}} -{{/if}} -{{#each executions}} -{{#isNotIn item.name @root.skipRequests}} -
-
-
-
-
- - {{#if cumulativeTests.skipped}} - {{cumulativeTests.skipped}} Пропущено {{#gt cumulativeTests.skipped 1}}Тестов{{else}}Тест{{/gt}} - {{/if}} -
-
-
- {{#with request}} - {{#if description.content}} -
-
-
-
-
-
Описание запроса
-
- {{description.content}} -
-
-
-
-
-
- {{/if}} - {{/with}} -
-
-
-
-
-
Информация о запросе
- HTTP-метод запроса: {{request.method}}
- URL запроса: {{request.url}}
-
-
-
-
-
Информация об ответе
- Код статуса ответа: {{response.code}} - {{response.status}}
- Среднее время на запрос: {{mean.time}}
- Средний размер одного запроса: {{mean.size}}
-
-
Процент прохождения тестов
-
- {{#if assertions.length}} -
-
-
{{#gte cumulativeTests.passed 1}}{{percent cumulativeTests.passed cumulativeTests.failed}} %{{else}}0 %{{/gte}}
-
-
- {{else}} -
-
-
Для данного запроса нет тестов
-
-
- {{/if}} -
-
-
-
-
-
- {{#with request}} - {{#unless @root.omitHeaders}} - {{#unless @root.skipSensitiveData}} -
-
-
-
-
-
Заголовки запроса
- {{#if headers}} -
- - - - {{#each headers.members}} - {{#isNotIn key @root.skipHeaders}} - - - - - {{/isNotIn}} - {{/each}} - -
Название заголовкаЗначение заголовка
{{key}}{{value}}
-
- {{/if}} -
-
-
-
-
- {{/unless}} - {{/unless}} - {{/with}} - {{#unless @root.skipSensitiveData}} - {{#unless @root.omitRequestBodies}} - {{#isNotIn item.name @root.hideRequestBody}} - {{#with request}} - {{#if body.raw}} -
-
-
-
-
-
Тело запроса
-
-
{{body.raw}}
-
- -
-
-
-
-
- {{/if}} - {{/with}} - {{/isNotIn}} - {{/unless}} - {{/unless}} - {{#unless @root.skipSensitiveData}} - {{#unless @root.omitRequestBodies}} - {{#isNotIn item.name @root.hideRequestBody}} - {{#with request}} - {{#if body.formdata.members}} -
-
-
-
-
-
Тело Запроса
-
-
{{formdata body.formdata.members}}
-
- -
-
-
-
-
- {{/if}} - {{/with}} - {{/isNotIn}} - {{/unless}} - {{/unless}} - {{#unless @root.skipSensitiveData}} - {{#unless @root.omitRequestBodies}} - {{#isNotIn item.name @root.hideRequestBody}} - {{#with request}} - {{#if body.urlencoded.members}} -
-
-
-
-
-
Тело Запроса
-
-
{{formdata body.urlencoded.members}}
-
- -
-
-
-
-
- {{/if}} - {{/with}} - {{/isNotIn}} - {{/unless}} - {{/unless}} - {{#unless @root.skipSensitiveData}} - {{#unless @root.omitRequestBodies}} - {{#isNotIn item.name @root.hideRequestBody}} - {{#with request}} - {{#if body.graphql}} -
-
-
-
-
-
Тело Запроса
-
-
{{body.graphql.query}}
-
- - {{#if body.graphql.variables }} -
Graphql Variables
-
-
{{body.graphql.variables}}
-
- {{/if}} -
-
-
-
-
- {{/if}} - {{/with}} - {{/isNotIn}} - {{/unless}} - {{/unless}} - {{#unless @root.omitHeaders}} - {{#unless @root.skipSensitiveData}} -
-
-
-
-
-
Заголовки Ответа
- {{#if response.header}} -
- - - - {{#each response.header}} - {{#isNotIn key @root.skipHeaders}} - - - - - {{/isNotIn}} - {{/each}} - -
Название заголовкаЗначение заголовка
{{key}}{{value}}
-
- {{/if}} -
-
-
-
-
- {{/unless}} - {{/unless}} - {{#unless @root.skipSensitiveData}} - {{#unless @root.omitResponseBodies}} - {{#isNotIn item.name @root.hideResponseBody}} -
-
-
-
-
-
Тело Ответа
- {{#if response.body}} -
-
{{response.body}}
-
- - {{else}} -
У ответа на этот запрос нет тела
- {{/if}} -
-
-
-
-
- {{/isNotIn}} - {{/unless}} - {{/unless}} - {{#if consoleLogs.length}} -
-
-
-
-
-
Логи консоли
-
-
- - - - {{#each consoleLogs}} - - - - {{/each}} - -
Залогированные сообщения
{{#each messages}}{{this}}{{/each}}
-
-
-
-
-
-
-
- {{/if}} -
-
-
-
Информация о тесте
- {{#if assertions.length}} -
- - - - {{#each assertions}} - - - - - - - {{/each}} - - - - - - - - - -
НазваниеПройденоПроваленоПропущено
{{this.name}}{{this.passed}}{{this.failed}}{{this.skipped}}
Всего{{cumulativeTests.passed}}{{cumulativeTests.failed}}{{cumulativeTests.skipped}}
-
-
-
-
-
-
-
{{#lte cumulativeTests.failed 1}}Проваленный тест{{else}}Проваленные тесты{{/lte}}
-
- - - - {{#each assertions}} - {{#isTheSame testFailure.test this.name}} - - - - - {{/isTheSame}} - {{/each}} - -
Название тестаОшибка проверки
{{testFailure.test}}
{{testFailure.message}}
-
-
-
-
-
-
- {{else}} -
No Tests for this request
- {{/if}} -
-
-
-
-
-
-
-
-
-{{/isNotIn}} -{{/each}} -{{/isNotIn}} -{{/inline}} - - - - - - -{{#eq noSyntaxHighlighting false}} - - -{{/eq}} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.github/workflows/run.sh b/.github/workflows/run.sh deleted file mode 100644 index b02c7e2..0000000 --- a/.github/workflows/run.sh +++ /dev/null @@ -1,10 +0,0 @@ -chmod a+x ./tests/.github/workflows/wait-for-it.sh -docker compose -f docker-compose.yml up --detach && -echo "Docker is up" && -./tests/.github/workflows/wait-for-it.sh -t 60 localhost:9090 && -echo "Server is up" && -./tests/.github/workflows/wait-for-it.sh -t 60 localhost:8080 && -echo "Gateway is up" -result=$? -docker compose -f docker-compose.yml logs -exit $result diff --git a/.github/workflows/wait-for-it.sh b/.github/workflows/wait-for-it.sh deleted file mode 100644 index 859de3f..0000000 --- a/.github/workflows/wait-for-it.sh +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env bash -# Use this script to test if a given TCP host/port are available - -WAITFORIT_cmdname=${0##*/} - -echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } - -usage() -{ - cat << USAGE >&2 -Usage: - $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] - -h HOST | --host=HOST Host or IP under test - -p PORT | --port=PORT TCP port under test - Alternatively, you specify the host and port as host:port - -s | --strict Only execute subcommand if the test succeeds - -q | --quiet Don't output any status messages - -t TIMEOUT | --timeout=TIMEOUT - Timeout in seconds, zero for no timeout - -- COMMAND ARGS Execute command with args after the test finishes -USAGE - exit 1 -} - -wait_for() -{ - if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then - echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" - else - echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" - fi - WAITFORIT_start_ts=$(date +%s) - while : - do - if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then - nc -z $WAITFORIT_HOST $WAITFORIT_PORT - WAITFORIT_result=$? - else -# (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 - (curl --fail --silent $WAITFORIT_HOST:$WAITFORIT_PORT/actuator/health | grep UP) >/dev/null 2>&1 - WAITFORIT_result=$? - fi - if [[ $WAITFORIT_result -eq 0 ]]; then - WAITFORIT_end_ts=$(date +%s) - echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" - break - fi - sleep 1 - done - return $WAITFORIT_result -} - -wait_for_wrapper() -{ - # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 - if [[ $WAITFORIT_QUIET -eq 1 ]]; then - timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & - else - timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & - fi - WAITFORIT_PID=$! - trap "kill -INT -$WAITFORIT_PID" INT - wait $WAITFORIT_PID - WAITFORIT_RESULT=$? - if [[ $WAITFORIT_RESULT -ne 0 ]]; then - echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" - fi - return $WAITFORIT_RESULT -} - -# process arguments -while [[ $# -gt 0 ]] -do - case "$1" in - *:* ) - WAITFORIT_hostport=(${1//:/ }) - WAITFORIT_HOST=${WAITFORIT_hostport[0]} - WAITFORIT_PORT=${WAITFORIT_hostport[1]} - shift 1 - ;; - --child) - WAITFORIT_CHILD=1 - shift 1 - ;; - -q | --quiet) - WAITFORIT_QUIET=1 - shift 1 - ;; - -s | --strict) - WAITFORIT_STRICT=1 - shift 1 - ;; - -h) - WAITFORIT_HOST="$2" - if [[ $WAITFORIT_HOST == "" ]]; then break; fi - shift 2 - ;; - --host=*) - WAITFORIT_HOST="${1#*=}" - shift 1 - ;; - -p) - WAITFORIT_PORT="$2" - if [[ $WAITFORIT_PORT == "" ]]; then break; fi - shift 2 - ;; - --port=*) - WAITFORIT_PORT="${1#*=}" - shift 1 - ;; - -t) - WAITFORIT_TIMEOUT="$2" - if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi - shift 2 - ;; - --timeout=*) - WAITFORIT_TIMEOUT="${1#*=}" - shift 1 - ;; - --) - shift - WAITFORIT_CLI=("$@") - break - ;; - --help) - usage - ;; - *) - echoerr "Unknown argument: $1" - usage - ;; - esac -done - -if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then - echoerr "Error: you need to provide a host and port to test." - usage -fi - -WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} -WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} -WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} -WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} - -# Check to see if timeout is from busybox? -WAITFORIT_TIMEOUT_PATH=$(type -p timeout) -WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) - -WAITFORIT_BUSYTIMEFLAG="" -if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then - WAITFORIT_ISBUSY=1 - # Check if busybox timeout uses -t flag - # (recent Alpine versions don't support -t anymore) - if timeout &>/dev/stdout | grep -q -e '-t '; then - WAITFORIT_BUSYTIMEFLAG="-t" - fi -else - WAITFORIT_ISBUSY=0 -fi - -if [[ $WAITFORIT_CHILD -gt 0 ]]; then - wait_for - WAITFORIT_RESULT=$? - exit $WAITFORIT_RESULT -else - if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then - wait_for_wrapper - WAITFORIT_RESULT=$? - else - wait_for - WAITFORIT_RESULT=$? - fi -fi - -if [[ $WAITFORIT_CLI != "" ]]; then - if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then - echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" - exit $WAITFORIT_RESULT - fi - exec "${WAITFORIT_CLI[@]}" -else - exit $WAITFORIT_RESULT -fi \ No newline at end of file diff --git a/README.md b/README.md index 9fbff3f..5c73228 100644 --- a/README.md +++ b/README.md @@ -1,4 +1 @@ # java-shareit - -### Database map -![SchemaDatabase.png](SchemaDatabase.png) diff --git a/checkstyle.xml b/checkstyle.xml index 05d5086..74f37d8 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -161,7 +161,7 @@ - + From df4b682c37397c2248290b706015b8af6576136c Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Thu, 16 Apr 2026 13:15:37 +0300 Subject: [PATCH 20/28] =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D0=BB=20=D1=87?= =?UTF-8?q?=D0=B5=D0=BA=D1=81=D1=82=D0=B0=D0=B9=D0=BB=20=D0=B8=D0=B7=20?= =?UTF-8?q?=D0=BA=D0=BE=D1=80=D0=BD=D1=8F=20=D0=B8=20=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D0=BB=D0=BE=D0=B6=D0=B8=D0=BB=20=D0=BF=D0=BE=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA=D1=82=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- checkstyle.xml => gateway/checkstyle.xml | 0 suppressions.xml => gateway/suppressions.xml | 0 server/checkstyle.xml | 257 +++++++++++++++++++ server/suppressions.xml | 7 + 4 files changed, 264 insertions(+) rename checkstyle.xml => gateway/checkstyle.xml (100%) rename suppressions.xml => gateway/suppressions.xml (100%) create mode 100644 server/checkstyle.xml create mode 100644 server/suppressions.xml diff --git a/checkstyle.xml b/gateway/checkstyle.xml similarity index 100% rename from checkstyle.xml rename to gateway/checkstyle.xml diff --git a/suppressions.xml b/gateway/suppressions.xml similarity index 100% rename from suppressions.xml rename to gateway/suppressions.xml diff --git a/server/checkstyle.xml b/server/checkstyle.xml new file mode 100644 index 0000000..74f37d8 --- /dev/null +++ b/server/checkstyle.xml @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/suppressions.xml b/server/suppressions.xml new file mode 100644 index 0000000..b2c6822 --- /dev/null +++ b/server/suppressions.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file From 748f6bebb767a4bc393056f7b1cd70f102929cd6 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Thu, 16 Apr 2026 13:26:46 +0300 Subject: [PATCH 21/28] Rerun CI From db6c5b538f447b24e60c604a4c83bd1f836613dc Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Thu, 16 Apr 2026 13:41:56 +0300 Subject: [PATCH 22/28] =?UTF-8?q?=D0=9E=D1=87=D0=B5=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=BF=D0=BF=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D1=87=D0=B5?= =?UTF-8?q?=D0=BA=D1=81=D1=82=D0=B0=D0=B9=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gateway/checkstyle.xml => checkstyle.xml | 0 server/checkstyle.xml | 257 ----------------------- 2 files changed, 257 deletions(-) rename gateway/checkstyle.xml => checkstyle.xml (100%) delete mode 100644 server/checkstyle.xml diff --git a/gateway/checkstyle.xml b/checkstyle.xml similarity index 100% rename from gateway/checkstyle.xml rename to checkstyle.xml diff --git a/server/checkstyle.xml b/server/checkstyle.xml deleted file mode 100644 index 74f37d8..0000000 --- a/server/checkstyle.xml +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 12a5377f228bac0db5de1cd288069dc4b2783008 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Thu, 16 Apr 2026 20:23:50 +0300 Subject: [PATCH 23/28] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B8=20=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=BD=D1=83=D1=82=D1=8C=20=D1=87=D0=B5=D0=BA=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=B9=D0=BB=20=D0=B8=D0=B7=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B2=D0=BE=D0=B3=D0=BE=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8=D1=82?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- checkstyle.xml | 2 +- .../MethodNotImplementedException.java | 7 - .../java/ru/practicum/shareit/TestUtils.java | 13 +- .../repository/BookingRepositoryTest.java | 292 +++++++++++++++--- .../service/ItemRequestServiceImplTest.java | 39 ++- .../HibernateEqualsAndHashCodeTest.java | 216 +++++++++++++ 6 files changed, 512 insertions(+), 57 deletions(-) delete mode 100644 server/src/main/java/ru/practicum/shareit/util/exception/MethodNotImplementedException.java create mode 100644 server/src/test/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCodeTest.java diff --git a/checkstyle.xml b/checkstyle.xml index 74f37d8..c28a0d3 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -161,7 +161,7 @@ - + diff --git a/server/src/main/java/ru/practicum/shareit/util/exception/MethodNotImplementedException.java b/server/src/main/java/ru/practicum/shareit/util/exception/MethodNotImplementedException.java deleted file mode 100644 index f2dbc41..0000000 --- a/server/src/main/java/ru/practicum/shareit/util/exception/MethodNotImplementedException.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.util.exception; - -public class MethodNotImplementedException extends RuntimeException { - public MethodNotImplementedException(String className, String methodName) { - super("Method not implemented " + className + "." + methodName); - } -} diff --git a/server/src/test/java/ru/practicum/shareit/TestUtils.java b/server/src/test/java/ru/practicum/shareit/TestUtils.java index a2c1402..0f39932 100644 --- a/server/src/test/java/ru/practicum/shareit/TestUtils.java +++ b/server/src/test/java/ru/practicum/shareit/TestUtils.java @@ -100,7 +100,6 @@ public Item makeNewItem(long id, User owner, ItemRequest request) { public Item makeNewItem(User owner, ItemRequest request) { return new Item() - .setId(null) .setName(generateRandomText(25, "", ' ', ' ')) .setDescription(generateRandomText(50, "", ' ', ' ', ' ', ' ')) .setAvailable(true) @@ -171,6 +170,18 @@ public Booking makeNewAnyFullFastBooking(int id, LocalDateTime date, BookingStat .setStatus(status); } + public Booking makeNewAnyFullFastBooking(LocalDateTime date, BookingStatus status, ItemRequest request) { + User owner = makeNewUser(); + User booker = makeNewUser(); + Item item = makeNewItem(owner, request); + return new Booking() + .setItem(item) + .setBooker(booker) + .setStart(date) + .setEnd(date.plusHours(10)) + .setStatus(status); + } + public Booking makeNewAnyFullFastBooking(long id, LocalDateTime date, BookingStatus status, ItemRequest request) { User owner = makeNewUser(id + 10); User booker = makeNewUser(id + 11); diff --git a/server/src/test/java/ru/practicum/shareit/booking/repository/BookingRepositoryTest.java b/server/src/test/java/ru/practicum/shareit/booking/repository/BookingRepositoryTest.java index 41931cd..c7dd49e 100644 --- a/server/src/test/java/ru/practicum/shareit/booking/repository/BookingRepositoryTest.java +++ b/server/src/test/java/ru/practicum/shareit/booking/repository/BookingRepositoryTest.java @@ -1,5 +1,8 @@ package ru.practicum.shareit.booking.repository; +import com.sun.istack.NotNull; +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.hamcrest.Matcher; import org.junit.jupiter.api.Test; @@ -9,7 +12,6 @@ import ru.practicum.shareit.booking.model.Booking; import ru.practicum.shareit.booking.model.BookingStatus; import ru.practicum.shareit.item.model.Item; -import ru.practicum.shareit.item.repository.CommentRepository; import ru.practicum.shareit.item.repository.ItemRepository; import ru.practicum.shareit.request.model.ItemRequest; import ru.practicum.shareit.request.repository.ItemRequestRepository; @@ -17,69 +19,79 @@ import ru.practicum.shareit.user.repository.UserRepository; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; import java.util.List; +import java.util.Set; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; @DataJpaTest +@Transactional @RequiredArgsConstructor(onConstructor_ = @Autowired) class BookingRepositoryTest { final TestUtils testUtils = new TestUtils(); + final EntityManager entityManager; + final UserRepository userRepository; final ItemRequestRepository requestRepository; final ItemRepository itemRepository; final BookingRepository bookingRepository; - final CommentRepository commentRepository; - private User addNewUser() { - return userRepository.save(testUtils.makeNewUser()); + private User getCheckedUser(@NotNull User user) { + return user.getId() == null ? + userRepository.save(user) : + entityManager.merge(user); } - private ItemRequest addNewItemRequest(User user) { - return requestRepository.save( - new ItemRequest() - .setRequestor(user) - .setDescription(testUtils.generateRandomText(50, "", ' ', ' ')) - .setCreatedDate(LocalDateTime.now()) + private Item getCheckedItem(@NotNull Item item) { + return item.getId() == null ? + magicSaveItem(item) : + entityManager.merge(item); + } + + private ItemRequest getCheckedRequest(ItemRequest request) { + return request.getId() == null ? + magicSaveRequest(request) : + entityManager.merge(request); + } + + private Booking magicSaveBooking(Booking booking) { + return bookingRepository.save(new Booking() + .setBooker(getCheckedUser(booking.getBooker())) + .setItem(getCheckedItem(booking.getItem())) + .setStart(booking.getStart()) + .setEnd(booking.getEnd()) + .setStatus(booking.getStatus()) ); } - private Item addNewItemWithRequest(User owner, User requestor) { + private Item magicSaveItem(Item item) { return itemRepository.save( new Item() - .setName(testUtils.generateRandomText(10, "", ' ')) - .setDescription(testUtils.generateRandomText(50, "", ' ', ' ')) - .setAvailable(true) - .setOwner(owner) - .setRequest(addNewItemRequest(requestor)) + .setName(item.getName()) + .setDescription(item.getDescription()) + .setAvailable(item.isAvailable()) + .setOwner(getCheckedUser(item.getOwner())) + .setRequest(item.getRequest() == null ? null : getCheckedRequest(item.getRequest())) ); } - private Booking addNewFullBooking(User requestor, User owner, User booker) { - LocalDateTime now = LocalDateTime.now(); - return bookingRepository.save( - new Booking() - .setBooker(booker) - .setItem(addNewItemWithRequest(owner, requestor)) - .setStart(now) - .setEnd(now.plusHours(1)) - .setStatus(BookingStatus.APPROVED) + private ItemRequest magicSaveRequest(ItemRequest request) { + return requestRepository.save( + new ItemRequest() + .setRequestor(getCheckedUser(request.getRequestor())) + .setDescription(request.getDescription()) + .setCreatedDate(request.getCreatedDate()) ); } - private Booking addNewBooking(Item item, User booker, BookingStatus status) { - LocalDateTime now = LocalDateTime.now(); - return bookingRepository.save( - new Booking() - .setBooker(booker) - .setItem(item) - .setStart(now) - .setEnd(now.plusHours(1)) - .setStatus(status) - ); + Booking saveNewMagicBooking(LocalDateTime date, BookingStatus status) { + return magicSaveBooking( + testUtils.makeNewAnyFullFastBooking(date, status, null)); } private Matcher matchesBooking(Booking booking) { @@ -95,69 +107,263 @@ private Matcher matchesBooking(Booking booking) { @Test void findAllByOwner() { - User requestor = addNewUser(); - User owner = addNewUser(); - User booker = addNewUser(); - - Booking booking1 = addNewFullBooking(requestor, owner, requestor); - Item item = booking1.getItem(); - Booking booking2 = addNewBooking(item, booker, BookingStatus.WAITING); + // region setup + List bookings = List.of( + saveNewMagicBooking(LocalDateTime.now(), BookingStatus.WAITING), + saveNewMagicBooking(LocalDateTime.now(), BookingStatus.WAITING), + saveNewMagicBooking(LocalDateTime.now(), BookingStatus.WAITING) + ); + User owner = bookings.get(0).getItem().getOwner(); + entityManager.merge(bookings.get(1).getItem().setOwner(owner)); + // endregion setup List bookingList = bookingRepository.findAllByOwner(owner.getId()); assertThat(bookingList, hasSize(2)); - assertThat(bookingList, contains(matchesBooking(booking2), matchesBooking(booking1))); + assertThat(bookingList, contains(matchesBooking(bookings.get(1)), matchesBooking(bookings.get(0)))); } @Test void findCurrentByOwner() { + // region setup + List bookings = List.of( + saveNewMagicBooking(LocalDateTime.now().plusDays(1), BookingStatus.WAITING), // future + saveNewMagicBooking(LocalDateTime.now().minusHours(5), BookingStatus.WAITING), // current + saveNewMagicBooking(LocalDateTime.now().minusHours(5), BookingStatus.WAITING) // current + ); + User owner = bookings.get(0).getItem().getOwner(); + entityManager.merge(bookings.get(1).getItem().setOwner(owner)); + // endregion setup + + List bookingList = bookingRepository.findCurrentByOwner(owner.getId()); + + assertThat(bookingList, hasSize(1)); + assertThat(bookingList, contains(matchesBooking(bookings.get(1)))); } @Test void findPastByOwner() { + // region setup + List bookings = List.of( + saveNewMagicBooking(LocalDateTime.now().plusDays(1), BookingStatus.WAITING), // future + saveNewMagicBooking(LocalDateTime.now().minusDays(5), BookingStatus.WAITING), // past + saveNewMagicBooking(LocalDateTime.now().minusDays(5), BookingStatus.WAITING) // past + ); + User owner = bookings.get(0).getItem().getOwner(); + entityManager.merge(bookings.get(1).getItem().setOwner(owner)); + // endregion setup + + List bookingList = bookingRepository.findPastByOwner(owner.getId()); + + assertThat(bookingList, hasSize(1)); + assertThat(bookingList, contains(matchesBooking(bookings.get(1)))); } @Test void findFutureByOwner() { + // region setup + List bookings = List.of( + saveNewMagicBooking(LocalDateTime.now().plusDays(1), BookingStatus.WAITING), // future + saveNewMagicBooking(LocalDateTime.now().minusDays(5), BookingStatus.WAITING), // past + saveNewMagicBooking(LocalDateTime.now().minusDays(5), BookingStatus.WAITING) // past + ); + User owner = bookings.get(0).getItem().getOwner(); + entityManager.merge(bookings.get(1).getItem().setOwner(owner)); + // endregion setup + + List bookingList = bookingRepository.findFutureByOwner(owner.getId()); + + assertThat(bookingList, hasSize(1)); + assertThat(bookingList, contains(matchesBooking(bookings.getFirst()))); } @Test void findAllByOwnerAndStatusOrderByStartDesc() { + // region setup + List bookings = List.of( + saveNewMagicBooking(LocalDateTime.now().plusDays(1), BookingStatus.WAITING), // future + saveNewMagicBooking(LocalDateTime.now().minusDays(5), BookingStatus.WAITING), // past + saveNewMagicBooking(LocalDateTime.now().minusDays(5), BookingStatus.REJECTED) // past + ); + User owner = bookings.get(0).getItem().getOwner(); + entityManager.merge(bookings.get(1).getItem().setOwner(owner)); + // endregion setup + + List bookingList = bookingRepository + .findAllByOwnerAndStatusOrderByStartDesc(owner.getId(), BookingStatus.WAITING); + + assertThat(bookingList, hasSize(2)); + assertThat(bookingList, contains(matchesBooking(bookings.get(0)), matchesBooking(bookings.get(1)))); } @Test void findCurrentByBooker() { + // region setup + List bookings = List.of( + saveNewMagicBooking(LocalDateTime.now().plusDays(1), BookingStatus.WAITING), // future + saveNewMagicBooking(LocalDateTime.now().minusHours(5), BookingStatus.WAITING), // current + saveNewMagicBooking(LocalDateTime.now().minusHours(5), BookingStatus.WAITING) // current + ); + User booker = bookings.get(0).getBooker(); + entityManager.merge(bookings.get(1).setBooker(booker)); + // endregion setup + + List bookingList = bookingRepository.findCurrentByBooker(booker.getId()); + + assertThat(bookingList, hasSize(1)); + assertThat(bookingList, contains(matchesBooking(bookings.get(1)))); } @Test void findPastByBooker() { + // region setup + List bookings = List.of( + saveNewMagicBooking(LocalDateTime.now().plusDays(1), BookingStatus.WAITING), // future + saveNewMagicBooking(LocalDateTime.now().minusDays(5), BookingStatus.WAITING), // past + saveNewMagicBooking(LocalDateTime.now().minusDays(5), BookingStatus.WAITING) // past + ); + User booker = bookings.get(0).getBooker(); + entityManager.merge(bookings.get(1).setBooker(booker)); + // endregion setup + + List bookingList = bookingRepository.findPastByBooker(booker.getId()); + + assertThat(bookingList, hasSize(1)); + assertThat(bookingList, contains(matchesBooking(bookings.get(1)))); } @Test void findFutureByBooker() { + // region setup + List bookings = List.of( + saveNewMagicBooking(LocalDateTime.now().plusDays(1), BookingStatus.WAITING), // future + saveNewMagicBooking(LocalDateTime.now().minusDays(5), BookingStatus.WAITING), // past + saveNewMagicBooking(LocalDateTime.now().minusDays(5), BookingStatus.WAITING) // past + ); + User booker = bookings.get(0).getBooker(); + entityManager.merge(bookings.get(1).setBooker(booker)); + // endregion setup + + List bookingList = bookingRepository.findFutureByBooker(booker.getId()); + + assertThat(bookingList, hasSize(1)); + assertThat(bookingList, contains(matchesBooking(bookings.getFirst()))); } @Test void findLastBookingDate() { + // region setup + List bookings = List.of( + saveNewMagicBooking(LocalDateTime.now().plusDays(1), BookingStatus.WAITING), // future + saveNewMagicBooking(LocalDateTime.now().minusDays(5), BookingStatus.WAITING), // past + saveNewMagicBooking(LocalDateTime.now().minusDays(6), BookingStatus.WAITING) // past + ); + Item item = bookings.get(0).getItem(); + entityManager.merge(bookings.get(1).setItem(item)); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); + // endregion setup + LocalDateTime date = bookingRepository.findLastBookingDate(item.getId()); + + assertThat(date.truncatedTo(ChronoUnit.SECONDS).toString(), + equalTo(bookings.get(1).getStart().truncatedTo(ChronoUnit.SECONDS).format(formatter)) + ); } @Test void findNextBookingDate() { + // region setup + List bookings = List.of( + saveNewMagicBooking(LocalDateTime.now().minusDays(1), BookingStatus.WAITING), // past + saveNewMagicBooking(LocalDateTime.now().plusDays(5), BookingStatus.WAITING), // future + saveNewMagicBooking(LocalDateTime.now().plusDays(4), BookingStatus.WAITING) // future + ); + Item item = bookings.get(0).getItem(); + entityManager.merge(bookings.get(1).setItem(item)); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); + // endregion setup + LocalDateTime date = bookingRepository.findNextBookingDate(item.getId()); + + assertThat(date.truncatedTo(ChronoUnit.SECONDS).toString(), + equalTo(bookings.get(1).getStart().truncatedTo(ChronoUnit.SECONDS).format(formatter)) + ); } @Test void findAllByBookerIdOrderByStartDesc() { + // region setup + List bookings = List.of( + saveNewMagicBooking(LocalDateTime.now().minusDays(1), BookingStatus.WAITING), // past + saveNewMagicBooking(LocalDateTime.now().plusDays(5), BookingStatus.WAITING), // future + saveNewMagicBooking(LocalDateTime.now().plusDays(4), BookingStatus.WAITING) // future + ); + User booker = bookings.get(0).getBooker(); + entityManager.merge(bookings.get(1).setBooker(booker)); + // endregion setup + + List bookingList = bookingRepository.findAllByBookerIdOrderByStartDesc(booker.getId()); + + assertThat(bookingList, hasSize(2)); + assertThat(bookingList, contains(matchesBooking(bookings.get(1)), matchesBooking(bookings.get(0)))); } @Test void findAllByBooker_IdAndStatusOrderByStartDesc() { + // region setup + List bookings = List.of( + saveNewMagicBooking(LocalDateTime.now().plusDays(1), BookingStatus.WAITING), // future + saveNewMagicBooking(LocalDateTime.now().minusDays(5), BookingStatus.WAITING), // past + saveNewMagicBooking(LocalDateTime.now().minusDays(5), BookingStatus.REJECTED) // past + ); + User booker = bookings.get(0).getBooker(); + entityManager.merge(bookings.get(1).setBooker(booker)); + // endregion setup + + List bookingList = bookingRepository + .findAllByBooker_IdAndStatusOrderByStartDesc(booker.getId(), BookingStatus.WAITING); + + assertThat(bookingList, hasSize(2)); + assertThat(bookingList, contains(matchesBooking(bookings.get(0)), matchesBooking(bookings.get(1)))); } @Test void findAllBookingByItem_Id() { + // region setup + List bookings = List.of( + saveNewMagicBooking(LocalDateTime.now().plusDays(1), BookingStatus.WAITING), // future + saveNewMagicBooking(LocalDateTime.now().minusDays(5), BookingStatus.WAITING), // past + saveNewMagicBooking(LocalDateTime.now().minusDays(6), BookingStatus.WAITING) // past + ); + Item item = bookings.get(0).getItem(); + entityManager.merge(bookings.get(1).setItem(item)); + // endregion setup + + List bookingList = bookingRepository.findAllBookingByItem_Id(item.getId()); + + assertThat(bookingList, hasSize(2)); + assertThat(bookingList, contains(matchesBooking(bookings.get(0)), matchesBooking(bookings.get(1)))); } @Test void findAllBookingByItemIdIn() { + // region setup + List bookings = List.of( + saveNewMagicBooking(LocalDateTime.now().plusDays(1), BookingStatus.WAITING), // future + saveNewMagicBooking(LocalDateTime.now().minusDays(5), BookingStatus.WAITING), // past + saveNewMagicBooking(LocalDateTime.now().minusDays(6), BookingStatus.WAITING), // past + saveNewMagicBooking(LocalDateTime.now().minusDays(6), BookingStatus.WAITING) // past + ); + Item item1 = bookings.get(0).getItem(); + entityManager.merge(bookings.get(1).setItem(item1)); + Item item2 = bookings.get(2).getItem(); + // endregion setup + + List bookingList = bookingRepository.findAllBookingByItemIdIn(Set.of(item1.getId(), item2.getId())); + + assertThat(bookingList, hasSize(3)); + assertThat(bookingList, contains( + matchesBooking(bookings.get(0)), + matchesBooking(bookings.get(1)), + matchesBooking(bookings.get(2)) + )); } } \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/request/service/ItemRequestServiceImplTest.java b/server/src/test/java/ru/practicum/shareit/request/service/ItemRequestServiceImplTest.java index 43f167d..25e3244 100644 --- a/server/src/test/java/ru/practicum/shareit/request/service/ItemRequestServiceImplTest.java +++ b/server/src/test/java/ru/practicum/shareit/request/service/ItemRequestServiceImplTest.java @@ -18,9 +18,7 @@ import ru.practicum.shareit.user.model.User; import ru.practicum.shareit.util.UtilService; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -48,7 +46,7 @@ class ItemRequestServiceImplTest { class AddNewRequest { @Test - void addNewRequest() { + void whenCreatedDateIsNotNull() { // region setup ItemRequest request = testUtils.makeNewItemRequest(1, testUtils.makeNewUser(10)); User requestor = request.getRequestor(); @@ -66,6 +64,26 @@ void addNewRequest() { assertThat(resultDto, equalTo(dtoOut)); } + + @Test + void whenCreatedDateIsNull() { + // region setup + ItemRequest request = testUtils.makeNewItemRequest(1, testUtils.makeNewUser(10)); + User requestor = request.getRequestor(); + ItemRequestDto dtoIn = ItemRequestDto.builder() + .description(request.getDescription()) + .createdDate(null) + .build(); + ItemRequestDto dtoOut = ItemRequestMapper.toDto(request); + // endregion setup + + when(utilService.getUser(requestor.getId())).thenReturn(requestor); + when(itemRequestRepository.save(testUtils.makeCopyOfRequest(request).setId(null))).thenReturn(request); + + ItemRequestDto resultDto = requestService.addNewRequest(requestor.getId(), dtoIn); + + assertThat(resultDto, equalTo(dtoOut)); + } } @Nested @@ -111,6 +129,18 @@ void getRequestListByUser() { assertThat(resultDtos, equalTo(bigDtos)); } + + @Test + void getRequestListByUser_thenReturnEmptyList() { + User requestor = testUtils.makeNewUser(5); + + doNothing().when(utilService).checkUser(requestor.getId()); + when(itemRequestRepository.findAllByRequestor_Id(requestor.getId())).thenReturn(Collections.emptyList()); + + List resultDtos = requestService.getRequestListByUser(requestor.getId()); + + assertThat(resultDtos, equalTo(Collections.emptyList())); + } } @Nested @@ -144,7 +174,6 @@ class GetRequestById { void getRequestById() { // region setup ItemRequest request = testUtils.makeNewItemRequest(1, testUtils.makeNewUser(10)); - User requestor = request.getRequestor(); List items = List.of( testUtils.makeNewFastItem(40), testUtils.makeNewFastItem(41), diff --git a/server/src/test/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCodeTest.java b/server/src/test/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCodeTest.java new file mode 100644 index 0000000..289be06 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCodeTest.java @@ -0,0 +1,216 @@ +package ru.practicum.shareit.util.hibernate; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import ru.practicum.shareit.TestUtils; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.item.model.Comment; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Transactional +@SpringBootTest +public class HibernateEqualsAndHashCodeTest { + + @PersistenceContext + private EntityManager em; + + final TestUtils testUtils = new TestUtils(); + + @Test + void shouldHandleHibernateProxyCorrectly_byUser() { + User user = new User() + .setName("John") + .setEmail("john@test.com"); + + em.persist(user); + em.flush(); + em.clear(); + + User proxy = em.getReference(User.class, user.getId()); + Class clazz = HibernateEqualsAndHashCode.persistentClass(proxy); + + assertEquals(User.class, clazz); + } + + @Test + void shouldBeEqualForProxyAndEntityWithSameId_byUser() { + User user = new User() + .setName("John") + .setEmail("john@test.com"); + + em.persist(user); + em.flush(); + em.clear(); + + User proxy = em.getReference(User.class, user.getId()); + User entity = em.find(User.class, user.getId()); + + assertEquals(proxy, entity); + assertEquals(entity, proxy); + assertEquals(entity.hashCode(), proxy.hashCode()); + } + + @Test + void shouldHandleHibernateProxyCorrectly_byRequest() { + ItemRequest request = new ItemRequest() + .setDescription("John") + .setCreatedDate(LocalDateTime.now()) + .setRequestor(testUtils.makeNewUser(1)); + + em.persist(request); + em.flush(); + em.clear(); + + ItemRequest proxy = em.getReference(ItemRequest.class, request.getId()); + Class clazz = HibernateEqualsAndHashCode.persistentClass(proxy); + + assertEquals(ItemRequest.class, clazz); + } + + @Test + void shouldBeEqualForProxyAndEntityWithSameId_byRequest() { + ItemRequest request = new ItemRequest() + .setDescription("John") + .setCreatedDate(LocalDateTime.now()) + .setRequestor(testUtils.makeNewUser(1)); + + em.persist(request); + em.flush(); + em.clear(); + + ItemRequest proxy = em.getReference(ItemRequest.class, request.getId()); + ItemRequest entity = em.find(ItemRequest.class, request.getId()); + + assertEquals(proxy, entity); + assertEquals(entity, proxy); + assertEquals(entity.hashCode(), proxy.hashCode()); + } + + @Test + void shouldHandleHibernateProxyCorrectly_byComment() { + Comment comment = new Comment() + .setText("John") + .setCreatedDate(LocalDateTime.now()) + .setAuthor(testUtils.makeNewUser(1)) + .setItem(testUtils.makeNewFastItem(10)); + + em.persist(comment); + em.flush(); + em.clear(); + + Comment proxy = em.getReference(Comment.class, comment.getId()); + Class clazz = HibernateEqualsAndHashCode.persistentClass(proxy); + + assertEquals(Comment.class, clazz); + } + + @Test + void shouldBeEqualForProxyAndEntityWithSameId_byComment() { + Comment comment = new Comment() + .setText("John") + .setCreatedDate(LocalDateTime.now()) + .setAuthor(testUtils.makeNewUser(1)) + .setItem(testUtils.makeNewFastItem(10)); + + em.persist(comment); + em.flush(); + em.clear(); + + Comment proxy = em.getReference(Comment.class, comment.getId()); + Comment entity = em.find(Comment.class, comment.getId()); + + assertEquals(proxy, entity); + assertEquals(entity, proxy); + assertEquals(entity.hashCode(), proxy.hashCode()); + } + + @Test + void shouldHandleHibernateProxyCorrectly_byBooking() { + Booking booking = new Booking() + .setBooker(testUtils.makeNewUser(1)) + .setItem(testUtils.makeNewFastItem(10)) + .setStart(LocalDateTime.now()) + .setEnd(LocalDateTime.now().plusDays(1)) + .setStatus(BookingStatus.WAITING); + + em.persist(booking); + em.flush(); + em.clear(); + + Booking proxy = em.getReference(Booking.class, booking.getId()); + Class clazz = HibernateEqualsAndHashCode.persistentClass(proxy); + + assertEquals(Booking.class, clazz); + } + + @Test + void shouldBeEqualForProxyAndEntityWithSameId_byBooking() { + Booking booking = new Booking() + .setBooker(testUtils.makeNewUser(1)) + .setItem(testUtils.makeNewFastItem(10)) + .setStart(LocalDateTime.now()) + .setEnd(LocalDateTime.now().plusDays(1)) + .setStatus(BookingStatus.WAITING); + + em.persist(booking); + em.flush(); + em.clear(); + + Booking proxy = em.getReference(Booking.class, booking.getId()); + Booking entity = em.find(Booking.class, booking.getId()); + + assertEquals(proxy, entity); + assertEquals(entity, proxy); + assertEquals(entity.hashCode(), proxy.hashCode()); + } + + @Test + void shouldHandleHibernateProxyCorrectly_byItem() { + Item item = new Item() + .setDescription("John") + .setName("name") + .setAvailable(true) + .setOwner(testUtils.makeNewUser(1)) + .setRequest(testUtils.makeNewItemRequest(10, testUtils.makeNewUser(2))); + + em.persist(item); + em.flush(); + em.clear(); + + Item proxy = em.getReference(Item.class, item.getId()); + Class clazz = HibernateEqualsAndHashCode.persistentClass(proxy); + + assertEquals(Item.class, clazz); + } + + @Test + void shouldBeEqualForProxyAndEntityWithSameId_byItem() { + Item item = new Item() + .setDescription("John") + .setName("name") + .setAvailable(true) + .setOwner(testUtils.makeNewUser(1)) + .setRequest(testUtils.makeNewItemRequest(10, testUtils.makeNewUser(2))); + + em.persist(item); + em.flush(); + em.clear(); + + Item proxy = em.getReference(Item.class, item.getId()); + Item entity = em.find(Item.class, item.getId()); + + assertEquals(proxy, entity); + assertEquals(entity, proxy); + assertEquals(entity.hashCode(), proxy.hashCode()); + } +} From a74689206ec5cc0b702035ddbcca556a6dab8c54 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Thu, 16 Apr 2026 21:35:08 +0300 Subject: [PATCH 24/28] =?UTF-8?q?=D0=9F=D0=BE=D0=BF=D1=8B=D1=82=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20Caused=20?= =?UTF-8?q?by:=20java.net.ConnectException:=20Connection=20refused?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application-test.yml | 4 ++ .../HibernateEqualsAndHashCodeTest.java | 53 ++++++++++++------- 2 files changed, 39 insertions(+), 18 deletions(-) create mode 100644 server/src/main/resources/application-test.yml diff --git a/server/src/main/resources/application-test.yml b/server/src/main/resources/application-test.yml new file mode 100644 index 0000000..5035758 --- /dev/null +++ b/server/src/main/resources/application-test.yml @@ -0,0 +1,4 @@ +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:shareit \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCodeTest.java b/server/src/test/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCodeTest.java index 289be06..587e539 100644 --- a/server/src/test/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCodeTest.java +++ b/server/src/test/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCodeTest.java @@ -1,10 +1,11 @@ package ru.practicum.shareit.util.hibernate; import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import jakarta.transaction.Transactional; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + import ru.practicum.shareit.TestUtils; import ru.practicum.shareit.booking.model.Booking; import ru.practicum.shareit.booking.model.BookingStatus; @@ -18,10 +19,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @Transactional -@SpringBootTest +@DataJpaTest public class HibernateEqualsAndHashCodeTest { - @PersistenceContext + @Autowired private EntityManager em; final TestUtils testUtils = new TestUtils(); @@ -62,10 +63,11 @@ void shouldBeEqualForProxyAndEntityWithSameId_byUser() { @Test void shouldHandleHibernateProxyCorrectly_byRequest() { + User requestor = em.merge(testUtils.makeNewUser()); ItemRequest request = new ItemRequest() .setDescription("John") .setCreatedDate(LocalDateTime.now()) - .setRequestor(testUtils.makeNewUser(1)); + .setRequestor(requestor); em.persist(request); em.flush(); @@ -79,10 +81,11 @@ void shouldHandleHibernateProxyCorrectly_byRequest() { @Test void shouldBeEqualForProxyAndEntityWithSameId_byRequest() { + User requestor = em.merge(testUtils.makeNewUser()); ItemRequest request = new ItemRequest() .setDescription("John") .setCreatedDate(LocalDateTime.now()) - .setRequestor(testUtils.makeNewUser(1)); + .setRequestor(requestor); em.persist(request); em.flush(); @@ -98,11 +101,14 @@ void shouldBeEqualForProxyAndEntityWithSameId_byRequest() { @Test void shouldHandleHibernateProxyCorrectly_byComment() { + User owner = em.merge(testUtils.makeNewUser()); + Item item = em.merge(testUtils.makeNewItem(owner, null)); + User commentor = em.merge(testUtils.makeNewUser()); Comment comment = new Comment() .setText("John") .setCreatedDate(LocalDateTime.now()) - .setAuthor(testUtils.makeNewUser(1)) - .setItem(testUtils.makeNewFastItem(10)); + .setAuthor(commentor) + .setItem(item); em.persist(comment); em.flush(); @@ -116,11 +122,14 @@ void shouldHandleHibernateProxyCorrectly_byComment() { @Test void shouldBeEqualForProxyAndEntityWithSameId_byComment() { + User owner = em.merge(testUtils.makeNewUser()); + Item item = em.merge(testUtils.makeNewItem(owner, null)); + User commentor = em.merge(testUtils.makeNewUser()); Comment comment = new Comment() .setText("John") .setCreatedDate(LocalDateTime.now()) - .setAuthor(testUtils.makeNewUser(1)) - .setItem(testUtils.makeNewFastItem(10)); + .setAuthor(commentor) + .setItem(item); em.persist(comment); em.flush(); @@ -136,9 +145,12 @@ void shouldBeEqualForProxyAndEntityWithSameId_byComment() { @Test void shouldHandleHibernateProxyCorrectly_byBooking() { + User booker = em.merge(testUtils.makeNewUser()); + User owner = em.merge(testUtils.makeNewUser()); + Item item = em.merge(testUtils.makeNewItem(owner, null)); Booking booking = new Booking() - .setBooker(testUtils.makeNewUser(1)) - .setItem(testUtils.makeNewFastItem(10)) + .setBooker(booker) + .setItem(item) .setStart(LocalDateTime.now()) .setEnd(LocalDateTime.now().plusDays(1)) .setStatus(BookingStatus.WAITING); @@ -155,9 +167,12 @@ void shouldHandleHibernateProxyCorrectly_byBooking() { @Test void shouldBeEqualForProxyAndEntityWithSameId_byBooking() { + User booker = em.merge(testUtils.makeNewUser()); + User owner = em.merge(testUtils.makeNewUser()); + Item item = em.merge(testUtils.makeNewItem(owner, null)); Booking booking = new Booking() - .setBooker(testUtils.makeNewUser(1)) - .setItem(testUtils.makeNewFastItem(10)) + .setBooker(booker) + .setItem(item) .setStart(LocalDateTime.now()) .setEnd(LocalDateTime.now().plusDays(1)) .setStatus(BookingStatus.WAITING); @@ -176,12 +191,13 @@ void shouldBeEqualForProxyAndEntityWithSameId_byBooking() { @Test void shouldHandleHibernateProxyCorrectly_byItem() { + User owner = em.merge(testUtils.makeNewUser()); Item item = new Item() .setDescription("John") .setName("name") .setAvailable(true) - .setOwner(testUtils.makeNewUser(1)) - .setRequest(testUtils.makeNewItemRequest(10, testUtils.makeNewUser(2))); + .setOwner(owner) + .setRequest(null); em.persist(item); em.flush(); @@ -195,12 +211,13 @@ void shouldHandleHibernateProxyCorrectly_byItem() { @Test void shouldBeEqualForProxyAndEntityWithSameId_byItem() { + User owner = em.merge(testUtils.makeNewUser()); Item item = new Item() .setDescription("John") .setName("name") .setAvailable(true) - .setOwner(testUtils.makeNewUser(1)) - .setRequest(testUtils.makeNewItemRequest(10, testUtils.makeNewUser(2))); + .setOwner(owner) + .setRequest(null); em.persist(item); em.flush(); From 855ad80fac559621c14a5ee32bb436f8fe3998e2 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Thu, 16 Apr 2026 21:58:34 +0300 Subject: [PATCH 25/28] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20@Transactional=20=D0=B2=20DataJpa=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shareit/request/repository/QueryDslRepository.java | 2 +- .../shareit/request/service/ItemRequestServiceImpl.java | 2 +- .../shareit/request/repository/QueryDslRepositoryTest.java | 7 ++++--- .../config => request/repository}/QuerydslTestConfig.java | 2 +- .../request/service/ItemRequestServiceImplTest.java | 2 +- .../shareit/user/repository/UserRepositoryTest.java | 2 ++ 6 files changed, 10 insertions(+), 7 deletions(-) rename server/src/test/java/ru/practicum/shareit/{util/config => request/repository}/QuerydslTestConfig.java (88%) diff --git a/server/src/main/java/ru/practicum/shareit/request/repository/QueryDslRepository.java b/server/src/main/java/ru/practicum/shareit/request/repository/QueryDslRepository.java index ecf2075..c9fb7e7 100644 --- a/server/src/main/java/ru/practicum/shareit/request/repository/QueryDslRepository.java +++ b/server/src/main/java/ru/practicum/shareit/request/repository/QueryDslRepository.java @@ -30,7 +30,7 @@ public Map> getMapOfItemsByRequestId(List requestIds) { .collect(Collectors.groupingBy(item -> item.getRequest().getId())); } - public List getAllRequestsWitoutByUser(Long userId) { + public List getAllRequestsWithoutThisUserRequests(Long userId) { return queryFactory.selectFrom(qItemRequest) .where(qItemRequest.requestor.id.notIn(userId)) .orderBy(qItemRequest.createdDate.desc()) diff --git a/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java index a940c70..644db68 100644 --- a/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java +++ b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java @@ -60,7 +60,7 @@ public List getRequestListByUser(long userId) { @Override public List getAllRequests(long userId) { utilService.checkUser(userId); - return queryDslRepository.getAllRequestsWitoutByUser(userId).stream() + return queryDslRepository.getAllRequestsWithoutThisUserRequests(userId).stream() .map(ItemRequestMapper::toDto) .toList(); } diff --git a/server/src/test/java/ru/practicum/shareit/request/repository/QueryDslRepositoryTest.java b/server/src/test/java/ru/practicum/shareit/request/repository/QueryDslRepositoryTest.java index c5564d1..d4593ef 100644 --- a/server/src/test/java/ru/practicum/shareit/request/repository/QueryDslRepositoryTest.java +++ b/server/src/test/java/ru/practicum/shareit/request/repository/QueryDslRepositoryTest.java @@ -2,6 +2,7 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; +import jakarta.transaction.Transactional; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; @@ -10,7 +11,6 @@ import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.request.model.ItemRequest; import ru.practicum.shareit.user.model.User; -import ru.practicum.shareit.util.config.QuerydslTestConfig; import java.time.LocalDateTime; import java.util.ArrayList; @@ -22,6 +22,7 @@ import static org.hamcrest.Matchers.is; @DataJpaTest +@Transactional @Import({QuerydslTestConfig.class, QueryDslRepository.class}) public class QueryDslRepositoryTest { @@ -92,7 +93,7 @@ void getMapOfItemsByRequestId() { } @Test - void getAllRequestsWitoutByUser() { + void getAllRequestsWithoutByUser() { List users = List.of( addNewUser(), addNewUser(), @@ -105,7 +106,7 @@ void getAllRequestsWitoutByUser() { addNewItemRequest(users.get(2)) ); - List result = queryDslRepository.getAllRequestsWitoutByUser(users.getFirst().getId()); + List result = queryDslRepository.getAllRequestsWithoutThisUserRequests(users.getFirst().getId()); assertThat(result, is(List.of(itemRequests.get(2), itemRequests.get(1)))); } diff --git a/server/src/test/java/ru/practicum/shareit/util/config/QuerydslTestConfig.java b/server/src/test/java/ru/practicum/shareit/request/repository/QuerydslTestConfig.java similarity index 88% rename from server/src/test/java/ru/practicum/shareit/util/config/QuerydslTestConfig.java rename to server/src/test/java/ru/practicum/shareit/request/repository/QuerydslTestConfig.java index de2f169..a02e83d 100644 --- a/server/src/test/java/ru/practicum/shareit/util/config/QuerydslTestConfig.java +++ b/server/src/test/java/ru/practicum/shareit/request/repository/QuerydslTestConfig.java @@ -1,4 +1,4 @@ -package ru.practicum.shareit.util.config; +package ru.practicum.shareit.request.repository; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; diff --git a/server/src/test/java/ru/practicum/shareit/request/service/ItemRequestServiceImplTest.java b/server/src/test/java/ru/practicum/shareit/request/service/ItemRequestServiceImplTest.java index 25e3244..b037d46 100644 --- a/server/src/test/java/ru/practicum/shareit/request/service/ItemRequestServiceImplTest.java +++ b/server/src/test/java/ru/practicum/shareit/request/service/ItemRequestServiceImplTest.java @@ -159,7 +159,7 @@ void getAllRequests() { // endregion setup doNothing().when(utilService).checkUser(userId); - when(queryDslRepository.getAllRequestsWitoutByUser(userId)).thenReturn(requests); + when(queryDslRepository.getAllRequestsWithoutThisUserRequests(userId)).thenReturn(requests); List resultDtos = requestService.getAllRequests(userId); diff --git a/server/src/test/java/ru/practicum/shareit/user/repository/UserRepositoryTest.java b/server/src/test/java/ru/practicum/shareit/user/repository/UserRepositoryTest.java index 9f404a5..d87bbb6 100644 --- a/server/src/test/java/ru/practicum/shareit/user/repository/UserRepositoryTest.java +++ b/server/src/test/java/ru/practicum/shareit/user/repository/UserRepositoryTest.java @@ -1,5 +1,6 @@ package ru.practicum.shareit.user.repository; +import jakarta.transaction.Transactional; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; @@ -8,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.*; @DataJpaTest +@Transactional class UserRepositoryTest { @Autowired From c556881fe5aff8e2c69005f57d5c2efa9f484fdd Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Fri, 17 Apr 2026 13:34:46 +0300 Subject: [PATCH 26/28] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B0=D1=82=D1=8C=20equals=20=D0=B8=20hashcode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Все сущности наследуются от базовой --- .run/ShareItGateway.run.xml | 1 - .run/ShareItServer.run.xml | 1 - lombok.config | 4 +- server/pom.xml | 24 +- .../shareit/booking/dto/BookingMapper.java | 2 +- .../shareit/booking/model/Booking.java | 38 +-- .../shareit/item/dto/CommentMapper.java | 2 +- .../shareit/item/dto/ItemMapper.java | 2 +- .../practicum/shareit/item/model/Comment.java | 36 +-- .../ru/practicum/shareit/item/model/Item.java | 36 +-- .../shareit/item/service/ItemServiceImpl.java | 10 +- .../request/dto/ItemRequestMapper.java | 2 +- .../shareit/request/model/ItemRequest.java | 33 +-- .../shareit/user/dto/UserMapper.java | 2 +- .../ru/practicum/shareit/user/model/User.java | 37 +-- .../shareit/util/entity/BaseEntity.java | 38 +++ .../hibernate/HibernateEqualsAndHashCode.java | 13 - .../java/ru/practicum/shareit/TestUtils.java | 41 +-- .../service/BookingServiceImplTest.java | 30 ++- .../shareit/item/ItemContextTest.java | 34 +++ .../service/ItemRequestServiceImplTest.java | 18 +- .../shareit/user/UserContextTest.java | 2 +- .../shareit/user/service/UserServiceTest.java | 4 +- .../shareit/util/UtilServiceTest.java | 4 - .../util/entity/BaseEntityJpaTest.java | 31 +++ .../shareit/util/entity/BaseEntityTest.java | 63 +++++ .../HibernateEqualsAndHashCodeTest.java | 233 ------------------ 27 files changed, 289 insertions(+), 452 deletions(-) create mode 100644 server/src/main/java/ru/practicum/shareit/util/entity/BaseEntity.java delete mode 100644 server/src/main/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCode.java create mode 100644 server/src/test/java/ru/practicum/shareit/util/entity/BaseEntityJpaTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/util/entity/BaseEntityTest.java delete mode 100644 server/src/test/java/ru/practicum/shareit/util/hibernate/HibernateEqualsAndHashCodeTest.java diff --git a/.run/ShareItGateway.run.xml b/.run/ShareItGateway.run.xml index 32c8129..8334725 100644 --- a/.run/ShareItGateway.run.xml +++ b/.run/ShareItGateway.run.xml @@ -1,7 +1,6 @@